@vellumai/assistant 0.4.11 → 0.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/ARCHITECTURE.md +401 -385
  2. package/package.json +1 -1
  3. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
  4. package/src/__tests__/registry.test.ts +235 -187
  5. package/src/__tests__/secure-keys.test.ts +27 -0
  6. package/src/__tests__/session-agent-loop.test.ts +521 -256
  7. package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
  8. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  9. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  10. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  11. package/src/__tests__/skills.test.ts +334 -276
  12. package/src/__tests__/starter-task-flow.test.ts +7 -17
  13. package/src/agent/loop.ts +9 -2
  14. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
  15. package/src/config/bundled-skills/doordash/SKILL.md +171 -0
  16. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
  17. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
  18. package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
  19. package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
  20. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
  21. package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
  22. package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
  23. package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
  24. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
  25. package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
  26. package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
  27. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
  28. package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
  29. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
  30. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
  31. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
  32. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
  33. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
  34. package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
  35. package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
  36. package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
  37. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +41 -41
  38. package/src/config/bundled-skills/messaging/SKILL.md +59 -42
  39. package/src/config/bundled-skills/messaging/TOOLS.json +2 -2
  40. package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
  41. package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
  42. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +10 -3
  43. package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
  44. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
  45. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -1
  46. package/src/config/bundled-skills/notion/SKILL.md +240 -0
  47. package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
  48. package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
  49. package/src/config/bundled-skills/phone-calls/SKILL.md +76 -45
  50. package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
  51. package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
  52. package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
  53. package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +20 -5
  54. package/src/config/bundled-tool-registry.ts +281 -267
  55. package/src/daemon/handlers/skills.ts +334 -234
  56. package/src/daemon/ipc-contract/messages.ts +2 -0
  57. package/src/daemon/ipc-contract/surfaces.ts +2 -0
  58. package/src/daemon/lifecycle.ts +358 -221
  59. package/src/daemon/response-tier.ts +2 -0
  60. package/src/daemon/server.ts +453 -193
  61. package/src/daemon/session-agent-loop-handlers.ts +42 -2
  62. package/src/daemon/session-agent-loop.ts +3 -0
  63. package/src/daemon/session-lifecycle.ts +3 -0
  64. package/src/daemon/session-process.ts +1 -0
  65. package/src/daemon/session-surfaces.ts +22 -20
  66. package/src/daemon/session-tool-setup.ts +1 -0
  67. package/src/daemon/session.ts +5 -2
  68. package/src/messaging/outreach-classifier.ts +12 -5
  69. package/src/messaging/provider-types.ts +2 -0
  70. package/src/messaging/providers/gmail/adapter.ts +9 -3
  71. package/src/messaging/providers/gmail/client.ts +2 -0
  72. package/src/runtime/http-errors.ts +33 -20
  73. package/src/runtime/http-server.ts +706 -291
  74. package/src/runtime/http-types.ts +26 -16
  75. package/src/runtime/routes/secret-routes.ts +57 -2
  76. package/src/runtime/routes/surface-action-routes.ts +66 -0
  77. package/src/runtime/routes/trust-rules-routes.ts +140 -0
  78. package/src/security/keychain-to-encrypted-migration.ts +59 -0
  79. package/src/security/secure-keys.ts +17 -0
  80. package/src/skills/frontmatter.ts +9 -7
  81. package/src/tools/apps/executors.ts +2 -1
  82. package/src/tools/tool-manifest.ts +44 -42
  83. package/src/tools/types.ts +9 -0
  84. package/src/__tests__/skill-mirror-parity.test.ts +0 -176
  85. package/src/config/vellum-skills/catalog.json +0 -63
  86. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
  87. package/src/skills/vellum-catalog-remote.ts +0 -166
  88. package/src/tools/skills/vellum-catalog.ts +0 -168
  89. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
  90. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
  91. /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
  92. /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
  93. /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
  94. /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
  95. /package/src/config/{vellum-skills → bundled-skills}/trusted-contacts/SKILL.md +0 -0
@@ -0,0 +1,380 @@
1
+ /**
2
+ * CDP Network recorder for capturing browser network traffic.
3
+ * Inlined from assistant/src/tools/browser/network-recorder.ts
4
+ */
5
+
6
+ import type {
7
+ ExtractedCredential,
8
+ NetworkRecordedEntry,
9
+ NetworkRecordedRequest,
10
+ } from "./recording-types.js";
11
+
12
+ /** Max response body size to capture (64 KB). */
13
+ const MAX_BODY_SIZE = 64 * 1024;
14
+
15
+ /** CDP endpoint to discover targets. */
16
+ const CDP_BASE = "http://localhost:9222";
17
+
18
+ class DirectCDPClient {
19
+ private ws: WebSocket | null = null;
20
+ private nextId = 1;
21
+ private callbacks = new Map<
22
+ number,
23
+ { resolve: (v: unknown) => void; reject: (e: Error) => void }
24
+ >();
25
+ private eventHandlers = new Map<
26
+ string,
27
+ Array<(params: Record<string, unknown>) => void>
28
+ >();
29
+
30
+ async connect(wsUrl: string): Promise<void> {
31
+ return new Promise((resolve, reject) => {
32
+ const ws = new WebSocket(wsUrl);
33
+ ws.onopen = () => {
34
+ this.ws = ws;
35
+ resolve();
36
+ };
37
+ ws.onerror = (e) => reject(new Error(`CDP WebSocket error: ${e}`));
38
+ ws.onclose = () => {
39
+ this.ws = null;
40
+ };
41
+ ws.onmessage = (event) => {
42
+ try {
43
+ const msg = JSON.parse(
44
+ typeof event.data === "string" ? event.data : "",
45
+ );
46
+ if (msg.id != null) {
47
+ const cb = this.callbacks.get(msg.id);
48
+ if (cb) {
49
+ this.callbacks.delete(msg.id);
50
+ if (msg.error) {
51
+ cb.reject(new Error(msg.error.message));
52
+ } else {
53
+ cb.resolve(msg.result);
54
+ }
55
+ }
56
+ } else if (msg.method) {
57
+ const handlers = this.eventHandlers.get(msg.method);
58
+ if (handlers) {
59
+ for (const h of handlers) h(msg.params ?? {});
60
+ }
61
+ }
62
+ } catch {
63
+ /* ignore parse errors */
64
+ }
65
+ };
66
+ });
67
+ }
68
+
69
+ async send(
70
+ method: string,
71
+ params?: Record<string, unknown>,
72
+ ): Promise<unknown> {
73
+ if (!this.ws) throw new Error("Not connected");
74
+ const id = this.nextId++;
75
+ return new Promise((resolve, reject) => {
76
+ this.callbacks.set(id, { resolve, reject });
77
+ this.ws!.send(JSON.stringify({ id, method, params }));
78
+ });
79
+ }
80
+
81
+ on(event: string, handler: (params: Record<string, unknown>) => void): void {
82
+ let handlers = this.eventHandlers.get(event);
83
+ if (!handlers) {
84
+ handlers = [];
85
+ this.eventHandlers.set(event, handlers);
86
+ }
87
+ handlers.push(handler);
88
+ }
89
+
90
+ close(): void {
91
+ if (this.ws) {
92
+ this.ws.close();
93
+ this.ws = null;
94
+ }
95
+ for (const cb of this.callbacks.values()) {
96
+ cb.reject(new Error("CDP client closed"));
97
+ }
98
+ this.callbacks.clear();
99
+ this.eventHandlers.clear();
100
+ }
101
+ }
102
+
103
+ export class NetworkRecorder {
104
+ private cdp: DirectCDPClient | null = null;
105
+ private entries = new Map<string, NetworkRecordedEntry>();
106
+ private targetDomain?: string;
107
+ private running = false;
108
+ private cdpBaseUrl = CDP_BASE;
109
+ private attachedTargetIds = new Set<string>();
110
+ private targetPollTimer?: ReturnType<typeof setInterval>;
111
+
112
+ onLoginDetected?: () => void;
113
+ loginSignals: string[] = [];
114
+
115
+ get entryCount(): number {
116
+ return this.entries.size;
117
+ }
118
+
119
+ constructor(targetDomain?: string) {
120
+ this.targetDomain = targetDomain;
121
+ }
122
+
123
+ async startDirect(cdpBaseUrl: string = CDP_BASE): Promise<void> {
124
+ if (this.running) return;
125
+ this.cdpBaseUrl = cdpBaseUrl;
126
+
127
+ const versionRes = await fetch(`${cdpBaseUrl}/json/version`);
128
+ const version = (await versionRes.json()) as {
129
+ webSocketDebuggerUrl: string;
130
+ };
131
+ const wsUrl = version.webSocketDebuggerUrl;
132
+
133
+ if (!wsUrl) {
134
+ throw new Error("Chrome CDP: no webSocketDebuggerUrl found");
135
+ }
136
+
137
+ this.cdp = new DirectCDPClient();
138
+ await this.cdp.connect(wsUrl);
139
+ this.running = true;
140
+
141
+ await this.discoverAndAttachTargets();
142
+
143
+ this.targetPollTimer = setInterval(() => {
144
+ this.discoverAndAttachTargets().catch(() => {});
145
+ }, 2000);
146
+ }
147
+
148
+ private async discoverAndAttachTargets(): Promise<void> {
149
+ if (!this.running) return;
150
+ try {
151
+ const res = await fetch(`${this.cdpBaseUrl}/json`);
152
+ const pages = (await res.json()) as Array<{
153
+ id: string;
154
+ type: string;
155
+ webSocketDebuggerUrl: string;
156
+ }>;
157
+
158
+ for (const page of pages) {
159
+ if (
160
+ page.type === "page" &&
161
+ page.webSocketDebuggerUrl &&
162
+ !this.attachedTargetIds.has(page.id)
163
+ ) {
164
+ try {
165
+ this.attachedTargetIds.add(page.id);
166
+ await this.attachToTarget(page.webSocketDebuggerUrl);
167
+ } catch {
168
+ this.attachedTargetIds.delete(page.id);
169
+ }
170
+ }
171
+ }
172
+ } catch {
173
+ // CDP endpoint may be temporarily unavailable
174
+ }
175
+ }
176
+
177
+ private pageClients: DirectCDPClient[] = [];
178
+
179
+ private async attachToTarget(wsUrl: string): Promise<void> {
180
+ const client = new DirectCDPClient();
181
+ await client.connect(wsUrl);
182
+
183
+ client.on("Network.requestWillBeSent", (params) =>
184
+ this.handleRequestWillBeSent(params),
185
+ );
186
+ client.on("Network.responseReceived", (params) =>
187
+ this.handleResponseReceived(params),
188
+ );
189
+ client.on("Network.loadingFinished", (params) =>
190
+ this.handleLoadingFinished(params, client),
191
+ );
192
+
193
+ await client.send("Network.enable");
194
+ this.pageClients.push(client);
195
+ }
196
+
197
+ async stop(): Promise<NetworkRecordedEntry[]> {
198
+ if (!this.running) return [];
199
+ this.running = false;
200
+
201
+ if (this.targetPollTimer) {
202
+ clearInterval(this.targetPollTimer);
203
+ this.targetPollTimer = undefined;
204
+ }
205
+
206
+ for (const client of this.pageClients) {
207
+ try {
208
+ await client.send("Network.disable");
209
+ } catch {
210
+ /* ignore */
211
+ }
212
+ client.close();
213
+ }
214
+ this.pageClients = [];
215
+
216
+ if (this.cdp) {
217
+ this.cdp.close();
218
+ this.cdp = null;
219
+ }
220
+
221
+ const result = Array.from(this.entries.values());
222
+ this.entries.clear();
223
+ this.attachedTargetIds.clear();
224
+ this.loginDetectedFired = false;
225
+ return result;
226
+ }
227
+
228
+ async extractCookies(domain?: string): Promise<ExtractedCredential[]> {
229
+ const client = this.pageClients[0];
230
+ if (!client) return [];
231
+ try {
232
+ const result = (await client.send("Network.getAllCookies")) as {
233
+ cookies: Array<{
234
+ name: string;
235
+ value: string;
236
+ domain: string;
237
+ path: string;
238
+ httpOnly: boolean;
239
+ secure: boolean;
240
+ expires: number;
241
+ }>;
242
+ };
243
+ let cookies = result.cookies ?? [];
244
+ if (domain) {
245
+ cookies = cookies.filter(
246
+ (c) =>
247
+ c.domain === domain ||
248
+ c.domain === `.${domain}` ||
249
+ c.domain.endsWith(`.${domain}`),
250
+ );
251
+ }
252
+ return cookies.map((c) => ({
253
+ name: c.name,
254
+ value: c.value,
255
+ domain: c.domain,
256
+ path: c.path,
257
+ httpOnly: c.httpOnly,
258
+ secure: c.secure,
259
+ expires: c.expires > 0 ? c.expires : undefined,
260
+ }));
261
+ } catch {
262
+ return [];
263
+ }
264
+ }
265
+
266
+ getEntries(): NetworkRecordedEntry[] {
267
+ return Array.from(this.entries.values());
268
+ }
269
+
270
+ private matchesDomain(url: string): boolean {
271
+ if (!this.targetDomain) return true;
272
+ try {
273
+ const hostname = new URL(url).hostname;
274
+ return (
275
+ hostname === this.targetDomain ||
276
+ hostname.endsWith(`.${this.targetDomain}`)
277
+ );
278
+ } catch {
279
+ return false;
280
+ }
281
+ }
282
+
283
+ private loginDetectedFired = false;
284
+
285
+ private handleRequestWillBeSent(params: Record<string, unknown>): void {
286
+ const resourceType = params.type as string;
287
+ if (resourceType !== "XHR" && resourceType !== "Fetch") return;
288
+
289
+ const request = params.request as Record<string, unknown>;
290
+ const url = request.url as string;
291
+ if (!this.matchesDomain(url)) return;
292
+
293
+ const requestId = params.requestId as string;
294
+ const headers = (request.headers as Record<string, string>) ?? {};
295
+ const method = (request.method as string) ?? "GET";
296
+ const postData = request.postData as string | undefined;
297
+
298
+ const recordedRequest: NetworkRecordedRequest = {
299
+ method,
300
+ url,
301
+ headers,
302
+ postData,
303
+ };
304
+ const entry: NetworkRecordedEntry = {
305
+ requestId,
306
+ resourceType,
307
+ timestamp: (params.timestamp as number) ?? Date.now() / 1000,
308
+ request: recordedRequest,
309
+ };
310
+ this.entries.set(requestId, entry);
311
+ }
312
+
313
+ private handleResponseReceived(params: Record<string, unknown>): void {
314
+ const requestId = params.requestId as string;
315
+ const entry = this.entries.get(requestId);
316
+ if (!entry) return;
317
+
318
+ const response = params.response as Record<string, unknown>;
319
+ const status = (response.status as number) ?? 0;
320
+ entry.response = {
321
+ status,
322
+ headers: (response.headers as Record<string, string>) ?? {},
323
+ mimeType: (response.mimeType as string) ?? "",
324
+ };
325
+
326
+ if (
327
+ status === 200 &&
328
+ this.onLoginDetected &&
329
+ !this.loginDetectedFired &&
330
+ this.loginSignals.length > 0 &&
331
+ this.loginSignals.some((sig) => entry.request.url.includes(sig))
332
+ ) {
333
+ this.loginDetectedFired = true;
334
+ setTimeout(() => this.onLoginDetected?.(), 5000);
335
+ }
336
+ }
337
+
338
+ private handleLoadingFinished(
339
+ params: Record<string, unknown>,
340
+ client: DirectCDPClient,
341
+ ): void {
342
+ const requestId = params.requestId as string;
343
+ const entry = this.entries.get(requestId);
344
+ if (!entry || !entry.response) return;
345
+
346
+ const mimeType = entry.response.mimeType;
347
+ if (!mimeType.includes("json") && !mimeType.includes("text")) return;
348
+
349
+ this.fetchResponseBody(requestId, entry, client);
350
+ }
351
+
352
+ private async fetchResponseBody(
353
+ requestId: string,
354
+ entry: NetworkRecordedEntry,
355
+ client: DirectCDPClient,
356
+ ): Promise<void> {
357
+ if (!this.running) return;
358
+ try {
359
+ const result = (await client.send("Network.getResponseBody", {
360
+ requestId,
361
+ })) as {
362
+ body: string;
363
+ base64Encoded: boolean;
364
+ };
365
+
366
+ if (result.body && entry.response) {
367
+ const body = result.base64Encoded
368
+ ? Buffer.from(result.body, "base64").toString("utf-8")
369
+ : result.body;
370
+
371
+ entry.response.body =
372
+ body.length > MAX_BODY_SIZE
373
+ ? body.slice(0, MAX_BODY_SIZE) + "...[truncated]"
374
+ : body;
375
+ }
376
+ } catch {
377
+ // Response body may not be available
378
+ }
379
+ }
380
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Inlined platform utilities used by the DoorDash skill.
3
+ * Subset of assistant/src/util/platform.ts — kept minimal.
4
+ */
5
+
6
+ import { readFileSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+
10
+ function getRootDir(): string {
11
+ const base = process.env.BASE_DATA_DIR?.trim();
12
+ return join(base || homedir(), ".vellum");
13
+ }
14
+
15
+ export function getDataDir(): string {
16
+ return join(getRootDir(), "workspace", "data");
17
+ }
18
+
19
+ export function getSocketPath(): string {
20
+ const override = process.env.VELLUM_DAEMON_SOCKET?.trim();
21
+ if (override) {
22
+ if (override === "~") return homedir();
23
+ if (override.startsWith("~/")) return join(homedir(), override.slice(2));
24
+ return override;
25
+ }
26
+ return join(getRootDir(), "vellum.sock");
27
+ }
28
+
29
+ export function readSessionToken(): string | null {
30
+ try {
31
+ return readFileSync(join(getRootDir(), "session-token"), "utf-8").trim();
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Simple read/write for session recording JSON files.
3
+ * Inlined from assistant/src/tools/browser/recording-store.ts
4
+ */
5
+
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { join, resolve } from "node:path";
8
+
9
+ import { getDataDir } from "./platform.js";
10
+ import type { SessionRecording } from "./recording-types.js";
11
+
12
+ function getRecordingsDir(): string {
13
+ return join(getDataDir(), "recordings");
14
+ }
15
+
16
+ export function saveRecording(recording: SessionRecording): string {
17
+ const dir = getRecordingsDir();
18
+ mkdirSync(dir, { recursive: true });
19
+
20
+ const filePath = resolve(dir, `${recording.id}.json`);
21
+ if (!filePath.startsWith(resolve(dir) + "/")) {
22
+ throw new Error(`Invalid recording ID: ${recording.id}`);
23
+ }
24
+ writeFileSync(filePath, JSON.stringify(recording, null, 2), "utf-8");
25
+ return filePath;
26
+ }
27
+
28
+ export function loadRecording(recordingId: string): SessionRecording | null {
29
+ const dir = getRecordingsDir();
30
+ const filePath = resolve(dir, `${recordingId}.json`);
31
+ if (!filePath.startsWith(resolve(dir) + "/")) {
32
+ return null;
33
+ }
34
+ if (!existsSync(filePath)) {
35
+ return null;
36
+ }
37
+ try {
38
+ const data = readFileSync(filePath, "utf-8");
39
+ return JSON.parse(data) as SessionRecording;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
@@ -0,0 +1,49 @@
1
+ /** Types for CDP network recording. Inlined from assistant/src/tools/browser/network-recording-types.ts */
2
+
3
+ export interface NetworkRecordedRequest {
4
+ method: string;
5
+ url: string;
6
+ headers: Record<string, string>;
7
+ postData?: string;
8
+ }
9
+
10
+ export interface NetworkRecordedResponse {
11
+ status: number;
12
+ headers: Record<string, string>;
13
+ mimeType: string;
14
+ body?: string;
15
+ }
16
+
17
+ export interface NetworkRecordedEntry {
18
+ requestId: string;
19
+ resourceType: string;
20
+ timestamp: number;
21
+ request: NetworkRecordedRequest;
22
+ response?: NetworkRecordedResponse;
23
+ }
24
+
25
+ export interface ExtractedCredential {
26
+ name: string;
27
+ value: string;
28
+ domain: string;
29
+ path: string;
30
+ httpOnly: boolean;
31
+ secure: boolean;
32
+ expires?: number;
33
+ }
34
+
35
+ export interface SessionRecording {
36
+ id: string;
37
+ startedAt: number;
38
+ endedAt: number;
39
+ targetDomain?: string;
40
+ networkEntries: NetworkRecordedEntry[];
41
+ cookies: ExtractedCredential[];
42
+ observations: Array<{
43
+ ocrText: string;
44
+ appName?: string;
45
+ windowTitle?: string;
46
+ timestamp: number;
47
+ captureIndex: number;
48
+ }>;
49
+ }
@@ -0,0 +1,6 @@
1
+ /** Truncate a string to `maxLen` characters, appending `suffix` if truncated. */
2
+ export function truncate(str: string, maxLen: number, suffix = "..."): string {
3
+ if (str.length <= maxLen) return str;
4
+ if (maxLen < suffix.length) return str.slice(0, maxLen);
5
+ return str.slice(0, maxLen - suffix.length) + suffix;
6
+ }