@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/bin/capture +662 -70
- package/package.json +7 -3
- package/src/capture.ts +9 -0
- package/src/cdp/commands/exec.ts +26 -8
- package/src/cdp/commands/lib.ts +356 -0
- package/src/cdp/dispatch.ts +2 -0
- package/src/cdp/usage.ts +7 -0
- package/src/vault/bundle.ts +205 -0
- package/src/vault/docs.ts +224 -0
- package/src/vault/paths.ts +87 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crouton-kit/capture",
|
|
3
|
-
"version": "1.3.
|
|
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
|
-
"
|
|
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)
|
package/src/cdp/commands/exec.ts
CHANGED
|
@@ -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
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
const expression =
|
|
55
|
-
?
|
|
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
|
+
}
|
package/src/cdp/dispatch.ts
CHANGED
|
@@ -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
|
`;
|