@jackwener/opencli 1.6.8 → 1.6.9
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 +2 -0
- package/README.zh-CN.md +2 -1
- package/dist/clis/jianyu/search.d.ts +14 -0
- package/dist/clis/jianyu/search.js +135 -0
- package/dist/clis/jianyu/search.test.d.ts +1 -0
- package/dist/clis/jianyu/search.test.js +23 -0
- package/dist/clis/quark/ls.d.ts +1 -0
- package/dist/clis/quark/ls.js +63 -0
- package/dist/clis/quark/mkdir.d.ts +1 -0
- package/dist/clis/quark/mkdir.js +36 -0
- package/dist/clis/quark/mv.d.ts +1 -0
- package/dist/clis/quark/mv.js +53 -0
- package/dist/clis/quark/rename.d.ts +1 -0
- package/dist/clis/quark/rename.js +26 -0
- package/dist/clis/quark/rm.d.ts +1 -0
- package/dist/clis/quark/rm.js +24 -0
- package/dist/clis/quark/save.d.ts +1 -0
- package/dist/clis/quark/save.js +80 -0
- package/dist/clis/quark/share-tree.d.ts +1 -0
- package/dist/clis/quark/share-tree.js +45 -0
- package/dist/clis/quark/utils.d.ts +50 -0
- package/dist/clis/quark/utils.js +146 -0
- package/dist/clis/quark/utils.test.d.ts +1 -0
- package/dist/clis/quark/utils.test.js +58 -0
- package/dist/clis/twitter/reply.js +3 -8
- package/dist/clis/twitter/reply.test.js +5 -5
- package/dist/clis/xiaohongshu/note.js +8 -3
- package/dist/clis/xiaohongshu/note.test.js +11 -0
- package/dist/clis/zhihu/answer.d.ts +1 -0
- package/dist/clis/zhihu/answer.js +194 -0
- package/dist/clis/zhihu/answer.test.d.ts +1 -0
- package/dist/clis/zhihu/answer.test.js +81 -0
- package/dist/clis/zhihu/comment.d.ts +1 -0
- package/dist/clis/zhihu/comment.js +335 -0
- package/dist/clis/zhihu/comment.test.d.ts +1 -0
- package/dist/clis/zhihu/comment.test.js +54 -0
- package/dist/clis/zhihu/favorite.d.ts +1 -0
- package/dist/clis/zhihu/favorite.js +224 -0
- package/dist/clis/zhihu/favorite.test.d.ts +1 -0
- package/dist/clis/zhihu/favorite.test.js +196 -0
- package/dist/clis/zhihu/follow.d.ts +1 -0
- package/dist/clis/zhihu/follow.js +80 -0
- package/dist/clis/zhihu/follow.test.d.ts +1 -0
- package/dist/clis/zhihu/follow.test.js +45 -0
- package/dist/clis/zhihu/like.d.ts +1 -0
- package/dist/clis/zhihu/like.js +91 -0
- package/dist/clis/zhihu/like.test.d.ts +1 -0
- package/dist/clis/zhihu/like.test.js +64 -0
- package/dist/clis/zhihu/target.d.ts +24 -0
- package/dist/clis/zhihu/target.js +91 -0
- package/dist/clis/zhihu/target.test.d.ts +1 -0
- package/dist/clis/zhihu/target.test.js +77 -0
- package/dist/clis/zhihu/write-shared.d.ts +32 -0
- package/dist/clis/zhihu/write-shared.js +221 -0
- package/dist/clis/zhihu/write-shared.test.d.ts +1 -0
- package/dist/clis/zhihu/write-shared.test.js +175 -0
- package/dist/src/browser/bridge.d.ts +2 -0
- package/dist/src/browser/bridge.js +30 -24
- package/dist/src/browser/daemon-client.d.ts +17 -8
- package/dist/src/browser/daemon-client.js +12 -13
- package/dist/src/browser/daemon-client.test.js +32 -25
- package/dist/src/browser/index.d.ts +2 -1
- package/dist/src/browser/index.js +1 -1
- package/dist/src/browser.test.js +2 -3
- package/dist/src/cli.js +3 -3
- package/dist/src/clis/binance/commands.test.d.ts +1 -0
- package/dist/src/clis/binance/commands.test.js +54 -0
- package/dist/src/commanderAdapter.js +19 -6
- package/dist/src/diagnostic.d.ts +1 -0
- package/dist/src/diagnostic.js +64 -2
- package/dist/src/diagnostic.test.js +91 -1
- package/dist/src/doctor.d.ts +2 -0
- package/dist/src/doctor.js +59 -31
- package/dist/src/doctor.test.js +89 -16
- package/dist/src/execution.js +1 -13
- package/dist/src/explore.js +1 -1
- package/dist/src/generate.d.ts +2 -5
- package/dist/src/generate.js +2 -5
- package/dist/src/plugin.d.ts +2 -1
- package/dist/src/plugin.js +25 -8
- package/dist/src/plugin.test.js +16 -1
- package/package.json +3 -3
- package/dist/src/browser/discover.d.ts +0 -15
- package/dist/src/browser/discover.js +0 -19
|
@@ -39,6 +39,18 @@ export async function fetchDaemonStatus(opts) {
|
|
|
39
39
|
return null;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Unified daemon health check — single entry point for all status queries.
|
|
44
|
+
* Replaces isDaemonRunning(), isExtensionConnected(), and checkDaemonStatus().
|
|
45
|
+
*/
|
|
46
|
+
export async function getDaemonHealth(opts) {
|
|
47
|
+
const status = await fetchDaemonStatus(opts);
|
|
48
|
+
if (!status)
|
|
49
|
+
return { state: 'stopped', status: null };
|
|
50
|
+
if (!status.extensionConnected)
|
|
51
|
+
return { state: 'no-extension', status };
|
|
52
|
+
return { state: 'ready', status };
|
|
53
|
+
}
|
|
42
54
|
export async function requestDaemonShutdown(opts) {
|
|
43
55
|
try {
|
|
44
56
|
const res = await requestDaemon('/shutdown', { method: 'POST', timeout: opts?.timeout ?? 5000 });
|
|
@@ -48,19 +60,6 @@ export async function requestDaemonShutdown(opts) {
|
|
|
48
60
|
return false;
|
|
49
61
|
}
|
|
50
62
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Check if daemon is running.
|
|
53
|
-
*/
|
|
54
|
-
export async function isDaemonRunning() {
|
|
55
|
-
return (await fetchDaemonStatus()) !== null;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Check if daemon is running AND the extension is connected.
|
|
59
|
-
*/
|
|
60
|
-
export async function isExtensionConnected() {
|
|
61
|
-
const status = await fetchDaemonStatus();
|
|
62
|
-
return !!status?.extensionConnected;
|
|
63
|
-
}
|
|
64
63
|
/**
|
|
65
64
|
* Send a command to the daemon and wait for a result.
|
|
66
65
|
* Retries up to 4 times: network errors retry at 500ms,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { fetchDaemonStatus,
|
|
2
|
+
import { fetchDaemonStatus, getDaemonHealth, requestDaemonShutdown, } from './daemon-client.js';
|
|
3
3
|
describe('daemon-client', () => {
|
|
4
4
|
beforeEach(() => {
|
|
5
5
|
vi.stubGlobal('fetch', vi.fn());
|
|
@@ -42,36 +42,43 @@ describe('daemon-client', () => {
|
|
|
42
42
|
headers: expect.objectContaining({ 'X-OpenCLI': '1' }),
|
|
43
43
|
}));
|
|
44
44
|
});
|
|
45
|
-
it('
|
|
45
|
+
it('getDaemonHealth returns stopped when daemon is not reachable', async () => {
|
|
46
|
+
vi.mocked(fetch).mockRejectedValue(new Error('ECONNREFUSED'));
|
|
47
|
+
await expect(getDaemonHealth()).resolves.toEqual({ state: 'stopped', status: null });
|
|
48
|
+
});
|
|
49
|
+
it('getDaemonHealth returns no-extension when daemon is running but extension disconnected', async () => {
|
|
50
|
+
const status = {
|
|
51
|
+
ok: true,
|
|
52
|
+
pid: 123,
|
|
53
|
+
uptime: 10,
|
|
54
|
+
extensionConnected: false,
|
|
55
|
+
pending: 0,
|
|
56
|
+
lastCliRequestTime: Date.now(),
|
|
57
|
+
memoryMB: 16,
|
|
58
|
+
port: 19825,
|
|
59
|
+
};
|
|
46
60
|
vi.mocked(fetch).mockResolvedValue({
|
|
47
61
|
ok: true,
|
|
48
|
-
json: () => Promise.resolve(
|
|
49
|
-
ok: true,
|
|
50
|
-
pid: 123,
|
|
51
|
-
uptime: 10,
|
|
52
|
-
extensionConnected: false,
|
|
53
|
-
pending: 0,
|
|
54
|
-
lastCliRequestTime: Date.now(),
|
|
55
|
-
memoryMB: 16,
|
|
56
|
-
port: 19825,
|
|
57
|
-
}),
|
|
62
|
+
json: () => Promise.resolve(status),
|
|
58
63
|
});
|
|
59
|
-
await expect(
|
|
64
|
+
await expect(getDaemonHealth()).resolves.toEqual({ state: 'no-extension', status });
|
|
60
65
|
});
|
|
61
|
-
it('
|
|
66
|
+
it('getDaemonHealth returns ready when daemon and extension are both connected', async () => {
|
|
67
|
+
const status = {
|
|
68
|
+
ok: true,
|
|
69
|
+
pid: 123,
|
|
70
|
+
uptime: 10,
|
|
71
|
+
extensionConnected: true,
|
|
72
|
+
extensionVersion: '1.2.3',
|
|
73
|
+
pending: 0,
|
|
74
|
+
lastCliRequestTime: Date.now(),
|
|
75
|
+
memoryMB: 32,
|
|
76
|
+
port: 19825,
|
|
77
|
+
};
|
|
62
78
|
vi.mocked(fetch).mockResolvedValue({
|
|
63
79
|
ok: true,
|
|
64
|
-
json: () => Promise.resolve(
|
|
65
|
-
ok: true,
|
|
66
|
-
pid: 123,
|
|
67
|
-
uptime: 10,
|
|
68
|
-
extensionConnected: false,
|
|
69
|
-
pending: 0,
|
|
70
|
-
lastCliRequestTime: Date.now(),
|
|
71
|
-
memoryMB: 16,
|
|
72
|
-
port: 19825,
|
|
73
|
-
}),
|
|
80
|
+
json: () => Promise.resolve(status),
|
|
74
81
|
});
|
|
75
|
-
await expect(
|
|
82
|
+
await expect(getDaemonHealth()).resolves.toEqual({ state: 'ready', status });
|
|
76
83
|
});
|
|
77
84
|
});
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
export { Page } from './page.js';
|
|
8
8
|
export { BrowserBridge } from './bridge.js';
|
|
9
9
|
export { CDPBridge } from './cdp.js';
|
|
10
|
-
export {
|
|
10
|
+
export { getDaemonHealth } from './daemon-client.js';
|
|
11
|
+
export type { DaemonHealth } from './daemon-client.js';
|
|
11
12
|
export { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
|
|
12
13
|
export { generateStealthJs } from './stealth.js';
|
|
13
14
|
export type { DomSnapshotOptions } from './dom-snapshot.js';
|
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
export { Page } from './page.js';
|
|
8
8
|
export { BrowserBridge } from './bridge.js';
|
|
9
9
|
export { CDPBridge } from './cdp.js';
|
|
10
|
-
export {
|
|
10
|
+
export { getDaemonHealth } from './daemon-client.js';
|
|
11
11
|
export { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
|
|
12
12
|
export { generateStealthJs } from './stealth.js';
|
package/dist/src/browser.test.js
CHANGED
|
@@ -105,10 +105,9 @@ describe('BrowserBridge state', () => {
|
|
|
105
105
|
await expect(bridge.connect()).rejects.toThrow('Session is closing');
|
|
106
106
|
});
|
|
107
107
|
it('fails fast when daemon is running but extension is disconnected', async () => {
|
|
108
|
-
vi.spyOn(daemonClient, '
|
|
109
|
-
vi.spyOn(daemonClient, 'fetchDaemonStatus').mockResolvedValue({ extensionConnected: false });
|
|
108
|
+
vi.spyOn(daemonClient, 'getDaemonHealth').mockResolvedValue({ state: 'no-extension', status: { extensionConnected: false } });
|
|
110
109
|
const bridge = new BrowserBridge();
|
|
111
|
-
await expect(bridge.connect({ timeout: 0.1 })).rejects.toThrow('Browser
|
|
110
|
+
await expect(bridge.connect({ timeout: 0.1 })).rejects.toThrow('Browser Bridge extension not connected');
|
|
112
111
|
});
|
|
113
112
|
});
|
|
114
113
|
describe('stealth anti-detection', () => {
|
package/dist/src/cli.js
CHANGED
|
@@ -270,11 +270,11 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
270
270
|
operate.command('open').argument('<url>').description('Open URL in automation window')
|
|
271
271
|
.action(operateAction(async (page, url) => {
|
|
272
272
|
// Start session-level capture before navigation (catches initial requests)
|
|
273
|
-
await page.startNetworkCapture?.();
|
|
273
|
+
const hasSessionCapture = await page.startNetworkCapture?.().then(() => true).catch(() => false);
|
|
274
274
|
await page.goto(url);
|
|
275
275
|
await page.wait(2);
|
|
276
|
-
// Fallback:
|
|
277
|
-
if (!
|
|
276
|
+
// Fallback: inject JS interceptor when session capture is unavailable
|
|
277
|
+
if (!hasSessionCapture) {
|
|
278
278
|
try {
|
|
279
279
|
await page.evaluate(NETWORK_INTERCEPTOR_JS);
|
|
280
280
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import yaml from 'js-yaml';
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { executePipeline } from '../../pipeline/index.js';
|
|
5
|
+
function loadPipeline(name) {
|
|
6
|
+
const file = new URL(`./${name}.yaml`, import.meta.url);
|
|
7
|
+
const def = yaml.load(fs.readFileSync(file, 'utf-8'));
|
|
8
|
+
return def.pipeline;
|
|
9
|
+
}
|
|
10
|
+
function mockJsonOnce(payload) {
|
|
11
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
12
|
+
ok: true,
|
|
13
|
+
status: 200,
|
|
14
|
+
statusText: 'OK',
|
|
15
|
+
json: vi.fn().mockResolvedValue(payload),
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.unstubAllGlobals();
|
|
20
|
+
vi.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
describe('binance YAML adapters', () => {
|
|
23
|
+
it('sorts top pairs by numeric quote volume', async () => {
|
|
24
|
+
mockJsonOnce([
|
|
25
|
+
{ symbol: 'SMALL', lastPrice: '1', priceChangePercent: '1.2', highPrice: '1', lowPrice: '1', quoteVolume: '9.9' },
|
|
26
|
+
{ symbol: 'LARGE', lastPrice: '2', priceChangePercent: '2.3', highPrice: '2', lowPrice: '2', quoteVolume: '100.0' },
|
|
27
|
+
{ symbol: 'MID', lastPrice: '3', priceChangePercent: '3.4', highPrice: '3', lowPrice: '3', quoteVolume: '11.0' },
|
|
28
|
+
]);
|
|
29
|
+
const result = await executePipeline(null, loadPipeline('top'), { args: { limit: 3 } });
|
|
30
|
+
expect(result.map((item) => item.symbol)).toEqual(['LARGE', 'MID', 'SMALL']);
|
|
31
|
+
expect(result.map((item) => item.rank)).toEqual([1, 2, 3]);
|
|
32
|
+
});
|
|
33
|
+
it('sorts gainers by numeric percent change', async () => {
|
|
34
|
+
mockJsonOnce([
|
|
35
|
+
{ symbol: 'TEN', lastPrice: '1', priceChangePercent: '10.0', quoteVolume: '100' },
|
|
36
|
+
{ symbol: 'NINE', lastPrice: '1', priceChangePercent: '9.5', quoteVolume: '100' },
|
|
37
|
+
{ symbol: 'HUNDRED', lastPrice: '1', priceChangePercent: '100.0', quoteVolume: '100' },
|
|
38
|
+
]);
|
|
39
|
+
const result = await executePipeline(null, loadPipeline('gainers'), { args: { limit: 3 } });
|
|
40
|
+
expect(result.map((item) => item.symbol)).toEqual(['HUNDRED', 'TEN', 'NINE']);
|
|
41
|
+
});
|
|
42
|
+
it('keeps only TRADING pairs', async () => {
|
|
43
|
+
mockJsonOnce({
|
|
44
|
+
symbols: [
|
|
45
|
+
{ symbol: 'BTCUSDT', baseAsset: 'BTC', quoteAsset: 'USDT', status: 'TRADING' },
|
|
46
|
+
{ symbol: 'OLDPAIR', baseAsset: 'OLD', quoteAsset: 'USDT', status: 'BREAK' },
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
const result = await executePipeline(null, loadPipeline('pairs'), { args: { limit: 10 } });
|
|
50
|
+
expect(result).toEqual([
|
|
51
|
+
{ symbol: 'BTCUSDT', base: 'BTC', quote: 'USDT', status: 'TRADING' },
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -15,7 +15,8 @@ import { formatRegistryHelpText } from './serialization.js';
|
|
|
15
15
|
import { render as renderOutput } from './output.js';
|
|
16
16
|
import { executeCommand } from './execution.js';
|
|
17
17
|
import { CliError, EXIT_CODES, ERROR_ICONS, getErrorMessage, BrowserConnectError, AuthRequiredError, TimeoutError, SelectorError, EmptyResultError, ArgumentError, AdapterLoadError, CommandExecutionError, } from './errors.js';
|
|
18
|
-
import {
|
|
18
|
+
import { getDaemonHealth } from './browser/daemon-client.js';
|
|
19
|
+
import { isDiagnosticEnabled } from './diagnostic.js';
|
|
19
20
|
export function normalizeArgValue(argType, value, name) {
|
|
20
21
|
if (argType !== 'bool' && argType !== 'boolean')
|
|
21
22
|
return value;
|
|
@@ -169,18 +170,27 @@ function renderBridgeStatus(running, extensionConnected) {
|
|
|
169
170
|
console.error(chalk.dim(' Try reloading the extension, or run: opencli doctor'));
|
|
170
171
|
}
|
|
171
172
|
}
|
|
173
|
+
/** Emit AutoFix hint for repairable adapter errors (skipped if already in diagnostic mode). */
|
|
174
|
+
function emitAutoFixHint(cmdName) {
|
|
175
|
+
if (isDiagnosticEnabled())
|
|
176
|
+
return; // Already collecting diagnostics, don't repeat
|
|
177
|
+
console.error();
|
|
178
|
+
console.error(chalk.cyan('💡 AutoFix: re-run with OPENCLI_DIAGNOSTIC=1 for repair context.'));
|
|
179
|
+
console.error(chalk.dim(` OPENCLI_DIAGNOSTIC=1 ${cmdName}`));
|
|
180
|
+
}
|
|
172
181
|
async function renderError(err, cmdName, verbose) {
|
|
173
182
|
// ── BrowserConnectError: real-time diagnosis, kind as fallback ────────
|
|
174
183
|
if (err instanceof BrowserConnectError) {
|
|
175
|
-
console.error(chalk.red(
|
|
184
|
+
console.error(chalk.red(`🔌 ${err.message}`));
|
|
185
|
+
if (err.hint)
|
|
186
|
+
console.error(chalk.yellow(`→ ${err.hint}`));
|
|
176
187
|
console.error();
|
|
177
188
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
renderBridgeStatus(status.running, status.extensionConnected);
|
|
189
|
+
const health = await getDaemonHealth({ timeout: 300 });
|
|
190
|
+
renderBridgeStatus(health.state !== 'stopped', health.state === 'ready');
|
|
181
191
|
}
|
|
182
192
|
catch (_statusErr) {
|
|
183
|
-
//
|
|
193
|
+
// getDaemonHealth itself failed — derive best-guess state from kind.
|
|
184
194
|
const running = err.kind !== 'daemon-not-running';
|
|
185
195
|
const extensionConnected = err.kind === 'command-failed';
|
|
186
196
|
renderBridgeStatus(running, extensionConnected);
|
|
@@ -208,6 +218,7 @@ async function renderError(err, cmdName, verbose) {
|
|
|
208
218
|
console.error(chalk.yellow(`→ ${err.hint ?? 'The page structure may have changed — this adapter may be outdated.'}`));
|
|
209
219
|
console.error(chalk.dim(` Debug: ${cmdName} --verbose`));
|
|
210
220
|
console.error(chalk.dim(` Report: ${ISSUES_URL}`));
|
|
221
|
+
emitAutoFixHint(cmdName);
|
|
211
222
|
return;
|
|
212
223
|
}
|
|
213
224
|
// ── ArgumentError ─────────────────────────────────────────────────────
|
|
@@ -251,6 +262,8 @@ async function renderError(err, cmdName, verbose) {
|
|
|
251
262
|
console.error(chalk.yellow(`→ ${classified.hint}`));
|
|
252
263
|
if (classified.kind === 'not-found')
|
|
253
264
|
console.error(chalk.dim(` Report: ${ISSUES_URL}`));
|
|
265
|
+
if (classified.kind === 'not-found')
|
|
266
|
+
emitAutoFixHint(cmdName);
|
|
254
267
|
return;
|
|
255
268
|
}
|
|
256
269
|
// ── Unknown error: show stack in verbose mode ─────────────────────────
|
package/dist/src/diagnostic.d.ts
CHANGED
package/dist/src/diagnostic.js
CHANGED
|
@@ -24,10 +24,16 @@ const MAX_SNAPSHOT_CHARS = 100_000;
|
|
|
24
24
|
const MAX_SOURCE_CHARS = 50_000;
|
|
25
25
|
/** Maximum number of network requests to include. */
|
|
26
26
|
const MAX_NETWORK_REQUESTS = 50;
|
|
27
|
+
/** Maximum number of captured interceptor payloads to include. */
|
|
28
|
+
const MAX_CAPTURED_PAYLOADS = 20;
|
|
27
29
|
/** Maximum characters for a single network request body. */
|
|
28
30
|
const MAX_REQUEST_BODY_CHARS = 4_000;
|
|
29
31
|
/** Maximum characters for error stack trace. */
|
|
30
32
|
const MAX_STACK_CHARS = 5_000;
|
|
33
|
+
/** Maximum nesting depth for arbitrary captured payloads. */
|
|
34
|
+
const MAX_CAPTURED_DEPTH = 4;
|
|
35
|
+
/** Maximum object keys or array items to keep per nesting level. */
|
|
36
|
+
const MAX_CAPTURED_CHILDREN = 20;
|
|
31
37
|
// ── Sensitive data patterns ──────────────────────────────────────────────────
|
|
32
38
|
const SENSITIVE_HEADERS = new Set([
|
|
33
39
|
'authorization',
|
|
@@ -82,6 +88,39 @@ function redactHeaders(headers) {
|
|
|
82
88
|
}
|
|
83
89
|
return result;
|
|
84
90
|
}
|
|
91
|
+
/** Recursively sanitize arbitrary captured response content for diagnostic output. */
|
|
92
|
+
function sanitizeCapturedValue(value, depth = 0) {
|
|
93
|
+
if (typeof value === 'string') {
|
|
94
|
+
return redactText(truncate(value, MAX_REQUEST_BODY_CHARS));
|
|
95
|
+
}
|
|
96
|
+
if (value === null || typeof value === 'number' || typeof value === 'boolean') {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
if (depth >= MAX_CAPTURED_DEPTH) {
|
|
100
|
+
return '[truncated: max depth reached]';
|
|
101
|
+
}
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
const items = value
|
|
104
|
+
.slice(0, MAX_CAPTURED_CHILDREN)
|
|
105
|
+
.map(item => sanitizeCapturedValue(item, depth + 1));
|
|
106
|
+
if (value.length > MAX_CAPTURED_CHILDREN) {
|
|
107
|
+
items.push(`[truncated, ${value.length - MAX_CAPTURED_CHILDREN} items omitted]`);
|
|
108
|
+
}
|
|
109
|
+
return items;
|
|
110
|
+
}
|
|
111
|
+
if (!value || typeof value !== 'object') {
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
const entries = Object.entries(value);
|
|
115
|
+
const result = {};
|
|
116
|
+
for (const [key, child] of entries.slice(0, MAX_CAPTURED_CHILDREN)) {
|
|
117
|
+
result[key] = sanitizeCapturedValue(child, depth + 1);
|
|
118
|
+
}
|
|
119
|
+
if (entries.length > MAX_CAPTURED_CHILDREN) {
|
|
120
|
+
result.__truncated__ = `[${entries.length - MAX_CAPTURED_CHILDREN} fields omitted]`;
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
85
124
|
/** Redact sensitive data from a single network request entry. */
|
|
86
125
|
function redactNetworkRequest(req) {
|
|
87
126
|
if (!req || typeof req !== 'object')
|
|
@@ -106,6 +145,12 @@ function redactNetworkRequest(req) {
|
|
|
106
145
|
if (typeof redacted.body === 'string') {
|
|
107
146
|
redacted.body = truncate(redacted.body, MAX_REQUEST_BODY_CHARS);
|
|
108
147
|
}
|
|
148
|
+
if ('responseBody' in redacted) {
|
|
149
|
+
redacted.responseBody = sanitizeCapturedValue(redacted.responseBody);
|
|
150
|
+
}
|
|
151
|
+
if ('responsePreview' in redacted) {
|
|
152
|
+
redacted.responsePreview = sanitizeCapturedValue(redacted.responsePreview);
|
|
153
|
+
}
|
|
109
154
|
return redacted;
|
|
110
155
|
}
|
|
111
156
|
// ── Timeout helper ───────────────────────────────────────────────────────────
|
|
@@ -163,23 +208,32 @@ function mapDistToSource(filePath) {
|
|
|
163
208
|
export function isDiagnosticEnabled() {
|
|
164
209
|
return process.env.OPENCLI_DIAGNOSTIC === '1';
|
|
165
210
|
}
|
|
211
|
+
function normalizeInterceptedRequests(interceptedRequests) {
|
|
212
|
+
return interceptedRequests.slice(0, MAX_CAPTURED_PAYLOADS).map(responseBody => ({
|
|
213
|
+
source: 'interceptor',
|
|
214
|
+
responseBody: sanitizeCapturedValue(responseBody),
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
166
217
|
/** Safely collect page diagnostic state with redaction, size caps, and timeout. */
|
|
167
218
|
async function collectPageState(page) {
|
|
168
219
|
const collect = async () => {
|
|
169
220
|
try {
|
|
170
|
-
const [url, snapshot, networkRequests, consoleErrors] = await Promise.all([
|
|
221
|
+
const [url, snapshot, networkRequests, interceptedRequests, consoleErrors] = await Promise.all([
|
|
171
222
|
page.getCurrentUrl?.().catch(() => null) ?? Promise.resolve(null),
|
|
172
223
|
page.snapshot().catch(() => '(snapshot unavailable)'),
|
|
173
224
|
page.networkRequests().catch(() => []),
|
|
225
|
+
page.getInterceptedRequests().catch(() => []),
|
|
174
226
|
page.consoleMessages('error').catch(() => []),
|
|
175
227
|
]);
|
|
176
228
|
const rawUrl = url ?? 'unknown';
|
|
229
|
+
const capturedResponses = normalizeInterceptedRequests(interceptedRequests);
|
|
177
230
|
return {
|
|
178
231
|
url: redactUrl(rawUrl),
|
|
179
232
|
snapshot: redactText(truncate(snapshot, MAX_SNAPSHOT_CHARS)),
|
|
180
233
|
networkRequests: networkRequests
|
|
181
234
|
.slice(0, MAX_NETWORK_REQUESTS)
|
|
182
235
|
.map(redactNetworkRequest),
|
|
236
|
+
capturedPayloads: capturedResponses,
|
|
183
237
|
consoleErrors: consoleErrors
|
|
184
238
|
.slice(0, 50)
|
|
185
239
|
.map(e => typeof e === 'string' ? redactText(e) : e),
|
|
@@ -235,7 +289,15 @@ export function emitDiagnostic(ctx) {
|
|
|
235
289
|
let json = JSON.stringify(ctx);
|
|
236
290
|
// Enforce total output budget — drop page state (largest section) first if over budget
|
|
237
291
|
if (json.length > MAX_DIAGNOSTIC_BYTES && ctx.page) {
|
|
238
|
-
const trimmed = {
|
|
292
|
+
const trimmed = {
|
|
293
|
+
...ctx,
|
|
294
|
+
page: {
|
|
295
|
+
...ctx.page,
|
|
296
|
+
snapshot: '[omitted: over size budget]',
|
|
297
|
+
networkRequests: [],
|
|
298
|
+
capturedPayloads: [],
|
|
299
|
+
},
|
|
300
|
+
};
|
|
239
301
|
json = JSON.stringify(trimmed);
|
|
240
302
|
}
|
|
241
303
|
// If still over budget, drop page entirely
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
-
import { buildRepairContext, isDiagnosticEnabled, emitDiagnostic, truncate, redactUrl, redactText, resolveAdapterSourcePath, MAX_DIAGNOSTIC_BYTES, } from './diagnostic.js';
|
|
2
|
+
import { buildRepairContext, collectDiagnostic, isDiagnosticEnabled, emitDiagnostic, truncate, redactUrl, redactText, resolveAdapterSourcePath, MAX_DIAGNOSTIC_BYTES, } from './diagnostic.js';
|
|
3
3
|
import { SelectorError, CommandExecutionError } from './errors.js';
|
|
4
4
|
function makeCmd(overrides = {}) {
|
|
5
5
|
return {
|
|
@@ -211,3 +211,93 @@ describe('emitDiagnostic', () => {
|
|
|
211
211
|
expect(redactUrl('https://api.com/data?token=secret123')).toContain('[REDACTED]');
|
|
212
212
|
});
|
|
213
213
|
});
|
|
214
|
+
function makePage(overrides = {}) {
|
|
215
|
+
return {
|
|
216
|
+
goto: vi.fn(),
|
|
217
|
+
evaluate: vi.fn(),
|
|
218
|
+
getCookies: vi.fn(),
|
|
219
|
+
snapshot: vi.fn().mockResolvedValue('<div>...</div>'),
|
|
220
|
+
click: vi.fn(),
|
|
221
|
+
typeText: vi.fn(),
|
|
222
|
+
pressKey: vi.fn(),
|
|
223
|
+
scrollTo: vi.fn(),
|
|
224
|
+
getFormState: vi.fn(),
|
|
225
|
+
wait: vi.fn(),
|
|
226
|
+
tabs: vi.fn(),
|
|
227
|
+
selectTab: vi.fn(),
|
|
228
|
+
networkRequests: vi.fn().mockResolvedValue([]),
|
|
229
|
+
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
230
|
+
scroll: vi.fn(),
|
|
231
|
+
autoScroll: vi.fn(),
|
|
232
|
+
installInterceptor: vi.fn(),
|
|
233
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
234
|
+
waitForCapture: vi.fn(),
|
|
235
|
+
screenshot: vi.fn(),
|
|
236
|
+
getCurrentUrl: vi.fn().mockResolvedValue('https://example.com/page'),
|
|
237
|
+
...overrides,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
describe('collectDiagnostic', () => {
|
|
241
|
+
it('keeps intercepted payloads in a dedicated capturedPayloads field', async () => {
|
|
242
|
+
const page = makePage({
|
|
243
|
+
networkRequests: vi.fn().mockResolvedValue([{ url: '/api/data', status: 200 }]),
|
|
244
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([{ items: [{ id: 1 }] }]),
|
|
245
|
+
});
|
|
246
|
+
const ctx = await collectDiagnostic(new Error('boom'), makeCmd(), page);
|
|
247
|
+
expect(ctx.page?.networkRequests).toEqual([
|
|
248
|
+
{ url: '/api/data', status: 200 },
|
|
249
|
+
]);
|
|
250
|
+
expect(ctx.page?.capturedPayloads).toEqual([
|
|
251
|
+
{ source: 'interceptor', responseBody: { items: [{ id: 1 }] } },
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
254
|
+
it('preserves the previous network request output when interception is empty', async () => {
|
|
255
|
+
const page = makePage({
|
|
256
|
+
networkRequests: vi.fn().mockResolvedValue([{ url: '/api/data', status: 200 }]),
|
|
257
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
258
|
+
});
|
|
259
|
+
const ctx = await collectDiagnostic(new Error('boom'), makeCmd(), page);
|
|
260
|
+
expect(ctx.page?.networkRequests).toEqual([{ url: '/api/data', status: 200 }]);
|
|
261
|
+
expect(ctx.page?.capturedPayloads).toEqual([]);
|
|
262
|
+
});
|
|
263
|
+
it('swallows intercepted request failures and still returns page state', async () => {
|
|
264
|
+
const page = makePage({
|
|
265
|
+
networkRequests: vi.fn().mockResolvedValue([{ url: '/api/data', status: 200 }]),
|
|
266
|
+
getInterceptedRequests: vi.fn().mockRejectedValue(new Error('interceptor unavailable')),
|
|
267
|
+
});
|
|
268
|
+
const ctx = await collectDiagnostic(new Error('boom'), makeCmd(), page);
|
|
269
|
+
expect(ctx.page).toEqual({
|
|
270
|
+
url: 'https://example.com/page',
|
|
271
|
+
snapshot: '<div>...</div>',
|
|
272
|
+
networkRequests: [{ url: '/api/data', status: 200 }],
|
|
273
|
+
capturedPayloads: [],
|
|
274
|
+
consoleErrors: [],
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
it('redacts and truncates intercepted payloads recursively', async () => {
|
|
278
|
+
const page = makePage({
|
|
279
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([{
|
|
280
|
+
token: 'token=abc123def456ghi789',
|
|
281
|
+
nested: {
|
|
282
|
+
cookie: 'cookie: session=super-secret-cookie-value',
|
|
283
|
+
body: 'x'.repeat(5000),
|
|
284
|
+
},
|
|
285
|
+
}]),
|
|
286
|
+
});
|
|
287
|
+
const ctx = await collectDiagnostic(new Error('boom'), makeCmd(), page);
|
|
288
|
+
const payload = ctx.page?.capturedPayloads?.[0];
|
|
289
|
+
const body = payload.responseBody.nested.body;
|
|
290
|
+
expect(payload).toEqual({
|
|
291
|
+
source: 'interceptor',
|
|
292
|
+
responseBody: {
|
|
293
|
+
token: 'token=[REDACTED]',
|
|
294
|
+
nested: {
|
|
295
|
+
cookie: 'cookie: [REDACTED]',
|
|
296
|
+
body,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
expect(body).toContain('[truncated,');
|
|
301
|
+
expect(body.length).toBeLessThan(5000);
|
|
302
|
+
});
|
|
303
|
+
});
|
package/dist/src/doctor.d.ts
CHANGED
|
@@ -17,7 +17,9 @@ export type ConnectivityResult = {
|
|
|
17
17
|
export type DoctorReport = {
|
|
18
18
|
cliVersion?: string;
|
|
19
19
|
daemonRunning: boolean;
|
|
20
|
+
daemonFlaky?: boolean;
|
|
20
21
|
extensionConnected: boolean;
|
|
22
|
+
extensionFlaky?: boolean;
|
|
21
23
|
extensionVersion?: string;
|
|
22
24
|
connectivity?: ConnectivityResult;
|
|
23
25
|
sessions?: Array<{
|