@crouton-kit/capture 1.3.4 → 1.3.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouton-kit/capture",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
4
4
  "description": "Browser automation and UI validation via CDP — session-based screenshots, HAR, a11y, click/type, and JS exec",
5
5
  "bin": {
6
6
  "capture": "./bin/capture"
@@ -10,7 +10,9 @@
10
10
  "src"
11
11
  ],
12
12
  "scripts": {
13
- "build": "esbuild src/capture.ts --bundle --platform=node --format=cjs --outfile=bin/capture '--banner:js=#!/usr/bin/env node' \"--define:globalThis.__CAPTURE_VERSION__=\\\"$npm_package_version\\\"\" && chmod +x bin/capture"
13
+ "build": "esbuild src/capture.ts --bundle --platform=node --format=cjs --outfile=bin/capture --external:esbuild '--banner:js=#!/usr/bin/env node' \"--define:globalThis.__CAPTURE_VERSION__=\\\"$npm_package_version\\\"\" && chmod +x bin/capture",
14
+ "dev": "tsx src/capture.ts",
15
+ "test": "node --import tsx --test test/*.test.ts"
14
16
  },
15
17
  "repository": {
16
18
  "type": "git",
@@ -33,7 +35,9 @@
33
35
  "devDependencies": {
34
36
  "@types/ws": "^8.18.1",
35
37
  "esbuild": "^0.24.0",
36
- "typescript": "^5.7.0"
38
+ "tsx": "^4",
39
+ "typescript": "^5.7.0",
40
+ "zod": "^4.3.5"
37
41
  },
38
42
  "dependencies": {
39
43
  "ws": "^8.19.0"
package/src/capture.ts CHANGED
@@ -69,6 +69,7 @@ async function main(): Promise<void> {
69
69
  case "record":
70
70
  case "navigate":
71
71
  case "har":
72
+ case "lib":
72
73
  case "network":
73
74
  case "list":
74
75
  return cdpMain();
@@ -125,6 +126,14 @@ DIAGNOSTICS & ONE-OFFS (no session needed)
125
126
  network <offline|online> Toggle connectivity for a tab
126
127
  har create | read [id] | delete <id> Manage standalone HAR recordings
127
128
 
129
+ LIBRARY (vault libs — dev checkout only)
130
+
131
+ lib list List available libs
132
+ lib search "<query>" [--limit N] Fuzzy-search functions across libs
133
+ lib show <name> Lib + function summaries
134
+ lib read <name> [fn…] Full input/output schemas (+ .ts source path)
135
+ exec "import {fn} from 'libs/<name>'; return await fn({…})" Run a lib in the active tab
136
+
128
137
  TARGETING (only when NOT in a session, or picking a parallel tab)
129
138
 
130
139
  --target <tabId> Exact id; a prefix of 8 chars is enough (preferred, parallel-safe)
@@ -3,6 +3,7 @@ import { withConnection } from '../connection.js';
3
3
  import { HARRecorder } from '../har-recorder.js';
4
4
  import { writeHarAndPrintSummary } from '../har-output.js';
5
5
  import { type ParsedArgs } from '../types.js';
6
+ import { hasImports, bundleExec } from '../../vault/bundle.js';
6
7
 
7
8
  export async function cmdExec(parsed: ParsedArgs, _args: string[]): Promise<void> {
8
9
  if (parsed.help) {
@@ -13,7 +14,9 @@ export async function cmdExec(parsed: ParsedArgs, _args: string[]): Promise<void
13
14
  ' capture exec "document.title" --target <id>\n' +
14
15
  ' capture exec "await fetch(\'/api/data\').then(r=>r.json())" --target <id>\n' +
15
16
  ' capture exec "document.querySelector(\'.btn\').click()" --target <id>\n' +
16
- ' capture exec --file /tmp/scrape.js --target <id> --record',
17
+ ' capture exec --file /tmp/scrape.js --target <id> --record\n\n' +
18
+ 'Import vault libs (dev checkout only): static imports first, then your code.\n' +
19
+ ' capture exec "import {searchEmails} from \'libs/gmail\'; const ctx = await getContext(); return await searchEmails({ ...ctx, query: \'invoice\' })"',
17
20
  );
18
21
  process.exit(0);
19
22
  }
@@ -35,6 +38,19 @@ export async function cmdExec(parsed: ParsedArgs, _args: string[]): Promise<void
35
38
  }
36
39
  }
37
40
 
41
+ // Import-driven exec: if the code has leading static imports, bundle the
42
+ // forked vault libs on the fly (esbuild) BEFORE opening a tab — fail fast,
43
+ // no wasted CDP connection. Plain exec (no imports) skips this entirely.
44
+ let prebuilt: string | undefined;
45
+ if (hasImports(code)) {
46
+ try {
47
+ prebuilt = await bundleExec(code);
48
+ } catch (e) {
49
+ console.error(`ERROR: ${(e as Error).message}`);
50
+ process.exit(1);
51
+ }
52
+ }
53
+
38
54
  await withConnection(
39
55
  parsed,
40
56
  async (client) => {
@@ -47,13 +63,15 @@ export async function cmdExec(parsed: ParsedArgs, _args: string[]): Promise<void
47
63
  await standaloneRecorder.start();
48
64
  }
49
65
 
50
- // Smart wrapping: only use async IIFE when code contains `await`.
51
- // Without wrapping, Runtime.evaluate returns the completion value
52
- // of the last expression, handling multi-statement code naturally.
53
- const needsAsyncWrap = /\bawait\b/.test(code);
54
- const expression = needsAsyncWrap
55
- ? `(async () => { ${code} })()`
56
- : code;
66
+ // A prebuilt bundle is already a complete IIFE returning the user's
67
+ // promise. Otherwise fall back to the existing smart wrap: async IIFE
68
+ // only when code contains `await`; bare code lets Runtime.evaluate return
69
+ // the last expression's completion value (multi-statement code works).
70
+ const expression = prebuilt
71
+ ? prebuilt
72
+ : /\bawait\b/.test(code)
73
+ ? `(async () => { ${code} })()`
74
+ : code;
57
75
 
58
76
  const evalResult = (await client.send('Runtime.evaluate', {
59
77
  expression,
@@ -0,0 +1,356 @@
1
+ /**
2
+ * `capture lib <list|search|show|read>` — search the forked vault libs.
3
+ *
4
+ * Pure local reads, NO CDP connection. Mirrors `nl lib`
5
+ * (northlight apps/core/src/cli/families/lib/lib-cli.provider.ts): same search
6
+ * scoring and the discover→schema→run loop, sourced from on-demand doc-gen
7
+ * (`../../vault/docs.ts`) + sha cache. Output contract (capture convention):
8
+ * the machine result → stdout `JSON.stringify(x, null, 2)`; next-step hints →
9
+ * stderr. Every object naming a lib/function carries `srcPath` = the absolute
10
+ * path to that lib's `.ts` source — the agent's pointer to rebuild on the fly.
11
+ */
12
+
13
+ import * as path from "path";
14
+ import { type ParsedArgs } from "../types.js";
15
+ import { vaultLibsDir, listLibs } from "../../vault/paths.js";
16
+ import {
17
+ getDoc,
18
+ listDocs,
19
+ isAgentVisible,
20
+ type LibraryDoc,
21
+ } from "../../vault/docs.js";
22
+
23
+ const USAGE =
24
+ "Usage: capture lib <list|search|show|read> [options]\n\n" +
25
+ " list List available libs (name, fn count, description, srcPath)\n" +
26
+ ' search "<query>" [--limit N] Fuzzy-search functions across all libs (ranked)\n' +
27
+ " show <name> Lib + function summaries (no schemas)\n" +
28
+ " read <name> [fn…] Full input/output JSON schemas (+ .ts source path)\n\n" +
29
+ "Then run a lib in the active authenticated tab:\n" +
30
+ " capture exec \"import {<fn>} from 'libs/<name>'; return await <fn>({…})\"\n\n" +
31
+ "Dev-checkout-only feature (requires vault/ source + esbuild).";
32
+
33
+ // ─── scoring (ported verbatim from lib-cli.provider.ts:71-152) ───────────────
34
+
35
+ const FUNCTION_NAME_EXACT = 100;
36
+ const FUNCTION_NAME_PREFIX = 60;
37
+ const FUNCTION_NAME_SUBSTRING = 40;
38
+ const SERVICE_NAME_MATCH = 50;
39
+ const FUNCTION_DESC_SUBSTRING = 20;
40
+ const SERVICE_DESC_SUBSTRING = 10;
41
+
42
+ function scoreToken(
43
+ token: string,
44
+ service: string,
45
+ fnName: string,
46
+ fnDesc: string,
47
+ serviceDesc: string,
48
+ ): { score: number; match: string } {
49
+ const q = token.toLowerCase();
50
+ const fn = fnName.toLowerCase();
51
+ const svc = service.toLowerCase();
52
+ const fnD = fnDesc.toLowerCase();
53
+ const svcD = serviceDesc.toLowerCase();
54
+
55
+ let score = 0;
56
+ let match = "";
57
+
58
+ if (fn === q) {
59
+ score += FUNCTION_NAME_EXACT;
60
+ match = "function-name (exact)";
61
+ } else if (fn.startsWith(q)) {
62
+ score += FUNCTION_NAME_PREFIX;
63
+ match = "function-name (prefix)";
64
+ } else if (fn.includes(q)) {
65
+ score += FUNCTION_NAME_SUBSTRING;
66
+ match = "function-name";
67
+ }
68
+
69
+ if (svc.includes(q)) {
70
+ score += SERVICE_NAME_MATCH;
71
+ if (!match) match = "service-name";
72
+ }
73
+
74
+ if (fnD.includes(q)) {
75
+ score += FUNCTION_DESC_SUBSTRING;
76
+ if (!match) match = "function-desc";
77
+ }
78
+
79
+ if (svcD.includes(q)) {
80
+ score += SERVICE_DESC_SUBSTRING;
81
+ if (!match) match = "service-desc";
82
+ }
83
+
84
+ return { score, match };
85
+ }
86
+
87
+ /**
88
+ * Every whitespace-separated token must contribute > 0 — without this,
89
+ * `lib search "send email"` would return everything matching "send" alone.
90
+ */
91
+ function scoreHit(
92
+ query: string,
93
+ service: string,
94
+ fnName: string,
95
+ fnDesc: string,
96
+ serviceDesc: string,
97
+ ): { score: number; match: string } {
98
+ const tokens = query.split(/\s+/).filter(Boolean);
99
+ if (tokens.length === 0) return { score: 0, match: "" };
100
+ if (tokens.length === 1) {
101
+ return scoreToken(tokens[0], service, fnName, fnDesc, serviceDesc);
102
+ }
103
+ let total = 0;
104
+ let bestScore = -1;
105
+ let bestMatch = "";
106
+ for (const token of tokens) {
107
+ const r = scoreToken(token, service, fnName, fnDesc, serviceDesc);
108
+ if (r.score === 0) return { score: 0, match: "" };
109
+ total += r.score;
110
+ if (r.score > bestScore) {
111
+ bestScore = r.score;
112
+ bestMatch = r.match;
113
+ }
114
+ }
115
+ return { score: total, match: bestMatch };
116
+ }
117
+
118
+ // ─── helpers ─────────────────────────────────────────────────────────────────
119
+
120
+ const svcName = (doc: LibraryDoc): string =>
121
+ doc.library.replace(/^@vallum\//, "");
122
+
123
+ const srcPathFor = (name: string): string =>
124
+ path.join(vaultLibsDir(), name, "index.ts");
125
+
126
+ /** Resolve a user-supplied name against a candidate set: exact, then ci-prefix. */
127
+ function resolveName(
128
+ query: string,
129
+ names: string[],
130
+ ): { name: string } | { error: string } {
131
+ if (names.includes(query)) return { name: query };
132
+ const lower = query.toLowerCase();
133
+ const prefix = names.filter((n) => n.toLowerCase().startsWith(lower));
134
+ if (prefix.length === 1) return { name: prefix[0] };
135
+ if (prefix.length > 1) {
136
+ return { error: `Ambiguous lib "${query}" — matches: ${prefix.join(", ")}` };
137
+ }
138
+ return { error: `Unknown lib "${query}". Available: ${names.join(", ")}` };
139
+ }
140
+
141
+ // ─── command ─────────────────────────────────────────────────────────────────
142
+
143
+ export async function cmdLib(
144
+ parsed: ParsedArgs,
145
+ _args: string[],
146
+ ): Promise<void> {
147
+ if (parsed.help) {
148
+ console.log(USAGE);
149
+ process.exit(0);
150
+ }
151
+
152
+ const sub = parsed.positional[0];
153
+
154
+ // ── lib list ──────────────────────────────────────────────────────────────
155
+ if (sub === "list") {
156
+ const docs = await listDocs();
157
+ const rows = docs.map((d) => {
158
+ const name = svcName(d);
159
+ return {
160
+ name,
161
+ functionCount: d.functions.length,
162
+ description: d.description,
163
+ srcPath: srcPathFor(name),
164
+ };
165
+ });
166
+ console.log(JSON.stringify(rows, null, 2));
167
+ console.error(
168
+ `\n${rows.length} libs. Next: capture lib show <name> · capture lib read <name> <fn>`,
169
+ );
170
+ return;
171
+ }
172
+
173
+ // ── lib search ──────────────────────────────────────────────────────────────
174
+ if (sub === "search") {
175
+ const query = parsed.positional[1];
176
+ if (!query) {
177
+ console.error('Usage: capture lib search "<query>" [--limit N]');
178
+ process.exit(1);
179
+ }
180
+ const limit = Math.min(
181
+ typeof parsed.limit === "number" && parsed.limit > 0 ? parsed.limit : 20,
182
+ 500,
183
+ );
184
+ const docs = await listDocs();
185
+ const hits: Array<{
186
+ service: string;
187
+ function: string;
188
+ match: string;
189
+ score: number;
190
+ description: string;
191
+ srcPath: string;
192
+ }> = [];
193
+ for (const d of docs) {
194
+ const service = svcName(d);
195
+ for (const fn of d.functions) {
196
+ const { score, match } = scoreHit(
197
+ query,
198
+ service,
199
+ fn.name,
200
+ fn.description ?? "",
201
+ d.description ?? "",
202
+ );
203
+ if (score > 0) {
204
+ hits.push({
205
+ service,
206
+ function: fn.name,
207
+ match,
208
+ score,
209
+ description: fn.description,
210
+ srcPath: srcPathFor(service),
211
+ });
212
+ }
213
+ }
214
+ }
215
+ hits.sort((a, b) => b.score - a.score);
216
+ const top = hits.slice(0, limit);
217
+ console.log(JSON.stringify(top, null, 2));
218
+ console.error(
219
+ `\n${hits.length} hit(s)${hits.length > limit ? ` (showing ${limit})` : ""}.` +
220
+ ` Next: capture lib read <service> <function>`,
221
+ );
222
+ return;
223
+ }
224
+
225
+ // ── lib show ──────────────────────────────────────────────────────────────
226
+ if (sub === "show") {
227
+ const name = parsed.positional[1];
228
+ if (!name) {
229
+ console.error("Usage: capture lib show <name>");
230
+ process.exit(1);
231
+ }
232
+ // Resolve + build ONLY the named lib (design §3.1: show is single-lib, not
233
+ // an all-lib bundle). Mirrors the read path; adds show's visibility filter
234
+ // (a present-but-non-visible lib is treated as Unknown).
235
+ const r = resolveName(name, listLibs());
236
+ if ("error" in r) {
237
+ console.error(r.error);
238
+ process.exit(1);
239
+ }
240
+ let doc: LibraryDoc;
241
+ try {
242
+ doc = await getDoc(r.name);
243
+ } catch (e) {
244
+ console.error((e as Error).message);
245
+ process.exit(1);
246
+ }
247
+ if (!isAgentVisible(doc)) {
248
+ console.error(
249
+ `Unknown lib "${name}". Available: ${listLibs().join(", ")}`,
250
+ );
251
+ process.exit(1);
252
+ }
253
+ const service = r.name;
254
+ const out = {
255
+ service,
256
+ description: doc.description,
257
+ ...(doc.loginUrl ? { loginUrl: doc.loginUrl } : {}),
258
+ ...(doc.notes ? { notes: doc.notes } : {}),
259
+ srcPath: srcPathFor(service),
260
+ functions: doc.functions.map((f) => ({
261
+ name: f.name,
262
+ description: f.description,
263
+ })),
264
+ };
265
+ console.log(JSON.stringify(out, null, 2));
266
+ console.error(
267
+ `\nSchemas required before exec — capture lib read ${service} <fn>`,
268
+ );
269
+ return;
270
+ }
271
+
272
+ // ── lib read ──────────────────────────────────────────────────────────────
273
+ if (sub === "read") {
274
+ const name = parsed.positional[1];
275
+ if (!name) {
276
+ console.error("Usage: capture lib read <name> [fn…]");
277
+ process.exit(1);
278
+ }
279
+ // `read` may read ANY present lib (no visibility filter).
280
+ const r = resolveName(name, listLibs());
281
+ if ("error" in r) {
282
+ console.error(r.error);
283
+ process.exit(1);
284
+ }
285
+ let doc: LibraryDoc;
286
+ try {
287
+ doc = await getDoc(r.name);
288
+ } catch (e) {
289
+ console.error((e as Error).message);
290
+ process.exit(1);
291
+ }
292
+ const service = svcName(doc);
293
+ const fns = parsed.positional.slice(2);
294
+
295
+ // No function names → summary fallback (show-style) + hint to name fns.
296
+ if (fns.length === 0) {
297
+ const out = {
298
+ service,
299
+ description: doc.description,
300
+ ...(doc.notes ? { notes: doc.notes } : {}),
301
+ srcPath: srcPathFor(service),
302
+ functions: doc.functions.map((f) => ({
303
+ name: f.name,
304
+ description: f.description,
305
+ })),
306
+ };
307
+ console.log(JSON.stringify(out, null, 2));
308
+ console.error(
309
+ `\nName functions to load schemas: capture lib read ${service} <fn> [fn…]`,
310
+ );
311
+ return;
312
+ }
313
+
314
+ const byName = new Map(doc.functions.map((f) => [f.name, f]));
315
+ const found: typeof doc.functions = [];
316
+ const missing: string[] = [];
317
+ for (const op of fns) {
318
+ const fn = byName.get(op);
319
+ if (fn) found.push(fn);
320
+ else missing.push(op);
321
+ }
322
+
323
+ if (found.length === 0) {
324
+ const available = doc.functions.map((f) => f.name).join(", ");
325
+ const label =
326
+ missing.length > 1
327
+ ? `Functions "${missing.join('", "')}"`
328
+ : `Function "${missing[0]}"`;
329
+ console.error(`${label} not in ${service}. Available: ${available}`);
330
+ process.exit(1);
331
+ }
332
+
333
+ const out = {
334
+ service,
335
+ srcPath: srcPathFor(service),
336
+ functions: found.map((f) => ({
337
+ name: f.name,
338
+ description: f.description,
339
+ ...(f.notes ? { notes: f.notes } : {}),
340
+ input: f.input,
341
+ output: f.output,
342
+ })),
343
+ ...(missing.length > 0 ? { missing } : {}),
344
+ };
345
+ console.log(JSON.stringify(out, null, 2));
346
+ const firstFn = found[0].name;
347
+ console.error(
348
+ `\nRun it: capture exec "import {${firstFn}} from 'libs/${service}'; return await ${firstFn}({…})"`,
349
+ );
350
+ return;
351
+ }
352
+
353
+ // ── lib (no/unknown subcommand) ─────────────────────────────────────────────
354
+ console.error(USAGE);
355
+ process.exit(1);
356
+ }
@@ -5,6 +5,7 @@ import { cmdExec } from './commands/exec.js';
5
5
  import { cmdScreenshot, cmdClick, cmdType, cmdA11y } from './commands/ui.js';
6
6
  import { cmdRecord, cmdNavigate, cmdNetwork } from './commands/traffic.js';
7
7
  import { cmdHar } from './commands/har.js';
8
+ import { cmdLib } from './commands/lib.js';
8
9
 
9
10
  export async function cdpMain(): Promise<void> {
10
11
  const args = process.argv.slice(2);
@@ -24,6 +25,7 @@ export async function cdpMain(): Promise<void> {
24
25
  case 'navigate': return cmdNavigate(parsed, args);
25
26
  case 'network': return cmdNetwork(parsed, args);
26
27
  case 'har': return cmdHar(parsed, args);
28
+ case 'lib': return cmdLib(parsed, args);
27
29
  default:
28
30
  console.log(CDP_USAGE);
29
31
  }
package/src/cdp/usage.ts CHANGED
@@ -17,6 +17,10 @@ Commands:
17
17
  har create Create a HAR recording (returns id)
18
18
  har read [id] Read accumulated HAR entries (id optional in active session)
19
19
  har delete <id> Delete a HAR recording
20
+ lib list List available vault libs (dev checkout only)
21
+ lib search "<q>" Fuzzy-search lib functions [--limit N]
22
+ lib show <name> Lib + function summaries
23
+ lib read <name> [fn…] Full input/output schemas (+ .ts source path)
20
24
 
21
25
  Options:
22
26
  --port <port> Override CDP port
@@ -59,4 +63,7 @@ Examples:
59
63
  capture har create
60
64
  capture exec "document.querySelector('form').submit()" --target <id> --har <id>
61
65
  capture har read <id>
66
+ capture lib search "search emails"
67
+ capture lib read gmail searchEmails
68
+ capture exec "import {searchEmails} from 'libs/gmail'; const ctx = await getContext(); return await searchEmails({ ...ctx, query: 'invoice' })"
62
69
  `;