@calliopelabs/cli 3.3.1 → 3.4.1

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 (43) hide show
  1. package/dist/attach.d.ts +26 -11
  2. package/dist/attach.d.ts.map +1 -1
  3. package/dist/attach.js +219 -98
  4. package/dist/attach.js.map +1 -1
  5. package/dist/bin.d.ts.map +1 -1
  6. package/dist/bin.js +9 -1
  7. package/dist/bin.js.map +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +3 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/model-detection.d.ts.map +1 -1
  12. package/dist/model-detection.js +7 -132
  13. package/dist/model-detection.js.map +1 -1
  14. package/dist/policy.d.ts.map +1 -1
  15. package/dist/policy.js +26 -36
  16. package/dist/policy.js.map +1 -1
  17. package/dist/providers/bedrock-auth.d.ts +9 -0
  18. package/dist/providers/bedrock-auth.d.ts.map +1 -0
  19. package/dist/providers/bedrock-auth.js +84 -0
  20. package/dist/providers/bedrock-auth.js.map +1 -0
  21. package/dist/providers/bedrock-credential-worker.d.ts +2 -0
  22. package/dist/providers/bedrock-credential-worker.d.ts.map +1 -0
  23. package/dist/providers/bedrock-credential-worker.js +54 -0
  24. package/dist/providers/bedrock-credential-worker.js.map +1 -0
  25. package/dist/providers/bedrock.d.ts +2 -2
  26. package/dist/providers/bedrock.d.ts.map +1 -1
  27. package/dist/providers/bedrock.js +14 -121
  28. package/dist/providers/bedrock.js.map +1 -1
  29. package/dist/runtime/permissions.d.ts +2 -0
  30. package/dist/runtime/permissions.d.ts.map +1 -1
  31. package/dist/runtime/permissions.js +33 -0
  32. package/dist/runtime/permissions.js.map +1 -1
  33. package/dist/runtime/turn.js +39 -14
  34. package/dist/runtime/turn.js.map +1 -1
  35. package/dist/sandbox/docker.d.ts.map +1 -1
  36. package/dist/sandbox/docker.js +7 -2
  37. package/dist/sandbox/docker.js.map +1 -1
  38. package/dist/ui/error-boundary.js +1 -1
  39. package/dist/ui/error-boundary.js.map +1 -1
  40. package/dist/ui/index.d.ts.map +1 -1
  41. package/dist/ui/index.js +3 -0
  42. package/dist/ui/index.js.map +1 -1
  43. package/package.json +7 -1
package/dist/attach.d.ts CHANGED
@@ -22,6 +22,7 @@
22
22
  */
23
23
  /** Protocol versions offered in `initialize`; keep in step with calliope-vscode scripts/ahp-supported-versions.json. */
24
24
  export declare const SUPPORTED_VERSIONS: string[];
25
+ export declare const ATTACH_TIMEOUT_MS = 60000;
25
26
  export type FallbackReason = 'disabled' | 'unentitled' | 'auth' | 'version' | 'unreachable';
26
27
  export declare const FALLBACK_EXIT = 3;
27
28
  type Fail = {
@@ -33,10 +34,13 @@ export declare function isDisabled(env?: NodeJS.ProcessEnv): boolean;
33
34
  /** A session's default chat channel: `ahp-chat://default/<base64url(session)>`. */
34
35
  export declare function defaultChatUri(session: string): string;
35
36
  /**
36
- * Node's WebSocket hides the status of a refused upgrade, so ask plainly first:
37
- * an authorized agent-host route answers a plain GET with 426 Upgrade Required.
37
+ * Classify authentication refusals before upgrading the transport. An authorized
38
+ * agent-host route answers a plain GET with 426 Upgrade Required.
38
39
  */
39
- export declare function preflight(wsUrl: string, headers: Record<string, string>, fetchImpl?: typeof fetch): Promise<Fail | undefined>;
40
+ export declare function preflight(wsUrl: string, headers: Record<string, string>, fetchImpl?: typeof fetch, options?: {
41
+ timeoutMs?: number;
42
+ signal?: AbortSignal;
43
+ }): Promise<Fail | undefined>;
40
44
  /** Find (or start) the user's agent host on a JupyterHub; returns its WebSocket URL. */
41
45
  export declare function resolveHubHost(opts: {
42
46
  hubUrl: string;
@@ -59,19 +63,27 @@ interface Message {
59
63
  message: string;
60
64
  };
61
65
  }
66
+ interface AttachSocket {
67
+ send(data: string): void;
68
+ close(): void;
69
+ terminate?(): void;
70
+ addEventListener(type: string, fn: (ev: any) => void): void;
71
+ }
62
72
  /** The minimal JSON-RPC client attach needs, over any WebSocket-shaped object. */
63
73
  export declare class AhpConnection {
64
74
  private readonly ws;
65
75
  private nextId;
66
76
  private readonly pending;
77
+ private failure?;
78
+ private onClosed?;
67
79
  readonly actions: Array<(action: any, channel: string, serverSeq?: number) => void>;
68
- closed?: (detail: string) => void;
69
- constructor(ws: {
70
- send(data: string): void;
71
- close(): void;
72
- addEventListener(type: string, fn: (ev: any) => void): void;
73
- });
74
- call(method: string, params: Record<string, unknown>): Promise<Message>;
80
+ set closed(callback: ((detail: string) => void) | undefined);
81
+ constructor(ws: AttachSocket);
82
+ private stop;
83
+ call(method: string, params: Record<string, unknown>, options?: {
84
+ timeoutMs?: number;
85
+ signal?: AbortSignal;
86
+ }): Promise<Message>;
75
87
  dispatch(channel: string, clientSeq: number, action: Record<string, unknown>): void;
76
88
  close(): void;
77
89
  }
@@ -144,6 +156,9 @@ export interface SessionSnapshot {
144
156
  export declare function follow(conn: AhpConnection, session: string, io: FollowIo, subscribed?: Promise<ChatSnapshot | undefined>, sessionSubscribed?: Promise<SessionSnapshot | undefined>): {
145
157
  done: Promise<string>;
146
158
  };
147
- export declare function runAttach(args: string[], env?: NodeJS.ProcessEnv): Promise<number>;
159
+ export declare function runAttach(args: string[], env?: NodeJS.ProcessEnv, options?: {
160
+ timeoutMs?: number;
161
+ createSocket?: (url: string, headers: Record<string, string>) => AttachSocket;
162
+ }): Promise<number>;
148
163
  export {};
149
164
  //# sourceMappingURL=attach.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"attach.d.ts","sourceRoot":"","sources":["../src/attach.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"attach.d.ts","sourceRoot":"","sources":["../src/attach.ts"],"names":[],"mappings":"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"}
package/dist/attach.js CHANGED
@@ -21,8 +21,10 @@
21
21
  * CALLIOPE_AHP=off is the kill switch.
22
22
  */
23
23
  import * as readline from 'readline';
24
+ import WebSocket from 'ws';
24
25
  /** Protocol versions offered in `initialize`; keep in step with calliope-vscode scripts/ahp-supported-versions.json. */
25
- export const SUPPORTED_VERSIONS = ['1.0.0', '0.6.0'];
26
+ export const SUPPORTED_VERSIONS = ['1.0.0', '0.9.0', '0.6.0'];
27
+ export const ATTACH_TIMEOUT_MS = 60_000;
26
28
  export const FALLBACK_EXIT = 3;
27
29
  const ROOT = 'ahp-root://';
28
30
  const UNSUPPORTED_PROTOCOL_VERSION = -32005;
@@ -35,13 +37,17 @@ export function defaultChatUri(session) {
35
37
  return `ahp-chat://default/${Buffer.from(session).toString('base64url')}`;
36
38
  }
37
39
  /**
38
- * Node's WebSocket hides the status of a refused upgrade, so ask plainly first:
39
- * an authorized agent-host route answers a plain GET with 426 Upgrade Required.
40
+ * Classify authentication refusals before upgrading the transport. An authorized
41
+ * agent-host route answers a plain GET with 426 Upgrade Required.
40
42
  */
41
- export async function preflight(wsUrl, headers, fetchImpl = fetch) {
43
+ export async function preflight(wsUrl, headers, fetchImpl = fetch, options = {}) {
42
44
  const http = wsUrl.replace(/^ws/, 'http');
45
+ const controller = new AbortController();
46
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? ATTACH_TIMEOUT_MS);
47
+ const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
43
48
  try {
44
- const res = await fetchImpl(http, { headers, redirect: 'manual' });
49
+ const res = await fetchImpl(http, { headers, redirect: 'manual', signal });
50
+ void res.body?.cancel().catch(() => { });
45
51
  if (res.status === 426 || res.ok) {
46
52
  return undefined;
47
53
  }
@@ -49,8 +55,12 @@ export async function preflight(wsUrl, headers, fetchImpl = fetch) {
49
55
  const toLogin = res.status >= 300 && res.status < 400 && /\/(hub\/)?login|oauth/.test(res.headers.get('location') ?? '');
50
56
  return fail(res.status === 401 || res.status === 403 || toLogin ? 'auth' : 'unreachable', `the host route answered HTTP ${res.status}`);
51
57
  }
52
- catch (err) {
53
- return fail('unreachable', `cannot reach ${http}: ${err.message}`);
58
+ catch {
59
+ return fail('unreachable', signal.aborted ? 'host preflight timed out' : 'host preflight failed');
60
+ }
61
+ finally {
62
+ clearTimeout(timer);
63
+ controller.abort();
54
64
  }
55
65
  }
56
66
  /** Find (or start) the user's agent host on a JupyterHub; returns its WebSocket URL. */
@@ -58,10 +68,30 @@ export async function resolveHubHost(opts) {
58
68
  const { hubUrl, user, token, serverName = 'agenthost', fetchImpl = fetch, timeoutMs = 60_000 } = opts;
59
69
  const api = new URL('hub/api/', hubUrl.endsWith('/') ? hubUrl : `${hubUrl}/`);
60
70
  const headers = { Authorization: `token ${token}` };
71
+ const controller = new AbortController();
72
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
73
+ const request = (url, init = {}) => fetchImpl(url, {
74
+ ...init, signal: controller.signal, redirect: 'error', credentials: 'omit',
75
+ });
61
76
  const deadline = Date.now() + timeoutMs;
62
77
  try {
78
+ const tap = await request(new URL('/agent-host/status', hubUrl), { headers });
79
+ if (tap.status === 401)
80
+ return fail('auth', 'hub tap refused the credential');
81
+ if (tap.status !== 404) {
82
+ const body = await tap.json();
83
+ if (tap.ok && body?.enabled === true && body.endpoint === '/agent-host/') {
84
+ const ws = new URL('/agent-host/', hubUrl);
85
+ ws.protocol = ws.protocol === 'https:' ? 'wss:' : 'ws:';
86
+ return { ok: true, url: ws.toString() };
87
+ }
88
+ const reason = body?.reason === 'disabled' || body?.reason === 'unentitled' || body?.reason === 'auth'
89
+ ? body.reason : tap.status === 403 ? 'auth' : 'unreachable';
90
+ return fail(reason, `hub tap answered ${tap.status}`);
91
+ }
63
92
  for (let spawned = false;;) {
64
- const res = await fetchImpl(new URL(`users/${encodeURIComponent(user)}`, api), { headers });
93
+ controller.signal.throwIfAborted();
94
+ const res = await request(new URL(`users/${encodeURIComponent(user)}`, api), { headers });
65
95
  if (!res.ok) {
66
96
  return fail(res.status === 401 || res.status === 403 ? 'auth' : 'unreachable', `hub user lookup answered ${res.status}`);
67
97
  }
@@ -69,30 +99,35 @@ export async function resolveHubHost(opts) {
69
99
  const server = body.servers?.[serverName];
70
100
  if (server?.ready && server.url) {
71
101
  const ws = new URL(`${server.url}agent-host/`, hubUrl);
102
+ if (ws.origin !== new URL(hubUrl).origin)
103
+ return fail('unreachable', 'hub answered a foreign server URL');
72
104
  ws.protocol = ws.protocol === 'https:' ? 'wss:' : 'ws:';
73
105
  return { ok: true, url: ws.toString() };
74
106
  }
75
- if (!spawned) {
76
- const spawn = await fetchImpl(new URL(`users/${encodeURIComponent(user)}/servers/${encodeURIComponent(serverName)}`, api), { method: 'POST', headers });
77
- if (spawn.status === 401) {
107
+ if (!spawned && !server?.pending) {
108
+ const spawn = await request(new URL(`users/${encodeURIComponent(user)}/servers/${encodeURIComponent(serverName)}`, api), {
109
+ method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ profile: serverName }),
110
+ });
111
+ if (spawn.status === 401)
78
112
  return fail('auth', 'hub refused the token');
79
- }
80
113
  if (spawn.status === 403 || (spawn.status === 400 && !server)) {
81
114
  return fail('unentitled', `hub refused to start ${serverName}: ${spawn.status}`);
82
115
  }
83
- if (!spawn.ok && spawn.status !== 400) {
116
+ if (!spawn.ok && spawn.status !== 400)
84
117
  return fail('unreachable', `hub could not start ${serverName}: ${spawn.status}`);
85
- }
86
118
  spawned = true;
87
119
  }
88
- if (Date.now() >= deadline) {
120
+ if (Date.now() >= deadline)
89
121
  return fail('unreachable', `${serverName} not ready within ${Math.round(timeoutMs / 1000)}s`);
90
- }
91
- await new Promise(resolve => setTimeout(resolve, 1000));
122
+ await new Promise(resolve => setTimeout(resolve, Math.min(1000, Math.max(0, deadline - Date.now()))));
92
123
  }
93
124
  }
94
125
  catch (err) {
95
- return fail('unreachable', `hub unreachable: ${err.message}`);
126
+ return fail('unreachable', controller.signal.aborted ? 'hub discovery timed out' : `hub unreachable: ${err.message}`);
127
+ }
128
+ finally {
129
+ clearTimeout(timer);
130
+ controller.abort();
96
131
  }
97
132
  }
98
133
  /** The minimal JSON-RPC client attach needs, over any WebSocket-shaped object. */
@@ -100,15 +135,34 @@ export class AhpConnection {
100
135
  ws;
101
136
  nextId = 1;
102
137
  pending = new Map();
138
+ failure;
139
+ onClosed;
103
140
  actions = [];
104
- closed;
141
+ set closed(callback) {
142
+ this.onClosed = callback;
143
+ if (this.failure)
144
+ callback?.(this.failure);
145
+ }
105
146
  constructor(ws) {
106
147
  this.ws = ws;
107
148
  ws.addEventListener('message', ev => {
108
- const msg = JSON.parse(String(ev.data));
149
+ if (this.failure)
150
+ return;
151
+ let msg;
152
+ try {
153
+ msg = JSON.parse(String(ev.data));
154
+ if (!msg || typeof msg !== 'object' || Array.isArray(msg))
155
+ throw new Error('invalid frame');
156
+ }
157
+ catch {
158
+ this.stop('invalid host message');
159
+ return;
160
+ }
109
161
  if (msg.id !== undefined && this.pending.has(msg.id)) {
110
- this.pending.get(msg.id).resolve(msg);
162
+ const pending = this.pending.get(msg.id);
111
163
  this.pending.delete(msg.id);
164
+ pending.cleanup();
165
+ pending.resolve(msg);
112
166
  return;
113
167
  }
114
168
  if (msg.method === 'action' && msg.params?.action) {
@@ -117,20 +171,51 @@ export class AhpConnection {
117
171
  }
118
172
  }
119
173
  });
120
- ws.addEventListener('close', ev => this.closed?.(`connection closed (${ev.code ?? 'unknown'})`));
174
+ ws.addEventListener('close', ev => this.stop(`connection closed (${ev.code ?? 'unknown'})`));
175
+ ws.addEventListener('error', () => this.stop('host transport failed'));
121
176
  }
122
- call(method, params) {
177
+ stop(detail) {
178
+ if (this.failure)
179
+ return;
180
+ this.failure = detail;
181
+ for (const pending of this.pending.values()) {
182
+ pending.cleanup();
183
+ pending.resolve({ error: { code: -32000, message: detail } });
184
+ }
185
+ this.pending.clear();
186
+ this.onClosed?.(detail);
187
+ try {
188
+ this.ws.terminate ? this.ws.terminate() : this.ws.close();
189
+ }
190
+ catch { /* Already closed. */ }
191
+ }
192
+ call(method, params, options = {}) {
193
+ if (this.failure)
194
+ return Promise.resolve({ error: { code: -32000, message: this.failure } });
195
+ if (options.signal?.aborted) {
196
+ this.stop('host setup timed out');
197
+ return Promise.resolve({ error: { code: -32000, message: this.failure } });
198
+ }
123
199
  const id = this.nextId++;
124
200
  return new Promise(resolve => {
125
- this.pending.set(id, { resolve });
126
- this.ws.send(JSON.stringify({ jsonrpc: '2.0', id, method, params }));
201
+ const aborted = () => this.stop('host setup timed out');
202
+ const timer = setTimeout(() => this.stop('host request timed out'), options.timeoutMs ?? ATTACH_TIMEOUT_MS);
203
+ const cleanup = () => { clearTimeout(timer); options.signal?.removeEventListener('abort', aborted); };
204
+ this.pending.set(id, { resolve, cleanup });
205
+ options.signal?.addEventListener('abort', aborted, { once: true });
206
+ try {
207
+ this.ws.send(JSON.stringify({ jsonrpc: '2.0', id, method, params }));
208
+ }
209
+ catch {
210
+ this.stop('host request failed');
211
+ }
127
212
  });
128
213
  }
129
214
  dispatch(channel, clientSeq, action) {
130
215
  this.ws.send(JSON.stringify({ jsonrpc: '2.0', method: 'dispatchAction', params: { channel, clientSeq, action } }));
131
216
  }
132
217
  close() {
133
- this.ws.close();
218
+ this.stop('connection closed');
134
219
  }
135
220
  }
136
221
  /** Classify the `initialize` answer; undefined when the host is usable. */
@@ -162,6 +247,7 @@ export function follow(conn, session, io, subscribed, sessionSubscribed) {
162
247
  const chat = defaultChatUri(session);
163
248
  let seq = 0;
164
249
  let finish;
250
+ let ended = false;
165
251
  const done = new Promise(resolve => { finish = resolve; });
166
252
  // Confirmations this client knows are open, by tool call, each able to
167
253
  // withdraw its prompt. One prompt at a time: a snapshot can hold several, and
@@ -191,11 +277,14 @@ export function follow(conn, session, io, subscribed, sessionSubscribed) {
191
277
  };
192
278
  // Following is over: no prompt outlives it, or its readline keeps the process alive.
193
279
  const end = (reason) => {
280
+ if (ended)
281
+ return;
282
+ ended = true;
194
283
  settleAll();
195
284
  finish(reason);
196
285
  };
197
286
  const confirm = (target, turnId, toolCallId, confirmationTitle, toolInput, request) => {
198
- if (open.has(toolCallId)) {
287
+ if (ended || open.has(toolCallId)) {
199
288
  return;
200
289
  }
201
290
  const asked = new AbortController();
@@ -231,8 +320,8 @@ export function follow(conn, session, io, subscribed, sessionSubscribed) {
231
320
  io.write(action.content ?? '');
232
321
  break;
233
322
  case 'chat/toolCallReady':
234
- if (action.confirmationTitle !== undefined) {
235
- confirm(chat, action.turnId, action.toolCallId, action.confirmationTitle, action.toolInput);
323
+ if (!action.confirmed) {
324
+ confirm(chat, action.turnId, action.toolCallId, action.confirmationTitle ?? action.invocationMessage, action.toolInput);
236
325
  }
237
326
  break;
238
327
  case 'chat/toolCallConfirmed':
@@ -244,6 +333,9 @@ export function follow(conn, session, io, subscribed, sessionSubscribed) {
244
333
  break;
245
334
  case 'chat/turnCancelled':
246
335
  settleTurn();
336
+ io.write('\n[turn cancelled]\n');
337
+ if (io.once)
338
+ end('cancelled');
247
339
  break;
248
340
  case 'chat/turnComplete':
249
341
  settleTurn();
@@ -254,7 +346,7 @@ export function follow(conn, session, io, subscribed, sessionSubscribed) {
254
346
  break;
255
347
  case 'chat/error':
256
348
  settleTurn();
257
- io.write(`\n[turn error] ${JSON.stringify(action.error ?? action).slice(0, 200)}\n`);
349
+ io.write(`\n[turn error] ${JSON.stringify(action.part?.error ?? action.error ?? action).slice(0, 200)}\n`);
258
350
  if (io.once) {
259
351
  end('error');
260
352
  }
@@ -293,7 +385,7 @@ export function follow(conn, session, io, subscribed, sessionSubscribed) {
293
385
  const track = (channel, onAction, seed, snapshot) => {
294
386
  let held = snapshot ? [] : undefined;
295
387
  conn.actions.push((action, from, serverSeq) => {
296
- if (from !== channel) {
388
+ if (ended || from !== channel) {
297
389
  return;
298
390
  }
299
391
  if (held) {
@@ -303,6 +395,8 @@ export function follow(conn, session, io, subscribed, sessionSubscribed) {
303
395
  onAction(action);
304
396
  });
305
397
  const join = (joined) => {
398
+ if (ended)
399
+ return;
306
400
  seed(joined);
307
401
  const early = held ?? [];
308
402
  held = undefined;
@@ -364,7 +458,7 @@ function askYesNo(title, input, signal) {
364
458
  rl.question(`\n${title}\n ${input}\nApprove? [y/N] `, answer => end(/^y(es)?$/i.test(answer.trim())));
365
459
  });
366
460
  }
367
- export async function runAttach(args, env = process.env) {
461
+ export async function runAttach(args, env = process.env, options = {}) {
368
462
  const opts = parseArgs(args);
369
463
  const err = (s) => process.stderr.write(`${s}\n`);
370
464
  const fallback = (f) => {
@@ -372,79 +466,106 @@ export async function runAttach(args, env = process.env) {
372
466
  err('Falling back: run `calliope` to work locally.');
373
467
  return FALLBACK_EXIT;
374
468
  };
375
- if (isDisabled(env)) {
469
+ if (isDisabled(env))
376
470
  return fallback(fail('disabled', 'CALLIOPE_AHP is off'));
471
+ const timeoutMs = options.timeoutMs ?? ATTACH_TIMEOUT_MS;
472
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 120_000) {
473
+ return fallback(fail('unreachable', 'invalid attach setup timeout'));
377
474
  }
378
- const token = env[opts.tokenEnv];
379
- const headers = token ? { Authorization: `token ${token}` } : {};
380
- let url = opts.url;
381
- if (!url && opts.hub && opts.user) {
382
- if (!token) {
383
- return fallback(fail('auth', `set ${opts.tokenEnv} to a hub API token`));
475
+ const controller = new AbortController();
476
+ const deadline = Date.now() + timeoutMs;
477
+ const remaining = () => Math.max(1, deadline - Date.now());
478
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
479
+ let ws;
480
+ let conn;
481
+ try {
482
+ const token = env[opts.tokenEnv];
483
+ const headers = token ? { Authorization: `token ${token}` } : {};
484
+ let url = opts.url;
485
+ if (!url && opts.hub && opts.user) {
486
+ if (!token)
487
+ return fallback(fail('auth', `set ${opts.tokenEnv} to a hub API token`));
488
+ const resolved = await resolveHubHost({ hubUrl: opts.hub, user: opts.user, token, timeoutMs: remaining() });
489
+ if (!resolved.ok)
490
+ return fallback(resolved);
491
+ url = resolved.url;
384
492
  }
385
- const resolved = await resolveHubHost({ hubUrl: opts.hub, user: opts.user, token });
386
- if (!resolved.ok) {
387
- return fallback(resolved);
493
+ if (!url) {
494
+ err('usage: calliope attach (--url <ws-url> | --hub <url> --user <name>) [session-uri] [--read-only]');
495
+ return 1;
388
496
  }
389
- url = resolved.url;
390
- }
391
- if (!url) {
392
- err('usage: calliope attach (--url <ws-url> | --hub <url> --user <name>) [session-uri] [--read-only]');
393
- return 1;
394
- }
395
- const refused = await preflight(url, headers);
396
- if (refused) {
397
- return fallback(refused);
398
- }
399
- const ws = new WebSocket(url, { headers });
400
- const opened = await new Promise(resolve => {
401
- ws.addEventListener('open', () => resolve(true));
402
- ws.addEventListener('error', () => resolve(false));
403
- });
404
- if (!opened) {
405
- return fallback(fail('unreachable', 'the WebSocket upgrade failed'));
497
+ const refused = await preflight(url, headers, fetch, { signal: controller.signal, timeoutMs: remaining() });
498
+ if (refused)
499
+ return fallback(refused);
500
+ controller.signal.throwIfAborted();
501
+ ws = options.createSocket ? options.createSocket(url, headers) : new WebSocket(url, { headers });
502
+ const socket = ws;
503
+ const opened = await new Promise(resolve => {
504
+ const finish = (ok) => { controller.signal.removeEventListener('abort', aborted); resolve(ok); };
505
+ const aborted = () => { try {
506
+ socket.terminate ? socket.terminate() : socket.close();
507
+ }
508
+ catch { /* Already closed. */ } finish(false); };
509
+ socket.addEventListener('open', () => finish(true));
510
+ socket.addEventListener('error', () => finish(false));
511
+ socket.addEventListener('close', () => finish(false));
512
+ controller.signal.addEventListener('abort', aborted, { once: true });
513
+ if (controller.signal.aborted)
514
+ aborted();
515
+ });
516
+ if (!opened)
517
+ return fallback(fail('unreachable', controller.signal.aborted ? 'host setup timed out' : 'the WebSocket upgrade failed'));
518
+ conn = new AhpConnection(socket);
519
+ const call = (method, params) => conn.call(method, params, { signal: controller.signal, timeoutMs: remaining() });
520
+ const init = await call('initialize', {
521
+ channel: ROOT, protocolVersions: SUPPORTED_VERSIONS, clientId: `calliope-cli-${process.pid}`,
522
+ clientInfo: { name: 'calliope-cli', version: '1' }, initialSubscriptions: [ROOT],
523
+ });
524
+ const bad = checkInitialize(init);
525
+ if (bad)
526
+ return fallback(bad);
527
+ if (!opts.session) {
528
+ const list = await call('listSessions', { channel: ROOT });
529
+ if (list.error)
530
+ return fallback(fail('unreachable', 'the host could not list sessions'));
531
+ const items = (list.result?.items ?? []);
532
+ process.stdout.write(items.length ? '' : 'No sessions on this host.\n');
533
+ for (const session of items)
534
+ process.stdout.write(`${session.resource} ${session.title ?? ''} ${session.modifiedAt ?? ''}\n`);
535
+ return 0;
536
+ }
537
+ // Register follow before either subscription answers: frames may follow a
538
+ // snapshot in the same network read. Keep the setup deadline through both.
539
+ const chat = defaultChatUri(opts.session);
540
+ const sessionAnswer = call('subscribe', { channel: opts.session });
541
+ const chatAnswer = sessionAnswer.then(answer => answer.error ? answer : call('subscribe', { channel: chat }));
542
+ const { done } = follow(conn, opts.session, {
543
+ write: s => process.stdout.write(s), approve: opts.readOnly ? undefined : askYesNo, once: opts.once,
544
+ }, chatAnswer.then(answer => answer.result?.snapshot), sessionAnswer.then(answer => answer.result?.snapshot));
545
+ const answers = await Promise.all([sessionAnswer, chatAnswer]);
546
+ if (answers.some(answer => answer.error))
547
+ return fallback(fail('unreachable', 'the host could not subscribe to the session'));
548
+ clearTimeout(timer);
549
+ process.stderr.write(`attached to ${opts.session} (AHP ${init.result.protocolVersion}); Ctrl+C to detach\n`);
550
+ const ended = await done;
551
+ if (ended.startsWith('connection') || ended.startsWith('host') || ended.startsWith('invalid')) {
552
+ return fallback(fail('unreachable', 'the host connection was lost; the run remains on its host'));
553
+ }
554
+ return ended === 'turnComplete' ? 0 : 1;
406
555
  }
407
- const conn = new AhpConnection(ws);
408
- const init = await conn.call('initialize', {
409
- channel: ROOT, protocolVersions: SUPPORTED_VERSIONS, clientId: `calliope-cli-${process.pid}`,
410
- clientInfo: { name: 'calliope-cli', version: '1' }, initialSubscriptions: [ROOT],
411
- });
412
- const bad = checkInitialize(init);
413
- if (bad) {
414
- conn.close();
415
- return fallback(bad);
556
+ catch {
557
+ return fallback(fail('unreachable', controller.signal.aborted ? 'host setup timed out' : 'host attachment failed'));
416
558
  }
417
- if (!opts.session) {
418
- const list = await conn.call('listSessions', { channel: ROOT });
419
- if (list.error) {
420
- err(`calliope attach: the host could not list sessions: ${list.error.message.split('\n')[0]}`);
421
- conn.close();
422
- return 1;
423
- }
424
- const items = (list.result?.items ?? []);
425
- process.stdout.write(items.length ? '' : 'No sessions on this host.\n');
426
- for (const s of items) {
427
- process.stdout.write(`${s.resource} ${s.title ?? ''} ${s.modifiedAt ?? ''}\n`);
559
+ finally {
560
+ clearTimeout(timer);
561
+ controller.abort();
562
+ conn?.close();
563
+ if (!conn && ws) {
564
+ try {
565
+ ws.terminate ? ws.terminate() : ws.close();
566
+ }
567
+ catch { /* Already closed. */ }
428
568
  }
429
- conn.close();
430
- return 0;
431
569
  }
432
- // The session snapshot lists the input waiting in every chat, a subagent's
433
- // included; the chat snapshot holds the confirmations waiting on the default
434
- // chat, which is subscribed once the session answers. follow() listens from
435
- // the moment the first request goes out: an action can share a network read
436
- // with an answer, and Node delivers it before an awaited call resumes.
437
- const chat = defaultChatUri(opts.session);
438
- const sessionAnswer = conn.call('subscribe', { channel: opts.session });
439
- const chatAnswer = sessionAnswer.then(() => conn.call('subscribe', { channel: chat }));
440
- const { done } = follow(conn, opts.session, {
441
- write: s => process.stdout.write(s),
442
- approve: opts.readOnly ? undefined : askYesNo,
443
- once: opts.once,
444
- }, chatAnswer.then(answer => answer.result?.snapshot), sessionAnswer.then(answer => answer.result?.snapshot));
445
- process.stderr.write(`attached to ${opts.session} (AHP ${init.result.protocolVersion}); Ctrl+C to detach\n`);
446
- const ended = await done;
447
- conn.close();
448
- return ended === 'turnComplete' ? 0 : 1;
449
570
  }
450
571
  //# sourceMappingURL=attach.js.map