@crouton-kit/capture 1.3.4 → 1.3.6
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 +211996 -376
- package/package.json +7 -3
- package/src/capture.ts +15 -5
- package/src/cdp/args.ts +37 -5
- package/src/cdp/commands/exec.ts +31 -10
- package/src/cdp/commands/lib.ts +356 -0
- package/src/cdp/commands/tabs.ts +2 -8
- package/src/cdp/commands/ui.ts +1 -1
- package/src/cdp/connection.ts +23 -21
- package/src/cdp/detect.ts +62 -13
- package/src/cdp/dispatch.ts +2 -0
- package/src/cdp/exec-expression.ts +49 -0
- package/src/cdp/execute.ts +5 -9
- package/src/cdp/record.ts +63 -20
- package/src/cdp/targets.ts +120 -4
- package/src/cdp/types.ts +1 -0
- package/src/cdp/usage.ts +11 -3
- package/src/session/commands.ts +107 -32
- 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.6",
|
|
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();
|
|
@@ -91,7 +92,7 @@ TYPICAL WORKFLOW
|
|
|
91
92
|
capture type "hi@me.com" --into "Email"
|
|
92
93
|
capture screenshot Save current state
|
|
93
94
|
capture navigate https://... Navigate within the session
|
|
94
|
-
capture exec "document.title" Run JS (
|
|
95
|
+
capture exec "document.title" Run JS (expressions, return, await)
|
|
95
96
|
capture har read --filter-url /api Inspect recorded traffic
|
|
96
97
|
|
|
97
98
|
3. Bundle and inspect:
|
|
@@ -112,7 +113,7 @@ INTERACTION COMMANDS (work inside or outside a session)
|
|
|
112
113
|
click "name" [--role <role>] Click by accessible name
|
|
113
114
|
type "text" [--into "Field"] Type into focused element or named field
|
|
114
115
|
screenshot [--out <path>] [--full-page] Screenshot (viewport: desktop|desktop-wide|tablet|mobile)
|
|
115
|
-
exec <code> | exec --file <path> Evaluate JS; await
|
|
116
|
+
exec <code> | exec --file <path> Evaluate JS; expressions, return, and await are supported
|
|
116
117
|
navigate <url> [--settle <ms>] Navigate the current tab + record HAR
|
|
117
118
|
|
|
118
119
|
DIAGNOSTICS & ONE-OFFS (no session needed)
|
|
@@ -125,11 +126,20 @@ 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
|
-
|
|
131
|
-
--
|
|
132
|
-
|
|
139
|
+
CDP_PORT / CDP_TARGET Orchestrators can pin the active browser + tab
|
|
140
|
+
--target <tabId> Exact id; a prefix of 8 chars is enough (preferred, parallel-safe)
|
|
141
|
+
--url <pattern> Fuzzy URL match against open tabs
|
|
142
|
+
NOTE: on \`session start\`, --url is a URL to OPEN, not a pattern.
|
|
133
143
|
|
|
134
144
|
HELP
|
|
135
145
|
|
package/src/cdp/args.ts
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
import { type ParsedArgs } from './types.js';
|
|
2
2
|
import { getActiveSession } from '../session-context.js';
|
|
3
3
|
|
|
4
|
+
// Every flag below is matched as `--flag` followed by a separate `next`
|
|
5
|
+
// token (space-separated). To accept the equally-common `--flag=value` form
|
|
6
|
+
// without duplicating every branch, split any `--flag=value` token into two
|
|
7
|
+
// tokens up front so the rest of the parser only ever has to handle one
|
|
8
|
+
// convention.
|
|
9
|
+
export function expandEqualsFlags(argv: string[]): string[] {
|
|
10
|
+
const out: string[] = [];
|
|
11
|
+
for (const arg of argv) {
|
|
12
|
+
if (arg.startsWith('--') && arg.includes('=')) {
|
|
13
|
+
const eq = arg.indexOf('=');
|
|
14
|
+
out.push(arg.slice(0, eq), arg.slice(eq + 1));
|
|
15
|
+
} else {
|
|
16
|
+
out.push(arg);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
4
22
|
export function parseCliArgs(argv: string[]): ParsedArgs {
|
|
5
23
|
const command = argv[0] ?? '';
|
|
24
|
+
const rest = expandEqualsFlags(argv.slice(1));
|
|
6
25
|
const positional: string[] = [];
|
|
7
26
|
const parsed: Partial<ParsedArgs> = { command, positional };
|
|
8
27
|
|
|
9
|
-
for (let i =
|
|
10
|
-
const arg =
|
|
11
|
-
const next =
|
|
28
|
+
for (let i = 0; i < rest.length; i++) {
|
|
29
|
+
const arg = rest[i];
|
|
30
|
+
const next = rest[i + 1];
|
|
12
31
|
|
|
13
32
|
if (arg === '--port' && next) {
|
|
14
33
|
parsed.port = parseInt(next, 10);
|
|
@@ -44,6 +63,9 @@ export function parseCliArgs(argv: string[]): ParsedArgs {
|
|
|
44
63
|
} else if (arg === '--target' && next) {
|
|
45
64
|
parsed.target = next;
|
|
46
65
|
i++;
|
|
66
|
+
} else if (arg === '--url' && next) {
|
|
67
|
+
parsed.url = next;
|
|
68
|
+
i++;
|
|
47
69
|
} else if (arg === '--role' && next) {
|
|
48
70
|
parsed.role = next;
|
|
49
71
|
i++;
|
|
@@ -83,7 +105,15 @@ export function parseCliArgs(argv: string[]): ParsedArgs {
|
|
|
83
105
|
}
|
|
84
106
|
|
|
85
107
|
// Fill gaps from environment variables (set by pipeline orchestrators)
|
|
86
|
-
if (!parsed.
|
|
108
|
+
if (!parsed.port && process.env.CDP_PORT) {
|
|
109
|
+
const envPort = Number.parseInt(process.env.CDP_PORT, 10);
|
|
110
|
+
if (Number.isNaN(envPort)) {
|
|
111
|
+
console.error(`Invalid CDP_PORT: ${process.env.CDP_PORT}`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
parsed.port = envPort;
|
|
115
|
+
}
|
|
116
|
+
if (!parsed.target && !parsed.url && process.env.CDP_TARGET) {
|
|
87
117
|
parsed.target = process.env.CDP_TARGET;
|
|
88
118
|
}
|
|
89
119
|
if (!parsed.har && process.env.CDP_HAR_ID) {
|
|
@@ -94,7 +124,9 @@ export function parseCliArgs(argv: string[]): ParsedArgs {
|
|
|
94
124
|
const session = getActiveSession();
|
|
95
125
|
if (session) {
|
|
96
126
|
if (!parsed.har && session.harId) parsed.har = session.harId;
|
|
97
|
-
|
|
127
|
+
// An explicit --url picks a different (parallel) tab than the session's
|
|
128
|
+
// own — don't let the session's targetId clobber that choice.
|
|
129
|
+
if (!parsed.target && !parsed.url && session.targetId) parsed.target = session.targetId;
|
|
98
130
|
}
|
|
99
131
|
|
|
100
132
|
return parsed as ParsedArgs;
|
package/src/cdp/commands/exec.ts
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import { withConnection } from '../connection.js';
|
|
3
|
+
import { buildExecExpression } from '../exec-expression.js';
|
|
3
4
|
import { HARRecorder } from '../har-recorder.js';
|
|
4
5
|
import { writeHarAndPrintSummary } from '../har-output.js';
|
|
5
6
|
import { type ParsedArgs } from '../types.js';
|
|
7
|
+
import { hasImports, bundleExec } from '../../vault/bundle.js';
|
|
6
8
|
|
|
7
9
|
export async function cmdExec(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
8
10
|
if (parsed.help) {
|
|
9
11
|
console.log(
|
|
10
12
|
'Usage: capture exec <code> [--target <id>] [--record] [--file <path>] [--har <id>]\n\n' +
|
|
11
|
-
'Execute JavaScript in a browser tab.
|
|
13
|
+
'Execute JavaScript in a browser tab. Plain expressions run directly; statement bodies can use top-level return/await and are wrapped in an async IIFE.\n\n' +
|
|
12
14
|
'Examples:\n' +
|
|
13
15
|
' capture exec "document.title" --target <id>\n' +
|
|
14
|
-
' capture exec "
|
|
16
|
+
' capture exec "return document.title" --target <id>\n' +
|
|
17
|
+
' capture exec "return await fetch(\'/api/data\').then(r=>r.json())" --target <id>\n' +
|
|
15
18
|
' capture exec "document.querySelector(\'.btn\').click()" --target <id>\n' +
|
|
16
|
-
' capture exec --file /tmp/scrape.js --target <id> --record'
|
|
19
|
+
' capture exec --file /tmp/scrape.js --target <id> --record\n\n' +
|
|
20
|
+
'Import vault libs (dev checkout only): static imports first, then your code.\n' +
|
|
21
|
+
' capture exec "import {searchEmails} from \'libs/gmail\'; const ctx = await getContext(); return await searchEmails({ ...ctx, query: \'invoice\' })"',
|
|
17
22
|
);
|
|
18
23
|
process.exit(0);
|
|
19
24
|
}
|
|
@@ -26,8 +31,13 @@ export async function cmdExec(parsed: ParsedArgs, _args: string[]): Promise<void
|
|
|
26
31
|
console.error(
|
|
27
32
|
'ERROR: Missing code to execute.\n\n' +
|
|
28
33
|
'Usage: capture exec <code> [--target <id>] [--record] [--file <path>]\n\n' +
|
|
34
|
+
'Supported shapes:\n' +
|
|
35
|
+
' document.title\n' +
|
|
36
|
+
' return document.title\n' +
|
|
37
|
+
' const r = await fetch(\'/api/data\'); return await r.json()\n\n' +
|
|
29
38
|
'Examples:\n' +
|
|
30
39
|
' capture exec "document.title" --target <id>\n' +
|
|
40
|
+
' capture exec "return document.title" --target <id>\n' +
|
|
31
41
|
' capture exec "document.querySelector(\'.btn\').click()" --target <id>\n' +
|
|
32
42
|
' capture exec --file /tmp/scrape.js --target <id> --record',
|
|
33
43
|
);
|
|
@@ -35,6 +45,19 @@ export async function cmdExec(parsed: ParsedArgs, _args: string[]): Promise<void
|
|
|
35
45
|
}
|
|
36
46
|
}
|
|
37
47
|
|
|
48
|
+
// Import-driven exec: if the code has leading static imports, bundle the
|
|
49
|
+
// forked vault libs on the fly (esbuild) BEFORE opening a tab — fail fast,
|
|
50
|
+
// no wasted CDP connection. Plain exec (no imports) skips this entirely.
|
|
51
|
+
let prebuilt: string | undefined;
|
|
52
|
+
if (hasImports(code)) {
|
|
53
|
+
try {
|
|
54
|
+
prebuilt = await bundleExec(code);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.error(`ERROR: ${(e as Error).message}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
38
61
|
await withConnection(
|
|
39
62
|
parsed,
|
|
40
63
|
async (client) => {
|
|
@@ -47,13 +70,11 @@ export async function cmdExec(parsed: ParsedArgs, _args: string[]): Promise<void
|
|
|
47
70
|
await standaloneRecorder.start();
|
|
48
71
|
}
|
|
49
72
|
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
const expression =
|
|
55
|
-
? `(async () => { ${code} })()`
|
|
56
|
-
: code;
|
|
73
|
+
// A prebuilt bundle is already a complete IIFE returning the user's
|
|
74
|
+
// promise. Otherwise fall back to the exec expression wrapper: bare
|
|
75
|
+
// expressions run directly, while snippets containing top-level `return`
|
|
76
|
+
// or `await` are wrapped in an async IIFE.
|
|
77
|
+
const expression = buildExecExpression(code, prebuilt);
|
|
57
78
|
|
|
58
79
|
const evalResult = (await client.send('Runtime.evaluate', {
|
|
59
80
|
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/commands/tabs.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { detectCdpPortsAsync, detectCdpPort, getDefaultBrowserId } from '../detect.js';
|
|
1
|
+
import { detectCdpPortsAsync, detectCdpPort, getDefaultBrowserId, pickPreferredEndpoint } from '../detect.js';
|
|
2
2
|
import { listTargets, openTab } from '../targets.js';
|
|
3
3
|
import { navigateAndWait } from '../record.js';
|
|
4
4
|
import { CDPClient } from '../client.js';
|
|
@@ -25,13 +25,7 @@ export async function cmdDetect(parsed: ParsedArgs, _args: string[]): Promise<vo
|
|
|
25
25
|
);
|
|
26
26
|
process.exit(1);
|
|
27
27
|
}
|
|
28
|
-
const
|
|
29
|
-
const preferred =
|
|
30
|
-
endpoints.find(
|
|
31
|
-
(p) => p.bundleId === defaultBrowser && !p.isElectron,
|
|
32
|
-
) ??
|
|
33
|
-
endpoints.find((p) => !p.isElectron) ??
|
|
34
|
-
endpoints[0];
|
|
28
|
+
const preferred = pickPreferredEndpoint(endpoints, getDefaultBrowserId());
|
|
35
29
|
console.log(
|
|
36
30
|
JSON.stringify(
|
|
37
31
|
{
|
package/src/cdp/commands/ui.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type ParsedArgs } from '../types.js';
|
|
|
9
9
|
export async function cmdScreenshot(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
10
10
|
if (parsed.help) {
|
|
11
11
|
console.log(
|
|
12
|
-
'Usage: capture screenshot [--target <id>] [--out <path>] [--viewport <preset>] [--height <px>] [--full-page]\n\n' +
|
|
12
|
+
'Usage: capture screenshot [--target <id> | --url <pattern>] [--out <path>] [--viewport <preset>] [--height <px>] [--full-page]\n\n' +
|
|
13
13
|
'Capture a screenshot of the targeted tab. Saves to --out or auto-generates a path.\n\n' +
|
|
14
14
|
'Options:\n' +
|
|
15
15
|
' --viewport <preset> Viewport preset (default: desktop)\n' +
|