{"_id":"@1jehuang/sdk","_rev":"2-439d13a0b7810e8acb9c346f181b6ad9","name":"@1jehuang/sdk","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@1jehuang/sdk","version":"1.0.0","keywords":["jcode","agent","sdk","llm","coding-agent","harness-api"],"author":{"name":"Jeremy Huang"},"license":"MIT","_id":"@1jehuang/sdk@1.0.0","maintainers":[{"name":"1jehuang","email":"jeremyhuang55555@gmail.com"}],"homepage":"https://github.com/1jehuang/jcode/tree/main/sdk/typescript#readme","bugs":{"url":"https://github.com/1jehuang/jcode/issues"},"dist":{"shasum":"3f9bc0326bbd518ec7b3179ca88c66bdbfbc9b19","tarball":"https://registry.npmjs.org/@1jehuang/sdk/-/sdk-1.0.0.tgz","fileCount":27,"integrity":"sha512-MOAGaFjMR0ufwRoHUqoI1yz7IP+2Ut5YycuczksUeKfDHlVNgKuQAuPD7ixnTKpUcuCx0rWeRKs4sBAY289TVg==","signatures":[{"sig":"MEQCICXX5auiqVS/bagh2NjHCMpagBmYLkpmVUlkkRiMvt0wAiAleMPrQJTVhrpJdP0FqBNjlBIbv+DdoBLcA3tAL9BhtA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":179297},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"b45bb9b52af29160b1f77838ed4fd070b9a2f978","scripts":{"test":"npm run build && node --test --experimental-strip-types test/*.test.ts","build":"tsc -p tsconfig.json","check":"npm run typecheck && npm run test","clean":"rm -rf dist","prepack":"npm run clean && npm run build","typecheck":"tsc -p tsconfig.json --noEmit"},"_npmUser":{"name":"1jehuang","email":"jeremyhuang55555@gmail.com"},"repository":{"url":"git+https://github.com/1jehuang/jcode.git","type":"git","directory":"sdk/typescript"},"_npmVersion":"12.0.1","description":"TypeScript SDK for the jcode harness API (protocol v1)","directories":{},"sideEffects":false,"_nodeVersion":"26.4.0","dependencies":{"ajv":"^8.20.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.7.2","@types/node":"^22.10.2"},"_npmOperationalInternal":{"tmp":"tmp/sdk_1.0.0_1785747094478_0.27378357837514455","host":"s3://npm-registry-packages-npm-production"},"deprecated":"This package has moved to @1jehuang/jcode-sdk. Install @1jehuang/jcode-sdk instead."}},"time":{"created":"2026-08-03T08:51:34.300Z","modified":"2026-08-03T10:34:16.153Z","1.0.0":"2026-08-03T08:51:34.635Z"},"bugs":{"url":"https://github.com/1jehuang/jcode/issues"},"author":{"name":"Jeremy Huang"},"license":"MIT","homepage":"https://github.com/1jehuang/jcode/tree/main/sdk/typescript#readme","keywords":["jcode","agent","sdk","llm","coding-agent","harness-api"],"repository":{"url":"git+https://github.com/1jehuang/jcode.git","type":"git","directory":"sdk/typescript"},"description":"TypeScript SDK for the jcode harness API (protocol v1)","maintainers":[{"name":"1jehuang","email":"jeremyhuang55555@gmail.com"}],"readme":"# @1jehuang/sdk\n\nTypeScript SDK for the **jcode harness API** (protocol v1) — the stable,\nversioned boundary between the jcode agent runtime and any client.\n\nIt mirrors `crates/jcode-harness-api` and talks NDJSON over the harness API\nUnix socket. Schema drift is guarded from both sides: a Rust test fails if a\nvariant is added without mirroring it here, and a Node test fails if the tag\nsets diverge.\n\nFull documentation: **[jcode.sh/sdk](https://jcode.sh/sdk)**\n\n## Install\n\n```bash\nnpm install @1jehuang/sdk\n```\n\nFrom a source checkout:\n\n```bash\ncd sdk/typescript\nnpm install\nnpm run build\n```\n\n## Requirements\n\njcode must be installed, and Node 20 or newer.\n\nmacOS and Linux are exercised end to end in CI. Windows builds and is wired up\n(the bridge listens on a named pipe rather than a Unix socket, and the SDK\nresolves the same pipe name), but it has no live end-to-end coverage yet, so\ntreat it as untested rather than unsupported and please report what breaks.\n\n`launch()` needs nothing else: it starts its own daemon and bridge. `connect()`\nneeds a bridge already running, which the user starts once and leaves running.\nThe bridge ships in the released binary, so no Rust toolchain is needed:\n\n```bash\njcode api-bridge\n```\n\nIt starts the jcode server if one is not already up, then exposes the API\nsocket (`$XDG_RUNTIME_DIR/jcode-api.sock`) and translates onto the internal\ndaemon socket. The socket is owner-only, matching the daemon socket it fronts.\n\nUse `--api-socket <path>` to listen elsewhere, and set `JCODE_API_SOCKET` to\nthe same path in your client. (The global `--socket` selects the *internal\ndaemon* socket, which is a different thing.)\n\n## Two ways to use jcode\n\n**Embed jcode as an agent engine** (`launch`). Starts a private instance with\nits own state, sessions, and sockets. It cannot see or disturb the jcode the\nuser runs in their terminal, and `close()` shuts it down. This is the default\nfor applications.\n\n```ts\nconst client = await JcodeClient.launch({ workingDir: process.cwd() });\nconst session = await client.createSession();\nconsole.log((await client.run(session.session_id, \"hello\")).text);\nawait client.close();  // stops the instance\n```\n\nProvider logins are inherited from the user by default, since an instance with\nno credentials cannot reach a model. Pass `inheritLogins: false` to start empty\nand supply your own. Pass `jcodeHome` to keep sessions across runs instead of\nusing a temporary directory.\n\nInheritance shares only recognized credential **files**, never whole config or\ntool directories. This keeps rotating OAuth tokens coherent without exposing\nunrelated transcripts and state, and instance cleanup cannot recurse into the\nuser's credential directories. Temporary homes are owner-only and cleanup is\nrestricted to SDK-created temp paths. The launched process still runs as the\ncurrent OS user and can spend those accounts' quota, so disable inheritance\nwhen running untrusted application code (`inheritLogins: false`).\n\n**Automate the user's own jcode** (`connect`). Attaches to the jcode already\nrunning on the machine, sharing its live sessions. This is what an editor\nplugin or a status dashboard wants. Anything it does is visible in the user's\nterminal, and it needs a bridge already running (`jcode api-bridge`).\n\n## Quick start\n\nSwap `launch` for `connect` to drive the user's own jcode instead of a private\ninstance; everything after that line is identical.\n\n```ts\nimport { JcodeClient } from \"@1jehuang/sdk\";\n\nconst client = await JcodeClient.launch({ workingDir: process.cwd() });\n\nconst session = await client.createSession(process.cwd());\nconst turn = await client.run(session.session_id, \"What files are in src/?\", {\n  autoApprove: true,\n  onEvent: (event) => {\n    if (event.ev === \"text_delta\") process.stdout.write(event.text);\n  },\n});\n\nconsole.log(\"\\ntools:\", turn.toolCalls.map((call) => call.name));\nconsole.log(\"tokens:\", turn.usage);\nclient.close();\n```\n\n## Structured output\n\n`runStructured()` asks the model for JSON, validates the response with Ajv, and\nsends bounded corrective retries when the response is not valid JSON or does not\nmatch your JSON Schema. It returns the normal turn metadata plus validated\n`data` and an `attempts` audit trail.\n\n```ts\nconst result = await client.runStructured<{ summary: string; count: number }>(\n  session.session_id,\n  \"Summarize the current changes\",\n  {\n    schema: {\n      type: \"object\",\n      additionalProperties: false,\n      required: [\"summary\", \"count\"],\n      properties: {\n        summary: { type: \"string\" },\n        count: { type: \"integer\", minimum: 0 },\n      },\n    },\n    maxRetries: 2, // default\n  },\n);\n\nconsole.log(result.data.summary);\n```\n\nIf all attempts fail validation, the promise rejects with\n`StructuredOutputError`. Its `validationErrors`, `lastText`, and `attempts`\nfields are stable for logging or user-facing diagnostics.\n\n## Streaming\n\n`run()` is the batch convenience path. For live UIs, iterate events directly:\n\n```ts\nconst session = await client.createSession();\nawait client.sendMessage(session.session_id, \"hello\");\n\nfor await (const event of client.events(session.session_id)) {\n  switch (event.ev) {\n    case \"text_delta\":\n      process.stdout.write(event.text);\n      break;\n    case \"tool_start\":\n      console.log(\"\\n[tool]\", event.name);\n      break;\n    case \"permission_request\":\n      await client.respondToPermission(session.session_id, event.request_id, \"allow\");\n      break;\n    case \"turn_done\":\n      return;\n  }\n}\n```\n\nPer-kind listeners work too: `client.on(\"token_usage\", handler)`.\n\nProtocol `error` frames arrive on the `harness_error` channel, not `error`.\nNode treats an unlistened `error` event as a fatal throw, so the plain channel\nis reserved for transport faults.\n\n### All-session events\n\n`globalEvents()` is the process-wide stream for dashboards and integrations. The\nbridge attaches one session per connection, so the SDK discovers every persisted\nsession, opens one child connection for each, and fans their streams into one\nbounded iterator. Discovery repeats to include sessions created later.\n\n```ts\nconst stop = new AbortController();\nfor await (const event of client.globalEvents({ signal: stop.signal })) {\n  if (\"session_id\" in event) console.log(event.session_id, event.ev);\n}\n```\n\nDelivery is **at-least-from-attach**, not historical replay. Protocol v1 cannot\nrecover events emitted before a child attaches or during an unexpected\ndisconnect and reattach. Per-session order is preserved, but there is no total\nordering across sessions. `return()`, aborting the signal, or closing the parent\ncloses all children. The iterator fails with `event_buffer_overflow` rather than\nsilently dropping events if its bounded queue fills. A custom `Transport` is\nrejected with `unsupported_transport` because it cannot be cloned safely into\nindependent child connections. Set `discoveryIntervalMs: 0` for one initial\ndiscovery pass only.\n\n## API surface\n\n| Method | Purpose |\n| --- | --- |\n| `JcodeClient.launch(options)` | Start a private instance and connect to it |\n| `JcodeClient.connect(options)` | Attach to the jcode already running on this machine |\n| `listSessions({ includeArchived? })` | Every persisted session, optionally including archived sessions |\n| `archiveSession(id)` / `restoreSession(id)` | Reversibly hide or restore a session |\n| `setRetentionPolicy(days?)` | Auto-archive inactive sessions, or disable retention |\n| `createSession(workingDir?)` | Create and attach |\n| `attachSession(id)` / `detachSession(id)` | Subscribe / unsubscribe |\n| `sendMessage(id, content, images?)` | Send a user message (awaits `message_accepted`) |\n| `run(id, content, options?)` | Send and collect one full turn |\n| `runStructured(id, content, options)` | Send, validate JSON Schema output, and retry corrections |\n| `events(sessionId?)` | Async iterator over stream events |\n| `globalEvents(options?)` | Bounded fan-in stream over all persisted and newly created sessions |\n| `cancel(id)` / `softInterrupt(id, content, urgent?)` | Interrupt a turn |\n| `getHistory(id)` / `peekSession(id, limit?)` | Read a transcript (peek works unattached) |\n| `clear(id)` / `rewind(id, index)` | Edit history |\n| `respondToPermission(id, requestId, decision)` | Answer a permission prompt |\n| `listModels(id)` / `setModel(id, model)` | List and choose the session's model |\n| `getRuntimeInfo(id)` | Provider, model route, protocol, capability, and health metadata |\n| `setApiKey(provider, key)` / `clearApiKey(provider)` | Atomically provision or remove owner-only API-key files |\n| `readFile(id, path, maxBytes?)` | Read bounded UTF-8 text under the session root |\n| `findFiles(id, query, limit?)` | Find rooted files by path substring |\n| `searchText(id, query, options?)` | Bounded rooted literal text search |\n| `fileStatus(id, path)` | Read safe rooted file metadata |\n| `setReasoningEffort(id, effort)` | Set the cost/quality dial |\n| `compact(id)` | Schedule transcript compaction to free context |\n| `renameSession(id, title?)` | Set a session title, or clear it |\n| `rewindUndo(id)` | Restore what the last `rewind` removed |\n| `cancelSoftInterrupts(id)` | Retract queued soft interrupts |\n| `ping()` | Liveness |\n\n## Models\n\nA client that cannot enumerate models cannot offer a picker, so the catalog is\nfirst-class. It is served from the push the daemon sends on attach, meaning\nopening a picker costs no round trip:\n\n```ts\nconst { models, current } = await client.listModels(session.session_id);\nawait client.setModel(session.session_id, \"claude-opus-5\");\n```\n\nAn unknown model, or one the provider refuses, rejects with `invalid_request`\nrather than silently leaving the session where it was. When the model changes,\nevery client attached to that session receives a `model_info` event, so a UI\nthat did not make the change still updates.\n\n`setReasoningEffort(id, effort)` sets how much the model deliberates. The\naccepted values are per-provider (typically `minimal` through `max`), so this\ntakes a string and reports what the provider says instead of guessing at a\nunion that would go stale.\n\n`getRuntimeInfo(id)` adds the active provider/model, every available model route,\nthe negotiated protocol version, advertised capability strings, and a live ping.\nAPI-key provisioning accepts the supported provider aliases, normalizes Gemini\naliases to `gemini`, supports the jcode subscription key, writes owner-only files\natomically, and asks the daemon to reload credentials. OAuth tokens are not part\nof this API.\n\nFile methods are rooted at the persisted session working directory. Absolute\npaths, `..`, and symlink escapes are rejected. Directory walks do not follow\nsymlinks and both file count and byte scanning are bounded. `readFile()` accepts\nUTF-8 text only and reports when its byte limit truncated the result.\n\n## Session archive and retention\n\nArchiving never deletes a transcript. It removes the session from the default\n`listSessions()` result and records an archive timestamp in owner-only state.\nPass `includeArchived: true` to display and restore archived sessions.\n`setRetentionPolicy(days)` applies the same reversible archive operation to\ninactive persisted sessions when sessions are listed. Omit `days` to disable\nautomatic retention.\n\n## Long sessions\n\n`compact(id)` summarizes the transcript so far, freeing context. It is\nasynchronous: the daemon summarizes at the next safe point rather than\ninterrupting a turn, so it resolving means the request was accepted, not that\nthe transcript has already shrunk. Read the history afterwards for the result.\n\nIt is refused below about 10% context usage, on the grounds that there is\nnothing worth compacting yet, and the rejection carries the current usage. So\ntreat `invalid_request` here as information for the user rather than an error\nto retry.\n\n```ts\nawait client.renameSession(id, \"nightly refactor\");  // omit the title to clear it\nawait client.rewind(id, 4);\nawait client.rewindUndo(id);                          // rewind is reversible\nawait client.cancelSoftInterrupts(id);                // retract what is queued\n```\n\n## Instance lifecycle\n\nA launched instance owns a daemon and a state directory, and both are cleaned\nup for you:\n\n- `close()` stops the daemon and removes an ephemeral home. It waits for the\n  process to actually be gone, so the directory cannot be recreated behind the\n  delete. Expect it to take a few seconds.\n- If your process exits without calling `close()`, including after an uncaught\n  exception, the instance is still reaped. Without this a server that restarts\n  would accumulate one daemon and one temp directory per restart.\n- `SIGKILL` is the one case nothing can cover, since no handler runs.\n\n### `launch()` options\n\n| Option | Effect |\n| --- | --- |\n| `workingDir` | Working directory for sessions. Defaults to `process.cwd()`. |\n| `jcodeHome` | Keep state at a fixed path across runs. Defaults to a temporary directory that is removed on `close()`. See the note below. |\n| `inheritLogins` | Inherit the user's provider logins. Defaults to `true`. |\n| `binary` | Path to the jcode binary. Defaults to `jcode` on `PATH`. |\n| `env` | Extra environment variables for the instance. |\n| `startupTimeoutMs` | How long to wait for the instance to come up. Defaults to 30000. |\n| `cleanupTimeoutMs` | How long `close()` spends removing an ephemeral home. Defaults to 30000. |\n| `inheritStderr` | Forward the instance's stderr to your process. Defaults to `false`. |\n\nA fixed `jcodeHome` persists transcripts on disk. `listSessions()` discovers\nthose records even on a fresh, unattached connection, so a restarted process can\nrebuild its complete session index without keeping a separate id registry.\n\n## Configuration\n\n| Env var | Effect |\n| --- | --- |\n| `JCODE_API_SOCKET` | Override the API socket path |\n| `JCODE_RUNTIME_DIR` | Override the runtime directory |\n| `XDG_RUNTIME_DIR` | Default runtime directory on Linux |\n\nOr pass `socketPath` to `connect()`.\n\n## Errors\n\nEvery failure is a `HarnessError` with a `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `jcode_not_found` | `launch()` could not run jcode: not installed, or not on `PATH`. Pass `binary` with a full path. |\n| `startup_failed` | The instance exited while starting. The message carries its stderr. |\n| `startup_timeout` | The instance never opened its socket within `startupTimeoutMs`. |\n| `connect_failed` | The bridge is not running, or the socket path is wrong. The message names the path and the command to start it. |\n| `disconnected` | The connection dropped mid-request. |\n| `timeout` | No reply within `requestTimeoutMs` (30s by default). |\n| `structured_schema_invalid` | `runStructured()` received an invalid JSON Schema. |\n| `structured_output_invalid` | The model did not produce valid structured output within the retry budget. |\n| `unsupported_transport` | `globalEvents()` was requested on a custom transport that cannot be cloned safely. |\n| `event_buffer_overflow` | A `globalEvents()` consumer fell behind its bounded queue. |\n| `unknown_session`, `invalid_request`, ... | Protocol errors relayed from the harness. |\n\n## Stability\n\nThis package is generally available and follows semver against the protocol\nit speaks.\n\n- **Protocol v1 is stable.** The handshake negotiates a major version, and a\n  server that cannot speak v1 is rejected with `unsupported_version` rather\n  than half-working. A breaking protocol change bumps to v2 and to a new SDK\n  major.\n- **Additive changes are minor releases.** New events, new request fields, and\n  new methods arrive in minors. Existing frames keep their shape.\n- **The API covers what real clients need.** A test diffs the API against every\n  request the terminal app makes and fails on an untriaged gap, so the surface\n  cannot quietly fall behind the app it mirrors.\n- **Drift is checked mechanically, in both directions.** A Rust test reads\n  `src/protocol.ts` and fails if a variant *or a field* is missing here; a Node\n  test reads the Rust enums and fails if the tag sets diverge. Neither side can\n  land a schema change alone.\n- **The tarball is tested as a tarball.** `scripts/test_sdk_package.sh` packs\n  it, installs it into a throwaway project, and imports it as ESM, as CJS, and\n  through `tsc`.\n\nCompatibility: Node 20+, ESM and CJS. Linux and macOS are covered end to end in\nCI; Windows builds and is wired up but is not yet exercised live.\n\n## Forward compatibility\n\nThe harness may add events at any time within protocol v1. `events()` and\n`run()` are typed as `ApiEvent`, the union of kinds this SDK knows, so\n`switch (event.ev)` narrows each case; always keep a `default` branch for kinds\nadded after your version. A frame of unknown kind is delivered as\n`UnknownApiEvent` (`{ ev: string; [key: string]: unknown }`); use\n`isKnownEvent(frame)` to narrow `AnyApiEvent` when you handle raw frames.\n\n`UnknownApiEvent` is deliberately not a member of `ApiEvent`: a member with\n`ev: string` widens the discriminant, and TypeScript then refuses to narrow any\ncase, typing every field as `unknown`.\n\n## Releasing\n\nSee [RELEASING.md](RELEASING.md). `bash scripts/sdk_publish_preflight.sh` runs\nevery gate and reports what is left.\n\n## Development\n\n```bash\nnpm run check   # typecheck + build + tests (mock harness, no daemon needed)\n```\n\n`test/schema-parity.test.ts` reads the Rust enums directly, and\n`crates/jcode-harness-api`'s `typescript_sdk_lists_every_variant` test reads\nthis package. Adding a variant on either side without the other fails CI.\n","readmeFilename":"README.md"}