@jackwener/opencli 1.6.6 → 1.6.8
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/README.md +3 -1
- package/README.zh-CN.md +6 -2
- package/dist/clis/1688/assets.d.ts +42 -0
- package/dist/clis/1688/assets.js +204 -0
- package/dist/clis/1688/assets.test.d.ts +1 -0
- package/dist/clis/1688/assets.test.js +39 -0
- package/dist/clis/1688/download.d.ts +9 -0
- package/dist/clis/1688/download.js +76 -0
- package/dist/clis/1688/download.test.d.ts +1 -0
- package/dist/clis/1688/download.test.js +31 -0
- package/dist/clis/1688/shared.d.ts +10 -0
- package/dist/clis/1688/shared.js +43 -0
- package/dist/clis/linux-do/topic-content.d.ts +35 -0
- package/dist/clis/linux-do/topic-content.js +154 -0
- package/dist/clis/linux-do/topic-content.test.d.ts +1 -0
- package/dist/clis/linux-do/topic-content.test.js +59 -0
- package/dist/clis/linux-do/topic.yaml +1 -16
- package/dist/clis/xueqiu/groups.yaml +23 -0
- package/dist/clis/xueqiu/kline.yaml +65 -0
- package/dist/clis/xueqiu/watchlist.yaml +9 -9
- package/dist/src/analysis.d.ts +2 -0
- package/dist/src/analysis.js +6 -0
- package/dist/src/browser/cdp.js +96 -0
- package/dist/src/build-manifest.d.ts +3 -1
- package/dist/src/build-manifest.js +10 -7
- package/dist/src/build-manifest.test.js +8 -4
- package/dist/src/cli.d.ts +2 -1
- package/dist/src/cli.js +48 -46
- package/dist/src/commands/daemon.js +2 -10
- package/dist/src/diagnostic.d.ts +63 -0
- package/dist/src/diagnostic.js +247 -0
- package/dist/src/diagnostic.test.d.ts +1 -0
- package/dist/src/diagnostic.test.js +213 -0
- package/dist/src/discovery.js +7 -17
- package/dist/src/download/progress.js +7 -2
- package/dist/src/execution.js +25 -4
- package/dist/src/explore.d.ts +0 -2
- package/dist/src/explore.js +61 -38
- package/dist/src/extension-manifest-regression.test.js +0 -1
- package/dist/src/generate.d.ts +1 -1
- package/dist/src/generate.js +2 -3
- package/dist/src/package-paths.d.ts +8 -0
- package/dist/src/package-paths.js +41 -0
- package/dist/src/plugin-scaffold.js +1 -3
- package/dist/src/record.d.ts +1 -2
- package/dist/src/record.js +14 -52
- package/dist/src/synthesize.d.ts +0 -2
- package/dist/src/synthesize.js +8 -4
- package/package.json +1 -1
- package/scripts/postinstall.js +18 -71
- package/dist/cli-manifest.json +0 -17250
package/dist/src/cli.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Dynamic adapter commands are registered via commanderAdapter.ts.
|
|
6
6
|
*/
|
|
7
7
|
import { Command } from 'commander';
|
|
8
|
+
import { findPackageRoot } from './package-paths.js';
|
|
8
9
|
export declare function createProgram(BUILTIN_CLIS: string, USER_CLIS: string): Command;
|
|
9
10
|
export declare function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void;
|
|
10
11
|
export interface OperateVerifyInvocation {
|
|
@@ -13,7 +14,7 @@ export interface OperateVerifyInvocation {
|
|
|
13
14
|
cwd: string;
|
|
14
15
|
shell?: boolean;
|
|
15
16
|
}
|
|
16
|
-
export
|
|
17
|
+
export { findPackageRoot };
|
|
17
18
|
export declare function resolveOperateVerifyInvocation(opts?: {
|
|
18
19
|
projectRoot?: string;
|
|
19
20
|
platform?: NodeJS.Platform;
|
package/dist/src/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import * as path from 'node:path';
|
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { Command } from 'commander';
|
|
11
11
|
import chalk from 'chalk';
|
|
12
|
+
import { findPackageRoot, getBuiltEntryCandidates } from './package-paths.js';
|
|
12
13
|
import { fullName, getRegistry, strategyLabel } from './registry.js';
|
|
13
14
|
import { serializeCommand, formatArgSummary } from './serialization.js';
|
|
14
15
|
import { render as renderOutput } from './output.js';
|
|
@@ -268,13 +269,17 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
268
269
|
const NETWORK_INTERCEPTOR_JS = `(function(){if(window.__opencli_net)return;window.__opencli_net=[];var M=200,B=50000,F=window.fetch;window.fetch=async function(){var r=await F.apply(this,arguments);try{var ct=r.headers.get('content-type')||'';if(ct.includes('json')||ct.includes('text')){var c=r.clone(),t=await c.text();if(window.__opencli_net.length<M){var b=null;if(t.length<=B)try{b=JSON.parse(t)}catch(e){b=t}window.__opencli_net.push({url:r.url||(arguments[0]&&arguments[0].url)||String(arguments[0]),method:(arguments[1]&&arguments[1].method)||'GET',status:r.status,size:t.length,ct:ct,body:b})}}}catch(e){}return r};var X=XMLHttpRequest.prototype,O=X.open,S=X.send;X.open=function(m,u){this._om=m;this._ou=u;return O.apply(this,arguments)};X.send=function(){var x=this;x.addEventListener('load',function(){try{var ct=x.getResponseHeader('content-type')||'';if((ct.includes('json')||ct.includes('text'))&&window.__opencli_net.length<M){var t=x.responseText,b=null;if(t&&t.length<=B)try{b=JSON.parse(t)}catch(e){b=t}window.__opencli_net.push({url:x._ou,method:x._om||'GET',status:x.status,size:t?t.length:0,ct:ct,body:b})}}catch(e){}});return S.apply(this,arguments)}})()`;
|
|
269
270
|
operate.command('open').argument('<url>').description('Open URL in automation window')
|
|
270
271
|
.action(operateAction(async (page, url) => {
|
|
272
|
+
// Start session-level capture before navigation (catches initial requests)
|
|
273
|
+
await page.startNetworkCapture?.();
|
|
271
274
|
await page.goto(url);
|
|
272
275
|
await page.wait(2);
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
+
// Fallback: also inject JS interceptor for pages without session capture
|
|
277
|
+
if (!page.startNetworkCapture) {
|
|
278
|
+
try {
|
|
279
|
+
await page.evaluate(NETWORK_INTERCEPTOR_JS);
|
|
280
|
+
}
|
|
281
|
+
catch { /* non-fatal */ }
|
|
276
282
|
}
|
|
277
|
-
catch { /* non-fatal */ }
|
|
278
283
|
console.log(`Navigated to: ${await page.getCurrentUrl?.() ?? url}`);
|
|
279
284
|
}));
|
|
280
285
|
operate.command('back').description('Go back in browser history')
|
|
@@ -456,17 +461,46 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
456
461
|
.option('--all', 'Show all requests including static resources')
|
|
457
462
|
.description('Show captured network requests (auto-captured since last open)')
|
|
458
463
|
.action(operateAction(async (page, opts) => {
|
|
459
|
-
const requests = await page.evaluate(`(function(){
|
|
460
|
-
var reqs = window.__opencli_net || [];
|
|
461
|
-
return JSON.stringify(reqs);
|
|
462
|
-
})()`);
|
|
463
464
|
let items = [];
|
|
464
|
-
|
|
465
|
-
|
|
465
|
+
if (page.readNetworkCapture) {
|
|
466
|
+
const raw = await page.readNetworkCapture();
|
|
467
|
+
// Normalize daemon/CDP capture entries to __opencli_net shape.
|
|
468
|
+
// Daemon returns: responseStatus, responseContentType, responsePreview
|
|
469
|
+
// CDP returns the same shape after PR A fix.
|
|
470
|
+
items = raw.map(e => {
|
|
471
|
+
const preview = e.responsePreview ?? null;
|
|
472
|
+
let body = null;
|
|
473
|
+
if (preview) {
|
|
474
|
+
try {
|
|
475
|
+
body = JSON.parse(preview);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
body = preview;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
url: e.url || '',
|
|
483
|
+
method: e.method || 'GET',
|
|
484
|
+
status: e.responseStatus || 0,
|
|
485
|
+
size: preview ? preview.length : 0,
|
|
486
|
+
ct: e.responseContentType || '',
|
|
487
|
+
body,
|
|
488
|
+
};
|
|
489
|
+
});
|
|
466
490
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
491
|
+
else {
|
|
492
|
+
// Fallback to JS interceptor data
|
|
493
|
+
const requests = await page.evaluate(`(function(){
|
|
494
|
+
var reqs = window.__opencli_net || [];
|
|
495
|
+
return JSON.stringify(reqs);
|
|
496
|
+
})()`);
|
|
497
|
+
try {
|
|
498
|
+
items = JSON.parse(requests);
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
console.log('No network data captured. Run "operate open <url>" first.');
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
470
504
|
}
|
|
471
505
|
if (items.length === 0) {
|
|
472
506
|
console.log('No requests captured.');
|
|
@@ -943,39 +977,7 @@ cli({
|
|
|
943
977
|
export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
944
978
|
createProgram(BUILTIN_CLIS, USER_CLIS).parse();
|
|
945
979
|
}
|
|
946
|
-
export
|
|
947
|
-
let dir = path.dirname(startFile);
|
|
948
|
-
while (true) {
|
|
949
|
-
if (fileExists(path.join(dir, 'package.json')))
|
|
950
|
-
return dir;
|
|
951
|
-
const parent = path.dirname(dir);
|
|
952
|
-
if (parent === dir) {
|
|
953
|
-
throw new Error(`Could not find package.json above ${startFile}`);
|
|
954
|
-
}
|
|
955
|
-
dir = parent;
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
function getBuiltEntryCandidates(packageRoot, readFile) {
|
|
959
|
-
const candidates = [];
|
|
960
|
-
try {
|
|
961
|
-
const pkg = JSON.parse(readFile(path.join(packageRoot, 'package.json')));
|
|
962
|
-
if (typeof pkg.bin === 'string') {
|
|
963
|
-
candidates.push(path.join(packageRoot, pkg.bin));
|
|
964
|
-
}
|
|
965
|
-
else if (pkg.bin && typeof pkg.bin === 'object' && typeof pkg.bin.opencli === 'string') {
|
|
966
|
-
candidates.push(path.join(packageRoot, pkg.bin.opencli));
|
|
967
|
-
}
|
|
968
|
-
if (typeof pkg.main === 'string') {
|
|
969
|
-
candidates.push(path.join(packageRoot, pkg.main));
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
catch {
|
|
973
|
-
// Fall through to compatibility candidates below.
|
|
974
|
-
}
|
|
975
|
-
// Compatibility fallback for partially-built trees or older layouts.
|
|
976
|
-
candidates.push(path.join(packageRoot, 'dist', 'src', 'main.js'), path.join(packageRoot, 'dist', 'main.js'));
|
|
977
|
-
return [...new Set(candidates)];
|
|
978
|
-
}
|
|
980
|
+
export { findPackageRoot };
|
|
979
981
|
export function resolveOperateVerifyInvocation(opts = {}) {
|
|
980
982
|
const platform = opts.platform ?? process.platform;
|
|
981
983
|
const fileExists = opts.fileExists ?? fs.existsSync;
|
|
@@ -6,15 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import { fetchDaemonStatus, requestDaemonShutdown } from '../browser/daemon-client.js';
|
|
9
|
-
|
|
10
|
-
const h = Math.floor(seconds / 3600);
|
|
11
|
-
const m = Math.floor((seconds % 3600) / 60);
|
|
12
|
-
if (h > 0)
|
|
13
|
-
return `${h}h ${m}m`;
|
|
14
|
-
if (m > 0)
|
|
15
|
-
return `${m}m`;
|
|
16
|
-
return `${Math.floor(seconds)}s`;
|
|
17
|
-
}
|
|
9
|
+
import { formatDuration } from '../download/progress.js';
|
|
18
10
|
function formatTimeSince(timestampMs) {
|
|
19
11
|
const seconds = (Date.now() - timestampMs) / 1000;
|
|
20
12
|
if (seconds < 60)
|
|
@@ -32,7 +24,7 @@ export async function daemonStatus() {
|
|
|
32
24
|
return;
|
|
33
25
|
}
|
|
34
26
|
console.log(`Daemon: ${chalk.green('running')} (PID ${status.pid})`);
|
|
35
|
-
console.log(`Uptime: ${
|
|
27
|
+
console.log(`Uptime: ${formatDuration(Math.round(status.uptime * 1000))}`);
|
|
36
28
|
console.log(`Extension: ${status.extensionConnected ? chalk.green('connected') : chalk.yellow('disconnected')}`);
|
|
37
29
|
console.log(`Last CLI request: ${formatTimeSince(status.lastCliRequestTime)}`);
|
|
38
30
|
console.log(`Memory: ${status.memoryMB} MB`);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured diagnostic output for AI-driven adapter repair.
|
|
3
|
+
*
|
|
4
|
+
* When OPENCLI_DIAGNOSTIC=1, failed commands emit a JSON RepairContext to stderr
|
|
5
|
+
* containing the error, adapter source, and browser state (DOM snapshot, network
|
|
6
|
+
* requests, console errors). AI Agents consume this to diagnose and fix adapters.
|
|
7
|
+
*
|
|
8
|
+
* Safety boundaries:
|
|
9
|
+
* - Sensitive headers/cookies are redacted before emission
|
|
10
|
+
* - Individual fields are capped to prevent unbounded output
|
|
11
|
+
* - Network response bodies from authenticated requests are stripped
|
|
12
|
+
* - Total output is capped to MAX_DIAGNOSTIC_BYTES
|
|
13
|
+
*/
|
|
14
|
+
import type { IPage } from './types.js';
|
|
15
|
+
import type { InternalCliCommand } from './registry.js';
|
|
16
|
+
/** Maximum bytes for the entire diagnostic JSON output. */
|
|
17
|
+
export declare const MAX_DIAGNOSTIC_BYTES: number;
|
|
18
|
+
export interface RepairContext {
|
|
19
|
+
error: {
|
|
20
|
+
code: string;
|
|
21
|
+
message: string;
|
|
22
|
+
hint?: string;
|
|
23
|
+
stack?: string;
|
|
24
|
+
};
|
|
25
|
+
adapter: {
|
|
26
|
+
site: string;
|
|
27
|
+
command: string;
|
|
28
|
+
sourcePath?: string;
|
|
29
|
+
source?: string;
|
|
30
|
+
};
|
|
31
|
+
page?: {
|
|
32
|
+
url: string;
|
|
33
|
+
snapshot: string;
|
|
34
|
+
networkRequests: unknown[];
|
|
35
|
+
consoleErrors: unknown[];
|
|
36
|
+
};
|
|
37
|
+
timestamp: string;
|
|
38
|
+
}
|
|
39
|
+
/** Truncate a string to maxLen, appending a truncation marker. */
|
|
40
|
+
export declare function truncate(str: string, maxLen: number): string;
|
|
41
|
+
/** Redact sensitive query parameters from a URL. */
|
|
42
|
+
export declare function redactUrl(url: string): string;
|
|
43
|
+
/** Redact inline secrets from free-text strings (error messages, stack traces, console output, DOM). */
|
|
44
|
+
export declare function redactText(text: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the editable source file path for an adapter.
|
|
47
|
+
*
|
|
48
|
+
* Priority:
|
|
49
|
+
* 1. cmd.source (set for FS-scanned YAML/TS and manifest lazy-loaded TS)
|
|
50
|
+
* 2. cmd._modulePath (set for manifest lazy-loaded TS, points to dist/)
|
|
51
|
+
*
|
|
52
|
+
* For dist/ paths, attempt to map back to the original .ts source file.
|
|
53
|
+
* Skip manifest: prefixed pseudo-paths (YAML commands inlined in manifest).
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveAdapterSourcePath(cmd: InternalCliCommand): string | undefined;
|
|
56
|
+
/** Whether diagnostic mode is enabled. */
|
|
57
|
+
export declare function isDiagnosticEnabled(): boolean;
|
|
58
|
+
/** Build a RepairContext from an error, command metadata, and optional page state. */
|
|
59
|
+
export declare function buildRepairContext(err: unknown, cmd: InternalCliCommand, pageState?: RepairContext['page']): RepairContext;
|
|
60
|
+
/** Collect full diagnostic context including page state (with timeout). */
|
|
61
|
+
export declare function collectDiagnostic(err: unknown, cmd: InternalCliCommand, page: IPage | null): Promise<RepairContext>;
|
|
62
|
+
/** Emit diagnostic JSON to stderr, enforcing total size cap. */
|
|
63
|
+
export declare function emitDiagnostic(ctx: RepairContext): void;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured diagnostic output for AI-driven adapter repair.
|
|
3
|
+
*
|
|
4
|
+
* When OPENCLI_DIAGNOSTIC=1, failed commands emit a JSON RepairContext to stderr
|
|
5
|
+
* containing the error, adapter source, and browser state (DOM snapshot, network
|
|
6
|
+
* requests, console errors). AI Agents consume this to diagnose and fix adapters.
|
|
7
|
+
*
|
|
8
|
+
* Safety boundaries:
|
|
9
|
+
* - Sensitive headers/cookies are redacted before emission
|
|
10
|
+
* - Individual fields are capped to prevent unbounded output
|
|
11
|
+
* - Network response bodies from authenticated requests are stripped
|
|
12
|
+
* - Total output is capped to MAX_DIAGNOSTIC_BYTES
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import { CliError, getErrorMessage } from './errors.js';
|
|
17
|
+
import { fullName } from './registry.js';
|
|
18
|
+
// ── Size budgets ─────────────────────────────────────────────────────────────
|
|
19
|
+
/** Maximum bytes for the entire diagnostic JSON output. */
|
|
20
|
+
export const MAX_DIAGNOSTIC_BYTES = 256 * 1024; // 256 KB
|
|
21
|
+
/** Maximum characters for DOM snapshot. */
|
|
22
|
+
const MAX_SNAPSHOT_CHARS = 100_000;
|
|
23
|
+
/** Maximum characters for adapter source. */
|
|
24
|
+
const MAX_SOURCE_CHARS = 50_000;
|
|
25
|
+
/** Maximum number of network requests to include. */
|
|
26
|
+
const MAX_NETWORK_REQUESTS = 50;
|
|
27
|
+
/** Maximum characters for a single network request body. */
|
|
28
|
+
const MAX_REQUEST_BODY_CHARS = 4_000;
|
|
29
|
+
/** Maximum characters for error stack trace. */
|
|
30
|
+
const MAX_STACK_CHARS = 5_000;
|
|
31
|
+
// ── Sensitive data patterns ──────────────────────────────────────────────────
|
|
32
|
+
const SENSITIVE_HEADERS = new Set([
|
|
33
|
+
'authorization',
|
|
34
|
+
'cookie',
|
|
35
|
+
'set-cookie',
|
|
36
|
+
'x-csrf-token',
|
|
37
|
+
'x-xsrf-token',
|
|
38
|
+
'proxy-authorization',
|
|
39
|
+
'x-api-key',
|
|
40
|
+
'x-auth-token',
|
|
41
|
+
]);
|
|
42
|
+
const SENSITIVE_URL_PARAMS = /([?&])(token|key|secret|password|auth|access_token|api_key|session_id|csrf)=[^&]*/gi;
|
|
43
|
+
/** Patterns that match inline secrets in free-text strings (error messages, stack traces, console output, DOM). */
|
|
44
|
+
const SENSITIVE_TEXT_PATTERNS = [
|
|
45
|
+
// Bearer tokens
|
|
46
|
+
{ pattern: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, replacement: 'Bearer [REDACTED]' },
|
|
47
|
+
// Generic "token=...", "key=...", etc. in non-URL text
|
|
48
|
+
{ pattern: /(token|secret|password|api_key|apikey|access_token|session_id)[=:]\s*['"]?[A-Za-z0-9\-._~+/]{8,}['"]?/gi, replacement: '$1=[REDACTED]' },
|
|
49
|
+
// Cookie header values (key=value pairs)
|
|
50
|
+
{ pattern: /(cookie[=:]\s*)[^\n;]{10,}/gi, replacement: '$1[REDACTED]' },
|
|
51
|
+
// JWT-like tokens (three base64 segments separated by dots)
|
|
52
|
+
{ pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, replacement: '[REDACTED_JWT]' },
|
|
53
|
+
];
|
|
54
|
+
// ── Redaction helpers ────────────────────────────────────────────────────────
|
|
55
|
+
/** Truncate a string to maxLen, appending a truncation marker. */
|
|
56
|
+
export function truncate(str, maxLen) {
|
|
57
|
+
if (str.length <= maxLen)
|
|
58
|
+
return str;
|
|
59
|
+
return str.slice(0, maxLen) + `\n...[truncated, ${str.length - maxLen} chars omitted]`;
|
|
60
|
+
}
|
|
61
|
+
/** Redact sensitive query parameters from a URL. */
|
|
62
|
+
export function redactUrl(url) {
|
|
63
|
+
return url.replace(SENSITIVE_URL_PARAMS, '$1$2=[REDACTED]');
|
|
64
|
+
}
|
|
65
|
+
/** Redact inline secrets from free-text strings (error messages, stack traces, console output, DOM). */
|
|
66
|
+
export function redactText(text) {
|
|
67
|
+
let result = text;
|
|
68
|
+
for (const { pattern, replacement } of SENSITIVE_TEXT_PATTERNS) {
|
|
69
|
+
// Reset lastIndex for global regexps
|
|
70
|
+
pattern.lastIndex = 0;
|
|
71
|
+
result = result.replace(pattern, replacement);
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
/** Redact sensitive headers from a headers object. */
|
|
76
|
+
function redactHeaders(headers) {
|
|
77
|
+
if (!headers || typeof headers !== 'object')
|
|
78
|
+
return headers;
|
|
79
|
+
const result = {};
|
|
80
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
81
|
+
result[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? '[REDACTED]' : value;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
/** Redact sensitive data from a single network request entry. */
|
|
86
|
+
function redactNetworkRequest(req) {
|
|
87
|
+
if (!req || typeof req !== 'object')
|
|
88
|
+
return req;
|
|
89
|
+
const r = req;
|
|
90
|
+
const redacted = { ...r };
|
|
91
|
+
// Redact URL
|
|
92
|
+
if (typeof redacted.url === 'string') {
|
|
93
|
+
redacted.url = redactUrl(redacted.url);
|
|
94
|
+
}
|
|
95
|
+
// Redact headers
|
|
96
|
+
if (redacted.headers && typeof redacted.headers === 'object') {
|
|
97
|
+
redacted.headers = redactHeaders(redacted.headers);
|
|
98
|
+
}
|
|
99
|
+
if (redacted.requestHeaders && typeof redacted.requestHeaders === 'object') {
|
|
100
|
+
redacted.requestHeaders = redactHeaders(redacted.requestHeaders);
|
|
101
|
+
}
|
|
102
|
+
if (redacted.responseHeaders && typeof redacted.responseHeaders === 'object') {
|
|
103
|
+
redacted.responseHeaders = redactHeaders(redacted.responseHeaders);
|
|
104
|
+
}
|
|
105
|
+
// Truncate response body
|
|
106
|
+
if (typeof redacted.body === 'string') {
|
|
107
|
+
redacted.body = truncate(redacted.body, MAX_REQUEST_BODY_CHARS);
|
|
108
|
+
}
|
|
109
|
+
return redacted;
|
|
110
|
+
}
|
|
111
|
+
// ── Timeout helper ───────────────────────────────────────────────────────────
|
|
112
|
+
/** Timeout for page state collection (prevents hang when CDP connection is stuck). */
|
|
113
|
+
const PAGE_STATE_TIMEOUT_MS = 5_000;
|
|
114
|
+
function withTimeout(promise, ms, fallback) {
|
|
115
|
+
return Promise.race([
|
|
116
|
+
promise,
|
|
117
|
+
new Promise(resolve => setTimeout(() => resolve(fallback), ms)),
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
// ── Source path resolution ───────────────────────────────────────────────────
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the editable source file path for an adapter.
|
|
123
|
+
*
|
|
124
|
+
* Priority:
|
|
125
|
+
* 1. cmd.source (set for FS-scanned YAML/TS and manifest lazy-loaded TS)
|
|
126
|
+
* 2. cmd._modulePath (set for manifest lazy-loaded TS, points to dist/)
|
|
127
|
+
*
|
|
128
|
+
* For dist/ paths, attempt to map back to the original .ts source file.
|
|
129
|
+
* Skip manifest: prefixed pseudo-paths (YAML commands inlined in manifest).
|
|
130
|
+
*/
|
|
131
|
+
export function resolveAdapterSourcePath(cmd) {
|
|
132
|
+
const candidates = [];
|
|
133
|
+
// cmd.source may be a real file path or 'manifest:site/name'
|
|
134
|
+
if (cmd.source && !cmd.source.startsWith('manifest:')) {
|
|
135
|
+
candidates.push(cmd.source);
|
|
136
|
+
}
|
|
137
|
+
if (cmd._modulePath) {
|
|
138
|
+
candidates.push(cmd._modulePath);
|
|
139
|
+
}
|
|
140
|
+
for (const candidate of candidates) {
|
|
141
|
+
// Try to map dist/ compiled JS back to source .ts
|
|
142
|
+
const sourceTs = mapDistToSource(candidate);
|
|
143
|
+
if (sourceTs && fs.existsSync(sourceTs))
|
|
144
|
+
return sourceTs;
|
|
145
|
+
// Try the candidate directly (YAML files, user clis, etc.)
|
|
146
|
+
if (fs.existsSync(candidate))
|
|
147
|
+
return candidate;
|
|
148
|
+
}
|
|
149
|
+
return candidates[0]; // Return best guess even if file doesn't exist
|
|
150
|
+
}
|
|
151
|
+
/** Map a dist/clis/xxx.js path back to clis/xxx.ts source. */
|
|
152
|
+
function mapDistToSource(filePath) {
|
|
153
|
+
// dist/clis/site/command.js → clis/site/command.ts
|
|
154
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
155
|
+
const distClisMatch = normalized.match(/^(.*)\/dist\/clis\/(.+)\.js$/);
|
|
156
|
+
if (distClisMatch) {
|
|
157
|
+
return path.join(distClisMatch[1], 'clis', distClisMatch[2] + '.ts');
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
// ── Diagnostic collection ────────────────────────────────────────────────────
|
|
162
|
+
/** Whether diagnostic mode is enabled. */
|
|
163
|
+
export function isDiagnosticEnabled() {
|
|
164
|
+
return process.env.OPENCLI_DIAGNOSTIC === '1';
|
|
165
|
+
}
|
|
166
|
+
/** Safely collect page diagnostic state with redaction, size caps, and timeout. */
|
|
167
|
+
async function collectPageState(page) {
|
|
168
|
+
const collect = async () => {
|
|
169
|
+
try {
|
|
170
|
+
const [url, snapshot, networkRequests, consoleErrors] = await Promise.all([
|
|
171
|
+
page.getCurrentUrl?.().catch(() => null) ?? Promise.resolve(null),
|
|
172
|
+
page.snapshot().catch(() => '(snapshot unavailable)'),
|
|
173
|
+
page.networkRequests().catch(() => []),
|
|
174
|
+
page.consoleMessages('error').catch(() => []),
|
|
175
|
+
]);
|
|
176
|
+
const rawUrl = url ?? 'unknown';
|
|
177
|
+
return {
|
|
178
|
+
url: redactUrl(rawUrl),
|
|
179
|
+
snapshot: redactText(truncate(snapshot, MAX_SNAPSHOT_CHARS)),
|
|
180
|
+
networkRequests: networkRequests
|
|
181
|
+
.slice(0, MAX_NETWORK_REQUESTS)
|
|
182
|
+
.map(redactNetworkRequest),
|
|
183
|
+
consoleErrors: consoleErrors
|
|
184
|
+
.slice(0, 50)
|
|
185
|
+
.map(e => typeof e === 'string' ? redactText(e) : e),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
return withTimeout(collect(), PAGE_STATE_TIMEOUT_MS, undefined);
|
|
193
|
+
}
|
|
194
|
+
/** Read adapter source file content with size cap. */
|
|
195
|
+
function readAdapterSource(sourcePath) {
|
|
196
|
+
if (!sourcePath)
|
|
197
|
+
return undefined;
|
|
198
|
+
try {
|
|
199
|
+
const content = fs.readFileSync(sourcePath, 'utf-8');
|
|
200
|
+
return truncate(content, MAX_SOURCE_CHARS);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** Build a RepairContext from an error, command metadata, and optional page state. */
|
|
207
|
+
export function buildRepairContext(err, cmd, pageState) {
|
|
208
|
+
const isCliError = err instanceof CliError;
|
|
209
|
+
const sourcePath = resolveAdapterSourcePath(cmd);
|
|
210
|
+
return {
|
|
211
|
+
error: {
|
|
212
|
+
code: isCliError ? err.code : 'UNKNOWN',
|
|
213
|
+
message: redactText(getErrorMessage(err)),
|
|
214
|
+
hint: isCliError && err.hint ? redactText(err.hint) : undefined,
|
|
215
|
+
stack: err instanceof Error ? redactText(truncate(err.stack ?? '', MAX_STACK_CHARS)) : undefined,
|
|
216
|
+
},
|
|
217
|
+
adapter: {
|
|
218
|
+
site: cmd.site,
|
|
219
|
+
command: fullName(cmd),
|
|
220
|
+
sourcePath,
|
|
221
|
+
source: readAdapterSource(sourcePath),
|
|
222
|
+
},
|
|
223
|
+
page: pageState,
|
|
224
|
+
timestamp: new Date().toISOString(),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/** Collect full diagnostic context including page state (with timeout). */
|
|
228
|
+
export async function collectDiagnostic(err, cmd, page) {
|
|
229
|
+
const pageState = page ? await collectPageState(page) : undefined;
|
|
230
|
+
return buildRepairContext(err, cmd, pageState);
|
|
231
|
+
}
|
|
232
|
+
/** Emit diagnostic JSON to stderr, enforcing total size cap. */
|
|
233
|
+
export function emitDiagnostic(ctx) {
|
|
234
|
+
const marker = '___OPENCLI_DIAGNOSTIC___';
|
|
235
|
+
let json = JSON.stringify(ctx);
|
|
236
|
+
// Enforce total output budget — drop page state (largest section) first if over budget
|
|
237
|
+
if (json.length > MAX_DIAGNOSTIC_BYTES && ctx.page) {
|
|
238
|
+
const trimmed = { ...ctx, page: { ...ctx.page, snapshot: '[omitted: over size budget]', networkRequests: [] } };
|
|
239
|
+
json = JSON.stringify(trimmed);
|
|
240
|
+
}
|
|
241
|
+
// If still over budget, drop page entirely
|
|
242
|
+
if (json.length > MAX_DIAGNOSTIC_BYTES) {
|
|
243
|
+
const minimal = { ...ctx, page: undefined };
|
|
244
|
+
json = JSON.stringify(minimal);
|
|
245
|
+
}
|
|
246
|
+
process.stderr.write(`\n${marker}\n${json}\n${marker}\n`);
|
|
247
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|