@availsync/node
v0.1.9
Published
Node SDK for Availsync coding-agent work guardrails.
Maintainers
Readme
@availsync/node
Node SDK for Availsync coding-agent work guardrails.
Use it before Codex, Claude, Cursor, OpenClaw, cron jobs, deploy scripts, or other automations touch a protected repo or project.
Install
npm install @availsync/nodeThe package is public and stable enough for production pilots. Pin an exact version in critical automations when you need fully repeatable installs.
Hello World
import { createAvailsyncClient } from '@availsync/node';
const availsync = createAvailsyncClient({
apiKey: process.env.AVAILSYNC_API_KEY!,
agentId: process.env.AVAILSYNC_AGENT_ID!,
});
const result = await availsync.work.withClaim('repo:owner/repo', { durationMinutes: 30 }, async () => {
console.log('safe to run guarded work');
});
console.log(result.action);Environment
AVAILSYNC_API_KEY=avs_live_...
AVAILSYNC_AGENT_ID=agent_uuid
AVAILSYNC_API_URL=https://availsync.devGuard a coding-agent run
import { createAvailsyncClient } from '@availsync/node';
const availsync = createAvailsyncClient({
apiKey: process.env.AVAILSYNC_API_KEY!,
agentId: process.env.AVAILSYNC_AGENT_ID!,
baseUrl: process.env.AVAILSYNC_API_URL,
timeoutMs: 30_000,
});
const result = await availsync.work.withClaim(
'repo:owner/repo',
{
reason: 'scheduled Codex run',
durationMinutes: 45,
autoExtendIntervalMs: 40 * 60_000,
autoExtendDurationMinutes: 45,
claimHeartbeatIntervalMs: 60_000,
claimHeartbeatTimeoutSeconds: 300,
onHeartbeatError(error) {
console.error('Claim heartbeat failed; stop this run safely:', error.message);
},
onAutoExtendError(error) {
console.error('Lease renewal failed; stop this run safely:', error.message);
},
autoExtendWorkingSet: {
resource: 'repo:owner/repo',
mode: 'edit',
paths: ['frontend/components/Hero.tsx', 'frontend/app/page.tsx'],
leaseSeconds: 600,
},
},
async ({ claim, shadow }) => {
if (shadow.wouldHaveBlocked) {
console.warn('Observe-only: Availsync would have skipped this run.');
}
await claim?.updateWorkingSet({
resource: 'repo:owner/repo',
mode: 'edit',
paths: ['frontend/components/Hero.tsx', 'frontend/app/page.tsx'],
leaseSeconds: 600,
});
await doWork();
},
);
if (result.action === 'skip_run') {
console.log(result.reason);
} else if (result.finishError) {
console.warn('Work succeeded, but Availsync finish cleanup failed:', result.finishError.message);
} else if (result.heartbeatError) {
console.warn('Work succeeded, but at least one claim heartbeat failed:', result.heartbeatError.message);
} else if (result.autoExtendError) {
console.warn('Work succeeded, but at least one lease extension failed:', result.autoExtendError.message);
}withClaim generates a per-run idempotency key when you do not pass one, so a lost response and retry do not create duplicate claims for the same invocation.
withClaim also enables claim-level heartbeat by default. Heartbeats do not extend the lease; they let Availsync mark a disappeared SDK process as lost after the configured timeout. Set claimHeartbeat: false for legacy lease-only behavior.
API surface
createAvailsyncClient({ apiKey, agentId, baseUrl?, timeoutMs? })client.work.withClaim(resource, options, fn)client.work.start({ resource, durationMinutes?, heartbeatTimeoutSeconds?, reason?, idempotencyKey?, metadata? })client.work.check({ resource, durationMinutes?, reason?, metadata? })client.work.get(claimId)client.work.list({ resource?, status?, limit?, offset? })client.work.extend(claimId, { durationMinutes?, workingSet? })client.work.heartbeat(claimId)client.work.updateWorkingSet(claimId, { paths, mode?, resource?, leaseSeconds? })claim.heartbeat()claim.updateWorkingSet({ paths, mode?, resource?, leaseSeconds? })client.work.finish(claimId, { outcome?, reason?, metadata? })
Manual flow
const started = await availsync.work.start({
resource: 'repo:owner/repo',
reason: 'deploy automation',
durationMinutes: 45,
idempotencyKey: `deploy-${process.env.GITHUB_RUN_ID}`,
});
if (started.action === 'skip_run') {
console.log(started.reason);
process.exit(0);
}
if (started.shadow_mode && started.would_have_blocked) {
console.warn('Observe-only: Availsync would have skipped this run.');
}
const extendTimer = started.claim
? setInterval(() => availsync.work.extend(started.claim!.id, { durationMinutes: 45 }).catch(console.error), 40 * 60_000)
: null;
let outcome: 'finished' | 'error' = 'finished';
try {
await doWork();
} catch (error) {
outcome = 'error';
throw error;
} finally {
if (extendTimer) clearInterval(extendTimer);
if (started.claim) {
await availsync.work.finish(started.claim.id, { outcome });
}
}Observe-only pilots
Set an agent to Observe in the dashboard when you want Availsync to report what it would have blocked without stopping the automation. Observe mode returns action: "proceed" and claim: null; no lease is created, so extend and finish are no-ops you should skip.
File-aware coordination
Use working set claims when repo-level locking is too broad. A run can keep its repo/project claim while declaring the files it is actively editing or reviewing.
await claim.updateWorkingSet({
resource: 'repo:owner/repo',
mode: 'edit',
paths: ['frontend/components/Hero.tsx', 'frontend/app/page.tsx'],
leaseSeconds: 600,
});read and review working sets are non-exclusive. edit and delete conflict with other active edit or delete claims for the same resource and path. Availsync stores paths and metadata only, not file contents.
For long-running work, pass the current working set when you extend. That refreshes the run lease and checks whether the files are still safe:
const extended = await claim.extend({
durationMinutes: 45,
workingSet: {
resource: 'repo:owner/repo',
mode: 'edit',
paths: currentFiles,
leaseSeconds: 600,
},
});
if (extended.working_set?.action === 'blocked') {
throw new Error('Working set is blocked by another agent');
}Error handling
The SDK throws AvailsyncError for auth, validation, plan-limit, network, and unexpected API errors. The error includes code, status, and safe details from the API response. It never logs or exposes API keys.
If your callback succeeds but final finish cleanup fails, withClaim still returns your successful result and includes finishError. If an automatic lease extension failed while your work continued, the result includes autoExtendError and calls onAutoExtendError if you provided one. This avoids reporting successful work as failed while still making cleanup issues visible.
By default, if Availsync is unavailable before a run starts, withClaim fails closed and throws. For observe-only pilots where availability matters more than coordination coverage, you can choose fail-open explicitly:
await availsync.work.withClaim(
'repo:owner/repo',
{ reason: 'observe pilot', onUnavailable: 'proceed' },
async ({ unavailable }) => {
if (unavailable) console.warn('Running without Availsync coordination:', unavailable.code);
await doWork();
},
);Use fail-open carefully in enforce-mode automations. The SDK only treats network errors, timeouts, aborts, and gateway/service-unavailable responses (502, 503, 504) as unavailable. Generic 500 responses still throw because they may indicate a request or server bug rather than an outage. If the server creates a claim but the response is lost before the SDK receives the claim id, the SDK cannot finish that claim and it will remain active until its lease expires. Fail-closed is the recommended default for repo edits, deploys, migrations, and other production-like work.
Resource scope
repo:owner/repois the safe default. Only one active agent can work in the repo.project:homepageallows parallel work, but only use it for independent work streams.- Availsync does not yet infer dependencies between different resources.
Local development
npm run build:sdk
npm run test:sdk