@tabbio-technologies/cli 1.2.8 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +86 -19
- package/dist/chunks/{chunk-7LNC3FIV.js → chunk-GGJEDG3P.js} +95 -14
- package/dist/chunks/{chunk-7LNC3FIV.js.map → chunk-GGJEDG3P.js.map} +4 -4
- package/dist/chunks/{chunk-Y6GWIFHI.js → chunk-KBIQKZUK.js} +3 -53
- package/dist/chunks/chunk-KBIQKZUK.js.map +7 -0
- package/dist/chunks/{chunk-QVR5KIEQ.js → chunk-KHLGZ5IV.js} +1796 -244
- package/dist/chunks/chunk-KHLGZ5IV.js.map +7 -0
- package/dist/chunks/{chunk-NAWA6NCZ.js → chunk-NRFVKK3S.js} +37 -96
- package/dist/chunks/chunk-NRFVKK3S.js.map +7 -0
- package/dist/chunks/chunk-TP3CRQJD.js +106 -0
- package/dist/chunks/chunk-TP3CRQJD.js.map +7 -0
- package/dist/chunks/{entry-WENOOT6W.js → entry-ZAXWZSEE.js} +2023 -770
- package/dist/chunks/entry-ZAXWZSEE.js.map +7 -0
- package/dist/chunks/{mount-NGUARMCL.js → mount-36JUVX3R.js} +3 -2
- package/dist/chunks/{mount-NGUARMCL.js.map → mount-36JUVX3R.js.map} +1 -1
- package/dist/chunks/{program-O2BA5L6Z.js → program-J6GENE2V.js} +314 -375
- package/dist/chunks/program-J6GENE2V.js.map +7 -0
- package/dist/chunks/{status-I5UAQPBY.js → status-2C7BECKK.js} +3 -3
- package/dist/cli.js +1 -1
- package/package.json +4 -8
- package/dist/chunks/chunk-NAWA6NCZ.js.map +0 -7
- package/dist/chunks/chunk-QVR5KIEQ.js.map +0 -7
- package/dist/chunks/chunk-Y6GWIFHI.js.map +0 -7
- package/dist/chunks/entry-WENOOT6W.js.map +0 -7
- package/dist/chunks/program-O2BA5L6Z.js.map +0 -7
- /package/dist/chunks/{status-I5UAQPBY.js.map → status-2C7BECKK.js.map} +0 -0
|
@@ -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
|
-
|
|
41
|
+
usageError,
|
|
42
|
+
warn,
|
|
43
|
+
writeOut
|
|
44
|
+
} from "./chunk-GGJEDG3P.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-
|
|
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
|
|
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
|
|
609
|
-
if (
|
|
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 -
|
|
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 =
|
|
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 -
|
|
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(
|
|
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(
|
|
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]) =>
|
|
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} ${
|
|
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
|
|
788
|
-
return
|
|
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 =
|
|
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((
|
|
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
|
|
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-${
|
|
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:
|
|
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
|
|
1381
|
-
return action.previewUrl === void 0 && action.status === void 0 && artifact.status === "ready" ? { ...
|
|
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-
|
|
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
|
-
|
|
1410
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
1441
|
-
|
|
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
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
const
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
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
|
-
|
|
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
|
|
1563
|
-
|
|
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
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
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 "
|
|
1711
|
-
|
|
1712
|
-
|
|
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
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
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-
|
|
3528
|
+
//# sourceMappingURL=chunk-KHLGZ5IV.js.map
|