@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.
- package/bin/runtime-selector.mjs +52 -3
- package/dist/api/client.d.ts +29 -16
- package/dist/api/client.js +2 -2
- package/dist/api/dto/modelauth.d.ts +15 -0
- package/dist/api/errors.d.ts +6 -3
- package/dist/api/errors.js +1 -1
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +1 -1
- package/dist/api/node-transport.d.ts +18 -0
- package/dist/api/node-transport.js +1 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -1
- package/dist/builtin-memory/crouter-sdk.md +86 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +1 -1
- package/dist/cli.js +8 -2
- package/dist/clients/attach/render/chat-view.d.ts +5 -1
- package/dist/clients/attach/render/chat-view.js +1 -1
- package/dist/clients/attach/session/frames.d.ts +1 -0
- package/dist/clients/attach/session/frames.js +1 -1
- package/dist/clients/attach/viewer.js +583 -583
- package/dist/commands/api-client.js +3 -3
- package/dist/commands/memory/read.js +2 -2
- package/dist/commands/sys/branch.js +1 -1
- package/dist/commands/sys/connect.d.ts +1 -0
- package/dist/commands/sys/connect.js +3 -0
- package/dist/commands/sys/daemon.js +1 -1
- package/dist/commands/sys/panels/provider-panel.js +1 -1
- package/dist/commands/sys/provider-login.d.ts +8 -0
- package/dist/commands/sys/provider-login.js +9 -0
- package/dist/commands/sys/setup-core.d.ts +4 -0
- package/dist/commands/sys/setup-core.js +4 -4
- package/dist/commands/sys.js +1 -1
- package/dist/core/canvas/paths.d.ts +4 -4
- package/dist/core/config.js +1 -1
- package/dist/core/runtime/broker/daemon-ops.js +1 -1
- package/dist/core/secrets.d.ts +3 -0
- package/dist/core/secrets.js +2 -2
- package/dist/core/subscription-state.d.ts +1 -1
- package/dist/core/subscription-state.js +3 -3
- package/dist/core/termrender/termrender.d.ts +3 -1
- package/dist/core/termrender/termrender.js +13 -13
- package/dist/core/user-settings.d.ts +4 -0
- package/dist/core/user-settings.js +1 -1
- package/dist/daemon/api/handlers/health.d.ts +1 -1
- package/dist/daemon/api/handlers/health.js +3 -3
- package/dist/daemon/api/handlers/modelauth.js +1 -1
- package/dist/daemon/api/server.d.ts +5 -1
- package/dist/daemon/api/server.js +4 -4
- package/dist/daemon/crtrd-cli.js +1 -1
- package/dist/daemon/manage.d.ts +1 -0
- package/dist/daemon/manage.js +2 -2
- package/dist/types.d.ts +7 -0
- package/dist/types.js +1 -1
- package/docs/sdk/README.md +54 -0
- package/docs/sdk/client.md +92 -0
- package/docs/sdk/docker.md +63 -0
- package/docs/sdk/errors.md +85 -0
- package/docs/sdk/getting-started.md +160 -0
- package/docs/sdk/memory.md +107 -0
- package/docs/sdk/migration.md +108 -0
- package/docs/sdk/nodes.md +193 -0
- package/docs/sdk/resources.md +71 -0
- package/docs/sdk/streaming.md +124 -0
- package/package.json +1 -1
- 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:
|
|
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.
|