@jackwener/opencli 1.5.8 → 1.6.0
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 +42 -0
- package/README.md +35 -1
- package/README.zh-CN.md +17 -1
- package/SKILL.md +31 -851
- package/autoresearch/baseline-browse.txt +1 -0
- package/autoresearch/baseline-skill.txt +1 -0
- package/autoresearch/browse-tasks.json +688 -0
- package/autoresearch/eval-browse.ts +185 -0
- package/autoresearch/eval-skill.ts +248 -0
- package/autoresearch/run-browse.sh +9 -0
- package/autoresearch/run-skill.sh +9 -0
- package/dist/browser/base-page.d.ts +48 -0
- package/dist/browser/base-page.js +160 -0
- package/dist/browser/cdp.js +4 -106
- package/dist/browser/daemon-client.d.ts +20 -7
- package/dist/browser/daemon-client.js +39 -39
- package/dist/browser/daemon-client.test.js +77 -0
- package/dist/browser/discover.d.ts +1 -4
- package/dist/browser/discover.js +9 -23
- package/dist/browser/errors.d.ts +4 -0
- package/dist/browser/errors.js +20 -0
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/page.d.ts +10 -35
- package/dist/browser/page.js +55 -187
- package/dist/browser/tabs.js +5 -5
- package/dist/browser.test.js +15 -15
- package/dist/cli-manifest.json +294 -22
- package/dist/cli.js +392 -0
- package/dist/clis/amazon/bestsellers.d.ts +21 -0
- package/dist/clis/amazon/bestsellers.js +130 -0
- package/dist/clis/amazon/bestsellers.test.js +20 -0
- package/dist/clis/amazon/discussion.d.ts +20 -0
- package/dist/clis/amazon/discussion.js +91 -0
- package/dist/clis/amazon/discussion.test.d.ts +1 -0
- package/dist/clis/amazon/discussion.test.js +36 -0
- package/dist/clis/amazon/offer.d.ts +23 -0
- package/dist/clis/amazon/offer.js +140 -0
- package/dist/clis/amazon/offer.test.d.ts +1 -0
- package/dist/clis/amazon/offer.test.js +29 -0
- package/dist/clis/amazon/product.d.ts +18 -0
- package/dist/clis/amazon/product.js +92 -0
- package/dist/clis/amazon/product.test.d.ts +1 -0
- package/dist/clis/amazon/product.test.js +24 -0
- package/dist/clis/amazon/search.d.ts +18 -0
- package/dist/clis/amazon/search.js +87 -0
- package/dist/clis/amazon/search.test.d.ts +1 -0
- package/dist/clis/amazon/search.test.js +22 -0
- package/dist/clis/amazon/shared.d.ts +64 -0
- package/dist/clis/amazon/shared.js +255 -0
- package/dist/clis/amazon/shared.test.d.ts +1 -0
- package/dist/clis/amazon/shared.test.js +33 -0
- package/dist/clis/gemini/ask.d.ts +1 -0
- package/dist/clis/gemini/ask.js +40 -0
- package/dist/clis/gemini/image.d.ts +1 -0
- package/dist/clis/gemini/image.js +105 -0
- package/dist/clis/gemini/new.d.ts +1 -0
- package/dist/clis/gemini/new.js +20 -0
- package/dist/clis/gemini/utils.d.ts +34 -0
- package/dist/clis/gemini/utils.js +463 -0
- package/dist/clis/gemini/utils.test.d.ts +1 -0
- package/dist/clis/gemini/utils.test.js +31 -0
- package/dist/clis/notebooklm/compat.test.d.ts +1 -1
- package/dist/clis/notebooklm/compat.test.js +3 -3
- package/dist/clis/notebooklm/current.js +2 -3
- package/dist/clis/notebooklm/get.js +2 -3
- package/dist/clis/notebooklm/history.js +2 -3
- package/dist/clis/notebooklm/note-list.js +2 -3
- package/dist/clis/notebooklm/notes-get.js +2 -3
- package/dist/clis/notebooklm/open.d.ts +1 -0
- package/dist/clis/notebooklm/open.js +41 -0
- package/dist/clis/notebooklm/open.test.d.ts +1 -0
- package/dist/clis/notebooklm/open.test.js +63 -0
- package/dist/clis/notebooklm/source-fulltext.js +2 -3
- package/dist/clis/notebooklm/source-get.js +2 -3
- package/dist/clis/notebooklm/source-guide.js +2 -3
- package/dist/clis/notebooklm/source-list.js +2 -3
- package/dist/clis/notebooklm/status.js +1 -2
- package/dist/clis/notebooklm/summary.js +2 -3
- package/dist/clis/notebooklm/utils.d.ts +2 -1
- package/dist/clis/notebooklm/utils.js +20 -21
- package/dist/clis/twitter/article.js +28 -1
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +11 -11
- package/dist/clis/xiaohongshu/creator-notes-summary.test.js +6 -6
- package/dist/clis/xiaohongshu/creator-notes.test.js +22 -22
- package/dist/clis/xiaohongshu/note.js +11 -0
- package/dist/clis/xiaohongshu/note.test.js +49 -0
- package/dist/commanderAdapter.js +7 -4
- package/dist/commanderAdapter.test.js +76 -0
- package/dist/commands/daemon.js +8 -47
- package/dist/commands/daemon.test.js +45 -70
- package/dist/discovery.js +27 -0
- package/dist/doctor.d.ts +1 -2
- package/dist/doctor.js +7 -8
- package/dist/explore.js +1 -1
- package/dist/output.js +28 -0
- package/dist/output.test.js +15 -0
- package/dist/pipeline/executor.js +2 -7
- package/dist/pipeline/steps/browser.js +1 -1
- package/dist/pipeline/template.js +25 -3
- package/dist/record.d.ts +50 -0
- package/dist/record.js +298 -57
- package/dist/record.test.d.ts +1 -0
- package/dist/record.test.js +293 -0
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +1 -0
- package/dist/registry.test.js +10 -0
- package/dist/runtime.js +3 -3
- package/dist/snapshotFormatter.d.ts +1 -1
- package/dist/snapshotFormatter.js +4 -4
- package/dist/snapshotFormatter.test.d.ts +1 -1
- package/dist/snapshotFormatter.test.js +2 -2
- package/dist/types.d.ts +11 -1
- package/dist/types.js +1 -1
- package/docs/.vitepress/config.mts +2 -0
- package/docs/adapters/browser/amazon.md +53 -0
- package/docs/adapters/browser/gemini.md +72 -0
- package/docs/adapters/browser/notebooklm.md +5 -5
- package/docs/adapters/index.md +3 -1
- package/docs/guide/getting-started.md +21 -0
- package/docs/superpowers/specs/2026-04-02-browse-skill-testing-design.md +144 -0
- package/docs/zh/guide/getting-started.md +21 -0
- package/extension/package-lock.json +2 -2
- package/extension/src/background.test.ts +7 -163
- package/extension/src/background.ts +58 -161
- package/extension/src/cdp.ts +77 -124
- package/extension/src/protocol.ts +5 -5
- package/package.json +1 -1
- package/skills/opencli-explorer/SKILL.md +853 -0
- package/skills/opencli-oneshot/SKILL.md +222 -0
- package/skills/opencli-operate/SKILL.md +213 -0
- package/skills/opencli-usage/SKILL.md +152 -0
- package/skills/opencli-usage/browser.md +429 -0
- package/skills/opencli-usage/desktop.md +118 -0
- package/skills/opencli-usage/plugins.md +82 -0
- package/skills/opencli-usage/public-api.md +149 -0
- package/src/browser/base-page.ts +197 -0
- package/src/browser/cdp.ts +7 -131
- package/src/browser/daemon-client.test.ts +103 -0
- package/src/browser/daemon-client.ts +55 -43
- package/src/browser/discover.ts +9 -21
- package/src/browser/errors.ts +22 -0
- package/src/browser/index.ts +1 -1
- package/src/browser/page.ts +57 -209
- package/src/browser/tabs.ts +5 -5
- package/src/browser.test.ts +15 -15
- package/src/cli.ts +392 -0
- package/src/clis/amazon/bestsellers.test.ts +22 -0
- package/src/clis/amazon/bestsellers.ts +180 -0
- package/src/clis/amazon/discussion.test.ts +38 -0
- package/src/clis/amazon/discussion.ts +131 -0
- package/src/clis/amazon/offer.test.ts +35 -0
- package/src/clis/amazon/offer.ts +185 -0
- package/src/clis/amazon/product.test.ts +26 -0
- package/src/clis/amazon/product.ts +131 -0
- package/src/clis/amazon/search.test.ts +24 -0
- package/src/clis/amazon/search.ts +128 -0
- package/src/clis/amazon/shared.test.ts +37 -0
- package/src/clis/amazon/shared.ts +316 -0
- package/src/clis/gemini/ask.ts +46 -0
- package/src/clis/gemini/image.ts +115 -0
- package/src/clis/gemini/new.ts +22 -0
- package/src/clis/gemini/utils.test.ts +36 -0
- package/src/clis/gemini/utils.ts +523 -0
- package/src/clis/notebooklm/compat.test.ts +3 -3
- package/src/clis/notebooklm/current.ts +2 -3
- package/src/clis/notebooklm/get.ts +1 -3
- package/src/clis/notebooklm/history.ts +1 -3
- package/src/clis/notebooklm/note-list.ts +1 -3
- package/src/clis/notebooklm/notes-get.ts +1 -3
- package/src/clis/notebooklm/open.test.ts +78 -0
- package/src/clis/notebooklm/open.ts +61 -0
- package/src/clis/notebooklm/source-fulltext.ts +1 -3
- package/src/clis/notebooklm/source-get.ts +1 -3
- package/src/clis/notebooklm/source-guide.ts +1 -3
- package/src/clis/notebooklm/source-list.ts +1 -3
- package/src/clis/notebooklm/status.ts +1 -2
- package/src/clis/notebooklm/summary.ts +1 -3
- package/src/clis/notebooklm/utils.ts +29 -20
- package/src/clis/twitter/article.ts +31 -1
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
- package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
- package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
- package/src/clis/xiaohongshu/note.test.ts +51 -0
- package/src/clis/xiaohongshu/note.ts +18 -0
- package/src/commanderAdapter.test.ts +109 -0
- package/src/commanderAdapter.ts +8 -4
- package/src/commands/daemon.test.ts +50 -84
- package/src/commands/daemon.ts +8 -56
- package/src/discovery.ts +22 -0
- package/src/doctor.ts +8 -9
- package/src/explore.ts +1 -1
- package/src/output.test.ts +17 -0
- package/src/output.ts +27 -0
- package/src/pipeline/executor.ts +2 -7
- package/src/pipeline/steps/browser.ts +1 -1
- package/src/pipeline/template.ts +27 -4
- package/src/record.test.ts +362 -0
- package/src/record.ts +341 -62
- package/src/registry.test.ts +12 -0
- package/src/registry.ts +3 -0
- package/src/runtime.ts +3 -3
- package/src/snapshotFormatter.test.ts +2 -2
- package/src/snapshotFormatter.ts +4 -4
- package/src/types.ts +11 -1
- package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
- package/.agents/workflows/cross-project-adapter-migration.md +0 -54
- package/dist/clis/notebooklm/bind-current.js +0 -29
- package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
- package/dist/clis/notebooklm/bind-current.test.js +0 -35
- package/dist/clis/notebooklm/binding.test.js +0 -44
- package/extension/dist/background.js +0 -819
- package/src/clis/notebooklm/bind-current.test.ts +0 -43
- package/src/clis/notebooklm/bind-current.ts +0 -36
- package/src/clis/notebooklm/binding.test.ts +0 -53
- /package/dist/browser/{mcp.d.ts → bridge.d.ts} +0 -0
- /package/dist/browser/{mcp.js → bridge.js} +0 -0
- /package/dist/{clis/notebooklm/bind-current.d.ts → browser/daemon-client.test.d.ts} +0 -0
- /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/bestsellers.test.d.ts} +0 -0
- /package/src/browser/{mcp.ts → bridge.ts} +0 -0
package/src/record.ts
CHANGED
|
@@ -35,6 +35,14 @@ export interface RecordedRequest {
|
|
|
35
35
|
url: string;
|
|
36
36
|
method: string;
|
|
37
37
|
status: number | null;
|
|
38
|
+
/** Request content type captured at record time, if available. */
|
|
39
|
+
requestContentType: string | null;
|
|
40
|
+
/** Response content type captured at record time, if available. */
|
|
41
|
+
responseContentType: string | null;
|
|
42
|
+
/** Parsed JSON request body for replayable write requests. */
|
|
43
|
+
requestBody: unknown;
|
|
44
|
+
/** Parsed JSON response body captured from the network call. */
|
|
45
|
+
responseBody: unknown;
|
|
38
46
|
contentType: string;
|
|
39
47
|
body: unknown;
|
|
40
48
|
capturedAt: number;
|
|
@@ -49,13 +57,109 @@ export interface RecordResult {
|
|
|
49
57
|
candidates: Array<{ name: string; path: string; strategy: string }>;
|
|
50
58
|
}
|
|
51
59
|
|
|
60
|
+
type RecordedCandidateKind = 'read' | 'write';
|
|
61
|
+
|
|
62
|
+
export interface RecordedCandidate {
|
|
63
|
+
kind: RecordedCandidateKind;
|
|
64
|
+
req: RecordedRequest;
|
|
65
|
+
score: number;
|
|
66
|
+
arrayResult: ReturnType<typeof findArrayPath> | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface GeneratedRecordedCandidate {
|
|
70
|
+
kind: RecordedCandidateKind;
|
|
71
|
+
name: string;
|
|
72
|
+
strategy: string;
|
|
73
|
+
yaml: unknown;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Keep the stronger candidate when multiple recordings share one bucket. */
|
|
77
|
+
function preferRecordedCandidate(current: RecordedCandidate, next: RecordedCandidate): RecordedCandidate {
|
|
78
|
+
if (next.score > current.score) return next;
|
|
79
|
+
if (next.score < current.score) return current;
|
|
80
|
+
return next;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Apply shared endpoint score tweaks. */
|
|
84
|
+
function applyCommonEndpointScoreAdjustments(req: RecordedRequest, score: number): number {
|
|
85
|
+
let adjusted = score;
|
|
86
|
+
if (req.url.includes('/api/')) adjusted += 3;
|
|
87
|
+
if (req.url.match(/\/(track|log|analytics|beacon|pixel|stats|metric)/i)) adjusted -= 10;
|
|
88
|
+
if (req.url.match(/\/(ping|heartbeat|keep.?alive)/i)) adjusted -= 10;
|
|
89
|
+
return adjusted;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Build a candidate-level dedupe key. */
|
|
93
|
+
function getRecordedCandidateKey(candidate: RecordedCandidate): string {
|
|
94
|
+
return `${candidate.kind} ${getRecordedRequestKey(candidate.req)}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Build a request dedupe key from method and URL pattern. */
|
|
98
|
+
function getRecordedRequestKey(req: RecordedRequest): string {
|
|
99
|
+
return `${req.method.toUpperCase()} ${urlToPattern(req.url)}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Deduplicate recorded requests by method and URL pattern. */
|
|
103
|
+
function dedupeRecordedRequests(requests: RecordedRequest[]): RecordedRequest[] {
|
|
104
|
+
const deduped = new Map<string, RecordedRequest>();
|
|
105
|
+
for (const req of requests) {
|
|
106
|
+
deduped.set(getRecordedRequestKey(req), req);
|
|
107
|
+
}
|
|
108
|
+
return [...deduped.values()];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Check whether a content type should be treated as JSON. */
|
|
112
|
+
function isJsonContentType(contentType: string | null | undefined): boolean {
|
|
113
|
+
const normalized = contentType?.toLowerCase() ?? '';
|
|
114
|
+
return normalized.includes('application/json') || normalized.includes('+json');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Parse a captured request body only when the request advertises JSON. */
|
|
118
|
+
function parseJsonBodyText(contentType: string | null | undefined, raw: string | null | undefined): unknown {
|
|
119
|
+
if (!isJsonContentType(contentType)) return null;
|
|
120
|
+
if (!raw || !raw.trim()) return null;
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(raw);
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Build one normalized recorded entry from captured request and response values. */
|
|
129
|
+
export function createRecordedEntry(input: {
|
|
130
|
+
url: string;
|
|
131
|
+
method: string;
|
|
132
|
+
requestContentType?: string | null;
|
|
133
|
+
requestBodyText?: string | null;
|
|
134
|
+
responseBody: unknown;
|
|
135
|
+
responseContentType?: string | null;
|
|
136
|
+
status?: number | null;
|
|
137
|
+
capturedAt?: number;
|
|
138
|
+
}): RecordedRequest {
|
|
139
|
+
const requestBody = parseJsonBodyText(input.requestContentType ?? null, input.requestBodyText ?? null);
|
|
140
|
+
const responseContentType = input.responseContentType ?? 'application/json';
|
|
141
|
+
return {
|
|
142
|
+
url: input.url,
|
|
143
|
+
method: input.method.toUpperCase(),
|
|
144
|
+
status: input.status ?? null,
|
|
145
|
+
requestContentType: input.requestContentType ?? null,
|
|
146
|
+
responseContentType,
|
|
147
|
+
requestBody,
|
|
148
|
+
responseBody: input.responseBody,
|
|
149
|
+
// Keep legacy fields in sync until the analyzer/template path is migrated.
|
|
150
|
+
contentType: responseContentType,
|
|
151
|
+
body: input.responseBody,
|
|
152
|
+
capturedAt: input.capturedAt ?? Date.now(),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
52
156
|
// ── Interceptor JS ─────────────────────────────────────────────────────────
|
|
53
157
|
|
|
54
158
|
/**
|
|
55
159
|
* Generates a full-capture interceptor that stores {url, method, status, body}
|
|
56
160
|
* for every JSON response. No URL pattern filter — captures everything.
|
|
57
161
|
*/
|
|
58
|
-
function generateFullCaptureInterceptorJs(): string {
|
|
162
|
+
export function generateFullCaptureInterceptorJs(): string {
|
|
59
163
|
return `
|
|
60
164
|
(() => {
|
|
61
165
|
// Restore original fetch/XHR if previously patched, then re-patch (idempotent injection)
|
|
@@ -63,24 +167,47 @@ function generateFullCaptureInterceptorJs(): string {
|
|
|
63
167
|
if (window.__opencli_orig_fetch) window.fetch = window.__opencli_orig_fetch;
|
|
64
168
|
if (window.__opencli_orig_xhr_open) XMLHttpRequest.prototype.open = window.__opencli_orig_xhr_open;
|
|
65
169
|
if (window.__opencli_orig_xhr_send) XMLHttpRequest.prototype.send = window.__opencli_orig_xhr_send;
|
|
170
|
+
if (window.__opencli_orig_xhr_set_request_header) XMLHttpRequest.prototype.setRequestHeader = window.__opencli_orig_xhr_set_request_header;
|
|
66
171
|
window.__opencli_record_patched = false;
|
|
67
172
|
}
|
|
68
173
|
// Preserve existing capture buffer across re-injections
|
|
69
174
|
window.__opencli_record = window.__opencli_record || [];
|
|
70
175
|
|
|
71
|
-
const
|
|
176
|
+
const _tryParseJson = (contentType, raw) => {
|
|
72
177
|
try {
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
178
|
+
const normalized = String(contentType || '').toLowerCase();
|
|
179
|
+
if (!normalized.includes('application/json') && !normalized.includes('+json')) return null;
|
|
180
|
+
if (typeof raw !== 'string' || !raw.trim()) return null;
|
|
181
|
+
return JSON.parse(raw);
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const _push = (entry) => {
|
|
188
|
+
try {
|
|
189
|
+
const responseBody = entry.responseBody;
|
|
190
|
+
if (typeof responseBody !== 'object' || responseBody === null) return;
|
|
191
|
+
const isReplayableWrite = ['POST', 'PUT', 'PATCH'].includes(String(entry.method).toUpperCase())
|
|
192
|
+
&& (() => {
|
|
193
|
+
const normalized = String(entry.requestContentType || '').toLowerCase();
|
|
194
|
+
return normalized.includes('application/json') || normalized.includes('+json');
|
|
195
|
+
})()
|
|
196
|
+
&& entry.requestBody
|
|
197
|
+
&& typeof entry.requestBody === 'object';
|
|
198
|
+
const keys = Object.keys(responseBody);
|
|
199
|
+
if (keys.length < 2 && !isReplayableWrite) return;
|
|
78
200
|
window.__opencli_record.push({
|
|
79
|
-
url: String(url),
|
|
80
|
-
method: String(method).toUpperCase(),
|
|
201
|
+
url: String(entry.url),
|
|
202
|
+
method: String(entry.method).toUpperCase(),
|
|
81
203
|
status: null,
|
|
82
|
-
|
|
83
|
-
|
|
204
|
+
requestContentType: entry.requestContentType || null,
|
|
205
|
+
responseContentType: entry.responseContentType || 'application/json',
|
|
206
|
+
requestBody: entry.requestBody || null,
|
|
207
|
+
responseBody,
|
|
208
|
+
contentType: entry.responseContentType || 'application/json',
|
|
209
|
+
body: responseBody,
|
|
210
|
+
capturedAt: Date.now(),
|
|
84
211
|
});
|
|
85
212
|
} catch {}
|
|
86
213
|
};
|
|
@@ -89,14 +216,53 @@ function generateFullCaptureInterceptorJs(): string {
|
|
|
89
216
|
window.__opencli_orig_fetch = window.fetch;
|
|
90
217
|
window.fetch = async function(...args) {
|
|
91
218
|
const req = args[0];
|
|
219
|
+
const init = args[1] || {};
|
|
92
220
|
const reqUrl = typeof req === 'string' ? req : (req instanceof Request ? req.url : String(req));
|
|
93
|
-
const method = (
|
|
221
|
+
const method = (init?.method || (req instanceof Request ? req.method : 'GET') || 'GET');
|
|
222
|
+
const requestContentType = (() => {
|
|
223
|
+
if (init?.headers) {
|
|
224
|
+
try {
|
|
225
|
+
const headers = new Headers(init.headers);
|
|
226
|
+
const value = headers.get('content-type');
|
|
227
|
+
if (value) return value;
|
|
228
|
+
} catch {}
|
|
229
|
+
}
|
|
230
|
+
if (req instanceof Request) {
|
|
231
|
+
return req.headers.get('content-type');
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
})();
|
|
235
|
+
const requestBodyText = (() => {
|
|
236
|
+
if (typeof init?.body === 'string') return init.body;
|
|
237
|
+
return null;
|
|
238
|
+
})();
|
|
239
|
+
const shouldReadRequestBodyFromRequest = req instanceof Request
|
|
240
|
+
&& !requestBodyText
|
|
241
|
+
&& ['POST', 'PUT', 'PATCH'].includes(String(method).toUpperCase())
|
|
242
|
+
&& (() => {
|
|
243
|
+
const normalized = String(requestContentType || '').toLowerCase();
|
|
244
|
+
return normalized.includes('application/json') || normalized.includes('+json');
|
|
245
|
+
})();
|
|
246
|
+
let requestBodyTextFromRequest = null;
|
|
247
|
+
if (shouldReadRequestBodyFromRequest) {
|
|
248
|
+
try {
|
|
249
|
+
requestBodyTextFromRequest = await req.clone().text();
|
|
250
|
+
} catch {}
|
|
251
|
+
}
|
|
252
|
+
const requestBody = _tryParseJson(requestContentType, requestBodyText || requestBodyTextFromRequest);
|
|
94
253
|
const res = await window.__opencli_orig_fetch.apply(this, args);
|
|
95
254
|
const ct = res.headers.get('content-type') || '';
|
|
96
255
|
if (ct.includes('json')) {
|
|
97
256
|
try {
|
|
98
|
-
const
|
|
99
|
-
_push(
|
|
257
|
+
const responseBody = await res.clone().json();
|
|
258
|
+
_push({
|
|
259
|
+
url: reqUrl,
|
|
260
|
+
method,
|
|
261
|
+
requestContentType,
|
|
262
|
+
requestBody,
|
|
263
|
+
responseContentType: ct,
|
|
264
|
+
responseBody,
|
|
265
|
+
});
|
|
100
266
|
} catch {}
|
|
101
267
|
}
|
|
102
268
|
return res;
|
|
@@ -106,20 +272,38 @@ function generateFullCaptureInterceptorJs(): string {
|
|
|
106
272
|
const _XHR = XMLHttpRequest.prototype;
|
|
107
273
|
window.__opencli_orig_xhr_open = _XHR.open;
|
|
108
274
|
window.__opencli_orig_xhr_send = _XHR.send;
|
|
275
|
+
window.__opencli_orig_xhr_set_request_header = _XHR.setRequestHeader;
|
|
109
276
|
_XHR.open = function(method, url) {
|
|
110
277
|
this.__rec_url = String(url);
|
|
111
278
|
this.__rec_method = String(method);
|
|
279
|
+
this.__rec_request_content_type = null;
|
|
112
280
|
this.__rec_listener_added = false; // reset per open() call
|
|
113
281
|
return window.__opencli_orig_xhr_open.apply(this, arguments);
|
|
114
282
|
};
|
|
283
|
+
_XHR.setRequestHeader = function(name, value) {
|
|
284
|
+
if (String(name).toLowerCase() === 'content-type') {
|
|
285
|
+
this.__rec_request_content_type = String(value);
|
|
286
|
+
}
|
|
287
|
+
return window.__opencli_orig_xhr_set_request_header.apply(this, arguments);
|
|
288
|
+
};
|
|
115
289
|
_XHR.send = function() {
|
|
290
|
+
const requestBody = _tryParseJson(this.__rec_request_content_type, typeof arguments[0] === 'string' ? arguments[0] : null);
|
|
116
291
|
// Guard: only add one listener per XHR instance to prevent duplicate captures
|
|
117
292
|
if (!this.__rec_listener_added) {
|
|
118
293
|
this.__rec_listener_added = true;
|
|
119
294
|
this.addEventListener('load', function() {
|
|
120
295
|
const ct = this.getResponseHeader?.('content-type') || '';
|
|
121
296
|
if (ct.includes('json')) {
|
|
122
|
-
try {
|
|
297
|
+
try {
|
|
298
|
+
_push({
|
|
299
|
+
url: this.__rec_url,
|
|
300
|
+
method: this.__rec_method || 'GET',
|
|
301
|
+
requestContentType: this.__rec_request_content_type,
|
|
302
|
+
requestBody,
|
|
303
|
+
responseContentType: ct,
|
|
304
|
+
responseBody: JSON.parse(this.responseText),
|
|
305
|
+
});
|
|
306
|
+
} catch {}
|
|
123
307
|
}
|
|
124
308
|
});
|
|
125
309
|
}
|
|
@@ -159,11 +343,42 @@ function scoreRequest(req: RecordedRequest, arrayResult: ReturnType<typeof findA
|
|
|
159
343
|
}
|
|
160
344
|
}
|
|
161
345
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
346
|
+
return applyCommonEndpointScoreAdjustments(req, s);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Check whether one recorded request is safe to treat as a write candidate. */
|
|
350
|
+
function isWriteCandidate(req: RecordedRequest): boolean {
|
|
351
|
+
return ['POST', 'PUT', 'PATCH'].includes(req.method)
|
|
352
|
+
&& isJsonContentType(req.requestContentType)
|
|
353
|
+
&& !!req.requestBody
|
|
354
|
+
&& typeof req.requestBody === 'object'
|
|
355
|
+
&& !Array.isArray(req.requestBody)
|
|
356
|
+
&& !!req.responseBody
|
|
357
|
+
&& typeof req.responseBody === 'object'
|
|
358
|
+
&& !Array.isArray(req.responseBody);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Score replayable write requests while keeping tracking and heartbeat traffic suppressed. */
|
|
362
|
+
function scoreWriteRequest(req: RecordedRequest): number {
|
|
363
|
+
return applyCommonEndpointScoreAdjustments(req, 6);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Analyze recorded requests into read and write candidates. */
|
|
367
|
+
export function analyzeRecordedRequests(requests: RecordedRequest[]): { candidates: RecordedCandidate[] } {
|
|
368
|
+
const candidates: RecordedCandidate[] = [];
|
|
369
|
+
for (const req of requests) {
|
|
370
|
+
const arrayResult = findArrayPath(req.responseBody);
|
|
371
|
+
if (isWriteCandidate(req)) {
|
|
372
|
+
const score = scoreWriteRequest(req);
|
|
373
|
+
if (score > 0) candidates.push({ kind: 'write', req, score, arrayResult: null });
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (arrayResult) {
|
|
377
|
+
const score = scoreRequest(req, arrayResult);
|
|
378
|
+
if (score > 0) candidates.push({ kind: 'read', req, score, arrayResult });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return { candidates };
|
|
167
382
|
}
|
|
168
383
|
|
|
169
384
|
// ── YAML generation ────────────────────────────────────────────────────────
|
|
@@ -214,15 +429,20 @@ function buildRecordedYaml(
|
|
|
214
429
|
const u = new URL(req.url);
|
|
215
430
|
if (hasSearch) {
|
|
216
431
|
for (const p of SEARCH_PARAMS) {
|
|
217
|
-
if (u.searchParams.has(p)) { u.searchParams.set(p, '{{args.keyword}}'); break; }
|
|
432
|
+
if (u.searchParams.has(p)) { u.searchParams.set(p, '${{ args.keyword }}'); break; }
|
|
218
433
|
}
|
|
219
434
|
}
|
|
220
435
|
if (hasPage) {
|
|
221
436
|
for (const p of PAGINATION_PARAMS) {
|
|
222
|
-
if (u.searchParams.has(p)) { u.searchParams.set(p, '{{args.page | default(1)}}'); break; }
|
|
437
|
+
if (u.searchParams.has(p)) { u.searchParams.set(p, '${{ args.page | default(1) }}'); break; }
|
|
223
438
|
}
|
|
224
439
|
}
|
|
225
440
|
fetchUrl = u.toString();
|
|
441
|
+
fetchUrl = fetchUrl
|
|
442
|
+
.replaceAll(encodeURIComponent('${{ args.keyword }}'), '${{ args.keyword }}')
|
|
443
|
+
.replaceAll('%24%7B%7B+args.keyword+%7D%7D', '${{ args.keyword }}')
|
|
444
|
+
.replaceAll(encodeURIComponent('${{ args.page | default(1) }}'), '${{ args.page | default(1) }}');
|
|
445
|
+
fetchUrl = fetchUrl.replaceAll('%24%7B%7B+args.page+%7C+default%281%29+%7D%7D', '${{ args.page | default(1) }}');
|
|
226
446
|
} catch {}
|
|
227
447
|
|
|
228
448
|
// When itemPath is empty, the array IS the response root; otherwise chain with ?.
|
|
@@ -271,6 +491,87 @@ function buildRecordedYaml(
|
|
|
271
491
|
};
|
|
272
492
|
}
|
|
273
493
|
|
|
494
|
+
/** Build a minimal YAML candidate for replayable JSON write requests. */
|
|
495
|
+
export function buildWriteRecordedYaml(
|
|
496
|
+
site: string,
|
|
497
|
+
pageUrl: string,
|
|
498
|
+
req: RecordedRequest,
|
|
499
|
+
capName: string,
|
|
500
|
+
): { name: string; yaml: unknown } {
|
|
501
|
+
const responseColumns = req.responseBody && typeof req.responseBody === 'object' && !Array.isArray(req.responseBody)
|
|
502
|
+
? Object.keys(req.responseBody as Record<string, unknown>).slice(0, 6)
|
|
503
|
+
: ['ok'];
|
|
504
|
+
|
|
505
|
+
const evaluateScript = [
|
|
506
|
+
'(async () => {',
|
|
507
|
+
` const res = await fetch(${JSON.stringify(req.url)}, {`,
|
|
508
|
+
` method: ${JSON.stringify(req.method)},`,
|
|
509
|
+
` credentials: 'include',`,
|
|
510
|
+
` headers: { 'content-type': ${JSON.stringify(req.requestContentType ?? 'application/json')} },`,
|
|
511
|
+
` body: JSON.stringify(${JSON.stringify(req.requestBody)}),`,
|
|
512
|
+
' });',
|
|
513
|
+
' return await res.json();',
|
|
514
|
+
'})()',
|
|
515
|
+
].join('\n');
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
name: capName,
|
|
519
|
+
yaml: {
|
|
520
|
+
site,
|
|
521
|
+
name: capName,
|
|
522
|
+
description: `${site} ${capName} (recorded write)`,
|
|
523
|
+
domain: (() => { try { return new URL(pageUrl).hostname; } catch { return ''; } })(),
|
|
524
|
+
strategy: 'cookie',
|
|
525
|
+
browser: true,
|
|
526
|
+
args: {},
|
|
527
|
+
pipeline: [
|
|
528
|
+
{ navigate: pageUrl },
|
|
529
|
+
{ evaluate: evaluateScript },
|
|
530
|
+
],
|
|
531
|
+
columns: responseColumns.length ? responseColumns : ['ok'],
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Turn recorded requests into YAML-ready read and write candidates. */
|
|
537
|
+
export function generateRecordedCandidates(
|
|
538
|
+
site: string,
|
|
539
|
+
pageUrl: string,
|
|
540
|
+
requests: RecordedRequest[],
|
|
541
|
+
): GeneratedRecordedCandidate[] {
|
|
542
|
+
const analysis = analyzeRecordedRequests(dedupeRecordedRequests(requests));
|
|
543
|
+
const deduped = new Map<string, RecordedCandidate>();
|
|
544
|
+
for (const candidate of analysis.candidates) {
|
|
545
|
+
const key = getRecordedCandidateKey(candidate);
|
|
546
|
+
const current = deduped.get(key);
|
|
547
|
+
deduped.set(key, current ? preferRecordedCandidate(current, candidate) : candidate);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const selected = [...deduped.values()]
|
|
551
|
+
.filter((candidate) => candidate.kind === 'read' ? candidate.score >= 8 : candidate.score >= 6)
|
|
552
|
+
.sort((a, b) => b.score - a.score)
|
|
553
|
+
.slice(0, 5);
|
|
554
|
+
|
|
555
|
+
const usedNames = new Set<string>();
|
|
556
|
+
return selected.map((candidate) => {
|
|
557
|
+
let capName = inferCapabilityName(candidate.req.url);
|
|
558
|
+
if (usedNames.has(capName)) capName = `${capName}_${usedNames.size + 1}`;
|
|
559
|
+
usedNames.add(capName);
|
|
560
|
+
|
|
561
|
+
const authIndicators = detectAuthFromContent(candidate.req.url, candidate.req.responseBody);
|
|
562
|
+
const strategy = candidate.kind === 'write' ? 'cookie' : inferStrategy(authIndicators);
|
|
563
|
+
const yamlCandidate = candidate.kind === 'write'
|
|
564
|
+
? buildWriteRecordedYaml(site, pageUrl, candidate.req, capName)
|
|
565
|
+
: buildRecordedYaml(site, pageUrl, candidate.req, capName, candidate.arrayResult!, authIndicators);
|
|
566
|
+
return {
|
|
567
|
+
kind: candidate.kind,
|
|
568
|
+
name: yamlCandidate.name,
|
|
569
|
+
strategy,
|
|
570
|
+
yaml: yamlCandidate.yaml,
|
|
571
|
+
};
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
274
575
|
// ── Main record function ───────────────────────────────────────────────────
|
|
275
576
|
|
|
276
577
|
export interface RecordOptions {
|
|
@@ -441,32 +742,9 @@ function analyzeAndWrite(
|
|
|
441
742
|
return { site, url: pageUrl, requests: [], outDir: targetDir, candidateCount: 0, candidates: [] };
|
|
442
743
|
}
|
|
443
744
|
|
|
444
|
-
//
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
const pattern = urlToPattern(req.url);
|
|
448
|
-
if (!seen.has(pattern)) seen.set(pattern, req);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Score and rank unique requests
|
|
452
|
-
type ScoredEntry = {
|
|
453
|
-
req: RecordedRequest;
|
|
454
|
-
pattern: string;
|
|
455
|
-
arrayResult: ReturnType<typeof findArrayPath>;
|
|
456
|
-
authIndicators: string[];
|
|
457
|
-
score: number;
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
const scored: ScoredEntry[] = [];
|
|
461
|
-
for (const [pattern, req] of seen) {
|
|
462
|
-
const arrayResult = findArrayPath(req.body);
|
|
463
|
-
const authIndicators = detectAuthFromContent(req.url, req.body);
|
|
464
|
-
const score = scoreRequest(req, arrayResult);
|
|
465
|
-
if (score > 0) {
|
|
466
|
-
scored.push({ req, pattern, arrayResult, authIndicators, score });
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
scored.sort((a, b) => b.score - a.score);
|
|
745
|
+
// Score and rank deduplicated requests for console output and candidate generation.
|
|
746
|
+
const analysisRequests = dedupeRecordedRequests(requests);
|
|
747
|
+
const analysis = analyzeRecordedRequests(analysisRequests);
|
|
470
748
|
|
|
471
749
|
// Save raw captured data
|
|
472
750
|
fs.writeFileSync(
|
|
@@ -480,35 +758,36 @@ function analyzeAndWrite(
|
|
|
480
758
|
|
|
481
759
|
console.log(chalk.bold('\n Captured endpoints (scored):\n'));
|
|
482
760
|
|
|
483
|
-
for (const entry of
|
|
761
|
+
for (const entry of analysis.candidates.sort((a, b) => b.score - a.score).slice(0, 8)) {
|
|
484
762
|
const itemCount = entry.arrayResult?.items.length ?? 0;
|
|
485
|
-
const strategy =
|
|
763
|
+
const strategy = entry.kind === 'write'
|
|
764
|
+
? 'cookie'
|
|
765
|
+
: inferStrategy(detectAuthFromContent(entry.req.url, entry.req.responseBody));
|
|
486
766
|
const marker = entry.score >= 15 ? chalk.green('★') : entry.score >= 8 ? chalk.yellow('◆') : chalk.dim('·');
|
|
487
767
|
console.log(
|
|
488
|
-
` ${marker} ${chalk.white(entry.
|
|
768
|
+
` ${marker} ${chalk.white(urlToPattern(entry.req.url))}` +
|
|
489
769
|
chalk.dim(` [${strategy}]`) +
|
|
490
|
-
(
|
|
770
|
+
(entry.kind === 'write'
|
|
771
|
+
? chalk.magenta(' ← write')
|
|
772
|
+
: itemCount ? chalk.cyan(` ← ${itemCount} items`) : ''),
|
|
491
773
|
);
|
|
492
774
|
}
|
|
493
775
|
|
|
494
776
|
console.log();
|
|
495
777
|
|
|
496
|
-
const topCandidates =
|
|
778
|
+
const topCandidates = generateRecordedCandidates(site, pageUrl, analysisRequests);
|
|
497
779
|
const candidatesDir = path.join(targetDir, 'candidates');
|
|
498
780
|
fs.mkdirSync(candidatesDir, { recursive: true });
|
|
499
781
|
|
|
500
782
|
for (const entry of topCandidates) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
usedNames.add(capName);
|
|
783
|
+
if (usedNames.has(entry.name)) continue;
|
|
784
|
+
usedNames.add(entry.name);
|
|
504
785
|
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
fs.writeFileSync(filePath, yaml.dump(candidate.yaml, { sortKeys: false, lineWidth: 120 }));
|
|
509
|
-
candidates.push({ name: capName, path: filePath, strategy });
|
|
786
|
+
const filePath = path.join(candidatesDir, `${entry.name}.yaml`);
|
|
787
|
+
fs.writeFileSync(filePath, yaml.dump(entry.yaml, { sortKeys: false, lineWidth: 120 }));
|
|
788
|
+
candidates.push({ name: entry.name, path: filePath, strategy: entry.strategy });
|
|
510
789
|
|
|
511
|
-
console.log(chalk.green(` ✓ Generated: ${chalk.bold(
|
|
790
|
+
console.log(chalk.green(` ✓ Generated: ${chalk.bold(entry.name)}.yaml [${entry.strategy}]`));
|
|
512
791
|
console.log(chalk.dim(` → ${filePath}`));
|
|
513
792
|
}
|
|
514
793
|
|
package/src/registry.test.ts
CHANGED
|
@@ -75,6 +75,18 @@ describe('cli() registration', () => {
|
|
|
75
75
|
expect(registry.get('test-registry/compat')).toBe(cmd);
|
|
76
76
|
expect(registry.get('test-registry/legacy-name')).toBe(cmd);
|
|
77
77
|
});
|
|
78
|
+
|
|
79
|
+
it('preserves defaultFormat on the registered command', () => {
|
|
80
|
+
const cmd = cli({
|
|
81
|
+
site: 'test-registry',
|
|
82
|
+
name: 'plain-default',
|
|
83
|
+
description: 'prefers plain output',
|
|
84
|
+
defaultFormat: 'plain',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(cmd.defaultFormat).toBe('plain');
|
|
88
|
+
expect(getRegistry().get('test-registry/plain-default')?.defaultFormat).toBe('plain');
|
|
89
|
+
});
|
|
78
90
|
});
|
|
79
91
|
|
|
80
92
|
describe('fullName', () => {
|
package/src/registry.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface CliCommand {
|
|
|
62
62
|
* - `string`: navigate to this specific URL instead of the domain root
|
|
63
63
|
*/
|
|
64
64
|
navigateBefore?: boolean | string;
|
|
65
|
+
/** Override the default CLI output format when the user does not pass -f/--format. */
|
|
66
|
+
defaultFormat?: 'table' | 'plain' | 'json' | 'yaml' | 'yml' | 'md' | 'markdown' | 'csv';
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/** Internal extension for lazy-loaded TS modules (not exposed in public API) */
|
|
@@ -105,6 +107,7 @@ export function cli(opts: CliOptions): CliCommand {
|
|
|
105
107
|
deprecated: opts.deprecated,
|
|
106
108
|
replacedBy: opts.replacedBy,
|
|
107
109
|
navigateBefore: opts.navigateBefore,
|
|
110
|
+
defaultFormat: opts.defaultFormat,
|
|
108
111
|
};
|
|
109
112
|
|
|
110
113
|
registerCommand(cmd);
|
package/src/runtime.ts
CHANGED
|
@@ -72,15 +72,15 @@ export async function browserSession<T>(
|
|
|
72
72
|
fn: (page: IPage) => Promise<T>,
|
|
73
73
|
opts: { workspace?: string; cdpEndpoint?: string } = {},
|
|
74
74
|
): Promise<T> {
|
|
75
|
-
const
|
|
75
|
+
const browser = new BrowserFactory();
|
|
76
76
|
try {
|
|
77
|
-
const page = await
|
|
77
|
+
const page = await browser.connect({
|
|
78
78
|
timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT,
|
|
79
79
|
workspace: opts.workspace,
|
|
80
80
|
cdpEndpoint: opts.cdpEndpoint,
|
|
81
81
|
});
|
|
82
82
|
return await fn(page);
|
|
83
83
|
} finally {
|
|
84
|
-
await
|
|
84
|
+
await browser.close().catch(() => {});
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for snapshotFormatter.ts:
|
|
2
|
+
* Tests for snapshotFormatter.ts: snapshot tree filtering.
|
|
3
3
|
*
|
|
4
4
|
* Uses sanitized excerpts from real websites (GitHub, Bilibili, Twitter)
|
|
5
5
|
* to validate noise filtering, annotation stripping, and output quality.
|
|
@@ -9,7 +9,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
9
9
|
import { formatSnapshot } from './snapshotFormatter.js';
|
|
10
10
|
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
|
-
// Fixtures: sanitized excerpts from real
|
|
12
|
+
// Fixtures: sanitized excerpts from real aria snapshots
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
|
|
15
15
|
/** GitHub dashboard navigation bar (generic-heavy, refs, /url: lines) */
|
package/src/snapshotFormatter.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Aria snapshot formatter: parses
|
|
2
|
+
* Aria snapshot formatter: parses snapshot text into clean format.
|
|
3
3
|
*
|
|
4
4
|
* 4-pass pipeline:
|
|
5
5
|
* 1. Parse & filter: strip annotations, metadata, noise, ads, boilerplate subtrees
|
|
@@ -62,10 +62,10 @@ const BOILERPLATE_LABELS = [
|
|
|
62
62
|
/**
|
|
63
63
|
* Parse role and text from a trimmed snapshot line.
|
|
64
64
|
* Handles quoted labels and trailing text after colon correctly,
|
|
65
|
-
* including lines wrapped in single quotes
|
|
65
|
+
* including lines wrapped in single quotes.
|
|
66
66
|
*/
|
|
67
67
|
function parseLine(trimmed: string): { role: string; text: string; hasText: boolean; trailingText: string } {
|
|
68
|
-
// Unwrap outer single quotes if present (
|
|
68
|
+
// Unwrap outer single quotes if present (snapshot wraps lines with special chars)
|
|
69
69
|
let line = trimmed;
|
|
70
70
|
if (line.startsWith("'") && line.endsWith("':")) {
|
|
71
71
|
line = line.slice(1, -2) + ':';
|
|
@@ -107,7 +107,7 @@ function parseLine(trimmed: string): { role: string; text: string; hasText: bool
|
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
109
|
* Strip ALL bracket annotations from a content line, preserving quoted strings.
|
|
110
|
-
* Handles both double-quoted and outer single-quoted lines
|
|
110
|
+
* Handles both double-quoted and outer single-quoted lines.
|
|
111
111
|
*/
|
|
112
112
|
function stripAnnotations(content: string): string {
|
|
113
113
|
// Unwrap outer single quotes first
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Page interface: type-safe abstraction over
|
|
2
|
+
* Page interface: type-safe abstraction over browser page.
|
|
3
3
|
*
|
|
4
4
|
* All pipeline steps and CLI adapters should use this interface
|
|
5
5
|
* instead of `any` for browser interactions.
|
|
@@ -75,4 +75,14 @@ export interface IPage {
|
|
|
75
75
|
closeWindow?(): Promise<void>;
|
|
76
76
|
/** Returns the current page URL, or null if unavailable. */
|
|
77
77
|
getCurrentUrl?(): Promise<string | null>;
|
|
78
|
+
/** Returns the active tab ID, or undefined if not yet resolved. */
|
|
79
|
+
getActiveTabId?(): number | undefined;
|
|
80
|
+
/** Send a raw CDP command via chrome.debugger passthrough. */
|
|
81
|
+
cdp?(method: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
82
|
+
/** Click at native coordinates via CDP Input.dispatchMouseEvent. */
|
|
83
|
+
nativeClick?(x: number, y: number): Promise<void>;
|
|
84
|
+
/** Type text via CDP Input.insertText. */
|
|
85
|
+
nativeType?(text: string): Promise<void>;
|
|
86
|
+
/** Press a key via CDP Input.dispatchKeyEvent. */
|
|
87
|
+
nativeKeyPress?(key: string, modifiers?: string[]): Promise<void>;
|
|
78
88
|
}
|