@north-light/sdk 0.0.0-stage → 0.3.414

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 (111) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +187 -2
  3. package/dist/client.d.ts +162 -0
  4. package/dist/client.js +543 -0
  5. package/dist/error-codes.d.ts +7 -0
  6. package/dist/error-codes.js +41 -0
  7. package/dist/errors.d.ts +57 -0
  8. package/dist/errors.js +87 -0
  9. package/dist/index.d.ts +41 -0
  10. package/dist/index.js +17 -0
  11. package/dist/keygen-cli.d.ts +2 -0
  12. package/dist/keygen-cli.js +6 -0
  13. package/dist/keygen-command.d.ts +6 -0
  14. package/dist/keygen-command.js +157 -0
  15. package/dist/oauth/index.d.ts +217 -0
  16. package/dist/oauth/index.js +535 -0
  17. package/dist/oauth/keygen.d.ts +18 -0
  18. package/dist/oauth/keygen.js +46 -0
  19. package/dist/oauth/refusal.d.ts +32 -0
  20. package/dist/oauth/refusal.js +17 -0
  21. package/dist/resources/activity.d.ts +22 -0
  22. package/dist/resources/activity.js +43 -0
  23. package/dist/resources/attachments.d.ts +15 -0
  24. package/dist/resources/attachments.js +13 -0
  25. package/dist/resources/bash.d.ts +9 -0
  26. package/dist/resources/bash.js +15 -0
  27. package/dist/resources/canvas/history.d.ts +11 -0
  28. package/dist/resources/canvas/history.js +20 -0
  29. package/dist/resources/canvas.d.ts +19 -0
  30. package/dist/resources/canvas.js +42 -0
  31. package/dist/resources/crons.d.ts +15 -0
  32. package/dist/resources/crons.js +33 -0
  33. package/dist/resources/custom-objects.d.ts +20 -0
  34. package/dist/resources/custom-objects.js +84 -0
  35. package/dist/resources/files.d.ts +34 -0
  36. package/dist/resources/files.js +34 -0
  37. package/dist/resources/follower.d.ts +71 -0
  38. package/dist/resources/follower.js +202 -0
  39. package/dist/resources/forward.d.ts +7 -0
  40. package/dist/resources/forward.js +67 -0
  41. package/dist/resources/grant-watcher.d.ts +57 -0
  42. package/dist/resources/grant-watcher.js +100 -0
  43. package/dist/resources/human/inbox.d.ts +14 -0
  44. package/dist/resources/human/inbox.js +30 -0
  45. package/dist/resources/human/requests.d.ts +13 -0
  46. package/dist/resources/human/requests.js +26 -0
  47. package/dist/resources/human.d.ts +8 -0
  48. package/dist/resources/human.js +10 -0
  49. package/dist/resources/identifiers.d.ts +8 -0
  50. package/dist/resources/identifiers.js +33 -0
  51. package/dist/resources/memory.d.ts +69 -0
  52. package/dist/resources/memory.js +30 -0
  53. package/dist/resources/models/config.d.ts +8 -0
  54. package/dist/resources/models/config.js +9 -0
  55. package/dist/resources/models/credentials.d.ts +10 -0
  56. package/dist/resources/models/credentials.js +17 -0
  57. package/dist/resources/models.d.ts +8 -0
  58. package/dist/resources/models.js +10 -0
  59. package/dist/resources/node-stream.d.ts +34 -0
  60. package/dist/resources/node-stream.js +176 -0
  61. package/dist/resources/nodes/jobs.d.ts +9 -0
  62. package/dist/resources/nodes/jobs.js +14 -0
  63. package/dist/resources/nodes/result.d.ts +8 -0
  64. package/dist/resources/nodes/result.js +11 -0
  65. package/dist/resources/nodes/worktree.d.ts +9 -0
  66. package/dist/resources/nodes/worktree.js +14 -0
  67. package/dist/resources/nodes.d.ts +23 -0
  68. package/dist/resources/nodes.js +47 -0
  69. package/dist/resources/providers.d.ts +51 -0
  70. package/dist/resources/providers.js +15 -0
  71. package/dist/resources/questions.d.ts +49 -0
  72. package/dist/resources/questions.js +21 -0
  73. package/dist/resources/request.d.ts +7 -0
  74. package/dist/resources/request.js +19 -0
  75. package/dist/resources/run-notice.d.ts +34 -0
  76. package/dist/resources/run-notice.js +67 -0
  77. package/dist/resources/run-reply.d.ts +79 -0
  78. package/dist/resources/run-reply.js +93 -0
  79. package/dist/resources/run-stream.d.ts +65 -0
  80. package/dist/resources/run-stream.js +243 -0
  81. package/dist/resources/run-watcher.d.ts +98 -0
  82. package/dist/resources/run-watcher.js +162 -0
  83. package/dist/resources/runs.d.ts +192 -0
  84. package/dist/resources/runs.js +208 -0
  85. package/dist/resources/shares.d.ts +40 -0
  86. package/dist/resources/shares.js +19 -0
  87. package/dist/resources/structured-requests.d.ts +14 -0
  88. package/dist/resources/structured-requests.js +19 -0
  89. package/dist/resources/uploads.d.ts +27 -0
  90. package/dist/resources/uploads.js +11 -0
  91. package/dist/schema.d.ts +7 -0
  92. package/dist/schema.js +10 -0
  93. package/dist/stores/postgres.d.ts +32 -0
  94. package/dist/stores/postgres.js +92 -0
  95. package/dist/testing/cloud.d.ts +209 -0
  96. package/dist/testing/cloud.js +880 -0
  97. package/dist/testing/index.d.ts +5 -0
  98. package/dist/testing/index.js +3 -0
  99. package/dist/testing/notices.d.ts +70 -0
  100. package/dist/testing/notices.js +205 -0
  101. package/dist/testing/run.d.ts +172 -0
  102. package/dist/testing/run.js +429 -0
  103. package/dist/testing/users.d.ts +121 -0
  104. package/dist/testing/users.js +70 -0
  105. package/dist/testing/wire.d.ts +62 -0
  106. package/dist/testing/wire.js +148 -0
  107. package/dist/types.d.ts +80 -0
  108. package/dist/types.js +1 -0
  109. package/dist/version.d.ts +2 -0
  110. package/dist/version.js +3 -0
  111. package/package.json +57 -4
package/dist/client.js ADDED
@@ -0,0 +1,543 @@
1
+ import { APIError, CrtrClient, MINIMUM_RUNTIME_VERSION_HEADER, RUNTIME_VERSION_HEADER, compareReleaseVersions, routes, } from '@north-light/api';
2
+ import { APIConnectionError, APIUserAbortError, AuthenticationError, CrouterError, mapError } from './errors.js';
3
+ import { Bash } from './resources/bash.js';
4
+ import { Canvas } from './resources/canvas.js';
5
+ import { Crons } from './resources/crons.js';
6
+ import { CustomObjects } from './resources/custom-objects.js';
7
+ import { Attachments } from './resources/attachments.js';
8
+ import { Files } from './resources/files.js';
9
+ import { Human } from './resources/human.js';
10
+ import { Models } from './resources/models.js';
11
+ import { NodesResource } from './resources/nodes.js';
12
+ import { nodeId, pathSegment } from './resources/identifiers.js';
13
+ import { NodeStream } from './resources/node-stream.js';
14
+ import { Questions } from './resources/questions.js';
15
+ import { Providers } from './resources/providers.js';
16
+ import { Runs } from './resources/runs.js';
17
+ import { Shares } from './resources/shares.js';
18
+ import { StructuredRequests } from './resources/structured-requests.js';
19
+ import { Memory } from './resources/memory.js';
20
+ import { Uploads } from './resources/uploads.js';
21
+ import { heldReadOptions } from './resources/request.js';
22
+ import { resolveJsonSchema } from './schema.js';
23
+ import { SDK_VERSION } from './version.js';
24
+ const LOCAL_SOCKET_FETCH = Symbol.for('@north-light/api/local-socket-fetch');
25
+ const DEFAULT_TIMEOUT_MS = 30_000;
26
+ const POLL_WAIT_SECONDS = 25;
27
+ /**
28
+ * The client, and the only class an application constructs. Each namespace below wraps a
29
+ * family of daemon routes; `request` is the escape hatch for a route with no wrapper.
30
+ */
31
+ export class Crouter {
32
+ /** Create, inspect, message, stream, and wait on nodes. */
33
+ nodes;
34
+ /** Run one command in an absolute working directory. */
35
+ bash;
36
+ /** Canvas-wide views: attention, snapshot, roster, dashboard, prune, and history. */
37
+ canvas;
38
+ /** Scheduled commands. */
39
+ crons;
40
+ /** Durable structured requests with deadlines and one settlement. */
41
+ structuredRequests;
42
+ /** Register, publish, receive, and acknowledge custom canvas objects. */
43
+ customObjects;
44
+ /** Human requests and the human inbox. */
45
+ human;
46
+ /** Model credentials and model configuration. */
47
+ models;
48
+ /** Agent profiles. */
49
+ profiles;
50
+ /** Daemon status and health. */
51
+ system;
52
+ /** Whether this client can reach a daemon that can run a model. */
53
+ auth;
54
+ /** Read, write, and list absolute host paths. */
55
+ files;
56
+ runs;
57
+ /** Uploads to runtime storage, for files a run prompt or message names (app listener only). */
58
+ uploads;
59
+ /** Signed links to files a run received as attachments (app listener only). */
60
+ attachments;
61
+ /** Public share links to files (app listener only). */
62
+ shares;
63
+ /** Questions agents in runs ask the user (app listener only). */
64
+ questions;
65
+ /** Read the documents the person owns (`store: 'user'`): an app sees their `public` documents, and every privacy level with a `crtr:memory:manage:user` grant. */
66
+ memory;
67
+ /** Scope-filtered provider tools and daemon-mediated calls. */
68
+ providers;
69
+ /** The caller's grant. With `wait` (1–25 s) and `since` (a `version` from an earlier answer), the runtime holds the
70
+ * read until the grant's `version` differs from `since` or `wait` seconds pass; the request timeout is extended by
71
+ * `wait`. To follow many people's grants, use `oauth.watchGrants`. */
72
+ grant;
73
+ health;
74
+ eventFetch;
75
+ bearerToken;
76
+ connectionToken;
77
+ tokenSource;
78
+ onUnauthorized;
79
+ client;
80
+ timeout;
81
+ baseURL;
82
+ socketPath;
83
+ constructor(options = {}) {
84
+ const env = nodeEnvironment();
85
+ const baseURL = options.baseURL ?? env['CRTR_BASE_URL'];
86
+ this.baseURL = baseURL;
87
+ this.socketPath = undefined;
88
+ if (options.baseURL !== undefined && options.socketPath !== undefined) {
89
+ throw new TypeError('Crouter accepts either baseURL or socketPath, not both');
90
+ }
91
+ if (baseURL !== undefined && options.autostart === true) {
92
+ throw new CrouterError('autostart is only valid for the local socket transport');
93
+ }
94
+ if (options.token !== undefined && options.tokenSource !== undefined)
95
+ throw new CrouterError('token and tokenSource are mutually exclusive');
96
+ if (options.tokenSource && baseURL === undefined)
97
+ throw new CrouterError('tokenSource requires baseURL');
98
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
99
+ const token = options.tokenSource ? undefined : options.token ?? env['CRTRD_TOKEN'];
100
+ this.bearerToken = token;
101
+ this.tokenSource = options.tokenSource;
102
+ this.onUnauthorized = options.onUnauthorized;
103
+ const headers = {
104
+ ...options.defaultHeaders,
105
+ ...options.headers,
106
+ ...(token === undefined ? {} : { Authorization: `Bearer ${token}` }),
107
+ };
108
+ const baseFetch = requireCurrentRuntime(options.fetch ?? (baseURL === undefined && isNodeRuntime() ? localSocketFetch(options.socketPath ?? env['CRTR_SOCKET']) : undefined));
109
+ this.eventFetch = options.tokenSource ? (input, init) => {
110
+ const url = new URL(input instanceof Request ? input.url : String(input));
111
+ if (url.pathname === '/v1/health')
112
+ return baseFetch(input, init);
113
+ return baseFetch(input, {
114
+ ...init, headers: { ...Object.fromEntries(new Headers(init?.headers)), Authorization: `Bearer ${this.connectionToken}` },
115
+ });
116
+ } : baseFetch;
117
+ if (baseURL !== undefined) {
118
+ this.client = new CrtrClient({
119
+ baseUrl: baseURL,
120
+ fetch: this.eventFetch,
121
+ headers,
122
+ timeoutMs: this.timeout,
123
+ maxRetries: options.maxRetries,
124
+ });
125
+ }
126
+ else {
127
+ if (!isNodeRuntime()) {
128
+ if (options.socketPath !== undefined)
129
+ throw new CrouterError('socketPath is only available in Node');
130
+ this.socketPath = undefined;
131
+ this.client = new CrtrClient({
132
+ baseUrl: 'http://localhost',
133
+ fetch: baseFetch,
134
+ headers,
135
+ timeoutMs: this.timeout,
136
+ maxRetries: options.maxRetries,
137
+ });
138
+ }
139
+ else {
140
+ const socketPath = options.socketPath ?? env['CRTR_SOCKET'];
141
+ this.socketPath = socketPath;
142
+ const autostart = options.autostart ?? true;
143
+ this.client = new CrtrClient({
144
+ baseUrl: 'http://localhost',
145
+ fetch: baseFetch,
146
+ headers,
147
+ timeoutMs: this.timeout,
148
+ maxRetries: options.maxRetries,
149
+ autostart,
150
+ onColdSocket: autostart ? startDaemon : undefined,
151
+ });
152
+ }
153
+ }
154
+ const request = (method, path, body, options) => this.request(method, path, body, options);
155
+ const nodes = new NodesResource(request);
156
+ const phaseOneNodes = {
157
+ create: (params, options) => this.call(() => this.client.request('POST', routes.nodes(), serializeCreate(params), options)),
158
+ retrieve: async (id, options) => {
159
+ const validId = nodeId(id);
160
+ return this.call(() => this.client.request('GET', routes.node(validId), undefined, options));
161
+ },
162
+ list: (query, options) => this.call(() => this.client.request('GET', withQuery(routes.nodes(), query), undefined, options)),
163
+ outcome: (id, options) => this.outcome(id, options),
164
+ waitForOutcome: (id, options) => this.waitForOutcome(id, options),
165
+ createAndWait: (params, options) => this.createAndWait(params, options),
166
+ parse: (params, options) => this.parse(params, options),
167
+ message: async (id, body, options) => {
168
+ const validId = nodeId(id);
169
+ return this.call(() => this.client.request('POST', routes.nodeMessages(validId), messageBody(body), options));
170
+ },
171
+ cancel: async (id, body, options) => {
172
+ const validId = nodeId(id);
173
+ return this.call(() => this.client.request('POST', routes.nodeClose(validId), body ?? {}, options));
174
+ },
175
+ interrupt: async (id, options) => {
176
+ const validId = nodeId(id);
177
+ return this.call(() => this.client.request('POST', routes.nodeInterrupt(validId), {}, options));
178
+ },
179
+ stream: (params, options) => this.stream(params, options),
180
+ events: (id, options) => this.events(id, options),
181
+ reports: {
182
+ list: async (id, query, options) => {
183
+ const validId = nodeId(id);
184
+ return this.call(() => this.client.request('GET', withQuery(routes.nodeReports(validId), query), undefined, options));
185
+ },
186
+ },
187
+ };
188
+ this.nodes = Object.assign(nodes, phaseOneNodes);
189
+ this.bash = new Bash(request, this.timeout);
190
+ this.canvas = new Canvas(request);
191
+ this.crons = new Crons(request);
192
+ this.structuredRequests = new StructuredRequests(request);
193
+ this.customObjects = new CustomObjects(request, (path, options) => this.openCustomDeliveries(path, options));
194
+ this.human = new Human(request);
195
+ this.models = new Models(request);
196
+ this.profiles = {
197
+ ensure: async (name, params, options) => {
198
+ const validName = pathSegment(name, 'profile name');
199
+ return this.call(() => this.client.request('PUT', routes.profile(validName), params ?? {}, options));
200
+ },
201
+ retrieve: async (name, options) => {
202
+ const validName = pathSegment(name, 'profile name');
203
+ return this.call(() => this.client.request('GET', routes.profile(validName), undefined, options));
204
+ },
205
+ list: (options) => this.call(() => this.client.request('GET', routes.profiles(), undefined, options)),
206
+ };
207
+ this.system = {
208
+ status: (options) => this.call(() => this.client.request('GET', routes.status(), undefined, options)),
209
+ health: (options) => this.call(() => this.client.request('GET', routes.healthz(), undefined, options)),
210
+ };
211
+ this.auth = {
212
+ status: (params, options) => this.authStatus(params, options),
213
+ };
214
+ this.files = new Files(request, (path, options) => this.openDownload(path, options));
215
+ this.uploads = new Uploads(request);
216
+ this.attachments = new Attachments(request);
217
+ this.shares = new Shares(request);
218
+ this.questions = new Questions(request);
219
+ this.memory = new Memory(request);
220
+ this.runs = new Runs(request, (id, after, signal) => this.openRunEvents(id, after, signal), this.questions);
221
+ this.providers = new Providers(request);
222
+ this.grant = { get: (options = {}) => {
223
+ const { wait, since, ...rest } = options;
224
+ const held = heldReadOptions(wait, rest);
225
+ return this.request('GET', withQuery('/v1/grant', { wait, since }), undefined, held);
226
+ } };
227
+ this.health = { get: (options) => this.request('GET', '/v1/health', undefined, options) };
228
+ }
229
+ /** Establish NDJSON through the JSON client's cold-socket/retry transport; leave the live body unread. */
230
+ async openCustomDeliveries(path, options) {
231
+ const response = await this.call(() => this.client.rawResponse('GET', path, {
232
+ headers: { accept: 'application/x-ndjson', ...options.headers }, signal: options.signal,
233
+ timeout: 0, maxRetries: options.maxRetries,
234
+ }));
235
+ if (!response.ok)
236
+ throw mapError(await responseError(response));
237
+ return response;
238
+ }
239
+ openRunEvents(id, after, signal) {
240
+ const baseURL = this.baseURL;
241
+ if (!baseURL)
242
+ throw new CrouterError('Run events require an app listener baseURL');
243
+ return this.call(() => this.eventFetch(`${baseURL.replace(/\/$/, '')}/v1/runs/${encodeURIComponent(id)}/events?after=${after}`, {
244
+ headers: { 'accept': 'text/event-stream', ...(this.bearerToken ? { Authorization: `Bearer ${this.bearerToken}` } : {}),
245
+ ...(after ? { 'Last-Event-ID': String(after) } : {}) }, signal,
246
+ }));
247
+ }
248
+ /** `GET /v1/files/download` as a raw fetch: bytes are not JSON, so it bypasses the JSON client but
249
+ * still goes through `call()`, which supplies the connection token and refreshes it once on a 401. */
250
+ async openDownload(path, options) {
251
+ const baseURL = this.baseURL;
252
+ if (!baseURL)
253
+ throw new CrouterError('File downloads require an app listener baseURL');
254
+ if (typeof path !== 'string' || path === '')
255
+ throw new TypeError('path is required');
256
+ const url = `${baseURL.replace(/\/$/, '')}/v1/files/download?${new URLSearchParams({ path })}`;
257
+ const response = await this.call(async () => {
258
+ try {
259
+ return await this.eventFetch(url, {
260
+ headers: { ...(this.bearerToken ? { Authorization: `Bearer ${this.bearerToken}` } : {}), ...(options.range ? { Range: options.range } : {}) },
261
+ ...(options.signal ? { signal: options.signal } : {}),
262
+ });
263
+ }
264
+ catch (error) {
265
+ if (options.signal?.aborted)
266
+ throw new APIUserAbortError(0, 'request_aborted', 'download aborted by caller');
267
+ throw new APIConnectionError(0, 'connection_error', error instanceof Error ? error.message : String(error));
268
+ }
269
+ });
270
+ if (response.ok)
271
+ return response;
272
+ throw mapError(await responseError(response));
273
+ }
274
+ /** Sends one request to any daemon route, for routes the namespaces above do not wrap. */
275
+ request(method, path, body, options) {
276
+ return this.call(() => this.client.request(method, path, body, options), path !== '/v1/health');
277
+ }
278
+ async authStatus(params = {}, options) {
279
+ if (this.baseURL === undefined && (!isNodeRuntime() || !await socketIsReachable(this.socketPath))) {
280
+ return connectStatus('Install crtr, run `crtr sys connect`, and paste the base URL and token it prints.');
281
+ }
282
+ let daemon;
283
+ try {
284
+ daemon = await this.call(() => this.client.request('GET', routes.status(), undefined, options));
285
+ }
286
+ catch (error) {
287
+ if (!(error instanceof APIError))
288
+ throw error;
289
+ const mapped = mapError(error);
290
+ if (mapped instanceof AuthenticationError) {
291
+ return connectStatus('The daemon rejected the saved token. Run `crtr sys connect` and paste the new base URL and token it prints.');
292
+ }
293
+ if (mapped instanceof APIConnectionError) {
294
+ const instruction = this.baseURL === undefined
295
+ ? 'Run `crtr sys connect` and paste the base URL and token it prints.'
296
+ : `The daemon at ${this.baseURL} could not be reached. Run \`crtr sys connect\` and paste the new base URL and token it prints.`;
297
+ return connectStatus(instruction);
298
+ }
299
+ throw mapped;
300
+ }
301
+ const daemonStatus = {
302
+ base_url: this.baseURL ?? daemon.listeners.socket_path,
303
+ runtime_version: daemon.runtime_version,
304
+ api_version: daemon.api_version,
305
+ startup_phase: daemon.startup_phase,
306
+ tcp: daemon.listeners.tcp,
307
+ };
308
+ if (daemon.startup_phase !== 'ready') {
309
+ return { connected: true, daemon: daemonStatus, model: null, next_step: null, instructions: null };
310
+ }
311
+ const model = await this.call(() => this.client.request('GET', withQuery(routes.modelAuthReadiness(), params), undefined, options));
312
+ if (model.credential !== 'ready') {
313
+ return {
314
+ connected: true,
315
+ daemon: daemonStatus,
316
+ model,
317
+ next_step: 'login',
318
+ instructions: `Run \`${connectCommand(params)}\` to log in to ${model.provider}.`,
319
+ };
320
+ }
321
+ return { connected: true, daemon: daemonStatus, model, next_step: null, instructions: null };
322
+ }
323
+ stream(params, options) {
324
+ const node = this.nodes.create(params, options);
325
+ const { timeout: _timeout, ...streamOptions } = options ?? {};
326
+ return new NodeStream(node, async (signal) => {
327
+ const created = await node;
328
+ return this.call(() => this.client.getNodeEvents(created.node_id, {}, { ...streamOptions, signal }));
329
+ }, options?.signal);
330
+ }
331
+ events(id, options = {}) {
332
+ const { after, ...streamOptions } = options;
333
+ const node = this.nodes.retrieve(id, streamOptions);
334
+ return new NodeStream(node, async (signal) => {
335
+ const existing = await node;
336
+ return this.call(() => this.client.getNodeEvents(existing.node_id, { after }, { ...streamOptions, signal }));
337
+ }, options.signal);
338
+ }
339
+ async outcome(id, options = {}) {
340
+ const validId = nodeId(id);
341
+ const { wait, ...requestOptions } = options;
342
+ if (wait !== undefined && (!Number.isInteger(wait) || wait < 0 || wait > POLL_WAIT_SECONDS)) {
343
+ throw new RangeError('wait must be an integer between 0 and 25');
344
+ }
345
+ const timeout = wait === undefined ? requestOptions.timeout : wait * 1_000 + (requestOptions.timeout ?? this.timeout);
346
+ return this.call(() => this.client.request('GET', withQuery(routes.nodeOutcome(validId), { wait }), undefined, { ...requestOptions, timeout }));
347
+ }
348
+ async waitForOutcome(id, options) {
349
+ for (;;) {
350
+ const response = await this.outcome(id, { ...options, wait: POLL_WAIT_SECONDS });
351
+ if (response.state === 'settled' && response.outcome !== null)
352
+ return response.outcome;
353
+ }
354
+ }
355
+ async createAndWait(params, options) {
356
+ const node = await this.nodes.create(params, options);
357
+ return this.waitForOutcome(node.node_id, options);
358
+ }
359
+ async parse(params, options) {
360
+ const outcome = await this.createAndWait({
361
+ ...params,
362
+ ...(params.parent == null && params.root_lifecycle === undefined ? { root_lifecycle: 'terminal' } : {}),
363
+ }, options);
364
+ if (outcome.kind === 'result') {
365
+ return { ...outcome, output_parsed: outcome.structured_result };
366
+ }
367
+ return { ...outcome, output_parsed: null };
368
+ }
369
+ async call(operation, authenticate = true) {
370
+ for (let attempt = 0;; attempt++) {
371
+ // A token refresh refused by the directory surfaces with its status class, like any other API error.
372
+ if (authenticate && this.tokenSource)
373
+ this.connectionToken = await this.tokenSource().catch(throwMapped);
374
+ let result;
375
+ try {
376
+ result = await operation();
377
+ }
378
+ catch (error) {
379
+ if (authenticate && attempt === 0 && error instanceof APIError && error.status === 401 && this.onUnauthorized) {
380
+ await this.onUnauthorized().catch(throwMapped);
381
+ continue;
382
+ }
383
+ throw mapError(error);
384
+ }
385
+ if (authenticate && attempt === 0 && result instanceof Response && result.status === 401 && this.onUnauthorized) {
386
+ await result.body?.cancel();
387
+ await this.onUnauthorized().catch(throwMapped);
388
+ continue;
389
+ }
390
+ return result;
391
+ }
392
+ }
393
+ }
394
+ /** Rethrow an `APIError` as its status class; any other error from caller-supplied token hooks passes through unchanged. */
395
+ function throwMapped(error) {
396
+ throw error instanceof APIError ? mapError(error) : error;
397
+ }
398
+ /** A non-2xx answer's JSON envelope as an `APIError`; a body that is not the envelope keeps the status. */
399
+ async function responseError(response) {
400
+ let error;
401
+ try {
402
+ error = (await response.json()).error;
403
+ }
404
+ catch {
405
+ error = undefined;
406
+ }
407
+ return new APIError(response.status, error?.code ?? 'invalid_response', error?.message ?? `HTTP ${response.status}`, error?.details, response.headers, error);
408
+ }
409
+ function serializeCreate(params) {
410
+ const { output_schema, ...rest } = params;
411
+ const independentRoot = rest.parent === undefined && rest.root === undefined ? { root: true } : {};
412
+ if (output_schema === undefined)
413
+ return { ...independentRoot, ...rest };
414
+ return {
415
+ ...independentRoot,
416
+ ...rest,
417
+ output_schema: typeof output_schema === 'string' ? output_schema : JSON.stringify(resolveJsonSchema(output_schema)),
418
+ };
419
+ }
420
+ function messageBody(body) {
421
+ if (typeof body === 'string')
422
+ return { body };
423
+ const { from: _from, ...request } = body;
424
+ return request;
425
+ }
426
+ function withQuery(path, query) {
427
+ if (query === undefined)
428
+ return path;
429
+ const search = new URLSearchParams();
430
+ for (const [key, value] of Object.entries(query)) {
431
+ if (value !== undefined)
432
+ search.set(key, String(value));
433
+ }
434
+ const suffix = search.toString();
435
+ return suffix === '' ? path : `${path}?${suffix}`;
436
+ }
437
+ function connectStatus(instructions) {
438
+ return { connected: false, daemon: null, model: null, next_step: 'connect', instructions };
439
+ }
440
+ function connectCommand(params) {
441
+ const selectors = [
442
+ ['profile', params.profile],
443
+ ['cwd', params.cwd],
444
+ ['kind', params.kind],
445
+ ['model', params.model],
446
+ ].flatMap(([name, value]) => value === undefined ? [] : [`--${name}`, shellArgument(value)]);
447
+ return ['crtr', 'sys', 'connect', ...selectors].join(' ');
448
+ }
449
+ function shellArgument(value) {
450
+ if (/^[A-Za-z0-9_./:-]+$/.test(value))
451
+ return value;
452
+ return `'${value.replaceAll("'", String.fromCharCode(39, 34, 39, 34, 39))}'`;
453
+ }
454
+ async function socketIsReachable(socketPath) {
455
+ const load = new Function('specifier', 'return import(specifier)');
456
+ const [{ createConnection }, { defaultSocketPath }] = await Promise.all([
457
+ load('node:net'),
458
+ load('@north-light/api/node'),
459
+ ]);
460
+ const path = socketPath ?? defaultSocketPath();
461
+ return new Promise((resolve) => {
462
+ const socket = createConnection({ path });
463
+ const finish = (reachable) => {
464
+ socket.destroy();
465
+ resolve(reachable);
466
+ };
467
+ socket.once('connect', () => finish(true));
468
+ socket.once('error', () => finish(false));
469
+ socket.setTimeout(100, () => finish(false));
470
+ });
471
+ }
472
+ function nodeEnvironment() {
473
+ return isNodeRuntime() ? process.env : {};
474
+ }
475
+ function isNodeRuntime() {
476
+ return typeof process !== 'undefined' && process.versions?.node !== undefined;
477
+ }
478
+ /**
479
+ * Latest only (P2): the SDK accepts a runtime at or above its own release (`SDK_VERSION`) and nothing older, so its
480
+ * types never describe a field an older runtime does not send. Every runtime response names its release in
481
+ * `Crouter-Runtime-Version`; a successful answer from an older runtime, or one that names no release (every runtime
482
+ * before the header existed), is replaced by a 426 `runtime_version_unsupported` envelope before anything reads it,
483
+ * which the client raises as `RuntimeVersionError`. A refusal without the header passes through unchanged: it can come
484
+ * from a router or proxy in front of the runtime, and it carries no runtime data. `/healthz`, the unversioned
485
+ * substrate probe the cold-start wait polls, is exempt.
486
+ */
487
+ function requireCurrentRuntime(inner) {
488
+ const checked = async (input, init) => {
489
+ const url = new URL(input instanceof Request ? input.url : String(input), 'http://localhost');
490
+ // Say which runtime this SDK accepts, so the router in front of a hosted runtime, or the runtime itself, refuses
491
+ // an older one before any handler runs. `/healthz` is exempt from both checks.
492
+ let sentInit = init;
493
+ if (url.pathname !== '/healthz') {
494
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
495
+ headers.set(MINIMUM_RUNTIME_VERSION_HEADER, SDK_VERSION);
496
+ sentInit = { ...init, headers };
497
+ }
498
+ const response = await (inner ?? globalThis.fetch)(input, sentInit);
499
+ if (url.pathname === '/healthz')
500
+ return response;
501
+ const runtimeVersion = response.headers.get(RUNTIME_VERSION_HEADER);
502
+ if (runtimeVersion === null ? !response.ok : compareReleaseVersions(runtimeVersion, SDK_VERSION) >= 0)
503
+ return response;
504
+ await response.body?.cancel();
505
+ const message = runtimeVersion === null
506
+ ? `The runtime did not name its version, so it predates the runtime version header; @north-light/sdk ${SDK_VERSION} requires a runtime at ${SDK_VERSION} or later. Upgrade the runtime, or pin @north-light/sdk to the runtime's release.`
507
+ : `The runtime is ${runtimeVersion}; @north-light/sdk ${SDK_VERSION} requires a runtime at ${SDK_VERSION} or later. Upgrade the runtime, or pin @north-light/sdk to ${runtimeVersion}.`;
508
+ return new Response(JSON.stringify({ error: {
509
+ code: 'runtime_version_unsupported', message, type: 'invalid_request_error', retryable: false,
510
+ details: { sdk_version: SDK_VERSION, runtime_version: runtimeVersion },
511
+ } }), { status: 426, headers: { 'content-type': 'application/json' } });
512
+ };
513
+ if (inner !== undefined && inner[LOCAL_SOCKET_FETCH] === true) {
514
+ checked[LOCAL_SOCKET_FETCH] = true;
515
+ }
516
+ return checked;
517
+ }
518
+ function localSocketFetch(socketPath) {
519
+ const load = new Function('specifier', 'return import(specifier)');
520
+ let fetchPromise;
521
+ const localFetch = (input, init) => {
522
+ fetchPromise ??= load('@north-light/api/node').then(({ socketFetch, defaultSocketPath }) => socketFetch(socketPath ?? defaultSocketPath()));
523
+ return fetchPromise.then((fetch) => fetch(input, init));
524
+ };
525
+ localFetch[LOCAL_SOCKET_FETCH] = true;
526
+ return localFetch;
527
+ }
528
+ async function startDaemon() {
529
+ const load = new Function('specifier', 'return import(specifier)');
530
+ const { spawn } = await load('node:child_process');
531
+ await new Promise((resolve, reject) => {
532
+ const child = spawn('crtr', ['sys', 'daemon', 'start'], { stdio: ['ignore', 'ignore', 'pipe'] });
533
+ let stderr = '';
534
+ child.stderr?.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
535
+ child.once('error', (error) => reject(new CrouterError(`could not run \`crtr sys daemon start\`: ${error.message}`)));
536
+ child.once('exit', (code) => {
537
+ if (code === 0)
538
+ resolve();
539
+ else
540
+ reject(new CrouterError(`\`crtr sys daemon start\` exited ${String(code)}${stderr === '' ? '' : `: ${stderr.trim()}`}`));
541
+ });
542
+ });
543
+ }
@@ -0,0 +1,7 @@
1
+ export { errorCodes } from '@north-light/api';
2
+ export type { ErrorCode, ErrorEnvelope, ErrorOrigin, ErrorType } from '@north-light/api';
3
+ export { mapError } from './errors.js';
4
+ /** Codes the SDK raises itself rather than reading from a daemon or directory response. */
5
+ export declare const sdkErrorCodes: readonly ["crouter_error", "id_token_invalid", "oauth_nonce_missing", "oauth_state_mismatch", "oauth_exchange_mismatch", "not_connected", "refusal_handler_reentry", "run_idle", "waiting_on_user", "request_aborted", "request_timeout", "connection_error", "transport_error", "daemon_unavailable", "daemon_request_interrupted", "daemon_health_unavailable", "runtime_version_unsupported", "invalid_response", "stream_error", "stream_ended"];
6
+ /** A code the SDK raises itself. An `APIError`'s `code` is one of these or an `ErrorCode` from the contract. */
7
+ export type SdkErrorCode = typeof sdkErrorCodes[number];
@@ -0,0 +1,41 @@
1
+ // The API contract owns the daemon's table; the SDK adds the codes it raises itself.
2
+ export { errorCodes } from '@north-light/api';
3
+ export { mapError } from './errors.js';
4
+ /** Codes the SDK raises itself rather than reading from a daemon or directory response. */
5
+ export const sdkErrorCodes = [
6
+ /** A `CrouterError` configuration or protocol failure with no more specific code. */
7
+ 'crouter_error',
8
+ /** An ID token was missing, failed verification, or its nonce did not match. */
9
+ 'id_token_invalid',
10
+ /** `exchangeIdentity` was called without the nonce `authorizeUrl` returned. */
11
+ 'oauth_nonce_missing',
12
+ /** The callback's `state` was missing or differed from the saved one. */
13
+ 'oauth_state_mismatch',
14
+ /** `exchangeCode` got a sign-in-only answer, or `exchangeIdentity` got one carrying a connection. */
15
+ 'oauth_exchange_mismatch',
16
+ /** `oauth.client(userId)` or `refresh` found no stored connection for the person: they must sign in again. */
17
+ 'not_connected',
18
+ /** `onRefusal` called `oauth.client` (or `refresh`) for the user whose dead connection it is handling. */
19
+ 'refusal_handler_reentry',
20
+ /** `runs.parse`: the run stopped without settling. */
21
+ 'run_idle',
22
+ /** `runs.parse`: the run waits on a question and no `onQuestion` was given. */
23
+ 'waiting_on_user',
24
+ /** The caller's `AbortSignal` fired (`APIUserAbortError`). */
25
+ 'request_aborted',
26
+ /** The request exceeded its timeout (`APIConnectionTimeoutError`). */
27
+ 'request_timeout',
28
+ /** The daemon could not be reached, or the connection failed (`APIConnectionError`). */
29
+ 'connection_error',
30
+ 'transport_error',
31
+ 'daemon_unavailable',
32
+ 'daemon_request_interrupted',
33
+ 'daemon_health_unavailable',
34
+ /** The runtime is older than this SDK, or sent no `Crouter-Runtime-Version` (`RuntimeVersionError`). */
35
+ 'runtime_version_unsupported',
36
+ /** A response the SDK could not read as the API's JSON or error envelope. */
37
+ 'invalid_response',
38
+ /** An event stream failed, or ended before the reply or node it carried did. */
39
+ 'stream_error',
40
+ 'stream_ended',
41
+ ];
@@ -0,0 +1,57 @@
1
+ import { APIError } from '@north-light/api';
2
+ import type { SdkErrorCode } from './error-codes.js';
3
+ export { APIError };
4
+ /** An SDK configuration or protocol error, raised by the SDK itself. Its `code` is an `SdkErrorCode`. */
5
+ export declare class CrouterError extends APIError {
6
+ readonly code: SdkErrorCode;
7
+ constructor(message: string, code?: SdkErrorCode);
8
+ }
9
+ /** The runtime is older than this SDK (or names no version at all), so the SDK's types do not describe what it
10
+ * sends. Raised before the SDK reads the response; never retryable. Upgrade the runtime (or pin the SDK to the
11
+ * runtime's release). `code` is `runtime_version_unsupported`, `status` 426. */
12
+ export declare class RuntimeVersionError extends APIError {
13
+ /** The oldest runtime this SDK accepts: its own release version. */
14
+ get sdkVersion(): string;
15
+ /** The version the runtime named in `Crouter-Runtime-Version`, or `null` when it sent none (a runtime from before the header existed). */
16
+ get runtimeVersion(): string | null;
17
+ }
18
+ /** `RuntimeVersionError.details`. */
19
+ export interface RuntimeVersionDetails {
20
+ sdk_version: string;
21
+ runtime_version: string | null;
22
+ }
23
+ /** The daemon rejected the request: HTTP 400. */
24
+ export declare class BadRequestError extends APIError {
25
+ }
26
+ /** The token was missing or wrong: HTTP 401. */
27
+ export declare class AuthenticationError extends APIError {
28
+ }
29
+ /** The caller is not allowed to do this: HTTP 403. */
30
+ export declare class PermissionDeniedError extends APIError {
31
+ }
32
+ /** No such node, profile, cron, or other resource: HTTP 404. */
33
+ export declare class NotFoundError extends APIError {
34
+ }
35
+ /** The resource is in a state that forbids the request: HTTP 409. */
36
+ export declare class ConflictError extends APIError {
37
+ }
38
+ /** The request was well-formed but unusable, or too large: HTTP 413 and 422. */
39
+ export declare class UnprocessableEntityError extends APIError {
40
+ }
41
+ /** Too many requests: HTTP 429. */
42
+ export declare class RateLimitError extends APIError {
43
+ }
44
+ /** The daemon failed while handling the request: HTTP 500 and above. */
45
+ export declare class InternalServerError extends APIError {
46
+ }
47
+ /** The daemon could not be reached, or the connection failed mid-request. */
48
+ export declare class APIConnectionError extends APIError {
49
+ }
50
+ /** The request exceeded its timeout before the daemon answered. */
51
+ export declare class APIConnectionTimeoutError extends APIConnectionError {
52
+ }
53
+ /** The caller's `AbortSignal` fired. */
54
+ export declare class APIUserAbortError extends APIError {
55
+ }
56
+ /** Maps a transport failure or a plain `APIError` onto the specific class for its status and code. */
57
+ export declare function mapError(error: unknown): APIError;