@tylerl0706/ahpx 0.2.0

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/dist/bin.js ADDED
@@ -0,0 +1,3090 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AhpClient,
4
+ AuthHandler,
5
+ ForwardingFormatter,
6
+ HealthChecker,
7
+ SessionPersistence,
8
+ SessionStore,
9
+ WebSocketForwarder,
10
+ WebhookForwarder,
11
+ createLogger,
12
+ ensureFileUri,
13
+ setVerbose
14
+ } from "./chunk-B6RV43UP.js";
15
+
16
+ // src/bin.ts
17
+ import { randomUUID as randomUUID3 } from "crypto";
18
+ import * as fs4 from "fs/promises";
19
+ import * as path4 from "path";
20
+ import { Command } from "commander";
21
+ import pc6 from "picocolors";
22
+
23
+ // src/completions.ts
24
+ var COMMANDS = ["connect", "server", "config", "session", "prompt", "exec", "cancel", "completions"];
25
+ var SERVER_SUBCOMMANDS = ["add", "list", "remove", "test"];
26
+ var SESSION_SUBCOMMANDS = ["new", "list", "show", "close", "history"];
27
+ var CONFIG_SUBCOMMANDS = ["show", "init"];
28
+ var COMPLETIONS_SUBCOMMANDS = ["bash", "zsh", "fish"];
29
+ var FORMATS = ["text", "json", "quiet"];
30
+ function bashCompletion() {
31
+ return `# ahpx bash completion
32
+ _ahpx_completions() {
33
+ local cur prev commands
34
+ COMPREPLY=()
35
+ cur="\${COMP_WORDS[COMP_CWORD]}"
36
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
37
+ commands="${COMMANDS.join(" ")}"
38
+
39
+ case "\${COMP_WORDS[1]}" in
40
+ server)
41
+ COMPREPLY=( $(compgen -W "${SERVER_SUBCOMMANDS.join(" ")}" -- "$cur") )
42
+ return 0
43
+ ;;
44
+ session)
45
+ COMPREPLY=( $(compgen -W "${SESSION_SUBCOMMANDS.join(" ")}" -- "$cur") )
46
+ return 0
47
+ ;;
48
+ config)
49
+ COMPREPLY=( $(compgen -W "${CONFIG_SUBCOMMANDS.join(" ")}" -- "$cur") )
50
+ return 0
51
+ ;;
52
+ completions)
53
+ COMPREPLY=( $(compgen -W "${COMPLETIONS_SUBCOMMANDS.join(" ")}" -- "$cur") )
54
+ return 0
55
+ ;;
56
+ esac
57
+
58
+ case "$prev" in
59
+ --format)
60
+ COMPREPLY=( $(compgen -W "${FORMATS.join(" ")}" -- "$cur") )
61
+ return 0
62
+ ;;
63
+ esac
64
+
65
+ if [[ "$cur" == -* ]]; then
66
+ COMPREPLY=( $(compgen -W "--format --verbose --json-strict --help --version" -- "$cur") )
67
+ return 0
68
+ fi
69
+
70
+ COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
71
+ return 0
72
+ }
73
+ complete -F _ahpx_completions ahpx
74
+ `;
75
+ }
76
+ function zshCompletion() {
77
+ return `#compdef ahpx
78
+ # ahpx zsh completion
79
+
80
+ _ahpx() {
81
+ local -a commands
82
+ commands=(
83
+ 'connect:Connect to an AHP server'
84
+ 'server:Manage saved server connections'
85
+ 'config:Manage ahpx configuration'
86
+ 'session:Manage agent sessions'
87
+ 'prompt:Send a prompt to an agent session'
88
+ 'exec:One-shot prompt with temp session'
89
+ 'cancel:Cancel the active turn'
90
+ 'completions:Generate shell completion scripts'
91
+ )
92
+
93
+ _arguments -C \\
94
+ '--format[Output format]:format:(${FORMATS.join(" ")})' \\
95
+ '--verbose[Enable debug logging]' \\
96
+ '--json-strict[Suppress non-JSON stderr output]' \\
97
+ '--help[Show help]' \\
98
+ '--version[Show version]' \\
99
+ '1:command:->command' \\
100
+ '*::arg:->args'
101
+
102
+ case $state in
103
+ command)
104
+ _describe -t commands 'ahpx commands' commands
105
+ ;;
106
+ args)
107
+ case $words[1] in
108
+ server)
109
+ _describe -t subcommands 'server subcommands' \\
110
+ '(${SERVER_SUBCOMMANDS.map((s) => `"${s}"`).join(" ")})'
111
+ ;;
112
+ session)
113
+ _describe -t subcommands 'session subcommands' \\
114
+ '(${SESSION_SUBCOMMANDS.map((s) => `"${s}"`).join(" ")})'
115
+ ;;
116
+ config)
117
+ _describe -t subcommands 'config subcommands' \\
118
+ '(${CONFIG_SUBCOMMANDS.map((s) => `"${s}"`).join(" ")})'
119
+ ;;
120
+ completions)
121
+ _describe -t subcommands 'completions subcommands' \\
122
+ '(${COMPLETIONS_SUBCOMMANDS.map((s) => `"${s}"`).join(" ")})'
123
+ ;;
124
+ esac
125
+ ;;
126
+ esac
127
+ }
128
+
129
+ _ahpx "$@"
130
+ `;
131
+ }
132
+ function fishCompletion() {
133
+ return `# ahpx fish completion
134
+
135
+ # Disable file completions by default
136
+ complete -c ahpx -f
137
+
138
+ # Top-level commands
139
+ ${COMMANDS.map((c) => `complete -c ahpx -n "__fish_use_subcommand" -a "${c}"`).join("\n")}
140
+
141
+ # Global flags
142
+ complete -c ahpx -l format -d "Output format" -xa "${FORMATS.join(" ")}"
143
+ complete -c ahpx -l verbose -d "Enable debug logging"
144
+ complete -c ahpx -l json-strict -d "Suppress non-JSON stderr output"
145
+
146
+ # server subcommands
147
+ ${SERVER_SUBCOMMANDS.map((s) => `complete -c ahpx -n "__fish_seen_subcommand_from server" -a "${s}"`).join("\n")}
148
+
149
+ # session subcommands
150
+ ${SESSION_SUBCOMMANDS.map((s) => `complete -c ahpx -n "__fish_seen_subcommand_from session" -a "${s}"`).join("\n")}
151
+
152
+ # config subcommands
153
+ ${CONFIG_SUBCOMMANDS.map((s) => `complete -c ahpx -n "__fish_seen_subcommand_from config" -a "${s}"`).join("\n")}
154
+
155
+ # completions subcommands
156
+ ${COMPLETIONS_SUBCOMMANDS.map((s) => `complete -c ahpx -n "__fish_seen_subcommand_from completions" -a "${s}"`).join("\n")}
157
+ `;
158
+ }
159
+
160
+ // src/config/index.ts
161
+ import * as fs2 from "fs/promises";
162
+ import * as os2 from "os";
163
+ import * as path2 from "path";
164
+
165
+ // src/config/connections.ts
166
+ import { randomUUID } from "crypto";
167
+ import * as fs from "fs/promises";
168
+ import * as os from "os";
169
+ import * as path from "path";
170
+ var log = createLogger("connections");
171
+ function isValidWsUrl(url) {
172
+ try {
173
+ const parsed = new URL(url);
174
+ return parsed.protocol === "ws:" || parsed.protocol === "wss:";
175
+ } catch {
176
+ return false;
177
+ }
178
+ }
179
+ function isLocalUrl(url) {
180
+ try {
181
+ const parsed = new URL(url);
182
+ const host = parsed.hostname;
183
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
184
+ } catch {
185
+ return false;
186
+ }
187
+ }
188
+ var ConnectionValidationError = class extends Error {
189
+ constructor(message) {
190
+ super(message);
191
+ this.name = "ConnectionValidationError";
192
+ }
193
+ };
194
+ var ConnectionStore = class {
195
+ filePath;
196
+ constructor(configDir) {
197
+ const dir = configDir ?? path.join(os.homedir(), ".ahpx");
198
+ this.filePath = path.join(dir, "connections.json");
199
+ }
200
+ /** Ensure the config directory exists. */
201
+ async ensureDir() {
202
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true, mode: 448 });
203
+ }
204
+ /** Read the connections file, returning an empty list if it doesn't exist. */
205
+ async read() {
206
+ try {
207
+ const raw = await fs.readFile(this.filePath, "utf-8");
208
+ return JSON.parse(raw);
209
+ } catch (err) {
210
+ if (err.code === "ENOENT") {
211
+ return { connections: [] };
212
+ }
213
+ log.warn("corrupt connections file, using empty list", { error: String(err) });
214
+ return { connections: [] };
215
+ }
216
+ }
217
+ /** Atomic write: write to temp file then rename. */
218
+ async write(data) {
219
+ await this.ensureDir();
220
+ const tmp = `${this.filePath}.${randomUUID()}.tmp`;
221
+ await fs.writeFile(tmp, `${JSON.stringify(data, null, " ")}
222
+ `, { mode: 384, encoding: "utf-8" });
223
+ await fs.rename(tmp, this.filePath);
224
+ }
225
+ /** List all connection profiles. */
226
+ async list() {
227
+ const data = await this.read();
228
+ return data.connections;
229
+ }
230
+ /** Get a connection by name, or undefined if not found. */
231
+ async get(name) {
232
+ const data = await this.read();
233
+ return data.connections.find((c) => c.name === name);
234
+ }
235
+ /** Get the default connection, or undefined if none is set. */
236
+ async getDefault() {
237
+ const data = await this.read();
238
+ return data.connections.find((c) => c.default === true);
239
+ }
240
+ /** Add a new connection profile. Throws if name already exists or URL is invalid. */
241
+ async add(profile) {
242
+ if (!isValidWsUrl(profile.url)) {
243
+ throw new ConnectionValidationError(`Invalid WebSocket URL: ${profile.url} (must be ws:// or wss://)`);
244
+ }
245
+ const data = await this.read();
246
+ if (data.connections.some((c) => c.name === profile.name)) {
247
+ throw new ConnectionValidationError(`Connection "${profile.name}" already exists`);
248
+ }
249
+ if (profile.default) {
250
+ for (const c of data.connections) {
251
+ c.default = false;
252
+ }
253
+ }
254
+ data.connections.push({ ...profile });
255
+ await this.write(data);
256
+ }
257
+ /** Remove a connection by name. Returns true if found and removed, false otherwise. */
258
+ async remove(name) {
259
+ const data = await this.read();
260
+ const idx = data.connections.findIndex((c) => c.name === name);
261
+ if (idx === -1) return false;
262
+ data.connections.splice(idx, 1);
263
+ await this.write(data);
264
+ return true;
265
+ }
266
+ /** Set a connection as the default. Throws if name not found. */
267
+ async setDefault(name) {
268
+ const data = await this.read();
269
+ const target = data.connections.find((c) => c.name === name);
270
+ if (!target) {
271
+ throw new ConnectionValidationError(`Connection "${name}" not found`);
272
+ }
273
+ for (const c of data.connections) {
274
+ c.default = c.name === name;
275
+ }
276
+ await this.write(data);
277
+ }
278
+ };
279
+
280
+ // src/config/index.ts
281
+ var CONFIG_KEYS = /* @__PURE__ */ new Set([
282
+ "defaultServer",
283
+ "defaultProvider",
284
+ "defaultModel",
285
+ "permissions",
286
+ "timeout",
287
+ "format",
288
+ "verbose"
289
+ ]);
290
+ var DEFAULT_CONFIG = Object.freeze({
291
+ permissions: "approve-reads",
292
+ timeout: 30,
293
+ format: "text",
294
+ verbose: false
295
+ });
296
+ function globalConfigDir() {
297
+ return path2.join(os2.homedir(), ".ahpx");
298
+ }
299
+ function globalConfigPath() {
300
+ return path2.join(globalConfigDir(), "config.json");
301
+ }
302
+ function projectConfigPath(cwd) {
303
+ return path2.join(cwd ?? process.cwd(), ".ahpxrc.json");
304
+ }
305
+ async function readJsonFile(filePath) {
306
+ try {
307
+ const raw = await fs2.readFile(filePath, "utf-8");
308
+ return JSON.parse(raw);
309
+ } catch (err) {
310
+ if (err.code === "ENOENT") {
311
+ return void 0;
312
+ }
313
+ throw err;
314
+ }
315
+ }
316
+ function pickConfigKeys(raw) {
317
+ const result = {};
318
+ for (const key of Object.keys(raw)) {
319
+ if (CONFIG_KEYS.has(key)) {
320
+ result[key] = raw[key];
321
+ }
322
+ }
323
+ return result;
324
+ }
325
+ async function loadConfig(options) {
326
+ const gPath = options?.globalPath ?? globalConfigPath();
327
+ const pPath = options?.projectPath ?? projectConfigPath();
328
+ const globalRaw = await readJsonFile(gPath);
329
+ const projectRaw = await readJsonFile(pPath);
330
+ const overrides = {};
331
+ if (options?.overrides) {
332
+ for (const [k, v] of Object.entries(options.overrides)) {
333
+ if (v !== void 0) {
334
+ overrides[k] = v;
335
+ }
336
+ }
337
+ }
338
+ return {
339
+ ...DEFAULT_CONFIG,
340
+ ...globalRaw ? pickConfigKeys(globalRaw) : {},
341
+ ...projectRaw ? pickConfigKeys(projectRaw) : {},
342
+ ...overrides
343
+ };
344
+ }
345
+ async function initGlobalConfig(configPath) {
346
+ const p = configPath ?? globalConfigPath();
347
+ try {
348
+ await fs2.access(p);
349
+ return false;
350
+ } catch {
351
+ await fs2.mkdir(path2.dirname(p), { recursive: true });
352
+ await fs2.writeFile(p, `${JSON.stringify(DEFAULT_CONFIG, null, " ")}
353
+ `, "utf-8");
354
+ return true;
355
+ }
356
+ }
357
+ async function loadConfigWithSources(options) {
358
+ const gPath = options?.globalPath ?? globalConfigPath();
359
+ const pPath = options?.projectPath ?? projectConfigPath();
360
+ const globalRaw = await readJsonFile(gPath);
361
+ const projectRaw = await readJsonFile(pPath);
362
+ const globalConfig = globalRaw ? pickConfigKeys(globalRaw) : {};
363
+ const projectConfig = projectRaw ? pickConfigKeys(projectRaw) : {};
364
+ const overrides = {};
365
+ if (options?.overrides) {
366
+ for (const [k, v] of Object.entries(options.overrides)) {
367
+ if (v !== void 0) {
368
+ overrides[k] = v;
369
+ }
370
+ }
371
+ }
372
+ const merged = {
373
+ ...DEFAULT_CONFIG,
374
+ ...globalConfig,
375
+ ...projectConfig,
376
+ ...overrides
377
+ };
378
+ const sources = {};
379
+ for (const key of CONFIG_KEYS) {
380
+ if (DEFAULT_CONFIG[key] !== void 0) {
381
+ sources[key] = "default";
382
+ }
383
+ }
384
+ for (const key of Object.keys(globalConfig)) {
385
+ sources[key] = "global";
386
+ }
387
+ for (const key of Object.keys(projectConfig)) {
388
+ sources[key] = "project";
389
+ }
390
+ for (const key of Object.keys(overrides)) {
391
+ sources[key] = "cli";
392
+ }
393
+ return { config: merged, sources, globalPath: gPath, projectPath: pPath };
394
+ }
395
+
396
+ // src/errors.ts
397
+ var ExitCode = {
398
+ Success: 0,
399
+ Error: 1,
400
+ Usage: 2,
401
+ Timeout: 3,
402
+ NoSession: 4,
403
+ PermissionDenied: 5,
404
+ Interrupted: 130
405
+ };
406
+ var AhpxError = class extends Error {
407
+ constructor(message, exitCode) {
408
+ super(message);
409
+ this.exitCode = exitCode;
410
+ this.name = "AhpxError";
411
+ }
412
+ };
413
+ var UsageError = class extends AhpxError {
414
+ constructor(message) {
415
+ super(message, ExitCode.Usage);
416
+ this.name = "UsageError";
417
+ }
418
+ };
419
+ var TimeoutError = class extends AhpxError {
420
+ constructor(message) {
421
+ super(message, ExitCode.Timeout);
422
+ this.name = "TimeoutError";
423
+ }
424
+ };
425
+ var NoSessionError = class extends AhpxError {
426
+ constructor(message) {
427
+ super(message, ExitCode.NoSession);
428
+ this.name = "NoSessionError";
429
+ }
430
+ };
431
+
432
+ // src/output/renderer.ts
433
+ import pc from "picocolors";
434
+ function textOf(v) {
435
+ return typeof v === "string" ? v : v.markdown;
436
+ }
437
+ var PromptRenderer = class {
438
+ constructor(out = process.stdout) {
439
+ this.out = out;
440
+ }
441
+ hasStreamedText = false;
442
+ reasoningOpen = false;
443
+ /** Append streaming text delta. */
444
+ onDelta(text) {
445
+ if (!this.hasStreamedText) {
446
+ this.out.write("\n");
447
+ this.hasStreamedText = true;
448
+ }
449
+ this.closeReasoningIfNeeded();
450
+ this.out.write(text);
451
+ }
452
+ /** Show [thinking] block text. */
453
+ onReasoning(text) {
454
+ if (!this.reasoningOpen) {
455
+ this.out.write(`${pc.dim("[thinking]")} `);
456
+ this.reasoningOpen = true;
457
+ }
458
+ this.out.write(pc.dim(text));
459
+ }
460
+ /** Tool call started (streaming parameters). */
461
+ onToolCallStart(_id, name) {
462
+ this.closeReasoningIfNeeded();
463
+ this.ensureNewline();
464
+ this.out.write(`${pc.yellow("[tool]")} ${name} ${pc.dim("(running)")}
465
+ `);
466
+ }
467
+ /** Tool call streaming parameter delta — silent, state tracked internally. */
468
+ onToolCallDelta(_id, _paramsDelta) {
469
+ }
470
+ /** Tool call parameters complete, pending confirmation. */
471
+ onToolCallReady(_id, call) {
472
+ this.closeReasoningIfNeeded();
473
+ this.ensureNewline();
474
+ const msg = textOf(call.invocationMessage);
475
+ this.out.write(`${pc.yellow("[tool]")} ${msg} ${pc.dim("(pending confirmation)")}
476
+ `);
477
+ }
478
+ /** Tool call completed with result. */
479
+ onToolCallComplete(_id, result) {
480
+ const msg = textOf(result.pastTenseMessage);
481
+ const color = result.success ? pc.green : pc.red;
482
+ this.out.write(`${color("[tool]")} ${msg} ${pc.dim("(completed)")}
483
+ `);
484
+ if (result.content) {
485
+ for (const block of result.content) {
486
+ if ("text" in block && block.text) {
487
+ const preview = block.text.length > 200 ? `${block.text.slice(0, 200)}\u2026` : block.text;
488
+ for (const line of preview.split("\n")) {
489
+ this.out.write(` ${pc.dim(line)}
490
+ `);
491
+ }
492
+ }
493
+ }
494
+ }
495
+ }
496
+ /** Tool call was cancelled. */
497
+ onToolCallCancelled(_id, reason) {
498
+ this.out.write(`${pc.red("[tool]")} cancelled: ${reason}
499
+ `);
500
+ }
501
+ /** Token usage report. */
502
+ onUsage(usage) {
503
+ const parts = [];
504
+ if (usage.inputTokens != null) {
505
+ parts.push(`${usage.inputTokens.toLocaleString()} in`);
506
+ }
507
+ if (usage.outputTokens != null) {
508
+ parts.push(`${usage.outputTokens.toLocaleString()} out`);
509
+ }
510
+ const model = usage.model ? ` (${usage.model})` : "";
511
+ if (parts.length > 0) {
512
+ this.out.write(pc.dim(`Tokens: ${parts.join(" / ")}${model}
513
+ `));
514
+ }
515
+ }
516
+ /** Turn completed successfully. */
517
+ onTurnComplete(_responseText) {
518
+ this.closeReasoningIfNeeded();
519
+ this.ensureNewline();
520
+ this.out.write(`
521
+ ${pc.green("[done]")} end_turn
522
+ `);
523
+ }
524
+ /** Turn ended with an error. */
525
+ onTurnError(error) {
526
+ this.closeReasoningIfNeeded();
527
+ this.ensureNewline();
528
+ this.out.write(`
529
+ ${pc.red("[error]")} ${error.message}
530
+ `);
531
+ }
532
+ /** Turn was cancelled. */
533
+ onTurnCancelled() {
534
+ this.closeReasoningIfNeeded();
535
+ this.ensureNewline();
536
+ this.out.write(`
537
+ ${pc.yellow("[cancelled]")} turn cancelled
538
+ `);
539
+ }
540
+ /** Session title changed. */
541
+ onTitleChanged(_title) {
542
+ }
543
+ // ── Private helpers ───────────────────────────────────────────────────
544
+ closeReasoningIfNeeded() {
545
+ if (this.reasoningOpen) {
546
+ this.out.write("\n\n");
547
+ this.reasoningOpen = false;
548
+ }
549
+ }
550
+ ensureNewline() {
551
+ }
552
+ };
553
+
554
+ // src/output/json-formatter.ts
555
+ var JsonFormatter = class {
556
+ constructor(out = process.stdout, strict = false, tags) {
557
+ this.out = out;
558
+ this.strict = strict;
559
+ this.tags = tags;
560
+ }
561
+ /** Whether strict mode is active (suppresses non-JSON stderr). */
562
+ get isStrict() {
563
+ return this.strict;
564
+ }
565
+ onDelta(text) {
566
+ this.emit("delta", { content: text });
567
+ }
568
+ onReasoning(text) {
569
+ this.emit("reasoning", { content: text });
570
+ }
571
+ onToolCallStart(id, name) {
572
+ this.emit("tool_call_start", { toolCallId: id, name });
573
+ }
574
+ onToolCallDelta(id, paramsDelta) {
575
+ this.emit("tool_call_delta", { toolCallId: id, content: paramsDelta });
576
+ }
577
+ onToolCallReady(id, call) {
578
+ this.emit("tool_call_ready", {
579
+ toolCallId: id,
580
+ toolName: call.toolName,
581
+ displayName: call.displayName,
582
+ invocationMessage: call.invocationMessage,
583
+ ...call.toolInput !== void 0 ? { toolInput: call.toolInput } : {}
584
+ });
585
+ }
586
+ onToolCallComplete(id, result) {
587
+ this.emit("tool_call_complete", { toolCallId: id, result });
588
+ }
589
+ onToolCallCancelled(id, reason) {
590
+ this.emit("tool_call_cancelled", { toolCallId: id, reason });
591
+ }
592
+ onUsage(usage) {
593
+ this.emit("usage", { usage });
594
+ }
595
+ onTurnComplete(responseText) {
596
+ this.emit("turn_complete", { responseText });
597
+ }
598
+ onTurnError(error) {
599
+ this.emit("turn_error", { error });
600
+ }
601
+ onTurnCancelled() {
602
+ this.emit("turn_cancelled", {});
603
+ }
604
+ onTitleChanged(title) {
605
+ this.emit("title_changed", { title });
606
+ }
607
+ // ── Helpers ───────────────────────────────────────────────────────────
608
+ emit(type, data) {
609
+ const envelope = {
610
+ type,
611
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
612
+ ...this.tags && Object.keys(this.tags).length > 0 ? { tags: this.tags } : {},
613
+ data
614
+ };
615
+ this.out.write(`${JSON.stringify(envelope)}
616
+ `);
617
+ }
618
+ };
619
+
620
+ // src/output/quiet-formatter.ts
621
+ var QuietFormatter = class {
622
+ constructor(out = process.stdout, err = process.stderr) {
623
+ this.out = out;
624
+ this.err = err;
625
+ }
626
+ onDelta(_text) {
627
+ }
628
+ onReasoning(_text) {
629
+ }
630
+ onToolCallStart(_id, _name) {
631
+ }
632
+ onToolCallDelta(_id, _paramsDelta) {
633
+ }
634
+ onToolCallReady(_id, _call) {
635
+ }
636
+ onToolCallComplete(_id, _result) {
637
+ }
638
+ onToolCallCancelled(_id, _reason) {
639
+ }
640
+ onUsage(_usage) {
641
+ }
642
+ onTurnComplete(responseText) {
643
+ if (responseText) {
644
+ this.out.write(`${responseText}
645
+ `);
646
+ }
647
+ }
648
+ onTurnError(error) {
649
+ this.err.write(`${error.message}
650
+ `);
651
+ }
652
+ onTurnCancelled() {
653
+ }
654
+ onTitleChanged(_title) {
655
+ }
656
+ };
657
+
658
+ // src/output/spinner.ts
659
+ import pc2 from "picocolors";
660
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
661
+ var INTERVAL = 80;
662
+ function startSpinner(message, enabled = true) {
663
+ if (!enabled || !process.stderr.isTTY) {
664
+ let stopped2 = false;
665
+ return {
666
+ update() {
667
+ },
668
+ stop() {
669
+ stopped2 = true;
670
+ },
671
+ succeed(msg) {
672
+ if (stopped2) return;
673
+ stopped2 = true;
674
+ process.stderr.write(`${pc2.green("\u2713")} ${msg}
675
+ `);
676
+ },
677
+ fail(msg) {
678
+ if (stopped2) return;
679
+ stopped2 = true;
680
+ process.stderr.write(`${pc2.red("\u2717")} ${msg}
681
+ `);
682
+ }
683
+ };
684
+ }
685
+ let frameIdx = 0;
686
+ let text = message;
687
+ let stopped = false;
688
+ const render = () => {
689
+ if (stopped) return;
690
+ const frame = FRAMES[frameIdx % FRAMES.length];
691
+ process.stderr.write(`\r\x1B[K${frame} ${text}`);
692
+ frameIdx++;
693
+ };
694
+ const timer = setInterval(render, INTERVAL);
695
+ render();
696
+ return {
697
+ update(msg) {
698
+ text = msg;
699
+ },
700
+ stop(finalMessage) {
701
+ if (stopped) return;
702
+ stopped = true;
703
+ clearInterval(timer);
704
+ process.stderr.write("\r\x1B[K");
705
+ if (finalMessage) {
706
+ process.stderr.write(`${finalMessage}
707
+ `);
708
+ }
709
+ },
710
+ succeed(msg) {
711
+ if (stopped) return;
712
+ stopped = true;
713
+ clearInterval(timer);
714
+ process.stderr.write(`\r\x1B[K${pc2.green("\u2713")} ${msg}
715
+ `);
716
+ },
717
+ fail(msg) {
718
+ if (stopped) return;
719
+ stopped = true;
720
+ clearInterval(timer);
721
+ process.stderr.write(`\r\x1B[K${pc2.red("\u2717")} ${msg}
722
+ `);
723
+ }
724
+ };
725
+ }
726
+
727
+ // src/output/index.ts
728
+ function createFormatter(format, options) {
729
+ switch (format) {
730
+ case "json":
731
+ return new JsonFormatter(options?.out, options?.jsonStrict, options?.tags);
732
+ case "quiet":
733
+ return new QuietFormatter(options?.out, options?.err);
734
+ default:
735
+ return new PromptRenderer(options?.out);
736
+ }
737
+ }
738
+
739
+ // src/permissions/handler.ts
740
+ import * as readline from "readline";
741
+ import pc3 from "picocolors";
742
+ var PermissionHandler = class {
743
+ constructor(mode, options) {
744
+ this.mode = mode;
745
+ this.input = options?.input ?? process.stdin;
746
+ this.output = options?.output ?? process.stdout;
747
+ }
748
+ input;
749
+ output;
750
+ /**
751
+ * Handle tool call confirmation.
752
+ * Returns true (approved) or false (denied).
753
+ */
754
+ async handleToolConfirmation(toolCall) {
755
+ if (this.mode === "approve-all") {
756
+ this.output.write(`${pc3.dim(" [auto-approved]")}
757
+ `);
758
+ return true;
759
+ }
760
+ if (this.mode === "deny-all") {
761
+ this.output.write(`${pc3.dim(" [denied]")}
762
+ `);
763
+ return false;
764
+ }
765
+ const msg = typeof toolCall.invocationMessage === "string" ? toolCall.invocationMessage : toolCall.invocationMessage.markdown;
766
+ return this.promptUser("tool", msg);
767
+ }
768
+ /**
769
+ * Interactive y/N prompt.
770
+ */
771
+ promptUser(kind, detail) {
772
+ return new Promise((resolve3) => {
773
+ let resolved = false;
774
+ const label = kind.charAt(0).toUpperCase() + kind.slice(1);
775
+ this.output.write(` Allow ${label}: ${detail}? (y/N): `);
776
+ const rl = readline.createInterface({
777
+ input: this.input,
778
+ terminal: false
779
+ });
780
+ rl.once("line", (answer) => {
781
+ if (resolved) return;
782
+ resolved = true;
783
+ rl.close();
784
+ const approved = answer.trim().toLowerCase() === "y";
785
+ if (approved) {
786
+ this.output.write(`${pc3.dim(" [approved]")}
787
+ `);
788
+ } else {
789
+ this.output.write(`${pc3.dim(" [denied]")}
790
+ `);
791
+ }
792
+ resolve3(approved);
793
+ });
794
+ rl.once("close", () => {
795
+ if (resolved) return;
796
+ resolved = true;
797
+ resolve3(false);
798
+ });
799
+ });
800
+ }
801
+ };
802
+
803
+ // src/prompt/controller.ts
804
+ import { randomUUID as randomUUID2 } from "crypto";
805
+ var TurnController = class {
806
+ constructor(client, sessionUri, renderer, permissionHandler) {
807
+ this.client = client;
808
+ this.sessionUri = sessionUri;
809
+ this.renderer = renderer;
810
+ this.permissionHandler = permissionHandler;
811
+ }
812
+ activeTurnId;
813
+ cancelled = false;
814
+ /** The currently active turn ID, if any. */
815
+ get turnId() {
816
+ return this.activeTurnId;
817
+ }
818
+ /**
819
+ * Send a message and stream the full response.
820
+ */
821
+ async prompt(text, attachments, options) {
822
+ const turnId = randomUUID2();
823
+ this.activeTurnId = turnId;
824
+ this.cancelled = false;
825
+ let responseText = "";
826
+ let toolCallCount = 0;
827
+ let usage;
828
+ return new Promise((resolve3) => {
829
+ let idleTimer;
830
+ const resetIdleTimer = () => {
831
+ if (idleTimer !== void 0) clearTimeout(idleTimer);
832
+ if (options?.idleTimeout) {
833
+ idleTimer = setTimeout(() => {
834
+ cleanup();
835
+ this.renderer.onTurnCancelled();
836
+ resolve3({
837
+ turnId,
838
+ responseText,
839
+ toolCalls: toolCallCount,
840
+ usage: usage ? {
841
+ inputTokens: usage.inputTokens ?? 0,
842
+ outputTokens: usage.outputTokens ?? 0,
843
+ model: usage.model
844
+ } : void 0,
845
+ state: "idle_timeout"
846
+ });
847
+ }, options.idleTimeout);
848
+ }
849
+ };
850
+ const onAction = (envelope) => {
851
+ const action = envelope.action;
852
+ if (!("session" in action) || action.session !== this.sessionUri) {
853
+ return;
854
+ }
855
+ if ("turnId" in action && action.turnId !== turnId) {
856
+ return;
857
+ }
858
+ resetIdleTimer();
859
+ switch (action.type) {
860
+ case "session/delta" /* SessionDelta */: {
861
+ const a = action;
862
+ responseText += a.content;
863
+ this.renderer.onDelta(a.content);
864
+ break;
865
+ }
866
+ case "session/reasoning" /* SessionReasoning */: {
867
+ const a = action;
868
+ this.renderer.onReasoning(a.content);
869
+ break;
870
+ }
871
+ case "session/toolCallStart" /* SessionToolCallStart */: {
872
+ const a = action;
873
+ toolCallCount++;
874
+ this.renderer.onToolCallStart(a.toolCallId, a.displayName);
875
+ break;
876
+ }
877
+ case "session/toolCallDelta" /* SessionToolCallDelta */: {
878
+ const a = action;
879
+ this.renderer.onToolCallDelta(a.toolCallId, a.content);
880
+ break;
881
+ }
882
+ case "session/toolCallReady" /* SessionToolCallReady */: {
883
+ const a = action;
884
+ const serverConfirmed = !!a.confirmed;
885
+ let toolClientId;
886
+ let stateName;
887
+ let stateDisplayName;
888
+ const session2 = this.client.state.getSession(this.sessionUri);
889
+ if (session2?.activeTurn) {
890
+ for (const part of session2.activeTurn.responseParts) {
891
+ if (part.kind === "toolCall" /* ToolCall */ && part.toolCall.toolCallId === a.toolCallId) {
892
+ toolClientId = part.toolCall.toolClientId;
893
+ stateName = part.toolCall.toolName;
894
+ stateDisplayName = part.toolCall.displayName;
895
+ break;
896
+ }
897
+ }
898
+ }
899
+ if (serverConfirmed) {
900
+ const isClientTool = toolClientId !== void 0 && toolClientId === this.client.clientId;
901
+ if (isClientTool) {
902
+ break;
903
+ }
904
+ }
905
+ const callInfo = {
906
+ toolCallId: a.toolCallId,
907
+ toolName: stateName ?? a.toolCallId,
908
+ displayName: stateDisplayName ?? a.toolCallId,
909
+ invocationMessage: a.invocationMessage,
910
+ toolInput: a.toolInput
911
+ };
912
+ this.renderer.onToolCallReady(a.toolCallId, callInfo);
913
+ this.permissionHandler.handleToolConfirmation(callInfo).then((approved) => {
914
+ if (this.cancelled) return;
915
+ if (serverConfirmed && approved) {
916
+ return;
917
+ }
918
+ if (approved) {
919
+ this.client.dispatchAction({
920
+ type: "session/toolCallConfirmed" /* SessionToolCallConfirmed */,
921
+ session: this.sessionUri,
922
+ turnId,
923
+ toolCallId: a.toolCallId,
924
+ approved: true,
925
+ confirmed: "user-action" /* UserAction */
926
+ });
927
+ } else {
928
+ this.client.dispatchAction({
929
+ type: "session/toolCallConfirmed" /* SessionToolCallConfirmed */,
930
+ session: this.sessionUri,
931
+ turnId,
932
+ toolCallId: a.toolCallId,
933
+ approved: false,
934
+ reason: "denied" /* Denied */
935
+ });
936
+ }
937
+ });
938
+ break;
939
+ }
940
+ case "session/toolCallComplete" /* SessionToolCallComplete */: {
941
+ const a = action;
942
+ this.renderer.onToolCallComplete(a.toolCallId, a.result);
943
+ break;
944
+ }
945
+ case "session/usage" /* SessionUsage */: {
946
+ const a = action;
947
+ usage = a.usage;
948
+ this.renderer.onUsage(a.usage);
949
+ break;
950
+ }
951
+ case "session/titleChanged" /* SessionTitleChanged */: {
952
+ const a = action;
953
+ this.renderer.onTitleChanged(a.title);
954
+ break;
955
+ }
956
+ case "session/turnComplete" /* SessionTurnComplete */: {
957
+ cleanup();
958
+ this.renderer.onTurnComplete(responseText);
959
+ resolve3({
960
+ turnId,
961
+ responseText,
962
+ toolCalls: toolCallCount,
963
+ usage: usage ? {
964
+ inputTokens: usage.inputTokens ?? 0,
965
+ outputTokens: usage.outputTokens ?? 0,
966
+ model: usage.model
967
+ } : void 0,
968
+ state: "complete"
969
+ });
970
+ break;
971
+ }
972
+ case "session/error" /* SessionError */: {
973
+ const a = action;
974
+ cleanup();
975
+ this.renderer.onTurnError(a.error);
976
+ resolve3({
977
+ turnId,
978
+ responseText,
979
+ toolCalls: toolCallCount,
980
+ usage: usage ? {
981
+ inputTokens: usage.inputTokens ?? 0,
982
+ outputTokens: usage.outputTokens ?? 0,
983
+ model: usage.model
984
+ } : void 0,
985
+ state: "error",
986
+ error: a.error.message
987
+ });
988
+ break;
989
+ }
990
+ case "session/turnCancelled" /* SessionTurnCancelled */: {
991
+ cleanup();
992
+ this.renderer.onTurnCancelled();
993
+ resolve3({
994
+ turnId,
995
+ responseText,
996
+ toolCalls: toolCallCount,
997
+ usage: usage ? {
998
+ inputTokens: usage.inputTokens ?? 0,
999
+ outputTokens: usage.outputTokens ?? 0,
1000
+ model: usage.model
1001
+ } : void 0,
1002
+ state: "cancelled"
1003
+ });
1004
+ break;
1005
+ }
1006
+ default:
1007
+ break;
1008
+ }
1009
+ };
1010
+ const cleanup = () => {
1011
+ if (idleTimer !== void 0) clearTimeout(idleTimer);
1012
+ this.client.removeListener("action", onAction);
1013
+ this.activeTurnId = void 0;
1014
+ };
1015
+ this.client.on("action", onAction);
1016
+ this.client.dispatchAction({
1017
+ type: "session/turnStarted" /* SessionTurnStarted */,
1018
+ session: this.sessionUri,
1019
+ turnId,
1020
+ userMessage: {
1021
+ text,
1022
+ ...attachments && attachments.length > 0 ? { attachments } : {}
1023
+ }
1024
+ });
1025
+ resetIdleTimer();
1026
+ });
1027
+ }
1028
+ /**
1029
+ * Cancel the active turn.
1030
+ */
1031
+ async cancel() {
1032
+ if (!this.activeTurnId) return;
1033
+ this.cancelled = true;
1034
+ this.client.dispatchAction({
1035
+ type: "session/turnCancelled" /* SessionTurnCancelled */,
1036
+ session: this.sessionUri,
1037
+ turnId: this.activeTurnId
1038
+ });
1039
+ }
1040
+ };
1041
+
1042
+ // src/session/scope.ts
1043
+ import * as fs3 from "fs/promises";
1044
+ import * as path3 from "path";
1045
+ async function findGitRoot(from) {
1046
+ let current = path3.resolve(from);
1047
+ while (true) {
1048
+ try {
1049
+ await fs3.access(path3.join(current, ".git"));
1050
+ return current;
1051
+ } catch {
1052
+ }
1053
+ const parent = path3.dirname(current);
1054
+ if (parent === current) {
1055
+ return void 0;
1056
+ }
1057
+ current = parent;
1058
+ }
1059
+ }
1060
+ async function resolveSession(options) {
1061
+ const { serverName, cwd, name, store: store2 } = options;
1062
+ const resolvedCwd = path3.resolve(cwd);
1063
+ const gitRoot = await findGitRoot(resolvedCwd);
1064
+ if (!gitRoot) {
1065
+ return store2.getByScope({ serverName, workingDirectory: resolvedCwd, name });
1066
+ }
1067
+ let current = resolvedCwd;
1068
+ while (true) {
1069
+ const match = await store2.getByScope({ serverName, workingDirectory: current, name });
1070
+ if (match) return match;
1071
+ if (current === gitRoot) break;
1072
+ const parent = path3.dirname(current);
1073
+ if (parent === current) break;
1074
+ current = parent;
1075
+ }
1076
+ return void 0;
1077
+ }
1078
+
1079
+ // src/session/connect-helper.ts
1080
+ import pc4 from "picocolors";
1081
+ async function withConnection(options, fn) {
1082
+ const store2 = new ConnectionStore();
1083
+ const { server: server2, config: config2, timeout } = options;
1084
+ let url;
1085
+ let token;
1086
+ let name;
1087
+ if (server2 && isValidWsUrl(server2)) {
1088
+ url = server2;
1089
+ name = server2;
1090
+ } else if (server2) {
1091
+ const conn = await store2.get(server2);
1092
+ if (!conn) {
1093
+ throw new Error(`Unknown connection "${server2}". Run ${pc4.bold("ahpx server list")} to see saved connections.`);
1094
+ }
1095
+ url = conn.url;
1096
+ token = conn.token;
1097
+ name = conn.name;
1098
+ } else if (config2.defaultServer) {
1099
+ const conn = await store2.get(config2.defaultServer);
1100
+ if (!conn) {
1101
+ throw new Error(
1102
+ `Default server "${config2.defaultServer}" not found in connections. Run ${pc4.bold("ahpx server list")} to check.`
1103
+ );
1104
+ }
1105
+ url = conn.url;
1106
+ token = conn.token;
1107
+ name = conn.name;
1108
+ } else {
1109
+ const def = await store2.getDefault();
1110
+ if (!def) {
1111
+ throw new Error(
1112
+ `No server specified and no default is set.
1113
+ Run ${pc4.bold("ahpx server add <name> --url <ws://...> --default")} to save one.`
1114
+ );
1115
+ }
1116
+ url = def.url;
1117
+ token = def.token;
1118
+ name = def.name;
1119
+ }
1120
+ const client = new AhpClient({
1121
+ connectTimeout: timeout ?? (config2.timeout ? config2.timeout * 1e3 : 1e4),
1122
+ initialSubscriptions: ["agenthost:/root"]
1123
+ });
1124
+ try {
1125
+ await client.connect(url);
1126
+ if (token) {
1127
+ await client.authenticate(url, token);
1128
+ }
1129
+ const authHandler = new AuthHandler(client, { token });
1130
+ const onNotification = (notification) => {
1131
+ if (notification.type === "notify/authRequired" /* AuthRequired */) {
1132
+ const authNotification = notification;
1133
+ authHandler.handleAuthRequired({ resource: authNotification.resource }).catch(() => {
1134
+ });
1135
+ }
1136
+ };
1137
+ client.on("notification", onNotification);
1138
+ try {
1139
+ await fn(client, { name, url, token });
1140
+ } finally {
1141
+ client.removeListener("notification", onNotification);
1142
+ }
1143
+ } finally {
1144
+ await client.disconnect();
1145
+ }
1146
+ }
1147
+
1148
+ // src/watch/watcher.ts
1149
+ import pc5 from "picocolors";
1150
+ var SessionWatcher = class {
1151
+ constructor(client, sessionUri, formatter, options = {}) {
1152
+ this.client = client;
1153
+ this.sessionUri = sessionUri;
1154
+ this.formatter = formatter;
1155
+ this.statusOut = options.statusOut ?? process.stderr;
1156
+ }
1157
+ onAction;
1158
+ onDisconnect;
1159
+ stopped = false;
1160
+ resolveWatch;
1161
+ statusOut;
1162
+ /**
1163
+ * Start watching — subscribes to the session and streams all actions.
1164
+ * Resolves when `stop()` is called or the session is disposed.
1165
+ *
1166
+ * Listeners are registered synchronously so callers can emit events
1167
+ * immediately after calling watch() without a microtask gap.
1168
+ */
1169
+ async watch() {
1170
+ this.stopped = false;
1171
+ const finished = new Promise((resolve3) => {
1172
+ this.resolveWatch = resolve3;
1173
+ this.onAction = (envelope) => {
1174
+ if (this.stopped) return;
1175
+ this.handleAction(envelope);
1176
+ };
1177
+ this.client.on("action", this.onAction);
1178
+ this.onDisconnect = () => {
1179
+ this.cleanup();
1180
+ resolve3();
1181
+ };
1182
+ this.client.once("disconnected", this.onDisconnect);
1183
+ });
1184
+ try {
1185
+ await this.client.subscribe(this.sessionUri);
1186
+ const sessionState = this.client.state.getSession(this.sessionUri);
1187
+ if (!sessionState) {
1188
+ this.cleanup();
1189
+ throw new Error(`Session ${this.sessionUri} not found after subscribe`);
1190
+ }
1191
+ this.showCurrentState(sessionState);
1192
+ } catch (err) {
1193
+ this.cleanup();
1194
+ throw err;
1195
+ }
1196
+ return finished;
1197
+ }
1198
+ /**
1199
+ * Stop watching and clean up listeners.
1200
+ */
1201
+ stop() {
1202
+ this.cleanup();
1203
+ }
1204
+ /**
1205
+ * Show the current in-progress state when joining mid-turn.
1206
+ */
1207
+ showCurrentState(state) {
1208
+ const turn = state.activeTurn;
1209
+ if (!turn) return;
1210
+ this.statusOut.write(pc5.dim("[watch] Joining turn in progress...\n"));
1211
+ for (const part of turn.responseParts) {
1212
+ switch (part.kind) {
1213
+ case "markdown" /* Markdown */:
1214
+ if (part.content) {
1215
+ this.formatter.onDelta(part.content);
1216
+ }
1217
+ break;
1218
+ case "reasoning" /* Reasoning */:
1219
+ if (part.content) {
1220
+ this.formatter.onReasoning(part.content);
1221
+ }
1222
+ break;
1223
+ case "toolCall" /* ToolCall */: {
1224
+ const tc = part.toolCall;
1225
+ if (tc.status === "streaming" /* Streaming */ || tc.status === "running" /* Running */) {
1226
+ this.formatter.onToolCallStart(tc.toolCallId, tc.displayName);
1227
+ } else if (tc.status === "pending-confirmation" /* PendingConfirmation */) {
1228
+ const info = {
1229
+ toolCallId: tc.toolCallId,
1230
+ toolName: tc.toolName,
1231
+ displayName: tc.displayName,
1232
+ invocationMessage: tc.invocationMessage,
1233
+ toolInput: tc.toolInput
1234
+ };
1235
+ this.formatter.onToolCallReady(tc.toolCallId, info);
1236
+ } else if (tc.status === "completed" /* Completed */) {
1237
+ this.formatter.onToolCallComplete(tc.toolCallId, {
1238
+ success: tc.success,
1239
+ pastTenseMessage: tc.pastTenseMessage,
1240
+ content: tc.content
1241
+ });
1242
+ }
1243
+ break;
1244
+ }
1245
+ }
1246
+ }
1247
+ }
1248
+ /**
1249
+ * Route an incoming action to the formatter.
1250
+ */
1251
+ handleAction(envelope) {
1252
+ const action = envelope.action;
1253
+ if (!("session" in action) || action.session !== this.sessionUri) {
1254
+ return;
1255
+ }
1256
+ switch (action.type) {
1257
+ case "session/delta" /* SessionDelta */: {
1258
+ const a = action;
1259
+ this.formatter.onDelta(a.content);
1260
+ break;
1261
+ }
1262
+ case "session/reasoning" /* SessionReasoning */: {
1263
+ const a = action;
1264
+ this.formatter.onReasoning(a.content);
1265
+ break;
1266
+ }
1267
+ case "session/turnStarted" /* SessionTurnStarted */: {
1268
+ const origin = this.getOriginLabel(envelope);
1269
+ this.statusOut.write(pc5.dim(`[watch] Turn started${origin}
1270
+ `));
1271
+ break;
1272
+ }
1273
+ case "session/toolCallStart" /* SessionToolCallStart */: {
1274
+ const a = action;
1275
+ this.formatter.onToolCallStart(a.toolCallId, a.displayName);
1276
+ break;
1277
+ }
1278
+ case "session/toolCallDelta" /* SessionToolCallDelta */: {
1279
+ const a = action;
1280
+ this.formatter.onToolCallDelta(a.toolCallId, a.content);
1281
+ break;
1282
+ }
1283
+ case "session/toolCallReady" /* SessionToolCallReady */: {
1284
+ const a = action;
1285
+ const info = {
1286
+ toolCallId: a.toolCallId,
1287
+ toolName: a.toolCallId,
1288
+ displayName: a.toolCallId,
1289
+ invocationMessage: a.invocationMessage,
1290
+ toolInput: a.toolInput
1291
+ };
1292
+ const session2 = this.client.state.getSession(this.sessionUri);
1293
+ if (session2?.activeTurn) {
1294
+ for (const part of session2.activeTurn.responseParts) {
1295
+ if (part.kind === "toolCall" /* ToolCall */ && part.toolCall.toolCallId === a.toolCallId) {
1296
+ info.toolName = part.toolCall.toolName;
1297
+ info.displayName = part.toolCall.displayName;
1298
+ break;
1299
+ }
1300
+ }
1301
+ }
1302
+ this.formatter.onToolCallReady(a.toolCallId, info);
1303
+ break;
1304
+ }
1305
+ case "session/toolCallComplete" /* SessionToolCallComplete */: {
1306
+ const a = action;
1307
+ this.formatter.onToolCallComplete(a.toolCallId, a.result);
1308
+ break;
1309
+ }
1310
+ case "session/usage" /* SessionUsage */: {
1311
+ const a = action;
1312
+ this.formatter.onUsage(a.usage);
1313
+ break;
1314
+ }
1315
+ case "session/titleChanged" /* SessionTitleChanged */: {
1316
+ const a = action;
1317
+ this.formatter.onTitleChanged(a.title);
1318
+ break;
1319
+ }
1320
+ case "session/turnComplete" /* SessionTurnComplete */: {
1321
+ const session2 = this.client.state.getSession(this.sessionUri);
1322
+ const lastTurn = session2?.turns[session2.turns.length - 1];
1323
+ let responseText = "";
1324
+ if (lastTurn) {
1325
+ for (const p of lastTurn.responseParts) {
1326
+ if (p.kind === "markdown" /* Markdown */) {
1327
+ responseText += p.content;
1328
+ }
1329
+ }
1330
+ }
1331
+ this.formatter.onTurnComplete(responseText);
1332
+ break;
1333
+ }
1334
+ case "session/error" /* SessionError */: {
1335
+ const a = action;
1336
+ this.formatter.onTurnError(a.error);
1337
+ break;
1338
+ }
1339
+ case "session/turnCancelled" /* SessionTurnCancelled */: {
1340
+ this.formatter.onTurnCancelled();
1341
+ break;
1342
+ }
1343
+ default:
1344
+ break;
1345
+ }
1346
+ }
1347
+ /**
1348
+ * Get a label describing who started the turn (if from another client).
1349
+ */
1350
+ getOriginLabel(envelope) {
1351
+ if (envelope.origin) {
1352
+ return ` (from ${envelope.origin.clientId})`;
1353
+ }
1354
+ return "";
1355
+ }
1356
+ /**
1357
+ * Clean up listeners and resolve the watch promise.
1358
+ */
1359
+ cleanup() {
1360
+ if (this.stopped) return;
1361
+ this.stopped = true;
1362
+ if (this.onAction) {
1363
+ this.client.removeListener("action", this.onAction);
1364
+ this.onAction = void 0;
1365
+ }
1366
+ if (this.onDisconnect) {
1367
+ this.client.removeListener("disconnected", this.onDisconnect);
1368
+ this.onDisconnect = void 0;
1369
+ }
1370
+ if (this.resolveWatch) {
1371
+ this.resolveWatch();
1372
+ this.resolveWatch = void 0;
1373
+ }
1374
+ }
1375
+ };
1376
+
1377
+ // src/bin.ts
1378
+ var store = new ConnectionStore();
1379
+ var sessionStore = new SessionStore();
1380
+ var sessionPersistence = new SessionPersistence(sessionStore);
1381
+ function turnResponseText(turn) {
1382
+ let text = "";
1383
+ for (const p of turn.responseParts) {
1384
+ if (p.kind === "markdown" /* Markdown */) {
1385
+ text += p.content;
1386
+ }
1387
+ }
1388
+ return text;
1389
+ }
1390
+ function turnToolCallCount(turn) {
1391
+ let count = 0;
1392
+ for (const p of turn.responseParts) {
1393
+ if (p.kind === "toolCall" /* ToolCall */) {
1394
+ count++;
1395
+ }
1396
+ }
1397
+ return count;
1398
+ }
1399
+ function parseGlobalOpts(cmd) {
1400
+ const opts = cmd.optsWithGlobals();
1401
+ return {
1402
+ format: opts.format ?? "text",
1403
+ jsonStrict: opts.jsonStrict ?? false,
1404
+ verbose: opts.verbose ?? false
1405
+ };
1406
+ }
1407
+ function formatterFromOpts(globalOpts, tags) {
1408
+ return createFormatter(globalOpts.format, { jsonStrict: globalOpts.jsonStrict, tags });
1409
+ }
1410
+ function parseTags(raw) {
1411
+ if (!raw || raw.length === 0) return void 0;
1412
+ const tags = {};
1413
+ for (const entry of raw) {
1414
+ const eq = entry.indexOf("=");
1415
+ if (eq <= 0) {
1416
+ throw new UsageError(`Invalid --tag format: "${entry}". Expected key=value`);
1417
+ }
1418
+ tags[entry.slice(0, eq)] = entry.slice(eq + 1);
1419
+ }
1420
+ return tags;
1421
+ }
1422
+ function parseIdleTimeout(raw) {
1423
+ if (raw === void 0) return void 0;
1424
+ const seconds = Number.parseInt(raw, 10);
1425
+ if (Number.isNaN(seconds) || seconds <= 0) {
1426
+ throw new UsageError(`--idle-timeout must be a positive integer (got: "${raw}")`);
1427
+ }
1428
+ return seconds;
1429
+ }
1430
+ function parseForwardHeaders(raw) {
1431
+ if (!raw) return void 0;
1432
+ try {
1433
+ const parsed = JSON.parse(raw);
1434
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1435
+ throw new UsageError(`--forward-headers must be a JSON object (e.g. '{"Authorization": "Bearer ..."}')`);
1436
+ }
1437
+ return parsed;
1438
+ } catch (err) {
1439
+ if (err instanceof UsageError) throw err;
1440
+ throw new UsageError(`--forward-headers must be valid JSON: ${err instanceof Error ? err.message : String(err)}`);
1441
+ }
1442
+ }
1443
+ function buildForwarders(opts) {
1444
+ const forwarders = [];
1445
+ const headers = parseForwardHeaders(opts.forwardHeaders);
1446
+ const filter = opts.forwardFilter?.split(",").map((s) => s.trim()).filter(Boolean);
1447
+ if (opts.forwardWebhook) {
1448
+ for (const url of opts.forwardWebhook) {
1449
+ forwarders.push(new WebhookForwarder({ url, headers, filter }));
1450
+ }
1451
+ }
1452
+ if (opts.forwardWs) {
1453
+ for (const url of opts.forwardWs) {
1454
+ forwarders.push(new WebSocketForwarder({ url, headers, filter }));
1455
+ }
1456
+ }
1457
+ return forwarders;
1458
+ }
1459
+ function spinnersEnabled(globalOpts) {
1460
+ return globalOpts.format === "text" && !!process.stdout.isTTY;
1461
+ }
1462
+ var program = new Command().name("ahpx").description(
1463
+ `Agent Host Protocol CLI \u2014 manage AHP server connections, sessions, and agent interactions
1464
+
1465
+ Usage:
1466
+ ahpx <prompt> Send a prompt (implicit)
1467
+ ahpx prompt <text> Send a prompt (explicit)
1468
+ ahpx exec <text> One-shot prompt
1469
+ ahpx session new Create a new session
1470
+ ahpx server add <name> --url Save a server connection
1471
+ ahpx connect [server] Connect and show server info
1472
+
1473
+ Examples:
1474
+ ahpx "fix the failing tests"
1475
+ ahpx --format json exec "summarize this repo"
1476
+ echo "review changes" | ahpx`
1477
+ ).version("0.1.0").option("--format <format>", "Output format: text, json, or quiet", "text").option("--json-strict", "Suppress non-JSON stderr output (only with --format json)").option("-v, --verbose", "Enable debug logging to stderr");
1478
+ async function resolveTarget(target) {
1479
+ if (target && isValidWsUrl(target)) {
1480
+ return { url: target };
1481
+ }
1482
+ if (target) {
1483
+ const conn = await store.get(target);
1484
+ if (!conn) {
1485
+ throw new AhpxError(
1486
+ `Unknown connection "${target}". Run ${pc6.bold("ahpx server list")} to see saved connections.`,
1487
+ ExitCode.Error
1488
+ );
1489
+ }
1490
+ return { url: conn.url, token: conn.token };
1491
+ }
1492
+ const def = await store.getDefault();
1493
+ if (!def) {
1494
+ throw new AhpxError(
1495
+ `No server specified and no default is set.
1496
+ Run ${pc6.bold("ahpx server add <name> --url <ws://...> --default")} to save one.`,
1497
+ ExitCode.Error
1498
+ );
1499
+ }
1500
+ return { url: def.url, token: def.token };
1501
+ }
1502
+ function printServerInfo(client, result) {
1503
+ console.log(pc6.green("\u2713 Connected"));
1504
+ console.log();
1505
+ console.log(pc6.bold("Protocol version:"), result.protocolVersion);
1506
+ console.log(pc6.bold("Server seq:"), result.serverSeq);
1507
+ if (result.defaultDirectory) {
1508
+ console.log(pc6.bold("Default directory:"), result.defaultDirectory);
1509
+ }
1510
+ const rootState = client.state.root;
1511
+ if (rootState.agents.length > 0) {
1512
+ console.log();
1513
+ console.log(pc6.bold("Agents:"));
1514
+ for (const agent of rootState.agents) {
1515
+ console.log(` ${pc6.cyan(agent.provider)} \u2014 ${agent.displayName}`);
1516
+ if (agent.models.length > 0) {
1517
+ console.log(` Models: ${agent.models.map((m) => m.name || m.id).join(", ")}`);
1518
+ }
1519
+ }
1520
+ }
1521
+ if (rootState.activeSessions !== void 0) {
1522
+ console.log();
1523
+ console.log(pc6.bold("Active sessions:"), rootState.activeSessions);
1524
+ }
1525
+ }
1526
+ function serverInfoJson(client, result) {
1527
+ const rootState = client.state.root;
1528
+ return {
1529
+ protocolVersion: result.protocolVersion,
1530
+ serverSeq: result.serverSeq,
1531
+ defaultDirectory: result.defaultDirectory,
1532
+ agents: rootState.agents.map((a) => ({
1533
+ provider: a.provider,
1534
+ displayName: a.displayName,
1535
+ models: a.models.map((m) => ({ id: m.id, name: m.name }))
1536
+ })),
1537
+ activeSessions: rootState.activeSessions
1538
+ };
1539
+ }
1540
+ function outputResult(globalOpts, textFn, data) {
1541
+ if (globalOpts.format === "json") {
1542
+ console.log(JSON.stringify(data));
1543
+ } else if (globalOpts.format === "quiet") {
1544
+ console.log(JSON.stringify(data));
1545
+ } else {
1546
+ textFn();
1547
+ }
1548
+ }
1549
+ function handleError(err, globalOpts) {
1550
+ const raw = err instanceof Error ? err.message : String(err);
1551
+ const code = err instanceof Error ? err.code : void 0;
1552
+ const message = raw || code || "Unknown error";
1553
+ const exitCode = err instanceof AhpxError ? err.exitCode : ExitCode.Error;
1554
+ if (globalOpts?.format === "json") {
1555
+ console.log(JSON.stringify({ error: message, exitCode }));
1556
+ } else if (globalOpts?.format !== "quiet" || exitCode !== ExitCode.Success) {
1557
+ console.error(pc6.red("\u2717"), message);
1558
+ }
1559
+ if (globalOpts?.verbose && err instanceof Error && err.stack) {
1560
+ console.error(pc6.dim(err.stack));
1561
+ }
1562
+ process.exitCode = exitCode;
1563
+ }
1564
+ program.command("connect").description("Connect to an AHP server and print server info").argument("[target]", "WebSocket URL or saved connection name (uses default if omitted)").option("-t, --timeout <ms>", "Connection timeout in milliseconds", "10000").action(async (target, opts, cmd) => {
1565
+ const globalOpts = parseGlobalOpts(cmd);
1566
+ applyGlobalOpts(globalOpts);
1567
+ const client = new AhpClient({
1568
+ connectTimeout: Number.parseInt(opts.timeout, 10),
1569
+ initialSubscriptions: ["agenthost:/root"]
1570
+ });
1571
+ try {
1572
+ const { url, token } = await resolveTarget(target);
1573
+ const spinner = startSpinner(`Connecting to ${url}...`, spinnersEnabled(globalOpts));
1574
+ try {
1575
+ const result = await client.connect(url);
1576
+ spinner.stop();
1577
+ if (token) {
1578
+ await client.authenticate(url, token);
1579
+ }
1580
+ outputResult(globalOpts, () => printServerInfo(client, result), serverInfoJson(client, result));
1581
+ } catch (err) {
1582
+ spinner.stop();
1583
+ throw err;
1584
+ }
1585
+ } catch (err) {
1586
+ handleError(err, globalOpts);
1587
+ } finally {
1588
+ await client.disconnect();
1589
+ }
1590
+ });
1591
+ var server = program.command("server").description("Manage saved server connections");
1592
+ server.command("add").description("Save a named connection profile").argument("<name>", "Connection name").requiredOption("--url <url>", "WebSocket URL (ws:// or wss://)").option("--token <token>", "Authentication token").option("--default", "Set as the default server").option(
1593
+ "--tag <tag>",
1594
+ "Add a tag to this server (repeatable)",
1595
+ (val, prev) => [...prev, val],
1596
+ []
1597
+ ).action(
1598
+ async (name, opts, cmd) => {
1599
+ const globalOpts = parseGlobalOpts(cmd);
1600
+ applyGlobalOpts(globalOpts);
1601
+ try {
1602
+ const tags = opts.tag.length > 0 ? opts.tag : void 0;
1603
+ await store.add({
1604
+ name,
1605
+ url: opts.url,
1606
+ token: opts.token,
1607
+ default: opts.default ?? false,
1608
+ tags
1609
+ });
1610
+ outputResult(
1611
+ globalOpts,
1612
+ () => {
1613
+ console.log(pc6.green("\u2713"), `Saved connection ${pc6.bold(name)} \u2192 ${pc6.dim(opts.url)}`);
1614
+ if (opts.default) {
1615
+ console.log(pc6.dim(" Set as default server"));
1616
+ }
1617
+ if (tags) {
1618
+ console.log(pc6.dim(` Tags: ${tags.join(", ")}`));
1619
+ }
1620
+ },
1621
+ { name, url: opts.url, default: opts.default ?? false, tags: tags ?? [] }
1622
+ );
1623
+ } catch (err) {
1624
+ if (err instanceof ConnectionValidationError) {
1625
+ handleError(new UsageError(err.message), globalOpts);
1626
+ } else {
1627
+ handleError(err, globalOpts);
1628
+ }
1629
+ }
1630
+ }
1631
+ );
1632
+ server.command("list").description("List saved connections").action(async (_opts, cmd) => {
1633
+ const globalOpts = parseGlobalOpts(cmd);
1634
+ applyGlobalOpts(globalOpts);
1635
+ try {
1636
+ const connections = await store.list();
1637
+ outputResult(
1638
+ globalOpts,
1639
+ () => {
1640
+ if (connections.length === 0) {
1641
+ console.log(pc6.dim("No saved connections. Run"), pc6.bold("ahpx server add"), pc6.dim("to add one."));
1642
+ return;
1643
+ }
1644
+ const nameW = Math.max(4, ...connections.map((c) => c.name.length));
1645
+ const urlW = Math.max(3, ...connections.map((c) => c.url.length));
1646
+ console.log(` ${pc6.bold("Name".padEnd(nameW))} ${pc6.bold("URL".padEnd(urlW))} ${pc6.bold("Default")}`);
1647
+ console.log(` ${"\u2500".repeat(nameW)} ${"\u2500".repeat(urlW)} ${"\u2500".repeat(7)}`);
1648
+ for (const c of connections) {
1649
+ const def = c.default ? pc6.green(" \u2713") : pc6.dim(" \xB7");
1650
+ console.log(` ${pc6.cyan(c.name.padEnd(nameW))} ${c.url.padEnd(urlW)} ${def}`);
1651
+ }
1652
+ },
1653
+ connections.map((c) => ({ name: c.name, url: c.url, default: c.default ?? false }))
1654
+ );
1655
+ } catch (err) {
1656
+ handleError(err, globalOpts);
1657
+ }
1658
+ });
1659
+ server.command("remove").description("Remove a saved connection").argument("<name>", "Connection name to remove").action(async (name, _opts, cmd) => {
1660
+ const globalOpts = parseGlobalOpts(cmd);
1661
+ applyGlobalOpts(globalOpts);
1662
+ try {
1663
+ const conn = await store.get(name);
1664
+ if (!conn) {
1665
+ throw new AhpxError(`Connection "${name}" not found`, ExitCode.Error);
1666
+ }
1667
+ if (conn.default && globalOpts.format === "text") {
1668
+ console.log(pc6.yellow("\u26A0"), `"${name}" is the default server.`);
1669
+ }
1670
+ const removed = await store.remove(name);
1671
+ if (removed) {
1672
+ outputResult(globalOpts, () => console.log(pc6.green("\u2713"), `Removed connection ${pc6.bold(name)}`), {
1673
+ removed: name
1674
+ });
1675
+ }
1676
+ } catch (err) {
1677
+ handleError(err, globalOpts);
1678
+ }
1679
+ });
1680
+ server.command("test").description("Test connectivity to a server").argument("<target>", "Connection name or WebSocket URL").option("-t, --timeout <ms>", "Connection timeout in milliseconds", "10000").action(async (target, opts, cmd) => {
1681
+ const globalOpts = parseGlobalOpts(cmd);
1682
+ applyGlobalOpts(globalOpts);
1683
+ const client = new AhpClient({
1684
+ connectTimeout: Number.parseInt(opts.timeout, 10),
1685
+ initialSubscriptions: ["agenthost:/root"]
1686
+ });
1687
+ try {
1688
+ const { url, token } = await resolveTarget(target);
1689
+ const spinner = startSpinner(`Testing connection to ${url}...`, spinnersEnabled(globalOpts));
1690
+ try {
1691
+ const result = await client.connect(url);
1692
+ spinner.stop();
1693
+ if (token) {
1694
+ await client.authenticate(url, token);
1695
+ }
1696
+ outputResult(globalOpts, () => printServerInfo(client, result), serverInfoJson(client, result));
1697
+ } catch (err) {
1698
+ spinner.stop();
1699
+ throw err;
1700
+ }
1701
+ } catch (err) {
1702
+ handleError(err, globalOpts);
1703
+ } finally {
1704
+ await client.disconnect();
1705
+ }
1706
+ });
1707
+ server.command("status").description("Health check all saved servers").option("--all", "Show all servers including unreachable").option("-t, --timeout <ms>", "Health check timeout in milliseconds", "10000").action(async (opts, cmd) => {
1708
+ const globalOpts = parseGlobalOpts(cmd);
1709
+ applyGlobalOpts(globalOpts);
1710
+ try {
1711
+ const connections = await store.list();
1712
+ if (connections.length === 0) {
1713
+ outputResult(
1714
+ globalOpts,
1715
+ () => console.log(pc6.dim("No saved connections. Run"), pc6.bold("ahpx server add"), pc6.dim("to add one.")),
1716
+ []
1717
+ );
1718
+ return;
1719
+ }
1720
+ const spinner = startSpinner(`Checking ${connections.length} server(s)...`, spinnersEnabled(globalOpts));
1721
+ const checker = new HealthChecker({ timeout: Number.parseInt(opts.timeout, 10) });
1722
+ const results = await checker.checkAll(connections);
1723
+ spinner.stop();
1724
+ const displayed = opts.all ? results : results.filter((r) => r.status !== "unreachable");
1725
+ outputResult(
1726
+ globalOpts,
1727
+ () => {
1728
+ if (displayed.length === 0) {
1729
+ console.log(pc6.dim("No servers to check."));
1730
+ return;
1731
+ }
1732
+ const nameW = Math.max(4, ...displayed.map((r) => r.name.length));
1733
+ const urlW = Math.max(3, ...displayed.map((r) => r.url.length));
1734
+ console.log(
1735
+ ` ${pc6.bold("Name".padEnd(nameW))} ${pc6.bold("URL".padEnd(urlW))} ${pc6.bold("Status".padEnd(11))} ${pc6.bold("Latency".padEnd(9))} ${pc6.bold("Sessions".padEnd(8))} ${pc6.bold("Agents")}`
1736
+ );
1737
+ console.log(
1738
+ ` ${"\u2500".repeat(nameW)} ${"\u2500".repeat(urlW)} ${"\u2500".repeat(11)} ${"\u2500".repeat(9)} ${"\u2500".repeat(8)} ${"\u2500".repeat(10)}`
1739
+ );
1740
+ for (const r of displayed) {
1741
+ const statusColor = r.status === "healthy" ? pc6.green : r.status === "degraded" ? pc6.yellow : pc6.red;
1742
+ const latency = r.status === "unreachable" ? pc6.dim("\u2014") : `${Math.round(r.latencyMs)}ms`;
1743
+ const sessions = r.status === "unreachable" ? pc6.dim("\u2014") : String(r.activeSessions);
1744
+ const agents = r.status === "unreachable" ? pc6.dim("\u2014") : r.agents.map((a) => a.provider).join(", ") || pc6.dim("none");
1745
+ console.log(
1746
+ ` ${pc6.cyan(r.name.padEnd(nameW))} ${r.url.padEnd(urlW)} ${statusColor(r.status.padEnd(11))} ${String(latency).padEnd(9)} ${String(sessions).padEnd(8)} ${agents}`
1747
+ );
1748
+ }
1749
+ },
1750
+ displayed.map((r) => ({
1751
+ name: r.name,
1752
+ url: r.url,
1753
+ status: r.status,
1754
+ latencyMs: r.latencyMs,
1755
+ activeSessions: r.activeSessions,
1756
+ agents: r.agents,
1757
+ error: r.error
1758
+ }))
1759
+ );
1760
+ } catch (err) {
1761
+ handleError(err, globalOpts);
1762
+ }
1763
+ });
1764
+ server.command("health").description("Detailed health check for a single server").argument("<name>", "Connection name").option("-t, --timeout <ms>", "Health check timeout in milliseconds", "10000").action(async (name, opts, cmd) => {
1765
+ const globalOpts = parseGlobalOpts(cmd);
1766
+ applyGlobalOpts(globalOpts);
1767
+ try {
1768
+ const conn = await store.get(name);
1769
+ if (!conn) {
1770
+ throw new AhpxError(
1771
+ `Connection "${name}" not found. Run ${pc6.bold("ahpx server list")} to see saved connections.`,
1772
+ ExitCode.Error
1773
+ );
1774
+ }
1775
+ const spinner = startSpinner(`Checking ${name}...`, spinnersEnabled(globalOpts));
1776
+ const checker = new HealthChecker({ timeout: Number.parseInt(opts.timeout, 10) });
1777
+ const health = await checker.check(conn.url, conn.name);
1778
+ spinner.stop();
1779
+ outputResult(
1780
+ globalOpts,
1781
+ () => {
1782
+ const statusColor = health.status === "healthy" ? pc6.green : health.status === "degraded" ? pc6.yellow : pc6.red;
1783
+ console.log(pc6.bold("Server:"), pc6.cyan(health.name));
1784
+ console.log(pc6.bold("URL:"), health.url);
1785
+ console.log(pc6.bold("Status:"), statusColor(health.status));
1786
+ console.log(
1787
+ pc6.bold("Latency:"),
1788
+ health.status === "unreachable" ? pc6.dim("\u2014") : `${Math.round(health.latencyMs)}ms`
1789
+ );
1790
+ if (health.protocolVersion !== void 0) {
1791
+ console.log(pc6.bold("Protocol:"), `v${health.protocolVersion}`);
1792
+ }
1793
+ console.log(
1794
+ pc6.bold("Active sessions:"),
1795
+ health.status === "unreachable" ? pc6.dim("\u2014") : String(health.activeSessions)
1796
+ );
1797
+ if (health.agents.length > 0) {
1798
+ console.log(pc6.bold("Agents:"));
1799
+ for (const agent of health.agents) {
1800
+ console.log(` ${pc6.cyan(agent.provider)}: ${agent.models.join(", ") || pc6.dim("no models")}`);
1801
+ }
1802
+ } else if (health.status !== "unreachable") {
1803
+ console.log(pc6.bold("Agents:"), pc6.dim("none"));
1804
+ }
1805
+ if (conn.tags && conn.tags.length > 0) {
1806
+ console.log(pc6.bold("Tags:"), conn.tags.join(", "));
1807
+ }
1808
+ if (health.error) {
1809
+ console.log(pc6.bold("Error:"), pc6.red(health.error));
1810
+ }
1811
+ console.log(pc6.bold("Checked at:"), health.checkedAt);
1812
+ },
1813
+ health
1814
+ );
1815
+ } catch (err) {
1816
+ handleError(err, globalOpts);
1817
+ }
1818
+ });
1819
+ var config = program.command("config").description("Manage ahpx configuration");
1820
+ config.command("show").description("Print resolved configuration with source annotations").action(async (_opts, cmd) => {
1821
+ const globalOpts = parseGlobalOpts(cmd);
1822
+ applyGlobalOpts(globalOpts);
1823
+ try {
1824
+ const result = await loadConfigWithSources({
1825
+ overrides: buildConfigOverrides(globalOpts)
1826
+ });
1827
+ const sourceLabel = (src) => {
1828
+ switch (src) {
1829
+ case "default":
1830
+ return pc6.dim("(default)");
1831
+ case "global":
1832
+ return pc6.blue(`(global: ${result.globalPath})`);
1833
+ case "project":
1834
+ return pc6.green(`(project: ${result.projectPath})`);
1835
+ case "cli":
1836
+ return pc6.yellow("(cli flag)");
1837
+ }
1838
+ };
1839
+ outputResult(
1840
+ globalOpts,
1841
+ () => {
1842
+ console.log(pc6.bold("Resolved configuration:"));
1843
+ console.log(pc6.dim(` Global: ${result.globalPath}`));
1844
+ console.log(pc6.dim(` Project: ${result.projectPath}`));
1845
+ console.log();
1846
+ for (const [key, value] of Object.entries(result.config)) {
1847
+ if (value !== void 0) {
1848
+ const src = result.sources[key] ?? "default";
1849
+ console.log(` ${pc6.cyan(key)}: ${value} ${sourceLabel(src)}`);
1850
+ }
1851
+ }
1852
+ },
1853
+ {
1854
+ config: result.config,
1855
+ sources: result.sources,
1856
+ globalPath: result.globalPath,
1857
+ projectPath: result.projectPath
1858
+ }
1859
+ );
1860
+ } catch (err) {
1861
+ handleError(err, globalOpts);
1862
+ }
1863
+ });
1864
+ config.command("init").description("Create ~/.ahpx/config.json with defaults").action(async (_opts, cmd) => {
1865
+ const globalOpts = parseGlobalOpts(cmd);
1866
+ applyGlobalOpts(globalOpts);
1867
+ try {
1868
+ const created = await initGlobalConfig();
1869
+ outputResult(
1870
+ globalOpts,
1871
+ () => {
1872
+ if (created) {
1873
+ console.log(pc6.green("\u2713"), `Created ${pc6.dim(globalConfigPath())}`);
1874
+ } else {
1875
+ console.log(pc6.dim("Config already exists at"), globalConfigPath());
1876
+ }
1877
+ },
1878
+ { created, path: globalConfigPath() }
1879
+ );
1880
+ } catch (err) {
1881
+ handleError(err, globalOpts);
1882
+ }
1883
+ });
1884
+ var session = program.command("session").description("Manage agent sessions");
1885
+ function formatAge(isoTimestamp) {
1886
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
1887
+ const seconds = Math.floor(ms / 1e3);
1888
+ if (seconds < 60) return `${seconds}s ago`;
1889
+ const minutes = Math.floor(seconds / 60);
1890
+ if (minutes < 60) return `${minutes}m ago`;
1891
+ const hours = Math.floor(minutes / 60);
1892
+ if (hours < 24) return `${hours}h ago`;
1893
+ const days = Math.floor(hours / 24);
1894
+ return `${days}d ago`;
1895
+ }
1896
+ function truncate(str, maxLen) {
1897
+ if (str.length <= maxLen) return str;
1898
+ return `${str.slice(0, maxLen - 1)}\u2026`;
1899
+ }
1900
+ async function resolveServerName(serverFlag, cfg) {
1901
+ if (serverFlag) {
1902
+ if (isValidWsUrl(serverFlag)) return serverFlag;
1903
+ const conn = await store.get(serverFlag);
1904
+ if (!conn) {
1905
+ throw new AhpxError(
1906
+ `Unknown connection "${serverFlag}". Run ${pc6.bold("ahpx server list")} to see saved connections.`,
1907
+ ExitCode.Error
1908
+ );
1909
+ }
1910
+ return conn.name;
1911
+ }
1912
+ if (cfg.defaultServer) {
1913
+ const conn = await store.get(cfg.defaultServer);
1914
+ if (!conn) {
1915
+ throw new AhpxError(
1916
+ `Default server "${cfg.defaultServer}" not found. Run ${pc6.bold("ahpx server list")} to check.`,
1917
+ ExitCode.Error
1918
+ );
1919
+ }
1920
+ return conn.name;
1921
+ }
1922
+ const def = await store.getDefault();
1923
+ if (!def) {
1924
+ throw new AhpxError(
1925
+ `No server specified and no default is set.
1926
+ Run ${pc6.bold("ahpx server add <name> --url <ws://...> --default")} to save one.`,
1927
+ ExitCode.Error
1928
+ );
1929
+ }
1930
+ return def.name;
1931
+ }
1932
+ async function resolveSessionRecord(id, opts) {
1933
+ if (id) {
1934
+ const record2 = await sessionStore.get(id);
1935
+ if (!record2) {
1936
+ throw new NoSessionError(`Session "${id}" not found.`);
1937
+ }
1938
+ return record2;
1939
+ }
1940
+ const cfg = await loadConfig();
1941
+ const serverName = await resolveServerName(opts.server, cfg);
1942
+ const cwd = process.cwd();
1943
+ const record = await resolveSession({
1944
+ serverName,
1945
+ cwd,
1946
+ name: opts.name,
1947
+ store: sessionStore
1948
+ });
1949
+ if (!record) {
1950
+ const hint = opts.name ? ` named "${opts.name}"` : "";
1951
+ throw new NoSessionError(
1952
+ `No active session${hint} found for ${pc6.bold(serverName)} in ${pc6.dim(cwd)}.
1953
+ Run ${pc6.bold("ahpx session new")} to create one.`
1954
+ );
1955
+ }
1956
+ return record;
1957
+ }
1958
+ function buildConfigOverrides(globalOpts) {
1959
+ const overrides = {};
1960
+ if (globalOpts.format !== "text") overrides.format = globalOpts.format;
1961
+ if (globalOpts.verbose) overrides.verbose = true;
1962
+ return overrides;
1963
+ }
1964
+ function applyGlobalOpts(globalOpts) {
1965
+ setVerbose(globalOpts.verbose);
1966
+ }
1967
+ async function requireCwdForRemoteServer(server2, cwd) {
1968
+ if (!server2 || cwd) return;
1969
+ let url;
1970
+ if (isValidWsUrl(server2)) {
1971
+ url = server2;
1972
+ } else {
1973
+ const store2 = new ConnectionStore();
1974
+ const conn = await store2.get(server2);
1975
+ if (!conn) return;
1976
+ url = conn.url;
1977
+ }
1978
+ if (!isLocalUrl(url)) {
1979
+ throw new UsageError(
1980
+ `--cwd is required when targeting a remote server.
1981
+ Use 'ahpx browse --server ${server2}' to browse the remote filesystem and find the correct working directory.`
1982
+ );
1983
+ }
1984
+ }
1985
+ session.command("new").description("Create a new agent session").option("-s, --server <name>", "Server name or WebSocket URL").option("-p, --provider <provider>", "Agent provider (e.g. copilot)").option("-m, --model <model>", "Model to use").option("-n, --name <name>", "Name this session (for scoped lookups)").option("--cwd <dir>", "Working directory").option("-t, --timeout <ms>", "Connection timeout in milliseconds", "10000").action(
1986
+ async (opts, cmd) => {
1987
+ const globalOpts = parseGlobalOpts(cmd);
1988
+ applyGlobalOpts(globalOpts);
1989
+ try {
1990
+ await requireCwdForRemoteServer(opts.server, opts.cwd);
1991
+ const cfg = await loadConfig({ overrides: buildConfigOverrides(globalOpts) });
1992
+ const provider = opts.provider ?? cfg.defaultProvider;
1993
+ const model = opts.model ?? cfg.defaultModel;
1994
+ const cwd = opts.cwd ?? process.cwd();
1995
+ const gitRoot = await findGitRoot(cwd);
1996
+ await withConnection(
1997
+ {
1998
+ server: opts.server,
1999
+ config: cfg,
2000
+ timeout: Number.parseInt(opts.timeout, 10)
2001
+ },
2002
+ async (client, serverInfo) => {
2003
+ const rootState = client.state.root;
2004
+ const resolvedProvider = provider ?? (rootState.agents.length > 0 ? rootState.agents[0].provider : void 0);
2005
+ if (!resolvedProvider) {
2006
+ throw new UsageError(
2007
+ "No agent provider available. Specify one with --provider or configure defaultProvider."
2008
+ );
2009
+ }
2010
+ const sessionId = randomUUID3();
2011
+ const sessionUri = `${resolvedProvider}:/${sessionId}`;
2012
+ const spinner = startSpinner(`Creating session on ${serverInfo.name}...`, spinnersEnabled(globalOpts));
2013
+ try {
2014
+ await client.createSession(sessionUri, resolvedProvider, model, ensureFileUri(cwd));
2015
+ await client.subscribe(sessionUri);
2016
+ spinner.update("Waiting for session ready...");
2017
+ const ready = await new Promise((resolve3, reject) => {
2018
+ const timeout = setTimeout(() => {
2019
+ reject(new TimeoutError("Timed out waiting for session to be ready"));
2020
+ }, 3e4);
2021
+ client.on("action", (envelope) => {
2022
+ const action = envelope.action;
2023
+ if (action.type === "session/ready" /* SessionReady */ && action.session === sessionUri) {
2024
+ clearTimeout(timeout);
2025
+ resolve3(true);
2026
+ } else if (action.type === "session/creationFailed" /* SessionCreationFailed */ && action.session === sessionUri) {
2027
+ clearTimeout(timeout);
2028
+ resolve3(false);
2029
+ }
2030
+ });
2031
+ const sessionState2 = client.state.getSession(sessionUri);
2032
+ if (sessionState2?.lifecycle === "ready") {
2033
+ clearTimeout(timeout);
2034
+ resolve3(true);
2035
+ } else if (sessionState2?.lifecycle === "creationFailed") {
2036
+ clearTimeout(timeout);
2037
+ resolve3(false);
2038
+ }
2039
+ });
2040
+ spinner.stop();
2041
+ if (!ready) {
2042
+ const sessionState2 = client.state.getSession(sessionUri);
2043
+ const errMsg = sessionState2?.creationError?.message ?? "Unknown error";
2044
+ throw new AhpxError(`Session creation failed: ${errMsg}`, ExitCode.Error);
2045
+ }
2046
+ const sessionState = client.state.getSession(sessionUri);
2047
+ const record = {
2048
+ id: sessionId,
2049
+ sessionUri,
2050
+ serverName: serverInfo.name,
2051
+ serverUrl: serverInfo.url,
2052
+ provider: resolvedProvider,
2053
+ model: model ?? sessionState?.summary.model,
2054
+ name: opts.name,
2055
+ workingDirectory: cwd,
2056
+ gitRoot,
2057
+ title: sessionState?.summary.title,
2058
+ status: "active",
2059
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2060
+ };
2061
+ await sessionStore.save(record);
2062
+ outputResult(
2063
+ globalOpts,
2064
+ () => {
2065
+ console.log(pc6.green("\u2713 Session created"));
2066
+ console.log();
2067
+ console.log(pc6.bold("ID:"), sessionId);
2068
+ console.log(pc6.bold("URI:"), pc6.cyan(sessionUri));
2069
+ console.log(pc6.bold("Provider:"), resolvedProvider);
2070
+ if (record.model) console.log(pc6.bold("Model:"), record.model);
2071
+ if (opts.name) console.log(pc6.bold("Name:"), opts.name);
2072
+ console.log(pc6.bold("Directory:"), cwd);
2073
+ if (gitRoot) console.log(pc6.bold("Git root:"), gitRoot);
2074
+ console.log(pc6.bold("Status:"), pc6.green("active"));
2075
+ },
2076
+ {
2077
+ id: sessionId,
2078
+ sessionUri,
2079
+ provider: resolvedProvider,
2080
+ model: record.model,
2081
+ name: opts.name,
2082
+ workingDirectory: cwd,
2083
+ gitRoot,
2084
+ status: "active"
2085
+ }
2086
+ );
2087
+ } catch (err) {
2088
+ spinner.stop();
2089
+ throw err;
2090
+ }
2091
+ }
2092
+ );
2093
+ } catch (err) {
2094
+ handleError(err, globalOpts);
2095
+ }
2096
+ }
2097
+ );
2098
+ session.command("list").description("List sessions (default: active only)").option("-s, --server <name>", "Filter by server name").option("-a, --all", "Include closed sessions").action(async (opts, cmd) => {
2099
+ const globalOpts = parseGlobalOpts(cmd);
2100
+ applyGlobalOpts(globalOpts);
2101
+ try {
2102
+ const records = await sessionStore.list({
2103
+ ...opts.server ? { serverName: opts.server } : {},
2104
+ ...opts.all ? {} : { status: "active" }
2105
+ });
2106
+ outputResult(
2107
+ globalOpts,
2108
+ () => {
2109
+ if (records.length === 0) {
2110
+ const hint = opts.all ? "" : " active";
2111
+ console.log(
2112
+ pc6.dim(`No${hint} sessions found.`),
2113
+ pc6.dim("Run"),
2114
+ pc6.bold("ahpx session new"),
2115
+ pc6.dim("to create one.")
2116
+ );
2117
+ return;
2118
+ }
2119
+ const idW = 8;
2120
+ const nameW = Math.max(4, ...records.map((r) => (r.name ?? "\u2014").length));
2121
+ const provW = Math.max(8, ...records.map((r) => r.provider.length));
2122
+ const modelW = Math.max(5, ...records.map((r) => (r.model ?? "\u2014").length));
2123
+ const titleW = 30;
2124
+ const statusW = 6;
2125
+ const ageW = 10;
2126
+ console.log(
2127
+ ` ${pc6.bold("ID".padEnd(idW))} ${pc6.bold("Name".padEnd(nameW))} ${pc6.bold("Provider".padEnd(provW))} ${pc6.bold("Model".padEnd(modelW))} ${pc6.bold("Title".padEnd(titleW))} ${pc6.bold("Status".padEnd(statusW))} ${pc6.bold("Age".padEnd(ageW))}`
2128
+ );
2129
+ console.log(
2130
+ ` ${"\u2500".repeat(idW)} ${"\u2500".repeat(nameW)} ${"\u2500".repeat(provW)} ${"\u2500".repeat(modelW)} ${"\u2500".repeat(titleW)} ${"\u2500".repeat(statusW)} ${"\u2500".repeat(ageW)}`
2131
+ );
2132
+ for (const r of records) {
2133
+ const status = r.status === "active" ? pc6.green("active") : pc6.dim("closed");
2134
+ const shortId = r.id.slice(0, 8);
2135
+ const name = r.name ?? pc6.dim("\u2014");
2136
+ const model = r.model ?? pc6.dim("\u2014");
2137
+ const title = truncate(r.title ?? "\u2014", titleW);
2138
+ console.log(
2139
+ ` ${pc6.cyan(shortId.padEnd(idW))} ${String(name).padEnd(nameW)} ${r.provider.padEnd(provW)} ${String(model).padEnd(modelW)} ${title.padEnd(titleW)} ${String(status).padEnd(statusW + 10)} ${formatAge(r.createdAt)}`
2140
+ );
2141
+ }
2142
+ },
2143
+ records.map((r) => ({
2144
+ id: r.id,
2145
+ sessionUri: r.sessionUri,
2146
+ serverName: r.serverName,
2147
+ provider: r.provider,
2148
+ model: r.model,
2149
+ name: r.name,
2150
+ title: r.title,
2151
+ status: r.status,
2152
+ workingDirectory: r.workingDirectory,
2153
+ createdAt: r.createdAt
2154
+ }))
2155
+ );
2156
+ } catch (err) {
2157
+ handleError(err, globalOpts);
2158
+ }
2159
+ });
2160
+ session.command("show").description("Show session details").argument("[id]", "Session ID").option("-n, --name <name>", "Session name (for scoped lookup)").option("-s, --server <name>", "Server name").action(async (id, opts, cmd) => {
2161
+ const globalOpts = parseGlobalOpts(cmd);
2162
+ applyGlobalOpts(globalOpts);
2163
+ try {
2164
+ const record = await resolveSessionRecord(id, opts);
2165
+ outputResult(
2166
+ globalOpts,
2167
+ () => {
2168
+ console.log(pc6.bold("Session Details"));
2169
+ console.log();
2170
+ console.log(pc6.bold("ID:"), record.id);
2171
+ console.log(pc6.bold("URI:"), pc6.cyan(record.sessionUri));
2172
+ console.log(pc6.bold("Server:"), record.serverName);
2173
+ console.log(pc6.bold("Provider:"), record.provider);
2174
+ if (record.model) console.log(pc6.bold("Model:"), record.model);
2175
+ if (record.name) console.log(pc6.bold("Name:"), record.name);
2176
+ if (record.title) console.log(pc6.bold("Title:"), record.title);
2177
+ console.log(pc6.bold("Status:"), record.status === "active" ? pc6.green("active") : pc6.dim("closed"));
2178
+ if (record.workingDirectory) console.log(pc6.bold("Directory:"), record.workingDirectory);
2179
+ if (record.gitRoot) console.log(pc6.bold("Git root:"), record.gitRoot);
2180
+ console.log(pc6.bold("Created:"), record.createdAt, pc6.dim(`(${formatAge(record.createdAt)})`));
2181
+ if (record.lastPromptAt) {
2182
+ console.log(pc6.bold("Last prompt:"), record.lastPromptAt, pc6.dim(`(${formatAge(record.lastPromptAt)})`));
2183
+ }
2184
+ if (record.closedAt) {
2185
+ console.log(pc6.bold("Closed:"), record.closedAt, pc6.dim(`(${formatAge(record.closedAt)})`));
2186
+ }
2187
+ },
2188
+ record
2189
+ );
2190
+ } catch (err) {
2191
+ handleError(err, globalOpts);
2192
+ }
2193
+ });
2194
+ session.command("close").description("Close a session (soft-close: keeps record for history)").argument("[id]", "Session ID").option("-n, --name <name>", "Session name (for scoped lookup)").option("-s, --server <name>", "Server name").option("-t, --timeout <ms>", "Connection timeout in milliseconds", "10000").action(async (id, opts, cmd) => {
2195
+ const globalOpts = parseGlobalOpts(cmd);
2196
+ applyGlobalOpts(globalOpts);
2197
+ try {
2198
+ const record = await resolveSessionRecord(id, opts);
2199
+ if (record.status === "closed") {
2200
+ if (globalOpts.format === "text") {
2201
+ console.log(pc6.dim("Session is already closed."));
2202
+ }
2203
+ return;
2204
+ }
2205
+ const spinner = startSpinner("Disposing session...", spinnersEnabled(globalOpts));
2206
+ try {
2207
+ const cfg = await loadConfig({ overrides: buildConfigOverrides(globalOpts) });
2208
+ await withConnection(
2209
+ {
2210
+ server: record.serverName,
2211
+ config: cfg,
2212
+ timeout: Number.parseInt(opts.timeout, 10)
2213
+ },
2214
+ async (client) => {
2215
+ await client.disposeSession(record.sessionUri);
2216
+ }
2217
+ );
2218
+ spinner.stop();
2219
+ } catch {
2220
+ spinner.stop();
2221
+ if (globalOpts.format === "text") {
2222
+ console.log(pc6.yellow("\u26A0"), "Could not dispose session on server (closing locally only)");
2223
+ }
2224
+ }
2225
+ const closed = await sessionStore.close(record.id);
2226
+ if (closed) {
2227
+ outputResult(
2228
+ globalOpts,
2229
+ () => console.log(
2230
+ pc6.green("\u2713"),
2231
+ `Closed session ${pc6.bold(record.id.slice(0, 8))}`,
2232
+ record.name ? pc6.dim(`(${record.name})`) : ""
2233
+ ),
2234
+ { closed: record.id }
2235
+ );
2236
+ }
2237
+ } catch (err) {
2238
+ handleError(err, globalOpts);
2239
+ }
2240
+ });
2241
+ function formatTurnEntry(entry) {
2242
+ const userMsg = truncate(entry.userMessage, 80);
2243
+ const responseMsg = truncate(entry.responsePreview, 80);
2244
+ const usageStr = entry.usage ? `${entry.usage.inputTokens ?? "?"}\u2192${entry.usage.outputTokens ?? "?"}t` : "";
2245
+ const timeStr = entry.timestamp ? pc6.dim(` (${formatAge(entry.timestamp)})`) : "";
2246
+ console.log(pc6.bold(pc6.cyan(` Turn ${entry.id.slice(0, 8)}`)) + timeStr);
2247
+ console.log(` ${pc6.bold("User:")} ${userMsg}`);
2248
+ console.log(` ${pc6.bold("Response:")} ${responseMsg}`);
2249
+ if (entry.toolCalls > 0) {
2250
+ console.log(` ${pc6.bold("Tool calls:")} ${entry.toolCalls}`);
2251
+ }
2252
+ if (usageStr) {
2253
+ console.log(` ${pc6.bold("Tokens:")} ${usageStr}`);
2254
+ }
2255
+ if (entry.usage?.model) {
2256
+ console.log(` ${pc6.bold("Model:")} ${entry.usage.model}`);
2257
+ }
2258
+ console.log();
2259
+ }
2260
+ session.command("history").description("Show turn history for a session").argument("[id]", "Session ID").option("-n, --name <name>", "Session name (for scoped lookup)").option("-s, --server <name>", "Server name").option("-l, --limit <n>", "Maximum number of turns to show", "10").option("-t, --timeout <ms>", "Connection timeout in milliseconds", "10000").option("--local", "Show only locally-cached turn history (no server connection)").action(
2261
+ async (id, opts, cmd) => {
2262
+ const globalOpts = parseGlobalOpts(cmd);
2263
+ applyGlobalOpts(globalOpts);
2264
+ try {
2265
+ const record = await resolveSessionRecord(id, opts);
2266
+ const limit = Number.parseInt(opts.limit, 10);
2267
+ if (opts.local) {
2268
+ showLocalHistory(record, limit, globalOpts);
2269
+ return;
2270
+ }
2271
+ const cfg = await loadConfig({ overrides: buildConfigOverrides(globalOpts) });
2272
+ try {
2273
+ await withConnection(
2274
+ {
2275
+ server: record.serverName,
2276
+ config: cfg,
2277
+ timeout: Number.parseInt(opts.timeout, 10)
2278
+ },
2279
+ async (client) => {
2280
+ const result = await client.fetchTurns(record.sessionUri, void 0, limit);
2281
+ outputResult(
2282
+ globalOpts,
2283
+ () => {
2284
+ if (result.turns.length === 0) {
2285
+ console.log(pc6.dim("No turns in this session."));
2286
+ return;
2287
+ }
2288
+ console.log(
2289
+ pc6.bold(`History for session ${record.id.slice(0, 8)}`),
2290
+ result.hasMore ? pc6.dim(`(showing last ${result.turns.length}, more available)`) : ""
2291
+ );
2292
+ console.log();
2293
+ for (const turn of result.turns) {
2294
+ formatTurnEntry({
2295
+ id: turn.id,
2296
+ userMessage: turn.userMessage.text,
2297
+ responsePreview: turnResponseText(turn) || "(no response)",
2298
+ toolCalls: turnToolCallCount(turn),
2299
+ usage: turn.usage
2300
+ });
2301
+ }
2302
+ },
2303
+ {
2304
+ source: "server",
2305
+ sessionId: record.id,
2306
+ hasMore: result.hasMore,
2307
+ turns: result.turns.map((t) => ({
2308
+ id: t.id,
2309
+ userMessage: t.userMessage.text,
2310
+ responseText: turnResponseText(t),
2311
+ toolCalls: turnToolCallCount(t),
2312
+ usage: t.usage
2313
+ }))
2314
+ }
2315
+ );
2316
+ }
2317
+ );
2318
+ } catch {
2319
+ if (globalOpts.format === "text") {
2320
+ console.error(pc6.yellow("Server unavailable. Showing locally-cached history.\n"));
2321
+ }
2322
+ showLocalHistory(record, limit, globalOpts);
2323
+ }
2324
+ } catch (err) {
2325
+ handleError(err, globalOpts);
2326
+ }
2327
+ }
2328
+ );
2329
+ function showLocalHistory(record, limit, globalOpts) {
2330
+ const turns = record.turns ?? [];
2331
+ const display = turns.slice(-limit);
2332
+ outputResult(
2333
+ globalOpts,
2334
+ () => {
2335
+ if (display.length === 0) {
2336
+ console.log(pc6.dim("No locally-cached turns for this session."));
2337
+ return;
2338
+ }
2339
+ console.log(
2340
+ pc6.bold(`Local history for session ${record.id.slice(0, 8)}`),
2341
+ pc6.dim(`(${display.length} of ${turns.length} turns)`)
2342
+ );
2343
+ console.log();
2344
+ for (const turn of display) {
2345
+ formatTurnEntry({
2346
+ id: turn.turnId,
2347
+ userMessage: turn.userMessage,
2348
+ responsePreview: turn.responsePreview,
2349
+ toolCalls: turn.toolCallCount,
2350
+ usage: turn.tokenUsage ? { inputTokens: turn.tokenUsage.input, outputTokens: turn.tokenUsage.output, model: turn.tokenUsage.model } : void 0,
2351
+ timestamp: turn.timestamp
2352
+ });
2353
+ }
2354
+ },
2355
+ {
2356
+ source: "local",
2357
+ sessionId: record.id,
2358
+ turns: display.map((t) => ({
2359
+ turnId: t.turnId,
2360
+ userMessage: t.userMessage,
2361
+ responsePreview: t.responsePreview,
2362
+ toolCallCount: t.toolCallCount,
2363
+ tokenUsage: t.tokenUsage,
2364
+ state: t.state,
2365
+ timestamp: t.timestamp
2366
+ }))
2367
+ }
2368
+ );
2369
+ }
2370
+ session.command("export").description("Export a session record (with local turn history) to JSON").argument("<id>", "Session ID").option("-o, --output <file>", "Write to file instead of stdout").action(async (id, opts, cmd) => {
2371
+ const globalOpts = parseGlobalOpts(cmd);
2372
+ applyGlobalOpts(globalOpts);
2373
+ try {
2374
+ const record = await sessionStore.get(id);
2375
+ if (!record) {
2376
+ throw new NoSessionError(`Session "${id}" not found.`);
2377
+ }
2378
+ const exported = JSON.stringify(record, null, " ");
2379
+ if (opts.output) {
2380
+ await fs4.writeFile(path4.resolve(opts.output), `${exported}
2381
+ `, "utf-8");
2382
+ if (globalOpts.format === "text") {
2383
+ console.error(pc6.green(`Session exported to ${opts.output}`));
2384
+ }
2385
+ } else {
2386
+ console.log(exported);
2387
+ }
2388
+ } catch (err) {
2389
+ handleError(err, globalOpts);
2390
+ }
2391
+ });
2392
+ session.command("import").description("Import a session record from a JSON file").argument("<file>", "Path to JSON file").action(async (filePath, _opts, cmd) => {
2393
+ const globalOpts = parseGlobalOpts(cmd);
2394
+ applyGlobalOpts(globalOpts);
2395
+ try {
2396
+ const resolved = path4.resolve(filePath);
2397
+ const raw = await fs4.readFile(resolved, "utf-8");
2398
+ let record;
2399
+ try {
2400
+ record = JSON.parse(raw);
2401
+ } catch {
2402
+ throw new UsageError(`Failed to parse "${filePath}" as JSON.`);
2403
+ }
2404
+ if (!record.id || !record.sessionUri || !record.serverName || !record.serverUrl || !record.provider) {
2405
+ throw new UsageError(
2406
+ "Invalid session record: missing required fields (id, sessionUri, serverName, serverUrl, provider)."
2407
+ );
2408
+ }
2409
+ if (!record.status || record.status !== "active" && record.status !== "closed") {
2410
+ throw new UsageError('Invalid session record: status must be "active" or "closed".');
2411
+ }
2412
+ if (!record.createdAt) {
2413
+ throw new UsageError("Invalid session record: missing createdAt timestamp.");
2414
+ }
2415
+ await sessionStore.save(record);
2416
+ outputResult(
2417
+ globalOpts,
2418
+ () => {
2419
+ console.log(pc6.green(`Session ${record.id.slice(0, 8)} imported successfully.`));
2420
+ console.log(` ${pc6.bold("URI:")} ${record.sessionUri}`);
2421
+ console.log(` ${pc6.bold("Server:")} ${record.serverName}`);
2422
+ console.log(` ${pc6.bold("Status:")} ${record.status}`);
2423
+ if (record.turns?.length) {
2424
+ console.log(` ${pc6.bold("Local turns:")} ${record.turns.length}`);
2425
+ }
2426
+ },
2427
+ { id: record.id, sessionUri: record.sessionUri, status: record.status }
2428
+ );
2429
+ } catch (err) {
2430
+ handleError(err, globalOpts);
2431
+ }
2432
+ });
2433
+ session.command("active").description("Show all active sessions on the server (live query)").option("-s, --server <name>", "Server name or WebSocket URL").option("-t, --timeout <ms>", "Connection timeout in milliseconds", "10000").action(async (opts, cmd) => {
2434
+ const globalOpts = parseGlobalOpts(cmd);
2435
+ applyGlobalOpts(globalOpts);
2436
+ try {
2437
+ const cfg = await loadConfig({ overrides: buildConfigOverrides(globalOpts) });
2438
+ await withConnection(
2439
+ {
2440
+ server: opts.server,
2441
+ config: cfg,
2442
+ timeout: Number.parseInt(opts.timeout, 10)
2443
+ },
2444
+ async (client, serverInfo) => {
2445
+ const result = await client.listSessions();
2446
+ outputResult(
2447
+ globalOpts,
2448
+ () => {
2449
+ if (result.items.length === 0) {
2450
+ console.log(pc6.dim(`No active sessions on ${serverInfo.name}.`));
2451
+ return;
2452
+ }
2453
+ console.log(pc6.bold(`Active sessions on ${serverInfo.name}`), pc6.dim(`(${result.items.length})`));
2454
+ console.log();
2455
+ for (const s of result.items) {
2456
+ const status = s.status === "in-progress" ? pc6.yellow("\u25CF in-progress") : s.status === "error" ? pc6.red("\u25CF error") : pc6.green("\u25CF idle");
2457
+ console.log(` ${pc6.bold(pc6.cyan(s.resource))}`);
2458
+ console.log(` ${pc6.bold("Provider:")} ${s.provider}`);
2459
+ if (s.model) console.log(` ${pc6.bold("Model:")} ${s.model}`);
2460
+ if (s.title) console.log(` ${pc6.bold("Title:")} ${s.title}`);
2461
+ console.log(` ${pc6.bold("Status:")} ${status}`);
2462
+ console.log();
2463
+ }
2464
+ },
2465
+ {
2466
+ server: serverInfo.name,
2467
+ sessions: result.items.map((s) => ({
2468
+ resource: s.resource,
2469
+ provider: s.provider,
2470
+ model: s.model,
2471
+ title: s.title,
2472
+ status: s.status,
2473
+ createdAt: s.createdAt
2474
+ }))
2475
+ }
2476
+ );
2477
+ }
2478
+ );
2479
+ } catch (err) {
2480
+ handleError(err, globalOpts);
2481
+ }
2482
+ });
2483
+ async function readPromptFile(filePath) {
2484
+ if (filePath === "-") {
2485
+ return readStdin();
2486
+ }
2487
+ const { readFile: readFile4 } = await import("fs/promises");
2488
+ const resolved = path4.resolve(filePath);
2489
+ return (await readFile4(resolved, "utf-8")).trim();
2490
+ }
2491
+ function readStdin() {
2492
+ return new Promise((resolve3, reject) => {
2493
+ let data = "";
2494
+ process.stdin.setEncoding("utf-8");
2495
+ process.stdin.on("data", (chunk) => {
2496
+ data += chunk;
2497
+ });
2498
+ process.stdin.on("end", () => resolve3(data.trim()));
2499
+ process.stdin.on("error", reject);
2500
+ });
2501
+ }
2502
+ function stdinIsPipe() {
2503
+ return !process.stdin.isTTY;
2504
+ }
2505
+ function resolvePermissionMode(opts, cfg) {
2506
+ if (opts.approveAll) return "approve-all";
2507
+ if (opts.approveReads) return "approve-reads";
2508
+ if (opts.denyAll) return "deny-all";
2509
+ return cfg.permissions ?? "approve-reads";
2510
+ }
2511
+ async function runPrompt(opts, globalOpts) {
2512
+ await requireCwdForRemoteServer(opts.server, opts.cwd);
2513
+ const cfg = await loadConfig({ overrides: buildConfigOverrides(globalOpts) });
2514
+ const cwd = opts.cwd ?? process.cwd();
2515
+ const gitRoot = await findGitRoot(cwd);
2516
+ const permMode = resolvePermissionMode(opts, cfg);
2517
+ let formatter = formatterFromOpts(globalOpts, opts.tags);
2518
+ let forwardingFormatter;
2519
+ if (opts.forwarders && opts.forwarders.length > 0) {
2520
+ forwardingFormatter = new ForwardingFormatter({
2521
+ inner: formatter,
2522
+ forwarders: opts.forwarders,
2523
+ tags: opts.tags
2524
+ });
2525
+ formatter = forwardingFormatter;
2526
+ }
2527
+ await withConnection(
2528
+ {
2529
+ server: opts.server,
2530
+ config: cfg
2531
+ },
2532
+ async (client, serverInfo) => {
2533
+ let sessionUri;
2534
+ let sessionRecord;
2535
+ if (opts.oneShot) {
2536
+ const spinner = startSpinner("Creating session...", spinnersEnabled(globalOpts));
2537
+ try {
2538
+ sessionUri = await createTempSession(client, opts, cfg, cwd);
2539
+ spinner.stop();
2540
+ } catch (err) {
2541
+ spinner.stop();
2542
+ throw err;
2543
+ }
2544
+ } else {
2545
+ const resolved = await resolveOrCreateSession(client, serverInfo, opts, cfg, cwd, gitRoot, globalOpts);
2546
+ sessionUri = resolved.sessionUri;
2547
+ sessionRecord = resolved.record;
2548
+ }
2549
+ if (forwardingFormatter) {
2550
+ forwardingFormatter.sessionUri = sessionUri;
2551
+ }
2552
+ const permHandler = new PermissionHandler(permMode);
2553
+ const controller = new TurnController(client, sessionUri, formatter, permHandler);
2554
+ let sigintCount = 0;
2555
+ const sigintHandler = () => {
2556
+ sigintCount++;
2557
+ if (sigintCount >= 2) {
2558
+ process.exitCode = ExitCode.Interrupted;
2559
+ process.exit(ExitCode.Interrupted);
2560
+ }
2561
+ if (globalOpts.format === "text") {
2562
+ console.error(pc6.dim("\nCancelling..."));
2563
+ }
2564
+ controller.cancel();
2565
+ };
2566
+ process.on("SIGINT", sigintHandler);
2567
+ try {
2568
+ const result = await controller.prompt(opts.text, void 0, {
2569
+ idleTimeout: opts.idleTimeout ? opts.idleTimeout * 1e3 : void 0
2570
+ });
2571
+ if (sessionRecord) {
2572
+ await sessionStore.update(sessionRecord.id, {
2573
+ lastPromptAt: (/* @__PURE__ */ new Date()).toISOString(),
2574
+ title: client.state.getSession(sessionUri)?.summary.title ?? sessionRecord.title
2575
+ });
2576
+ if (result.state === "complete" || result.state === "cancelled" || result.state === "error") {
2577
+ await sessionPersistence.saveTurn(sessionRecord.id, {
2578
+ turnId: result.turnId,
2579
+ responseText: result.responseText,
2580
+ toolCalls: result.toolCalls,
2581
+ usage: result.usage,
2582
+ state: result.state,
2583
+ userMessage: opts.text
2584
+ });
2585
+ }
2586
+ }
2587
+ if (result.state === "error") {
2588
+ process.exitCode = ExitCode.Error;
2589
+ }
2590
+ if (result.state === "idle_timeout") {
2591
+ throw new TimeoutError(`Idle timeout: no events received for ${opts.idleTimeout} seconds`);
2592
+ }
2593
+ } finally {
2594
+ process.removeListener("SIGINT", sigintHandler);
2595
+ if (forwardingFormatter) {
2596
+ await forwardingFormatter.close();
2597
+ }
2598
+ if (opts.oneShot) {
2599
+ const spinner = startSpinner("Disposing session...", spinnersEnabled(globalOpts));
2600
+ try {
2601
+ await client.disposeSession(sessionUri);
2602
+ spinner.stop();
2603
+ } catch {
2604
+ spinner.stop();
2605
+ }
2606
+ }
2607
+ }
2608
+ }
2609
+ );
2610
+ }
2611
+ async function createTempSession(client, opts, cfg, cwd) {
2612
+ const rootState = client.state.root;
2613
+ const provider = opts.provider ?? cfg.defaultProvider ?? (rootState.agents.length > 0 ? rootState.agents[0].provider : void 0);
2614
+ if (!provider) {
2615
+ throw new UsageError("No agent provider available. Specify one with --provider.");
2616
+ }
2617
+ const sessionId = randomUUID3();
2618
+ const sessionUri = `${provider}:/${sessionId}`;
2619
+ await client.createSession(sessionUri, provider, opts.model ?? cfg.defaultModel, ensureFileUri(cwd));
2620
+ await client.subscribe(sessionUri);
2621
+ await waitForReady(client, sessionUri);
2622
+ return sessionUri;
2623
+ }
2624
+ async function resolveOrCreateSession(client, serverInfo, opts, cfg, cwd, gitRoot, globalOpts) {
2625
+ const record = await resolveSession({
2626
+ serverName: serverInfo.name,
2627
+ cwd,
2628
+ name: opts.sessionName,
2629
+ store: sessionStore
2630
+ });
2631
+ if (record) {
2632
+ const outcome = await sessionPersistence.resume(record, client);
2633
+ if (outcome.status === "resumed") {
2634
+ return { sessionUri: record.sessionUri, record };
2635
+ }
2636
+ if (outcome.status === "not_found") {
2637
+ if (globalOpts.format === "text") {
2638
+ console.error(
2639
+ pc6.yellow(`Previous session ${record.id.slice(0, 8)} was disposed on the server. Creating a new one.`)
2640
+ );
2641
+ }
2642
+ await sessionStore.close(record.id);
2643
+ }
2644
+ }
2645
+ const rootState = client.state.root;
2646
+ const provider = opts.provider ?? cfg.defaultProvider ?? (rootState.agents.length > 0 ? rootState.agents[0].provider : void 0);
2647
+ if (!provider) {
2648
+ throw new UsageError("No agent provider available. Specify one with --provider.");
2649
+ }
2650
+ const sessionId = randomUUID3();
2651
+ const sessionUri = `${provider}:/${sessionId}`;
2652
+ const spinner = startSpinner(`Creating session on ${serverInfo.name}...`, spinnersEnabled(globalOpts));
2653
+ try {
2654
+ await client.createSession(sessionUri, provider, opts.model ?? cfg.defaultModel, ensureFileUri(cwd));
2655
+ await client.subscribe(sessionUri);
2656
+ spinner.update("Waiting for session ready...");
2657
+ await waitForReady(client, sessionUri);
2658
+ spinner.stop();
2659
+ } catch (err) {
2660
+ spinner.stop();
2661
+ throw err;
2662
+ }
2663
+ const newRecord = {
2664
+ id: sessionId,
2665
+ sessionUri,
2666
+ serverName: serverInfo.name,
2667
+ serverUrl: serverInfo.url,
2668
+ provider,
2669
+ model: opts.model ?? cfg.defaultModel ?? client.state.getSession(sessionUri)?.summary.model,
2670
+ name: opts.sessionName,
2671
+ workingDirectory: cwd,
2672
+ gitRoot,
2673
+ title: client.state.getSession(sessionUri)?.summary.title,
2674
+ status: "active",
2675
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2676
+ };
2677
+ await sessionStore.save(newRecord);
2678
+ return { sessionUri, record: newRecord };
2679
+ }
2680
+ function waitForReady(client, sessionUri) {
2681
+ return new Promise((resolve3, reject) => {
2682
+ const timeout = setTimeout(() => {
2683
+ reject(new TimeoutError("Timed out waiting for session to be ready"));
2684
+ }, 3e4);
2685
+ client.on("action", (envelope) => {
2686
+ const action = envelope.action;
2687
+ if (action.type === "session/ready" /* SessionReady */ && action.session === sessionUri) {
2688
+ clearTimeout(timeout);
2689
+ resolve3();
2690
+ } else if (action.type === "session/creationFailed" /* SessionCreationFailed */ && action.session === sessionUri) {
2691
+ clearTimeout(timeout);
2692
+ const sessionState2 = client.state.getSession(sessionUri);
2693
+ const errMsg = sessionState2?.creationError?.message ?? "Unknown error";
2694
+ reject(new AhpxError(`Session creation failed: ${errMsg}`, ExitCode.Error));
2695
+ }
2696
+ });
2697
+ const sessionState = client.state.getSession(sessionUri);
2698
+ if (sessionState?.lifecycle === "ready") {
2699
+ clearTimeout(timeout);
2700
+ resolve3();
2701
+ } else if (sessionState?.lifecycle === "creationFailed") {
2702
+ clearTimeout(timeout);
2703
+ const errMsg = sessionState?.creationError?.message ?? "Unknown error";
2704
+ reject(new AhpxError(`Session creation failed: ${errMsg}`, ExitCode.Error));
2705
+ }
2706
+ });
2707
+ }
2708
+ program.command("prompt").description("Send a prompt to an agent session").argument("<text...>", "Prompt text").option("-s, --server <name>", "Server name or WebSocket URL").option("-n, --session-name <name>", "Session name for scoped lookup").option("-f, --file <path>", "Read prompt from file (- for stdin)").option("--cwd <dir>", "Working directory for auto-created sessions").option("--approve-all", "Auto-approve all permissions").option("--approve-reads", "Auto-approve read permissions, prompt for others").option("--deny-all", "Auto-deny all permissions").option("--idle-timeout <seconds>", "Cancel if no events received within N seconds").option("--tag <key=value...>", "Add metadata tags to JSON events (repeatable)").option("--forward-webhook <url...>", "POST events to webhook URL (repeatable)").option("--forward-ws <url...>", "Stream events over WebSocket (repeatable)").option("--forward-filter <types>", "Comma-separated event types to forward (default: all)").option("--forward-headers <json>", "Custom headers for forwarders (JSON object)").action(
2709
+ async (textParts, opts, cmd) => {
2710
+ const globalOpts = parseGlobalOpts(cmd);
2711
+ applyGlobalOpts(globalOpts);
2712
+ try {
2713
+ let text = textParts.join(" ");
2714
+ if (opts.file) {
2715
+ text = await readPromptFile(opts.file);
2716
+ }
2717
+ if (!text) {
2718
+ throw new UsageError("No prompt text provided.");
2719
+ }
2720
+ await runPrompt(
2721
+ {
2722
+ text,
2723
+ ...opts,
2724
+ tags: parseTags(opts.tag),
2725
+ idleTimeout: parseIdleTimeout(opts.idleTimeout),
2726
+ forwarders: buildForwarders(opts)
2727
+ },
2728
+ globalOpts
2729
+ );
2730
+ } catch (err) {
2731
+ handleError(err, globalOpts);
2732
+ }
2733
+ }
2734
+ );
2735
+ program.command("exec").description("One-shot prompt: create temp session, prompt, dispose").argument("<text...>", "Prompt text").option("-s, --server <name>", "Server name or WebSocket URL").option("-p, --provider <provider>", "Agent provider (e.g. copilot)").option("-m, --model <model>", "Model to use").option("--cwd <dir>", "Working directory for the session").option("--approve-all", "Auto-approve all permissions").option("--approve-reads", "Auto-approve read permissions, prompt for others").option("--deny-all", "Auto-deny all permissions").option("--idle-timeout <seconds>", "Cancel if no events received within N seconds").option("--tag <key=value...>", "Add metadata tags to JSON events (repeatable)").option("--forward-webhook <url...>", "POST events to webhook URL (repeatable)").option("--forward-ws <url...>", "Stream events over WebSocket (repeatable)").option("--forward-filter <types>", "Comma-separated event types to forward (default: all)").option("--forward-headers <json>", "Custom headers for forwarders (JSON object)").action(
2736
+ async (textParts, opts, cmd) => {
2737
+ const globalOpts = parseGlobalOpts(cmd);
2738
+ applyGlobalOpts(globalOpts);
2739
+ try {
2740
+ const text = textParts.join(" ");
2741
+ if (!text) {
2742
+ throw new UsageError("No prompt text provided.");
2743
+ }
2744
+ await runPrompt(
2745
+ {
2746
+ text,
2747
+ oneShot: true,
2748
+ ...opts,
2749
+ tags: parseTags(opts.tag),
2750
+ idleTimeout: parseIdleTimeout(opts.idleTimeout),
2751
+ forwarders: buildForwarders(opts)
2752
+ },
2753
+ globalOpts
2754
+ );
2755
+ } catch (err) {
2756
+ handleError(err, globalOpts);
2757
+ }
2758
+ }
2759
+ );
2760
+ program.command("cancel").description("Cancel the active turn in a session").option("-n, --session-name <name>", "Session name (for scoped lookup)").option("-s, --server <name>", "Server name").action(async (opts, cmd) => {
2761
+ const globalOpts = parseGlobalOpts(cmd);
2762
+ applyGlobalOpts(globalOpts);
2763
+ try {
2764
+ const cfg = await loadConfig({ overrides: buildConfigOverrides(globalOpts) });
2765
+ const serverName = await resolveServerName(opts.server, cfg);
2766
+ const cwd = process.cwd();
2767
+ const record = await resolveSession({
2768
+ serverName,
2769
+ cwd,
2770
+ name: opts.sessionName,
2771
+ store: sessionStore
2772
+ });
2773
+ if (!record) {
2774
+ if (globalOpts.format === "text") {
2775
+ console.log(pc6.dim("No active session found. Nothing to cancel."));
2776
+ }
2777
+ return;
2778
+ }
2779
+ await withConnection({ server: opts.server, config: cfg }, async (client) => {
2780
+ await client.subscribe(record.sessionUri);
2781
+ const sessionState = client.state.getSession(record.sessionUri);
2782
+ if (!sessionState?.activeTurn) {
2783
+ if (globalOpts.format === "text") {
2784
+ console.log(pc6.dim("No active turn. Nothing to cancel."));
2785
+ }
2786
+ return;
2787
+ }
2788
+ client.dispatchAction({
2789
+ type: "session/turnCancelled" /* SessionTurnCancelled */,
2790
+ session: record.sessionUri,
2791
+ turnId: sessionState.activeTurn.id
2792
+ });
2793
+ outputResult(globalOpts, () => console.log(pc6.green("\u2713"), "Cancellation dispatched."), {
2794
+ cancelled: true,
2795
+ sessionUri: record.sessionUri
2796
+ });
2797
+ });
2798
+ } catch (err) {
2799
+ handleError(err, globalOpts);
2800
+ }
2801
+ });
2802
+ program.command("watch").description("Attach to a session as an observer and stream all activity").argument("[id]", "Session ID").option("-n, --session-name <name>", "Session name (for scoped lookup)").option("-s, --server <name>", "Server name").action(async (id, opts, cmd) => {
2803
+ const globalOpts = parseGlobalOpts(cmd);
2804
+ applyGlobalOpts(globalOpts);
2805
+ try {
2806
+ const record = await resolveSessionRecord(id, { name: opts.sessionName, server: opts.server });
2807
+ const cfg = await loadConfig({ overrides: buildConfigOverrides(globalOpts) });
2808
+ const formatter = formatterFromOpts(globalOpts);
2809
+ await withConnection({ server: opts.server, config: cfg }, async (client) => {
2810
+ const watcher = new SessionWatcher(client, record.sessionUri, formatter, {
2811
+ statusOut: process.stderr
2812
+ });
2813
+ const onSigint = () => watcher.stop();
2814
+ process.on("SIGINT", onSigint);
2815
+ if (globalOpts.format === "text") {
2816
+ process.stderr.write(
2817
+ pc6.dim(
2818
+ `[watch] Observing session ${pc6.bold(record.id.slice(0, 8))}${record.name ? ` (${record.name})` : ""}...
2819
+ `
2820
+ )
2821
+ );
2822
+ process.stderr.write(pc6.dim("[watch] Press Ctrl+C to detach.\n"));
2823
+ }
2824
+ try {
2825
+ await watcher.watch();
2826
+ } finally {
2827
+ process.removeListener("SIGINT", onSigint);
2828
+ }
2829
+ });
2830
+ } catch (err) {
2831
+ handleError(err, globalOpts);
2832
+ }
2833
+ });
2834
+ program.command("browse").description("Browse server filesystem").argument("[directory]", "Directory URI to browse (uses server default if omitted)").option("-s, --server <name>", "Server name").action(async (directory, opts, cmd) => {
2835
+ const globalOpts = parseGlobalOpts(cmd);
2836
+ applyGlobalOpts(globalOpts);
2837
+ try {
2838
+ const cfg = await loadConfig({ overrides: buildConfigOverrides(globalOpts) });
2839
+ await withConnection({ server: opts.server, config: cfg }, async (client) => {
2840
+ const result = await client.resourceList(directory ? ensureFileUri(directory) : "");
2841
+ outputResult(
2842
+ globalOpts,
2843
+ () => {
2844
+ if (result.entries.length === 0) {
2845
+ console.log(pc6.dim("Directory is empty."));
2846
+ return;
2847
+ }
2848
+ const sorted = [...result.entries].sort((a, b) => {
2849
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
2850
+ return a.name.localeCompare(b.name);
2851
+ });
2852
+ for (const entry of sorted) {
2853
+ const icon = entry.type === "directory" ? pc6.blue("\u{1F4C1}") : "\u{1F4C4}";
2854
+ const name = entry.type === "directory" ? pc6.bold(entry.name) : entry.name;
2855
+ console.log(` ${icon} ${name}`);
2856
+ }
2857
+ },
2858
+ result
2859
+ );
2860
+ });
2861
+ } catch (err) {
2862
+ handleError(err, globalOpts);
2863
+ }
2864
+ });
2865
+ program.command("content").description("Fetch content by URI from the server").argument("<uri>", "Content reference URI").option("-s, --server <name>", "Server name").option("-o, --output <file>", "Write content to a file instead of stdout").action(async (uri, opts, cmd) => {
2866
+ const globalOpts = parseGlobalOpts(cmd);
2867
+ applyGlobalOpts(globalOpts);
2868
+ try {
2869
+ const cfg = await loadConfig({ overrides: buildConfigOverrides(globalOpts) });
2870
+ await withConnection({ server: opts.server, config: cfg }, async (client) => {
2871
+ const result = await client.resourceRead(ensureFileUri(uri));
2872
+ const data = result.encoding === "base64" ? Buffer.from(result.data, "base64") : Buffer.from(result.data, "utf-8");
2873
+ if (opts.output) {
2874
+ await fs4.writeFile(opts.output, data);
2875
+ if (globalOpts.format === "text") {
2876
+ console.log(pc6.green("\u2713"), `Wrote ${data.length} bytes to ${pc6.bold(opts.output)}`);
2877
+ }
2878
+ } else if (globalOpts.format === "json") {
2879
+ console.log(
2880
+ JSON.stringify({
2881
+ uri,
2882
+ encoding: result.encoding,
2883
+ contentType: result.contentType,
2884
+ data: result.data,
2885
+ size: data.length
2886
+ })
2887
+ );
2888
+ } else {
2889
+ process.stdout.write(data);
2890
+ }
2891
+ });
2892
+ } catch (err) {
2893
+ handleError(err, globalOpts);
2894
+ }
2895
+ });
2896
+ program.command("model").description("Switch the model for a session").argument("<model-id>", "Model ID to switch to").option("-n, --session-name <name>", "Session name (for scoped lookup)").option("-s, --server <name>", "Server name").action(async (modelId, opts, cmd) => {
2897
+ const globalOpts = parseGlobalOpts(cmd);
2898
+ applyGlobalOpts(globalOpts);
2899
+ try {
2900
+ const record = await resolveSessionRecord(void 0, { name: opts.sessionName, server: opts.server });
2901
+ const cfg = await loadConfig({ overrides: buildConfigOverrides(globalOpts) });
2902
+ await withConnection({ server: opts.server, config: cfg }, async (client) => {
2903
+ await client.subscribe(record.sessionUri);
2904
+ client.dispatchAction({
2905
+ type: "session/modelChanged" /* SessionModelChanged */,
2906
+ session: record.sessionUri,
2907
+ model: modelId
2908
+ });
2909
+ outputResult(globalOpts, () => console.log(pc6.green("\u2713"), `Model change to ${pc6.bold(modelId)} dispatched.`), {
2910
+ sessionUri: record.sessionUri,
2911
+ model: modelId
2912
+ });
2913
+ });
2914
+ } catch (err) {
2915
+ handleError(err, globalOpts);
2916
+ }
2917
+ });
2918
+ program.command("agents").description("List available agents and models on the server").option("-s, --server <name>", "Server name").action(async (opts, cmd) => {
2919
+ const globalOpts = parseGlobalOpts(cmd);
2920
+ applyGlobalOpts(globalOpts);
2921
+ try {
2922
+ const cfg = await loadConfig({ overrides: buildConfigOverrides(globalOpts) });
2923
+ await withConnection({ server: opts.server, config: cfg }, async (client) => {
2924
+ const rootState = client.state.root;
2925
+ outputResult(
2926
+ globalOpts,
2927
+ () => {
2928
+ if (rootState.agents.length === 0) {
2929
+ console.log(pc6.dim("No agents available on this server."));
2930
+ return;
2931
+ }
2932
+ for (const agent of rootState.agents) {
2933
+ console.log(pc6.bold(pc6.cyan(agent.provider)), pc6.dim("\u2014"), agent.displayName);
2934
+ if (agent.description) {
2935
+ console.log(` ${pc6.dim(agent.description)}`);
2936
+ }
2937
+ if (agent.models.length > 0) {
2938
+ console.log(` ${pc6.bold("Models:")}`);
2939
+ for (const m of agent.models) {
2940
+ console.log(` ${pc6.cyan(m.id)}${m.name ? ` \u2014 ${m.name}` : ""}`);
2941
+ }
2942
+ }
2943
+ console.log();
2944
+ }
2945
+ },
2946
+ rootState.agents.map((a) => ({
2947
+ provider: a.provider,
2948
+ displayName: a.displayName,
2949
+ description: a.description,
2950
+ models: a.models.map((m) => ({ id: m.id, name: m.name }))
2951
+ }))
2952
+ );
2953
+ });
2954
+ } catch (err) {
2955
+ handleError(err, globalOpts);
2956
+ }
2957
+ });
2958
+ var completions = program.command("completions").description("Generate shell completion scripts");
2959
+ completions.command("bash").description("Print bash completion script").action(() => {
2960
+ process.stdout.write(bashCompletion());
2961
+ });
2962
+ completions.command("zsh").description("Print zsh completion script").action(() => {
2963
+ process.stdout.write(zshCompletion());
2964
+ });
2965
+ completions.command("fish").description("Print fish completion script").action(() => {
2966
+ process.stdout.write(fishCompletion());
2967
+ });
2968
+ async function handleImplicitPrompt() {
2969
+ const args = process.argv.slice(2);
2970
+ const globalOpts = { format: "text", jsonStrict: false, verbose: false };
2971
+ for (let i2 = 0; i2 < args.length; i2++) {
2972
+ if (args[i2] === "--format" && i2 + 1 < args.length) {
2973
+ globalOpts.format = args[i2 + 1];
2974
+ i2++;
2975
+ } else if (args[i2] === "--json-strict") {
2976
+ globalOpts.jsonStrict = true;
2977
+ } else if (args[i2] === "--verbose" || args[i2] === "-v") {
2978
+ globalOpts.verbose = true;
2979
+ }
2980
+ }
2981
+ applyGlobalOpts(globalOpts);
2982
+ if (args.length === 0 && stdinIsPipe()) {
2983
+ const text = await readStdin();
2984
+ if (text) {
2985
+ await runPrompt({ text }, globalOpts);
2986
+ return true;
2987
+ }
2988
+ return false;
2989
+ }
2990
+ const positional = [];
2991
+ const flags = {};
2992
+ let i = 0;
2993
+ while (i < args.length) {
2994
+ if (args[i] === "--format") {
2995
+ i += 2;
2996
+ continue;
2997
+ }
2998
+ if (args[i] === "--json-strict" || args[i] === "--verbose" || args[i] === "-v") {
2999
+ i++;
3000
+ continue;
3001
+ }
3002
+ if (args[i] === "--server" || args[i] === "-s") {
3003
+ flags.server = args[++i];
3004
+ } else if (args[i] === "--session-name" || args[i] === "-n") {
3005
+ flags.sessionName = args[++i];
3006
+ } else if (args[i] === "--cwd") {
3007
+ flags.cwd = args[++i];
3008
+ } else if (args[i] === "--approve-all") {
3009
+ flags.approveAll = true;
3010
+ } else if (args[i] === "--approve-reads") {
3011
+ flags.approveReads = true;
3012
+ } else if (args[i] === "--deny-all") {
3013
+ flags.denyAll = true;
3014
+ } else if (args[i] === "--file" || args[i] === "-f") {
3015
+ flags.file = args[++i];
3016
+ } else if (!args[i].startsWith("-")) {
3017
+ positional.push(args[i]);
3018
+ }
3019
+ i++;
3020
+ }
3021
+ if (positional.length > 0) {
3022
+ const knownCommands = /* @__PURE__ */ new Set([
3023
+ "connect",
3024
+ "server",
3025
+ "config",
3026
+ "session",
3027
+ "prompt",
3028
+ "exec",
3029
+ "cancel",
3030
+ "watch",
3031
+ "browse",
3032
+ "content",
3033
+ "model",
3034
+ "agents",
3035
+ "completions",
3036
+ "help",
3037
+ "--help",
3038
+ "-h",
3039
+ "--version",
3040
+ "-V"
3041
+ ]);
3042
+ if (!knownCommands.has(positional[0])) {
3043
+ let text;
3044
+ if (flags.file && typeof flags.file === "string") {
3045
+ text = await readPromptFile(flags.file);
3046
+ } else {
3047
+ text = positional.join(" ");
3048
+ }
3049
+ if (text) {
3050
+ await runPrompt(
3051
+ {
3052
+ text,
3053
+ server: typeof flags.server === "string" ? flags.server : void 0,
3054
+ sessionName: typeof flags.sessionName === "string" ? flags.sessionName : void 0,
3055
+ cwd: typeof flags.cwd === "string" ? flags.cwd : void 0,
3056
+ approveAll: flags.approveAll === true ? true : void 0,
3057
+ approveReads: flags.approveReads === true ? true : void 0,
3058
+ denyAll: flags.denyAll === true ? true : void 0
3059
+ },
3060
+ globalOpts
3061
+ );
3062
+ return true;
3063
+ }
3064
+ }
3065
+ }
3066
+ if (positional.length === 0 && stdinIsPipe()) {
3067
+ const text = await readStdin();
3068
+ if (text) {
3069
+ await runPrompt({ text }, globalOpts);
3070
+ return true;
3071
+ }
3072
+ }
3073
+ return false;
3074
+ }
3075
+ (async () => {
3076
+ try {
3077
+ const handled = await handleImplicitPrompt();
3078
+ if (!handled) {
3079
+ program.hook("preAction", (thisCommand) => {
3080
+ const globalOpts = parseGlobalOpts(thisCommand);
3081
+ applyGlobalOpts(globalOpts);
3082
+ });
3083
+ program.parse();
3084
+ }
3085
+ } catch (err) {
3086
+ const exitCode = err instanceof AhpxError ? err.exitCode : ExitCode.Error;
3087
+ console.error(pc6.red("\u2717"), err instanceof Error ? err.message : String(err));
3088
+ process.exitCode = exitCode;
3089
+ }
3090
+ })();