@crouter/sdk 0.3.377

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