@north-light/sdk 0.0.0 → 0.3.415
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/LICENSE +18 -0
- package/README.md +187 -2
- package/dist/client.d.ts +176 -0
- package/dist/client.js +582 -0
- package/dist/crtr.d.ts +24 -0
- package/dist/crtr.js +50 -0
- package/dist/env-command.d.ts +15 -0
- package/dist/env-command.js +138 -0
- package/dist/error-codes.d.ts +7 -0
- package/dist/error-codes.js +43 -0
- package/dist/errors.d.ts +72 -0
- package/dist/errors.js +101 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +17 -0
- package/dist/keygen-cli.d.ts +2 -0
- package/dist/keygen-cli.js +11 -0
- package/dist/keygen-command.d.ts +6 -0
- package/dist/keygen-command.js +131 -0
- package/dist/oauth/index.d.ts +235 -0
- package/dist/oauth/index.js +621 -0
- package/dist/oauth/keygen.d.ts +18 -0
- package/dist/oauth/keygen.js +46 -0
- package/dist/oauth/refusal.d.ts +32 -0
- package/dist/oauth/refusal.js +17 -0
- package/dist/resources/activity.d.ts +22 -0
- package/dist/resources/activity.js +43 -0
- package/dist/resources/attachments.d.ts +15 -0
- package/dist/resources/attachments.js +13 -0
- package/dist/resources/bash.d.ts +9 -0
- package/dist/resources/bash.js +15 -0
- package/dist/resources/canvas/history.d.ts +11 -0
- package/dist/resources/canvas/history.js +20 -0
- package/dist/resources/canvas.d.ts +19 -0
- package/dist/resources/canvas.js +42 -0
- package/dist/resources/crons.d.ts +15 -0
- package/dist/resources/crons.js +33 -0
- package/dist/resources/custom-objects.d.ts +20 -0
- package/dist/resources/custom-objects.js +84 -0
- package/dist/resources/files.d.ts +34 -0
- package/dist/resources/files.js +34 -0
- package/dist/resources/follower.d.ts +71 -0
- package/dist/resources/follower.js +202 -0
- package/dist/resources/forward.d.ts +7 -0
- package/dist/resources/forward.js +67 -0
- package/dist/resources/grant-watcher.d.ts +57 -0
- package/dist/resources/grant-watcher.js +100 -0
- package/dist/resources/human/inbox.d.ts +14 -0
- package/dist/resources/human/inbox.js +30 -0
- package/dist/resources/human/requests.d.ts +13 -0
- package/dist/resources/human/requests.js +26 -0
- package/dist/resources/human.d.ts +8 -0
- package/dist/resources/human.js +10 -0
- package/dist/resources/identifiers.d.ts +8 -0
- package/dist/resources/identifiers.js +33 -0
- package/dist/resources/memory.d.ts +69 -0
- package/dist/resources/memory.js +30 -0
- package/dist/resources/models/config.d.ts +8 -0
- package/dist/resources/models/config.js +9 -0
- package/dist/resources/models/credentials.d.ts +10 -0
- package/dist/resources/models/credentials.js +17 -0
- package/dist/resources/models.d.ts +8 -0
- package/dist/resources/models.js +10 -0
- package/dist/resources/node-stream.d.ts +34 -0
- package/dist/resources/node-stream.js +176 -0
- package/dist/resources/nodes/jobs.d.ts +9 -0
- package/dist/resources/nodes/jobs.js +14 -0
- package/dist/resources/nodes/result.d.ts +8 -0
- package/dist/resources/nodes/result.js +11 -0
- package/dist/resources/nodes/worktree.d.ts +9 -0
- package/dist/resources/nodes/worktree.js +14 -0
- package/dist/resources/nodes.d.ts +23 -0
- package/dist/resources/nodes.js +47 -0
- package/dist/resources/providers.d.ts +51 -0
- package/dist/resources/providers.js +15 -0
- package/dist/resources/questions.d.ts +49 -0
- package/dist/resources/questions.js +21 -0
- package/dist/resources/request.d.ts +12 -0
- package/dist/resources/request.js +50 -0
- package/dist/resources/run-notice.d.ts +34 -0
- package/dist/resources/run-notice.js +67 -0
- package/dist/resources/run-reply.d.ts +79 -0
- package/dist/resources/run-reply.js +93 -0
- package/dist/resources/run-stream.d.ts +65 -0
- package/dist/resources/run-stream.js +243 -0
- package/dist/resources/run-watcher.d.ts +98 -0
- package/dist/resources/run-watcher.js +162 -0
- package/dist/resources/runs.d.ts +199 -0
- package/dist/resources/runs.js +228 -0
- package/dist/resources/shares.d.ts +40 -0
- package/dist/resources/shares.js +19 -0
- package/dist/resources/structured-requests.d.ts +14 -0
- package/dist/resources/structured-requests.js +19 -0
- package/dist/resources/uploads.d.ts +27 -0
- package/dist/resources/uploads.js +11 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +10 -0
- package/dist/stores/postgres.d.ts +32 -0
- package/dist/stores/postgres.js +92 -0
- package/dist/testing/clock.d.ts +7 -0
- package/dist/testing/clock.js +12 -0
- package/dist/testing/cloud.d.ts +228 -0
- package/dist/testing/cloud.js +914 -0
- package/dist/testing/conformance/index.d.ts +2 -0
- package/dist/testing/conformance/index.js +3 -0
- package/dist/testing/conformance/keyed-write.d.ts +43 -0
- package/dist/testing/conformance/keyed-write.js +124 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/notices.d.ts +76 -0
- package/dist/testing/notices.js +209 -0
- package/dist/testing/run.d.ts +174 -0
- package/dist/testing/run.js +429 -0
- package/dist/testing/users.d.ts +127 -0
- package/dist/testing/users.js +78 -0
- package/dist/testing/wire.d.ts +62 -0
- package/dist/testing/wire.js +148 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/package.json +60 -5
package/dist/client.js
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
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 { deadline, heldReadOptions, withinDeadline } 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
|
+
/** The caller's month-to-date spend (`usage.get`): user-paid model and provider usage, app-paid usage, the grant's
|
|
75
|
+
* `spend_cap`, and `reset_at`. Once user-paid spend reaches `spend_cap`, user-paid model calls answer `cap_exhausted`
|
|
76
|
+
* (a `RateLimitError`, status 402, `userAction: 'raise_cap'`) until `reset_at`. `month` is `YYYY-MM` (UTC; default: current). */
|
|
77
|
+
usage;
|
|
78
|
+
eventFetch;
|
|
79
|
+
bearerToken;
|
|
80
|
+
connectionToken;
|
|
81
|
+
tokenSource;
|
|
82
|
+
onUnauthorized;
|
|
83
|
+
client;
|
|
84
|
+
timeout;
|
|
85
|
+
baseURL;
|
|
86
|
+
socketPath;
|
|
87
|
+
constructor(options = {}) {
|
|
88
|
+
const env = nodeEnvironment();
|
|
89
|
+
const baseURL = options.baseURL ?? env['CRTR_BASE_URL'];
|
|
90
|
+
this.baseURL = baseURL;
|
|
91
|
+
this.socketPath = undefined;
|
|
92
|
+
if (options.baseURL !== undefined && options.socketPath !== undefined) {
|
|
93
|
+
throw new TypeError('Crouter accepts either baseURL or socketPath, not both');
|
|
94
|
+
}
|
|
95
|
+
if (baseURL !== undefined && options.autostart === true) {
|
|
96
|
+
throw new CrouterError('autostart is only valid for the local socket transport');
|
|
97
|
+
}
|
|
98
|
+
if (options.token !== undefined && options.tokenSource !== undefined)
|
|
99
|
+
throw new CrouterError('token and tokenSource are mutually exclusive');
|
|
100
|
+
if (options.tokenSource && baseURL === undefined)
|
|
101
|
+
throw new CrouterError('tokenSource requires baseURL');
|
|
102
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
103
|
+
const token = options.tokenSource ? undefined : options.token ?? env['CRTRD_TOKEN'];
|
|
104
|
+
this.bearerToken = token;
|
|
105
|
+
this.tokenSource = options.tokenSource;
|
|
106
|
+
this.onUnauthorized = options.onUnauthorized;
|
|
107
|
+
const headers = {
|
|
108
|
+
...options.defaultHeaders,
|
|
109
|
+
...options.headers,
|
|
110
|
+
...(token === undefined ? {} : { Authorization: `Bearer ${token}` }),
|
|
111
|
+
};
|
|
112
|
+
const baseFetch = requireCurrentRuntime(options.fetch ?? (baseURL === undefined && isNodeRuntime() ? localSocketFetch(options.socketPath ?? env['CRTR_SOCKET']) : undefined));
|
|
113
|
+
this.eventFetch = options.tokenSource ? (input, init) => {
|
|
114
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
115
|
+
if (url.pathname === '/v1/health')
|
|
116
|
+
return baseFetch(input, init);
|
|
117
|
+
return baseFetch(input, {
|
|
118
|
+
...init, headers: { ...Object.fromEntries(new Headers(init?.headers)), Authorization: `Bearer ${this.connectionToken}` },
|
|
119
|
+
});
|
|
120
|
+
} : baseFetch;
|
|
121
|
+
if (baseURL !== undefined) {
|
|
122
|
+
this.client = new CrtrClient({
|
|
123
|
+
baseUrl: baseURL,
|
|
124
|
+
fetch: this.eventFetch,
|
|
125
|
+
headers,
|
|
126
|
+
timeoutMs: this.timeout,
|
|
127
|
+
maxRetries: options.maxRetries,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
if (!isNodeRuntime()) {
|
|
132
|
+
if (options.socketPath !== undefined)
|
|
133
|
+
throw new CrouterError('socketPath is only available in Node');
|
|
134
|
+
this.socketPath = undefined;
|
|
135
|
+
this.client = new CrtrClient({
|
|
136
|
+
baseUrl: 'http://localhost',
|
|
137
|
+
fetch: baseFetch,
|
|
138
|
+
headers,
|
|
139
|
+
timeoutMs: this.timeout,
|
|
140
|
+
maxRetries: options.maxRetries,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const socketPath = options.socketPath ?? env['CRTR_SOCKET'];
|
|
145
|
+
this.socketPath = socketPath;
|
|
146
|
+
const autostart = options.autostart ?? true;
|
|
147
|
+
this.client = new CrtrClient({
|
|
148
|
+
baseUrl: 'http://localhost',
|
|
149
|
+
fetch: baseFetch,
|
|
150
|
+
headers,
|
|
151
|
+
timeoutMs: this.timeout,
|
|
152
|
+
maxRetries: options.maxRetries,
|
|
153
|
+
autostart,
|
|
154
|
+
onColdSocket: autostart ? startDaemon : undefined,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const request = (method, path, body, options) => this.request(method, path, body, options);
|
|
159
|
+
const nodes = new NodesResource(request);
|
|
160
|
+
const phaseOneNodes = {
|
|
161
|
+
create: (params, options) => this.call(deadline(options), (options) => this.client.request('POST', routes.nodes(), serializeCreate(params), options)),
|
|
162
|
+
retrieve: async (id, options) => {
|
|
163
|
+
const validId = nodeId(id);
|
|
164
|
+
return this.call(deadline(options), (options) => this.client.request('GET', routes.node(validId), undefined, options));
|
|
165
|
+
},
|
|
166
|
+
list: (query, options) => this.call(deadline(options), (options) => this.client.request('GET', withQuery(routes.nodes(), query), undefined, options)),
|
|
167
|
+
outcome: (id, options) => this.outcome(id, options),
|
|
168
|
+
waitForOutcome: (id, options) => this.waitForOutcome(id, options),
|
|
169
|
+
createAndWait: (params, options) => this.createAndWait(params, options),
|
|
170
|
+
parse: (params, options) => this.parse(params, options),
|
|
171
|
+
message: async (id, body, options) => {
|
|
172
|
+
const validId = nodeId(id);
|
|
173
|
+
return this.call(deadline(options), (options) => this.client.request('POST', routes.nodeMessages(validId), messageBody(body), options));
|
|
174
|
+
},
|
|
175
|
+
cancel: async (id, body, options) => {
|
|
176
|
+
const validId = nodeId(id);
|
|
177
|
+
return this.call(deadline(options), (options) => this.client.request('POST', routes.nodeClose(validId), body ?? {}, options));
|
|
178
|
+
},
|
|
179
|
+
interrupt: async (id, options) => {
|
|
180
|
+
const validId = nodeId(id);
|
|
181
|
+
return this.call(deadline(options), (options) => this.client.request('POST', routes.nodeInterrupt(validId), {}, options));
|
|
182
|
+
},
|
|
183
|
+
stream: (params, options) => this.stream(params, options),
|
|
184
|
+
events: (id, options) => this.events(id, options),
|
|
185
|
+
reports: {
|
|
186
|
+
list: async (id, query, options) => {
|
|
187
|
+
const validId = nodeId(id);
|
|
188
|
+
return this.call(deadline(options), (options) => this.client.request('GET', withQuery(routes.nodeReports(validId), query), undefined, options));
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
this.nodes = Object.assign(nodes, phaseOneNodes);
|
|
193
|
+
this.bash = new Bash(request, this.timeout);
|
|
194
|
+
this.canvas = new Canvas(request);
|
|
195
|
+
this.crons = new Crons(request);
|
|
196
|
+
this.structuredRequests = new StructuredRequests(request);
|
|
197
|
+
this.customObjects = new CustomObjects(request, (path, options) => this.openCustomDeliveries(path, options));
|
|
198
|
+
this.human = new Human(request);
|
|
199
|
+
this.models = new Models(request);
|
|
200
|
+
this.profiles = {
|
|
201
|
+
ensure: async (name, params, options) => {
|
|
202
|
+
const validName = pathSegment(name, 'profile name');
|
|
203
|
+
return this.call(deadline(options), (options) => this.client.request('PUT', routes.profile(validName), params ?? {}, options));
|
|
204
|
+
},
|
|
205
|
+
retrieve: async (name, options) => {
|
|
206
|
+
const validName = pathSegment(name, 'profile name');
|
|
207
|
+
return this.call(deadline(options), (options) => this.client.request('GET', routes.profile(validName), undefined, options));
|
|
208
|
+
},
|
|
209
|
+
list: (options) => this.call(deadline(options), (options) => this.client.request('GET', routes.profiles(), undefined, options)),
|
|
210
|
+
};
|
|
211
|
+
this.system = {
|
|
212
|
+
status: (options) => this.call(deadline(options), (options) => this.client.request('GET', routes.status(), undefined, options)),
|
|
213
|
+
health: (options) => this.call(deadline(options), (options) => this.client.request('GET', routes.healthz(), undefined, options)),
|
|
214
|
+
};
|
|
215
|
+
this.auth = {
|
|
216
|
+
status: (params, options) => this.authStatus(params, options),
|
|
217
|
+
};
|
|
218
|
+
this.files = new Files(request, (path, options) => this.openDownload(path, options));
|
|
219
|
+
this.uploads = new Uploads(request);
|
|
220
|
+
this.attachments = new Attachments(request);
|
|
221
|
+
this.shares = new Shares(request);
|
|
222
|
+
this.questions = new Questions(request);
|
|
223
|
+
this.memory = new Memory(request);
|
|
224
|
+
this.runs = new Runs(request, (id, after, signal) => this.openRunEvents(id, after, signal), this.questions);
|
|
225
|
+
this.providers = new Providers(request);
|
|
226
|
+
this.grant = { get: (options = {}) => {
|
|
227
|
+
const { wait, since, ...rest } = options;
|
|
228
|
+
const held = heldReadOptions(wait, rest);
|
|
229
|
+
return this.request('GET', withQuery('/v1/grant', { wait, since }), undefined, held);
|
|
230
|
+
} };
|
|
231
|
+
this.health = { get: (options) => this.request('GET', '/v1/health', undefined, options) };
|
|
232
|
+
this.usage = { get: (params = {}, options) => this.request('GET', withQuery('/v1/usage', { month: params.month }), undefined, options) };
|
|
233
|
+
}
|
|
234
|
+
/** Establish NDJSON through the JSON client's cold-socket/retry transport; leave the live body unread. */
|
|
235
|
+
async openCustomDeliveries(path, options) {
|
|
236
|
+
const response = await this.call(undefined, () => this.client.rawResponse('GET', path, {
|
|
237
|
+
headers: { accept: 'application/x-ndjson', ...options.headers }, signal: options.signal,
|
|
238
|
+
timeout: 0, maxRetries: options.maxRetries,
|
|
239
|
+
}));
|
|
240
|
+
if (!response.ok)
|
|
241
|
+
throw mapError(await responseError(response));
|
|
242
|
+
return response;
|
|
243
|
+
}
|
|
244
|
+
openRunEvents(id, after, signal) {
|
|
245
|
+
const baseURL = this.baseURL;
|
|
246
|
+
if (!baseURL)
|
|
247
|
+
throw new CrouterError('Run events require an app listener baseURL');
|
|
248
|
+
return this.call(undefined, () => this.eventFetch(`${baseURL.replace(/\/$/, '')}/v1/runs/${encodeURIComponent(id)}/events?after=${after}`, {
|
|
249
|
+
headers: { 'accept': 'text/event-stream', ...(this.bearerToken ? { Authorization: `Bearer ${this.bearerToken}` } : {}),
|
|
250
|
+
...(after ? { 'Last-Event-ID': String(after) } : {}) }, signal,
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
/** `GET /v1/files/download` as a raw fetch: bytes are not JSON, so it bypasses the JSON client but
|
|
254
|
+
* still goes through `call()`, which supplies the connection token and refreshes it once on a 401. */
|
|
255
|
+
async openDownload(path, options) {
|
|
256
|
+
const baseURL = this.baseURL;
|
|
257
|
+
if (!baseURL)
|
|
258
|
+
throw new CrouterError('File downloads require an app listener baseURL');
|
|
259
|
+
if (typeof path !== 'string' || path === '')
|
|
260
|
+
throw new TypeError('path is required');
|
|
261
|
+
const url = `${baseURL.replace(/\/$/, '')}/v1/files/download?${new URLSearchParams({ path })}`;
|
|
262
|
+
const response = await this.call(undefined, async () => {
|
|
263
|
+
try {
|
|
264
|
+
return await this.eventFetch(url, {
|
|
265
|
+
headers: { ...(this.bearerToken ? { Authorization: `Bearer ${this.bearerToken}` } : {}), ...(options.range ? { Range: options.range } : {}) },
|
|
266
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (options.signal?.aborted)
|
|
271
|
+
throw new APIUserAbortError(0, 'request_aborted', 'download aborted by caller');
|
|
272
|
+
throw new APIConnectionError(0, 'connection_error', error instanceof Error ? error.message : String(error));
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
if (response.ok)
|
|
276
|
+
return response;
|
|
277
|
+
throw mapError(await responseError(response));
|
|
278
|
+
}
|
|
279
|
+
/** Sends one request to any daemon route, for routes the namespaces above do not wrap. */
|
|
280
|
+
request(method, path, body, options) {
|
|
281
|
+
return this.call(deadline(options), (options) => this.client.request(method, path, body, options), path !== '/v1/health');
|
|
282
|
+
}
|
|
283
|
+
async authStatus(params = {}, options) {
|
|
284
|
+
if (this.baseURL === undefined && (!isNodeRuntime() || !await socketIsReachable(this.socketPath))) {
|
|
285
|
+
return connectStatus('Install crtr, run `crtr sys connect`, and paste the base URL and token it prints.');
|
|
286
|
+
}
|
|
287
|
+
let daemon;
|
|
288
|
+
try {
|
|
289
|
+
daemon = await this.call(deadline(options), (options) => this.client.request('GET', routes.status(), undefined, options));
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
if (!(error instanceof APIError))
|
|
293
|
+
throw error;
|
|
294
|
+
const mapped = mapError(error);
|
|
295
|
+
if (mapped instanceof AuthenticationError) {
|
|
296
|
+
return connectStatus('The daemon rejected the saved token. Run `crtr sys connect` and paste the new base URL and token it prints.');
|
|
297
|
+
}
|
|
298
|
+
if (mapped instanceof APIConnectionError) {
|
|
299
|
+
const instruction = this.baseURL === undefined
|
|
300
|
+
? 'Run `crtr sys connect` and paste the base URL and token it prints.'
|
|
301
|
+
: `The daemon at ${this.baseURL} could not be reached. Run \`crtr sys connect\` and paste the new base URL and token it prints.`;
|
|
302
|
+
return connectStatus(instruction);
|
|
303
|
+
}
|
|
304
|
+
throw mapped;
|
|
305
|
+
}
|
|
306
|
+
const daemonStatus = {
|
|
307
|
+
base_url: this.baseURL ?? daemon.listeners.socket_path,
|
|
308
|
+
runtime_version: daemon.runtime_version,
|
|
309
|
+
api_version: daemon.api_version,
|
|
310
|
+
startup_phase: daemon.startup_phase,
|
|
311
|
+
tcp: daemon.listeners.tcp,
|
|
312
|
+
};
|
|
313
|
+
if (daemon.startup_phase !== 'ready') {
|
|
314
|
+
return { connected: true, daemon: daemonStatus, model: null, next_step: null, instructions: null };
|
|
315
|
+
}
|
|
316
|
+
const model = await this.call(deadline(options), (options) => this.client.request('GET', withQuery(routes.modelAuthReadiness(), params), undefined, options));
|
|
317
|
+
if (model.credential !== 'ready') {
|
|
318
|
+
return {
|
|
319
|
+
connected: true,
|
|
320
|
+
daemon: daemonStatus,
|
|
321
|
+
model,
|
|
322
|
+
next_step: 'login',
|
|
323
|
+
instructions: `Run \`${connectCommand(params)}\` to log in to ${model.provider}.`,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return { connected: true, daemon: daemonStatus, model, next_step: null, instructions: null };
|
|
327
|
+
}
|
|
328
|
+
stream(params, options) {
|
|
329
|
+
const node = this.nodes.create(params, options);
|
|
330
|
+
const { timeout: _timeout, budgetMs, deadlineAt, ...streamOptions } = options ?? {};
|
|
331
|
+
const opening = deadline({ budgetMs, deadlineAt })?.deadlineAt;
|
|
332
|
+
return new NodeStream(node, async (signal) => {
|
|
333
|
+
const created = await node;
|
|
334
|
+
return this.openWithin(opening, signal, (signal) => this.call(undefined, () => this.client.getNodeEvents(created.node_id, {}, { ...streamOptions, signal })));
|
|
335
|
+
}, options?.signal);
|
|
336
|
+
}
|
|
337
|
+
events(id, options = {}) {
|
|
338
|
+
const { after, budgetMs, deadlineAt, ...streamOptions } = options;
|
|
339
|
+
const opening = deadline({ budgetMs, deadlineAt })?.deadlineAt;
|
|
340
|
+
const node = this.nodes.retrieve(id, { ...streamOptions, ...(opening === undefined ? {} : { deadlineAt: opening }) });
|
|
341
|
+
return new NodeStream(node, async (signal) => {
|
|
342
|
+
const existing = await node;
|
|
343
|
+
return this.openWithin(opening, signal, (signal) => this.call(undefined, () => this.client.getNodeEvents(existing.node_id, { after }, { ...streamOptions, signal })));
|
|
344
|
+
}, options.signal);
|
|
345
|
+
}
|
|
346
|
+
/** Opening a stream under the call's deadline: the open (token hook included) is abandoned and aborted at
|
|
347
|
+
* `deadlineAt`. Once open, a stream has no deadline: it lives until it ends or `signal` aborts. */
|
|
348
|
+
async openWithin(deadlineAt, signal, open) {
|
|
349
|
+
if (deadlineAt === undefined)
|
|
350
|
+
return open(signal);
|
|
351
|
+
const controller = new AbortController();
|
|
352
|
+
const onAbort = () => controller.abort(signal.reason);
|
|
353
|
+
if (signal.aborted)
|
|
354
|
+
onAbort();
|
|
355
|
+
else
|
|
356
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
357
|
+
try {
|
|
358
|
+
return await withinDeadline(open(controller.signal), deadlineAt);
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
controller.abort(error);
|
|
362
|
+
throw error instanceof APIError ? mapError(error) : error;
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
signal.removeEventListener('abort', onAbort);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async outcome(id, options = {}) {
|
|
369
|
+
const validId = nodeId(id);
|
|
370
|
+
const { wait, ...requestOptions } = options;
|
|
371
|
+
if (wait !== undefined && (!Number.isInteger(wait) || wait < 0 || wait > POLL_WAIT_SECONDS)) {
|
|
372
|
+
throw new RangeError('wait must be an integer between 0 and 25');
|
|
373
|
+
}
|
|
374
|
+
const timeout = wait === undefined ? requestOptions.timeout : wait * 1_000 + (requestOptions.timeout ?? this.timeout);
|
|
375
|
+
return this.call(deadline({ ...requestOptions, timeout }), (options) => this.client.request('GET', withQuery(routes.nodeOutcome(validId), { wait }), undefined, options));
|
|
376
|
+
}
|
|
377
|
+
async waitForOutcome(id, unbounded) {
|
|
378
|
+
// One deadline for the whole wait: every poll shares it instead of restarting `budgetMs`.
|
|
379
|
+
const options = deadline(unbounded);
|
|
380
|
+
for (;;) {
|
|
381
|
+
const response = await this.outcome(id, { ...options, wait: POLL_WAIT_SECONDS });
|
|
382
|
+
if (response.state === 'settled' && response.outcome !== null)
|
|
383
|
+
return response.outcome;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async createAndWait(params, unbounded) {
|
|
387
|
+
const options = deadline(unbounded);
|
|
388
|
+
const node = await this.nodes.create(params, options);
|
|
389
|
+
return this.waitForOutcome(node.node_id, options);
|
|
390
|
+
}
|
|
391
|
+
async parse(params, options) {
|
|
392
|
+
const outcome = await this.createAndWait({
|
|
393
|
+
...params,
|
|
394
|
+
...(params.parent == null && params.root_lifecycle === undefined ? { root_lifecycle: 'terminal' } : {}),
|
|
395
|
+
}, options);
|
|
396
|
+
if (outcome.kind === 'result') {
|
|
397
|
+
return { ...outcome, output_parsed: outcome.structured_result };
|
|
398
|
+
}
|
|
399
|
+
return { ...outcome, output_parsed: null };
|
|
400
|
+
}
|
|
401
|
+
/** Runs `operation` with the connection token, refreshing it once after a 401. `options` carries the call's one
|
|
402
|
+
* deadline (resolved by `deadline`): the token fetch, the refresh and every attempt share it, and `operation`
|
|
403
|
+
* receives the options with the deadline resolved. */
|
|
404
|
+
async call(options, operation, authenticate = true) {
|
|
405
|
+
const deadlineAt = options?.deadlineAt;
|
|
406
|
+
for (let attempt = 0;; attempt++) {
|
|
407
|
+
// A token refresh refused by the directory surfaces with its status class, like any other API error.
|
|
408
|
+
if (authenticate && this.tokenSource)
|
|
409
|
+
this.connectionToken = await withinDeadline(this.tokenSource(), deadlineAt).catch(throwMapped);
|
|
410
|
+
let result;
|
|
411
|
+
try {
|
|
412
|
+
result = await operation(options ?? {});
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
if (authenticate && attempt === 0 && error instanceof APIError && error.status === 401 && this.onUnauthorized) {
|
|
416
|
+
await withinDeadline(this.onUnauthorized(), deadlineAt).catch(throwMapped);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
throw mapError(error);
|
|
420
|
+
}
|
|
421
|
+
if (authenticate && attempt === 0 && result instanceof Response && result.status === 401 && this.onUnauthorized) {
|
|
422
|
+
await withinDeadline((async () => { try {
|
|
423
|
+
await result.body?.cancel();
|
|
424
|
+
}
|
|
425
|
+
catch { /* discarded */ } })(), deadlineAt).catch(() => { });
|
|
426
|
+
await withinDeadline(this.onUnauthorized(), deadlineAt).catch(throwMapped);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/** Rethrow an `APIError` as its status class; any other error from caller-supplied token hooks passes through unchanged. */
|
|
434
|
+
function throwMapped(error) {
|
|
435
|
+
throw error instanceof APIError ? mapError(error) : error;
|
|
436
|
+
}
|
|
437
|
+
/** A non-2xx answer's JSON envelope as an `APIError`; a body that is not the envelope keeps the status. */
|
|
438
|
+
async function responseError(response) {
|
|
439
|
+
let error;
|
|
440
|
+
try {
|
|
441
|
+
error = (await response.json()).error;
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
error = undefined;
|
|
445
|
+
}
|
|
446
|
+
return new APIError(response.status, error?.code ?? 'invalid_response', error?.message ?? `HTTP ${response.status}`, error?.details, response.headers, error);
|
|
447
|
+
}
|
|
448
|
+
function serializeCreate(params) {
|
|
449
|
+
const { output_schema, ...rest } = params;
|
|
450
|
+
const independentRoot = rest.parent === undefined && rest.root === undefined ? { root: true } : {};
|
|
451
|
+
if (output_schema === undefined)
|
|
452
|
+
return { ...independentRoot, ...rest };
|
|
453
|
+
return {
|
|
454
|
+
...independentRoot,
|
|
455
|
+
...rest,
|
|
456
|
+
output_schema: typeof output_schema === 'string' ? output_schema : JSON.stringify(resolveJsonSchema(output_schema)),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function messageBody(body) {
|
|
460
|
+
if (typeof body === 'string')
|
|
461
|
+
return { body };
|
|
462
|
+
const { from: _from, ...request } = body;
|
|
463
|
+
return request;
|
|
464
|
+
}
|
|
465
|
+
function withQuery(path, query) {
|
|
466
|
+
if (query === undefined)
|
|
467
|
+
return path;
|
|
468
|
+
const search = new URLSearchParams();
|
|
469
|
+
for (const [key, value] of Object.entries(query)) {
|
|
470
|
+
if (value !== undefined)
|
|
471
|
+
search.set(key, String(value));
|
|
472
|
+
}
|
|
473
|
+
const suffix = search.toString();
|
|
474
|
+
return suffix === '' ? path : `${path}?${suffix}`;
|
|
475
|
+
}
|
|
476
|
+
function connectStatus(instructions) {
|
|
477
|
+
return { connected: false, daemon: null, model: null, next_step: 'connect', instructions };
|
|
478
|
+
}
|
|
479
|
+
function connectCommand(params) {
|
|
480
|
+
const selectors = [
|
|
481
|
+
['profile', params.profile],
|
|
482
|
+
['cwd', params.cwd],
|
|
483
|
+
['kind', params.kind],
|
|
484
|
+
['model', params.model],
|
|
485
|
+
].flatMap(([name, value]) => value === undefined ? [] : [`--${name}`, shellArgument(value)]);
|
|
486
|
+
return ['crtr', 'sys', 'connect', ...selectors].join(' ');
|
|
487
|
+
}
|
|
488
|
+
function shellArgument(value) {
|
|
489
|
+
if (/^[A-Za-z0-9_./:-]+$/.test(value))
|
|
490
|
+
return value;
|
|
491
|
+
return `'${value.replaceAll("'", String.fromCharCode(39, 34, 39, 34, 39))}'`;
|
|
492
|
+
}
|
|
493
|
+
async function socketIsReachable(socketPath) {
|
|
494
|
+
const load = new Function('specifier', 'return import(specifier)');
|
|
495
|
+
const [{ createConnection }, { defaultSocketPath }] = await Promise.all([
|
|
496
|
+
load('node:net'),
|
|
497
|
+
load('@north-light/api/node'),
|
|
498
|
+
]);
|
|
499
|
+
const path = socketPath ?? defaultSocketPath();
|
|
500
|
+
return new Promise((resolve) => {
|
|
501
|
+
const socket = createConnection({ path });
|
|
502
|
+
const finish = (reachable) => {
|
|
503
|
+
socket.destroy();
|
|
504
|
+
resolve(reachable);
|
|
505
|
+
};
|
|
506
|
+
socket.once('connect', () => finish(true));
|
|
507
|
+
socket.once('error', () => finish(false));
|
|
508
|
+
socket.setTimeout(100, () => finish(false));
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
function nodeEnvironment() {
|
|
512
|
+
return isNodeRuntime() ? process.env : {};
|
|
513
|
+
}
|
|
514
|
+
function isNodeRuntime() {
|
|
515
|
+
return typeof process !== 'undefined' && process.versions?.node !== undefined;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Latest only (P2): the SDK accepts a runtime at or above its own release (`SDK_VERSION`) and nothing older, so its
|
|
519
|
+
* types never describe a field an older runtime does not send. Every runtime response names its release in
|
|
520
|
+
* `Crouter-Runtime-Version`; a successful answer from an older runtime, or one that names no release (every runtime
|
|
521
|
+
* before the header existed), is replaced by a 426 `runtime_version_unsupported` envelope before anything reads it,
|
|
522
|
+
* which the client raises as `RuntimeVersionError`. A refusal without the header passes through unchanged: it can come
|
|
523
|
+
* from a router or proxy in front of the runtime, and it carries no runtime data. `/healthz`, the unversioned
|
|
524
|
+
* substrate probe the cold-start wait polls, is exempt.
|
|
525
|
+
*/
|
|
526
|
+
function requireCurrentRuntime(inner) {
|
|
527
|
+
const checked = async (input, init) => {
|
|
528
|
+
const url = new URL(input instanceof Request ? input.url : String(input), 'http://localhost');
|
|
529
|
+
// Say which runtime this SDK accepts, so the router in front of a hosted runtime, or the runtime itself, refuses
|
|
530
|
+
// an older one before any handler runs. `/healthz` is exempt from both checks.
|
|
531
|
+
let sentInit = init;
|
|
532
|
+
if (url.pathname !== '/healthz') {
|
|
533
|
+
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
|
|
534
|
+
headers.set(MINIMUM_RUNTIME_VERSION_HEADER, SDK_VERSION);
|
|
535
|
+
sentInit = { ...init, headers };
|
|
536
|
+
}
|
|
537
|
+
const response = await (inner ?? globalThis.fetch)(input, sentInit);
|
|
538
|
+
if (url.pathname === '/healthz')
|
|
539
|
+
return response;
|
|
540
|
+
const runtimeVersion = response.headers.get(RUNTIME_VERSION_HEADER);
|
|
541
|
+
if (runtimeVersion === null ? !response.ok : compareReleaseVersions(runtimeVersion, SDK_VERSION) >= 0)
|
|
542
|
+
return response;
|
|
543
|
+
await response.body?.cancel();
|
|
544
|
+
const message = runtimeVersion === null
|
|
545
|
+
? `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.`
|
|
546
|
+
: `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}.`;
|
|
547
|
+
return new Response(JSON.stringify({ error: {
|
|
548
|
+
code: 'runtime_version_unsupported', message, type: 'invalid_request_error', retryable: false,
|
|
549
|
+
details: { sdk_version: SDK_VERSION, runtime_version: runtimeVersion },
|
|
550
|
+
} }), { status: 426, headers: { 'content-type': 'application/json' } });
|
|
551
|
+
};
|
|
552
|
+
if (inner !== undefined && inner[LOCAL_SOCKET_FETCH] === true) {
|
|
553
|
+
checked[LOCAL_SOCKET_FETCH] = true;
|
|
554
|
+
}
|
|
555
|
+
return checked;
|
|
556
|
+
}
|
|
557
|
+
function localSocketFetch(socketPath) {
|
|
558
|
+
const load = new Function('specifier', 'return import(specifier)');
|
|
559
|
+
let fetchPromise;
|
|
560
|
+
const localFetch = (input, init) => {
|
|
561
|
+
fetchPromise ??= load('@north-light/api/node').then(({ socketFetch, defaultSocketPath }) => socketFetch(socketPath ?? defaultSocketPath()));
|
|
562
|
+
return fetchPromise.then((fetch) => fetch(input, init));
|
|
563
|
+
};
|
|
564
|
+
localFetch[LOCAL_SOCKET_FETCH] = true;
|
|
565
|
+
return localFetch;
|
|
566
|
+
}
|
|
567
|
+
async function startDaemon() {
|
|
568
|
+
const load = new Function('specifier', 'return import(specifier)');
|
|
569
|
+
const { spawn } = await load('node:child_process');
|
|
570
|
+
await new Promise((resolve, reject) => {
|
|
571
|
+
const child = spawn('crtr', ['sys', 'daemon', 'start'], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
572
|
+
let stderr = '';
|
|
573
|
+
child.stderr?.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
|
|
574
|
+
child.once('error', (error) => reject(new CrouterError(`could not run \`crtr sys daemon start\`: ${error.message}`)));
|
|
575
|
+
child.once('exit', (code) => {
|
|
576
|
+
if (code === 0)
|
|
577
|
+
resolve();
|
|
578
|
+
else
|
|
579
|
+
reject(new CrouterError(`\`crtr sys daemon start\` exited ${String(code)}${stderr === '' ? '' : `: ${stderr.trim()}`}`));
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
}
|
package/dist/crtr.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** How long a `crtr` call from an SDK command may run before the command gives up. */
|
|
2
|
+
export declare const CRTR_TIMEOUT_MS = 60000;
|
|
3
|
+
/** The join step crtr's `not_joined` answer names when it carries none of its own. */
|
|
4
|
+
export declare const DEFAULT_JOIN_STEP = "Open https://account.northlight.ai/account/machines, choose Create join token, then run `crtr sys join --directory https://account.northlight.ai --token <token>`.";
|
|
5
|
+
export type CrtrOutcome = {
|
|
6
|
+
kind: 'ok';
|
|
7
|
+
stdout: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'missing';
|
|
10
|
+
}
|
|
11
|
+
/** crtr answered `not_joined`: this machine was never joined to an account, so no `cloud` leaf exists. */
|
|
12
|
+
| {
|
|
13
|
+
kind: 'not_joined';
|
|
14
|
+
joinStep: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'failed';
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
export declare const parseJson: (text: string) => unknown;
|
|
20
|
+
/** Runs `crtr <argv>` once: execFile, no shell, the caller's environment. */
|
|
21
|
+
export declare function runCrtr(argv: string[], timeoutMs: number): Promise<CrtrOutcome>;
|
|
22
|
+
/** "This machine's crtr isn't joined …, so <what>." then the join step. */
|
|
23
|
+
export declare const notJoinedLines: (what: string, joinStep: string) => string[];
|
|
24
|
+
export declare const say: (lines: string[]) => void;
|
package/dist/crtr.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
/** How long a `crtr` call from an SDK command may run before the command gives up. */
|
|
3
|
+
export const CRTR_TIMEOUT_MS = 60_000;
|
|
4
|
+
/** The join step crtr's `not_joined` answer names when it carries none of its own. */
|
|
5
|
+
export const DEFAULT_JOIN_STEP = 'Open https://account.northlight.ai/account/machines, choose Create join token, then run `crtr sys join --directory https://account.northlight.ai --token <token>`.';
|
|
6
|
+
const seconds = (ms) => {
|
|
7
|
+
const n = Math.max(1, Math.round(ms / 1000));
|
|
8
|
+
return `${n} second${n === 1 ? '' : 's'}`;
|
|
9
|
+
};
|
|
10
|
+
export const parseJson = (text) => {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(text.trim());
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const lastLine = (text) => text.split('\n').map((line) => line.trim()).filter(Boolean).at(-1);
|
|
19
|
+
/** The JSON error that ends `text`, even when other lines (a warning, a notice) precede it. */
|
|
20
|
+
const errorBody = (text) => {
|
|
21
|
+
const body = parseJson(text.slice(text.lastIndexOf('\n{') + 1));
|
|
22
|
+
return body && typeof body === 'object' && !Array.isArray(body) ? body : undefined;
|
|
23
|
+
};
|
|
24
|
+
const text = (value) => (typeof value === 'string' && value ? value : undefined);
|
|
25
|
+
/** Runs `crtr <argv>` once: execFile, no shell, the caller's environment. */
|
|
26
|
+
export function runCrtr(argv, timeoutMs) {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
execFile('crtr', argv, { timeout: timeoutMs, env: process.env, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
|
|
29
|
+
const err = error;
|
|
30
|
+
if (err?.code === 'ENOENT')
|
|
31
|
+
return resolve({ kind: 'missing' });
|
|
32
|
+
if (!err)
|
|
33
|
+
return resolve({ kind: 'ok', stdout });
|
|
34
|
+
if (err.killed)
|
|
35
|
+
return resolve({ kind: 'failed', message: `crtr did not answer within ${seconds(timeoutMs)}.` });
|
|
36
|
+
const bodies = [errorBody(stdout), errorBody(stderr)];
|
|
37
|
+
const notJoined = bodies.find((body) => (text(body?.error) ?? text(body?.code)) === 'not_joined');
|
|
38
|
+
if (notJoined)
|
|
39
|
+
return resolve({ kind: 'not_joined', joinStep: text(notJoined.next) ?? DEFAULT_JOIN_STEP });
|
|
40
|
+
const message = bodies.map((body) => text(body?.message)).find(Boolean);
|
|
41
|
+
resolve({ kind: 'failed', message: message ?? lastLine(stderr) ?? lastLine(stdout) ?? err.message });
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** "This machine's crtr isn't joined …, so <what>." then the join step. */
|
|
46
|
+
export const notJoinedLines = (what, joinStep) => [
|
|
47
|
+
`This machine's crtr isn't joined to your crouter cloud account, so ${what}.`,
|
|
48
|
+
joinStep,
|
|
49
|
+
];
|
|
50
|
+
export const say = (lines) => console.error(lines.join('\n'));
|