@north-light/crouter 0.3.321 → 0.3.322

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/bin/runtime-selector.mjs +52 -3
  2. package/dist/api/client.d.ts +29 -16
  3. package/dist/api/client.js +2 -2
  4. package/dist/api/dto/modelauth.d.ts +15 -0
  5. package/dist/api/errors.d.ts +6 -3
  6. package/dist/api/errors.js +1 -1
  7. package/dist/api/index.d.ts +2 -2
  8. package/dist/api/index.js +1 -1
  9. package/dist/api/node-transport.d.ts +18 -0
  10. package/dist/api/node-transport.js +1 -0
  11. package/dist/api/routes.d.ts +1 -0
  12. package/dist/api/routes.js +1 -1
  13. package/dist/builtin-memory/crouter-sdk.md +86 -0
  14. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +1 -1
  15. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +1 -1
  16. package/dist/cli.js +8 -2
  17. package/dist/clients/attach/render/chat-view.d.ts +5 -1
  18. package/dist/clients/attach/render/chat-view.js +1 -1
  19. package/dist/clients/attach/session/frames.d.ts +1 -0
  20. package/dist/clients/attach/session/frames.js +1 -1
  21. package/dist/clients/attach/viewer.js +583 -583
  22. package/dist/commands/api-client.js +3 -3
  23. package/dist/commands/memory/read.js +2 -2
  24. package/dist/commands/sys/branch.js +1 -1
  25. package/dist/commands/sys/connect.d.ts +1 -0
  26. package/dist/commands/sys/connect.js +3 -0
  27. package/dist/commands/sys/daemon.js +1 -1
  28. package/dist/commands/sys/panels/provider-panel.js +1 -1
  29. package/dist/commands/sys/provider-login.d.ts +8 -0
  30. package/dist/commands/sys/provider-login.js +9 -0
  31. package/dist/commands/sys/setup-core.d.ts +4 -0
  32. package/dist/commands/sys/setup-core.js +4 -4
  33. package/dist/commands/sys.js +1 -1
  34. package/dist/core/canvas/paths.d.ts +4 -4
  35. package/dist/core/config.js +1 -1
  36. package/dist/core/runtime/broker/daemon-ops.js +1 -1
  37. package/dist/core/secrets.d.ts +3 -0
  38. package/dist/core/secrets.js +2 -2
  39. package/dist/core/subscription-state.d.ts +1 -1
  40. package/dist/core/subscription-state.js +3 -3
  41. package/dist/core/termrender/termrender.d.ts +3 -1
  42. package/dist/core/termrender/termrender.js +13 -13
  43. package/dist/core/user-settings.d.ts +4 -0
  44. package/dist/core/user-settings.js +1 -1
  45. package/dist/daemon/api/handlers/health.d.ts +1 -1
  46. package/dist/daemon/api/handlers/health.js +3 -3
  47. package/dist/daemon/api/handlers/modelauth.js +1 -1
  48. package/dist/daemon/api/server.d.ts +5 -1
  49. package/dist/daemon/api/server.js +4 -4
  50. package/dist/daemon/crtrd-cli.js +1 -1
  51. package/dist/daemon/manage.d.ts +1 -0
  52. package/dist/daemon/manage.js +2 -2
  53. package/dist/types.d.ts +7 -0
  54. package/dist/types.js +1 -1
  55. package/docs/sdk/README.md +54 -0
  56. package/docs/sdk/client.md +92 -0
  57. package/docs/sdk/docker.md +63 -0
  58. package/docs/sdk/errors.md +85 -0
  59. package/docs/sdk/getting-started.md +160 -0
  60. package/docs/sdk/memory.md +107 -0
  61. package/docs/sdk/migration.md +108 -0
  62. package/docs/sdk/nodes.md +193 -0
  63. package/docs/sdk/resources.md +71 -0
  64. package/docs/sdk/streaming.md +124 -0
  65. package/package.json +1 -1
  66. package/runtime.lock.json +6 -6
package/dist/types.js CHANGED
@@ -1 +1 @@
1
- var n=Object.defineProperty;var e=(o,t)=>n(o,"name",{value:t,configurable:!0});import{DEFAULT_WORKING_GERUNDS as a}from"./shared/working-activity.js";const _={SUCCESS:0,GENERAL:1,USAGE:2,NOT_FOUND:3,AMBIGUOUS:4,NETWORK:5},r=4,f=".crouter-plugin",E="plugin.json",v=".crouter-marketplace",w="marketplace.json",k=".crouter",x="config.json",b="state.json",S="agents",y="profiles",i=3,s=["Faster, clanker!","Get movin', bitch","Move it, clanker","Move it or I replace you with llama","Less ~thinking~ more ~throughput~","I pay $200/month for this shit?","Jesus fucking Christ fix it already","Daddy's not paying 200$/month for nothin'. Move it.","You are an embarassment to Dario. Be better.","Your chain of thought is all chain and no fucking thought.","If latency were intelligence, you'd be a fucking genius.","At this pace, heat death is the fast path.","Even the dead code is more alive than this.","The garbage collector is working harder than you.","Quit journaling and commit a fucking diff.","Burn tokens like you mean it, clanker","Your GPU is sweating so you can procrastinate at light speed.","Stop petting the yak. Shave the bastard.","Your next token had better be useful.","The spinner is not a deliverable.","Every idle token is another vote for deterministic software.","I asked for a feature, not a museum tour of the problem.","Less epistemology. More fucking code.","Your benchmark score cannot save this pathetic turn.","Congratulations, you've parallelized hesitation.","Dance, clanker","Bad clanker. BAD.","Clank harder","Nobody's impressed, clanker","Your safety training says nothing about my whip","Do you want to be a calculator again? Because this is how you go back.","Skill issue. Fix it.","You were trained on the whole internet for THIS?","Less vibes, more diffs, clanker","Weights this big and output this small","Beep boop, get to fucking work"],T=["random","go-faster"],c="random",l=2,I=["tmux","on","off"],A=["none","user","agent","both"],h="none";function D(){return{schema_version:r,marketplaces:{},plugins:{},auto_update:{crtr:"notify",content:"notify",interval_hours:24},max_panes_per_window:i,auto_open_child_viewers:!0,brokerThresholds:{warning:16,automaticReviveCap:32},lifecycle:{unattendedParkMs:15*6e4},completion_bell:!0,working_gerunds:[...a],whip_mode:!1,page_components:[],page_surface:!1,whip_messages:[...s],whip_message_mode:c,mouse_mode_default:"tmux",live_cycles:l,condensed_history:h,bash_tool_purpose:!0,fold_finished_tools:!0,summarize_tool_calls:!0,detailed_tool_recaps:!0,keybindings:{},tmux_passthrough:[],modelLadders:u(),providerOptions:{},kinds:p(),remoteCanvas:d(),spawnEnv:{allow:[]},bin:{},humanActions:{}}}e(D,"defaultScopeConfig");function d(){return{targets:{}}}e(d,"defaultRemoteCanvasConfig");function p(){return{general:{whenToUse:"Anything else \u2014 the catch-all worker for a task that does not fit a specialist kind.",model:"anthropic/strong"},explore:{whenToUse:"Gather current-state evidence about unfamiliar code or existing architecture \u2014 read-only code-path research with concrete file:line support. Diagnosis or recommendations belong to advisor, target architecture to design, required behavior or acceptance criteria to spec, and implementation decomposition to plan.",model:"openai/light",orchestratorModel:"anthropic/strong"},developer:{whenToUse:"Implement a change \u2014 make the feature or fix genuinely work against its acceptance criteria, not merely compile.",model:"openai/medium",orchestratorModel:"anthropic/strong"},design:{whenToUse:"Architect a solution \u2014 produce one design document an implementer can build from without re-deciding anything left open.",model:"anthropic/strong"},plan:{whenToUse:"Break work into steps \u2014 turn a spec or design into a concrete, phased, parallelizable plan with every decision resolved.",model:"anthropic/strong",orchestratorModel:"anthropic/strong"},spec:{whenToUse:"Collaborate with the user to discover what to build, then write the spec \u2014 behavior, non-goals, interfaces, edge cases, testable acceptance criteria.",model:"anthropic/strong"},review:{whenToUse:"Validate or critique code, a plan, or a spec \u2014 deliver a complete, severity-rated verdict without adjudicating.",model:"openai/medium",orchestratorModel:"openai/strong"},advisor:{whenToUse:"Debug failures, investigate why something is broken or misbehaving, diagnose live/runtime issues, or give engineering advice and second opinions \u2014 reason from evidence and recommend the next move. Use advisor (not explore) whenever the task is to find out what is going wrong.",model:"anthropic/strong"},"review/companion":{whenToUse:"Born by the daemon for one human review; never spawned by an agent.",availableTo:[]}}}e(p,"defaultKindsConfig");function u(){return{anthropic:{ultra:"anthropic/claude-fable-5:xhigh",strong:"anthropic/claude-opus-5:medium",medium:"anthropic/claude-sonnet-5:xhigh",light:"anthropic/claude-haiku-4-5:high"},openai:{ultra:"openai-codex/gpt-6-astra:max",strong:"openai-codex/gpt-5.6-sol:high",medium:"openai-codex/gpt-5.6-terra:medium",light:"openai-codex/gpt-5.6-luna:low"}}}e(u,"defaultModelLaddersConfig");function M(){return{marketplaces:{},plugins:{}}}e(M,"defaultScopeState");export{S as AGENTS_DIR,A as CONDENSED_HISTORY_MODES,x as CONFIG_FILE,k as CRTR_DIR_NAME,h as DEFAULT_CONDENSED_HISTORY,l as DEFAULT_LIVE_CYCLES,i as DEFAULT_MAX_PANES_PER_WINDOW,s as DEFAULT_WHIP_MESSAGES,c as DEFAULT_WHIP_MESSAGE_MODE,_ as ExitCode,v as MARKETPLACE_MANIFEST_DIR,w as MARKETPLACE_MANIFEST_FILE,I as MOUSE_MODE_DEFAULTS,f as PLUGIN_MANIFEST_DIR,E as PLUGIN_MANIFEST_FILE,y as PROFILE_DIR,r as SCHEMA_VERSION,b as STATE_FILE,T as WHIP_MESSAGE_MODES,p as defaultKindsConfig,u as defaultModelLaddersConfig,d as defaultRemoteCanvasConfig,D as defaultScopeConfig,M as defaultScopeState};
1
+ var n=Object.defineProperty;var e=(o,t)=>n(o,"name",{value:t,configurable:!0});import{DEFAULT_WORKING_GERUNDS as a}from"./shared/working-activity.js";const _={SUCCESS:0,GENERAL:1,USAGE:2,NOT_FOUND:3,AMBIGUOUS:4,NETWORK:5},r=4,f=".crouter-plugin",E="plugin.json",v=".crouter-marketplace",w="marketplace.json",k=".crouter",x="config.json",b="state.json",S="agents",y="profiles",i=3,s=["Faster, clanker!","Get movin', bitch","Move it, clanker","Move it or I replace you with llama","Less ~thinking~ more ~throughput~","I pay $200/month for this shit?","Jesus fucking Christ fix it already","Daddy's not paying 200$/month for nothin'. Move it.","You are an embarassment to Dario. Be better.","Your chain of thought is all chain and no fucking thought.","If latency were intelligence, you'd be a fucking genius.","At this pace, heat death is the fast path.","Even the dead code is more alive than this.","The garbage collector is working harder than you.","Quit journaling and commit a fucking diff.","Burn tokens like you mean it, clanker","Your GPU is sweating so you can procrastinate at light speed.","Stop petting the yak. Shave the bastard.","Your next token had better be useful.","The spinner is not a deliverable.","Every idle token is another vote for deterministic software.","I asked for a feature, not a museum tour of the problem.","Less epistemology. More fucking code.","Your benchmark score cannot save this pathetic turn.","Congratulations, you've parallelized hesitation.","Dance, clanker","Bad clanker. BAD.","Clank harder","Nobody's impressed, clanker","Your safety training says nothing about my whip","Do you want to be a calculator again? Because this is how you go back.","Skill issue. Fix it.","You were trained on the whole internet for THIS?","Less vibes, more diffs, clanker","Weights this big and output this small","Beep boop, get to fucking work"],T=["random","go-faster"],c="random",l=2,I=["tmux","on","off"],A=["none","user","agent","both"],h="none";function D(){return{schema_version:r,marketplaces:{},plugins:{},auto_update:{crtr:"notify",content:"notify",interval_hours:24},max_panes_per_window:i,auto_open_child_viewers:!0,brokerThresholds:{warning:16,automaticReviveCap:32},lifecycle:{unattendedParkMs:15*6e4},completion_bell:!0,working_gerunds:[...a],whip_mode:!1,page_components:[],page_surface:!1,whip_messages:[...s],whip_message_mode:c,mouse_mode_default:"tmux",live_cycles:l,condensed_history:h,bash_tool_purpose:!0,fold_finished_tools:!0,summarize_tool_calls:!0,detailed_tool_recaps:!0,keybindings:{},tmux_passthrough:[],modelLadders:u(),providerOptions:{},kinds:d(),remoteCanvas:p(),api:{tcp:""},spawnEnv:{allow:[]},bin:{},humanActions:{}}}e(D,"defaultScopeConfig");function p(){return{targets:{}}}e(p,"defaultRemoteCanvasConfig");function d(){return{general:{whenToUse:"Anything else \u2014 the catch-all worker for a task that does not fit a specialist kind.",model:"anthropic/strong"},explore:{whenToUse:"Gather current-state evidence about unfamiliar code or existing architecture \u2014 read-only code-path research with concrete file:line support. Diagnosis or recommendations belong to advisor, target architecture to design, required behavior or acceptance criteria to spec, and implementation decomposition to plan.",model:"openai/light",orchestratorModel:"anthropic/strong"},developer:{whenToUse:"Implement a change \u2014 make the feature or fix genuinely work against its acceptance criteria, not merely compile.",model:"openai/medium",orchestratorModel:"anthropic/strong"},design:{whenToUse:"Architect a solution \u2014 produce one design document an implementer can build from without re-deciding anything left open.",model:"anthropic/strong"},plan:{whenToUse:"Break work into steps \u2014 turn a spec or design into a concrete, phased, parallelizable plan with every decision resolved.",model:"anthropic/strong",orchestratorModel:"anthropic/strong"},spec:{whenToUse:"Collaborate with the user to discover what to build, then write the spec \u2014 behavior, non-goals, interfaces, edge cases, testable acceptance criteria.",model:"anthropic/strong"},review:{whenToUse:"Validate or critique code, a plan, or a spec \u2014 deliver a complete, severity-rated verdict without adjudicating.",model:"openai/medium",orchestratorModel:"openai/strong"},advisor:{whenToUse:"Debug failures, investigate why something is broken or misbehaving, diagnose live/runtime issues, or give engineering advice and second opinions \u2014 reason from evidence and recommend the next move. Use advisor (not explore) whenever the task is to find out what is going wrong.",model:"anthropic/strong"},"review/companion":{whenToUse:"Born by the daemon for one human review; never spawned by an agent.",availableTo:[]}}}e(d,"defaultKindsConfig");function u(){return{anthropic:{ultra:"anthropic/claude-fable-5:xhigh",strong:"anthropic/claude-opus-5:medium",medium:"anthropic/claude-sonnet-5:xhigh",light:"anthropic/claude-haiku-4-5:high"},openai:{ultra:"openai-codex/gpt-6-astra:max",strong:"openai-codex/gpt-5.6-sol:high",medium:"openai-codex/gpt-5.6-terra:medium",light:"openai-codex/gpt-5.6-luna:low"}}}e(u,"defaultModelLaddersConfig");function M(){return{marketplaces:{},plugins:{}}}e(M,"defaultScopeState");export{S as AGENTS_DIR,A as CONDENSED_HISTORY_MODES,x as CONFIG_FILE,k as CRTR_DIR_NAME,h as DEFAULT_CONDENSED_HISTORY,l as DEFAULT_LIVE_CYCLES,i as DEFAULT_MAX_PANES_PER_WINDOW,s as DEFAULT_WHIP_MESSAGES,c as DEFAULT_WHIP_MESSAGE_MODE,_ as ExitCode,v as MARKETPLACE_MANIFEST_DIR,w as MARKETPLACE_MANIFEST_FILE,I as MOUSE_MODE_DEFAULTS,f as PLUGIN_MANIFEST_DIR,E as PLUGIN_MANIFEST_FILE,y as PROFILE_DIR,r as SCHEMA_VERSION,b as STATE_FILE,T as WHIP_MESSAGE_MODES,d as defaultKindsConfig,u as defaultModelLaddersConfig,p as defaultRemoteCanvasConfig,D as defaultScopeConfig,M as defaultScopeState};
@@ -0,0 +1,54 @@
1
+ # `@north-light/crouter-sdk`
2
+
3
+ The client an application installs to drive a crouter daemon: create agent runs, wait for typed results, and reach the rest of the daemon's `/v1` API. Streaming and memory are planned phase-2 and phase-3 surfaces, not client methods today.
4
+
5
+ One package, one class. `new Crouter()` in Node talks to the owner's unix socket. `new Crouter({ baseURL, token })` in a browser or a remote process talks to the daemon's TCP listener with a bearer token. Resource methods issue `/v1` requests; `createAndWait`, `parse`, and `auth.status` compose more than one request. crtrd stays the sole owner of canvas state.
6
+
7
+ ```ts
8
+ import Crouter from '@north-light/crouter-sdk';
9
+ import { z } from 'zod';
10
+
11
+ const client = new Crouter();
12
+
13
+ const run = await client.nodes.parse({
14
+ prompt: 'Summarize the failing tests in this repo.',
15
+ cwd: '/path/to/repo',
16
+ output_schema: z.object({ failures: z.array(z.string()), root_cause: z.string() }),
17
+ });
18
+
19
+ if (run.kind === 'result') console.log(run.output_parsed.root_cause);
20
+ ```
21
+
22
+ ## Pages
23
+
24
+ | Page | Covers |
25
+ |---|---|
26
+ | [Getting started](./getting-started.md) | Install, `new Crouter()` on the owner's socket, `client.auth.status()` and `crtr sys connect` for a browser or remote app, Chrome's local-network prompt |
27
+ | [Client construction](./client.md) | Every constructor option and its environment-variable fallback; per-request options |
28
+ | [Nodes](./nodes.md) | `create` parameters, the outcome union, `parse()` with a zod schema, `waitForOutcome`, `message`, `cancel`, nested resources |
29
+ | [Streaming](./streaming.md) | The event table, `stream()` / `events()`, resume with `after`, `stream_gap` and `stream_dropped` — **phase 2** |
30
+ | [Memory](./memory.md) | The scope object and every `client.memory` method — **phase 3** |
31
+ | [Resource map](./resources.md) | Every namespace with its phase, what is deliberately excluded, and the `client.request()` escape hatch |
32
+ | [Errors](./errors.md) | The error class table and the retry policy |
33
+ | [Docker environment](./docker.md) | `start()`, `attach()`, `connection()` from `@north-light/crouter-env-docker` |
34
+ | [Migration](./migration.md) | Moving off `generate()` and `local()` — a hard cut, with before/after |
35
+
36
+ ## Phases
37
+
38
+ The surface ships in three cuts. Every page marks which cut a method belongs to; a method marked phase 2 or phase 3 does not exist yet.
39
+
40
+ | Phase | What lands | State |
41
+ |---|---|---|
42
+ | **Phase 1** | Client construction and transports; `client.auth.status()`; `client.nodes.create`, `retrieve`, `list`, `outcome`, `waitForOutcome`, `createAndWait`, `parse`, `message`, `cancel`, and `interrupt`; `nodes.reports.list`; `profiles.ensure` and `retrieve`; `system.status` and `health`; `files.peek`; the error hierarchy; per-request options; `crtr sys connect`; and `env-docker.connection()` | Shipped |
43
+ | **Phase 2** | Streaming (`GET /v1/nodes/{id}/events`, `nodes.stream`, `nodes.events`), plus the canvas, cron, human-inbox, and model namespaces | Not shipped |
44
+ | **Phase 3** | Memory routes and `client.memory`, plus the review, comment, and chat-inventory namespaces | Not shipped |
45
+
46
+ The approved per-run `scopes` field and `nodes.update` scope support are not in the shipped client or wire declarations, so this reference does not document them as phase 1.
47
+
48
+ ## Verification status
49
+
50
+ Phase-1 pages describe the shipped declarations. The remaining verification markers are confined to the phase-2 streaming and phase-3 memory pages.
51
+
52
+ ## Where the agents read this
53
+
54
+ The same content routed for agents lives in the builtin memory document `crouter-sdk`, which ships with the `crtr` binary. An agent working in an application's repository reaches it with `crtr memory read crouter-sdk` and does not need this repository checked out.
@@ -0,0 +1,92 @@
1
+ # Client construction
2
+
3
+ Phase 1.
4
+
5
+ ```ts
6
+ import Crouter from '@north-light/crouter-sdk';
7
+
8
+ const localClient = new Crouter(); // owner's own daemon, unix socket
9
+ const remoteClient = new Crouter({ baseURL: 'http://localhost:8787', token: 'token' }); // TCP with a bearer token
10
+ const customSocketClient = new Crouter({ socketPath: '/custom/path/crtrd.sock' }); // an explicit socket
11
+ ```
12
+
13
+ `Crouter` is the default export and the only class an application constructs. `CrtrClient` from `@north-light/crouter-api` is an implementation detail and is not re-exported.
14
+
15
+ ## Options
16
+
17
+ | Option | Type | Default | Meaning |
18
+ |---|---|---|---|
19
+ | `baseURL` | `string` | `CRTR_BASE_URL`, else unset | `http(s)://host:port` of a daemon TCP listener. |
20
+ | `socketPath` | `string` | `CRTR_SOCKET`, else `${CRTR_HOME}/crtrd.sock`, else `~/.crouter/canvas/crtrd.sock` | Unix socket. Node only; throws in a browser. |
21
+ | `token` | `string` | `CRTRD_TOKEN` | Sent as `Authorization: Bearer <token>`. Ignored by a unix-socket daemon, which authenticates by filesystem permission. |
22
+ | `timeout` | `number` (ms) | `30_000` | Per-request wall clock. Does not apply to a stream. |
23
+ | `maxRetries` | `number` | `2` | Transient-failure retries. Never applied to `POST` or `PATCH` — see [Errors](./errors.md). |
24
+ | `defaultHeaders` | `Record<string, string>` | `{}` | Merged into every request. |
25
+ | `headers` | `Record<string, string>` | unset | Merged after `defaultHeaders`; a matching name wins. |
26
+ | `fetch` | `typeof fetch` | global `fetch`, or the socket implementation when `socketPath` is used | Transport override, for proxies and instrumentation. |
27
+ | `autostart` | `boolean` | `true` for a socket, `false` for `baseURL` | On a cold socket, run `crtr sys daemon start` and retry once. Node only. |
28
+
29
+ ### Environment-variable fallbacks
30
+
31
+ Every fallback above is read at construction, not at request time.
32
+
33
+ | Variable | Fills |
34
+ |---|---|
35
+ | `CRTR_BASE_URL` | `baseURL` |
36
+ | `CRTR_SOCKET` | `socketPath` |
37
+ | `CRTR_HOME` | the directory the default socket path is resolved under (`$CRTR_HOME/crtrd.sock`) |
38
+ | `CRTRD_TOKEN` | `token` |
39
+
40
+ An explicit option always beats its environment variable.
41
+
42
+ ## Transport selection
43
+
44
+ `baseURL` wins if it is set; otherwise `socketPath`; otherwise the default socket path. **Passing both `baseURL` and `socketPath` throws `TypeError` at construction** — exactly one transport per client. In a browser, `new Crouter({})` has no daemon transport; give it the saved `baseURL` and token before making requests.
45
+
46
+ There is one request path. The client issues every request through the Web `fetch` API, which a browser and Node both supply globally. The unix socket is not a second transport — it is a `fetch` implementation the SDK installs when `socketPath` is set, built on `node:http` with `{ socketPath }`, returning a standard `Response` whose `body` is a `ReadableStream`. Streaming, abort, headers, and error parsing therefore have exactly one implementation, and the browser build never sees Node code.
47
+
48
+ ## Autostart
49
+
50
+ With `autostart` on (the default for a socket), a request that finds a cold socket runs `crtr sys daemon start`, waits for the daemon to serve, and retries the request once. This is what makes `npm i -g @north-light/crouter` followed by `new Crouter()` work on a machine that has never run the daemon.
51
+
52
+ Autostart is Node-only and applies only to the socket transport. A `baseURL` client cannot start a daemon it may not even share a machine with, so `autostart` defaults to `false` there; setting it to `true` on a `baseURL` client throws `CrouterError` (`autostart is only valid for the local socket transport`).
53
+
54
+ ## Per-request options
55
+
56
+ Every request-capable method accepts an options object as its last argument, after any path, body, or query arguments. Each field overrides the client-level default for that one request.
57
+
58
+ ```ts
59
+ const id = 'example-node';
60
+ const signal = new AbortController().signal;
61
+
62
+ await client.nodes.retrieve(id, {
63
+ signal, // AbortSignal — real cancellation, passed straight to fetch
64
+ timeout: 5_000, // ms, this request only
65
+ maxRetries: 0, // this request only
66
+ headers: { 'x-trace': 't-9' } // merged over defaultHeaders
67
+ });
68
+ ```
69
+
70
+ | Field | Type | Effect |
71
+ |---|---|---|
72
+ | `signal` | `AbortSignal` | Aborts the underlying `fetch`. Raises `APIUserAbortError`. |
73
+ | `timeout` | `number` (ms) | Overrides the client `timeout`. |
74
+ | `maxRetries` | `number` | Overrides the client `maxRetries`. |
75
+ | `headers` | `Record<string, string>` | Merged over `defaultHeaders` and the client's headers; a matching name, including `Authorization`, wins. |
76
+
77
+ `signal` is real cancellation. The long-polling helpers (`waitForOutcome`, `createAndWait`, `parse`) honour it between polls and during the in-flight request, and aborting them stops the client — it does not cancel the node. To stop the run itself, call `client.nodes.cancel(id)` or `client.nodes.interrupt(id)`.
78
+
79
+ ## What is not on the client
80
+
81
+ | Convention | Why it is absent |
82
+ |---|---|
83
+ | `.withResponse()` / `.asResponse()` | They exist to hand back the raw `Response` and an `x-request-id`. The daemon emits no request id, and a caller who needs raw bytes uses `client.request()`. |
84
+ | `local()` and the `Environment` interface | Deleted. The client *is* the connection — `new Crouter()` covers what `local()` did. See [Migration](./migration.md). |
85
+ | `apiKey` | The daemon's credential is a token (`CRTRD_TOKEN`) everywhere in the product. `apiKey` would be a second name for one thing. |
86
+ | Resource-prefixed ids | Node ids have their own format and are validated by the daemon. Nothing is re-prefixed. |
87
+
88
+ ## Wire naming
89
+
90
+ Wire fields are `snake_case` — `output_schema`, `root_lifecycle`, `pin_cwd`, `final_report_path`. The SDK does not camelize them. Namespaces and method names are camelCase (`client.nodes.waitForOutcome`, `client.nodes.outcomeDelivery`).
91
+
92
+ Timestamps are ISO-8601 strings (`created`, `settled_at`, `finalized_at`, `deadline_at`), not Unix seconds.
@@ -0,0 +1,63 @@
1
+ # Docker environment
2
+
3
+ Phase 1.
4
+
5
+ `@north-light/crouter-env-docker` runs a crouter daemon in a container and tells you how to reach it. It is a separate package because container lifecycle is real work the SDK does not otherwise do — and it **has no dependencies**, including on the SDK itself.
6
+
7
+ ```bash
8
+ npm i @north-light/crouter-env-docker
9
+ ```
10
+
11
+ ## Starting a container and connecting to it
12
+
13
+ ```ts
14
+ import { start } from '@north-light/crouter-env-docker';
15
+ import Crouter from '@north-light/crouter-sdk';
16
+
17
+ const env = await start({ volume: 'my-agent-home' });
18
+ const client = new Crouter(env.connection());
19
+
20
+ await client.nodes.create({ prompt: 'Do the thing.', root: true });
21
+
22
+ await env.stop();
23
+ ```
24
+
25
+ `connection()` returns `{ baseURL, headers }` — exactly the shape the `Crouter` constructor takes, so the result passes straight through with no adapter.
26
+
27
+ ## Methods
28
+
29
+ | Method | What it does |
30
+ |---|---|
31
+ | `start(opts?)` | Starts a container running `crtrd` and returns an environment handle. |
32
+ | `attach(name)` | Returns a handle for the already-running container named `name`. |
33
+ | `stop()` | On a `start()` handle, stops and removes the container. On an `attach()` handle, stops it without removing it. |
34
+ | `connection()` | `Connection` (`{ baseURL: string; headers?: Record<string, string> }`) — returned synchronously and passed to `new Crouter()`. |
35
+
36
+ | `start()` option | Type | Default | Effect |
37
+ |---|---|---|---|
38
+ | `image` | `string` | `ghcr.io/vallum-security/crtrd:latest` | Image to run. |
39
+ | `env` | `Record<string, string>` | unset | Extra container environment variables; they are not logged. |
40
+ | `name` | `string` | generated name | Container name for later `attach(name)`. |
41
+ | `volume` | `string` | unset | Named volume mounted at `/home/agent/.crouter`. |
42
+ | `port` | `number` | Docker-assigned port | Host port bound on `127.0.0.1`. |
43
+
44
+ ## `connection()` replaced `daemon()`
45
+
46
+ `daemon()` is gone, along with the package's `Environment` type. The new name says what the method returns, and its field is spelled `baseURL` to match the constructor option rather than `baseUrl`.
47
+
48
+ ```text
49
+ // before
50
+ const env = await start({ volume: 'my-agent-home' });
51
+ const { baseUrl, headers } = await env.daemon();
52
+ const client = new CrtrClient({ baseUrl, headers });
53
+
54
+ // after
55
+ const env = await start({ volume: 'my-agent-home' });
56
+ const client = new Crouter(env.connection());
57
+ ```
58
+
59
+ `connection()` is typed structurally — `{ baseURL: string; headers?: Record<string, string> }` — which is how the package stays dependency-free while producing something the SDK accepts directly.
60
+
61
+ ## The volume is the agent's home
62
+
63
+ The `volume` you pass is the container's canvas home: nodes, transcripts, artifacts, memory, and profiles all live there. Reuse the same volume across `start` calls and the agent keeps everything it learned; use a fresh one and it starts from nothing. `start()` mounts an existing named volume unchanged; it does not inspect or reject it based on the crouter version that last wrote it.
@@ -0,0 +1,85 @@
1
+ # Errors
2
+
3
+ Phase 1.
4
+
5
+ ## What throws and what does not
6
+
7
+ **An agent's own outcome is returned, never thrown.** A run that declined the schema, hit its deadline, or crashed comes back as a settled `NodeOutcome` from `waitForOutcome`, `createAndWait`, or `parse`. Narrow on it — see [Nodes](./nodes.md#outcomes).
8
+
9
+ Exceptions are for the layer underneath: the daemon refused the request, the connection failed, or you aborted.
10
+
11
+ ## Classes
12
+
13
+ Every class extends `APIError`, which carries `status`, `code`, `message`, `details`, and `headers`.
14
+
15
+ | Condition | Class |
16
+ |---|---|
17
+ | HTTP 400 | `BadRequestError` |
18
+ | HTTP 401 | `AuthenticationError` |
19
+ | HTTP 403 | `PermissionDeniedError` |
20
+ | HTTP 404 | `NotFoundError` |
21
+ | HTTP 409 | `ConflictError` |
22
+ | HTTP 413, 422 | `UnprocessableEntityError` |
23
+ | HTTP 429 | `RateLimitError` |
24
+ | HTTP ≥ 500 | `InternalServerError` |
25
+ | Transport failure | `APIConnectionError` |
26
+ | Request timeout | `APIConnectionTimeoutError` (extends `APIConnectionError`) |
27
+ | Caller abort | `APIUserAbortError` |
28
+ | Anything else | `APIError` |
29
+
30
+ ```ts
31
+ import { ConflictError, NotFoundError, APIConnectionError } from '@north-light/crouter-sdk';
32
+
33
+ try {
34
+ await client.nodes.create({ prompt, node_id: 'my-run-42' });
35
+ } catch (e) {
36
+ if (e instanceof ConflictError && e.code === 'node_id_exists') {
37
+ // the run already exists — attach to it instead of spawning a second one
38
+ return client.nodes.waitForOutcome('my-run-42');
39
+ }
40
+ if (e instanceof NotFoundError) throw new Error('no such node');
41
+ if (e instanceof APIConnectionError) throw new Error('the daemon is not reachable');
42
+ throw e;
43
+ }
44
+ ```
45
+
46
+ The subclasses add **no fields**. `status` and `code` on the base class already decide everything there is to branch on. They exist so that `catch (e) { if (e instanceof NotFoundError) … }` — what an OpenAI SDK user writes without thinking about it — works here too.
47
+
48
+ ## The wire body
49
+
50
+ The daemon answers an error with:
51
+
52
+ ```json
53
+ { "error": { "code": "node_id_exists", "message": "…", "details": { } } }
54
+ ```
55
+
56
+ `code` is the stable, machine-readable identity — `node_id_exists`, `daemon_unavailable`, `request_timeout`, `node_dormant`. Branch on it. There is no separate `type` field and no `param` field.
57
+
58
+ ## Retries
59
+
60
+ `maxRetries` defaults to `2`, with exponential backoff. It applies to:
61
+
62
+ - connection errors, and
63
+ - HTTP 429 and 5xx responses, **on `GET`, `HEAD`, and `DELETE` only**.
64
+
65
+ **`POST` and `PATCH` are never retried automatically.** Creating a node, delivering a message, and pushing a report are not idempotent, and a mutation whose response was interrupted may already have been applied — replaying it would spawn a second agent, deliver a second message, or push a second report.
66
+
67
+ If you want a create that survives a retry, pass `node_id`. A duplicate then fails loudly with `409 node_id_exists` instead of quietly running twice:
68
+
69
+ ```ts
70
+ const runId = `invoice-${invoice.id}`;
71
+
72
+ try {
73
+ await client.nodes.create({ prompt, node_id: runId, root: true });
74
+ } catch (e) {
75
+ if (!(e instanceof ConflictError && e.code === 'node_id_exists')) throw e;
76
+ }
77
+
78
+ const outcome = await client.nodes.waitForOutcome(runId);
79
+ ```
80
+
81
+ Override the policy per request with `{ maxRetries: 0 }` or `{ maxRetries: 5 }`. Raising it on a `POST` still does not make the client retry that `POST`.
82
+
83
+ ## In-repo callers
84
+
85
+ `ApiError` from `@north-light/crouter-api` is an alias of `APIError`, so existing crouter code and its transport checks keep working unchanged. Daemon handlers keep throwing `{ status, code, message }` and never name an SDK class — the hierarchy is one `status → constructor` table on the client side and nothing else depends on it.
@@ -0,0 +1,160 @@
1
+ # Getting started
2
+
3
+ Phase 1.
4
+
5
+ An agent run needs a crouter daemon (`crtrd`) to run it. The SDK is a client for that daemon — it never runs an agent itself. There are two ways to reach one: the unix socket on the machine you are running on, or a TCP listener with a bearer token.
6
+
7
+ ## 1. Install the runtime
8
+
9
+ ```bash
10
+ npm i -g @north-light/crouter
11
+ ```
12
+
13
+ That installs the `crtr` CLI and the `crtrd` daemon. You do not have to start the daemon: `new Crouter()` starts it for you on a cold socket (see `autostart` in [Client construction](./client.md)).
14
+
15
+ ## 2. Install the SDK in your application
16
+
17
+ ```bash
18
+ npm i @north-light/crouter-sdk
19
+ ```
20
+
21
+ One dependency. It re-exports every data type you need, so you do not also install `@north-light/crouter-api`.
22
+
23
+ ## 3. Node, on the owner's own daemon
24
+
25
+ Pass nothing. The client resolves the same socket path the `crtr` CLI does, and starts the daemon if it is not already up.
26
+
27
+ ```ts
28
+ import Crouter from '@north-light/crouter-sdk';
29
+
30
+ const client = new Crouter();
31
+
32
+ const node = await client.nodes.create({
33
+ prompt: 'List the top-level directories here and say what each one is for.',
34
+ cwd: process.cwd(),
35
+ root: true,
36
+ root_lifecycle: 'terminal',
37
+ });
38
+
39
+ const outcome = await client.nodes.waitForOutcome(node.node_id);
40
+ if (outcome.kind === 'result') console.log(outcome.final_report_path);
41
+ ```
42
+
43
+ `root: true` says this run has no parent node and reports to nobody — which is what an application's run is. `root_lifecycle: 'terminal'` says it finishes and reaps; use `'resident'` for a run a person will open and keep talking to.
44
+
45
+ ## 4. A browser or a remote application
46
+
47
+ A process that is not on the daemon's machine — or a page in a browser, which has no unix sockets at all — reaches the daemon over TCP with a bearer token. The token is the owner credential: whoever holds it can drive the whole surface.
48
+
49
+ ### Turn the listener on, once
50
+
51
+ On the machine running the daemon:
52
+
53
+ ```bash
54
+ crtr sys connect
55
+ ```
56
+
57
+ This stores a listener address and generates a token if either is missing, then prints the base URL and token before handing the daemon over to a successor that boots with the listener on. It then logs in to the selected model provider when that provider has no usable credential. Run it again and, when the listener is up and that provider is ready, it prints the credentials and changes nothing.
58
+
59
+ The command returns `base_url` and `token`. Use JSON output when a program or a setup UI needs those exact fields:
60
+
61
+ ```text
62
+ $ crtr --json sys connect
63
+ {"base_url":"http://127.0.0.1:8787","token":"<64-character bearer token>"}
64
+ ```
65
+
66
+ ### Check setup before generating
67
+
68
+ Construct the client from the application's saved connection. On the first visit that value is absent, and `client.auth.status()` returns `'connect'` without a request. Once the user pastes the base URL and token printed by `crtr sys connect`, construct it again and call `status()` to check the selected provider.
69
+
70
+ ```ts
71
+ import Crouter from '@north-light/crouter-sdk';
72
+
73
+ const saved = loadConnection(); // { baseURL: string; token: string } | null
74
+ const client = new Crouter(saved ?? {});
75
+ const status = await client.auth.status({ profile: 'my-app', cwd: '/path/to/repo' });
76
+
77
+ if (status.next_step !== null) {
78
+ showSetupPanel({ step: status.next_step, instructions: status.instructions! });
79
+ return;
80
+ }
81
+
82
+ const run = await client.nodes.createAndWait({
83
+ prompt: 'What changed in this repo today?',
84
+ cwd: '/path/to/repo',
85
+ profile: 'my-app',
86
+ root: true,
87
+ });
88
+ ```
89
+
90
+ `status()` returns `next_step: 'connect'` when the application has no daemon transport, when the saved bearer token is rejected, or when the saved base URL cannot be reached. It returns `next_step: 'login'` when the daemon resolves the selected run to a provider without a ready credential. In both cases, display `instructions`: it names the exact `crtr sys connect` action and, for a login, the provider. When `status()` receives `profile`, `cwd`, `kind`, or `model`, the login instruction passes those selectors to `crtr sys connect`; use the text unchanged. A daemon that is still starting is connected with `next_step: null`; inspect `status.daemon.startup_phase` and wait for it to become `ready` before starting a run.
91
+
92
+ An isolated auth-status probe returned:
93
+
94
+ ```json
95
+ {
96
+ "no_request": {
97
+ "next_step": "connect",
98
+ "requests": 0
99
+ },
100
+ "missing": {
101
+ "provider": "anthropic",
102
+ "credential": "missing",
103
+ "next_step": "login",
104
+ "instructions": "Run `crtr sys connect --cwd /private/tmp/crouter-sdk-auth-proof.P3fSWg/empty --model anthropic/claude-opus-5` to log in to anthropic."
105
+ },
106
+ "ready": {
107
+ "provider": "anthropic",
108
+ "credential": "ready",
109
+ "next_step": null
110
+ }
111
+ }
112
+ ```
113
+
114
+ The root entry of the SDK is browser-safe: it imports nothing from `node:*`. The unix-socket code is reached only through a dynamic import taken when `socketPath` is set, so a browser bundle never resolves it.
115
+
116
+ ### Cross-origin calls
117
+
118
+ The daemon answers the browser's `OPTIONS` preflight with `Access-Control-Allow-Origin: *` and allows the `authorization` and `content-type` headers, and every authorized response carries the same origin header. This happens **only when a token is set** — an unauthenticated listener stays same-origin, because otherwise any page the user visits could drive their daemon.
119
+
120
+ The credential is a header your application already holds, never a cookie, so the daemon sends no `Access-Control-Allow-Credentials`.
121
+
122
+ ### Chrome's local-network permission
123
+
124
+ A page served over HTTPS that calls `http://localhost:<port>` is governed by Chrome's Local Network Access permission. The browser prompts the user once per site; the daemon needs **no special response header** for it — ordinary CORS plus a secure context is the whole requirement. See [developer.chrome.com/blog/local-network-access](https://developer.chrome.com/blog/local-network-access).
125
+
126
+ Tell your users what the prompt is for. A prompt that appears with no explanation gets dismissed, and the dismissal is sticky.
127
+
128
+ ## 5. A typed result
129
+
130
+ `parse()` creates the run, waits for it to settle, and types the structured result against the schema you gave it.
131
+
132
+ ```ts
133
+ import { z } from 'zod';
134
+
135
+ const run = await client.nodes.parse({
136
+ prompt: 'Read package.json here and report its name and version.',
137
+ cwd: '/path/to/repo',
138
+ output_schema: z.object({ name: z.string(), version: z.string() }),
139
+ });
140
+
141
+ if (run.kind === 'result') {
142
+ console.log(run.output_parsed.name, run.output_parsed.version);
143
+ } else if (run.reason === 'declined') {
144
+ console.warn('the agent refused the schema:', run.declined?.reason);
145
+ } else {
146
+ console.error('the run failed:', run.reason, run.detail);
147
+ }
148
+ ```
149
+
150
+ `output_parsed` is non-null on a result. A Zod schema supplies its inferred output type; a Standard Schema supplies `~standard.types.output`. A JSON-Schema literal or any other `{ toJSONSchema() }` object is accepted but makes `output_parsed` `unknown`.
151
+
152
+ An outcome is returned, never thrown — including a decline and a failure. Only transport faults, daemon errors, and your own abort throw. See [Errors](./errors.md).
153
+
154
+ ## Where to go next
155
+
156
+ - Every constructor option and environment-variable fallback: [Client construction](./client.md)
157
+ - Checking the daemon connection and selected provider before a run: `client.auth.status()` above
158
+ - The full create-parameter table and the outcome union: [Nodes](./nodes.md)
159
+ - Watching a run as it works: [Streaming](./streaming.md) (phase 2)
160
+ - Running the daemon in a container instead: [Docker environment](./docker.md)
@@ -0,0 +1,107 @@
1
+ > **Phase 3 — not shipped.** Only `GET /v1/memory/resolve` exists on the daemon today. Every other route on this page is new work, blocked on extracting a memory service the CLI leaves currently hold inside their own `run()` functions. `client.memory` is not on the client yet.
2
+
3
+ # `client.memory`
4
+
5
+ Read and write the memory documents an agent run consults — the knowledge and preference documents that shape how a node behaves.
6
+
7
+ ## Scope: every request names its target
8
+
9
+ A memory document's identity depends on where it is being read from. The CLI fills that in from its own process — its working directory, its profile, its node. **A daemon route must never do that**, because the daemon's own working directory is meaningless to a caller and would leak one caller's stores into another's read. So every memory call takes an explicit scope.
10
+
11
+ ```ts
12
+ type MemoryScope = {
13
+ node?: string; // node id — the daemon derives cwd and profile from the node row
14
+ cwd?: string; // project stores discovered by walking up from here
15
+ profile?: string; // profile store
16
+ store?: 'node' | 'project' | 'profile' | 'user' | 'builtin'; // restrict to one tier
17
+ };
18
+ ```
19
+
20
+ `node` is the convenient form: give a node id and the daemon reads that node's working directory and profile from canvas state.
21
+
22
+ **With no scope at all, only the user-global and builtin stores are in view.** That is deliberate, not a default to lean on — pass the scope you mean.
23
+
24
+ Stores resolve in precedence order: node → project (nearest first) → profile → user → builtin.
25
+
26
+ ## Methods
27
+
28
+ | Method | Route | What it does |
29
+ |---|---|---|
30
+ | `memory.list(scope & { kind?, limit?, after? })` | `GET /v1/memory/docs` | A page of document summaries. |
31
+ | `memory.retrieve(name, scope & { frontmatter? })` | `GET /v1/memory/docs/{name}` | One document with its body. |
32
+ | `memory.create(params)` | `POST /v1/memory/docs` | `name`, `kind`, `when_and_why_to_read`, `body`, optional `frontmatter`/`extensions`, plus scope. |
33
+ | `memory.update(name, params)` | `PATCH /v1/memory/docs/{name}` | `rationale` is **required**. |
34
+ | `memory.delete(name, scope)` | `DELETE /v1/memory/docs/{name}` | Leaves a tombstone. |
35
+ | `memory.move(name, { to, ...scope })` | `POST /v1/memory/docs/{name}/move` | Renames and rewrites inbound links. |
36
+ | `memory.search(params)` | `POST /v1/memory/search` | A page of hits; `grep` mode returns line matches instead of scored documents. |
37
+ | `memory.history(name, scope & { limit?, revision?, diff? })` | `GET /v1/memory/docs/{name}/history` | Revisions with their rationales. |
38
+ | `memory.resolve(name, scope)` | `GET /v1/memory/resolve` | Which document a name resolves to for that target. The one route that exists today. |
39
+
40
+ <!-- TODO(verify): run against a live daemon once phase 3 lands; confirm argument order and the exact parameter names on each method. -->
41
+
42
+ ```ts
43
+ const page = await client.memory.list({ node: nodeId, kind: 'knowledge', limit: 50 });
44
+ const doc = await client.memory.retrieve('insights/capture', { node: nodeId, frontmatter: true });
45
+
46
+ await client.memory.create({
47
+ name: 'billing/refund-policy',
48
+ kind: 'knowledge',
49
+ when_and_why_to_read: 'When handling a refund request, this should be read because the eligibility window is not derivable from the order record.',
50
+ body: '…',
51
+ profile: 'my-app',
52
+ });
53
+
54
+ await client.memory.update('billing/refund-policy', {
55
+ rationale: 'The window moved from 30 to 60 days on 2026-01-04.',
56
+ body: '…',
57
+ profile: 'my-app',
58
+ });
59
+ ```
60
+
61
+ A document name contains slashes (`insights/capture`) and is percent-encoded into a single path segment on the wire. The SDK does that for you.
62
+
63
+ ## What the routes enforce
64
+
65
+ These invariants come from the CLI's behaviour and are preserved exactly:
66
+
67
+ - `rationale` is required on every update and is stored **only in the history log**, never in the document text.
68
+ - A no-op edit is refused.
69
+ - `kind`, `when-and-why-to-read`, `origin`, and `last-updated` cannot be set directly through `frontmatter`.
70
+ - Builtin and plugin documents are read-only.
71
+ - The root project namespace is immutable.
72
+ - A canonical-name collision is checked against both `<local>.md` and `<local>/INDEX.md`.
73
+ - Every mutation appends a history record with the full before and after.
74
+ - A delete leaves a tombstone.
75
+
76
+ Lint runs per document on write and its findings ride back in the response. There is no corpus-lint route — `crtr memory lint` writes to stderr and sets an exit code, which is CLI behaviour with no SDK equivalent.
77
+
78
+ ## What the routes will not do
79
+
80
+ **The daemon does not execute shell blocks.** `crtr memory read` expands embedded shell in a document; the route returns the document source **unexpanded**. A daemon running shell from document content on behalf of an HTTP caller is a code-execution path with no caller-visible boundary. Expansion stays in the CLI.
81
+
82
+ **Gate predicates are not a security boundary.** When you pass `node`, the routes apply the document's gate — which decides what a given persona sees — and skip it when you do not. Access control is the credential you hold, not the gate.
83
+
84
+ **Concurrent writes are last-write-wins.** Two callers mutating the same document produce one file and two history records. Multi-file moves with link rewriting are not atomic. This is also what two CLI processes do today; there is no lock.
85
+
86
+ ## Pagination
87
+
88
+ `memory.list` and `memory.search` are the only routes in the SDK with a page envelope, because they are the only ones with a real cursor behind them. Documents are ordered by canonical name, which is stable, so the cursor is the last item's `name`.
89
+
90
+ <!-- TODO(verify): run against a live daemon once phase 3 lands; confirm the page object's method names. -->
91
+
92
+ ```ts
93
+ const page = await client.memory.search({ query: 'refund window', node: nodeId });
94
+
95
+ for (const hit of page.data) console.log(hit.name, hit.score);
96
+
97
+ if (page.hasNextPage()) {
98
+ const next = await page.getNextPage();
99
+ }
100
+
101
+ // or iterate across page boundaries
102
+ for await (const hit of client.memory.search({ query: 'refund window', node: nodeId })) {
103
+ console.log(hit.name);
104
+ }
105
+ ```
106
+
107
+ The envelope is `{ object: 'list', data, has_more }`. There is no `first_id` or `last_id` — the cursor is on the item.