@tabbio-technologies/cli 1.2.8

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.
@@ -0,0 +1,2755 @@
1
+ import { createRequire as __tabbioCreateRequire } from 'node:module';
2
+ const require = __tabbioCreateRequire(import.meta.url);
3
+ import {
4
+ createCommandContext,
5
+ mcpSummary,
6
+ readGlobalOptions,
7
+ registerStatusCommand,
8
+ registerWhoamiCommand
9
+ } from "./chunk-Y6GWIFHI.js";
10
+ import {
11
+ ARTIFACT_KINDS,
12
+ ARTIFACT_PRODUCING_TOOL_IDS,
13
+ PREVIEW_VARIANTS,
14
+ RESERVED_GLOBAL_FLAGS,
15
+ artifactFromResult,
16
+ assertInteractive,
17
+ autoFilledProperties,
18
+ canUseInteractiveUi,
19
+ capabilityDenial,
20
+ capabilityRequestFields,
21
+ defaultVariantFor,
22
+ deleteArtifact,
23
+ describeSchema,
24
+ didYouMean,
25
+ displayWidth,
26
+ downloadToFile,
27
+ downloadUrl,
28
+ exampleInvocation,
29
+ extensionFor,
30
+ flagsForProperties,
31
+ formatDuration,
32
+ formatKv,
33
+ formatTable,
34
+ formatToVariant,
35
+ getArtifact,
36
+ getInteractiveHooks,
37
+ hyperlink,
38
+ isInteractiveTerminal,
39
+ jsonEnvelope,
40
+ listArtifacts,
41
+ listVersions,
42
+ loadCapabilityPrefs,
43
+ mergeSources,
44
+ parseFields,
45
+ parseToolInput,
46
+ previewUrl,
47
+ previewVariantFor,
48
+ printEmptyNotice,
49
+ renderResult,
50
+ resolveCapabilities,
51
+ resolveOutputMode,
52
+ resolveOutputPath,
53
+ restoreVersion,
54
+ safeFileName,
55
+ saveCapabilityPrefs,
56
+ schemaToFlags,
57
+ schemaToOptions,
58
+ setInteractiveHooks,
59
+ shortDescription,
60
+ sourcesFromResult,
61
+ streamChat,
62
+ streamChatFrames,
63
+ supportsHyperlinks,
64
+ toTsv,
65
+ truncate,
66
+ waitForArtifact,
67
+ writeJson
68
+ } from "./chunk-QVR5KIEQ.js";
69
+ import {
70
+ ApiClient,
71
+ CLI_PACKAGE_NAME,
72
+ CLI_VERSION,
73
+ CliError,
74
+ DEFAULT_PROFILE,
75
+ EXIT_CODE_DOCS,
76
+ ExitCode,
77
+ McpSession,
78
+ PRIMARY_AGENT_KEY,
79
+ PROFILE_PRESETS,
80
+ assertJwtSession,
81
+ assertProfileName,
82
+ checkForUpdate,
83
+ chooseLoginMethod,
84
+ clearCatalogCache,
85
+ configPaths,
86
+ credentialPermissionIssues,
87
+ credentialsFromLogin,
88
+ debug,
89
+ describeMcpBearer,
90
+ deviceLabel,
91
+ findTool,
92
+ fingerprint,
93
+ getGlobalOptions,
94
+ getMcpAccessState,
95
+ groupCatalog,
96
+ hasDisplay,
97
+ info,
98
+ interpretToolPayload,
99
+ interruptedError,
100
+ isCliError,
101
+ isInteractive,
102
+ listCredentialProfiles,
103
+ listProfileNames,
104
+ loadConfig,
105
+ loadCredentials,
106
+ loadStoredCredentials,
107
+ loginWithBrowser,
108
+ loginWithEmailOtp,
109
+ logoutProfile,
110
+ normalizeBaseUrl,
111
+ notSignedInError,
112
+ printJson,
113
+ printKeyValues,
114
+ readCachedCatalog,
115
+ readRequestId,
116
+ redactSecrets,
117
+ resolveProfile,
118
+ resolveProfileWithSources,
119
+ revokeSession,
120
+ saveCredentials,
121
+ selectMcpBearer,
122
+ setGlobalOptions,
123
+ successLine,
124
+ toCliError,
125
+ updateConfig,
126
+ usageError,
127
+ warn,
128
+ writeOut
129
+ } from "./chunk-7LNC3FIV.js";
130
+ import {
131
+ activeCommandSignal
132
+ } from "./chunk-NK5RPNSV.js";
133
+ import {
134
+ configureTheme,
135
+ theme
136
+ } from "./chunk-VHHZFMIF.js";
137
+
138
+ // src/program.ts
139
+ import { Command as Command2, CommanderError as CommanderError2 } from "commander";
140
+
141
+ // src/core/prompt.ts
142
+ import { createInterface } from "node:readline";
143
+ function cancelled() {
144
+ return interruptedError();
145
+ }
146
+ function promptLine(question) {
147
+ return new Promise((resolve, reject2) => {
148
+ const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: Boolean(process.stdin.isTTY) });
149
+ let answered = false;
150
+ rl.once("SIGINT", () => {
151
+ rl.close();
152
+ reject2(cancelled());
153
+ });
154
+ rl.once("close", () => {
155
+ if (!answered) reject2(usageError("No input received"));
156
+ });
157
+ rl.question(question, (answer) => {
158
+ answered = true;
159
+ rl.close();
160
+ resolve(answer.trim());
161
+ });
162
+ });
163
+ }
164
+ function promptSecret(question) {
165
+ const stdin = process.stdin;
166
+ if (!stdin.isTTY || typeof stdin.setRawMode !== "function") return promptLine(question);
167
+ return new Promise((resolve, reject2) => {
168
+ let value = "";
169
+ process.stderr.write(question);
170
+ const wasRaw = stdin.isRaw;
171
+ stdin.setRawMode(true);
172
+ stdin.resume();
173
+ stdin.setEncoding("utf8");
174
+ const finish = (error) => {
175
+ stdin.off("data", onData);
176
+ stdin.setRawMode(wasRaw);
177
+ stdin.pause();
178
+ process.stderr.write("\n");
179
+ if (error) reject2(error);
180
+ else resolve(value.trim());
181
+ };
182
+ const onData = (chunk) => {
183
+ for (const char of chunk) {
184
+ if (char === "") return finish(cancelled());
185
+ if (char === "" && value === "") return finish(cancelled());
186
+ if (char === "\r" || char === "\n") return finish();
187
+ if (char === "\x7F" || char === "\b") {
188
+ if (value.length > 0) {
189
+ value = value.slice(0, -1);
190
+ process.stderr.write("\b \b");
191
+ }
192
+ continue;
193
+ }
194
+ if (char < " ") continue;
195
+ value += char;
196
+ process.stderr.write("\u2022");
197
+ }
198
+ };
199
+ stdin.on("data", onData);
200
+ });
201
+ }
202
+ async function confirm(question, opts = {}) {
203
+ if (opts.assumeYes) return true;
204
+ if (!process.stdin.isTTY) {
205
+ throw usageError(`${question} (needs confirmation)`, "Re-run with --yes to confirm non-interactively.");
206
+ }
207
+ const suffix = opts.defaultYes ? " [Y/n] " : " [y/N] ";
208
+ const answer = (await promptLine(`${question}${suffix}`)).toLowerCase();
209
+ if (!answer) return Boolean(opts.defaultYes);
210
+ return answer === "y" || answer === "yes";
211
+ }
212
+
213
+ // src/core/approvals.ts
214
+ var DEFAULT_WAIT_TIMEOUT_MS = 10 * 6e4;
215
+ var POLL_START_MS = 3e3;
216
+ var POLL_MAX_MS = 1e4;
217
+ var CONFIRM_TIMEOUT_MS = 10 * 6e4;
218
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
219
+ function parseDuration(value, fallbackMs = DEFAULT_WAIT_TIMEOUT_MS) {
220
+ if (value === void 0 || value === "") return fallbackMs;
221
+ const match = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h)?$/i.exec(value.trim());
222
+ if (!match) throw usageError(`Invalid duration "${value}"`, "Use e.g. 30s, 10m or 1h.");
223
+ const amount = Number(match[1]);
224
+ const unit = (match[2] ?? "s").toLowerCase();
225
+ const factor = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : 36e5;
226
+ return Math.round(amount * factor);
227
+ }
228
+ function hasJwtSession(ctx) {
229
+ return Boolean(ctx.api.credentials?.accessToken);
230
+ }
231
+ function label(tool, approval) {
232
+ return tool?.commandPath?.length ? tool.commandPath.join(" ") : approval.toolName || tool?.id || "this action";
233
+ }
234
+ async function confirmApproval(api, approvalId, reason) {
235
+ const response = await api.request(`/api/agent/approvals/${encodeURIComponent(approvalId)}/confirm`, {
236
+ method: "POST",
237
+ body: reason ? { reason } : {},
238
+ timeoutMs: CONFIRM_TIMEOUT_MS
239
+ });
240
+ const data = response.data ?? {};
241
+ const result = data.mcp ? data.mcp.result : data.assistant?.text ?? null;
242
+ return { result: result === void 0 ? null : result, data, requestId: response.requestId };
243
+ }
244
+ async function rejectApproval(api, approvalId, reason) {
245
+ const response = await api.request(`/api/agent/approvals/${encodeURIComponent(approvalId)}/reject`, {
246
+ method: "POST",
247
+ body: reason ? { reason } : {}
248
+ });
249
+ return { data: response.data ?? {}, requestId: response.requestId };
250
+ }
251
+ async function fetchApprovalStatus(session, approvalId) {
252
+ const response = await session.callTool("mcp.approvalStatus", { approvalId });
253
+ if (!response.ok) throw response.error;
254
+ const record = response.result;
255
+ if (!record || typeof record.status !== "string") {
256
+ throw new CliError({ code: "BAD_RESPONSE", message: "Unexpected approval status response", exitCode: ExitCode.Server });
257
+ }
258
+ return { id: approvalId, toolName: "", ...record };
259
+ }
260
+ async function pollApprovalStatus(session, approvalId, opts = {}) {
261
+ const sleep = opts.sleep ?? defaultSleep;
262
+ const now = opts.now ?? Date.now;
263
+ const deadline = now() + (opts.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS);
264
+ let interval = POLL_START_MS;
265
+ let last = null;
266
+ for (; ; ) {
267
+ try {
268
+ last = await fetchApprovalStatus(session, approvalId);
269
+ if (last.status !== "pending") return { record: last, timedOut: false };
270
+ } catch (error) {
271
+ if (!(error instanceof CliError) || error.exitCode !== ExitCode.Network) throw error;
272
+ }
273
+ const remaining = deadline - now();
274
+ if (remaining <= 0) return { record: last, timedOut: true };
275
+ await sleep(Math.min(interval, remaining));
276
+ interval = Math.min(POLL_MAX_MS, Math.round(interval * 1.5));
277
+ }
278
+ }
279
+ function notice(mode2, line) {
280
+ if (mode2 === "json") return;
281
+ process.stderr.write(`${line}
282
+ `);
283
+ }
284
+ function exitCodeForApprovalStatus(status2, timedOut = false) {
285
+ if (timedOut || status2 === "pending") return ExitCode.ApprovalPending;
286
+ return status2 === "approved" ? ExitCode.Ok : ExitCode.Error;
287
+ }
288
+ function describeWaitResult(toolLabel, record, timedOut, timeoutMs) {
289
+ const status2 = record?.status ?? "pending";
290
+ if (timedOut || status2 === "pending") {
291
+ return [`${theme.warn(theme.symbols.warning)} Still pending after ${formatDuration(timeoutMs)}`];
292
+ }
293
+ if (status2 === "approved") {
294
+ return [
295
+ `${theme.success(theme.symbols.success)} Approved ${toolLabel}`,
296
+ ` ${theme.dim("Tabbio ran it when it was approved; the output is shown where it was approved, not here.")}`
297
+ ];
298
+ }
299
+ const reason = record?.reason ? `: ${record.reason}` : "";
300
+ return [`${theme.error(theme.symbols.error)} ${status2 === "rejected" ? "Rejected" : `Approval ${status2}`} ${toolLabel}${reason}`];
301
+ }
302
+ async function decide(ctx, tool, approval, args, mode2) {
303
+ const jwt = hasJwtSession(ctx);
304
+ if (ctx.globals.yes && jwt) return "approve";
305
+ if (mode2 === "json" || ctx.globals.json || !jwt) return "leave";
306
+ const hooks = getInteractiveHooks();
307
+ if (hooks && canUseInteractiveUi(ctx) && mode2 !== "tsv") {
308
+ return hooks.approvalPrompt(ctx, { ...approval, args });
309
+ }
310
+ if (process.stdin.isTTY && process.stderr.isTTY) {
311
+ notice(mode2, `${theme.warn(theme.symbols.warning)} ${approval.message}`);
312
+ return await confirm(`Approve ${label(tool, approval)} now?`) ? "approve" : "leave";
313
+ }
314
+ return "leave";
315
+ }
316
+ async function handleApproval(ctx, session, tool, approval, args, opts = {}) {
317
+ const mode2 = opts.mode;
318
+ const name = label(tool, approval);
319
+ const id = approval.approvalId;
320
+ const decision = await decide(ctx, tool, approval, args, mode2);
321
+ if (decision === "approve") {
322
+ const confirmed = await confirmApproval(ctx.api, id);
323
+ notice(mode2, `${theme.success(theme.symbols.success)} Approved ${name}`);
324
+ return { approval, status: "approved", executed: true, result: confirmed.result, requestId: confirmed.requestId };
325
+ }
326
+ if (decision === "reject") {
327
+ await rejectApproval(ctx.api, id);
328
+ notice(mode2, `${theme.error(theme.symbols.error)} Rejected ${name} ${theme.dim(id)}`);
329
+ process.exitCode = ExitCode.Error;
330
+ return { approval, status: "rejected", executed: false };
331
+ }
332
+ const jwt = hasJwtSession(ctx);
333
+ if (!opts.wait) {
334
+ notice(mode2, `${theme.warn(theme.symbols.warning)} Approval needed: ${name} ${theme.dim(id)}`);
335
+ notice(mode2, ` ${approval.message}`);
336
+ if (ctx.globals.yes && !jwt) {
337
+ notice(mode2, ` ${theme.dim("--yes can only approve with a full sign-in (`tabbio login`); this profile uses a personal MCP token.")}`);
338
+ }
339
+ notice(
340
+ mode2,
341
+ ` ${theme.dim(`\u2192 Approve it in the Tabbio app${jwt ? ` or with \`tabbio approvals approve ${id}\`` : ""}, then \`tabbio approvals wait ${id}\`.`)}`
342
+ );
343
+ process.exitCode = ExitCode.ApprovalPending;
344
+ return { approval, status: "pending", executed: false };
345
+ }
346
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
347
+ notice(
348
+ mode2,
349
+ `${theme.warn(theme.symbols.warning)} Waiting for approval of ${name} ${theme.dim(`${id} \xB7 up to ${formatDuration(timeoutMs)} \xB7 approve it in the Tabbio app`)}`
350
+ );
351
+ const { record, timedOut } = await pollApprovalStatus(session, id, { timeoutMs, sleep: opts.sleep });
352
+ const status2 = timedOut ? "pending" : record?.status ?? "pending";
353
+ const lines = describeWaitResult(name, record, timedOut, timeoutMs);
354
+ for (const line of opts.followsOutput && status2 === "approved" ? lines.slice(0, 1) : lines) notice(mode2, line);
355
+ const code = exitCodeForApprovalStatus(status2, timedOut);
356
+ if (code !== ExitCode.Ok) process.exitCode = code;
357
+ return { approval, status: status2, executed: false, ...record ? { record } : {}, timedOut };
358
+ }
359
+ function approvalMeta(outcome) {
360
+ return {
361
+ approvalId: outcome.approval.approvalId,
362
+ toolName: outcome.approval.toolName,
363
+ status: outcome.status,
364
+ message: outcome.approval.message,
365
+ executed: outcome.executed,
366
+ ...outcome.timedOut ? { timedOut: true } : {},
367
+ ...outcome.record?.reason ? { reason: outcome.record.reason } : {}
368
+ };
369
+ }
370
+
371
+ // src/core/artifact-wait.ts
372
+ function isArtifactProducingTool(toolId) {
373
+ return ARTIFACT_PRODUCING_TOOL_IDS.has(toolId);
374
+ }
375
+ function artifactStatusReader(ctx, session, artifactId) {
376
+ return async () => {
377
+ if (ctx.api.credentials?.accessToken || !session) {
378
+ const detail = await getArtifact(ctx.api, artifactId);
379
+ const { document: _document, sources: _sources, ...summary } = detail;
380
+ return summary;
381
+ }
382
+ const response = await session.callTool("artifact.get", { artifactId });
383
+ if (!response.ok) throw response.error;
384
+ const card = artifactFromResult(response.result);
385
+ return { ...response.result, status: card?.status ?? "pending" };
386
+ };
387
+ }
388
+ function notice2(ctx, json, line) {
389
+ if (json || ctx.globals.quiet) return;
390
+ process.stderr.write(`${line}
391
+ `);
392
+ }
393
+ async function waitForToolArtifact(ctx, session, result, opts) {
394
+ const card = artifactFromResult(result);
395
+ if (!card) return null;
396
+ if (card.status !== "pending") return { artifactId: card.id, status: card.status, timedOut: false, latest: null };
397
+ notice2(ctx, opts.json, theme.dim(`${theme.symbols.pending} Waiting for ${card.kind} ${card.id} to finish rendering (up to ${formatDuration(opts.timeoutMs)})`));
398
+ const { artifact, timedOut } = await waitForArtifact(artifactStatusReader(ctx, session, card.id), {
399
+ timeoutMs: opts.timeoutMs,
400
+ sleep: opts.sleep
401
+ });
402
+ return { artifactId: card.id, status: artifact?.status ?? "pending", timedOut, latest: artifact };
403
+ }
404
+ function mergeWaitedArtifact(result, outcome) {
405
+ const { message: _message, ...base } = result ?? {};
406
+ const latest = outcome.latest ?? {};
407
+ const variants = latest.variants ?? base.variants;
408
+ const kind = String(latest.kind ?? base.kind ?? "");
409
+ const previewAvailable = kind === "image" ? outcome.status === "ready" && Boolean(variants?.image || variants?.thumbnail) : outcome.status !== "failed" && Boolean(variants?.html);
410
+ const error = latest.error ?? latest.failure;
411
+ return {
412
+ ...base,
413
+ ...typeof latest.kind === "string" ? { kind: latest.kind } : {},
414
+ ...typeof latest.title === "string" ? { title: latest.title } : {},
415
+ status: outcome.status,
416
+ ...variants ? { variants } : {},
417
+ previewAvailable,
418
+ ...latest.image !== void 0 ? { image: latest.image } : {},
419
+ ...outcome.status === "failed" && error ? { failure: error } : {}
420
+ };
421
+ }
422
+ function artifactHints(ctx, json, artifactId, status2, timedOut = false) {
423
+ if (status2 === "failed") return;
424
+ if (status2 === "pending" || timedOut) {
425
+ notice2(ctx, json, theme.dim(`${theme.symbols.arrowRight} Still rendering: tabbio artifacts wait ${artifactId}`));
426
+ return;
427
+ }
428
+ notice2(
429
+ ctx,
430
+ json,
431
+ theme.dim(`${theme.symbols.arrowRight} tabbio artifacts open ${artifactId} ${theme.symbols.middot} tabbio artifacts download ${artifactId}`)
432
+ );
433
+ }
434
+ async function findArtifactForApproval(ctx, input, approvalCreatedAt) {
435
+ if (typeof input.artifactId === "string" && input.artifactId) return input.artifactId;
436
+ const title = typeof input.title === "string" ? input.title.trim() : "";
437
+ if (!title || !ctx.api.credentials?.accessToken) return null;
438
+ const since = approvalCreatedAt ? Date.parse(approvalCreatedAt) : Number.NaN;
439
+ const { artifacts } = await listArtifacts(ctx.api, { limit: 10 });
440
+ const match = artifacts.find((a) => a.title === title && (!Number.isFinite(since) || Date.parse(a.createdAt) >= since - 1e3));
441
+ return match?.id ?? null;
442
+ }
443
+
444
+ // src/commands/approvals.ts
445
+ var LIST_FIELDS = ["id", "toolName", "status", "createdAt", "args"];
446
+ function requireJwtForApprovals(ctx, what) {
447
+ if (hasJwtSession(ctx)) return;
448
+ if (ctx.credentials?.mcpToken) {
449
+ throw new CliError({
450
+ code: "FULL_SIGN_IN_REQUIRED",
451
+ message: `${what} needs a full sign-in; this profile only has a personal MCP token`,
452
+ hint: "With the token you can still check one approval: `tabbio approvals show <id>` or `tabbio approvals wait <id>`. Run `tabbio login` for the rest.",
453
+ exitCode: ExitCode.Auth
454
+ });
455
+ }
456
+ throw notSignedInError(ctx.profile.name);
457
+ }
458
+ function mode(ctx) {
459
+ return resolveOutputMode({ json: ctx.globals.json });
460
+ }
461
+ function write(lines) {
462
+ for (const line of lines) process.stdout.write(`${line}
463
+ `);
464
+ }
465
+ async function withContext(cmd, fn) {
466
+ const ctx = createCommandContext(cmd);
467
+ try {
468
+ await fn(ctx);
469
+ } finally {
470
+ await ctx.close();
471
+ }
472
+ }
473
+ async function list(ctx) {
474
+ requireJwtForApprovals(ctx, "Listing approvals");
475
+ const started = Date.now();
476
+ const state = await getMcpAccessState(ctx.api);
477
+ const pending = state.pendingApprovals ?? [];
478
+ const meta = { durationMs: Date.now() - started, requestId: ctx.api.lastRequestId };
479
+ const out = mode(ctx);
480
+ if (out === "json") return writeJson(jsonEnvelope({ id: "approvals.list" }, pending, meta));
481
+ if (pending.length === 0) return printEmptyNotice(out, "No pending approvals", { quiet: ctx.globals.quiet });
482
+ if (out === "tsv") {
483
+ process.stdout.write(toTsv(pending, LIST_FIELDS));
484
+ return;
485
+ }
486
+ const rows = pending.map((p) => ({ ...p }));
487
+ write(formatTable(rows, { fields: LIST_FIELDS, width: process.stdout.columns }));
488
+ write([
489
+ theme.dim(`${pending.length} pending \xB7 tabbio approvals approve <id> \xB7 tabbio approvals reject <id> \xB7 tabbio approvals show <id>`)
490
+ ]);
491
+ }
492
+ async function show(ctx, id) {
493
+ const started = Date.now();
494
+ const session = await ctx.mcp();
495
+ const record = await fetchApprovalStatus(session, id);
496
+ if (hasJwtSession(ctx) && record.status === "pending") {
497
+ const state = await getMcpAccessState(ctx.api).catch(() => null);
498
+ const match = state?.pendingApprovals.find((p) => p.id === id);
499
+ if (match) record.args = match.args;
500
+ }
501
+ const out = mode(ctx);
502
+ if (out === "json") return writeJson(jsonEnvelope({ id: "approvals.show" }, record, { durationMs: Date.now() - started }));
503
+ if (out === "tsv") {
504
+ process.stdout.write(toTsv(record));
505
+ return;
506
+ }
507
+ write(formatKv(record, { width: process.stdout.columns }));
508
+ }
509
+ async function approve(ctx, id, opts) {
510
+ requireJwtForApprovals(ctx, "Approving");
511
+ const started = Date.now();
512
+ const { result, data, requestId } = await confirmApproval(ctx.api, id, opts.reason);
513
+ const toolName = typeof data.approval?.toolName === "string" ? data.approval.toolName : "approval";
514
+ const extra = { approval: { approvalId: id, toolName, status: "approved", executed: Boolean(data.mcp) } };
515
+ const out = mode(ctx);
516
+ if (out !== "json") process.stderr.write(`${theme.success(theme.symbols.success)} Approved ${toolName} ${theme.dim(id)}
517
+ `);
518
+ const ran = data.mcp ? interpretToolPayload(result, toolName) : null;
519
+ if (ran && !ran.ok) throw ran.error;
520
+ let shown = result;
521
+ const card = artifactFromResult(result);
522
+ if (card && opts.wait) {
523
+ const waited = await waitForToolArtifact(ctx, null, result, { timeoutMs: parseDuration(opts.timeout, 5 * 6e4), json: out === "json" });
524
+ if (waited) {
525
+ extra.artifactWait = { artifactId: waited.artifactId, status: waited.status, timedOut: waited.timedOut };
526
+ shown = mergeWaitedArtifact(result, waited);
527
+ if (waited.timedOut) process.exitCode = ExitCode.Network;
528
+ else if (waited.status === "failed") process.exitCode = ExitCode.Error;
529
+ }
530
+ }
531
+ await renderResult(ctx, { id: toolName, commandPath: [toolName] }, shown, {
532
+ durationMs: Date.now() - started,
533
+ requestId,
534
+ mode: out,
535
+ extra
536
+ });
537
+ const final = artifactFromResult(shown);
538
+ if (final) artifactHints(ctx, out === "json", final.id, final.status);
539
+ }
540
+ async function reject(ctx, id, reason) {
541
+ requireJwtForApprovals(ctx, "Rejecting");
542
+ const started = Date.now();
543
+ const { data, requestId } = await rejectApproval(ctx.api, id, reason);
544
+ const toolName = typeof data.approval?.toolName === "string" ? data.approval.toolName : "approval";
545
+ const out = mode(ctx);
546
+ if (out === "json") {
547
+ writeJson(
548
+ jsonEnvelope({ id: "approvals.reject" }, data.approval ?? null, {
549
+ durationMs: Date.now() - started,
550
+ requestId,
551
+ extra: { approval: { approvalId: id, toolName, status: "rejected", executed: false } }
552
+ })
553
+ );
554
+ return;
555
+ }
556
+ write([`${theme.error(theme.symbols.error)} Rejected ${toolName} ${theme.dim(id)}`]);
557
+ }
558
+ async function wait(ctx, id, timeout) {
559
+ const timeoutMs = parseDuration(timeout);
560
+ const started = Date.now();
561
+ const session = await ctx.mcp();
562
+ const out = mode(ctx);
563
+ if (out !== "json") {
564
+ process.stderr.write(`${theme.dim(`Waiting for ${id} (up to ${formatDuration(timeoutMs)}) \xB7 approve it in the Tabbio app`)}
565
+ `);
566
+ }
567
+ const { record, timedOut } = await pollApprovalStatus(session, id, { timeoutMs });
568
+ const status2 = timedOut ? "pending" : record?.status ?? "pending";
569
+ if (out === "json") {
570
+ writeJson(
571
+ jsonEnvelope({ id: "approvals.wait" }, record, {
572
+ durationMs: Date.now() - started,
573
+ extra: { approval: { approvalId: id, toolName: record?.toolName ?? null, status: status2, timedOut, executed: false } }
574
+ })
575
+ );
576
+ } else {
577
+ for (const line of describeWaitResult(record?.toolName || id, record, timedOut, timeoutMs)) {
578
+ process.stderr.write(`${line}
579
+ `);
580
+ }
581
+ }
582
+ const code = exitCodeForApprovalStatus(status2, timedOut);
583
+ if (code !== ExitCode.Ok) process.exitCode = code;
584
+ }
585
+ function registerApprovalsCommand(program) {
586
+ const approvals = program.command("approvals").description("Review and decide actions waiting for your approval").addHelpText(
587
+ "after",
588
+ "\nExamples:\n $ tabbio approvals\n $ tabbio approvals approve apr_123\n $ tabbio approvals wait apr_123 --timeout 5m"
589
+ );
590
+ approvals.command("list", { isDefault: true }).description("List pending approvals (needs a full sign-in)").action((_opts, cmd) => withContext(cmd, list));
591
+ approvals.command("show").description("Show one approval (works with a personal MCP token)").argument("<id>", "Approval id").action((id, _opts, cmd) => withContext(cmd, (ctx) => show(ctx, id)));
592
+ approvals.command("approve").description("Approve and run it now; prints the tool output").argument("<id>", "Approval id").option("--reason <text>", "Note stored with the decision").option("--wait", "When it created a file (document, page, image), wait until it has rendered").option("--timeout <duration>", "How long --wait waits for the file (default 5m)").action(
593
+ (id, opts, cmd) => withContext(cmd, (ctx) => approve(ctx, id, opts))
594
+ );
595
+ approvals.command("reject").description("Reject it; the action never runs").argument("<id>", "Approval id").option("--reason <text>", "Note stored with the decision").action((id, opts, cmd) => withContext(cmd, (ctx) => reject(ctx, id, opts.reason)));
596
+ approvals.command("wait").description("Wait until it is approved or rejected (exit 0 approved, 1 rejected, 6 still pending)").argument("<id>", "Approval id").option("--timeout <duration>", "Give up after this long (default 10m)").action((id, opts, cmd) => withContext(cmd, (ctx) => wait(ctx, id, opts.timeout)));
597
+ }
598
+
599
+ // src/commands/artifacts.ts
600
+ import { Option } from "commander";
601
+
602
+ // src/commands/artifacts-files.ts
603
+ import open from "open";
604
+ var DEFAULT_WAIT = 5 * 6e4;
605
+ function outMode(ctx) {
606
+ return resolveOutputMode({ json: ctx.globals.json });
607
+ }
608
+ function status(ctx, out, line) {
609
+ if (out !== "json" && !ctx.globals.quiet) process.stderr.write(`${line}
610
+ `);
611
+ }
612
+ function strip(detail) {
613
+ const { document: _document, sources, ...summary } = detail;
614
+ return { ...summary, sourceCount: summary.sourceCount ?? sources?.length ?? 0 };
615
+ }
616
+ function notReady(id) {
617
+ return new CliError({
618
+ code: "ARTIFACT_NOT_READY",
619
+ message: "This file is still being created",
620
+ hint: `Wait for it with \`tabbio artifacts wait ${id}\`, or pass --wait.`,
621
+ exitCode: ExitCode.Error
622
+ });
623
+ }
624
+ async function waitCommand(ctx, id, opts) {
625
+ const out = outMode(ctx);
626
+ const timeoutMs = parseDuration(opts.timeout, DEFAULT_WAIT);
627
+ const started = Date.now();
628
+ status(ctx, out, theme.dim(`${theme.symbols.pending} Waiting for ${id} (up to ${formatDuration(timeoutMs)})`));
629
+ const { artifact, timedOut } = await waitForArtifact(() => getArtifact(ctx.api, id), { timeoutMs });
630
+ const summary = artifact ? strip(artifact) : null;
631
+ if (out === "json") writeJson(jsonEnvelope({ id: "artifacts.wait" }, summary, { durationMs: Date.now() - started, extra: { timedOut } }));
632
+ if (timedOut || !artifact) {
633
+ throw new CliError({
634
+ code: "ARTIFACT_WAIT_TIMEOUT",
635
+ message: `${id} was still rendering after ${formatDuration(timeoutMs)}`,
636
+ hint: `Try again later: tabbio artifacts wait ${id}`,
637
+ exitCode: ExitCode.Network,
638
+ retry: true
639
+ });
640
+ }
641
+ if (artifact.status === "failed") {
642
+ if (out !== "json") process.stdout.write(`${theme.error(theme.symbols.error)} ${artifact.title} failed: ${artifact.error?.message ?? "unknown error"}
643
+ `);
644
+ process.exitCode = ExitCode.Error;
645
+ return;
646
+ }
647
+ if (out === "json") return;
648
+ if (out === "tsv") {
649
+ process.stdout.write(`${artifact.id} ${artifact.status} ${artifact.kind} ${artifact.title}
650
+ `);
651
+ return;
652
+ }
653
+ process.stdout.write(`${theme.success(theme.symbols.success)} ${artifact.title} ${theme.dim(`${theme.symbols.middot} ${artifact.kind} ${theme.symbols.middot} ready`)}
654
+ `);
655
+ status(ctx, out, theme.dim(`${theme.symbols.arrowRight} tabbio artifacts open ${id} ${theme.symbols.middot} tabbio artifacts download ${id}`));
656
+ }
657
+ async function openCommand(ctx, id, opts) {
658
+ const out = outMode(ctx);
659
+ const started = Date.now();
660
+ if (opts.variant && !PREVIEW_VARIANTS.includes(opts.variant)) {
661
+ throw usageError(`--variant must be one of ${PREVIEW_VARIANTS.join(", ")}`);
662
+ }
663
+ if (opts.theme && opts.theme !== "light" && opts.theme !== "dark") throw usageError("--theme must be light or dark");
664
+ let variant = opts.variant;
665
+ if (!variant) variant = previewVariantFor((await getArtifact(ctx.api, id)).kind);
666
+ const signed = await previewUrl(ctx.api, id, { variant, theme: opts.theme, versionId: opts.version });
667
+ if (out === "json") {
668
+ writeJson(jsonEnvelope({ id: "artifacts.open" }, signed, { durationMs: Date.now() - started }));
669
+ return;
670
+ }
671
+ process.stdout.write(`${signed.url}
672
+ `);
673
+ const browse = !opts.print && process.stdout.isTTY && !process.env.CI;
674
+ if (browse) {
675
+ await open(signed.url).catch(() => void 0);
676
+ const label2 = supportsHyperlinks() ? hyperlink("Opened the preview", signed.url) : "Opened the preview";
677
+ status(ctx, out, theme.dim(`${label2} ${theme.symbols.middot} the link works for 15 minutes`));
678
+ } else {
679
+ status(ctx, out, theme.dim("The link works for 15 minutes."));
680
+ }
681
+ }
682
+ function humanBytes(bytes) {
683
+ if (bytes < 1024) return `${bytes} B`;
684
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
685
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
686
+ }
687
+ async function mintDownload(ctx, id, variant, opts) {
688
+ const deadline = Date.now() + opts.timeoutMs;
689
+ for (; ; ) {
690
+ try {
691
+ return await downloadUrl(ctx.api, id, { variant, versionId: opts.version });
692
+ } catch (error) {
693
+ const preparing = isCliError(error) && error.code === "ARTIFACT_NOT_READY";
694
+ if (!preparing || !opts.wait || Date.now() > deadline) throw preparing ? notReady(id) : error;
695
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
696
+ }
697
+ }
698
+ }
699
+ async function downloadCommand(ctx, id, opts) {
700
+ const out = outMode(ctx);
701
+ const started = Date.now();
702
+ const timeoutMs = parseDuration(opts.timeout, DEFAULT_WAIT);
703
+ let artifact = await getArtifact(ctx.api, id);
704
+ if (artifact.status === "pending" && !opts.version) {
705
+ if (!opts.wait) throw notReady(id);
706
+ status(ctx, out, theme.dim(`${theme.symbols.pending} Waiting for ${id} to finish rendering`));
707
+ const waited = await waitForArtifact(() => getArtifact(ctx.api, id), { timeoutMs });
708
+ if (!waited.artifact || waited.timedOut) throw notReady(id);
709
+ artifact = waited.artifact;
710
+ }
711
+ if (artifact.status === "failed" && !opts.version) {
712
+ throw new CliError({ code: "ARTIFACT_FAILED", message: `${artifact.title} failed: ${artifact.error?.message ?? "no file was made"}`, exitCode: ExitCode.Error });
713
+ }
714
+ const variant = opts.format ? formatToVariant(opts.format) : defaultVariantFor(artifact);
715
+ const signed = await mintDownload(ctx, id, variant, { version: opts.version, wait: Boolean(opts.wait), timeoutMs });
716
+ const fileName = safeFileName(artifact.title, extensionFor(variant, signed.contentType));
717
+ const dest = resolveOutputPath(opts.out, fileName);
718
+ const tty = out !== "json" && process.stderr.isTTY && !ctx.globals.quiet && !process.env.CI;
719
+ let lastPaint = 0;
720
+ const saved = await downloadToFile(signed.url, dest, {
721
+ force: opts.force,
722
+ signal: activeCommandSignal,
723
+ onProgress: ({ received, total }) => {
724
+ if (!tty || Date.now() - lastPaint < 100) return;
725
+ lastPaint = Date.now();
726
+ const of = total ? ` / ${humanBytes(total)}` : "";
727
+ process.stderr.write(`\r\x1B[2K${theme.dim(`${theme.symbols.pending} ${fileName} ${humanBytes(received)}${of}`)}`);
728
+ }
729
+ });
730
+ if (tty) process.stderr.write("\r\x1B[2K");
731
+ const data = { artifactId: id, path: saved.path, bytes: saved.bytes, variant, contentType: saved.contentType ?? signed.contentType };
732
+ if (out === "json") {
733
+ writeJson(jsonEnvelope({ id: "artifacts.download" }, data, { durationMs: Date.now() - started }));
734
+ return;
735
+ }
736
+ status(ctx, out, `${theme.success(theme.symbols.success)} Saved ${variant.toUpperCase()} ${theme.dim(`${humanBytes(saved.bytes)} ${theme.symbols.middot} ${formatDuration(Date.now() - started)}`)}`);
737
+ process.stdout.write(`${saved.path}
738
+ `);
739
+ }
740
+
741
+ // src/commands/artifacts.ts
742
+ var LIST_FIELDS2 = ["id", "kind", "title", "status", "version", "createdAt"];
743
+ function outMode2(ctx) {
744
+ return resolveOutputMode({ json: ctx.globals.json });
745
+ }
746
+ function write2(lines) {
747
+ for (const line of lines) process.stdout.write(`${line}
748
+ `);
749
+ }
750
+ async function withSession(cmd, fn) {
751
+ const ctx = createCommandContext(cmd);
752
+ try {
753
+ assertJwtSession(ctx.credentials, ctx.profile, "Artifacts");
754
+ await fn(ctx);
755
+ } finally {
756
+ await ctx.close();
757
+ }
758
+ }
759
+ function row(a) {
760
+ return {
761
+ id: a.id,
762
+ kind: a.kind,
763
+ title: a.title,
764
+ status: a.status,
765
+ version: a.currentVersion?.number ?? a.versionCount ?? null,
766
+ createdAt: a.createdAt,
767
+ ...a.site?.url ? { site: a.site.url } : {}
768
+ };
769
+ }
770
+ function parseLimit(value) {
771
+ if (value === void 0) return void 0;
772
+ const n = Number(value);
773
+ if (!Number.isInteger(n) || n < 1 || n > 50) throw usageError("--limit must be a whole number from 1 to 50");
774
+ return n;
775
+ }
776
+ async function list2(ctx, opts) {
777
+ const started = Date.now();
778
+ const page = await listArtifacts(ctx.api, { kind: opts.kind, status: opts.status, threadId: opts.thread, limit: parseLimit(opts.limit), cursor: opts.cursor });
779
+ const out = outMode2(ctx);
780
+ if (out === "json") {
781
+ writeJson(jsonEnvelope({ id: "artifacts.list" }, page.artifacts, { durationMs: Date.now() - started, extra: { nextCursor: page.nextCursor } }));
782
+ return;
783
+ }
784
+ if (page.artifacts.length === 0) return printEmptyNotice(out, "No files yet. Ask Tabbio for a document, a page or an image.", { quiet: ctx.globals.quiet });
785
+ const rows = page.artifacts.map(row);
786
+ if (out === "tsv") {
787
+ process.stdout.write(toTsv(rows, LIST_FIELDS2));
788
+ return;
789
+ }
790
+ write2(formatTable(rows, { fields: LIST_FIELDS2, width: process.stdout.columns }));
791
+ const more = page.nextCursor ? ` ${theme.symbols.middot} more: --cursor ${page.nextCursor}` : "";
792
+ write2([theme.dim(`${rows.length} ${rows.length === 1 ? "file" : "files"}${more} ${theme.symbols.middot} tabbio artifacts open <id> ${theme.symbols.middot} tabbio artifacts download <id>`)]);
793
+ }
794
+ async function get(ctx, id) {
795
+ const started = Date.now();
796
+ const detail = await getArtifact(ctx.api, id);
797
+ const out = outMode2(ctx);
798
+ if (out === "json") {
799
+ writeJson(jsonEnvelope({ id: "artifacts.get" }, detail, { durationMs: Date.now() - started }));
800
+ return;
801
+ }
802
+ const { document: _document, sources, variants, ...summary } = detail;
803
+ const panel = {
804
+ ...summary,
805
+ variants: Object.entries(variants ?? {}).filter(([, on]) => on).map(([name]) => name).join(", "),
806
+ currentVersion: detail.currentVersion ? `v${detail.currentVersion.number} (${detail.currentVersion.id})` : null,
807
+ site: detail.site?.url ?? null,
808
+ error: detail.error ? `${detail.error.code}: ${detail.error.message}` : null,
809
+ sourceCount: sources?.length ?? detail.sourceCount
810
+ };
811
+ if (out === "tsv") {
812
+ process.stdout.write(toTsv(panel));
813
+ return;
814
+ }
815
+ write2(formatKv(panel, { width: process.stdout.columns }));
816
+ if (detail.status === "ready") write2([theme.dim(`${theme.symbols.arrowRight} tabbio artifacts open ${id} ${theme.symbols.middot} tabbio artifacts download ${id}`)]);
817
+ }
818
+ async function versions(ctx, id) {
819
+ const started = Date.now();
820
+ const page = await listVersions(ctx.api, id, { limit: 50 });
821
+ const out = outMode2(ctx);
822
+ if (out === "json") {
823
+ writeJson(jsonEnvelope({ id: "artifacts.versions" }, page.versions, { durationMs: Date.now() - started, extra: { nextCursor: page.nextCursor } }));
824
+ return;
825
+ }
826
+ const rows = page.versions.map((v) => ({
827
+ number: v.number,
828
+ id: v.id,
829
+ status: v.status,
830
+ source: v.restoredFrom ? `restore of v${v.restoredFrom.number}` : v.source,
831
+ current: out === "table" ? v.isCurrent ? theme.symbols.success : "" : v.isCurrent,
832
+ summary: v.summary ?? "",
833
+ createdAt: v.createdAt
834
+ }));
835
+ if (rows.length === 0) return printEmptyNotice(out, "No versions yet", { quiet: ctx.globals.quiet });
836
+ if (out === "tsv") {
837
+ process.stdout.write(toTsv(rows, ["number", "id", "status", "source", "current", "createdAt"]));
838
+ return;
839
+ }
840
+ write2(formatTable(rows, { fields: ["number", "id", "status", "source", "current", "summary", "createdAt"], width: process.stdout.columns }));
841
+ write2([theme.dim(`tabbio artifacts restore ${id} <versionId> makes a copy of that version the newest one`)]);
842
+ }
843
+ async function restore(ctx, id, versionId) {
844
+ const started = Date.now();
845
+ const result = await restoreVersion(ctx.api, id, versionId);
846
+ const out = outMode2(ctx);
847
+ if (out === "json") {
848
+ writeJson(jsonEnvelope({ id: "artifacts.restore" }, result, { durationMs: Date.now() - started }));
849
+ return;
850
+ }
851
+ const from = result.version.restoredFrom ? `version ${result.version.restoredFrom.number}` : versionId;
852
+ write2([`${theme.success(theme.symbols.success)} Restored ${from} as version ${result.version.number} ${theme.dim(`${theme.symbols.middot} ${result.artifact.title} ${theme.symbols.middot} ${result.version.status}`)}`]);
853
+ }
854
+ async function remove(ctx, id) {
855
+ const started = Date.now();
856
+ const out = outMode2(ctx);
857
+ if (!ctx.globals.yes) {
858
+ const detail = await getArtifact(ctx.api, id);
859
+ const site = detail.site?.url ? ` Its site ${detail.site.url} goes offline too.` : "";
860
+ process.stderr.write(`${theme.warn(theme.symbols.warning)} Delete ${detail.kind} "${detail.title}"?${site} It is removed for good after 30 days.
861
+ `);
862
+ if (!await confirm("Delete it?")) {
863
+ process.stderr.write(`${theme.dim("Kept it.")}
864
+ `);
865
+ return;
866
+ }
867
+ }
868
+ const result = await deleteArtifact(ctx.api, id);
869
+ if (out === "json") writeJson(jsonEnvelope({ id: "artifacts.delete" }, result, { durationMs: Date.now() - started }));
870
+ else write2([`${theme.success(theme.symbols.success)} Deleted ${id}`]);
871
+ }
872
+ function registerArtifactsCommand(program) {
873
+ const artifacts = program.command("artifacts").description("Documents, slide decks, pages and images Tabbio made for you (list, open, download, versions)").addHelpText(
874
+ "after",
875
+ [
876
+ "",
877
+ "Examples:",
878
+ " $ tabbio artifacts list --kind document",
879
+ " $ tabbio artifacts open art_123",
880
+ " $ tabbio artifacts download art_123 --format pdf --out ~/Downloads",
881
+ " $ tabbio artifacts wait art_123 --timeout 2m",
882
+ "",
883
+ "Needs an app session (tabbio login). With a personal token use `tabbio artifact list|get|read`."
884
+ ].join("\n")
885
+ );
886
+ artifacts.command("list", { isDefault: true }).description("Your files, newest first").addOption(new Option("--kind <kind>", "Only this kind").choices([...ARTIFACT_KINDS])).addOption(new Option("--status <status>", "Only this status").choices(["pending", "ready", "failed"])).option("--thread <id>", "Only files from this chat thread").option("--limit <n>", "How many (1-50, default 20)").option("--cursor <id>", "Next page (from the previous listing)").action(
887
+ (opts, cmd) => withSession(cmd, (ctx) => list2(ctx, opts))
888
+ );
889
+ artifacts.command("get").description("One file: kind, status, versions, formats, site").argument("<id>", "Artifact id").action((id, _opts, cmd) => withSession(cmd, (ctx) => get(ctx, id)));
890
+ artifacts.command("open").description("Print a 15-minute preview link and open it in the browser (on a terminal)").argument("<id>", "Artifact id").addOption(new Option("--variant <variant>", "What to preview (default: image for images, else html)").choices(["html", "pdf", "image", "thumbnail"])).addOption(new Option("--theme <theme>", "HTML preview theme").choices(["light", "dark"])).option("--version <versionId>", "A version other than the newest").option("--print", "Only print the link, never open a browser").action(
891
+ (id, opts, cmd) => withSession(cmd, (ctx) => openCommand(ctx, id, opts))
892
+ );
893
+ artifacts.command("download").description("Save a file to disk (default name from its title; never overwrites without --force)").argument("<id>", "Artifact id").option("--format <format>", "pdf | pptx | html | png (image formats save the stored image). Default: pdf for documents, pptx for slides, html for pages, the image for images").option("-O, --out <path>", "File or directory to save to (default: the current directory)").option("--force", "Overwrite an existing file").option("--version <versionId>", "A version other than the newest").option("--wait", "If it is still rendering, wait for it").option("--timeout <duration>", "How long --wait waits (default 5m)").action(
894
+ (id, opts, cmd) => withSession(cmd, (ctx) => downloadCommand(ctx, id, opts))
895
+ );
896
+ artifacts.command("versions").description("Every version of a file, newest first").argument("<id>", "Artifact id").action((id, _opts, cmd) => withSession(cmd, (ctx) => versions(ctx, id)));
897
+ artifacts.command("restore").description("Make a copy of an older version the newest one (nothing is lost)").argument("<id>", "Artifact id").argument("<versionId>", "Version id from `tabbio artifacts versions <id>`").action((id, versionId, _opts, cmd) => withSession(cmd, (ctx) => restore(ctx, id, versionId)));
898
+ artifacts.command("delete").description("Delete a file (asks first unless --yes; its published site goes offline)").argument("<id>", "Artifact id").action((id, _opts, cmd) => withSession(cmd, (ctx) => remove(ctx, id)));
899
+ artifacts.command("wait").description("Wait until a file has rendered (exit 0 ready, 1 failed, 7 still rendering at the timeout)").argument("<id>", "Artifact id").option("--timeout <duration>", "Give up after this long (default 5m)").action((id, opts, cmd) => withSession(cmd, (ctx) => waitCommand(ctx, id, opts)));
900
+ }
901
+
902
+ // src/core/run-tool.ts
903
+ import { readFileSync } from "node:fs";
904
+ import { Command, CommanderError, Option as Option2 } from "commander";
905
+ function addRunLevelOptions(cmd) {
906
+ const options = [
907
+ new Option2("--input <json>", 'Tool input as a JSON object; "-" reads stdin (flags override its keys)'),
908
+ new Option2("--input-file <path>", 'Read the tool input JSON from a file ("-" for stdin)'),
909
+ new Option2("--fields <list>", "Only these fields/columns, comma-separated (dot paths ok)"),
910
+ new Option2("--wait", "Wait for an approval instead of exiting 6, and for a created file to finish rendering"),
911
+ new Option2("--timeout <duration>", "How long --wait waits (default 10m)"),
912
+ new Option2("-o, --output <mode>", "json | table | plain (default: table on a TTY, plain TSV when piped)")
913
+ ];
914
+ for (const option of options) cmd.addOption(option);
915
+ return cmd;
916
+ }
917
+ function addToolOptions(cmd, tool) {
918
+ for (const option of schemaToOptions(tool.inputSchema)) cmd.addOption(option);
919
+ return cmd;
920
+ }
921
+ function validFlagsFor(tool) {
922
+ return [
923
+ ...schemaToFlags(tool.inputSchema).map((spec) => `--${spec.flag}`),
924
+ "--input",
925
+ "--input-file",
926
+ "--fields",
927
+ "--wait",
928
+ "--timeout",
929
+ "--output"
930
+ ];
931
+ }
932
+ function unknownArgsError(tool, args, commandLabel) {
933
+ const flag = args.find((arg) => arg.startsWith("-"));
934
+ const valid = validFlagsFor(tool);
935
+ if (!flag) {
936
+ return usageError(
937
+ `Unexpected argument "${args[0]}" for ${commandLabel}`,
938
+ `Tool input goes in flags: ${valid.slice(0, 8).join(" ")}${valid.length > 8 ? " \u2026" : ""}`
939
+ );
940
+ }
941
+ const name = flag.split("=")[0];
942
+ const guess = didYouMean(name, [...valid, ...RESERVED_GLOBAL_FLAGS.map((f) => `--${f}`)]);
943
+ const toolFlags = schemaToFlags(tool.inputSchema).map((spec) => `--${spec.flag}`);
944
+ return usageError(
945
+ `Unknown flag ${name} for ${commandLabel}`,
946
+ [
947
+ guess ? `Did you mean ${guess}?` : null,
948
+ toolFlags.length ? `Valid flags: ${toolFlags.join(", ")}.` : "This tool takes no flags.",
949
+ `See \`tabbio tools describe ${tool.id}\`.`
950
+ ].filter(Boolean).join(" ")
951
+ );
952
+ }
953
+ function parseDynamicFlags(tool, args) {
954
+ const cmd = addToolOptions(new Command(tool.id), tool).exitOverride().configureOutput({ writeOut: () => void 0, writeErr: () => void 0, outputError: () => void 0 });
955
+ try {
956
+ const parsed = cmd.parseOptions([...args]);
957
+ return { opts: cmd.opts(), rest: [...parsed.operands, ...parsed.unknown] };
958
+ } catch (error) {
959
+ if (error instanceof CommanderError) throw usageError(error.message.replace(/^error:\s*/i, ""));
960
+ throw error;
961
+ }
962
+ }
963
+ async function readStdin(stream = process.stdin) {
964
+ if (stream.isTTY) {
965
+ throw usageError('"-" reads the input from stdin, but stdin is a terminal', "Pipe JSON in, e.g. `echo '{\"limit\":5}' | tabbio run cv.list --input -`.");
966
+ }
967
+ const chunks = [];
968
+ for await (const chunk of stream) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
969
+ return Buffer.concat(chunks).toString("utf8");
970
+ }
971
+ function parseInputObject(text, source) {
972
+ let value;
973
+ try {
974
+ value = JSON.parse(text);
975
+ } catch (error) {
976
+ throw usageError(`${source} is not valid JSON: ${error.message}`);
977
+ }
978
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
979
+ throw usageError(`${source} must be a JSON object`, `Example: --input '{"limit":5}'`);
980
+ }
981
+ return value;
982
+ }
983
+ async function loadInputBase(opts, stdin = () => readStdin()) {
984
+ if (opts.input === "-" && opts.inputFile === "-") throw usageError("Only one of --input and --input-file can read stdin");
985
+ let base;
986
+ if (opts.inputFile !== void 0) {
987
+ let text;
988
+ if (opts.inputFile === "-") text = await stdin();
989
+ else {
990
+ try {
991
+ text = readFileSync(opts.inputFile, "utf8");
992
+ } catch (error) {
993
+ throw usageError(`Cannot read --input-file ${opts.inputFile}: ${error.code ?? error.message}`);
994
+ }
995
+ }
996
+ base = parseInputObject(text, "--input-file");
997
+ }
998
+ if (opts.input !== void 0) {
999
+ const text = opts.input === "-" ? await stdin() : opts.input;
1000
+ base = { ...base, ...parseInputObject(text, "--input") };
1001
+ }
1002
+ return base;
1003
+ }
1004
+ function toolInputDefaults(ctx, tool) {
1005
+ const userId = ctx.credentials?.user?.id;
1006
+ return autoFilledProperties(tool, tool.inputSchema).includes("userId") && userId ? { userId } : {};
1007
+ }
1008
+ async function resolveCatalogTool(session, ref, filter = () => true) {
1009
+ const find = (tools2) => findTool(tools2.filter(filter), ref);
1010
+ let tools = await session.listTools();
1011
+ let tool = find(tools);
1012
+ if (!tool) {
1013
+ tools = await session.listTools({ refresh: true });
1014
+ tool = find(tools);
1015
+ }
1016
+ if (tool) return tool;
1017
+ const candidates = tools.filter(filter).flatMap((t) => [t.id, t.commandPath.join(" ")]);
1018
+ const guess = didYouMean(ref, candidates);
1019
+ throw new CliError({
1020
+ code: "UNKNOWN_TOOL",
1021
+ message: `Unknown tool: ${ref}`,
1022
+ hint: guess ? `Did you mean ${guess}? List tools with \`tabbio tools\`.` : "List tools with `tabbio tools` (or `tabbio tools --refresh`).",
1023
+ exitCode: ExitCode.NotFound
1024
+ });
1025
+ }
1026
+ function startProgress(ctx, mode2, text) {
1027
+ if (mode2 !== "table" || ctx.globals.quiet || !process.stderr.isTTY || process.env.CI) return () => void 0;
1028
+ process.stderr.write(`${theme.dim(`${theme.symbols.pending} ${text}`)}`);
1029
+ return () => process.stderr.write("\r\x1B[2K");
1030
+ }
1031
+ function outputModeFor(ctx, output) {
1032
+ const mode2 = resolveOutputMode({ json: ctx.globals.json, output });
1033
+ if (mode2 === "json" && !ctx.globals.json) {
1034
+ ctx.globals.json = true;
1035
+ setGlobalOptions({ json: true });
1036
+ }
1037
+ return mode2;
1038
+ }
1039
+ async function runTool(ctx, tool, opts, extraArgs = []) {
1040
+ const commandLabel = tool.kind === "tool" ? tool.commandPath.join(" ") : tool.id;
1041
+ if (extraArgs.length) throw unknownArgsError(tool, extraArgs, commandLabel);
1042
+ const mode2 = outputModeFor(ctx, opts.output);
1043
+ const fields = parseFields(opts.fields);
1044
+ const timeoutMs = parseDuration(opts.timeout);
1045
+ const base = await loadInputBase(opts);
1046
+ const parsed = parseToolInput(tool.inputSchema, opts, { base, defaults: toolInputDefaults(ctx, tool) });
1047
+ const { missingRequired } = parsed;
1048
+ let input = parsed.input;
1049
+ if (missingRequired.length) {
1050
+ const hooks = getInteractiveHooks();
1051
+ if (hooks && mode2 === "table" && canUseInteractiveUi(ctx)) {
1052
+ const filled = await hooks.runToolForm(ctx, tool, input, { missingRequired });
1053
+ if (!filled) throw interruptedError("Cancelled");
1054
+ input = filled;
1055
+ } else {
1056
+ const flags = flagsForProperties(tool.inputSchema, missingRequired);
1057
+ throw usageError(
1058
+ `Missing required ${flags.length === 1 ? "flag" : "flags"} for ${commandLabel}: ${flags.join(", ")}`,
1059
+ `Pass ${flags.length === 1 ? "it as a flag" : "them as flags"} or in --input '{\u2026}'. See \`tabbio tools describe ${tool.id}\`.`
1060
+ );
1061
+ }
1062
+ }
1063
+ const session = await ctx.mcp();
1064
+ const started = Date.now();
1065
+ const clear = startProgress(ctx, mode2, `Running ${commandLabel}${theme.symbols.ellipsis}`);
1066
+ let response;
1067
+ try {
1068
+ response = await session.callTool(tool.id, input);
1069
+ } finally {
1070
+ clear();
1071
+ }
1072
+ if (!response.ok) throw response.error;
1073
+ if (response.approval) {
1074
+ const outcome = await handleApproval(ctx, session, tool, response.approval, input, {
1075
+ wait: Boolean(opts.wait),
1076
+ timeoutMs,
1077
+ mode: mode2,
1078
+ followsOutput: isArtifactProducingTool(tool.id)
1079
+ });
1080
+ const extra2 = { approval: approvalMeta(outcome) };
1081
+ if (outcome.executed) {
1082
+ const ran = interpretToolPayload(outcome.result, tool.id);
1083
+ if (!ran.ok) throw ran.error;
1084
+ const result2 = await settleArtifact(ctx, session, tool, outcome.result, { wait: opts.wait, timeoutMs, mode: mode2, extra: extra2 });
1085
+ await renderResult(ctx, tool, result2, {
1086
+ durationMs: Date.now() - started,
1087
+ requestId: outcome.requestId,
1088
+ mode: mode2,
1089
+ fields,
1090
+ extra: extra2
1091
+ });
1092
+ afterArtifact(ctx, tool, result2, mode2);
1093
+ } else if (opts.wait && outcome.status === "approved" && isArtifactProducingTool(tool.id)) {
1094
+ const artifactId = await findArtifactForApproval(ctx, input, outcome.record?.createdAt).catch(() => null);
1095
+ const result2 = artifactId ? await settleArtifact(ctx, session, tool, { artifactId, status: "pending", kind: "file", title: String(input.title ?? "") }, { wait: true, timeoutMs, mode: mode2, extra: extra2 }) : null;
1096
+ if (result2 || mode2 === "json") {
1097
+ await renderResult(ctx, tool, result2, { durationMs: Date.now() - started, mode: mode2, fields, extra: extra2 });
1098
+ }
1099
+ if (result2) afterArtifact(ctx, tool, result2, mode2);
1100
+ } else if (mode2 === "json") {
1101
+ writeJson(jsonEnvelope(tool, null, { durationMs: Date.now() - started, extra: extra2 }));
1102
+ }
1103
+ return;
1104
+ }
1105
+ const extra = {};
1106
+ const result = await settleArtifact(ctx, session, tool, response.result, { wait: opts.wait, timeoutMs, mode: mode2, extra });
1107
+ await renderResult(ctx, tool, result, { durationMs: Date.now() - started, mode: mode2, fields, extra });
1108
+ afterArtifact(ctx, tool, result, mode2);
1109
+ }
1110
+ async function settleArtifact(ctx, session, tool, result, opts) {
1111
+ if (!opts.wait || !isArtifactProducingTool(tool.id) || !artifactFromResult(result)) return result;
1112
+ const outcome = await waitForToolArtifact(ctx, session, result, { timeoutMs: opts.timeoutMs, json: opts.mode === "json" });
1113
+ if (!outcome) return result;
1114
+ opts.extra.artifactWait = { artifactId: outcome.artifactId, status: outcome.status, timedOut: outcome.timedOut };
1115
+ if (outcome.timedOut) process.exitCode = ExitCode.Network;
1116
+ else if (outcome.status === "failed") process.exitCode = ExitCode.Error;
1117
+ return mergeWaitedArtifact(result, outcome);
1118
+ }
1119
+ function afterArtifact(ctx, tool, result, mode2) {
1120
+ const card = artifactFromResult(result);
1121
+ if (!card || !tool.id.startsWith("artifact.")) return;
1122
+ artifactHints(ctx, mode2 === "json", card.id, card.status);
1123
+ }
1124
+ function wantsHelp(args) {
1125
+ return args.includes("--help") || args.includes("-h");
1126
+ }
1127
+
1128
+ // src/commands/ask.ts
1129
+ function askInputField(schema) {
1130
+ const properties = schema?.properties ?? {};
1131
+ const isString = (key) => {
1132
+ const type = properties[key]?.type;
1133
+ return type === "string" || Array.isArray(type) && type.includes("string");
1134
+ };
1135
+ const required = (schema?.required ?? []).find((key) => key in properties && isString(key));
1136
+ return required ?? Object.keys(properties).find(isString) ?? "message";
1137
+ }
1138
+ function findAskTool(tools) {
1139
+ return tools.find((t) => t.kind === "agent" && t.key === PRIMARY_AGENT_KEY) ?? findTool(tools, `ask_${PRIMARY_AGENT_KEY}`);
1140
+ }
1141
+ function answerText(result) {
1142
+ if (typeof result === "string") return result;
1143
+ if (result && typeof result === "object") {
1144
+ const record = result;
1145
+ if (typeof record.text === "string") return record.text;
1146
+ if (typeof record.answer === "string") return record.answer;
1147
+ }
1148
+ return result === null || result === void 0 ? "" : JSON.stringify(result, null, 2);
1149
+ }
1150
+ function answerData(result) {
1151
+ const finishReason = result && typeof result === "object" && typeof result.finishReason === "string" ? result.finishReason : void 0;
1152
+ return { text: answerText(result), ...finishReason ? { finishReason } : {} };
1153
+ }
1154
+ async function ask(ctx, words, opts = {}) {
1155
+ const mode2 = outputModeFor(ctx);
1156
+ let question = words.join(" ").trim();
1157
+ if (!question && !process.stdin.isTTY) question = (await readStdin()).trim();
1158
+ if (!question) throw usageError("Ask a question", 'e.g. tabbio ask "Which of my CVs fits product roles best?"');
1159
+ if (!opts.viaMcp && describeMcpBearer(ctx.credentials)?.kind === "app-session") {
1160
+ await askViaChat(ctx, question, mode2, opts.mode ?? "seeker");
1161
+ return;
1162
+ }
1163
+ const session = await ctx.mcp();
1164
+ const tool = findAskTool(await session.listTools());
1165
+ if (!tool) {
1166
+ throw new CliError({
1167
+ code: "UNKNOWN_TOOL",
1168
+ message: "This Tabbio server does not offer the ask_tabbio tool",
1169
+ hint: "Try `tabbio chat`, or `tabbio tools --refresh`.",
1170
+ exitCode: ExitCode.NotFound
1171
+ });
1172
+ }
1173
+ const input = { [askInputField(tool.inputSchema)]: question };
1174
+ const started = Date.now();
1175
+ const clear = startProgress(ctx, mode2, "Asking Tabbio\u2026");
1176
+ let response;
1177
+ try {
1178
+ response = await session.callTool(tool.id, input);
1179
+ } finally {
1180
+ clear();
1181
+ }
1182
+ if (!response.ok) throw response.error;
1183
+ let result = response.result;
1184
+ let extra;
1185
+ if (response.approval) {
1186
+ const outcome = await handleApproval(ctx, session, tool, response.approval, input, { mode: mode2 });
1187
+ extra = { approval: approvalMeta(outcome) };
1188
+ if (!outcome.executed) {
1189
+ if (mode2 === "json") writeJson(jsonEnvelope(tool, null, { durationMs: Date.now() - started, extra }));
1190
+ return;
1191
+ }
1192
+ result = outcome.result;
1193
+ }
1194
+ if (mode2 === "json") {
1195
+ writeJson(jsonEnvelope(tool, answerData(result), { durationMs: Date.now() - started, extra }));
1196
+ return;
1197
+ }
1198
+ const text = answerText(result).trimEnd();
1199
+ process.stdout.write(text ? `${text}
1200
+ ` : "");
1201
+ }
1202
+ async function askViaChat(ctx, question, mode2, agentMode) {
1203
+ const started = Date.now();
1204
+ const controller = new AbortController();
1205
+ const onSigint = () => controller.abort();
1206
+ process.on("SIGINT", onSigint);
1207
+ const clear = mode2 === "json" ? () => void 0 : startProgress(ctx, mode2, "Asking Tabbio\u2026");
1208
+ let cleared = false;
1209
+ let text = "";
1210
+ let finishReason;
1211
+ let threadId;
1212
+ let approval;
1213
+ let errorMessage;
1214
+ let sources = [];
1215
+ const artifacts = /* @__PURE__ */ new Map();
1216
+ const notices = [];
1217
+ try {
1218
+ const capabilities = capabilityRequestFields(agentMode, loadCapabilityPrefs(ctx.profile.name));
1219
+ const events = streamChat(ctx, { mode: agentMode, message: question, ...capabilities }, { signal: controller.signal });
1220
+ for await (const event of events) {
1221
+ const payload = event.payload ?? {};
1222
+ switch (event.type) {
1223
+ case "start":
1224
+ if (typeof payload.threadId === "string") threadId = payload.threadId;
1225
+ break;
1226
+ case "text-delta": {
1227
+ const delta = typeof payload.text === "string" ? payload.text : "";
1228
+ if (!delta) break;
1229
+ if (!cleared) {
1230
+ clear();
1231
+ cleared = true;
1232
+ }
1233
+ text += delta;
1234
+ if (mode2 !== "json") process.stdout.write(delta);
1235
+ break;
1236
+ }
1237
+ case "tool-call-approval":
1238
+ approval = { approvalId: String(payload.approvalId ?? ""), name: String(payload.name ?? "A tool") };
1239
+ break;
1240
+ case "tool-result": {
1241
+ sources = mergeSources(sources, sourcesFromResult(payload.result));
1242
+ const artifact = artifactFromResult(payload.result);
1243
+ if (artifact) artifacts.set(artifact.id, artifact);
1244
+ const denial = capabilityDenial(payload.result);
1245
+ if (denial) notices.push(`${denial.message}${denial.reason ? ` (${denial.reason})` : ""}`);
1246
+ break;
1247
+ }
1248
+ case "error":
1249
+ errorMessage = typeof payload.message === "string" ? payload.message : "The agent reported an error";
1250
+ break;
1251
+ case "finish":
1252
+ if (typeof payload.finishReason === "string") finishReason = payload.finishReason;
1253
+ if (typeof payload.threadId === "string") threadId = payload.threadId;
1254
+ if (!text.trim() && typeof payload.text === "string") text = payload.text;
1255
+ break;
1256
+ default:
1257
+ break;
1258
+ }
1259
+ }
1260
+ } finally {
1261
+ if (!cleared) clear();
1262
+ process.off("SIGINT", onSigint);
1263
+ }
1264
+ if (errorMessage && !text.trim()) {
1265
+ throw new CliError({ code: "AGENT_ERROR", message: errorMessage, exitCode: ExitCode.Server });
1266
+ }
1267
+ const durationMs = Date.now() - started;
1268
+ const files = [...artifacts.values()].map(({ id, kind, title, status: status2 }) => ({ id, kind, title, status: status2 }));
1269
+ if (mode2 === "json") {
1270
+ writeJson({
1271
+ data: {
1272
+ text: text.trimEnd(),
1273
+ ...finishReason ? { finishReason } : {},
1274
+ ...threadId ? { threadId } : {},
1275
+ ...sources.length ? { sources } : {},
1276
+ ...files.length ? { artifacts: files } : {}
1277
+ },
1278
+ meta: { schemaVersion: 1, tool: "chat", durationMs, ...approval ? { approval } : {} }
1279
+ });
1280
+ } else {
1281
+ if (text && !text.endsWith("\n")) process.stdout.write("\n");
1282
+ printAskExtras({ sources, files, notices });
1283
+ if (approval) {
1284
+ process.stderr.write(
1285
+ `${theme.warn(`${theme.symbols.pending} ${approval.name} is waiting for your approval`)} ${theme.dim(approval.approvalId)}
1286
+ ${theme.dim("Run:")} tabbio approvals approve ${approval.approvalId}
1287
+ `
1288
+ );
1289
+ }
1290
+ }
1291
+ if (approval) process.exitCode = ExitCode.ApprovalPending;
1292
+ }
1293
+ function printAskExtras(extras) {
1294
+ for (const note of extras.notices) process.stderr.write(`${theme.dim(`${theme.symbols.arrowRight} ${note}`)}
1295
+ `);
1296
+ if (extras.sources.length) {
1297
+ process.stdout.write(`
1298
+ ${theme.dim("Sources")}
1299
+ `);
1300
+ extras.sources.forEach((source, i) => process.stdout.write(`${i + 1}. ${source.title} ${theme.dim(source.url)}
1301
+ `));
1302
+ }
1303
+ for (const file of extras.files) {
1304
+ process.stdout.write(`${theme.dim(`${file.kind} ${file.status}:`)} ${file.title} ${theme.dim(`tabbio artifacts open ${file.id}`)}
1305
+ `);
1306
+ }
1307
+ }
1308
+ function registerAskCommand(program) {
1309
+ program.command("ask").description("Ask Tabbio one question (streams through your session; MCP ask tool with a personal token)").argument("[question...]", "Your question; read from stdin when omitted and piped").option("--mode <mode>", "seeker or employer (app session only)", "seeker").option("--via-mcp", "Force the MCP ask_tabbio tool even with an app session").addHelpText(
1310
+ "after",
1311
+ '\nExamples:\n $ tabbio ask "What jobs fit my CV?"\n $ echo "Summarise my applications" | tabbio ask\n $ tabbio ask "Draft a headline" --json'
1312
+ ).action(async (words, opts, cmd) => {
1313
+ const ctx = createCommandContext(cmd);
1314
+ const agentMode = opts.mode === "employer" ? "employer" : "seeker";
1315
+ try {
1316
+ await ask(ctx, words, { mode: agentMode, viaMcp: Boolean(opts.viaMcp) });
1317
+ } finally {
1318
+ await ctx.close();
1319
+ }
1320
+ });
1321
+ }
1322
+
1323
+ // src/commands/chat.ts
1324
+ import { Option as Option3 } from "commander";
1325
+ async function readStdin2() {
1326
+ if (process.stdin.isTTY) return void 0;
1327
+ const chunks = [];
1328
+ for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
1329
+ return Buffer.concat(chunks).toString("utf8");
1330
+ }
1331
+ function isFailureEvent(event) {
1332
+ return event.type === "error" || event.type === "finish" && event.payload.finishReason === "error";
1333
+ }
1334
+ async function runChatNdjson(ctx, message, opts) {
1335
+ const text = (message ?? await readStdin2())?.trim();
1336
+ if (!text) {
1337
+ throw usageError("Chat needs a message when it is not interactive", 'Pass it as an argument, or pipe it in: echo "hi" | tabbio chat --json');
1338
+ }
1339
+ const controller = new AbortController();
1340
+ const onSigint = () => controller.abort();
1341
+ process.on("SIGINT", onSigint);
1342
+ let failed = false;
1343
+ try {
1344
+ const capabilities = opts.capabilities ?? loadCapabilityPrefs(ctx.api.profile.name);
1345
+ const frames = streamChatFrames(
1346
+ ctx,
1347
+ {
1348
+ mode: opts.mode,
1349
+ message: text,
1350
+ threadId: opts.thread,
1351
+ modelId: opts.model,
1352
+ companyId: opts.company,
1353
+ ...capabilityRequestFields(opts.mode, capabilities)
1354
+ },
1355
+ { signal: controller.signal }
1356
+ );
1357
+ for await (const frame of frames) {
1358
+ process.stdout.write(`${ndjsonLine(frame.data)}
1359
+ `);
1360
+ if (frame.event && isFailureEvent(frame.event)) failed = true;
1361
+ }
1362
+ } finally {
1363
+ process.off("SIGINT", onSigint);
1364
+ }
1365
+ if (failed) process.exitCode = ExitCode.Error;
1366
+ }
1367
+ function ndjsonLine(data) {
1368
+ try {
1369
+ return JSON.stringify(JSON.parse(data));
1370
+ } catch {
1371
+ return JSON.stringify({ type: "unparsed", payload: { data } });
1372
+ }
1373
+ }
1374
+ function registerChatCommand(program) {
1375
+ program.command("chat").description("Chat with Tabbio (streaming; interactive on a terminal, NDJSON events with --json or when piped)").argument("[message]", "First message (non-interactive: the only message; stdin works too)").addOption(new Option3("--mode <mode>", "Workspace").choices(["seeker", "employer"]).default("seeker")).option("--company <id>", "Company id for employer mode").option("--thread <id>", "Continue an existing thread").option("--model <id>", "Model id from the app's picker (default: auto)").addOption(new Option3("--research <mode>", "Web research for this chat (default: your saved choice, else auto)").choices(["auto", "on", "off"])).addOption(new Option3("--image <mode>", "Image generation for this chat").choices(["auto", "on", "off"])).addOption(
1376
+ new Option3("--result <mode>", "What to make: an answer, a document, slides, a page or an image").choices([
1377
+ "auto",
1378
+ "summary",
1379
+ "document",
1380
+ "slides",
1381
+ "page",
1382
+ "image"
1383
+ ])
1384
+ ).option("--save", "Remember --research/--image/--result for this profile (like /research in the chat)").addHelpText(
1385
+ "after",
1386
+ [
1387
+ "",
1388
+ "In the chat: /new \xB7 /threads \xB7 /mode seeker|employer \xB7 /model <id> \xB7 /research \xB7 /image \xB7 /result \xB7 /help \xB7 /exit",
1389
+ "Research, image and result modes are sent with every message (seeker mode) with your timezone.",
1390
+ "Keys: enter send \xB7 ctrl+j newline \xB7 esc interrupt \xB7 ctrl+o expand tool \xB7 ctrl+c twice quit",
1391
+ "",
1392
+ "Examples:",
1393
+ " $ tabbio chat",
1394
+ ' $ tabbio chat "Find senior product design roles in Dubai"',
1395
+ ` $ tabbio chat --json "Summarise my CV" | jq -r 'select(.type=="text-delta").payload.text'`,
1396
+ ' $ tabbio chat --research on "Latest UAE labour law changes, with sources"',
1397
+ ' $ tabbio chat --result document "A one-page brief on my job search"'
1398
+ ].join("\n")
1399
+ ).action(async (message, opts, cmd) => {
1400
+ const ctx = createCommandContext(cmd);
1401
+ try {
1402
+ assertJwtSession(ctx.credentials, ctx.profile, "Chat");
1403
+ opts.capabilities = resolveCapabilities(loadCapabilityPrefs(ctx.profile.name), opts);
1404
+ if (opts.save) saveCapabilityPrefs(ctx.profile.name, opts.capabilities);
1405
+ if (ctx.globals.json || !isInteractiveTerminal() || process.env.CI) {
1406
+ await runChatNdjson(ctx, message, opts);
1407
+ return;
1408
+ }
1409
+ assertInteractive("Chat");
1410
+ const { runChat } = await import("./entry-WENOOT6W.js");
1411
+ await runChat(ctx, {
1412
+ mode: opts.mode,
1413
+ companyId: opts.company,
1414
+ threadId: opts.thread,
1415
+ modelId: opts.model,
1416
+ message,
1417
+ capabilities: opts.capabilities
1418
+ });
1419
+ } finally {
1420
+ await ctx.close();
1421
+ }
1422
+ });
1423
+ }
1424
+
1425
+ // src/commands/completion.ts
1426
+ var SHELLS = ["bash", "zsh", "fish"];
1427
+ function isHidden(cmd) {
1428
+ return Boolean(cmd._hidden);
1429
+ }
1430
+ function toOption(option) {
1431
+ const flags = [option.long, option.short].filter((f) => Boolean(f));
1432
+ return {
1433
+ flags,
1434
+ long: option.long?.replace(/^--/, ""),
1435
+ short: option.short?.replace(/^-/, ""),
1436
+ description: option.description ?? "",
1437
+ takesValue: Boolean(option.required || option.optional)
1438
+ };
1439
+ }
1440
+ function buildCompletionTree(program) {
1441
+ const walk = (cmd, path) => {
1442
+ const children = cmd.commands.filter((c) => !isHidden(c)).map((c) => walk(c, [...path, c.name()]));
1443
+ const options = cmd.options.filter((o) => !o.hidden).map(toOption);
1444
+ options.push({ flags: ["--help", "-h"], long: "help", short: "h", description: "Show help", takesValue: false });
1445
+ return { path: path.join(" "), name: cmd.name(), description: cmd.description(), children, options };
1446
+ };
1447
+ const root = walk(program, []);
1448
+ if (!root.children.some((c) => c.name === "help")) {
1449
+ root.children.push({ path: "help", name: "help", description: "Show help for a command", children: [], options: [] });
1450
+ }
1451
+ return root;
1452
+ }
1453
+ function flatten(node) {
1454
+ return [node, ...node.children.flatMap(flatten)];
1455
+ }
1456
+ var sq = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
1457
+ var oneLine = (value) => value.replace(/\s+/g, " ").trim();
1458
+ function bashScript(root) {
1459
+ const nodes = flatten(root);
1460
+ const paths = nodes.filter((n) => n.path).map((n) => sq(n.path)).join("|");
1461
+ const cases = nodes.map((n) => {
1462
+ const words = [...n.children.map((c) => c.name), ...n.options.flatMap((o) => o.flags)].join(" ");
1463
+ return ` ${n.path ? sq(n.path) : "''"}) cmdwords=${sq(words)} ;;`;
1464
+ }).join("\n");
1465
+ return `# tabbio bash completion
1466
+ # Install: tabbio completion bash > ~/.local/share/bash-completion/completions/tabbio
1467
+ _tabbio() {
1468
+ local cur w i cmdpath="" cmdwords=""
1469
+ cur="\${COMP_WORDS[COMP_CWORD]}"
1470
+ for ((i=1; i<COMP_CWORD; i++)); do
1471
+ w="\${COMP_WORDS[i]}"
1472
+ [[ "$w" == -* ]] && continue
1473
+ case "\${cmdpath:+$cmdpath }$w" in
1474
+ ${paths || "''"}) cmdpath="\${cmdpath:+$cmdpath }$w" ;;
1475
+ esac
1476
+ done
1477
+ case "$cmdpath" in
1478
+ ${cases}
1479
+ esac
1480
+ COMPREPLY=( $(compgen -W "$cmdwords" -- "$cur") )
1481
+ }
1482
+ complete -F _tabbio tabbio
1483
+ `;
1484
+ }
1485
+ function zshScript(root) {
1486
+ const nodes = flatten(root);
1487
+ const paths = nodes.filter((n) => n.path).map((n) => sq(n.path)).join("|");
1488
+ const escapeName = (value) => value.replace(/:/g, "\\:");
1489
+ const cases = nodes.map((n) => {
1490
+ const subs = n.children.map((c) => sq(`${escapeName(c.name)}:${oneLine(c.description)}`)).join(" ");
1491
+ const opts = n.options.flatMap((o) => o.flags).map(sq).join(" ");
1492
+ return ` ${n.path ? sq(n.path) : "''"}) subcmds=(${subs}); opts=(${opts}) ;;`;
1493
+ }).join("\n");
1494
+ return `#compdef tabbio
1495
+ # tabbio zsh completion
1496
+ # Install: tabbio completion zsh > "\${fpath[1]}/_tabbio" (or: eval "$(tabbio completion zsh)")
1497
+ _tabbio() {
1498
+ local cmdpath="" w i
1499
+ local -a subcmds opts
1500
+ for ((i=2; i<CURRENT; i++)); do
1501
+ w="\${words[i]}"
1502
+ [[ "$w" == -* ]] && continue
1503
+ case "\${cmdpath:+$cmdpath }$w" in
1504
+ ${paths || "''"}) cmdpath="\${cmdpath:+$cmdpath }$w" ;;
1505
+ esac
1506
+ done
1507
+ case "$cmdpath" in
1508
+ ${cases}
1509
+ esac
1510
+ if [[ "\${words[CURRENT]}" == -* ]]; then
1511
+ compadd -- "\${opts[@]}"
1512
+ else
1513
+ _describe -t commands 'tabbio command' subcmds
1514
+ fi
1515
+ }
1516
+ if [[ "\${funcstack[1]}" == "_tabbio" ]]; then
1517
+ _tabbio "$@"
1518
+ else
1519
+ compdef _tabbio tabbio
1520
+ fi
1521
+ `;
1522
+ }
1523
+ function fishScript(root) {
1524
+ const nodes = flatten(root);
1525
+ const fq = (value) => `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
1526
+ const lines = [
1527
+ "# tabbio fish completion",
1528
+ "# Install: tabbio completion fish > ~/.config/fish/completions/tabbio.fish",
1529
+ `set -g __tabbio_paths ${nodes.filter((n) => n.path).map((n) => fq(n.path)).join(" ")}`,
1530
+ "function __tabbio_cmdpath",
1531
+ " set -l tokens (commandline -opc)",
1532
+ " set -l cmdpath",
1533
+ " for t in $tokens[2..-1]",
1534
+ " if string match -q -- '-*' $t",
1535
+ " continue",
1536
+ " end",
1537
+ " set -l candidate (string join ' ' $cmdpath $t)",
1538
+ " if contains -- $candidate $__tabbio_paths",
1539
+ " set cmdpath $cmdpath $t",
1540
+ " end",
1541
+ " end",
1542
+ " string join ' ' $cmdpath",
1543
+ "end",
1544
+ "function __tabbio_is",
1545
+ " set -l current (__tabbio_cmdpath)",
1546
+ ' test "$current" = "$argv[1]"',
1547
+ "end",
1548
+ "complete -c tabbio -f"
1549
+ ];
1550
+ for (const node of nodes) {
1551
+ const condition = `-n ${fq(`__tabbio_is ${fq(node.path)}`)}`;
1552
+ for (const child of node.children) {
1553
+ lines.push(`complete -c tabbio ${condition} -a ${fq(child.name)} -d ${fq(oneLine(child.description))}`);
1554
+ }
1555
+ for (const option of node.options) {
1556
+ const parts = [
1557
+ option.long ? `-l ${option.long}` : "",
1558
+ option.short ? `-s ${option.short}` : "",
1559
+ option.takesValue ? "-r" : ""
1560
+ ].filter(Boolean);
1561
+ if (parts.length === 0) continue;
1562
+ lines.push(`complete -c tabbio ${condition} ${parts.join(" ")} -d ${fq(oneLine(option.description))}`);
1563
+ }
1564
+ }
1565
+ return `${lines.join("\n")}
1566
+ `;
1567
+ }
1568
+ function completionScript(shell, program) {
1569
+ const tree = buildCompletionTree(program);
1570
+ switch (shell) {
1571
+ case "bash":
1572
+ return bashScript(tree);
1573
+ case "zsh":
1574
+ return zshScript(tree);
1575
+ case "fish":
1576
+ return fishScript(tree);
1577
+ default:
1578
+ throw usageError(`Unsupported shell "${shell}"`, `Choose one of: ${SHELLS.join(", ")}`);
1579
+ }
1580
+ }
1581
+ function registerCompletionCommand(program) {
1582
+ program.command("completion").description("Print a shell completion script (bash, zsh or fish)").argument("<shell>", SHELLS.join(" | ")).addHelpText(
1583
+ "after",
1584
+ '\nExamples:\n $ tabbio completion zsh > "${fpath[1]}/_tabbio"\n $ tabbio completion bash > ~/.local/share/bash-completion/completions/tabbio\n $ tabbio completion fish > ~/.config/fish/completions/tabbio.fish'
1585
+ ).action((shell, _opts, cmd) => {
1586
+ let root = cmd;
1587
+ while (root.parent) root = root.parent;
1588
+ writeOut(completionScript(shell, root).trimEnd());
1589
+ });
1590
+ }
1591
+
1592
+ // src/commands/config.ts
1593
+ var CONFIG_KEYS = {
1594
+ profile: "Current profile used when --profile / TABBIO_PROFILE are not set",
1595
+ "api-url": "API base URL for the profile",
1596
+ "app-url": "Web app URL for the profile (browser sign-in)",
1597
+ "mcp-url": "MCP endpoint for the profile (default: <api-url>/api/mcp)",
1598
+ "update-check": "Check npm for a newer CLI once a day (true/false)"
1599
+ };
1600
+ function assertKey(key) {
1601
+ if (key in CONFIG_KEYS) return key;
1602
+ throw usageError(`Unknown config key "${key}"`, `Known keys: ${Object.keys(CONFIG_KEYS).join(", ")}`);
1603
+ }
1604
+ var PROFILE_FIELD = {
1605
+ "api-url": "apiUrl",
1606
+ "app-url": "appUrl",
1607
+ "mcp-url": "mcpUrl"
1608
+ };
1609
+ function parseBoolean(value) {
1610
+ const normalized = value.trim().toLowerCase();
1611
+ if (["true", "1", "yes", "on"].includes(normalized)) return true;
1612
+ if (["false", "0", "no", "off"].includes(normalized)) return false;
1613
+ throw usageError(`Expected true or false, got "${value}"`);
1614
+ }
1615
+ function effectiveConfig(opts) {
1616
+ const { profile, sources } = resolveProfileWithSources(opts);
1617
+ const config = loadConfig();
1618
+ return {
1619
+ profile: { value: profile.name, source: sources.name },
1620
+ "api-url": { value: profile.apiUrl, source: sources.apiUrl },
1621
+ "app-url": { value: profile.appUrl, source: sources.appUrl },
1622
+ "mcp-url": { value: profile.mcpUrl, source: sources.mcpUrl },
1623
+ "update-check": { value: config.updateCheck !== false, source: config.updateCheck === void 0 ? "default" : "config" }
1624
+ };
1625
+ }
1626
+ function registerConfig(program) {
1627
+ const config = program.command("config").description("Read and change CLI settings");
1628
+ config.command("list").description("Show effective settings and where each comes from").action((_opts, cmd) => {
1629
+ const ctx = createCommandContext(cmd);
1630
+ const values = effectiveConfig(ctx.globals);
1631
+ if (ctx.globals.json) {
1632
+ printJson({ ...values, paths: configPaths() });
1633
+ return;
1634
+ }
1635
+ const rows = Object.entries(values).map(([key, entry]) => [
1636
+ key,
1637
+ `${String(entry.value)} ${theme.dim(`(${entry.source})`)}`
1638
+ ]);
1639
+ rows.push(["config dir", configPaths().dir]);
1640
+ printKeyValues(rows);
1641
+ });
1642
+ config.command("get").description("Print one setting").argument("<key>", `One of: ${Object.keys(CONFIG_KEYS).join(", ")}`).action((key, _opts, cmd) => {
1643
+ const ctx = createCommandContext(cmd);
1644
+ const entry = effectiveConfig(ctx.globals)[assertKey(key)];
1645
+ if (ctx.globals.json) printJson({ key, ...entry });
1646
+ else writeOut(String(entry.value));
1647
+ });
1648
+ config.command("set").description("Change a setting (profile-scoped keys apply to --profile or the current profile)").argument("<key>", `One of: ${Object.keys(CONFIG_KEYS).join(", ")}`).argument("<value>").action((rawKey, value, _opts, cmd) => {
1649
+ const ctx = createCommandContext(cmd);
1650
+ const key = assertKey(rawKey);
1651
+ const profileName = ctx.profile.name;
1652
+ updateConfig((c) => {
1653
+ if (key === "profile") c.currentProfile = assertProfileName(value);
1654
+ else if (key === "update-check") c.updateCheck = parseBoolean(value);
1655
+ else {
1656
+ const label2 = key === "mcp-url" ? "MCP URL" : key === "api-url" ? "API URL" : "App URL";
1657
+ c.profiles[profileName] = { ...c.profiles[profileName], [PROFILE_FIELD[key]]: normalizeBaseUrl(value, label2) };
1658
+ }
1659
+ });
1660
+ if (ctx.globals.json) printJson({ key, value: effectiveConfig({ profile: key === "profile" ? value : profileName })[key].value });
1661
+ else writeOut(successLine(`Set ${key}${key in PROFILE_FIELD ? ` for profile ${profileName}` : ""}`));
1662
+ });
1663
+ config.command("unset").description("Reset a setting to its default").argument("<key>", `One of: ${Object.keys(CONFIG_KEYS).join(", ")}`).action((rawKey, _opts, cmd) => {
1664
+ const ctx = createCommandContext(cmd);
1665
+ const key = assertKey(rawKey);
1666
+ updateConfig((c) => {
1667
+ if (key === "profile") delete c.currentProfile;
1668
+ else if (key === "update-check") delete c.updateCheck;
1669
+ else {
1670
+ const current = { ...c.profiles[ctx.profile.name] };
1671
+ delete current[PROFILE_FIELD[key]];
1672
+ c.profiles[ctx.profile.name] = current;
1673
+ }
1674
+ });
1675
+ if (ctx.globals.json) printJson({ key, unset: true });
1676
+ else writeOut(successLine(`Reset ${key}`));
1677
+ });
1678
+ config.command("path").description("Print the config, credentials and cache locations").action((_opts, cmd) => {
1679
+ const ctx = createCommandContext(cmd);
1680
+ const paths = configPaths();
1681
+ if (ctx.globals.json) printJson(paths);
1682
+ else printKeyValues([
1683
+ ["config", paths.configFile],
1684
+ ["credentials", paths.credentialsFile],
1685
+ ["cache", paths.cacheDir]
1686
+ ]);
1687
+ });
1688
+ return config;
1689
+ }
1690
+ function registerProfile(config) {
1691
+ const profile = config.command("profile").description("Manage named profiles (e.g. production, local)");
1692
+ profile.command("list").description("List profiles").action((_opts, cmd) => {
1693
+ const ctx = createCommandContext(cmd);
1694
+ const names = Array.from(/* @__PURE__ */ new Set([...listProfileNames(), ...listCredentialProfiles()])).sort();
1695
+ const rows = names.map((name) => {
1696
+ const { profile: resolved } = resolveProfileWithSources({ profile: name });
1697
+ const creds = loadStoredCredentials(name);
1698
+ return {
1699
+ name,
1700
+ current: name === ctx.profile.name,
1701
+ apiUrl: resolved.apiUrl,
1702
+ signedIn: Boolean(creds?.accessToken || creds?.mcpToken),
1703
+ user: creds?.user?.email ?? null
1704
+ };
1705
+ });
1706
+ if (ctx.globals.json) {
1707
+ printJson(rows);
1708
+ return;
1709
+ }
1710
+ for (const row2 of rows) {
1711
+ const marker = row2.current ? theme.accent(theme.symbols.dot) : " ";
1712
+ const who = row2.signedIn ? row2.user ?? "token" : theme.dim("signed out");
1713
+ writeOut(`${marker} ${row2.name.padEnd(14)} ${who} ${theme.dim(row2.apiUrl)}`);
1714
+ }
1715
+ });
1716
+ profile.command("use").description("Make a profile the default").argument("<name>").action((name, _opts, cmd) => {
1717
+ const ctx = createCommandContext(cmd);
1718
+ const profileName = assertProfileName(name);
1719
+ const known = /* @__PURE__ */ new Set([...listProfileNames(), ...listCredentialProfiles(), ...Object.keys(PROFILE_PRESETS)]);
1720
+ if (!known.has(profileName)) {
1721
+ throw usageError(`No profile named "${profileName}"`, `Create it with \`tabbio config profile add ${profileName} --api-url \u2026\`.`);
1722
+ }
1723
+ updateConfig((c) => {
1724
+ if (profileName === DEFAULT_PROFILE) delete c.currentProfile;
1725
+ else c.currentProfile = profileName;
1726
+ });
1727
+ if (ctx.globals.json) printJson({ current: profileName });
1728
+ else writeOut(successLine(`Now using profile ${profileName}`));
1729
+ });
1730
+ profile.command("add").description("Create or update a profile").argument("<name>").option("--api-url <url>", "API base URL").option("--app-url <url>", "Web app URL (browser sign-in)").option("--mcp-url <url>", "MCP endpoint (default: <api-url>/api/mcp)").action((name, opts, cmd) => {
1731
+ const ctx = createCommandContext(cmd);
1732
+ const profileName = assertProfileName(name);
1733
+ const local = cmd.opts();
1734
+ const apiUrl = local.apiUrl ?? opts.apiUrl ?? ctx.globals.apiUrl;
1735
+ const appUrl = local.appUrl ?? opts.appUrl ?? ctx.globals.appUrl;
1736
+ const mcpUrl = local.mcpUrl ?? opts.mcpUrl;
1737
+ if (!apiUrl && !PROFILE_PRESETS[profileName]) {
1738
+ throw usageError("A new profile needs --api-url", "Example: tabbio config profile add staging --api-url https://\u2026 --app-url https://\u2026");
1739
+ }
1740
+ updateConfig((c) => {
1741
+ c.profiles[profileName] = {
1742
+ ...c.profiles[profileName],
1743
+ ...apiUrl ? { apiUrl: normalizeBaseUrl(apiUrl, "API URL") } : {},
1744
+ ...appUrl ? { appUrl: normalizeBaseUrl(appUrl, "App URL") } : {},
1745
+ ...mcpUrl ? { mcpUrl: normalizeBaseUrl(mcpUrl, "MCP URL") } : {}
1746
+ };
1747
+ });
1748
+ const { profile: resolved } = resolveProfileWithSources({ profile: profileName });
1749
+ if (ctx.globals.json) printJson(resolved);
1750
+ else writeOut(successLine(`Saved profile ${profileName} (${resolved.apiUrl})`));
1751
+ });
1752
+ profile.command("remove").description("Delete a profile from the config (sign out of it first)").argument("<name>").action((name, _opts, cmd) => {
1753
+ const ctx = createCommandContext(cmd);
1754
+ const profileName = assertProfileName(name);
1755
+ if (loadStoredCredentials(profileName)) {
1756
+ throw usageError(
1757
+ `Profile "${profileName}" is still signed in`,
1758
+ `Run \`tabbio logout --profile ${profileName}\` first so its tokens are revoked.`
1759
+ );
1760
+ }
1761
+ updateConfig((c) => {
1762
+ delete c.profiles[profileName];
1763
+ if (c.currentProfile === profileName) delete c.currentProfile;
1764
+ });
1765
+ if (ctx.globals.json) printJson({ removed: profileName });
1766
+ else writeOut(successLine(`Removed profile ${profileName}`));
1767
+ });
1768
+ }
1769
+ function registerConfigCommands(program) {
1770
+ const config = registerConfig(program);
1771
+ registerProfile(config);
1772
+ }
1773
+
1774
+ // src/commands/doctor.ts
1775
+ var MAX_CLOCK_SKEW_MS = 2 * 60 * 1e3;
1776
+ function checkNodeVersion(version = process.versions.node) {
1777
+ const major = Number.parseInt(version.split(".")[0] ?? "0", 10);
1778
+ return major >= 20 ? { name: "Node.js", status: "ok", detail: `v${version}` } : { name: "Node.js", status: "fail", detail: `v${version}`, hint: "Tabbio CLI needs Node.js 20 or newer." };
1779
+ }
1780
+ function checkPermissions() {
1781
+ const issues = credentialPermissionIssues();
1782
+ return issues.length === 0 ? { name: "Credential store", status: "ok", detail: `${configPaths().dir} (private)` } : {
1783
+ name: "Credential store",
1784
+ status: "fail",
1785
+ detail: issues.join("; "),
1786
+ hint: `Run: chmod 700 "${configPaths().dir}" && chmod 600 "${configPaths().credentialsFile}"`
1787
+ };
1788
+ }
1789
+ async function checkApi(ctx) {
1790
+ const started = Date.now();
1791
+ const api = new ApiClient(ctx.profile, null, { persist: false });
1792
+ try {
1793
+ const response = await api.raw("/api/health", { auth: "none", timeoutMs: 1e4 });
1794
+ const latency = Date.now() - started;
1795
+ const body = await response.json().catch(() => null);
1796
+ const checks = [
1797
+ {
1798
+ name: "API health",
1799
+ status: response.ok ? "ok" : "fail",
1800
+ detail: `${ctx.profile.apiUrl} \u2192 HTTP ${response.status} in ${latency}ms${body?.data?.commitSha ? ` \xB7 ${body.data.commitSha.slice(0, 7)}` : ""}${readRequestId(response.headers) ? ` \xB7 req ${readRequestId(response.headers)}` : ""}`
1801
+ }
1802
+ ];
1803
+ const serverDate = response.headers.get("date");
1804
+ if (serverDate) {
1805
+ const skew = Date.parse(serverDate) - (started + latency / 2);
1806
+ checks.push(
1807
+ Math.abs(skew) > MAX_CLOCK_SKEW_MS ? {
1808
+ name: "Clock",
1809
+ status: "warn",
1810
+ detail: `local clock is ${Math.round(Math.abs(skew) / 1e3)}s ${skew > 0 ? "behind" : "ahead of"} the server`,
1811
+ hint: "Token expiry checks may misfire; sync your system clock."
1812
+ } : { name: "Clock", status: "ok", detail: `within ${Math.round(Math.abs(skew) / 1e3)}s of the server` }
1813
+ );
1814
+ }
1815
+ return checks;
1816
+ } catch (error) {
1817
+ return [
1818
+ {
1819
+ name: "API health",
1820
+ status: "fail",
1821
+ detail: `${ctx.profile.apiUrl}: ${error.message}`,
1822
+ hint: "Check your network or the API URL (`tabbio config get api-url`)."
1823
+ }
1824
+ ];
1825
+ }
1826
+ }
1827
+ async function checkSession(ctx) {
1828
+ if (!ctx.credentials?.accessToken) {
1829
+ return { name: "Session (JWT)", status: "skip", detail: "no app session (MCP-only or signed out)" };
1830
+ }
1831
+ try {
1832
+ const me = await ctx.api.json("/api/users/me");
1833
+ return { name: "Session (JWT)", status: "ok", detail: `valid${me?.email ? ` for ${me.email}` : ""}` };
1834
+ } catch (error) {
1835
+ return {
1836
+ name: "Session (JWT)",
1837
+ status: "fail",
1838
+ detail: error.message,
1839
+ hint: "Run `tabbio login`."
1840
+ };
1841
+ }
1842
+ }
1843
+ async function checkMcp(ctx) {
1844
+ const creds = ctx.api.credentials ?? ctx.credentials;
1845
+ const via = mcpSummary(creds);
1846
+ if (!via) return { name: "MCP", status: "skip", detail: "not signed in", hint: "Run `tabbio login`." };
1847
+ const started = Date.now();
1848
+ let session = null;
1849
+ try {
1850
+ session = await McpSession.connect(ctx.profile, creds, { api: ctx.api });
1851
+ const tools = await session.listTools({ refresh: true });
1852
+ return {
1853
+ name: "MCP",
1854
+ status: tools.length > 0 ? "ok" : "warn",
1855
+ detail: `${ctx.profile.mcpUrl} \u2192 ${tools.length} tools in ${Date.now() - started}ms via ${via.label}`
1856
+ };
1857
+ } catch (error) {
1858
+ return {
1859
+ name: "MCP",
1860
+ status: "fail",
1861
+ detail: `${error.message} (via ${via.label})`,
1862
+ hint: error.hint
1863
+ };
1864
+ } finally {
1865
+ await session?.close();
1866
+ }
1867
+ }
1868
+ var SYMBOL = {
1869
+ ok: () => theme.success(theme.symbols.success),
1870
+ warn: () => theme.warn(theme.symbols.warning),
1871
+ fail: () => theme.error(theme.symbols.error),
1872
+ skip: () => theme.dim("-")
1873
+ };
1874
+ function registerDoctorCommand(program) {
1875
+ program.command("doctor").description("Diagnose Node, config permissions, API reachability, clock skew, session and MCP").action(async (_opts, cmd) => {
1876
+ const ctx = createCommandContext(cmd);
1877
+ const checks = [
1878
+ { name: "Tabbio CLI", status: "ok", detail: `v${CLI_VERSION} \xB7 profile ${ctx.profile.name}` },
1879
+ checkNodeVersion(),
1880
+ checkPermissions(),
1881
+ ...await checkApi(ctx),
1882
+ await checkSession(ctx),
1883
+ await checkMcp(ctx)
1884
+ ];
1885
+ const failed = checks.some((check) => check.status === "fail");
1886
+ if (ctx.globals.json) {
1887
+ printJson({ ok: !failed, checks });
1888
+ } else {
1889
+ const width = Math.max(...checks.map((c) => c.name.length));
1890
+ for (const check of checks) {
1891
+ writeOut(`${SYMBOL[check.status]()} ${check.name.padEnd(width)} ${theme.dim(check.detail)}`);
1892
+ if (check.hint && check.status !== "ok") writeOut(` ${" ".repeat(width)} ${check.hint}`);
1893
+ }
1894
+ }
1895
+ if (failed) process.exitCode = ExitCode.Error;
1896
+ });
1897
+ }
1898
+
1899
+ // src/commands/home.ts
1900
+ function registerHome() {
1901
+ setHomeHandler(async (ctx) => {
1902
+ const { runHome } = await import("./entry-WENOOT6W.js");
1903
+ await runHome(ctx);
1904
+ });
1905
+ }
1906
+
1907
+ // src/commands/generated.ts
1908
+ var STATIC_COMMAND_NAMES = /* @__PURE__ */ new Set([
1909
+ "login",
1910
+ "logout",
1911
+ "status",
1912
+ "whoami",
1913
+ "doctor",
1914
+ "config",
1915
+ "mcp",
1916
+ "completion",
1917
+ "tools",
1918
+ "run",
1919
+ "workflows",
1920
+ "approvals",
1921
+ "artifacts",
1922
+ "ask",
1923
+ "chat",
1924
+ "help"
1925
+ ]);
1926
+ var TOOL_COMMANDS_HELP_GROUP = "Tool commands:";
1927
+ function generatedCommandPath(tool) {
1928
+ if (tool.hidden || tool.kind !== "tool" || tool.commandPath.length < 2) return null;
1929
+ const [group, action] = tool.commandPath;
1930
+ if (!group || !action || STATIC_COMMAND_NAMES.has(group)) return null;
1931
+ return [group, action];
1932
+ }
1933
+ function invocationPath(tool) {
1934
+ if (tool.kind === "agent") return tool.commandPath.length === 1 ? tool.commandPath : ["run", tool.id];
1935
+ if (tool.kind === "workflow") return ["workflows", "run", tool.action];
1936
+ return generatedCommandPath(tool) ?? ["run", tool.id];
1937
+ }
1938
+ function invocation(tool) {
1939
+ return `tabbio ${invocationPath(tool).join(" ")}`;
1940
+ }
1941
+ function toolHelpText(tool) {
1942
+ const lines = ["", "Example:", ` $ ${exampleInvocation(tool, tool.inputSchema)}`, ""];
1943
+ lines.push(`Tool: ${tool.id}${tool.readOnly ? " (read-only)" : " (writes; may need approval)"}`);
1944
+ return lines.join("\n");
1945
+ }
1946
+ function registerGeneratedCommands(program, catalog) {
1947
+ const taken = new Set(program.commands.flatMap((c) => [c.name(), ...c.aliases()]));
1948
+ const groups = /* @__PURE__ */ new Map();
1949
+ const counts = /* @__PURE__ */ new Map();
1950
+ for (const tool of catalog) {
1951
+ const path = generatedCommandPath(tool);
1952
+ if (!path) continue;
1953
+ const [group, action] = path;
1954
+ let groupCmd = groups.get(group);
1955
+ if (!groupCmd) {
1956
+ if (taken.has(group)) continue;
1957
+ groupCmd = program.command(group).helpGroup(TOOL_COMMANDS_HELP_GROUP);
1958
+ groups.set(group, groupCmd);
1959
+ }
1960
+ if (groupCmd.commands.some((c) => c.name() === action)) continue;
1961
+ counts.set(group, (counts.get(group) ?? 0) + 1);
1962
+ const cmd = groupCmd.command(action).summary(tool.title).description(tool.description || tool.title).allowUnknownOption().allowExcessArguments().addHelpText("after", () => toolHelpText(tool));
1963
+ addToolOptions(cmd, tool);
1964
+ addRunLevelOptions(cmd);
1965
+ cmd.action(async (opts, command) => {
1966
+ const ctx = createCommandContext(command);
1967
+ try {
1968
+ await runTool(ctx, tool, opts, command.args);
1969
+ } finally {
1970
+ await ctx.close();
1971
+ }
1972
+ });
1973
+ }
1974
+ for (const [group, cmd] of groups) {
1975
+ const n = counts.get(group) ?? 0;
1976
+ cmd.description(`${n} ${group} ${n === 1 ? "tool" : "tools"}`);
1977
+ }
1978
+ }
1979
+
1980
+ // src/commands/login.ts
1981
+ import open2 from "open";
1982
+ async function readTokenFromStdin(stdin = process.stdin) {
1983
+ if (stdin.isTTY) {
1984
+ throw usageError("--with-token reads the token from stdin", "Pipe it in: `tabbio login --with-token < token.txt`.");
1985
+ }
1986
+ const chunks = [];
1987
+ for await (const chunk of stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
1988
+ const token = Buffer.concat(chunks).toString("utf8").trim();
1989
+ if (!token) throw usageError("No token on stdin");
1990
+ return token;
1991
+ }
1992
+ function persistProfileUrls(profile, sources) {
1993
+ if (sources.apiUrl !== "flag" && sources.appUrl !== "flag") return;
1994
+ updateConfig((config) => {
1995
+ const current = config.profiles[profile.name] ?? {};
1996
+ config.profiles[profile.name] = {
1997
+ ...current,
1998
+ ...sources.apiUrl === "flag" ? { apiUrl: profile.apiUrl } : {},
1999
+ ...sources.appUrl === "flag" ? { appUrl: profile.appUrl } : {}
2000
+ };
2001
+ });
2002
+ }
2003
+ async function revokePrevious(profile, refreshToken, keep) {
2004
+ if (!refreshToken || refreshToken === keep) return;
2005
+ await revokeSession(new ApiClient(profile, null, { persist: false }), refreshToken).catch(
2006
+ (error) => debug(`previous session not revoked: ${error.message}`)
2007
+ );
2008
+ }
2009
+ async function loginWithToken(ctx, rawToken) {
2010
+ const token = rawToken.trim();
2011
+ if (!token.startsWith("tabbio_mcp_")) {
2012
+ warn("This is not a tabbio_mcp_ token; treating it as an OAuth access token.");
2013
+ }
2014
+ const probe = { profile: ctx.profile.name, mcpToken: token };
2015
+ const session = await McpSession.connect(ctx.profile, probe);
2016
+ let toolCount = 0;
2017
+ try {
2018
+ toolCount = (await session.listTools({ refresh: true })).length;
2019
+ } finally {
2020
+ await session.close();
2021
+ }
2022
+ const previous = loadStoredCredentials(ctx.profile.name);
2023
+ saveCredentials(probe);
2024
+ persistProfileUrls(ctx.profile, ctx.sources);
2025
+ await revokePrevious(ctx.profile, previous?.refreshToken);
2026
+ if (ctx.globals.json) {
2027
+ printJson({
2028
+ profile: ctx.profile.name,
2029
+ method: "token",
2030
+ user: null,
2031
+ mcp: { kind: "personal-token", token: fingerprint(token) },
2032
+ tools: toolCount
2033
+ });
2034
+ return;
2035
+ }
2036
+ writeOut(successLine(`Token accepted \xB7 ${toolCount} tools available \xB7 profile ${ctx.profile.name}`));
2037
+ writeOut(theme.dim(" MCP-only mode: `tabbio chat` and approving actions need `tabbio login` (browser or --email)."));
2038
+ }
2039
+ async function obtainSession(ctx, method, opts) {
2040
+ const api = new ApiClient(ctx.profile, null, { persist: false });
2041
+ if (method === "email") {
2042
+ const email = opts.email?.trim() || await promptLine("Email: ");
2043
+ info(`Sending a sign-in code to ${email}\u2026`);
2044
+ return loginWithEmailOtp(api, {
2045
+ email,
2046
+ promptCode: (attempt) => promptSecret(attempt === 1 ? "Enter the 6-digit code: " : "Try the code again: "),
2047
+ onInvalidCode: (left) => warn(`That code did not match. ${left} attempt${left === 1 ? "" : "s"} left.`)
2048
+ });
2049
+ }
2050
+ const agentMode = Boolean(opts.nonInteractive);
2051
+ if (!agentMode) info(`Connecting this computer (${deviceLabel()}) to Tabbio\u2026`);
2052
+ const controller = new AbortController();
2053
+ const onSigint = () => controller.abort();
2054
+ process.once("SIGINT", onSigint);
2055
+ try {
2056
+ return await loginWithBrowser(api, {
2057
+ device: deviceLabel(),
2058
+ signal: controller.signal,
2059
+ openUrl: async (url) => {
2060
+ if (agentMode) return false;
2061
+ try {
2062
+ await open2(url);
2063
+ return true;
2064
+ } catch {
2065
+ return false;
2066
+ }
2067
+ },
2068
+ onWaiting: (url, opened, details) => {
2069
+ if (agentMode) {
2070
+ printJson({ url, state: details.state, expiresAt: details.expiresAt });
2071
+ return;
2072
+ }
2073
+ info(opened ? theme.dim("If your browser did not open, visit:") : "Open this URL to continue:");
2074
+ info(` ${theme.accent(url)}`);
2075
+ info(theme.dim("Waiting for you to approve in the browser\u2026 (Ctrl-C to cancel)"));
2076
+ }
2077
+ });
2078
+ } finally {
2079
+ process.off("SIGINT", onSigint);
2080
+ }
2081
+ }
2082
+ async function runLogin(opts, cmd) {
2083
+ const ctx = createCommandContext(cmd);
2084
+ if (opts.nonInteractive) ctx.globals.json = true;
2085
+ const method = chooseLoginMethod({
2086
+ browser: opts.browser,
2087
+ email: opts.email,
2088
+ token: opts.token,
2089
+ withToken: opts.withToken,
2090
+ nonInteractive: opts.nonInteractive,
2091
+ isTTY: Boolean(process.stdin.isTTY && process.stderr.isTTY),
2092
+ hasDisplay: hasDisplay()
2093
+ });
2094
+ if (method === "token") {
2095
+ return loginWithToken(ctx, opts.withToken ? await readTokenFromStdin() : opts.token ?? "");
2096
+ }
2097
+ const previous = loadStoredCredentials(ctx.profile.name);
2098
+ const payload = await obtainSession(ctx, method, opts);
2099
+ const creds = credentialsFromLogin(ctx.profile, payload);
2100
+ saveCredentials(creds);
2101
+ clearCatalogCache(ctx.profile);
2102
+ persistProfileUrls(ctx.profile, ctx.sources);
2103
+ await revokePrevious(ctx.profile, previous?.refreshToken, payload.refreshToken);
2104
+ if (ctx.globals.json) {
2105
+ printJson({
2106
+ profile: ctx.profile.name,
2107
+ method,
2108
+ apiUrl: ctx.profile.apiUrl,
2109
+ user: creds.user ?? null,
2110
+ mcp: { kind: "app-session" }
2111
+ });
2112
+ return;
2113
+ }
2114
+ const who = creds.user?.name ? `${creds.user.name} <${creds.user.email}>` : creds.user?.email ?? "your account";
2115
+ writeOut(successLine(`Signed in as ${theme.bold(who)} \xB7 profile ${ctx.profile.name}`));
2116
+ if (process.env.TABBIO_TOKEN) {
2117
+ warn("TABBIO_TOKEN is set, so tool commands keep using that personal token.");
2118
+ }
2119
+ writeOut(theme.dim(" Next: tabbio status \xB7 tabbio tools \xB7 tabbio chat"));
2120
+ }
2121
+ function registerLoginCommand(program) {
2122
+ program.command("login").description("Sign in (browser by default; --email for SSH/headless; --token for MCP-only)").option("--browser", "Approve this computer in your browser").option("--email <address>", "Sign in with a one-time code sent to this email").option("--token <token>", "Use a personal MCP token (tabbio_mcp_\u2026); tool commands only").option("--with-token", "Read a personal MCP token from stdin (CI)").option(
2123
+ "--non-interactive",
2124
+ 'For agents: print {"url","state","expiresAt"} as JSON, wait for the browser approval, never prompt'
2125
+ ).addHelpText(
2126
+ "after",
2127
+ "\nThe browser flow opens <app-url>/cli/connect and receives the session on 127.0.0.1.\nEnvironment: TABBIO_TOKEN / TABBIO_ACCESS_TOKEN override stored credentials (never saved)."
2128
+ ).action(async (opts, cmd) => runLogin(opts, cmd));
2129
+ }
2130
+
2131
+ // src/commands/logout.ts
2132
+ async function logoutOne(name, apiUrl, appUrl) {
2133
+ const profile = resolveProfile({ profile: name, apiUrl, appUrl });
2134
+ const creds = loadStoredCredentials(profile.name);
2135
+ if (!creds) {
2136
+ return { profile: profile.name, signedOut: false, revokedSession: false, warnings: [] };
2137
+ }
2138
+ const result = await logoutProfile(profile, creds);
2139
+ return { profile: profile.name, signedOut: true, ...result };
2140
+ }
2141
+ function registerLogoutCommand(program) {
2142
+ program.command("logout").description("Sign out: end the session on the server and delete local credentials").option("--all", "Sign out of every profile").action(async (opts, cmd) => {
2143
+ const ctx = createCommandContext(cmd);
2144
+ const names = opts.all ? listCredentialProfiles() : [ctx.profile.name];
2145
+ const results = [];
2146
+ for (const name of names) {
2147
+ results.push(
2148
+ await logoutOne(name, opts.all ? void 0 : ctx.globals.apiUrl, opts.all ? void 0 : ctx.globals.appUrl)
2149
+ );
2150
+ }
2151
+ const envNote = process.env.TABBIO_TOKEN || process.env.TABBIO_ACCESS_TOKEN ? "TABBIO_TOKEN / TABBIO_ACCESS_TOKEN is still set in your environment." : void 0;
2152
+ if (ctx.globals.json) {
2153
+ printJson({ results, ...envNote ? { note: envNote } : {} });
2154
+ return;
2155
+ }
2156
+ if (results.length === 0 || results.every((r) => !r.signedOut)) {
2157
+ writeOut(theme.dim(opts.all ? "No stored sign-ins." : `Not signed in (profile ${ctx.profile.name}).`));
2158
+ }
2159
+ for (const result of results.filter((r) => r.signedOut)) {
2160
+ for (const warning of result.warnings) warn(warning);
2161
+ writeOut(successLine(`Signed out of profile ${result.profile}`));
2162
+ }
2163
+ if (envNote) warn(envNote);
2164
+ });
2165
+ }
2166
+
2167
+ // src/commands/mcp-serve.ts
2168
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2169
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2170
+ import {
2171
+ CallToolRequestSchema,
2172
+ ListToolsRequestSchema
2173
+ } from "@modelcontextprotocol/sdk/types.js";
2174
+ var BRIDGE_CALL_TIMEOUT_MS = 15 * 60 * 1e3;
2175
+ function bridgeErrorResult(error) {
2176
+ const cliError = toCliError(error);
2177
+ const text = [cliError.message, cliError.hint].filter(Boolean).join("\n");
2178
+ return { isError: true, content: [{ type: "text", text: redactSecrets(text) }] };
2179
+ }
2180
+ function createBridgeServer(remote, opts = {}) {
2181
+ const server = new Server(
2182
+ { name: "tabbio", version: CLI_VERSION },
2183
+ { capabilities: { tools: { listChanged: false } }, instructions: remote.instructions }
2184
+ );
2185
+ let refreshNext = Boolean(opts.refreshFirstList);
2186
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
2187
+ const tools = await remote.listRawTools({ refresh: refreshNext });
2188
+ refreshNext = false;
2189
+ return { tools };
2190
+ });
2191
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
2192
+ try {
2193
+ return await remote.callRaw(request.params, { signal: extra.signal, timeoutMs: BRIDGE_CALL_TIMEOUT_MS });
2194
+ } catch (error) {
2195
+ return bridgeErrorResult(error);
2196
+ }
2197
+ });
2198
+ return server;
2199
+ }
2200
+ async function serve(opts, cmd) {
2201
+ const ctx = createCommandContext(cmd);
2202
+ const remote = await ctx.mcp();
2203
+ const server = createBridgeServer(remote, { refreshFirstList: opts.refresh });
2204
+ const transport = new StdioServerTransport();
2205
+ const closed = new Promise((resolve) => {
2206
+ transport.onclose = () => resolve();
2207
+ process.stdin.once("end", () => resolve());
2208
+ process.stdin.once("close", () => resolve());
2209
+ });
2210
+ await server.connect(transport);
2211
+ const via = mcpSummary(ctx.api.credentials ?? ctx.credentials);
2212
+ info(theme.dim(`tabbio mcp serve: bridging stdio \u2192 ${ctx.profile.mcpUrl} (profile ${ctx.profile.name}, via ${via?.label ?? "unknown"})`));
2213
+ await closed;
2214
+ await server.close().catch(() => void 0);
2215
+ await ctx.close();
2216
+ }
2217
+ function registerMcpCommands(program) {
2218
+ const mcp = program.command("mcp").description("Use Tabbio from other MCP clients (Claude, Cursor, \u2026)");
2219
+ mcp.command("serve").description("Run a stdio MCP server that forwards to Tabbio using this CLI's sign-in").option("--refresh", "Reload the tool list from the server instead of the 1h cache").addHelpText(
2220
+ "after",
2221
+ "\nExample:\n $ claude mcp add tabbio -- tabbio mcp serve\n $ claude mcp add tabbio -- tabbio --profile work mcp serve"
2222
+ ).action(async (opts, cmd) => serve(opts, cmd));
2223
+ mcp.command("url").description("Print the MCP endpoint URL and how to add it to a client").action((_opts, cmd) => {
2224
+ const ctx = createCommandContext(cmd);
2225
+ const url = ctx.profile.mcpUrl;
2226
+ const commands = {
2227
+ http: `claude mcp add --transport http tabbio ${url}`,
2228
+ stdio: `claude mcp add tabbio -- tabbio${ctx.profile.name === "default" ? "" : ` --profile ${ctx.profile.name}`} mcp serve`
2229
+ };
2230
+ if (ctx.globals.json) {
2231
+ printJson({ url, profile: ctx.profile.name, commands });
2232
+ return;
2233
+ }
2234
+ writeOut(url);
2235
+ if (!ctx.globals.quiet) {
2236
+ writeOut(theme.dim(` Remote (the client signs in with OAuth): ${commands.http}`));
2237
+ writeOut(theme.dim(` Local stdio bridge (uses this CLI): ${commands.stdio}`));
2238
+ }
2239
+ });
2240
+ }
2241
+
2242
+ // src/commands/tools.ts
2243
+ function toListEntry(tool) {
2244
+ return {
2245
+ id: tool.id,
2246
+ name: tool.mcpName,
2247
+ title: tool.title,
2248
+ description: tool.description,
2249
+ readOnly: tool.readOnly,
2250
+ kind: tool.kind,
2251
+ group: tool.group,
2252
+ action: tool.action,
2253
+ command: invocation(tool)
2254
+ };
2255
+ }
2256
+ function matchScore(tool, query) {
2257
+ const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
2258
+ if (tokens.length === 0) return 1;
2259
+ const id = `${tool.id} ${tool.mcpName} ${tool.commandPath.join(" ")}`.toLowerCase();
2260
+ const title = tool.title.toLowerCase();
2261
+ const description = tool.description.toLowerCase();
2262
+ let score = 0;
2263
+ for (const token of tokens) {
2264
+ if (id.includes(token)) score += 3;
2265
+ else if (title.includes(token)) score += 2;
2266
+ else if (description.includes(token)) score += 1;
2267
+ else return 0;
2268
+ }
2269
+ return score;
2270
+ }
2271
+ function filterTools(tools, filter = {}) {
2272
+ if (filter.reads && filter.writes) throw usageError("Use either --reads or --writes, not both");
2273
+ const group = filter.group?.trim().toLowerCase();
2274
+ const scored = tools.filter((t) => !t.hidden).filter((t) => !group || t.group === group || t.group.startsWith(group)).filter((t) => filter.reads ? t.readOnly : filter.writes ? !t.readOnly : true).map((tool) => ({ tool, score: filter.query ? matchScore(tool, filter.query) : 1 })).filter((entry) => entry.score > 0);
2275
+ if (filter.query) scored.sort((a, b) => b.score - a.score);
2276
+ return scored.map((entry) => entry.tool);
2277
+ }
2278
+ var WRITES_LABEL = " \xB7 writes";
2279
+ function readWrite(tool) {
2280
+ return tool.readOnly ? theme.muted(theme.symbols.dot) : theme.warn(theme.symbols.dot);
2281
+ }
2282
+ function formatToolList(tools, width = process.stdout.columns ?? 100) {
2283
+ if (tools.length === 0) return [theme.dim("No tools match.")];
2284
+ const idWidth = Math.min(36, Math.max(...tools.map((t) => displayWidth(t.id))));
2285
+ const lines = [];
2286
+ const groups = [...groupCatalog(tools)].sort(([a], [b]) => a.localeCompare(b));
2287
+ for (const [group, list3] of groups) {
2288
+ lines.push(`${theme.bold(group)} ${theme.dim(`(${list3.length})`)}`);
2289
+ for (const tool of list3) {
2290
+ const id = truncate(tool.id, idWidth);
2291
+ const room = Math.max(10, width - idWidth - 6 - (tool.readOnly ? 0 : WRITES_LABEL.length));
2292
+ const writes = tool.readOnly ? "" : theme.dim(WRITES_LABEL);
2293
+ lines.push(` ${readWrite(tool)} ${id}${" ".repeat(idWidth - displayWidth(id))} ${truncate(tool.title, room)}${writes}`);
2294
+ }
2295
+ }
2296
+ lines.push(
2297
+ theme.dim(
2298
+ `${tools.length} tools \xB7 ${theme.symbols.dot} read \xB7 ${theme.symbols.dot} writes (may need approval) \xB7 tabbio tools describe <id>`
2299
+ )
2300
+ );
2301
+ return lines;
2302
+ }
2303
+ function toolsToTsv(tools) {
2304
+ return tools.map((t) => [t.id, invocationPath(t).join(" "), t.readOnly ? "read" : "write", t.title].join(" ")).join("\n");
2305
+ }
2306
+ function noToolsMessage(filter) {
2307
+ const scope = [
2308
+ filter.group ? `in group "${filter.group}"` : null,
2309
+ filter.reads ? "(read-only)" : filter.writes ? "(writes)" : null
2310
+ ].filter(Boolean);
2311
+ if (!filter.query && scope.length === 0) return "No tools are available to this account";
2312
+ return ["No tools match", filter.query ? `"${filter.query}"` : "these filters", ...scope].join(" ");
2313
+ }
2314
+ async function listTools(ctx, filter) {
2315
+ const mode2 = resolveOutputMode({ json: ctx.globals.json });
2316
+ const session = await ctx.mcp();
2317
+ const all = await session.listTools({ refresh: filter.refresh });
2318
+ const hooks = getInteractiveHooks();
2319
+ if (hooks && mode2 === "table" && canUseInteractiveUi(ctx)) {
2320
+ await hooks.browseTools(ctx, filterTools(all, { reads: filter.reads, writes: filter.writes }), {
2321
+ query: filter.query,
2322
+ group: filter.group
2323
+ });
2324
+ return;
2325
+ }
2326
+ const tools = filterTools(all, filter);
2327
+ if (mode2 === "json") writeJson(tools.map(toListEntry));
2328
+ else if (tools.length === 0) printEmptyNotice(mode2, noToolsMessage(filter), { quiet: ctx.globals.quiet });
2329
+ else if (mode2 === "tsv") process.stdout.write(`${toolsToTsv(tools)}
2330
+ `);
2331
+ else for (const line of formatToolList(tools)) process.stdout.write(`${line}
2332
+ `);
2333
+ }
2334
+ function describeTool(tool) {
2335
+ return {
2336
+ ...toListEntry(tool),
2337
+ inputSchema: tool.inputSchema,
2338
+ flags: describeSchema(tool.inputSchema),
2339
+ example: exampleInvocation(tool, tool.inputSchema, invocationPath(tool)),
2340
+ mayRequireApproval: !tool.readOnly,
2341
+ autoFilled: autoFilledProperties(tool, tool.inputSchema)
2342
+ };
2343
+ }
2344
+ function formatToolDescription(info2, width = process.stdout.columns ?? 100) {
2345
+ const lines = [
2346
+ theme.bold(info2.title),
2347
+ theme.dim(`${info2.id} \xB7 MCP ${info2.name} \xB7 ${info2.kind} \xB7 ${info2.readOnly ? "read-only" : "writes"}`)
2348
+ ];
2349
+ if (info2.description) lines.push("", info2.description);
2350
+ if (info2.mayRequireApproval) {
2351
+ lines.push("", `${theme.warn(theme.symbols.warning)} May require approval before it runs (exit 6 until approved; --wait to wait).`);
2352
+ }
2353
+ lines.push("", theme.bold("Flags"));
2354
+ if (info2.flags.length === 0) lines.push(theme.dim(" (none)"));
2355
+ const flagWidth = Math.min(32, Math.max(0, ...info2.flags.map((f) => f.flag.length)));
2356
+ const typeWidth = Math.min(24, Math.max(0, ...info2.flags.map((f) => f.type.length)));
2357
+ for (const row2 of info2.flags) {
2358
+ const auto = info2.autoFilled.includes(row2.property) ? "filled from your session" : null;
2359
+ const notes = [row2.required && !auto ? "required" : null, auto, ...row2.notes].filter(Boolean).join("; ");
2360
+ const text = [row2.description, notes ? `(${notes})` : ""].filter(Boolean).join(" ");
2361
+ const room = Math.max(20, width - flagWidth - typeWidth - 6);
2362
+ lines.push(` ${row2.flag.padEnd(flagWidth)} ${theme.dim(truncate(row2.type, typeWidth).padEnd(typeWidth))} ${truncate(text, room)}`.trimEnd());
2363
+ }
2364
+ lines.push(
2365
+ theme.dim(" Also: --input <json> | --input-file <path> | --fields a,b | --wait | --timeout 10m | -o json|table|plain"),
2366
+ "",
2367
+ theme.bold("Example"),
2368
+ ` $ ${info2.example}`
2369
+ );
2370
+ return lines;
2371
+ }
2372
+ async function describe(ctx, ref) {
2373
+ const session = await ctx.mcp();
2374
+ const tool = await resolveCatalogTool(session, ref);
2375
+ const info2 = describeTool(tool);
2376
+ if (resolveOutputMode({ json: ctx.globals.json }) === "json") writeJson(info2);
2377
+ else for (const line of formatToolDescription(info2)) process.stdout.write(`${line}
2378
+ `);
2379
+ }
2380
+ function registerToolsCommand(program) {
2381
+ const tools = program.command("tools").description("Browse and search every Tabbio tool (TTY: interactive browser)").argument("[query]", "Filter by id, title or description").option("--group <group>", "Only this group (e.g. cv, job)").option("--reads", "Only read-only tools").option("--writes", "Only tools that change data").option("--refresh", "Reload the catalog from Tabbio (ignores the 1h cache)").addHelpText("after", "\nExamples:\n $ tabbio tools cv\n $ tabbio tools --group job --reads\n $ tabbio tools describe cv.list").action(async (query, opts, cmd) => {
2382
+ const ctx = createCommandContext(cmd);
2383
+ try {
2384
+ await listTools(ctx, { ...opts, query });
2385
+ } finally {
2386
+ await ctx.close();
2387
+ }
2388
+ });
2389
+ tools.command("describe").description("Show a tool: flags, types, approval note and an example").argument("<tool>", "Tool id (cv.list), MCP name (cvList) or command (cv list)").action(async (ref, _opts, cmd) => {
2390
+ const ctx = createCommandContext(cmd);
2391
+ try {
2392
+ await describe(ctx, ref);
2393
+ } finally {
2394
+ await ctx.close();
2395
+ }
2396
+ });
2397
+ }
2398
+
2399
+ // src/commands/run.ts
2400
+ function runLevel(opts) {
2401
+ const { input, inputFile, fields, wait: wait2, timeout, output } = opts;
2402
+ return { input, inputFile, fields, wait: wait2, timeout, output };
2403
+ }
2404
+ async function runDynamic(cmd, ref, rest, opts, filter) {
2405
+ if (!ref) {
2406
+ if (opts.help) {
2407
+ cmd.outputHelp();
2408
+ return;
2409
+ }
2410
+ throw usageError(`Missing the ${filter ? "workflow" : "tool"} to run`, `Usage: tabbio ${cmd.parent?.name() === "workflows" ? "workflows run <key>" : "run <tool>"} [flags]. List them with \`tabbio ${filter ? "workflows list" : "tools"}\`.`);
2411
+ }
2412
+ if (ref.startsWith("-")) throw usageError(`Put the ${filter ? "workflow key" : "tool"} first, then its flags`, `e.g. tabbio run cv.list --limit 5`);
2413
+ const ctx = createCommandContext(cmd);
2414
+ try {
2415
+ const session = await ctx.mcp();
2416
+ const tool = await resolveCatalogTool(session, ref, filter);
2417
+ if (opts.help || wantsHelp(rest)) {
2418
+ const info2 = describeTool(tool);
2419
+ if (resolveOutputMode({ json: ctx.globals.json }) === "json") writeJson(info2);
2420
+ else for (const line of formatToolDescription(info2)) process.stdout.write(`${line}
2421
+ `);
2422
+ return;
2423
+ }
2424
+ const parsed = parseDynamicFlags(tool, rest);
2425
+ await runTool(ctx, tool, { ...parsed.opts, ...runLevel(opts) }, parsed.rest);
2426
+ } finally {
2427
+ await ctx.close();
2428
+ }
2429
+ }
2430
+ function configureDynamicRunCommand(cmd, refName, refHelp) {
2431
+ cmd.argument(`[${refName}]`, refHelp).argument("[flags...]", "The tool's own flags, e.g. --limit 5 (see --help after the tool)").helpOption(false).option("-h, --help", "Show help; after a tool, show that tool's flags").allowUnknownOption().allowExcessArguments();
2432
+ return addRunLevelOptions(cmd);
2433
+ }
2434
+ function registerRunCommand(program) {
2435
+ const run = program.command("run").description("Run any Tabbio tool by id, MCP name or command path");
2436
+ configureDynamicRunCommand(run, "tool", 'Tool id (cv.list), MCP name (cvList) or command ("cv list")').addHelpText(
2437
+ "after",
2438
+ [
2439
+ "",
2440
+ "Examples:",
2441
+ " $ tabbio run cv.list",
2442
+ ' $ tabbio run job.search --query "product designer" --fields id,title,company',
2443
+ " $ tabbio run cv.delete --cv-id cv_123 --wait",
2444
+ ` $ echo '{"limit":5}' | tabbio run cv.list --input - --json`,
2445
+ " $ tabbio run cv.get --help # that tool's flags"
2446
+ ].join("\n")
2447
+ ).action(async (ref, rest, opts, cmd) => {
2448
+ await runDynamic(cmd, ref, rest, opts);
2449
+ });
2450
+ }
2451
+
2452
+ // src/commands/workflows.ts
2453
+ var isWorkflow = (tool) => tool.kind === "workflow";
2454
+ function workflowSummary(tool) {
2455
+ return shortDescription(tool.description.replace(/^Run workflow '[^']*'\.\s*(Workflow description:\s*)?/i, ""), 120);
2456
+ }
2457
+ function toWorkflowEntry(tool) {
2458
+ const auto = new Set(autoFilledProperties(tool, tool.inputSchema));
2459
+ const required = (tool.inputSchema.required ?? []).filter((key) => !auto.has(key));
2460
+ return {
2461
+ key: tool.action,
2462
+ id: tool.id,
2463
+ name: tool.mcpName,
2464
+ command: invocation(tool),
2465
+ description: workflowSummary(tool),
2466
+ requiredFlags: flagsForProperties(tool.inputSchema, required)
2467
+ };
2468
+ }
2469
+ function formatWorkflowList(entries, width = process.stdout.columns ?? 100) {
2470
+ if (entries.length === 0) return [theme.dim("No workflows are exposed over MCP; multi-step flows run through `tabbio chat`.")];
2471
+ const keyWidth = Math.max(...entries.map((e) => e.key.length));
2472
+ const lines = [`${theme.bold("Workflows")} ${theme.dim(`(${entries.length})`)}`];
2473
+ for (const entry of entries) {
2474
+ const needs = entry.requiredFlags.length ? theme.dim(` needs ${entry.requiredFlags.join(" ")}`) : "";
2475
+ const room = Math.max(20, width - keyWidth - 4 - (entry.requiredFlags.join(" ").length + 8));
2476
+ lines.push(` ${entry.key.padEnd(keyWidth)} ${truncate(entry.description, room)}${needs}`);
2477
+ }
2478
+ lines.push(theme.dim("Run one: tabbio workflows run <key> [flags] \xB7 its flags: tabbio workflows run <key> --help"));
2479
+ return lines;
2480
+ }
2481
+ function registerWorkflowsCommand(program) {
2482
+ const workflows = program.command("workflows").description("List and run multi-step Tabbio workflows");
2483
+ workflows.command("list", { isDefault: true }).description("List the workflows available to you").action(async (_opts, cmd) => {
2484
+ const ctx = createCommandContext(cmd);
2485
+ try {
2486
+ const session = await ctx.mcp();
2487
+ const entries = (await session.listTools()).filter(isWorkflow).map(toWorkflowEntry);
2488
+ const mode2 = resolveOutputMode({ json: ctx.globals.json });
2489
+ if (mode2 === "json") writeJson(entries);
2490
+ else if (entries.length === 0) {
2491
+ printEmptyNotice(mode2, "No workflows are exposed over MCP; multi-step flows run through tabbio chat", { quiet: ctx.globals.quiet });
2492
+ } else if (mode2 === "tsv") {
2493
+ for (const e of entries) process.stdout.write(`${[e.key, e.command, e.description].join(" ")}
2494
+ `);
2495
+ } else for (const line of formatWorkflowList(entries)) process.stdout.write(`${line}
2496
+ `);
2497
+ } finally {
2498
+ await ctx.close();
2499
+ }
2500
+ });
2501
+ const run = workflows.command("run").description("Run a workflow (same flags and output options as `tabbio run`)");
2502
+ configureDynamicRunCommand(run, "key", "Workflow key (tailored-cv-flow) or MCP name (run_tailoredCvFlow)").addHelpText(
2503
+ "after",
2504
+ "\nExamples:\n $ tabbio workflows run tailored-cv-flow --cv-id cv_1 --job-id job_9\n $ tabbio workflows run tailored-cv-flow --help"
2505
+ ).action(async (ref, rest, opts, cmd) => {
2506
+ await runDynamic(cmd, ref, rest, opts, isWorkflow);
2507
+ });
2508
+ }
2509
+
2510
+ // src/ui/index.ts
2511
+ function installInteractiveUi() {
2512
+ const hooks = {
2513
+ async browseTools(ctx, tools, options) {
2514
+ const { runBrowseTools } = await import("./entry-WENOOT6W.js");
2515
+ return runBrowseTools(ctx, tools, options);
2516
+ },
2517
+ async runToolForm(ctx, tool, initial, options) {
2518
+ const { runToolForm } = await import("./entry-WENOOT6W.js");
2519
+ return runToolForm(ctx, tool, initial, options);
2520
+ },
2521
+ async approvalPrompt(ctx, approval) {
2522
+ const { runApprovalPrompt } = await import("./entry-WENOOT6W.js");
2523
+ return runApprovalPrompt(ctx, approval);
2524
+ },
2525
+ async showResult(ctx, tool, result, meta) {
2526
+ const { runShowResult } = await import("./entry-WENOOT6W.js");
2527
+ return runShowResult(ctx, tool, result, meta);
2528
+ }
2529
+ };
2530
+ setInteractiveHooks(hooks);
2531
+ }
2532
+
2533
+ // src/program.ts
2534
+ var PROGRAM_NAME = "tabbio";
2535
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["--profile", "--api-url", "--app-url"]);
2536
+ var CACHED_CATALOG_COMMANDS = /* @__PURE__ */ new Set(["completion", "help"]);
2537
+ var NO_UPDATE_NOTICE = /* @__PURE__ */ new Set(["mcp", "completion"]);
2538
+ var homeHandler = null;
2539
+ function setHomeHandler(handler) {
2540
+ homeHandler = handler;
2541
+ }
2542
+ function createProgram() {
2543
+ const program = new Command2(PROGRAM_NAME).description("Tabbio in your terminal: every Tabbio tool, chat, approvals and an MCP bridge.").version(CLI_VERSION, "-v, --version", "Print the CLI version").option("--profile <name>", "Use a named profile (env TABBIO_PROFILE)").option("--api-url <url>", "Override the API URL (env TABBIO_API_URL)").option("--app-url <url>", "Override the web app URL used for browser sign-in (env TABBIO_APP_URL)").option("--json", "Machine-readable JSON output").option("--color", "Force colors (also FORCE_COLOR / CLICOLOR_FORCE)").option("--no-color", "Disable colors (also NO_COLOR)").option("--debug", "Trace requests to stderr (secrets redacted)").option("-y, --yes", "Non-interactive: assume yes for confirmations").option("-q, --quiet", "Only print results").showHelpAfterError("(add --help for usage)").addHelpText(
2544
+ "after",
2545
+ "\nGet started:\n $ tabbio login\n $ tabbio status\n $ tabbio tools\n\nExit codes: tabbio help exit-codes\nDocs: https://tabbio.com/developers"
2546
+ );
2547
+ program.hook("preAction", (_root, actionCommand) => {
2548
+ const globals = readGlobalOptions(actionCommand);
2549
+ configureTheme({ noColor: !globals.color });
2550
+ });
2551
+ return program;
2552
+ }
2553
+ function registerStaticCommands(program) {
2554
+ registerLoginCommand(program);
2555
+ registerLogoutCommand(program);
2556
+ registerStatusCommand(program);
2557
+ registerWhoamiCommand(program);
2558
+ registerDoctorCommand(program);
2559
+ registerConfigCommands(program);
2560
+ registerMcpCommands(program);
2561
+ registerCompletionCommand(program);
2562
+ registerToolsCommand(program);
2563
+ registerRunCommand(program);
2564
+ registerWorkflowsCommand(program);
2565
+ registerApprovalsCommand(program);
2566
+ registerArtifactsCommand(program);
2567
+ registerAskCommand(program);
2568
+ registerChatCommand(program);
2569
+ registerHome();
2570
+ }
2571
+ function applySettings(command) {
2572
+ command.exitOverride();
2573
+ command.configureOutput({
2574
+ writeOut: (str) => process.stdout.write(str),
2575
+ writeErr: (str) => process.stderr.write(str),
2576
+ outputError: (str, write3) => write3(theme.error(redactSecrets(str)))
2577
+ });
2578
+ for (const sub of command.commands) applySettings(sub);
2579
+ }
2580
+ function scanArgv(args) {
2581
+ const scan = { help: false };
2582
+ for (let i = 0; i < args.length; i += 1) {
2583
+ const arg = args[i];
2584
+ if (arg === "--") break;
2585
+ if (arg === "--help" || arg === "-h") {
2586
+ scan.help = true;
2587
+ continue;
2588
+ }
2589
+ const [flag, inline] = arg.startsWith("--") ? arg.split("=", 2) : [arg];
2590
+ if (VALUE_FLAGS.has(flag)) {
2591
+ const value = inline ?? args[i + 1];
2592
+ if (inline === void 0) i += 1;
2593
+ if (flag === "--profile") scan.profile = value;
2594
+ if (flag === "--api-url") scan.apiUrl = value;
2595
+ if (flag === "--app-url") scan.appUrl = value;
2596
+ continue;
2597
+ }
2598
+ if (arg.startsWith("-")) continue;
2599
+ if (scan.firstCommand === void 0) scan.firstCommand = arg;
2600
+ }
2601
+ return scan;
2602
+ }
2603
+ async function attachCatalog(program, scan) {
2604
+ const staticNames = new Set(program.commands.flatMap((c) => [c.name(), ...c.aliases()]));
2605
+ const first = scan.firstCommand;
2606
+ if (first && staticNames.has(first) && !CACHED_CATALOG_COMMANDS.has(first)) return;
2607
+ let profile;
2608
+ try {
2609
+ profile = resolveProfile({ profile: scan.profile, apiUrl: scan.apiUrl, appUrl: scan.appUrl });
2610
+ } catch {
2611
+ return;
2612
+ }
2613
+ const creds = loadCredentials(profile.name);
2614
+ if (!selectMcpBearer(creds)) return;
2615
+ const cached = readCachedCatalog(profile, creds);
2616
+ const cacheOnly = !first || CACHED_CATALOG_COMMANDS.has(first);
2617
+ if (cacheOnly || cached?.fresh) {
2618
+ if (cached) registerGeneratedCommands(program, cached.tools);
2619
+ return;
2620
+ }
2621
+ let session = null;
2622
+ try {
2623
+ session = await McpSession.connect(profile, creds);
2624
+ registerGeneratedCommands(program, await session.listTools());
2625
+ } catch (error) {
2626
+ if (!cached) throw error;
2627
+ debug(`catalog refresh failed, using stale cache: ${error.message}`);
2628
+ registerGeneratedCommands(program, cached.tools);
2629
+ } finally {
2630
+ await session?.close();
2631
+ }
2632
+ }
2633
+ function formatErrorLines(error, opts = {}) {
2634
+ const lines = [`${theme.error(`${theme.symbols.error} ${redactSecrets(error.message)}`)}`];
2635
+ if (error.hint) lines.push(` ${theme.dim(redactSecrets(error.hint))}`);
2636
+ if (error.requestId) lines.push(` ${theme.dim(`request id: ${error.requestId}`)}`);
2637
+ if (opts.debug) {
2638
+ const cause = error.cause instanceof Error ? error.cause : error;
2639
+ if (cause.stack) lines.push(theme.dim(redactSecrets(cause.stack)));
2640
+ }
2641
+ return lines;
2642
+ }
2643
+ function handleCliError(error) {
2644
+ if (error instanceof CommanderError2) {
2645
+ if (error.exitCode === 0) return ExitCode.Ok;
2646
+ if (error.code === "commander.unknownCommand" && !getGlobalOptions().json) {
2647
+ process.stderr.write(
2648
+ `${theme.dim(" Tool commands (e.g. `tabbio cv list`) load from your account: run `tabbio login`, then `tabbio tools`.")}
2649
+ `
2650
+ );
2651
+ }
2652
+ return ExitCode.Usage;
2653
+ }
2654
+ const globals = getGlobalOptions();
2655
+ const cliError = toCliError(error);
2656
+ if (globals.json) {
2657
+ process.stderr.write(`${JSON.stringify({ error: cliError.toJSON() })}
2658
+ `);
2659
+ } else if (isCliError(error)) {
2660
+ for (const line of formatErrorLines(cliError, { debug: globals.debug })) process.stderr.write(`${line}
2661
+ `);
2662
+ } else {
2663
+ const message = error instanceof Error ? error.message : String(error);
2664
+ process.stderr.write(`${theme.error(`${theme.symbols.error} Unexpected error: ${redactSecrets(message)}`)}
2665
+ `);
2666
+ if (globals.debug && error instanceof Error && error.stack) {
2667
+ process.stderr.write(`${theme.dim(redactSecrets(error.stack))}
2668
+ `);
2669
+ } else {
2670
+ process.stderr.write(` ${theme.dim("Re-run with --debug for details.")}
2671
+ `);
2672
+ }
2673
+ }
2674
+ return cliError.exitCode;
2675
+ }
2676
+ function printExitCodes(json) {
2677
+ if (json) {
2678
+ printJson(EXIT_CODE_DOCS);
2679
+ return;
2680
+ }
2681
+ writeOut(theme.bold("Exit codes"));
2682
+ for (const doc of EXIT_CODE_DOCS) {
2683
+ writeOut(` ${String(doc.code).padStart(3)} ${doc.name.padEnd(16)}${theme.dim(doc.meaning)}`);
2684
+ }
2685
+ }
2686
+ async function maybeNotifyUpdate(scan) {
2687
+ const globals = getGlobalOptions();
2688
+ if (globals.json || globals.quiet || NO_UPDATE_NOTICE.has(scan.firstCommand ?? "")) return;
2689
+ const latest = await checkForUpdate().catch(() => null);
2690
+ if (latest) {
2691
+ process.stderr.write(
2692
+ `
2693
+ ${theme.accent(`Update available ${CLI_VERSION} \u2192 ${latest}`)} ${theme.dim(`npm i -g ${CLI_PACKAGE_NAME}`)}
2694
+ `
2695
+ );
2696
+ }
2697
+ }
2698
+ async function runCli(argv = process.argv) {
2699
+ const args = argv.slice(2);
2700
+ const scan = scanArgv(args);
2701
+ setGlobalOptions({
2702
+ json: args.includes("--json"),
2703
+ debug: args.includes("--debug") || getGlobalOptions().debug,
2704
+ color: !args.includes("--no-color"),
2705
+ quiet: args.includes("--quiet") || args.includes("-q")
2706
+ });
2707
+ configureTheme({ noColor: args.includes("--no-color") });
2708
+ const positionals = args.filter((a) => !a.startsWith("-"));
2709
+ if (positionals[0] === "help" && positionals[1] === "exit-codes") {
2710
+ printExitCodes(args.includes("--json"));
2711
+ return ExitCode.Ok;
2712
+ }
2713
+ const program = createProgram();
2714
+ try {
2715
+ installInteractiveUi();
2716
+ registerStaticCommands(program);
2717
+ await attachCatalog(program, scan);
2718
+ applySettings(program);
2719
+ const bare = scan.firstCommand === void 0 && !scan.help && !args.some((a) => a === "-v" || a === "--version");
2720
+ const unknownFlags = bare ? program.parseOptions(args).unknown : [];
2721
+ if (bare && unknownFlags.length === 0) {
2722
+ const ctx = createCommandContext(program);
2723
+ if (homeHandler && isInteractive() && !ctx.globals.json) {
2724
+ try {
2725
+ await homeHandler(ctx);
2726
+ } finally {
2727
+ await ctx.close();
2728
+ }
2729
+ } else {
2730
+ program.outputHelp();
2731
+ }
2732
+ return ExitCode.Ok;
2733
+ }
2734
+ await program.parseAsync(argv);
2735
+ if (activeCommandSignal.aborted) return ExitCode.Interrupted;
2736
+ const code = typeof process.exitCode === "number" ? process.exitCode : ExitCode.Ok;
2737
+ if (code === ExitCode.Ok) await maybeNotifyUpdate(scan);
2738
+ return code;
2739
+ } catch (error) {
2740
+ if (activeCommandSignal.aborted) return ExitCode.Interrupted;
2741
+ return handleCliError(error);
2742
+ }
2743
+ }
2744
+ export {
2745
+ PROGRAM_NAME,
2746
+ createProgram,
2747
+ formatErrorLines,
2748
+ handleCliError,
2749
+ registerGeneratedCommands,
2750
+ registerStaticCommands,
2751
+ runCli,
2752
+ scanArgv,
2753
+ setHomeHandler
2754
+ };
2755
+ //# sourceMappingURL=program-O2BA5L6Z.js.map