@jaato/sdk
v0.17.0
Published
Jaato SDK for TypeScript / JavaScript — protocol and client library for jaato-server. Mirrors the Python jaato-sdk method-for-method.
Maintainers
Readme
@jaato/sdk
TypeScript / JavaScript SDK for jaato-server.
Mirrors the Python jaato-sdk method-for-method, with
identical noun naming (camelCase per JS convention) so cross-language
parity is enforced by construction.
Status: pre-release (Phase 3 code + full Python parity shipped,
not yet on npm). The JaatoClient class is implemented and
tested (40 unit tests pass against a mock WebSocket). Method
surface is feature-equivalent to the Python IPCClient —
every wire verb the TUI / dashboard / external SDK consumers
need is exposed as a typed method (see API reference
below), including the multi-frame stageFiles and opt-in
autoReattachSessionId recovery. The codegen-generated event /
request types stay in lockstep with the Python SDK via the CI
staleness gate.
What's still pending: the npm publish workflow and the first
npm publish @jaato/[email protected]. Consume locally per the
Consuming this SDK
section below until the first publish lands.
Plan history: project_backlog_sdk_feature_parity.md.
Background: why this exists
The motivating audit was a comparison against
@mariozechner/pi-agent-core,
a TypeScript library that exposes a stateful agent with prompt,
steer, followUp, continue, abort, and subscribe methods —
plus beforeToolCall / afterToolCall hooks and BYO browser-resident
tools.
Walking through pi-agent's surface against jaato-server uncovered three realisations:
- Most of pi-agent's capabilities already exist on the jaato side — under different names. Client-side tools, mid-turn injection, per-session caching, streaming events, abort, parallel tool execution, fork-and-replay primitives — all shipped.
- The gap was at the SDK layer, not the daemon. Many capabilities
were reachable only as model-callable tools (premium
session_ops) or via stringly-typedCommandRequest("permissions", [...])calls. Programmatic clients (TS web components, third-party SDKs) couldn't drive them ergonomically. - A TS SDK without parity to the Python SDK would just shift drift
one level up. Both languages have to expose the same surface, with
jaato-native naming (no
prompt/steer/continueborrowed from pi-agent), or every protocol change splits into two divergent implementations.
The result was the SDK feature parity workstream —
project_backlog_sdk_feature_parity.md — with five phases:
- Phase 0 — migrate
events.pyfrom@dataclasstopydantic.BaseModelso the codegen pipeline has a real schema source. ✅ shipped (jaato-server 0.5.26 / jaato-sdk 0.3.0). - Phase 1 — typed WS verbs over
JaatoSession.inject_prompt,replay_messages,resolve_fork_point; typed permission-policy mutators; per-callparallel_toolsoverride onSendMessageRequest. Matching async methods on PythonIPCClient/IPCRecoveryClient. ✅ shipped (jaato-server 0.5.27 / jaato-sdk 0.3.1). - Phase 2 — codegen pipeline (this package). ✅ shipped.
- Phase 3 —
JaatoClientclass wrapping the WS protocol method-for-method with the Python SDK. ✅ code + tests shipped; first npm publish still pending (see Publishing below). - Phase 3.1 — closed the remaining 6 gaps premium flagged
before starting the jaato-task migration:
attachSession,createSession,getDefaultSession,listSessions,listProfiles,respondToToolExecution. The last is also new on the Python side (jaato-sdk 0.3.3). ✅ shipped. - Phase 3.2 — landed the two items I'd initially deferred
to v0.2:
stageFiles(multi-frame TEXT + N binary frame protocol;transport.sendBinaryexposed for any other future multi-frame verbs) and opt-inrecovery.autoReattachSessionId(consumer no longer needs to wire the re-attach status handler manually). ✅ shipped.
What pi-agent calls agent.prompt() is JaatoClient.send_message()
here. agent.steer(msg) is inject_prompt(text, source_type="user").
agent.followUp(msg) is inject_prompt(text, source_type="child").
agent.continue() is replay_messages() with no message argument
(replays current history). The naming is jaato's; the capability is
pi-agent-equivalent.
What jaato has that pi-agent doesn't:
- Daemon model: the same session can be driven from multiple clients concurrently (TUI + dashboard + reactor); the agent state outlives any single client connection.
- Plugin system: tools, GC strategies, model providers, telemetry, permission policies are all pluggable.
- Fork / interrogate primitives (
replay_messages+resolve_fork_point): an external client can fork a session at any point in history, ask a question on the fork, and never disturb the source. Premium'ssession_opsplugin builds these into model-callable tools (interrogate_session,setup_replay_workspace, etc.); the SDK exposes the underlying primitives so JS / TS clients can compose their own flows. - Subagents: a session can spawn child sessions that share the parent's runtime but maintain isolated state, with a priority-based message queue between them.
What pi-agent has that jaato doesn't (today):
- In-process embedding — pi-agent runs as a TS library inside the caller's process. Jaato runs as a daemon; clients connect over IPC or WebSocket. Different deployment model; not on the parity backlog.
Wire-protocol types
The full event/request type surface is generated from the Python side's pydantic models:
jaato-sdk/jaato_sdk/events.py (source of truth, pydantic)
│
▼
scripts/codegen_ts_events.py (uses pydantic.TypeAdapter to emit JSON Schema,
then pipes through json-schema-to-typescript)
│
▼
jaato-sdk-ts/src/events.ts (generated, committed)CI fails any PR that touches events.py without re-running codegen and
committing the regenerated events.ts.
Per-turn usage shape
The three usage-bearing events (TurnCompletedEvent, TurnProgressEvent, ContextUpdatedEvent) all expose a typed usage: UsageBreakdown field carrying token counts (prompt/output/total), cache hits, reasoning/thinking tokens, and cost_usd when the daemon can derive it. Cost resolution: provider-reported wins over pricing-table computed from .jaato/pricing.json; otherwise null. See docs/sdk-pricing.md for the full pricing contract. GC configuration moved to its own GCConfigEvent in v1.0.
Regenerating
From the repo root:
.venv/bin/python scripts/codegen_ts_events.pyOr from jaato-sdk-ts/:
npm run codegenVerifying
The CI staleness gate uses:
.venv/bin/python scripts/codegen_ts_events.py --checkwhich exits non-zero (with a unified diff) if the committed
events.ts is stale relative to a fresh regeneration.
Importing
Wire-protocol types only:
import {
EventType,
JaatoEvent,
SendMessageRequest,
AgentOutputEvent,
// ...
} from "@jaato/sdk";
function handle(event: JaatoEvent): void {
switch (event.type) {
case EventType.AGENT_OUTPUT:
// event narrowed to AgentOutputEvent
console.log(event.text);
break;
case EventType.PERMISSION_REQUESTED:
// ...
break;
}
}Full client (connect, send, subscribe):
import { JaatoClient, EventType } from "@jaato/sdk";
const client = new JaatoClient({
url: "ws://localhost:8080",
token: "<bearer-token>", // omit when behind a proxy that injects it;
// or a function returning a fresh per-user
// ticket per attempt (protocol 1.10, #1074)
recovery: {
autoReconnect: true,
autoReattachSessionId: true, // re-attach session automatically after a reconnect
},
clientConfig: {
// Optional fields, all forwarded to the server as a
// ClientConfigRequest in the post-connect handshake. Type is
// Omit<ClientConfigRequest, "type" | "timestamp">.
config_root: "/path/to/project/.jaato", // see "config_root" below
apparmor: false, // see "apparmor" below
},
});
// Typed subscriptions — handler receives the narrowed event type.
const unsub = client.subscribe("agent.output", (event) => {
document.getElementById("chat")!.append(event.text);
});
// Or use the generated string-literal enum if you prefer constants:
// client.subscribe(EventTypeValue.AGENT_OUTPUT, handler);
await client.connect();
// By profile name — references .jaato/profiles/<name>.json on the server
await client.createSession({ profile: "researcher" });
// By inline spec — same shape as a profile JSON, no disk file needed
await client.createSession({
profile: {
model: "claude-sonnet-4-5",
provider: "anthropic",
plugins: ["cli", "web_search"],
system_instructions: "You are an operations engineer.",
// plugin_configs, gc, env, runtime_limits, model_tiers, ...
},
});
await client.sendMessage("Summarise the latest commits.");The profile option is polymorphic:
string→ references a profile JSON on the server's disk under.jaato/profiles/. Use this when an operator has curated profiles for human users.object→ inline spec with the same shape. Use this when you're an orchestrator with your own governance layer and don't want to depend on disk files.
clientConfig.config_root
Decouples where the agent runs (workspace_path, set transport-side per WS workspace) from where the daemon reads its read-only framework config — profiles, agent .md files, prompts, references, completion_schemas, instructions, scripts, services. The daemon scans <config_root> instead of <workspace_path>/.jaato/. The user-tier ~/.jaato/ is always honored.
Pair with a workspace_path that does not contain a .jaato/ symlink to give the agent's filesystem tools no visibility into the framework config.
clientConfig.apparmor
Default false. Set to true to ask the daemon to confine each session created on this connection with a per-session AppArmor profile. Useful for orchestrator-style harnesses where the LLM-driven tool plugins (cli, file_edit, interactive_shell) are the threat surface and a hallucinated path should be blocked at the kernel level rather than only inside the workspace dir.
The profile grants:
workspace_path— read/writeconfig_root— read-only (when set)~/.jaato/{agents,profiles,prompts,references,...}— read-only~/.jaato/memories— read/write- venv + jaato source tree — read-only
When AppArmor is unavailable on the host (non-Linux, kernel module not loaded, apparmor_parser missing) the session falls back to running unconfined — but does not fail silently. The daemon emits a SystemMessageEvent to the client with prefix [apparmor] ... (style info when confinement is in effect, style warning when it isn't). Surface those in your event handlers so the user can see at a glance whether kernel confinement is really active.
Note: WS workspaces provisioned by the daemon (when the WS server has a workspace_root configured) are confined automatically by the WS path regardless of this flag — set apparmor: true only when running over plain IPC or against a WS server that doesn't manage its own workspace tree. See docs/apparmor-setup.md on the server repo for prerequisites.
The two forms are mutually exclusive — pass one or the other. The server validates inline specs and rejects them with an ErrorEvent if model is missing (no silent default fallback). agent and agentParams are independent of profile and compose with either form: profile decides capabilities (model, plugins, GC), agent decides persona (system instructions / personality).
Event subscription API
| Method | Receives | Use when |
|---|---|---|
| subscribe(type, handler) | only events of type, narrowed | the common case — you care about specific events |
| subscribeOnce(type, handler) | first matching event, then auto-unsub | you want to react to a one-shot lifecycle event |
| subscribeAll(handler) | every event, untyped | logging, telemetry, or a generic firehose |
| subscribeMany({ ... }) | typed handlers grouped under one unsub | "configure my whole client" call sites |
import { EventTypeValue } from "@jaato/sdk";
// One handler per type — `event` is automatically narrowed.
client.subscribe("permission.requested", (event) => {
console.log(event.request_id); // typed as PermissionRequestedEvent
});
// Fire once, then auto-unsubscribe.
client.subscribeOnce("agent.completed", onDone);
// Catchall (every event).
const unsub = client.subscribeAll((event) => log(event));
// Many at once — single unsub removes them all atomically.
const unsubAll = client.subscribeMany({
"permission.requested": onPerm,
"tool.call_start": onToolStart,
"agent.completed": onDone,
});Every subscribe* returns an idempotent unsubscribe. Handlers may be sync or async; async handlers are dispatched fire-and-forget — delivery is FIFO across events, but completion of async handlers is not ordered. Exceptions and rejections are logged and swallowed; one bad handler never breaks the stream or affects others. Subscribing during dispatch only takes effect for the next event (the handler list is snapshotted before iterating).
Migration from
@jaato/sdk< 0.2: the catchallsubscribe(handler)was renamed tosubscribeAll(handler), and theevents()async iterator was removed. Usesubscribe(type, handler)for typed handlers andsubscribeAllwhen you need every event.
If you want to react to connection-state transitions yourself
(e.g. show a "Reconnecting…" banner) instead of relying on the
opt-in re-attach, drop autoReattachSessionId and wire the
handler explicitly:
import { ConnectionState } from "@jaato/sdk";
client.onStatus((status) => {
if (status.state === ConnectionState.RECONNECTING) {
showBanner(`Reconnecting (attempt ${status.reconnectAttempt})…`);
}
if (status.state === ConnectionState.CONNECTED && client.sessionId) {
void client.attachSession(client.sessionId);
hideBanner();
}
});Daemon-extension verbs (premium reconnect / asset-picker /
custom WS handlers) — use sendRawEvent for envelopes that
aren't in the public JaatoEvent union:
// Premium's session_reconnect.extension verbs.
await client.sendRawEvent({
type: "reconnect.list",
filter: { only_attached: true },
});
// Response arrives via subscribeAll() — caller filters by type since
// the verb's response shape isn't in the public JaatoEvent union.
client.subscribeAll((event) => {
if (event.type === "reconnect.list_response") {
console.log(event); // Caller knows the shape; SDK doesn't.
}
});executeCommand is the right escape hatch when the verb is
dispatched via command.execute (e.g. executeCommand("session.list")).
sendRawEvent is the right escape hatch when the verb registers
its OWN top-level message type. Both bypass type-checking; both
stay supported.
File staging (multi-frame protocol — TEXT request + N binary frames + typed response):
const fileBlob = await fetch("/some-asset.png").then((r) => r.arrayBuffer());
const result = await client.stageFiles("workspace_abc", [
{ name: "logo.png", data: fileBlob, contentType: "image/png" },
{ name: "config.json", data: new TextEncoder().encode(JSON.stringify(cfg)) },
]);
if (result.failed.length > 0) {
console.error("Some files failed:", result.failed);
}
console.log("Staged:", result.staged.map((f) => f.name));The call resolves with the typed StageFilesEvent once the
server reports back; per-file failures are surfaced in
result.failed so partial successes are recoverable.
Concurrent stageFiles calls on the same client must be
serialised (the response correlation is by ordering, not by ID).
API reference
Method-for-method mirror of the Python IPCClient /
IPCRecoveryClient. All methods are async and return
Promise<void> unless otherwise noted; results arrive on the
event stream and are correlated by request_id where applicable.
Lifecycle
| Method | WS verb | Purpose |
|---|---|---|
| connect() | (handshake) | Open WS, await ConnectedEvent, enforce MIN_PROTOCOL_VERSION (semver compat against protocol_version; package server_version is diagnostics only) |
| close() | — | Close WS and cancel any pending reconnect |
| subscribe(type, handler) | — | Subscribe to a specific event type — handler receives the narrowed interface |
| subscribeOnce(type, handler) | — | Same as subscribe, but fires once then auto-unsubscribes |
| subscribeAll(handler) | — | Catchall — receive every incoming JaatoEvent |
| subscribeMany(map) | — | Register many typed handlers atomically; returned unsub removes them all |
| onStatus(handler) | — | Connection-state transitions |
Conversation
| Method | WS verb |
|---|---|
| sendMessage(text, attachments?, parallelTools?) | message.send |
| injectPrompt(text, sourceType?, sourceId?, attachments?) | inject_prompt.request (steer / follow-up). attachments needs protocol 1.5+ and makes the delivery idle-only — a busy target answers "busy" with nothing enqueued, because the queued path folds a message into the running turn as text and cannot carry bytes. |
| wakeSession(sessionId, text?, {attachments?, source?, eventId?}) | session.wake (command) — revive a cold session and drive a turn. text may be empty when attachments are the message (a spoken utterance). |
| replayMessages(requestId, messages?, timeoutSeconds?) | replay_messages.request (continue from current) |
| resolveForkPoint(requestId, opts) | resolve_fork_point.request |
| stop(agentId?) | stop |
| requestHistory(agentId?) | history.request |
Session management
| Method | WS verb |
|---|---|
| createSession({ name?, profile?, agent?, agentParams? }) | command.execute session.new — profile is string (profile name on disk) or object (inline spec); inline spec rides on CommandRequest.payload, no disk file needed |
| attachSession(sessionId) | command.execute session.attach |
| getDefaultSession() | command.execute session.default |
| listSessions() | command.execute session.list |
| listProfiles() | command.execute session.profiles — response is a SessionProfilesEvent with schema_version: "1.0", a typed profiles: ProfileSummary[] array, and a separate parse_errors: ProfileParseError[] for files that failed discovery. ProfileSummary is the safe-to-display subset (name, description, plugins, model, provider, plugin_configs, gc, model_tiers, runtime_limits, completion_payload_schema, env_var_names — values never exposed). |
| endSession() | command.execute session.end (terminate current attached session) |
| reloadSessionEnv(sessionId?) | command.execute session.reload_env — re-read a live session's workspace .env and stored credentials and rebuild its provider, so a key stored with <provider>-auth key after the session opened is used on the next turn; refused below protocol 1.11 |
| toggleWorkspaceIgnore(path) | command.execute workspace.ignore — add one entry to the session workspace's .gitignore or remove it again (the TUI Files panel's i key, served daemon-side); the daemon answers with workspace.ignore.result; refused below protocol 1.12 |
| deleteSession(sessionId) | command.execute session.delete (purge from disk + memory) |
Tools (model-callable + client-registered)
| Method | WS verb |
|---|---|
| registerClientTools(tools, categories?) | tools.register_client |
| respondToToolExecution(callId, result?, error?) | tool.execute_result (return result for client-registered tool) |
| disableTool(toolName) | tool.disable.request |
| requestCommandList() | command_list.request |
| executeCommand(command, args?, payload?) | command.execute (escape hatch for any command-router verb without a typed method). payload is the structured body verbs like cascade.budget.set and session.wake take. |
| sendRawEvent(envelope) | arbitrary type (escape hatch for daemon-extension verbs that register their OWN top-level message type — premium's reconnect.list / reconnect.delete / auth.token / assets.list, etc.) |
File staging
| Method | WS verb |
|---|---|
| stageFiles(workspaceId, files) | workspace.files.stage_request (TEXT) + N binary frames; resolves with StageFilesEvent |
Permissions
| Method | WS verb |
|---|---|
| addWhitelistTools(tools?, patterns?) | permission.add_whitelist |
| addBlacklistTools(tools?, patterns?) | permission.add_blacklist |
| removePermissionRules(target, tools?, patterns?) | permission.remove |
| clearPermissionRules(target?) | permission.clear |
| setDefaultPolicy(policy) | permission.set_default |
| requestPolicySnapshot(requestId?) | permission.policy_snapshot.request |
| respondToPermission(requestId, response, editedArgs?) | permission.response |
response is one of the keys offered by the server in PermissionRequestedEvent.response_options. The defaults:
| Key | Meaning |
|-----|---------|
| y | allow this tool execution |
| n | deny this tool execution |
| a / always | allow and whitelist the tool for this session |
| t / turn | allow remaining tool calls this turn |
| i / idle | allow until the session goes idle |
| once | allow once without remembering |
| all | allow all future requests in this session |
| never | deny and blacklist the tool for this session |
| c:<text> | deny with feedback the model sees as the tool result |
| yc:<text> | allow with feedback the model sees alongside the tool result |
| e | edit arguments and re-prompt (pass editedArgs); only when the request has editable content |
The two comment variants let you steer the model without simply rejecting the call:
await client.respondToPermission(requestId, "c:please check the file size first");
await client.respondToPermission(requestId, "yc:ok but write the result to /tmp/audit.log");The server strips the c: / yc: prefix and forwards the comment to the model alongside the deny/allow decision. Empty text after the prefix falls back to plain n / y.
Prompts (mid-flow)
| Method | WS verb |
|---|---|
| respondToClarification(requestId, response, questionIndex?) | clarification.response |
| respondToReferenceSelection(requestId, response) | reference_selection.response |
| respondToPostAuthSetup(requestId, {connect, modelName?, persistEnv?}) | auth.setup_response — answers the daemon's auth.setup offer that follows a successful daemon-level auth command (<provider>-auth login) with no session open: pick a model, optionally persist JAATO_PROVIDER / MODEL_NAME to the workspace .env, or decline |
Consuming this SDK before it's published to npm
Premium webcomponents (and any other early consumer) can wire the
SDK in locally without waiting for the first npm publish. Pick
the option that matches your setup.
Option A — npm link (developer-mode symlink)
Best when you're actively iterating on both the SDK and the
consumer. A symlink in the consumer's node_modules points at
your local jaato-sdk-ts/dist/, so a rebuild here is picked up
immediately on the consumer side.
# In jaato repo:
cd jaato-sdk-ts
npm install
npm run build
npm link
# In premium repo (any consuming package.json):
npm link @jaato/sdkRe-run npm run build in jaato-sdk-ts/ whenever you edit a
source file. The consumer doesn't need to reinstall.
Option B — file: dependency
Better for CI or repeatable test environments. The consumer's
package.json declares a relative path; npm install copies the
built package into node_modules.
{
"dependencies": {
"@jaato/sdk": "file:../jaato/jaato-sdk-ts"
}
}Run npm install again in the consumer to pick up SDK changes.
The SDK must be built (npm run build) before the consumer
installs.
Option C — npm pack (most production-like)
Closest to what npm publish would deliver — produces a tarball
containing exactly what would be uploaded to the registry. Use
this right before publishing to catch missing files in the
package's files field, broken exports, etc.
cd jaato-sdk-ts
npm run build
npm pack
# produces jaato-sdk-0.1.0.tgz
cd ../../jaato-premium/<consumer-package>
npm install /path/to/jaato-sdk-0.1.0.tgzOption D — direct ESM import (vanilla JS, no build step)
If the consumer is a vanilla-JS webcomponent served as a static
file (no package.json, no bundler), it can import the built ESM
output directly:
<script type="module">
import { JaatoClient } from "/path/to/jaato-sdk-ts/dist/index.js";
const client = new JaatoClient({
url: "ws://localhost:8080",
token: "<bearer-token>",
});
await client.connect();
client.subscribe((event) => console.log(event));
await client.sendMessage("hello");
</script>Or copy jaato-sdk-ts/dist/ into the consumer's static asset
path. Cost: no autocomplete or type-checking on the consumer
side — for that, introduce a build step (Vite, esbuild) and use
Option A / B / C instead.
Recommended workflow for premium webcomponent migration
The SDK surface is feature-complete as of Phase 3.2 (commit
1181fb7e) — every wire verb the dashboard's webcomponents use
today (including stageFiles and the auto re-attach pattern)
is exposed as a typed method. No "premium keeps it inline"
caveats remain.
- Use Option A during active development — fastest iteration.
- Switch to Option C right before any publish to verify the tarball contents are correct.
- If a webcomponent is currently vanilla JS with no build pipeline, this is a good moment to introduce one (Vite or esbuild + single-file bundle output). That unlocks tree-shaking, type-checking, and pulls the SDK into the same module graph instead of relying on a script tag.
- Pin
jaato-server >= 0.5.28in production so the migrated webcomponent gets theAgentCompletedEvent.token_usageregression fix (the SDK floor itself is0.5.27, but the server pin is operationally stricter).
Publishing
@jaato/sdk is published to npmjs.com under the @jaato scope.
Cutting a release
- Bump
versioninpackage.json(semver per policy below). - Commit + push to
main. - Trigger the publish workflow:
gh workflow run publish-npm-sdk-ts.yml --ref main gh run watch --exit-status # optional, follow the run - Approve the deployment when the
publishjob pauses on thenpm-sdkenvironment's required-reviewer rule. - The run stages the version; it is not on the registry until a
maintainer with 2FA promotes it:
(or the same buttons on npmjs.com).npm stage list @jaato/sdk # find the stage id npm stage view <stage-id> # optional: inspect what was uploaded npm stage approve <stage-id> # or: npm stage reject <stage-id>npm stageneeds npm 11.15+. The second factor is whatever the account uses: a passkey is answered in the browser the CLI opens; an authenticator code can be passed with--otp <code>. Nothing has to be typed on the command line for a passkey account.
The workflow (.github/workflows/publish-npm-sdk-ts.yml) does the
gates in order:
- Codegen staleness check —
events.tsmust be up to date withevents.py. Mirror of the on-PRcodegen-ts-events.ymlcheck. npm install+npm run build+npm test— same suite that runs locally.- Version-already-on-registry check —
https://registry.npmjs.org/@jaato/sdk/<version>returns 404 (free), 200 (already published — fail), anything else (transient — fail loudly rather than guess). npm whoamiwith the token — an expired or revoked token fails here by name, before the build, instead of surfacing at the last step as npm's bare404 Not Found - PUT, which is what the registry answers an unauthorised write with.npm stage publish --access public— scoped packages default to restricted on npm; the flag makes the package installable without auth. Staged, not published: see step 5 above.
Authentication uses an NPM_TOKEN secret in the GitHub npm-sdk
environment: a granular access token with Read and write (stage
only) on @jaato/*. npm is retiring direct publish by token in
January 2027, and a stage-only token answers npm publish with
E_STAGE_REQUIRED, so the workflow stages and a human promotes.
Migrating to npm Trusted Publishing (OIDC, no token) is a one-step
change once the package is on the registry — see the workflow file
header for the migration note.
Versioning policy
Versioning policy for the npm package mirrors the Python SDK: minor bumps for additive surface (new methods, new event types), patch bumps for fixes, major bumps for SDK-shape breaks.
Wire-protocol versioning is separate. The exported
MIN_PROTOCOL_VERSION (currently "1.0") is the minimum
ConnectedEvent.protocol_version this SDK will accept from the
daemon. On connect() the client checks that the daemon's
protocol_version matches its major and is at least the client's
required minor; mismatch raises IncompatibleServerError. The
daemon's package version (server_version) is surfaced as
client.serverVersion for diagnostics but is not the compat
signal.
A newer field a client only sometimes uses does not belong in that
floor: MIN_ATTACHMENT_RESUME_PROTOCOL ("1.5") is exported and
checked per call by injectPrompt / wakeSession, so a client that
never sends binary content still talks to any 1.x daemon, while one that
does is refused rather than having its payload silently dropped. See docs/sdk-protocol-versioning.md
for the bump policy and CHANGELOG.
License
BUSL-1.1 (matches jaato-server / jaato-sdk).
