@tabbio-technologies/cli 1.2.8 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,178 +1,54 @@
1
1
  import { createRequire as __tabbioCreateRequire } from 'node:module';
2
2
  const require = __tabbioCreateRequire(import.meta.url);
3
3
  import {
4
+ displayWidth,
5
+ padEnd,
6
+ safeTerminalText,
7
+ truncate,
8
+ wrapWords
9
+ } from "./chunk-NRFVKK3S.js";
10
+ import {
11
+ ApiClient,
4
12
  CliError,
5
13
  ExitCode,
14
+ McpSession,
15
+ PRODUCTION_APP_URL,
16
+ chooseLoginMethod,
17
+ clearCatalogCache,
18
+ combineSignals,
19
+ createCommandContext,
20
+ credentialsFromLogin,
6
21
  debug,
22
+ deviceLabel,
23
+ fingerprint,
24
+ hasDisplay,
25
+ info,
7
26
  interruptedError,
27
+ isCliError,
8
28
  kebabCase,
9
29
  loadConfig,
30
+ loadStoredCredentials,
31
+ loginWithBrowser,
32
+ loginWithEmailOtp,
10
33
  networkError,
11
34
  parseJsonResponse,
35
+ printJson,
12
36
  relativeTime,
37
+ revokeSession,
38
+ saveCredentials,
39
+ successLine,
13
40
  updateConfig,
14
- usageError
15
- } from "./chunk-7LNC3FIV.js";
41
+ usageError,
42
+ warn,
43
+ writeOut
44
+ } from "./chunk-XR6APSWD.js";
45
+ import {
46
+ activeCommandSignal
47
+ } from "./chunk-NK5RPNSV.js";
16
48
  import {
17
49
  theme
18
50
  } from "./chunk-VHHZFMIF.js";
19
51
 
20
- // src/core/chat-model.ts
21
- import { marked } from "marked";
22
-
23
- // src/core/capabilities.ts
24
- var TOGGLE_MODES = ["auto", "on", "off"];
25
- var RESULT_MODES = ["auto", "summary", "document", "slides", "page", "image"];
26
- var DEFAULT_CAPABILITIES = { research: "auto", image: "auto", result: "auto" };
27
- var CAPABILITY_CHOICES = {
28
- research: TOGGLE_MODES,
29
- image: TOGGLE_MODES,
30
- result: RESULT_MODES
31
- };
32
- var CAPABILITY_CHOICE_HELP = {
33
- research: { auto: "Search the web when it helps", on: "Always search first", off: "Never search the web" },
34
- image: { auto: "Only when you ask for one", on: "Make an image (uses image credits)", off: "Never make images" },
35
- result: {
36
- auto: "Let the message decide",
37
- summary: "Answer in the chat",
38
- document: "A document (HTML and PDF)",
39
- slides: "A slide deck (PPTX)",
40
- page: "A web page (HTML)",
41
- image: "An image"
42
- }
43
- };
44
- function isToggle(value) {
45
- return typeof value === "string" && TOGGLE_MODES.includes(value);
46
- }
47
- function isResult(value) {
48
- return typeof value === "string" && RESULT_MODES.includes(value);
49
- }
50
- function parseCapabilityMode(key, value) {
51
- const normalized = value.trim().toLowerCase();
52
- const alias = key === "result" && (normalized === "doc" || normalized === "pdf") ? "document" : key === "result" && normalized === "deck" ? "slides" : normalized;
53
- if (key === "result" ? isResult(alias) : isToggle(alias)) return alias;
54
- throw usageError(`--${key} must be one of ${CAPABILITY_CHOICES[key].join(", ")}; got "${value}"`);
55
- }
56
- function sanitizeCapabilities(raw) {
57
- const record = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
58
- return {
59
- research: isToggle(record.research) ? record.research : DEFAULT_CAPABILITIES.research,
60
- image: isToggle(record.image) ? record.image : DEFAULT_CAPABILITIES.image,
61
- result: isResult(record.result) ? record.result : DEFAULT_CAPABILITIES.result
62
- };
63
- }
64
- function loadCapabilityPrefs(profileName) {
65
- try {
66
- const stored = loadConfig().profiles[profileName];
67
- return sanitizeCapabilities(stored?.capabilities);
68
- } catch {
69
- return { ...DEFAULT_CAPABILITIES };
70
- }
71
- }
72
- function saveCapabilityPrefs(profileName, capabilities) {
73
- updateConfig((config) => {
74
- config.profiles[profileName] = { ...config.profiles[profileName], capabilities: { ...capabilities } };
75
- });
76
- }
77
- function resolveCapabilities(stored, flags = {}) {
78
- return {
79
- research: flags.research !== void 0 ? parseCapabilityMode("research", flags.research) : stored.research,
80
- image: flags.image !== void 0 ? parseCapabilityMode("image", flags.image) : stored.image,
81
- result: flags.result !== void 0 ? parseCapabilityMode("result", flags.result) : stored.result
82
- };
83
- }
84
- function toWireCapabilities(capabilities) {
85
- return { research: capabilities.research, image: capabilities.image, artifact: capabilities.result };
86
- }
87
- function deviceTimeZone() {
88
- try {
89
- const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
90
- return typeof zone === "string" && zone ? zone : void 0;
91
- } catch {
92
- return void 0;
93
- }
94
- }
95
- function capabilityRequestFields(mode, capabilities, timezone = deviceTimeZone()) {
96
- return {
97
- ...mode === "employer" ? {} : { capabilities: toWireCapabilities(capabilities) },
98
- ...timezone ? { timezone } : {}
99
- };
100
- }
101
- var REASON_TEXT = {
102
- disabled: "switched off on this server",
103
- not_allowlisted: "not enabled for this account yet (canary or plan tier)",
104
- not_configured: "the provider is not configured on this server",
105
- upgrade_required: "needs a paid plan"
106
- };
107
- function reasonText(reason) {
108
- return reason && REASON_TEXT[reason] || reason || "unavailable";
109
- }
110
- function unavailableNotices(capabilities, availability) {
111
- if (!availability) return [];
112
- const notices = [];
113
- if (capabilities.research === "on" && !availability.research.available) {
114
- notices.push(`Research is unavailable: ${reasonText(availability.research.reason)}. This message runs without it.`);
115
- }
116
- const wantsImage = capabilities.image === "on" || capabilities.result === "image";
117
- if (wantsImage && !availability.image.available) {
118
- notices.push(`Images are unavailable: ${reasonText(availability.image.reason)}. Tabbio answers in the chat instead.`);
119
- }
120
- const wantsFile = capabilities.result === "document" || capabilities.result === "slides" || capabilities.result === "page";
121
- if (wantsFile && !availability.artifacts.available) {
122
- notices.push(`Documents, slides and pages are unavailable: ${reasonText(availability.artifacts.reason)}. Tabbio answers in the chat instead.`);
123
- }
124
- return notices;
125
- }
126
-
127
- // src/core/chat-model.ts
128
- function initialChatState(opts) {
129
- return {
130
- mode: opts.mode,
131
- threadId: opts.threadId,
132
- modelId: opts.modelId,
133
- entries: [],
134
- turn: null,
135
- approvals: [],
136
- streaming: false,
137
- seq: 0,
138
- capabilities: opts.capabilities ?? { ...DEFAULT_CAPABILITIES },
139
- links: opts.links ?? false
140
- };
141
- }
142
- var FINAL_STAGES = /* @__PURE__ */ new Set(["completed", "failed", "declined"]);
143
- function toolTitle(payload) {
144
- const raw = payload.display?.title?.trim() || payload.name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
145
- return raw.split(/\s+/).map((w, i) => i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : /^[A-Z][a-z]/.test(w) ? w.toLowerCase() : w).join(" ");
146
- }
147
- function stageOf(payload, fallback) {
148
- const stage = payload.stage ?? payload.status;
149
- if (stage === "completed" || stage === "failed" || stage === "declined" || stage === "running") return stage;
150
- if (stage === "pending_approval" || stage === "pending" || stage === "requires_approval") return "pending_approval";
151
- return fallback;
152
- }
153
- function stableBoundary(text) {
154
- let tokens;
155
- try {
156
- tokens = marked.lexer(text, { gfm: true });
157
- } catch {
158
- return 0;
159
- }
160
- let lastContent = -1;
161
- tokens.forEach((token, i) => {
162
- if (token.type !== "space") lastContent = i;
163
- });
164
- if (lastContent <= 0) return 0;
165
- const prefix = tokens.slice(0, lastContent).map((t) => t.raw).join("");
166
- if (!prefix || !text.startsWith(prefix) || !prefix.endsWith("\n")) return 0;
167
- return prefix.length;
168
- }
169
- function normalizeText(text) {
170
- return text.replace(/\s+/g, " ").trim();
171
- }
172
- function isDestructiveTool(name) {
173
- return /(delete|remove|revoke|withdraw|unpublish|send|close|reject|archive)/i.test(name);
174
- }
175
-
176
52
  // src/core/schema-flags.ts
177
53
  import { Option } from "commander";
178
54
 
@@ -545,6 +421,167 @@ function exampleInvocation(tool, schema, commandPath = tool.commandPath) {
545
421
  return parts.join(" ");
546
422
  }
547
423
 
424
+ // src/core/chat-model.ts
425
+ import { marked } from "marked";
426
+
427
+ // src/core/capabilities.ts
428
+ var TOGGLE_MODES = ["auto", "on", "off"];
429
+ var RESULT_MODES = ["auto", "summary", "document", "slides", "page", "image"];
430
+ var DEFAULT_CAPABILITIES = { research: "auto", image: "auto", result: "auto" };
431
+ var CAPABILITY_CHOICES = {
432
+ research: TOGGLE_MODES,
433
+ image: TOGGLE_MODES,
434
+ result: RESULT_MODES
435
+ };
436
+ var CAPABILITY_CHOICE_HELP = {
437
+ research: { auto: "Search the web when it helps", on: "Always search first", off: "Never search the web" },
438
+ image: { auto: "Only when you ask for one", on: "Make an image (uses image credits)", off: "Never make images" },
439
+ result: {
440
+ auto: "Let the message decide",
441
+ summary: "Answer in the chat",
442
+ document: "A document (HTML and PDF)",
443
+ slides: "A slide deck (PPTX)",
444
+ page: "A web page (HTML)",
445
+ image: "An image"
446
+ }
447
+ };
448
+ function isToggle(value) {
449
+ return typeof value === "string" && TOGGLE_MODES.includes(value);
450
+ }
451
+ function isResult(value) {
452
+ return typeof value === "string" && RESULT_MODES.includes(value);
453
+ }
454
+ function parseCapabilityMode(key, value) {
455
+ const normalized = value.trim().toLowerCase();
456
+ const alias = key === "result" && (normalized === "doc" || normalized === "pdf") ? "document" : key === "result" && normalized === "deck" ? "slides" : normalized;
457
+ if (key === "result" ? isResult(alias) : isToggle(alias)) return alias;
458
+ throw usageError(`--${key} must be one of ${CAPABILITY_CHOICES[key].join(", ")}; got "${value}"`);
459
+ }
460
+ function sanitizeCapabilities(raw) {
461
+ const record = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
462
+ return {
463
+ research: isToggle(record.research) ? record.research : DEFAULT_CAPABILITIES.research,
464
+ image: isToggle(record.image) ? record.image : DEFAULT_CAPABILITIES.image,
465
+ result: isResult(record.result) ? record.result : DEFAULT_CAPABILITIES.result
466
+ };
467
+ }
468
+ function loadCapabilityPrefs(profileName) {
469
+ try {
470
+ const stored = loadConfig().profiles[profileName];
471
+ return sanitizeCapabilities(stored?.capabilities);
472
+ } catch {
473
+ return { ...DEFAULT_CAPABILITIES };
474
+ }
475
+ }
476
+ function saveCapabilityPrefs(profileName, capabilities) {
477
+ updateConfig((config) => {
478
+ config.profiles[profileName] = { ...config.profiles[profileName], capabilities: { ...capabilities } };
479
+ });
480
+ }
481
+ function resolveCapabilities(stored, flags = {}) {
482
+ return {
483
+ research: flags.research !== void 0 ? parseCapabilityMode("research", flags.research) : stored.research,
484
+ image: flags.image !== void 0 ? parseCapabilityMode("image", flags.image) : stored.image,
485
+ result: flags.result !== void 0 ? parseCapabilityMode("result", flags.result) : stored.result
486
+ };
487
+ }
488
+ function toWireCapabilities(capabilities) {
489
+ return { research: capabilities.research, image: capabilities.image, artifact: capabilities.result };
490
+ }
491
+ function deviceTimeZone() {
492
+ try {
493
+ const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
494
+ return typeof zone === "string" && zone ? zone : void 0;
495
+ } catch {
496
+ return void 0;
497
+ }
498
+ }
499
+ function capabilityRequestFields(mode, capabilities, timezone = deviceTimeZone()) {
500
+ return {
501
+ ...mode === "employer" ? {} : { capabilities: toWireCapabilities(capabilities) },
502
+ ...timezone ? { timezone } : {}
503
+ };
504
+ }
505
+ var REASON_TEXT = {
506
+ disabled: "switched off on this server",
507
+ not_allowlisted: "not enabled for this account yet (canary or plan tier)",
508
+ not_configured: "the provider is not configured on this server",
509
+ upgrade_required: "needs a paid plan"
510
+ };
511
+ function reasonText(reason) {
512
+ return reason && REASON_TEXT[reason] || reason || "unavailable";
513
+ }
514
+ function unavailableNotices(capabilities, availability) {
515
+ if (!availability) return [];
516
+ const notices = [];
517
+ if (capabilities.research === "on" && !availability.research.available) {
518
+ notices.push(`Research is unavailable: ${reasonText(availability.research.reason)}. This message runs without it.`);
519
+ }
520
+ const wantsImage = capabilities.image === "on" || capabilities.result === "image";
521
+ if (wantsImage && !availability.image.available) {
522
+ notices.push(`Images are unavailable: ${reasonText(availability.image.reason)}. Tabbio answers in the chat instead.`);
523
+ }
524
+ const wantsFile = capabilities.result === "document" || capabilities.result === "slides" || capabilities.result === "page";
525
+ if (wantsFile && !availability.artifacts.available) {
526
+ notices.push(`Documents, slides and pages are unavailable: ${reasonText(availability.artifacts.reason)}. Tabbio answers in the chat instead.`);
527
+ }
528
+ return notices;
529
+ }
530
+
531
+ // src/core/chat-model.ts
532
+ function initialChatState(opts) {
533
+ return {
534
+ mode: opts.mode,
535
+ threadId: opts.threadId,
536
+ modelId: opts.modelId,
537
+ entries: [],
538
+ turn: null,
539
+ approvals: [],
540
+ streaming: false,
541
+ seq: 0,
542
+ capabilities: opts.capabilities ?? { ...DEFAULT_CAPABILITIES },
543
+ links: opts.links ?? false,
544
+ ...opts.appUrl ? { appUrl: opts.appUrl } : {}
545
+ };
546
+ }
547
+ var HIDDEN_TOOL_NAMES = /* @__PURE__ */ new Set(["updateWorkingMemory", "getWorkingMemory", "clearWorkingMemory"]);
548
+ function isHiddenTool(name) {
549
+ return HIDDEN_TOOL_NAMES.has(name);
550
+ }
551
+ var FINAL_STAGES = /* @__PURE__ */ new Set(["completed", "failed", "declined"]);
552
+ function toolTitle(payload) {
553
+ const raw = payload.display?.title?.trim() || payload.name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
554
+ return raw.split(/\s+/).map((w, i) => i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : /^[A-Z][a-z]/.test(w) ? w.toLowerCase() : w).join(" ");
555
+ }
556
+ function stageOf(payload, fallback) {
557
+ const stage = payload.stage ?? payload.status;
558
+ if (stage === "completed" || stage === "failed" || stage === "declined" || stage === "running") return stage;
559
+ if (stage === "pending_approval" || stage === "pending" || stage === "requires_approval") return "pending_approval";
560
+ return fallback;
561
+ }
562
+ function stableBoundary(text) {
563
+ let tokens;
564
+ try {
565
+ tokens = marked.lexer(text, { gfm: true });
566
+ } catch {
567
+ return 0;
568
+ }
569
+ let lastContent = -1;
570
+ tokens.forEach((token, i) => {
571
+ if (token.type !== "space") lastContent = i;
572
+ });
573
+ if (lastContent <= 0) return 0;
574
+ const prefix = tokens.slice(0, lastContent).map((t) => t.raw).join("");
575
+ if (!prefix || !text.startsWith(prefix) || !prefix.endsWith("\n")) return 0;
576
+ return prefix.length;
577
+ }
578
+ function normalizeText(text) {
579
+ return text.replace(/\s+/g, " ").trim();
580
+ }
581
+ function isDestructiveTool(name) {
582
+ return /(delete|remove|revoke|withdraw|unpublish|send|close|reject|archive)/i.test(name);
583
+ }
584
+
548
585
  // src/ui/render.ts
549
586
  function isInteractiveTerminal(stdin = process.stdin, stdout = process.stdout) {
550
587
  return Boolean(stdin.isTTY && stdout.isTTY);
@@ -558,10 +595,250 @@ function assertInteractive(what = "This screen", stdin = process.stdin, stdout =
558
595
  exitCode: ExitCode.Usage
559
596
  });
560
597
  }
561
- async function renderScreen(what, build, opts = {}) {
562
- assertInteractive(what);
563
- const { mountScreen } = await import("./mount-NGUARMCL.js");
564
- return mountScreen(build, opts);
598
+ async function renderScreen(what, build, opts = {}) {
599
+ assertInteractive(what);
600
+ const { mountScreen } = await import("./mount-36JUVX3R.js");
601
+ return mountScreen(build, opts);
602
+ }
603
+
604
+ // src/commands/login.ts
605
+ import open from "open";
606
+
607
+ // src/core/prompt.ts
608
+ import { createInterface } from "node:readline";
609
+ function cancelled() {
610
+ return interruptedError();
611
+ }
612
+ function promptLine(question) {
613
+ return new Promise((resolve2, reject) => {
614
+ const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: Boolean(process.stdin.isTTY) });
615
+ let answered = false;
616
+ rl.once("SIGINT", () => {
617
+ rl.close();
618
+ reject(cancelled());
619
+ });
620
+ rl.once("close", () => {
621
+ if (!answered) reject(usageError("No input received"));
622
+ });
623
+ rl.question(question, (answer) => {
624
+ answered = true;
625
+ rl.close();
626
+ resolve2(answer.trim());
627
+ });
628
+ });
629
+ }
630
+ function promptSecret(question) {
631
+ const stdin = process.stdin;
632
+ if (!stdin.isTTY || typeof stdin.setRawMode !== "function") return promptLine(question);
633
+ return new Promise((resolve2, reject) => {
634
+ let value = "";
635
+ process.stderr.write(question);
636
+ const wasRaw = stdin.isRaw;
637
+ stdin.setRawMode(true);
638
+ stdin.resume();
639
+ stdin.setEncoding("utf8");
640
+ const finish = (error) => {
641
+ stdin.off("data", onData);
642
+ stdin.setRawMode(wasRaw);
643
+ stdin.pause();
644
+ process.stderr.write("\n");
645
+ if (error) reject(error);
646
+ else resolve2(value.trim());
647
+ };
648
+ const onData = (chunk) => {
649
+ for (const char of chunk) {
650
+ if (char === "") return finish(cancelled());
651
+ if (char === "" && value === "") return finish(cancelled());
652
+ if (char === "\r" || char === "\n") return finish();
653
+ if (char === "\x7F" || char === "\b") {
654
+ if (value.length > 0) {
655
+ value = value.slice(0, -1);
656
+ process.stderr.write("\b \b");
657
+ }
658
+ continue;
659
+ }
660
+ if (char < " ") continue;
661
+ value += char;
662
+ process.stderr.write("\u2022");
663
+ }
664
+ };
665
+ stdin.on("data", onData);
666
+ });
667
+ }
668
+ async function confirm(question, opts = {}) {
669
+ if (opts.assumeYes) return true;
670
+ if (!process.stdin.isTTY) {
671
+ throw usageError(`${question} (needs confirmation)`, "Re-run with --yes to confirm non-interactively.");
672
+ }
673
+ const suffix = opts.defaultYes ? " [Y/n] " : " [y/N] ";
674
+ const answer = (await promptLine(`${question}${suffix}`)).toLowerCase();
675
+ if (!answer) return Boolean(opts.defaultYes);
676
+ return answer === "y" || answer === "yes";
677
+ }
678
+
679
+ // src/commands/login.ts
680
+ async function readTokenFromStdin(stdin = process.stdin) {
681
+ if (stdin.isTTY) {
682
+ throw usageError("--with-token reads the token from stdin", "Pipe it in: `tabbio login --with-token < token.txt`.");
683
+ }
684
+ const chunks = [];
685
+ for await (const chunk of stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
686
+ const token = Buffer.concat(chunks).toString("utf8").trim();
687
+ if (!token) throw usageError("No token on stdin");
688
+ return token;
689
+ }
690
+ function persistProfileUrls(profile, sources) {
691
+ if (sources.apiUrl !== "flag" && sources.appUrl !== "flag") return;
692
+ updateConfig((config) => {
693
+ const current = config.profiles[profile.name] ?? {};
694
+ config.profiles[profile.name] = {
695
+ ...current,
696
+ ...sources.apiUrl === "flag" ? { apiUrl: profile.apiUrl } : {},
697
+ ...sources.appUrl === "flag" ? { appUrl: profile.appUrl } : {}
698
+ };
699
+ });
700
+ }
701
+ async function revokePrevious(profile, refreshToken, keep, fetchImpl) {
702
+ if (!refreshToken || refreshToken === keep) return;
703
+ await revokeSession(new ApiClient(profile, null, { persist: false, fetch: fetchImpl }), refreshToken).catch(
704
+ (error) => debug(`previous session not revoked: ${error.message}`)
705
+ );
706
+ }
707
+ async function saveAppSession(ctx, payload, opts = {}) {
708
+ const previous = loadStoredCredentials(ctx.profile.name);
709
+ const creds = credentialsFromLogin(ctx.profile, payload);
710
+ saveCredentials(creds);
711
+ clearCatalogCache(ctx.profile);
712
+ persistProfileUrls(ctx.profile, ctx.sources);
713
+ await revokePrevious(ctx.profile, previous?.refreshToken, payload.refreshToken, opts.fetch);
714
+ return creds;
715
+ }
716
+ async function saveTokenSession(ctx, rawToken, opts = {}) {
717
+ const token = rawToken.trim();
718
+ if (!token) throw usageError("No token entered");
719
+ const probe = { profile: ctx.profile.name, mcpToken: token };
720
+ const session = await McpSession.connect(ctx.profile, probe, opts.fetch ? { fetch: opts.fetch } : {});
721
+ let toolCount = 0;
722
+ let visibleTools = 0;
723
+ try {
724
+ const tools = await session.listTools({ refresh: true });
725
+ toolCount = tools.length;
726
+ visibleTools = tools.filter((t) => !t.hidden).length;
727
+ } finally {
728
+ await session.close();
729
+ }
730
+ const previous = loadStoredCredentials(ctx.profile.name);
731
+ saveCredentials(probe);
732
+ persistProfileUrls(ctx.profile, ctx.sources);
733
+ await revokePrevious(ctx.profile, previous?.refreshToken, void 0, opts.fetch);
734
+ return { token, toolCount, visibleTools };
735
+ }
736
+ function describeUser(user) {
737
+ if (!user) return "your account";
738
+ return user.name ? `${user.name} <${user.email}>` : user.email;
739
+ }
740
+ async function loginWithToken(ctx, rawToken) {
741
+ if (!rawToken.trim().startsWith("tabbio_mcp_")) {
742
+ warn("This is not a tabbio_mcp_ token; treating it as an OAuth access token.");
743
+ }
744
+ const { token, toolCount } = await saveTokenSession(ctx, rawToken);
745
+ if (ctx.globals.json) {
746
+ printJson({
747
+ profile: ctx.profile.name,
748
+ method: "token",
749
+ user: null,
750
+ mcp: { kind: "personal-token", token: fingerprint(token) },
751
+ tools: toolCount
752
+ });
753
+ return;
754
+ }
755
+ writeOut(successLine(`Token accepted \xB7 ${toolCount} tools available \xB7 profile ${ctx.profile.name}`));
756
+ writeOut(theme.dim(" MCP-only mode: `tabbio chat` and approving actions need `tabbio login` (browser or --email)."));
757
+ }
758
+ async function obtainSession(ctx, method, opts) {
759
+ const api = new ApiClient(ctx.profile, null, { persist: false });
760
+ if (method === "email") {
761
+ const email = opts.email?.trim() || await promptLine("Email: ");
762
+ info(`Sending a sign-in code to ${email}\u2026`);
763
+ return loginWithEmailOtp(api, {
764
+ email,
765
+ promptCode: (attempt) => promptSecret(attempt === 1 ? "Enter the 6-digit code: " : "Try the code again: "),
766
+ onInvalidCode: (left) => warn(`That code did not match. ${left} attempt${left === 1 ? "" : "s"} left.`)
767
+ });
768
+ }
769
+ const agentMode = Boolean(opts.nonInteractive);
770
+ if (!agentMode) info(`Connecting this computer (${deviceLabel()}) to Tabbio\u2026`);
771
+ const controller = new AbortController();
772
+ const onSigint = () => controller.abort();
773
+ process.once("SIGINT", onSigint);
774
+ try {
775
+ return await loginWithBrowser(api, {
776
+ device: deviceLabel(),
777
+ signal: controller.signal,
778
+ openUrl: async (url) => {
779
+ if (agentMode) return false;
780
+ try {
781
+ await open(url);
782
+ return true;
783
+ } catch {
784
+ return false;
785
+ }
786
+ },
787
+ onWaiting: (url, opened, details) => {
788
+ if (agentMode) {
789
+ printJson({ url, state: details.state, expiresAt: details.expiresAt });
790
+ return;
791
+ }
792
+ info(opened ? theme.dim("If your browser did not open, visit:") : "Open this URL to continue:");
793
+ info(` ${theme.accent(url)}`);
794
+ info(theme.dim("Waiting for you to approve in the browser\u2026 (Ctrl-C to cancel)"));
795
+ }
796
+ });
797
+ } finally {
798
+ process.off("SIGINT", onSigint);
799
+ }
800
+ }
801
+ async function runLogin(opts, cmd) {
802
+ const ctx = createCommandContext(cmd);
803
+ if (opts.nonInteractive) ctx.globals.json = true;
804
+ const method = chooseLoginMethod({
805
+ browser: opts.browser,
806
+ email: opts.email,
807
+ token: opts.token,
808
+ withToken: opts.withToken,
809
+ nonInteractive: opts.nonInteractive,
810
+ isTTY: Boolean(process.stdin.isTTY && process.stderr.isTTY),
811
+ hasDisplay: hasDisplay()
812
+ });
813
+ if (method === "token") {
814
+ return loginWithToken(ctx, opts.withToken ? await readTokenFromStdin() : opts.token ?? "");
815
+ }
816
+ const payload = await obtainSession(ctx, method, opts);
817
+ const creds = await saveAppSession(ctx, payload);
818
+ if (ctx.globals.json) {
819
+ printJson({
820
+ profile: ctx.profile.name,
821
+ method,
822
+ apiUrl: ctx.profile.apiUrl,
823
+ user: creds.user ?? null,
824
+ mcp: { kind: "app-session" }
825
+ });
826
+ return;
827
+ }
828
+ writeOut(successLine(`Signed in as ${theme.bold(describeUser(creds.user))} \xB7 profile ${ctx.profile.name}`));
829
+ if (process.env.TABBIO_TOKEN) {
830
+ warn("TABBIO_TOKEN is set, so tool commands keep using that personal token.");
831
+ }
832
+ writeOut(theme.dim(" Next: tabbio (home) \xB7 tabbio chat \xB7 tabbio tools \xB7 tabbio status"));
833
+ }
834
+ function registerLoginCommand(program) {
835
+ 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(
836
+ "--non-interactive",
837
+ 'For agents: print {"url","state","expiresAt"} as JSON, wait for the browser approval, never prompt'
838
+ ).addHelpText(
839
+ "after",
840
+ "\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)."
841
+ ).action(async (opts, cmd) => runLogin(opts, cmd));
565
842
  }
566
843
 
567
844
  // src/ui/hooks.ts
@@ -594,7 +871,7 @@ var ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
594
871
  function isRowList(value) {
595
872
  return Array.isArray(value) && value.length > 0 && value.every(isRecord3);
596
873
  }
597
- function displayWidth(text) {
874
+ function displayWidth2(text) {
598
875
  let width = 0;
599
876
  for (const char of text) {
600
877
  const code = char.codePointAt(0) ?? 0;
@@ -605,14 +882,14 @@ function displayWidth(text) {
605
882
  }
606
883
  return width;
607
884
  }
608
- function truncate(text, max) {
609
- if (displayWidth(text) <= max) return text;
885
+ function truncate2(text, max) {
886
+ if (displayWidth2(text) <= max) return text;
610
887
  const ellipsis = theme.symbols.ellipsis;
611
- const budget = Math.max(0, max - displayWidth(ellipsis));
888
+ const budget = Math.max(0, max - displayWidth2(ellipsis));
612
889
  let out = "";
613
890
  let width = 0;
614
891
  for (const char of text) {
615
- const w = displayWidth(char);
892
+ const w = displayWidth2(char);
616
893
  if (width + w > budget) break;
617
894
  out += char;
618
895
  width += w;
@@ -620,7 +897,7 @@ function truncate(text, max) {
620
897
  return `${out}${ellipsis}`;
621
898
  }
622
899
  function pad(text, width, align) {
623
- const fill = " ".repeat(Math.max(0, width - displayWidth(text)));
900
+ const fill = " ".repeat(Math.max(0, width - displayWidth2(text)));
624
901
  return align === "right" ? `${fill}${text}` : `${text}${fill}`;
625
902
  }
626
903
  function compactJson(value) {
@@ -724,10 +1001,10 @@ function formatTable(rows, opts = {}) {
724
1001
  const columns = pickColumns(rows, opts.fields);
725
1002
  if (columns.length === 0) return rows.map((r) => compactJson(r));
726
1003
  const cells = rows.map((row) => columns.map((c) => humanCell(getPath(row, c.key), opts.now)));
727
- const natural = columns.map((c, i) => Math.max(displayWidth(c.label), ...cells.map((r) => displayWidth(r[i]))));
1004
+ const natural = columns.map((c, i) => Math.max(displayWidth2(c.label), ...cells.map((r) => displayWidth2(r[i]))));
728
1005
  const widths = fitWidths(natural, opts.width);
729
1006
  const line = (values, style) => values.map((v, i) => {
730
- const text = pad(truncate(v, widths[i]), widths[i], columns[i].align);
1007
+ const text = pad(truncate2(v, widths[i]), widths[i], columns[i].align);
731
1008
  return style ? style(text) : text;
732
1009
  }).join(" ").trimEnd();
733
1010
  return [line(columns.map((c) => c.label), theme.dim), ...cells.map((r) => line(r))];
@@ -736,7 +1013,7 @@ function formatKv(value, opts = {}) {
736
1013
  const keys = opts.fields?.length ? [...opts.fields] : Object.keys(value);
737
1014
  const rows = keys.map((key) => [key, getPath(value, key)]).filter(([, v]) => v !== void 0);
738
1015
  if (rows.length === 0) return [theme.dim("(empty)")];
739
- const width = Math.max(...rows.map(([k]) => displayWidth(k)));
1016
+ const width = Math.max(...rows.map(([k]) => displayWidth2(k)));
740
1017
  const lines = [];
741
1018
  for (const [key, raw] of rows) {
742
1019
  const label = theme.dim(pad(key, width, "left"));
@@ -747,7 +1024,7 @@ function formatKv(value, opts = {}) {
747
1024
  continue;
748
1025
  }
749
1026
  const text = isScalar(raw) || Array.isArray(raw) && raw.every(isScalar) ? humanCell(raw, opts.now) : compactJson(raw);
750
- lines.push(`${label} ${truncate(text, 100)}`);
1027
+ lines.push(`${label} ${truncate2(text, 100)}`);
751
1028
  }
752
1029
  return lines;
753
1030
  }
@@ -784,8 +1061,8 @@ function findListWrapper(value) {
784
1061
  if (arrays.length !== 1) return null;
785
1062
  const [key, list] = arrays[0];
786
1063
  if (list.length === 0) {
787
- const counted = COUNT_KEYS.some((k) => typeof value[k] === "number");
788
- return counted || Object.keys(value).length === 1 ? { key, rows: [] } : null;
1064
+ const counted2 = COUNT_KEYS.some((k) => typeof value[k] === "number");
1065
+ return counted2 || Object.keys(value).length === 1 ? { key, rows: [] } : null;
789
1066
  }
790
1067
  return list.every(isRecord3) ? { key, rows: list } : null;
791
1068
  }
@@ -869,7 +1146,7 @@ function formatResultHeader(tool, meta) {
869
1146
  const name = (tool.commandPath?.length ? tool.commandPath.join(" ") : tool.id).trim();
870
1147
  const right = [formatDuration(meta.durationMs), meta.requestId ? `req ${shortId(meta.requestId)}` : null].filter(Boolean).join(" \xB7 ");
871
1148
  const width = Math.min(meta.width ?? 80, 100);
872
- const used = displayWidth(theme.symbols.success) + 1 + displayWidth(name) + 2 + displayWidth(right) + 1;
1149
+ const used = displayWidth2(theme.symbols.success) + 1 + displayWidth2(name) + 2 + displayWidth2(right) + 1;
873
1150
  const rule = theme.symbols.line.repeat(Math.max(3, width - used));
874
1151
  return `${theme.success(theme.symbols.success)} ${theme.bold(name)} ${theme.accent(rule)} ${theme.dim(right)}`;
875
1152
  }
@@ -946,6 +1223,7 @@ function artifactFromResult(result) {
946
1223
  title: str(result.title) ?? "Untitled",
947
1224
  status,
948
1225
  ...typeof result.versionNumber === "number" ? { versionNumber: result.versionNumber } : {},
1226
+ ...str(result.versionId) ? { versionId: str(result.versionId) } : {},
949
1227
  ...typeof result.previewAvailable === "boolean" ? { previewAvailable: result.previewAvailable } : {},
950
1228
  ...failure ? { failure } : {}
951
1229
  };
@@ -1062,6 +1340,7 @@ function previewVariantFor(kind) {
1062
1340
  }
1063
1341
 
1064
1342
  // src/core/artifacts.ts
1343
+ import { randomUUID } from "node:crypto";
1065
1344
  import { createWriteStream, existsSync, renameSync, rmSync, statSync } from "node:fs";
1066
1345
  import { join } from "node:path";
1067
1346
  import { Readable } from "node:stream";
@@ -1076,8 +1355,11 @@ function listArtifacts(api, query = {}) {
1076
1355
  const qs = params.toString();
1077
1356
  return api.json(`${BASE}${qs ? `?${qs}` : ""}`);
1078
1357
  }
1079
- function getArtifact(api, id) {
1080
- return api.json(`${BASE}/${enc(id)}`);
1358
+ function getArtifact(api, id, signal) {
1359
+ return api.json(`${BASE}/${enc(id)}`, { signal });
1360
+ }
1361
+ function getArtifactVersion(api, id, versionId, signal) {
1362
+ return api.json(`${BASE}/${enc(id)}/versions/${enc(versionId)}`, { signal });
1081
1363
  }
1082
1364
  function previewUrl(api, id, opts) {
1083
1365
  const params = new URLSearchParams({ variant: opts.variant, ...opts.theme ? { theme: opts.theme } : {}, ...opts.versionId ? { versionId: opts.versionId } : {} });
@@ -1085,7 +1367,7 @@ function previewUrl(api, id, opts) {
1085
1367
  }
1086
1368
  function downloadUrl(api, id, opts) {
1087
1369
  const params = new URLSearchParams({ variant: opts.variant, ...opts.versionId ? { versionId: opts.versionId } : {} });
1088
- return api.json(`${BASE}/${enc(id)}/download-url?${params.toString()}`);
1370
+ return api.json(`${BASE}/${enc(id)}/download-url?${params.toString()}`, { signal: opts.signal });
1089
1371
  }
1090
1372
  function listVersions(api, id, opts = {}) {
1091
1373
  const params = new URLSearchParams({ ...opts.limit ? { limit: String(opts.limit) } : {}, ...opts.cursor ? { cursor: opts.cursor } : {} });
@@ -1132,7 +1414,44 @@ function resolveOutputPath(out, fileName, cwd = process.cwd()) {
1132
1414
  var WAIT_POLL_START_MS = 2e3;
1133
1415
  var WAIT_POLL_MAX_MS = 5e3;
1134
1416
  var DEFAULT_ARTIFACT_WAIT_MS = 5 * 6e4;
1135
- var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1417
+ var defaultSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
1418
+ function assertNotCancelled(signal) {
1419
+ if (signal?.aborted) throw interruptedError("File save cancelled");
1420
+ }
1421
+ function sleepUntilPoll(ms, sleep, signal) {
1422
+ if (!signal) return sleep(ms);
1423
+ assertNotCancelled(signal);
1424
+ return new Promise((resolve2, reject) => {
1425
+ const onAbort = () => {
1426
+ clearTimeout(timer);
1427
+ signal.removeEventListener("abort", onAbort);
1428
+ reject(interruptedError("File save cancelled"));
1429
+ };
1430
+ let timer;
1431
+ signal.addEventListener("abort", onAbort, { once: true });
1432
+ if (signal.aborted) {
1433
+ onAbort();
1434
+ return;
1435
+ }
1436
+ if (sleep === defaultSleep) {
1437
+ timer = setTimeout(() => {
1438
+ signal.removeEventListener("abort", onAbort);
1439
+ resolve2();
1440
+ }, ms);
1441
+ } else {
1442
+ sleep(ms).then(
1443
+ () => {
1444
+ signal.removeEventListener("abort", onAbort);
1445
+ resolve2();
1446
+ },
1447
+ (error) => {
1448
+ signal.removeEventListener("abort", onAbort);
1449
+ reject(error);
1450
+ }
1451
+ );
1452
+ }
1453
+ });
1454
+ }
1136
1455
  async function waitForArtifact(fetchStatus, opts = {}) {
1137
1456
  const sleep = opts.sleep ?? defaultSleep;
1138
1457
  const now = opts.now ?? Date.now;
@@ -1140,8 +1459,10 @@ async function waitForArtifact(fetchStatus, opts = {}) {
1140
1459
  let interval = WAIT_POLL_START_MS;
1141
1460
  let last = null;
1142
1461
  for (let attempt = 1; ; attempt += 1) {
1462
+ assertNotCancelled(opts.signal);
1143
1463
  try {
1144
1464
  last = await fetchStatus();
1465
+ assertNotCancelled(opts.signal);
1145
1466
  opts.onPoll?.({ status: last.status, attempt });
1146
1467
  if (last.status !== "pending") return { artifact: last, timedOut: false };
1147
1468
  } catch (error) {
@@ -1149,7 +1470,7 @@ async function waitForArtifact(fetchStatus, opts = {}) {
1149
1470
  }
1150
1471
  const remaining = deadline - now();
1151
1472
  if (remaining <= 0) return { artifact: last, timedOut: true };
1152
- await sleep(Math.min(interval, remaining));
1473
+ await sleepUntilPoll(Math.min(interval, remaining), sleep, opts.signal);
1153
1474
  interval = Math.min(WAIT_POLL_MAX_MS, Math.round(interval * 1.5));
1154
1475
  }
1155
1476
  }
@@ -1180,7 +1501,7 @@ async function downloadToFile(url, dest, opts = {}) {
1180
1501
  }
1181
1502
  const totalHeader = Number(response.headers.get("content-length"));
1182
1503
  const total = Number.isFinite(totalHeader) && totalHeader > 0 ? totalHeader : null;
1183
- const temp = `${dest}.part-${process.pid}`;
1504
+ const temp = `${dest}.part-${randomUUID()}`;
1184
1505
  let received = 0;
1185
1506
  const source = Readable.fromWeb(response.body);
1186
1507
  source.on("data", (chunk) => {
@@ -1188,7 +1509,8 @@ async function downloadToFile(url, dest, opts = {}) {
1188
1509
  opts.onProgress?.({ received, total });
1189
1510
  });
1190
1511
  try {
1191
- await pipeline(source, createWriteStream(temp, { mode: 420 }), { signal: opts.signal });
1512
+ await pipeline(source, createWriteStream(temp, { flags: "wx", mode: 384 }), { signal: opts.signal });
1513
+ assertNotCancelled(opts.signal);
1192
1514
  if (existsSync(dest) && !opts.force) throw new CliError({ code: "FILE_EXISTS", message: `${dest} already exists`, exitCode: ExitCode.Usage });
1193
1515
  renameSync(temp, dest);
1194
1516
  } catch (error) {
@@ -1198,6 +1520,150 @@ async function downloadToFile(url, dest, opts = {}) {
1198
1520
  return { path: dest, bytes: received, contentType: response.headers.get("content-type") };
1199
1521
  }
1200
1522
 
1523
+ // src/core/artifact-save.ts
1524
+ import { existsSync as existsSync2, mkdirSync } from "node:fs";
1525
+ import { readFile } from "node:fs/promises";
1526
+ import { homedir } from "node:os";
1527
+ import { isAbsolute, join as join2, relative, resolve, sep } from "node:path";
1528
+ var CV_EXPORT_FORMATS = ["pdf", "docx", "txt", "md"];
1529
+ function artifactsDir(env = process.env, cwd = process.cwd()) {
1530
+ const custom = env.TABBIO_ARTIFACTS_DIR?.trim();
1531
+ return custom ? resolve(cwd, custom) : join2(cwd, "tabbio-artifacts");
1532
+ }
1533
+ function autoSaveDisabled(env = process.env) {
1534
+ const value = env.TABBIO_NO_ARTIFACT_SAVE?.trim().toLowerCase();
1535
+ return Boolean(value && value !== "0" && value !== "false" && value !== "no");
1536
+ }
1537
+ function ensureDir(dir) {
1538
+ mkdirSync(dir, { recursive: true, mode: 448 });
1539
+ }
1540
+ function artifactBaseName(title, id) {
1541
+ const slug = title.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/, "") || "artifact";
1542
+ const short = id.replace(/[^A-Za-z0-9]/g, "").slice(-6).toLowerCase();
1543
+ return short ? `${slug}-${short}` : slug;
1544
+ }
1545
+ function uniquePath(dir, base, ext) {
1546
+ const suffix = ext ? `.${ext}` : "";
1547
+ let candidate = join2(dir, `${base}${suffix}`);
1548
+ for (let n = 2; existsSync2(candidate); n += 1) candidate = join2(dir, `${base}-${n}${suffix}`);
1549
+ return candidate;
1550
+ }
1551
+ function displayPath(path, cwd = process.cwd(), home = homedir()) {
1552
+ const rel = relative(cwd, path);
1553
+ if (rel && !rel.startsWith("..") && !isAbsolute(rel)) return `.${sep}${rel}`;
1554
+ const fromHome = relative(home, path);
1555
+ if (home && fromHome && !fromHome.startsWith("..") && !isAbsolute(fromHome)) return `~${sep}${fromHome}`;
1556
+ return path;
1557
+ }
1558
+ function formatBytes(bytes) {
1559
+ if (bytes < 1024) return `${bytes} B`;
1560
+ if (bytes < 1024 * 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`;
1561
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1562
+ }
1563
+ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set(["html", "htm", "md", "markdown", "txt", "csv", "json", "svg", "css", "js"]);
1564
+ function isTextFile(path) {
1565
+ const ext = path.split(".").pop()?.toLowerCase() ?? "";
1566
+ return TEXT_EXTENSIONS.has(ext);
1567
+ }
1568
+ function variantsToSave(artifact) {
1569
+ const v = artifact.variants ?? { html: false, pdf: false, pptx: false, image: false, thumbnail: false };
1570
+ switch (artifact.kind) {
1571
+ case "page":
1572
+ return ["html"];
1573
+ case "image":
1574
+ return ["image"];
1575
+ case "slides":
1576
+ return v.pptx ? ["pptx"] : [defaultVariantFor(artifact)];
1577
+ case "document": {
1578
+ const both = ["html", "pdf"].filter((variant) => v[variant]);
1579
+ return both.length ? [...both] : [defaultVariantFor(artifact)];
1580
+ }
1581
+ default:
1582
+ return [defaultVariantFor(artifact)];
1583
+ }
1584
+ }
1585
+ function notReadyError(id, failed, message) {
1586
+ return failed ? new CliError({ code: "ARTIFACT_FAILED", message: message ?? "No file was made", exitCode: ExitCode.Error }) : new CliError({
1587
+ code: "ARTIFACT_NOT_READY",
1588
+ message: "This file is still being created",
1589
+ hint: `Wait for it with \`tabbio artifacts wait ${id}\`, then save it again.`,
1590
+ exitCode: ExitCode.Error
1591
+ });
1592
+ }
1593
+ async function saveArtifactToDisk(ctx, artifactId, opts = {}) {
1594
+ const signal = combineSignals(activeCommandSignal, opts.signal);
1595
+ const [artifact, version] = await Promise.all([
1596
+ getArtifact(ctx.api, artifactId, signal),
1597
+ opts.versionId ? getArtifactVersion(ctx.api, artifactId, opts.versionId, signal) : Promise.resolve(null)
1598
+ ]);
1599
+ const status = version?.status ?? artifact.status;
1600
+ if (status !== "ready") throw notReadyError(artifactId, status === "failed", version?.error?.message ?? artifact.error?.message);
1601
+ if (signal.aborted) throw new CliError({ code: "INTERRUPTED", message: "File save cancelled", exitCode: ExitCode.Interrupted });
1602
+ const variants = opts.formats?.length ? [...new Set(opts.formats.map(formatToVariant))] : variantsToSave({ kind: artifact.kind, variants: version?.variants ?? artifact.variants });
1603
+ const dir = opts.dir ? resolve(opts.dir) : artifactsDir();
1604
+ ensureDir(dir);
1605
+ const base = artifactBaseName(artifact.title, artifact.id || artifactId);
1606
+ const saved = [];
1607
+ for (const [i, variant] of variants.entries()) {
1608
+ try {
1609
+ const signed = await downloadUrl(ctx.api, artifactId, { variant, versionId: opts.versionId, signal });
1610
+ const dest = uniquePath(dir, base, extensionFor(variant, signed.contentType));
1611
+ const file = await downloadToFile(signed.url, dest, { signal });
1612
+ saved.push({ path: file.path, bytes: file.bytes, kind: variant });
1613
+ } catch (error) {
1614
+ if (i === 0 || !isCliError(error) || error.code !== "ARTIFACT_NOT_READY") throw error;
1615
+ }
1616
+ }
1617
+ return saved;
1618
+ }
1619
+ function isMainCv(cvId) {
1620
+ const id = cvId?.trim().toLowerCase() ?? "";
1621
+ return !id || id === "master" || id === "main";
1622
+ }
1623
+ function dispositionBaseName(header) {
1624
+ if (!header) return null;
1625
+ const star = /filename\*\s*=\s*(?:UTF-8'')?([^;]+)/i.exec(header)?.[1];
1626
+ const plain = /filename\s*=\s*"?([^";]+)"?/i.exec(header)?.[1];
1627
+ let name = star ?? plain;
1628
+ if (!name) return null;
1629
+ try {
1630
+ name = decodeURIComponent(name.trim().replace(/^"|"$/g, ""));
1631
+ } catch {
1632
+ name = name.trim();
1633
+ }
1634
+ const base = name.split(/[\\/]/).pop()?.replace(/\.[A-Za-z0-9]{1,5}$/, "").normalize("NFKC").replace(/[^\p{L}\p{N}._-]+/gu, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 80);
1635
+ return base || null;
1636
+ }
1637
+ async function saveCvExportToDisk(ctx, cvId, opts = {}) {
1638
+ const signal = combineSignals(activeCommandSignal, opts.signal);
1639
+ const format = (opts.format ?? "pdf").trim().toLowerCase().replace(/^\./, "");
1640
+ if (!CV_EXPORT_FORMATS.includes(format)) {
1641
+ throw usageError(`CV exports are ${CV_EXPORT_FORMATS.join(", ")}; got "${opts.format}"`);
1642
+ }
1643
+ const main = isMainCv(cvId);
1644
+ const path = main ? `/api/cv/export/${format}` : `/api/cv/${encodeURIComponent(String(cvId))}/export/${format}`;
1645
+ const response = await ctx.api.raw(path, { method: "GET", headers: { accept: "*/*" }, timeoutMs: 18e4, signal });
1646
+ if (!response.ok) {
1647
+ await parseJsonResponse(response);
1648
+ throw new CliError({ code: "EXPORT_FAILED", message: `The CV export failed (HTTP ${response.status})`, exitCode: ExitCode.Server });
1649
+ }
1650
+ if (signal.aborted) throw new CliError({ code: "INTERRUPTED", message: "File save cancelled", exitCode: ExitCode.Interrupted });
1651
+ const dir = opts.dir ? resolve(opts.dir) : artifactsDir();
1652
+ ensureDir(dir);
1653
+ const fallback = main ? "main-cv" : artifactBaseName("cv", String(cvId));
1654
+ const base = dispositionBaseName(response.headers.get("content-disposition")) ?? fallback;
1655
+ const dest = uniquePath(dir, base, format);
1656
+ const url = `${ctx.api.profile.apiUrl}${path}`;
1657
+ const file = await downloadToFile(url, dest, { fetch: async () => response, signal });
1658
+ return { path: file.path, bytes: file.bytes, kind: format };
1659
+ }
1660
+ async function readSavedPreview(path, maxLines = 12) {
1661
+ const text = await readFile(path, "utf8");
1662
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
1663
+ if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
1664
+ return { lines: lines.slice(0, Math.max(0, maxLines)), total: lines.length };
1665
+ }
1666
+
1201
1667
  // src/ui/hyperlink.ts
1202
1668
  function supportsHyperlinks(env = process.env, isTTY = Boolean(process.stdout.isTTY)) {
1203
1669
  const forced = env.FORCE_HYPERLINK?.trim();
@@ -1334,6 +1800,718 @@ function parseStreamEvent(data) {
1334
1800
  }
1335
1801
  }
1336
1802
 
1803
+ // ../../packages/utils/src/public-url.ts
1804
+ var DEFAULT_PUBLIC_SITE_URL = "https://www.tabbio.com";
1805
+ var DEFAULT_PUBLIC_DOMAIN = "tabbio.com";
1806
+ var RESERVED_PUBLIC_SUBDOMAINS = [
1807
+ "www",
1808
+ "app",
1809
+ "api",
1810
+ "server",
1811
+ "v2",
1812
+ "beta",
1813
+ "beta-api",
1814
+ "staging",
1815
+ "admin",
1816
+ // The partner portal is its own app on partners.tabbio.com. Reserving the
1817
+ // label keeps it out of the handle space so no user or company can claim it
1818
+ // and shadow the host through wildcard DNS.
1819
+ "partners",
1820
+ "mail",
1821
+ "cdn",
1822
+ "static",
1823
+ "assets",
1824
+ "jobs",
1825
+ "companies",
1826
+ "support",
1827
+ "help",
1828
+ // Public skill share pages (skills.tabbio.com/<skillId>); reserving both
1829
+ // labels keeps the singular from ever resolving as a profile handle.
1830
+ "skills",
1831
+ "skill",
1832
+ // The public CV Health check (cvhealth.tabbio.com, served by apps/web). The
1833
+ // hyphenated spelling is reserved with it so neither can become a handle
1834
+ // that reads as the first-party check.
1835
+ "cvhealth",
1836
+ "cv-health",
1837
+ // apps/docs and apps/changelog run on their own hosts. Both labels were
1838
+ // already blocked at signup by RESERVED_USERNAMES, so handle registration
1839
+ // was never exposed - but this list is what governs host semantics, and a
1840
+ // label that names a first-party deployment must not be handle-like here
1841
+ // either. Keeping the two lists in agreement is what stops a subdomain from
1842
+ // being routed as a profile.
1843
+ "docs",
1844
+ "changelog",
1845
+ // The developer portal (apps/developer) runs on developer.tabbio.com. The two
1846
+ // near-misses are reserved with it: a handle at developers.* or dev.* would
1847
+ // read as a first-party developer surface without being one.
1848
+ "developer",
1849
+ "developers",
1850
+ "dev",
1851
+ // The developer platform (apps/platform) runs on platform.tabbio.com, and the
1852
+ // admin console (apps/admin) on console.tabbio.com. `dashboard` is the
1853
+ // near-miss reserved with them: a handle there would read as a first-party
1854
+ // control surface without being one.
1855
+ "platform",
1856
+ "console",
1857
+ "dashboard"
1858
+ ];
1859
+ var RESERVED_PUBLIC_SUBDOMAIN_SET = new Set(
1860
+ RESERVED_PUBLIC_SUBDOMAINS
1861
+ );
1862
+ function normalizeBaseUrl(baseUrl, fallback) {
1863
+ const clean = typeof baseUrl === "string" && baseUrl.trim().length > 0 ? baseUrl.trim() : fallback;
1864
+ return clean.endsWith("/") ? clean.slice(0, -1) : clean;
1865
+ }
1866
+ function getLocalPublicBaseUrl(baseUrl) {
1867
+ const normalized = normalizeBaseUrl(baseUrl, DEFAULT_PUBLIC_SITE_URL);
1868
+ try {
1869
+ const parsed = new URL(
1870
+ normalized.startsWith("http://") || normalized.startsWith("https://") ? normalized : `https://${normalized}`
1871
+ );
1872
+ const hostname = parsed.hostname.toLowerCase();
1873
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1") {
1874
+ return parsed.origin;
1875
+ }
1876
+ } catch {
1877
+ return null;
1878
+ }
1879
+ return null;
1880
+ }
1881
+ function getPublicRootDomain(value = DEFAULT_PUBLIC_DOMAIN) {
1882
+ const clean = normalizeBaseUrl(value, DEFAULT_PUBLIC_DOMAIN).replace(/^https?:\/\//, "").split("/")[0].toLowerCase();
1883
+ if (!clean) {
1884
+ return DEFAULT_PUBLIC_DOMAIN;
1885
+ }
1886
+ try {
1887
+ const parsed = new URL(
1888
+ clean.includes("://") ? clean : `https://${clean}`
1889
+ );
1890
+ return parsed.host.toLowerCase().replace(/^www\./, "");
1891
+ } catch {
1892
+ return clean.replace(/^www\./, "");
1893
+ }
1894
+ }
1895
+ function normalizePublicHandle(handle) {
1896
+ return typeof handle === "string" ? handle.trim().replace(/^@+/, "").replace(/_/g, "-").toLowerCase() : "";
1897
+ }
1898
+ function buildPersonCvUrl(handle, domain = DEFAULT_PUBLIC_DOMAIN, locale) {
1899
+ const normalized = normalizePublicHandle(handle);
1900
+ const localPublicBaseUrl = getLocalPublicBaseUrl(domain);
1901
+ const normalizedLocale = typeof locale === "string" ? locale.trim().replace(/^\/+|\/+$/g, "") : "";
1902
+ if (localPublicBaseUrl) {
1903
+ return `${localPublicBaseUrl}/${encodeURIComponent(normalized)}/cv`;
1904
+ }
1905
+ const rootDomain = getPublicRootDomain(domain);
1906
+ const base = `https://${normalized}.${rootDomain}`;
1907
+ return normalizedLocale ? `${base}/${encodeURIComponent(normalizedLocale)}` : base;
1908
+ }
1909
+
1910
+ // src/core/chat-card-fields.ts
1911
+ function isRec(value) {
1912
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1913
+ }
1914
+ function rec(value) {
1915
+ return isRec(value) ? value : void 0;
1916
+ }
1917
+ function str2(value) {
1918
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
1919
+ }
1920
+ function num(value) {
1921
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1922
+ }
1923
+ function listOf(result, ...keys) {
1924
+ if (Array.isArray(result)) return result.filter(isRec);
1925
+ const record = rec(result);
1926
+ if (!record) return [];
1927
+ for (const key of keys) {
1928
+ const value = record[key];
1929
+ if (Array.isArray(value)) return value.filter(isRec);
1930
+ }
1931
+ return [];
1932
+ }
1933
+ function httpUrl(value) {
1934
+ const text = str2(value);
1935
+ return text && !/[\u0000-\u001f\u007f-\u009f]/.test(text) && /^https?:\/\/\S+$/i.test(text) ? text : void 0;
1936
+ }
1937
+ function publicSiteUrl(appUrl) {
1938
+ try {
1939
+ const url = new URL(appUrl);
1940
+ if (["localhost", "127.0.0.1", "[::1]", "::1"].includes(url.hostname)) return "http://localhost:3000";
1941
+ const host = url.hostname.startsWith("app.") ? url.hostname.slice(4) : url.hostname;
1942
+ return `${url.protocol}//${host}${url.port ? `:${url.port}` : ""}`;
1943
+ } catch {
1944
+ return "https://tabbio.com";
1945
+ }
1946
+ }
1947
+ function companyName(job) {
1948
+ const company = job.company;
1949
+ return (isRec(company) ? str2(company.name) : str2(company)) ?? str2(job.companyName);
1950
+ }
1951
+ function jobLink(job, siteUrl) {
1952
+ const source = rec(job.sourceInfo);
1953
+ const direct = httpUrl(job.applicationUrl) ?? httpUrl(job.applyUrl) ?? httpUrl(source?.directApplyUrl) ?? httpUrl(source?.sourceUrl);
1954
+ if (direct) return direct;
1955
+ const slug = str2(job.publicSlug) ?? str2(job.slug);
1956
+ if (slug) return `${siteUrl}/en/jobs/${encodeURIComponent(slug)}`;
1957
+ return httpUrl(job.canonicalUrl);
1958
+ }
1959
+ function matchLabel(score) {
1960
+ const value = num(score);
1961
+ if (value === void 0 || value <= 0) return void 0;
1962
+ return `${Math.round(value <= 1 ? value * 100 : value)}% match`;
1963
+ }
1964
+ function when(value, now) {
1965
+ const text = typeof value === "string" ? value : value instanceof Date ? value.toISOString() : void 0;
1966
+ if (!text) return void 0;
1967
+ const date = new Date(text);
1968
+ return Number.isNaN(date.getTime()) ? void 0 : relativeTime(date, now);
1969
+ }
1970
+ var DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1971
+ function scheduleLabel(cron) {
1972
+ const text = str2(cron);
1973
+ if (!text) return void 0;
1974
+ const parts = text.split(/\s+/);
1975
+ if (parts.length !== 5) return text;
1976
+ const [minute, hour, dom, month, dow] = parts;
1977
+ const time = /^\d+$/.test(minute) && /^\d+$/.test(hour) ? `${hour.padStart(2, "0")}:${minute.padStart(2, "0")}` : null;
1978
+ if (/^\d+$/.test(minute) && hour === "*" && dom === "*" && month === "*" && dow === "*") return "hourly";
1979
+ if (time && dom === "*" && month === "*" && dow === "*") return `daily ${time}`;
1980
+ if (time && dom === "*" && month === "*" && /^[0-6]$/.test(dow)) return `weekly ${DAYS[Number(dow)]} ${time}`;
1981
+ if (time && dom === "*" && month === "*" && dow === "1-5") return `weekdays ${time}`;
1982
+ if (time && /^\d+$/.test(dom) && month === "*" && dow === "*") return `monthly on day ${dom} ${time}`;
1983
+ return text;
1984
+ }
1985
+ function excerpt(value, max = 320) {
1986
+ const text = str2(value);
1987
+ if (!text) return void 0;
1988
+ const flat = text.replace(/\s+/g, " ");
1989
+ return flat.length > max ? `${flat.slice(0, max - 1).trimEnd()}\u2026` : flat;
1990
+ }
1991
+ function humanKey(key) {
1992
+ const words = key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim().toLowerCase();
1993
+ return words.charAt(0).toUpperCase() + words.slice(1);
1994
+ }
1995
+ function scalarLabel(value) {
1996
+ if (value === null || value === void 0) return void 0;
1997
+ if (typeof value === "boolean") return value ? "yes" : "no";
1998
+ if (typeof value === "number") return Number.isFinite(value) ? String(value) : void 0;
1999
+ if (typeof value === "string") return value.trim() ? value.replace(/\s+/g, " ").trim() : void 0;
2000
+ if (Array.isArray(value)) return `${value.length} ${value.length === 1 ? "item" : "items"}`;
2001
+ if (isRec(value)) {
2002
+ const name = str2(value.title) ?? str2(value.name) ?? str2(value.label);
2003
+ if (name) return name;
2004
+ const n = Object.keys(value).length;
2005
+ return n ? `${n} ${n === 1 ? "field" : "fields"}` : void 0;
2006
+ }
2007
+ return void 0;
2008
+ }
2009
+
2010
+ // src/core/chat-card-kinds.ts
2011
+ var MAX_CARD_ROWS = 8;
2012
+ function card(kind, title, rows, extra = {}) {
2013
+ const links = rows.flatMap((row) => row.link ? [row.link] : []);
2014
+ return { kind, title, rows, links, ...extra };
2015
+ }
2016
+ function counted(noun, shown, total) {
2017
+ return total > shown ? `${noun} \xB7 ${shown} of ${total}` : `${noun} \xB7 ${total}`;
2018
+ }
2019
+ function jobRow(job, n, opts) {
2020
+ const title = str2(job.title) ?? "Untitled role";
2021
+ const url = jobLink(job, opts.siteUrl);
2022
+ const id = str2(job.id);
2023
+ const meta = [
2024
+ companyName(job),
2025
+ str2(job.location),
2026
+ str2(job.workMode),
2027
+ str2(job.type),
2028
+ matchLabel(job.matchScore),
2029
+ job.applied === true ? "applied" : void 0,
2030
+ when(job.postedAt, opts.now),
2031
+ url ? void 0 : "no link"
2032
+ ].filter((part) => Boolean(part));
2033
+ const link = url ? { n, label: title, url, ...id ? { jobId: id, jobTitle: title } : {} } : void 0;
2034
+ return { n, title, meta, ...link ? { link } : {}, ...id ? { job: { id, title } } : {} };
2035
+ }
2036
+ function jobListCard(result, opts) {
2037
+ const jobs = listOf(result, "jobs", "items", "savedJobs");
2038
+ if (!jobs.length) return null;
2039
+ const total = num(rec(result)?.total) ?? jobs.length;
2040
+ const rows = jobs.slice(0, MAX_CARD_ROWS).map((job, i) => jobRow(rec(job.job) ?? job, i + 1, opts));
2041
+ return card("job-list", counted("Jobs", rows.length, Math.max(total, jobs.length)), rows, {
2042
+ hint: "/open <n> opens a job \xB7 /apply <n> prepares an application",
2043
+ ...jobs.length > rows.length ? { more: jobs.length - rows.length } : {}
2044
+ });
2045
+ }
2046
+ function jobDetailCard(result, opts) {
2047
+ const job = rec(rec(result)?.job) ?? rec(result);
2048
+ if (!job || !str2(job.title)) return null;
2049
+ const row = jobRow(job, 1, opts);
2050
+ const status = str2(job.status);
2051
+ const body = excerpt(job.summary ?? job.description);
2052
+ return card("job-detail", "Job", [
2053
+ { ...row, meta: [...row.meta ?? [], ...status && status !== "active" ? [status] : []], ...body ? { body } : {} }
2054
+ ], { hint: row.link ? "/open 1 opens the job \xB7 /apply 1 prepares an application" : "/apply 1 prepares an application" });
2055
+ }
2056
+ function labelRow(label, value, body) {
2057
+ if (!value && !body) return [];
2058
+ return [{ label, title: value ?? "", ...body ? { body } : {} }];
2059
+ }
2060
+ function applicationPreviewCard(result, opts) {
2061
+ const r = rec(result);
2062
+ if (!r) return null;
2063
+ const job = rec(r.job) ?? {};
2064
+ const title = str2(job.title) ?? "This job";
2065
+ const existing = rec(r.existingApplication);
2066
+ const redirect = r.redirectRequired === true;
2067
+ const ready = r.readyToSubmit === true;
2068
+ const url = httpUrl(r.redirectUrl) ?? httpUrl(existing?.redirectUrl) ?? httpUrl(existing?.applicationUrl) ?? jobLink(job, opts.siteUrl);
2069
+ const id = str2(job.id);
2070
+ const link = url ? { n: 1, label: redirect ? "Apply on the employer site" : title, url, ...id ? { jobId: id, jobTitle: title } : {} } : void 0;
2071
+ const status = str2(existing?.status) ? `already ${str2(existing?.status)}` : ready ? "ready to submit" : redirect ? "continues on the employer site" : "needs a few details";
2072
+ const notes = Array.isArray(r.notes) ? r.notes.filter((note) => typeof note === "string").join(" ") : str2(r.notes);
2073
+ const rows = [
2074
+ {
2075
+ n: 1,
2076
+ title,
2077
+ meta: [companyName(job), str2(job.location)].filter((part) => Boolean(part)),
2078
+ ...link ? { link } : {},
2079
+ ...id ? { job: { id, title } } : {}
2080
+ },
2081
+ ...labelRow("Status", status),
2082
+ ...labelRow("Ready to submit", ready ? "yes" : "no"),
2083
+ ...labelRow("CV", str2(rec(r.cv)?.title) ?? (r.cv === null ? "none yet" : void 0)),
2084
+ ...labelRow("Cover letter", void 0, excerpt(r.coverLetter, 480)),
2085
+ ...labelRow("Note", excerpt(notes, 200))
2086
+ ];
2087
+ const hint = [
2088
+ link ? `/open 1 ${redirect ? "applies on the employer site" : "opens the job"}` : null,
2089
+ ready ? "reply 'submit' to send it (you approve it first)" : "/apply <n> or reply 'submit' to continue"
2090
+ ].filter(Boolean);
2091
+ return card("application-preview", "Application", rows, { hint: hint.join(" \xB7 ") });
2092
+ }
2093
+ function applicationConfirmCard(result, opts) {
2094
+ const r = rec(result);
2095
+ if (!r) return null;
2096
+ const application = rec(r.application) ?? r;
2097
+ const job = rec(r.job) ?? rec(application.job) ?? {};
2098
+ const title = str2(job.title) ?? str2(application.jobTitle) ?? "Application";
2099
+ const url = httpUrl(r.redirectUrl) ?? httpUrl(application.redirectUrl) ?? httpUrl(application.applicationUrl);
2100
+ const outcome = str2(r.outcome)?.replace(/_/g, " ");
2101
+ const rows = [
2102
+ {
2103
+ n: 1,
2104
+ title,
2105
+ meta: [companyName(job) ?? str2(rec(r.company)?.name), when(application.submittedAt ?? application.appliedAt, opts.now)].filter(
2106
+ (part) => Boolean(part)
2107
+ ),
2108
+ ...url ? { link: { n: 1, label: title, url } } : {}
2109
+ },
2110
+ ...labelRow("Outcome", outcome),
2111
+ ...labelRow("Status", str2(application.status)),
2112
+ ...labelRow("Message", excerpt(r.message, 200))
2113
+ ];
2114
+ return card("application-confirm", "Application", rows, url ? { hint: "/open 1 finishes it on the employer site" } : {});
2115
+ }
2116
+ function applicationListCard(result, opts) {
2117
+ const apps = listOf(result, "applications", "items", "history", "intents");
2118
+ if (!apps.length) return null;
2119
+ const rows = apps.slice(0, MAX_CARD_ROWS).map((app, i) => {
2120
+ const job = rec(app.job) ?? {};
2121
+ const title = str2(job.title) ?? str2(app.jobTitle) ?? str2(app.title) ?? "Application";
2122
+ const url = httpUrl(app.redirectUrl) ?? httpUrl(app.applicationUrl) ?? jobLink(job, opts.siteUrl);
2123
+ const meta = [
2124
+ companyName(job) ?? str2(rec(app.company)?.name),
2125
+ str2(app.status)?.replace(/_/g, " "),
2126
+ when(app.updatedAt ?? app.submittedAt ?? app.appliedAt ?? app.preparedAt, opts.now)
2127
+ ].filter((part) => Boolean(part));
2128
+ return { n: i + 1, title, meta, ...url ? { link: { n: i + 1, label: title, url } } : {} };
2129
+ });
2130
+ return card("application-list", counted("Applications", rows.length, apps.length), rows, {
2131
+ ...apps.length > rows.length ? { more: apps.length - rows.length } : {},
2132
+ ...rows.some((row) => row.link) ? { hint: "/open <n> opens an application" } : {}
2133
+ });
2134
+ }
2135
+ function skillListCard(result) {
2136
+ const skills = listOf(result, "skills", "items");
2137
+ if (!skills.length) return null;
2138
+ const total = num(rec(result)?.total) ?? skills.length;
2139
+ const rows = skills.slice(0, MAX_CARD_ROWS).flatMap((skill, i) => {
2140
+ const id = str2(skill.skillId) ?? str2(skill.id);
2141
+ if (!id) return [];
2142
+ const command = `/${id.replace(/^\/+/, "").toLowerCase()}`;
2143
+ const tier = str2(skill.tier);
2144
+ const meta = [str2(skill.name), tier && tier !== "free" ? tier : void 0].filter((part) => Boolean(part));
2145
+ const body = excerpt(skill.description, 160);
2146
+ return [{ n: i + 1, title: command, meta, ...body ? { body, bodyLines: 1 } : {}, action: command }];
2147
+ });
2148
+ if (!rows.length) return null;
2149
+ return card("skill-list", counted("Skills", rows.length, Math.max(total, skills.length)), rows, {
2150
+ hint: "Type /<skill-id> to run a skill",
2151
+ ...skills.length > rows.length ? { more: skills.length - rows.length } : {}
2152
+ });
2153
+ }
2154
+ function automationMeta(item, now) {
2155
+ const last = when(item.lastRunAt, now);
2156
+ const lastStatus = str2(item.lastRunStatus)?.replace(/_/g, " ");
2157
+ const next = when(item.nextRunAt, now);
2158
+ return [
2159
+ str2(item.status)?.replace(/_/g, " "),
2160
+ scheduleLabel(item.cron),
2161
+ last ? `last run ${last}${lastStatus ? ` (${lastStatus})` : ""}` : "never run",
2162
+ next ? `next ${next}` : void 0
2163
+ ].filter((part) => Boolean(part));
2164
+ }
2165
+ function automationListCard(result, opts, runs) {
2166
+ const items = listOf(result, runs ? "runs" : "automations", "items");
2167
+ if (!items.length) return null;
2168
+ const rows = items.slice(0, MAX_CARD_ROWS).map((item, i) => {
2169
+ if (!runs) return { n: i + 1, title: str2(item.name) ?? "Automation", meta: automationMeta(item, opts.now) };
2170
+ const started = when(item.startTime ?? item.scheduledTime ?? item.createdAt, opts.now);
2171
+ const meta = [str2(item.triggerType), started, excerpt(item.errorSummary, 80)].filter((part) => Boolean(part));
2172
+ return { n: i + 1, title: (str2(item.status) ?? "run").replace(/_/g, " "), meta };
2173
+ });
2174
+ return card("automation-list", counted(runs ? "Runs" : "Automations", rows.length, items.length), rows, {
2175
+ ...items.length > rows.length ? { more: items.length - rows.length } : {}
2176
+ });
2177
+ }
2178
+ function automationDetailCard(result, opts, run) {
2179
+ const r = rec(result);
2180
+ if (!r) return null;
2181
+ const automation = rec(r.automation) ?? (run ? void 0 : r);
2182
+ const runRec = rec(r.run) ?? (run ? r : void 0);
2183
+ const name = str2(automation?.name) ?? str2(r.name) ?? str2(rec(r.plan)?.name);
2184
+ if (!name && !runRec) return null;
2185
+ const rows = [
2186
+ { title: name ?? "Automation", meta: automation ? automationMeta(automation, opts.now) : [] },
2187
+ ...labelRow("Run", str2(runRec?.status)?.replace(/_/g, " ")),
2188
+ ...labelRow("Started", when(runRec?.startTime ?? runRec?.createdAt, opts.now)),
2189
+ ...labelRow("Problem", excerpt(runRec?.errorSummary ?? r.message, 200))
2190
+ ];
2191
+ return card(run ? "automation-run" : "automation-detail", run ? "Automation run" : "Automation", rows);
2192
+ }
2193
+ function profileSummaryCard(result) {
2194
+ const r = rec(result);
2195
+ if (!r) return null;
2196
+ const p = rec(r.profile) ?? r;
2197
+ const count = (value) => Array.isArray(value) && value.length ? String(value.length) : void 0;
2198
+ const experiences = listOf(r, "experiences", "experience");
2199
+ const latest = experiences[0];
2200
+ const latestRole = latest ? [str2(latest.title), str2(latest.company)].filter(Boolean).join(" at ") : void 0;
2201
+ const rows = [
2202
+ { title: str2(p.name) ?? "Your profile", meta: [str2(p.headline) ?? str2(p.title), str2(p.location)].filter((x) => Boolean(x)) },
2203
+ ...labelRow("Experience", experiences.length ? `${experiences.length} ${experiences.length === 1 ? "role" : "roles"}${latestRole ? `, latest ${latestRole}` : ""}` : void 0),
2204
+ ...labelRow("Education", count(r.education ?? r.educations)),
2205
+ ...labelRow("Certifications", count(r.certifications)),
2206
+ ...labelRow("Projects", count(r.projects)),
2207
+ ...labelRow("Email", str2(p.contactEmail) ?? str2(p.email)),
2208
+ ...labelRow("Summary", void 0, excerpt(p.summary ?? p.about ?? p.bio, 240))
2209
+ ];
2210
+ let n = 0;
2211
+ for (const key of ["website", "linkedin", "github", "behance", "dribbble"]) {
2212
+ const url = httpUrl(p[key]);
2213
+ if (!url) continue;
2214
+ n += 1;
2215
+ rows.push({ n, label: humanKey(key), title: url, link: { n, label: humanKey(key), url } });
2216
+ }
2217
+ return card("profile-summary", "Profile", rows, n ? { hint: "/open <n> opens a link" } : {});
2218
+ }
2219
+ function companyListCard(result) {
2220
+ const companies = listOf(result, "companies", "items");
2221
+ if (!companies.length) return null;
2222
+ const rows = companies.slice(0, MAX_CARD_ROWS).map((company, i) => {
2223
+ const title = str2(company.name) ?? "Company";
2224
+ const url = httpUrl(company.website);
2225
+ const meta = [str2(company.industry), str2(company.location), str2(company.employeeCount), str2(company.id)].filter(
2226
+ (part) => Boolean(part)
2227
+ );
2228
+ return { n: i + 1, title, meta, ...url ? { link: { n: i + 1, label: title, url } } : {} };
2229
+ });
2230
+ return card("company-list", counted("Companies", rows.length, companies.length), rows, {
2231
+ ...companies.length > rows.length ? { more: companies.length - rows.length } : {}
2232
+ });
2233
+ }
2234
+ var GENERIC_SKIP = /* @__PURE__ */ new Set(["success", "ok", "userId", "display", "message", "decision", "featureKey"]);
2235
+ function genericCard(result, title) {
2236
+ if (typeof result === "string") {
2237
+ const body = excerpt(result, 480);
2238
+ return body ? card("generic", title, [{ title: "", body }]) : null;
2239
+ }
2240
+ const list = Array.isArray(result) ? result.filter(isRec) : [];
2241
+ if (list.length) {
2242
+ const rows2 = list.slice(0, MAX_CARD_ROWS).map((item, i) => ({
2243
+ n: i + 1,
2244
+ title: str2(item.title) ?? str2(item.name) ?? str2(item.label) ?? str2(item.id) ?? `Item ${i + 1}`,
2245
+ meta: [str2(item.status), str2(item.type), str2(item.id) !== str2(item.title) ? str2(item.id) : void 0].filter(
2246
+ (part) => Boolean(part)
2247
+ )
2248
+ }));
2249
+ return card("generic", counted(title, rows2.length, list.length), rows2, list.length > rows2.length ? { more: list.length - rows2.length } : {});
2250
+ }
2251
+ const r = rec(result);
2252
+ if (!r) return null;
2253
+ const rows = Object.entries(r).filter(([key]) => !GENERIC_SKIP.has(key)).flatMap(([key, value]) => {
2254
+ const text = scalarLabel(value);
2255
+ return text ? [{ label: humanKey(key), title: text }] : [];
2256
+ });
2257
+ if (!rows.length) return null;
2258
+ return card("generic", title, rows.slice(0, MAX_CARD_ROWS), rows.length > MAX_CARD_ROWS ? { more: rows.length - MAX_CARD_ROWS } : {});
2259
+ }
2260
+
2261
+ // src/core/chat-card-cv.ts
2262
+ function cvListCard(result, opts) {
2263
+ const r = rec(result);
2264
+ const cvs = listOf(result, "cvs");
2265
+ const main = rec(r?.mainCv);
2266
+ if (!cvs.length && !main) return null;
2267
+ const defaultId = str2(r?.defaultCvId) ?? str2(main?.id) ?? "master";
2268
+ const all = [...main ? [main] : [], ...cvs];
2269
+ const rows = all.slice(0, MAX_CARD_ROWS).map((cv, i) => {
2270
+ const id = str2(cv.id) ?? "master";
2271
+ const isProfile = cv === main;
2272
+ const meta = [
2273
+ id === defaultId ? "main" : void 0,
2274
+ isProfile ? "profile CV" : void 0,
2275
+ when(cv.updatedAt, opts.now) ? `updated ${when(cv.updatedAt, opts.now)}` : void 0,
2276
+ cv.isPublic === true ? "public" : void 0,
2277
+ id
2278
+ ].filter((part) => Boolean(part));
2279
+ return { n: i + 1, title: str2(cv.title) ?? "Untitled CV", meta, action: `/download ${id}` };
2280
+ });
2281
+ return card("cv-list", counted("CVs", rows.length, all.length), rows, {
2282
+ hint: "/download <id> saves a PDF to ./tabbio-artifacts",
2283
+ ...all.length > rows.length ? { more: all.length - rows.length } : {}
2284
+ });
2285
+ }
2286
+ function cvDetailCard(result, opts) {
2287
+ const r = rec(rec(result)?.cv) ?? rec(result);
2288
+ if (!r) return null;
2289
+ const id = str2(r.id) ?? str2(r.cvId);
2290
+ const title = str2(r.displayTitle) ?? str2(r.title) ?? str2(r.targetLabel);
2291
+ if (!id && !title) return null;
2292
+ const target = rec(r.targetJob);
2293
+ const meta = [
2294
+ target ? [str2(target.title), str2(target.companyName)].filter(Boolean).join(" at ") || void 0 : void 0,
2295
+ r.isPublic === true ? "public" : void 0,
2296
+ when(r.updatedAt, opts.now) ? `updated ${when(r.updatedAt, opts.now)}` : void 0,
2297
+ id
2298
+ ].filter((part) => Boolean(part));
2299
+ const body = excerpt(r.summary, 240);
2300
+ return card("cv-detail", "CV", [
2301
+ { n: 1, title: title ?? "CV", meta, ...body ? { body } : {}, ...id ? { action: `/download ${id}` } : {} }
2302
+ ]);
2303
+ }
2304
+ function cvExportCard(result) {
2305
+ const r = rec(result);
2306
+ if (!r || !Array.isArray(r.formats)) return null;
2307
+ const id = str2(r.cvId) ?? "master";
2308
+ const formats = r.formats.filter(isRec).map((f) => {
2309
+ const label = str2(f.label) ?? str2(f.format)?.toUpperCase() ?? "";
2310
+ return f.locked === true ? `${label} (paid plans)` : label;
2311
+ });
2312
+ return card("cv-export", "CV export", [
2313
+ {
2314
+ n: 1,
2315
+ title: str2(r.cvTitle) ?? "My CV",
2316
+ meta: [r.isTailored === true ? "tailored CV" : "main CV", ...formats.filter(Boolean)],
2317
+ action: `/download ${id}`
2318
+ }
2319
+ ]);
2320
+ }
2321
+ function cvShareCard(result, opts, grant) {
2322
+ const r = rec(result);
2323
+ if (!r) return null;
2324
+ const url = httpUrl(grant ? r.inviteUrl : r.shareUrl) ?? httpUrl(r.shareUrl) ?? httpUrl(r.inviteUrl);
2325
+ const title = str2(r.cvTitle) ?? "Your CV";
2326
+ const permissions = Array.isArray(r.permissions) ? r.permissions.filter((p) => typeof p === "string") : [];
2327
+ const expires = when(r.expiresAt, opts.now);
2328
+ const meta = [
2329
+ r.isTailored === true ? "tailored CV" : "main CV",
2330
+ grant ? str2(r.granteeEmail) : void 0,
2331
+ grant ? str2(r.role) : void 0,
2332
+ grant ? str2(r.status) : void 0,
2333
+ permissions.length ? permissions.join(", ") : void 0,
2334
+ r.allowDownload === true ? "downloads allowed" : void 0,
2335
+ grant ? void 0 : expires ? `expires ${expires}` : "no expiry"
2336
+ ].filter((part) => Boolean(part));
2337
+ const row = { n: 1, title, meta, ...url ? { link: { n: 1, label: grant ? "Invite link" : "Share link", url } } : {} };
2338
+ return card("cv-share", grant ? "CV access invite" : "CV share link", [row], url ? { hint: "/open 1 opens the link" } : {});
2339
+ }
2340
+ function cvPublishCard(result, opts, published) {
2341
+ const r = rec(result);
2342
+ if (!r) return null;
2343
+ const handle = str2(r.username) ?? str2(r.handle);
2344
+ const url = published ? handle ? buildPersonCvUrl(handle, opts.siteUrl) : httpUrl(r.publicUrl) : void 0;
2345
+ const title = str2(r.title) ?? "CV";
2346
+ const meta = [published ? "public" : "private", str2(r.id)].filter((part) => Boolean(part));
2347
+ const row = { n: 1, title, meta, ...url ? { link: { n: 1, label: title, url } } : {} };
2348
+ return card("cv-publish", published ? "Published CV" : "CV unpublished", [row], url ? { hint: "/open 1 opens the public CV" } : {});
2349
+ }
2350
+
2351
+ // src/core/chat-cards.ts
2352
+ var NO_CARD_TOOLS = /* @__PURE__ */ new Set([
2353
+ "updateWorkingMemory",
2354
+ "clearWorkingMemory",
2355
+ "getWorkingMemory",
2356
+ "tailoredCvFlow",
2357
+ "seekerApplyFlow",
2358
+ "autoApplyQueueFlow",
2359
+ "catalogApplyFlow",
2360
+ "catalogAutoApplyQueueFlow",
2361
+ "documentIngestionFlow",
2362
+ "employerJobPostFlow",
2363
+ "candidateOutreachFlow",
2364
+ "talentPipelineFlow",
2365
+ "skillsLoad",
2366
+ "skillPrepareRun",
2367
+ "cvCompareWithMaster",
2368
+ "novaPause",
2369
+ "novaResume",
2370
+ "exportMemory",
2371
+ "memoryGraphGet",
2372
+ "artifactRead"
2373
+ ]);
2374
+ var KIND_BY_TOOL = {
2375
+ jobSearch: "job-list",
2376
+ jobRecommend: "job-list",
2377
+ catalogJobSearch: "job-list",
2378
+ catalogSavedJobList: "job-list",
2379
+ jobListByCompany: "job-list",
2380
+ jobGet: "job-detail",
2381
+ catalogJobGet: "job-detail",
2382
+ jobDraft: "job-detail",
2383
+ jobUpdate: "job-detail",
2384
+ jobPublish: "job-detail",
2385
+ jobClose: "job-detail",
2386
+ applicationPrepare: "application-preview",
2387
+ catalogApplyPrepare: "application-preview",
2388
+ applicationSubmit: "application-confirm",
2389
+ catalogApplySubmit: "application-confirm",
2390
+ applicationGet: "application-confirm",
2391
+ applicationUpdateStatus: "application-confirm",
2392
+ applicationWithdraw: "application-confirm",
2393
+ applicationTrack: "application-list",
2394
+ catalogApplyTrack: "application-list",
2395
+ applicationListByJob: "application-list",
2396
+ cvList: "cv-list",
2397
+ cvCreate: "cv-detail",
2398
+ cvDuplicate: "cv-detail",
2399
+ cvTailor: "cv-detail",
2400
+ cvUpdateSummary: "cv-detail",
2401
+ cvGet: "cv-detail",
2402
+ cvUpdate: "cv-detail",
2403
+ cvUpdateOverrides: "cv-detail",
2404
+ cvSetFitToOnePage: "cv-detail",
2405
+ cvGetResolved: "cv-detail",
2406
+ cvExport: "cv-export",
2407
+ cvShareLinkCreate: "cv-share",
2408
+ cvAccessGrantCreate: "cv-share",
2409
+ cvPublish: "cv-publish",
2410
+ cvUnpublish: "cv-publish",
2411
+ skillsList: "skill-list",
2412
+ novaList: "automation-list",
2413
+ novaRunHistory: "automation-list",
2414
+ novaCompile: "automation-detail",
2415
+ novaActivate: "automation-detail",
2416
+ novaRunNow: "automation-run",
2417
+ profileGet: "profile-summary",
2418
+ companyList: "company-list"
2419
+ };
2420
+ var KNOWN_CARD_TYPES = /* @__PURE__ */ new Set([
2421
+ "job-list",
2422
+ "job-detail",
2423
+ "application-preview",
2424
+ "application-confirm",
2425
+ "application-list",
2426
+ "cv-detail",
2427
+ "cv-export",
2428
+ "cv-share",
2429
+ "cv-publish",
2430
+ "skill-list",
2431
+ "automation-list",
2432
+ "automation-detail",
2433
+ "automation-run",
2434
+ "profile-summary",
2435
+ "company-list"
2436
+ ]);
2437
+ function kindOf(payload) {
2438
+ const byName = KIND_BY_TOOL[payload.name];
2439
+ if (byName) return byName;
2440
+ if (payload.cardType === "automation-run-list") return "automation-list";
2441
+ return payload.cardType && KNOWN_CARD_TYPES.has(payload.cardType) ? payload.cardType : "generic";
2442
+ }
2443
+ function isFailure(result) {
2444
+ if (!isRec(result)) return false;
2445
+ return result.ok === false || result.error === true || isRec(result.error) && typeof result.error.code === "string";
2446
+ }
2447
+ function cardTitle(payload) {
2448
+ const raw = payload.display?.title?.trim() || payload.name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
2449
+ const text = raw.replace(/\s+/g, " ").trim();
2450
+ return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
2451
+ }
2452
+ function cardFromToolResult(payload, opts) {
2453
+ const { result } = payload;
2454
+ if (result === void 0 || result === null) return null;
2455
+ if (payload.stage === "failed" || payload.status === "failed" || isFailure(result)) return null;
2456
+ if (NO_CARD_TOOLS.has(payload.name) || isResearchTool(payload) || isArtifactTool(payload)) return null;
2457
+ if (payload.cardType === "artifact-suggestions" || payload.cardType === "site-publish") return null;
2458
+ if (isRec(result) && result.approvalRequired === true) return null;
2459
+ const build = { siteUrl: publicSiteUrl(opts.appUrl), now: opts.now ?? Date.now() };
2460
+ switch (kindOf(payload)) {
2461
+ case "job-list":
2462
+ return jobListCard(result, build);
2463
+ case "job-detail":
2464
+ return jobDetailCard(result, build);
2465
+ case "application-preview":
2466
+ return applicationPreviewCard(result, build);
2467
+ case "application-confirm":
2468
+ return applicationConfirmCard(result, build);
2469
+ case "application-list":
2470
+ return applicationListCard(result, build);
2471
+ case "cv-list":
2472
+ return cvListCard(result, build);
2473
+ case "cv-detail":
2474
+ return isRec(result) && Array.isArray(result.cvs) ? cvListCard(result, build) : cvDetailCard(result, build);
2475
+ case "cv-export":
2476
+ return cvExportCard(result);
2477
+ case "cv-share":
2478
+ return cvShareCard(result, build, payload.name === "cvAccessGrantCreate");
2479
+ case "cv-publish":
2480
+ return cvPublishCard(result, build, payload.name !== "cvUnpublish");
2481
+ case "skill-list":
2482
+ return skillListCard(result);
2483
+ case "automation-list":
2484
+ return automationListCard(result, build, payload.name === "novaRunHistory" || payload.cardType === "automation-run-list");
2485
+ case "automation-detail":
2486
+ return automationDetailCard(result, build, false);
2487
+ case "automation-run":
2488
+ return automationDetailCard(result, build, true);
2489
+ case "profile-summary":
2490
+ return profileSummaryCard(result);
2491
+ case "company-list":
2492
+ return companyListCard(result);
2493
+ default:
2494
+ return genericCard(result, cardTitle(payload));
2495
+ }
2496
+ }
2497
+ function latestCardLinks(cards) {
2498
+ for (let i = cards.length - 1; i >= 0; i -= 1) {
2499
+ const card2 = cards[i];
2500
+ if (card2.links.length) return card2.links;
2501
+ }
2502
+ return [];
2503
+ }
2504
+ function findCardLink(cards, n) {
2505
+ return latestCardLinks(cards).find((link) => link.n === n);
2506
+ }
2507
+ function findCardJob(cards, n) {
2508
+ for (let i = cards.length - 1; i >= 0; i -= 1) {
2509
+ const rows = cards[i].rows.filter((row) => row.job);
2510
+ if (rows.length) return rows.find((row) => row.n === n)?.job;
2511
+ }
2512
+ return void 0;
2513
+ }
2514
+
1337
2515
  // src/core/chat-tool-extras.ts
1338
2516
  function holdsArtifact(call) {
1339
2517
  const artifact = call.artifact;
@@ -1373,12 +2551,14 @@ function patchArtifact(artifact, action, links) {
1373
2551
  ...artifact,
1374
2552
  ...action.status ? { status: action.status } : {},
1375
2553
  ...action.title ? { title: action.title } : {},
2554
+ ...action.versionId ? { versionId: action.versionId } : {},
2555
+ ...action.versionNumber !== void 0 ? { versionNumber: action.versionNumber } : {},
1376
2556
  ...action.previewUrl ? { previewUrl: action.previewUrl } : {},
1377
2557
  ...action.failure ? { failure: action.failure } : {},
1378
2558
  ...action.release ? { released: true } : {}
1379
2559
  };
1380
- const linked = withLinkState(next, links && !action.release);
1381
- return action.previewUrl === void 0 && action.status === void 0 && artifact.status === "ready" ? { ...linked, linkPending: false } : linked;
2560
+ const linked2 = withLinkState(next, links && !action.release);
2561
+ return action.previewUrl === void 0 && action.status === void 0 && artifact.status === "ready" ? { ...linked2, linkPending: false } : linked2;
1382
2562
  }
1383
2563
  function lastShownArtifact(entries, artifactId) {
1384
2564
  for (let i = entries.length - 1; i >= 0; i -= 1) {
@@ -1389,7 +2569,114 @@ function lastShownArtifact(entries, artifactId) {
1389
2569
  return void 0;
1390
2570
  }
1391
2571
 
1392
- // src/core/chat-transcript.ts
2572
+ // src/core/chat-text.ts
2573
+ var ECHO_HOLD_CHARS = 24;
2574
+ var ECHO_COLLAPSE_CHARS = 80;
2575
+ var WHOLE_REPEAT_CHARS = 16;
2576
+ function compact(raw) {
2577
+ let text = "";
2578
+ const index = [];
2579
+ for (let i = 0; i < raw.length; i += 1) {
2580
+ const ch = raw[i];
2581
+ if (ch === " " || ch === "\n" || ch === " " || ch === "\r" || ch === "\f" || ch === "\v" || ch === "\xA0") continue;
2582
+ text += ch;
2583
+ index.push(i);
2584
+ }
2585
+ return { text, index };
2586
+ }
2587
+ function compactKey(raw) {
2588
+ return compact(raw).text;
2589
+ }
2590
+ function rawOffsetAfter(raw, n) {
2591
+ if (n <= 0) return 0;
2592
+ const { index } = compact(raw);
2593
+ const last = index[Math.min(n, index.length) - 1];
2594
+ return last === void 0 ? 0 : last + 1;
2595
+ }
2596
+ function lineStarts(raw) {
2597
+ const starts = [];
2598
+ for (let i = raw.indexOf("\n"); i !== -1; i = raw.indexOf("\n", i + 1)) starts.push(i + 1);
2599
+ return starts;
2600
+ }
2601
+ function findEcho(raw, starts = []) {
2602
+ const c = compact(raw);
2603
+ const n = c.text.length;
2604
+ if (n < 2 * ECHO_HOLD_CHARS) return null;
2605
+ const candidates = /* @__PURE__ */ new Set();
2606
+ const toCompact = (offset) => {
2607
+ let lo = 0;
2608
+ let hi = c.index.length;
2609
+ while (lo < hi) {
2610
+ const mid = lo + hi >> 1;
2611
+ if (c.index[mid] < offset) lo = mid + 1;
2612
+ else hi = mid;
2613
+ }
2614
+ return lo;
2615
+ };
2616
+ for (const offset of [0, ...starts, ...lineStarts(raw)]) candidates.add(toCompact(offset));
2617
+ let best = null;
2618
+ for (const q of [...candidates].sort((a, b) => a - b)) {
2619
+ if (n - q < 2 * ECHO_HOLD_CHARS) continue;
2620
+ const opening = c.text.slice(q, q + ECHO_HOLD_CHARS);
2621
+ for (let p = c.text.indexOf(opening, q + ECHO_HOLD_CHARS); p !== -1; p = c.text.indexOf(opening, p + 1)) {
2622
+ const tail = n - p;
2623
+ if (tail > p - q) continue;
2624
+ if (!c.text.startsWith(c.text.slice(p), q)) continue;
2625
+ const echo = {
2626
+ at: c.index[p],
2627
+ cut: c.index[p - 1] + 1,
2628
+ complete: tail === p - q,
2629
+ length: tail,
2630
+ fromStart: q === 0
2631
+ };
2632
+ if (!best || echo.at < best.at) best = echo;
2633
+ break;
2634
+ }
2635
+ }
2636
+ return best;
2637
+ }
2638
+ function repeatCut(raw, starts = []) {
2639
+ const c = compact(raw);
2640
+ const n = c.text.length;
2641
+ if (n % 2 === 0 && n / 2 >= WHOLE_REPEAT_CHARS && c.text.slice(0, n / 2) === c.text.slice(n / 2)) {
2642
+ return c.index[n / 2 - 1] + 1;
2643
+ }
2644
+ const echo = findEcho(raw, starts);
2645
+ return echo?.complete && echo.length >= ECHO_COLLAPSE_CHARS ? echo.cut : null;
2646
+ }
2647
+ function collapseRepeat(raw, starts = []) {
2648
+ const cut = repeatCut(raw, starts);
2649
+ return cut === null ? raw : raw.slice(0, cut).trimEnd();
2650
+ }
2651
+ function isSnapshotHead(head, full) {
2652
+ let rest = head;
2653
+ while (rest) {
2654
+ let k = 0;
2655
+ while (k < rest.length && k < full.length && rest[k] === full[k]) k += 1;
2656
+ if (k === 0) return false;
2657
+ rest = rest.slice(k);
2658
+ }
2659
+ return true;
2660
+ }
2661
+ function planFinal(streamed, finalText) {
2662
+ const final = collapseRepeat(finalText.trim());
2663
+ const f = compactKey(final);
2664
+ const s = compactKey(streamed);
2665
+ if (!f || f === s) return { kind: "same" };
2666
+ if (!s) return { kind: "different", text: final };
2667
+ if (f.startsWith(s)) {
2668
+ const rest = final.slice(rawOffsetAfter(final, s.length));
2669
+ const r = f.slice(s.length);
2670
+ if (!rest.trim() || r.length >= WHOLE_REPEAT_CHARS && s.endsWith(r)) return { kind: "same" };
2671
+ return { kind: "append", text: rest };
2672
+ }
2673
+ if (s.startsWith(f)) return { kind: "same" };
2674
+ if (f.endsWith(s) && isSnapshotHead(f.slice(0, f.length - s.length), s)) return { kind: "same" };
2675
+ return { kind: "different", text: final };
2676
+ }
2677
+
2678
+ // src/core/chat-draft.ts
2679
+ var HIDDEN_CALLS_KEPT = 20;
1393
2680
  function nextId(d, prefix) {
1394
2681
  d.state = { ...d.state, seq: d.state.seq + 1 };
1395
2682
  return `${prefix}-${d.state.seq}`;
@@ -1405,17 +2692,55 @@ function commitMarkdown(d, turn, text) {
1405
2692
  push(d, { kind: "markdown", id: nextId(d, "md"), text, gap: turn.lastKind !== "label" });
1406
2693
  return { ...turn, committedText: true, lastKind: "markdown" };
1407
2694
  }
2695
+ function commitTool(d, turn, call) {
2696
+ const { card: card2, saved, ...row } = call;
2697
+ push(d, { kind: "tool", id: nextId(d, "tool"), call: row, gap: turn.lastKind === "markdown" });
2698
+ if (card2) push(d, { kind: "card", id: nextId(d, "card"), card: card2, toolCallId: call.id });
2699
+ if (saved) push(d, { kind: "saved", id: nextId(d, "saved"), saved });
2700
+ return { ...turn, lastKind: "tool" };
2701
+ }
2702
+ function closeOpenText(items) {
2703
+ return items.map((item) => item.kind === "text" && !item.closed ? { ...item, closed: true } : item);
2704
+ }
2705
+ function dropTextAfter(turn, cut) {
2706
+ let end = cut;
2707
+ const items = turn.items.flatMap((item) => {
2708
+ if (item.kind !== "text" || item.start === void 0) return [item];
2709
+ const keep = Math.max(cut - item.start, item.committed);
2710
+ if (keep >= item.text.length) return [item];
2711
+ end = Math.max(end, item.start + keep);
2712
+ return keep > 0 || item.committed > 0 ? [{ ...item, text: item.text.slice(0, keep) }] : [];
2713
+ });
2714
+ const { echoAt: _echoAt, ...rest } = turn;
2715
+ return { ...rest, items, text: turn.text.slice(0, end) };
2716
+ }
2717
+ function hasPendingText(turn) {
2718
+ return turn.items.some((item) => item.kind === "text" && item.text.length > item.committed);
2719
+ }
2720
+ function guardRepeat(turn) {
2721
+ if (!hasPendingText(turn)) return turn.echoAt === void 0 ? turn : { ...turn, echoAt: void 0 };
2722
+ const echo = findEcho(turn.text, turn.segments ?? []);
2723
+ if (echo?.complete && echo.length >= ECHO_COLLAPSE_CHARS) return dropTextAfter(turn, echo.cut);
2724
+ if (echo?.at === turn.echoAt) return turn;
2725
+ return { ...turn, echoAt: echo?.at };
2726
+ }
2727
+ function committable(item, echoAt) {
2728
+ const pending = item.text.slice(item.committed);
2729
+ if (echoAt === void 0 || item.start === void 0) return stableBoundary(pending);
2730
+ const limit = echoAt - item.start - item.committed;
2731
+ if (limit <= 0) return 0;
2732
+ return limit >= pending.length ? stableBoundary(pending) : stableBoundary(pending.slice(0, limit));
2733
+ }
1408
2734
  function promote(d, final, force = false) {
1409
- let turn = d.state.turn;
1410
- if (!turn) return;
2735
+ if (!d.state.turn) return;
2736
+ let turn = final ? d.state.turn : guardRepeat(d.state.turn);
1411
2737
  const items = [...turn.items];
1412
2738
  while (items.length > 0) {
1413
2739
  const item = items[0];
1414
2740
  if (item.kind === "tool") {
1415
2741
  if (!force && (!FINAL_STAGES.has(item.call.stage) && !item.call.left || holdsArtifact(item.call))) break;
1416
2742
  const call = item.call.artifact ? { ...item.call, artifact: { ...item.call.artifact, linkPending: false } } : item.call;
1417
- push(d, { kind: "tool", id: nextId(d, "tool"), call, gap: turn.lastKind === "markdown" });
1418
- turn = { ...turn, lastKind: "tool" };
2743
+ turn = commitTool(d, turn, call);
1419
2744
  items.shift();
1420
2745
  continue;
1421
2746
  }
@@ -1425,7 +2750,7 @@ function promote(d, final, force = false) {
1425
2750
  items.shift();
1426
2751
  continue;
1427
2752
  }
1428
- const boundary = stableBoundary(pending);
2753
+ const boundary = committable(item, turn.echoAt);
1429
2754
  if (boundary > 0) {
1430
2755
  turn = commitMarkdown(d, turn, pending.slice(0, boundary));
1431
2756
  items[0] = { ...item, committed: item.committed + boundary };
@@ -1435,11 +2760,52 @@ function promote(d, final, force = false) {
1435
2760
  turn = { ...turn, items };
1436
2761
  const settled = (turn.finished || force) && items.length === 0;
1437
2762
  if (settled && turn.sources.length) push(d, { kind: "sources", id: nextId(d, "sources"), sources: turn.sources });
2763
+ if (settled && turn.hidden?.length) {
2764
+ const at = d.state.entries.length;
2765
+ const kept = [...d.state.hiddenCalls ?? [], ...turn.hidden.map((call) => ({ call, at }))].slice(-HIDDEN_CALLS_KEPT);
2766
+ d.state = { ...d.state, hiddenCalls: kept };
2767
+ }
1438
2768
  setTurn(d, settled ? null : turn);
1439
2769
  }
1440
- function closeOpenText(items) {
1441
- return items.map((item) => item.kind === "text" && !item.closed ? { ...item, closed: true } : item);
2770
+ function finishTurn(d, payload) {
2771
+ let turn = d.state.turn;
2772
+ if (!turn) return;
2773
+ const finalText = payload.text?.trim() ?? "";
2774
+ const cut = repeatCut(turn.text, turn.segments ?? []);
2775
+ if (cut !== null) turn = dropTextAfter(turn, cut);
2776
+ let items = closeOpenText(turn.items);
2777
+ let revision = null;
2778
+ let extraNote = null;
2779
+ if (payload.finishReason === "error") {
2780
+ if (!turn.error && finalText) turn = { ...turn, error: finalText };
2781
+ else if (finalText && normalizeText(finalText) !== normalizeText(turn.error ?? "")) extraNote = finalText;
2782
+ } else if (finalText) {
2783
+ const plan = planFinal(turn.text, finalText);
2784
+ if (plan.kind === "append") {
2785
+ const last = items[items.length - 1];
2786
+ items = last?.kind === "text" ? [...items.slice(0, -1), { ...last, text: last.text + plan.text }] : [...items, { kind: "text", id: "final", text: plan.text.replace(/^\s+/, ""), committed: 0, closed: true }];
2787
+ } else if (plan.kind === "different") {
2788
+ if (!turn.text.trim() || !turn.committedText) {
2789
+ items = [...items.filter((item) => item.kind !== "text"), { kind: "text", id: "final", text: plan.text, committed: 0, closed: true }];
2790
+ } else {
2791
+ revision = plan.text;
2792
+ }
2793
+ }
2794
+ }
2795
+ turn = { ...turn, items, finished: true, finishReason: payload.finishReason, echoAt: void 0 };
2796
+ if (payload.threadId) d.state = { ...d.state, threadId: payload.threadId };
2797
+ setTurn(d, turn);
2798
+ promote(d, true);
2799
+ if (revision) {
2800
+ push(d, { kind: "notice", id: nextId(d, "note"), tone: "info", text: "Tabbio revised this answer:" });
2801
+ push(d, { kind: "markdown", id: nextId(d, "md"), text: revision, gap: false });
2802
+ }
2803
+ if (turn.error) push(d, { kind: "notice", id: nextId(d, "note"), tone: "error", text: turn.error });
2804
+ if (extraNote) push(d, { kind: "notice", id: nextId(d, "note"), tone: "info", text: extraNote });
2805
+ d.state = { ...d.state, streaming: false };
1442
2806
  }
2807
+
2808
+ // src/core/chat-transcript.ts
1443
2809
  function upsertTool(d, payload, stage, extra = {}) {
1444
2810
  const turn = d.state.turn;
1445
2811
  if (!turn) return;
@@ -1448,6 +2814,10 @@ function upsertTool(d, payload, stage, extra = {}) {
1448
2814
  const previous = index >= 0 ? turn.items[index].call : void 0;
1449
2815
  const enriched = toolExtras(payload, stage, { links: d.state.links, previous });
1450
2816
  extra = { ...enriched, ...extra };
2817
+ if (final && payload.result !== void 0) {
2818
+ const card2 = cardFromToolResult({ ...payload, stage }, { appUrl: d.state.appUrl ?? PRODUCTION_APP_URL, now: d.now });
2819
+ if (card2) extra = { ...extra, card: card2 };
2820
+ }
1451
2821
  if (enriched.sources?.length) {
1452
2822
  setTurn(d, { ...turn, sources: mergeSources(turn.sources, enriched.sources) });
1453
2823
  }
@@ -1511,34 +2881,48 @@ function applyArtifactStatus(d, action) {
1511
2881
  if (next.status === shown.status && next.previewUrl === shown.previewUrl) return;
1512
2882
  push(d, { kind: "artifact", id: nextId(d, "artifact"), artifact: next });
1513
2883
  }
1514
- function finishTurn(d, payload) {
1515
- let turn = d.state.turn;
1516
- if (!turn) return;
1517
- const finalText = payload.text?.trim() ?? "";
1518
- let items = closeOpenText(turn.items);
1519
- let revision = null;
1520
- let extraNote = null;
1521
- if (payload.finishReason === "error") {
1522
- if (!turn.error && finalText) turn = { ...turn, error: finalText };
1523
- else if (finalText && normalizeText(finalText) !== normalizeText(turn.error ?? "")) extraNote = finalText;
1524
- } else if (finalText && normalizeText(finalText) !== normalizeText(turn.text)) {
1525
- if (!normalizeText(turn.text) || !turn.committedText) {
1526
- items = [...items.filter((item) => item.kind !== "text"), { kind: "text", id: "final", text: finalText, committed: 0, closed: true }];
1527
- } else {
1528
- revision = finalText;
1529
- }
1530
- }
1531
- turn = { ...turn, items, finished: true, finishReason: payload.finishReason };
1532
- if (payload.threadId) d.state = { ...d.state, threadId: payload.threadId };
1533
- setTurn(d, turn);
1534
- promote(d, true);
1535
- if (revision) {
1536
- push(d, { kind: "notice", id: nextId(d, "note"), tone: "info", text: "Tabbio revised this answer:" });
1537
- push(d, { kind: "markdown", id: nextId(d, "md"), text: revision, gap: false });
2884
+ function upsertHidden(d, payload, stage) {
2885
+ const turn = d.state.turn;
2886
+ const hidden = turn.hidden ?? [];
2887
+ const previous = hidden.find((call2) => call2.id === payload.id);
2888
+ const final = FINAL_STAGES.has(stage);
2889
+ const call = {
2890
+ ...previous ?? { id: payload.id, name: payload.name, title: toolTitle(payload), args: payload.args, startedAt: d.now },
2891
+ stage,
2892
+ ...Object.keys(payload.args).length ? { args: payload.args } : {},
2893
+ ...payload.result !== void 0 ? { result: payload.result } : {},
2894
+ ...final && previous && previous.endedAt === void 0 ? { endedAt: d.now, durationMs: Math.max(0, d.now - previous.startedAt) } : {},
2895
+ ...final && !previous ? { endedAt: d.now } : {}
2896
+ };
2897
+ const next = previous ? hidden.map((c) => c.id === payload.id ? call : c) : [...hidden, call];
2898
+ setTurn(d, { ...turn, hidden: next });
2899
+ }
2900
+ function expandLastTool(d) {
2901
+ const entries = d.state.entries;
2902
+ let lastUser = -1;
2903
+ let lastTool = -1;
2904
+ entries.forEach((entry, i) => {
2905
+ if (entry.kind === "user") lastUser = i;
2906
+ if (entry.kind === "tool") lastTool = i;
2907
+ });
2908
+ const hidden = (d.state.hiddenCalls ?? []).filter((h) => h.at > lastUser).at(-1);
2909
+ const visible = lastTool >= 0 ? entries[lastTool] : void 0;
2910
+ const call = visible && lastTool > lastUser ? visible.call : hidden?.call ?? visible?.call;
2911
+ if (call) push(d, { kind: "tool-detail", id: nextId(d, "detail"), call });
2912
+ }
2913
+ function applyFilesSaved(d, toolCallId, saved) {
2914
+ const turn = d.state.turn;
2915
+ const matches = (call) => toolCallId !== void 0 && call.id === toolCallId || saved.artifactId !== void 0 && call.artifact?.id === saved.artifactId;
2916
+ const index = turn?.items.findIndex((item) => item.kind === "tool" && matches(item.call)) ?? -1;
2917
+ if (turn && index >= 0) {
2918
+ const item = turn.items[index];
2919
+ const items = [...turn.items];
2920
+ items[index] = { ...item, call: { ...item.call, saved } };
2921
+ setTurn(d, { ...turn, items });
2922
+ promote(d, false);
2923
+ return;
1538
2924
  }
1539
- if (turn.error) push(d, { kind: "notice", id: nextId(d, "note"), tone: "error", text: turn.error });
1540
- if (extraNote) push(d, { kind: "notice", id: nextId(d, "note"), tone: "info", text: extraNote });
1541
- d.state = { ...d.state, streaming: false };
2925
+ push(d, { kind: "saved", id: nextId(d, "saved"), saved });
1542
2926
  }
1543
2927
  function applyStream(d, event) {
1544
2928
  const turn = d.state.turn;
@@ -1558,19 +2942,24 @@ function applyStream(d, event) {
1558
2942
  const current = d.state.turn;
1559
2943
  const items = [...current.items];
1560
2944
  const last = items[items.length - 1];
2945
+ let segments = current.segments;
1561
2946
  if (last?.kind === "text" && !last.closed) items[items.length - 1] = { ...last, text: last.text + event.payload.text };
1562
- else items.push({ kind: "text", id: nextId(d, "text"), text: event.payload.text, committed: 0, closed: false });
1563
- setTurn(d, { ...current, items, text: current.text + event.payload.text });
2947
+ else {
2948
+ const start = current.text.length;
2949
+ items.push({ kind: "text", id: nextId(d, "text"), text: event.payload.text, committed: 0, closed: false, start });
2950
+ segments = [...segments ?? [], start];
2951
+ }
2952
+ setTurn(d, { ...current, items, text: current.text + event.payload.text, ...segments ? { segments } : {} });
1564
2953
  return;
1565
2954
  }
1566
2955
  case "tool-call":
1567
- upsertTool(d, event.payload, stageOf(event.payload, "running"));
1568
- promote(d, false);
1569
- return;
1570
- case "tool-result":
1571
- upsertTool(d, event.payload, stageOf(event.payload, "completed"));
2956
+ case "tool-result": {
2957
+ const stage = stageOf(event.payload, event.type === "tool-call" ? "running" : "completed");
2958
+ if (isHiddenTool(event.payload.name)) upsertHidden(d, event.payload, stage);
2959
+ else upsertTool(d, event.payload, stage);
1572
2960
  promote(d, false);
1573
2961
  return;
2962
+ }
1574
2963
  case "artifact-draft":
1575
2964
  applyArtifactDraft(d, event.payload);
1576
2965
  return;
@@ -1707,11 +3096,12 @@ function reduceChat(state, action, now) {
1707
3096
  case "artifact-status":
1708
3097
  applyArtifactStatus(d, action);
1709
3098
  return d.state;
1710
- case "expand-last-tool": {
1711
- const last = [...d.state.entries].reverse().find((e) => e.kind === "tool");
1712
- if (last) push(d, { kind: "tool-detail", id: nextId(d, "detail"), call: last.call });
3099
+ case "files-saved":
3100
+ applyFilesSaved(d, action.toolCallId, action.saved);
3101
+ return d.state;
3102
+ case "expand-last-tool":
3103
+ expandLastTool(d);
1713
3104
  return d.state;
1714
- }
1715
3105
  default:
1716
3106
  applyStream(d, action);
1717
3107
  return d.state;
@@ -1894,13 +3284,148 @@ function messageText(content) {
1894
3284
  return "";
1895
3285
  }
1896
3286
 
3287
+ // src/core/chat-save-targets.ts
3288
+ var PRODUCING_TOOLS = /* @__PURE__ */ new Set(["generateImage", "createDocument", "createPage", "updatePage", "updateDocument", "editImage"]);
3289
+ function isRecord7(value) {
3290
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3291
+ }
3292
+ function cvExportFormat(requested, formats) {
3293
+ const open2 = (Array.isArray(formats) ? formats : []).filter(isRecord7).filter((f) => f.locked !== true && typeof f.format === "string").map((f) => String(f.format).toLowerCase());
3294
+ const asked = typeof requested === "string" ? requested.toLowerCase() : "";
3295
+ if (asked && open2.includes(asked)) return asked;
3296
+ if (open2.includes("pdf")) return "pdf";
3297
+ return open2[0] ?? null;
3298
+ }
3299
+ function cvTarget(call) {
3300
+ if (call.name !== "cvExport" || call.stage !== "completed" || !isRecord7(call.result)) return null;
3301
+ const format = cvExportFormat(call.args.format, call.result.formats);
3302
+ if (!format) return null;
3303
+ const cvId = typeof call.result.cvId === "string" && call.result.cvId ? call.result.cvId : "master";
3304
+ const title = typeof call.result.cvTitle === "string" && call.result.cvTitle ? call.result.cvTitle : "My CV";
3305
+ return { type: "cv", key: `cv:${call.id}:${format}`, cvId, title, format, toolCallId: call.id };
3306
+ }
3307
+ function artifactTarget(artifact, toolCallId) {
3308
+ if (artifact.status !== "ready") return null;
3309
+ return {
3310
+ type: "artifact",
3311
+ key: `artifact:${artifact.id}:${artifact.versionNumber ?? 0}`,
3312
+ artifactId: artifact.id,
3313
+ ...artifact.versionId ? { versionId: artifact.versionId } : {},
3314
+ title: artifact.title,
3315
+ kind: artifact.kind,
3316
+ ...toolCallId ? { toolCallId } : {}
3317
+ };
3318
+ }
3319
+ function saveTargets(state) {
3320
+ const targets = /* @__PURE__ */ new Map();
3321
+ const made = /* @__PURE__ */ new Set();
3322
+ const add = (target) => {
3323
+ if (target && !targets.has(target.key)) targets.set(target.key, target);
3324
+ };
3325
+ const addCall = (call) => {
3326
+ add(cvTarget(call));
3327
+ if (!call.artifact || !PRODUCING_TOOLS.has(call.name)) return;
3328
+ made.add(call.artifact.id);
3329
+ add(artifactTarget(call.artifact, call.id));
3330
+ };
3331
+ for (const entry of state.entries) {
3332
+ if (entry.kind === "tool") addCall(entry.call);
3333
+ else if (entry.kind === "artifact" && made.has(entry.artifact.id)) add(artifactTarget(entry.artifact));
3334
+ }
3335
+ for (const item of state.turn?.items ?? []) if (item.kind === "tool") addCall(item.call);
3336
+ return [...targets.values()];
3337
+ }
3338
+
3339
+ // src/ui/components/cards/card-lines.ts
3340
+ var MIN_WIDTH = 24;
3341
+ var MAX_LABEL = 16;
3342
+ function linked(text, link, links) {
3343
+ return link && links && safeTerminalText(link.url) === link.url ? hyperlink(text, link.url) : text;
3344
+ }
3345
+ function labelRowLines(row, numberWidth, pad2, labelWidth, width, links) {
3346
+ const label = theme.dim(padEnd(truncate(safeTerminalText(row.label ?? ""), labelWidth), labelWidth));
3347
+ const valueWidth = Math.max(8, width - displayWidth(pad2) - labelWidth - 2);
3348
+ const valuePad = `${pad2}${" ".repeat(labelWidth + 2)}`;
3349
+ const lead = row.n !== void 0 && numberWidth ? `${theme.dim(String(row.n).padStart(numberWidth))} ` : pad2;
3350
+ const lines = [];
3351
+ if (row.title) lines.push(`${lead}${label} ${linked(truncate(safeTerminalText(row.title), valueWidth), row.link, links)}`);
3352
+ const body = row.body ? wrapWords(safeTerminalText(row.body), valueWidth, row.bodyLines ?? 3) : [];
3353
+ body.forEach((line, i) => lines.push(i === 0 && !row.title ? `${lead}${label} ${line}` : `${valuePad}${line}`));
3354
+ if (row.link && row.link.url !== row.title) lines.push(`${valuePad}${theme.dim(safeTerminalText(row.link.url))}`);
3355
+ return lines;
3356
+ }
3357
+ function itemRowLines(row, numberWidth, pad2, width, links) {
3358
+ const contentWidth = Math.max(8, width - displayWidth(pad2));
3359
+ const number = row.n !== void 0 && numberWidth ? `${theme.dim(String(row.n).padStart(numberWidth))} ` : pad2;
3360
+ const url = row.link && row.link.url !== row.title ? safeTerminalText(row.link.url) : void 0;
3361
+ const title = truncate(safeTerminalText(row.title), contentWidth);
3362
+ const shown = linked(theme.bold(title), row.link, links);
3363
+ const lines = [];
3364
+ if (url && displayWidth(title) + 2 + displayWidth(url) <= contentWidth) lines.push(`${number}${shown} ${theme.dim(url)}`);
3365
+ else {
3366
+ lines.push(`${number}${shown}`);
3367
+ if (url) lines.push(`${pad2}${theme.dim(url)}`);
3368
+ }
3369
+ if (row.meta?.length) lines.push(`${pad2}${theme.dim(truncate(row.meta.map(safeTerminalText).join(` ${theme.symbols.middot} `), contentWidth))}`);
3370
+ if (row.body) for (const line of wrapWords(safeTerminalText(row.body), contentWidth, row.bodyLines ?? 3)) lines.push(`${pad2}${line}`);
3371
+ if (row.action && row.action !== row.title) lines.push(`${pad2}${theme.dim(`${theme.symbols.arrowRight} ${safeTerminalText(row.action)}`)}`);
3372
+ return lines;
3373
+ }
3374
+ function cardLines(card2, opts) {
3375
+ const width = Math.max(MIN_WIDTH, opts.width);
3376
+ const numbers = card2.rows.filter((row) => row.n !== void 0).map((row) => row.n);
3377
+ const numberWidth = numbers.length ? String(Math.max(...numbers)).length : 0;
3378
+ const pad2 = " ".repeat(numberWidth ? numberWidth + 2 : 0);
3379
+ const labelWidth = Math.min(MAX_LABEL, Math.max(0, ...card2.rows.filter((row) => row.label).map((row) => displayWidth(safeTerminalText(row.label ?? "")))));
3380
+ const lines = [theme.dim(truncate(safeTerminalText(card2.title), width))];
3381
+ for (const row of card2.rows) {
3382
+ lines.push(...row.label ? labelRowLines(row, numberWidth, pad2, labelWidth, width, opts.links) : itemRowLines(row, numberWidth, pad2, width, opts.links));
3383
+ }
3384
+ if (card2.more) lines.push(`${pad2}${theme.dim(`+${card2.more} more`)}`);
3385
+ if (card2.hint) {
3386
+ const arrow = `${theme.symbols.arrowRight} `;
3387
+ wrapWords(safeTerminalText(card2.hint), width - displayWidth(arrow)).forEach((line, i) => lines.push(theme.dim(`${i === 0 ? arrow : " ".repeat(displayWidth(arrow))}${line}`)));
3388
+ }
3389
+ return lines;
3390
+ }
3391
+ function printable(line) {
3392
+ return line.replace(/\t/g, " ").replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
3393
+ }
3394
+ function savedLines(saved, opts) {
3395
+ const width = Math.max(MIN_WIDTH, opts.width);
3396
+ const retry = saved.artifactId ? `/save ${safeTerminalText(saved.artifactId)}` : saved.cvId ? `/download ${safeTerminalText(saved.cvId)}` : null;
3397
+ if (saved.error) {
3398
+ const what = saved.title ? `"${safeTerminalText(saved.title)}"` : safeTerminalText(saved.artifactId ?? "the file");
3399
+ return [
3400
+ `${theme.attention(theme.symbols.attention)} Could not save ${what}: ${safeTerminalText(saved.error)}`,
3401
+ ...retry ? [theme.dim(` ${theme.symbols.arrowRight} ${retry} to try again`)] : []
3402
+ ];
3403
+ }
3404
+ const lines = saved.files.map(
3405
+ (file) => `${theme.success(theme.symbols.success)} Saved ${displayPath(file.path, opts.cwd)} ${theme.dim(`(${formatBytes(file.bytes)})`)}`
3406
+ );
3407
+ const preview = saved.preview;
3408
+ if (preview?.lines.length) {
3409
+ const inner = Math.max(8, width - 4);
3410
+ for (const line of preview.lines) lines.push(theme.dim(` ${theme.symbols.gutter} ${truncate(printable(line), inner)}`));
3411
+ const hidden = preview.total - preview.lines.length;
3412
+ const hints = [
3413
+ hidden > 0 ? `+${hidden} lines` : null,
3414
+ saved.artifactId ? `/show ${saved.artifactId} for the full file` : null,
3415
+ saved.artifactId ? `/open ${saved.artifactId} in the browser` : null
3416
+ ].filter((hint) => Boolean(hint));
3417
+ if (hints.length) lines.push(theme.dim(` ${hints.join(` ${theme.symbols.middot} `)}`));
3418
+ }
3419
+ return lines;
3420
+ }
3421
+
1897
3422
  export {
1898
3423
  setInteractiveHooks,
1899
3424
  getInteractiveHooks,
1900
3425
  canUseInteractiveUi,
1901
3426
  getPath,
1902
- displayWidth,
1903
- truncate,
3427
+ displayWidth2 as displayWidth,
3428
+ truncate2 as truncate,
1904
3429
  humanCell,
1905
3430
  pickColumns,
1906
3431
  formatTable,
@@ -1914,6 +3439,7 @@ export {
1914
3439
  formatDuration,
1915
3440
  printEmptyNotice,
1916
3441
  renderResult,
3442
+ confirm,
1917
3443
  ARTIFACT_PRODUCING_TOOL_IDS,
1918
3444
  artifactFromResult,
1919
3445
  sourcesFromResult,
@@ -1937,8 +3463,29 @@ export {
1937
3463
  resolveOutputPath,
1938
3464
  waitForArtifact,
1939
3465
  downloadToFile,
3466
+ CV_EXPORT_FORMATS,
3467
+ artifactsDir,
3468
+ autoSaveDisabled,
3469
+ artifactBaseName,
3470
+ uniquePath,
3471
+ displayPath,
3472
+ formatBytes,
3473
+ isTextFile,
3474
+ saveArtifactToDisk,
3475
+ saveCvExportToDisk,
3476
+ readSavedPreview,
1940
3477
  supportsHyperlinks,
1941
3478
  hyperlink,
3479
+ RESERVED_GLOBAL_FLAGS,
3480
+ shortDescription,
3481
+ schemaToFlags,
3482
+ schemaToOptions,
3483
+ didYouMean,
3484
+ parseToolInput,
3485
+ flagsForProperties,
3486
+ describeSchema,
3487
+ autoFilledProperties,
3488
+ exampleInvocation,
1942
3489
  DEFAULT_CAPABILITIES,
1943
3490
  CAPABILITY_CHOICES,
1944
3491
  CAPABILITY_CHOICE_HELP,
@@ -1948,8 +3495,14 @@ export {
1948
3495
  resolveCapabilities,
1949
3496
  capabilityRequestFields,
1950
3497
  unavailableNotices,
3498
+ cardFromToolResult,
3499
+ latestCardLinks,
3500
+ findCardLink,
3501
+ findCardJob,
1951
3502
  initialChatState,
1952
3503
  isDestructiveTool,
3504
+ collapseRepeat,
3505
+ planFinal,
1953
3506
  chatReducer,
1954
3507
  streamChat,
1955
3508
  streamChatFrames,
@@ -1959,18 +3512,17 @@ export {
1959
3512
  rejectApproval,
1960
3513
  listModels,
1961
3514
  messageText,
1962
- RESERVED_GLOBAL_FLAGS,
1963
- shortDescription,
1964
- schemaToFlags,
1965
- schemaToOptions,
1966
- didYouMean,
1967
- parseToolInput,
1968
- flagsForProperties,
1969
- describeSchema,
1970
- autoFilledProperties,
1971
- exampleInvocation,
3515
+ PRODUCING_TOOLS,
3516
+ cvExportFormat,
3517
+ saveTargets,
3518
+ cardLines,
3519
+ savedLines,
1972
3520
  isInteractiveTerminal,
1973
3521
  assertInteractive,
1974
- renderScreen
3522
+ renderScreen,
3523
+ saveAppSession,
3524
+ saveTokenSession,
3525
+ describeUser,
3526
+ registerLoginCommand
1975
3527
  };
1976
- //# sourceMappingURL=chunk-QVR5KIEQ.js.map
3528
+ //# sourceMappingURL=chunk-WRDHOZD7.js.map