@jackwener/opencli 1.6.7 → 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 +5 -1
- package/README.zh-CN.md +8 -3
- 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/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/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/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/xueqiu/groups.yaml +23 -0
- package/dist/clis/xueqiu/kline.yaml +65 -0
- package/dist/clis/xueqiu/watchlist.yaml +9 -9
- 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/analysis.d.ts +2 -0
- package/dist/src/analysis.js +6 -0
- package/dist/src/browser/bridge.d.ts +2 -0
- package/dist/src/browser/bridge.js +30 -24
- package/dist/src/browser/cdp.js +96 -0
- 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/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/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/commands/daemon.js +2 -10
- package/dist/src/diagnostic.d.ts +28 -2
- package/dist/src/diagnostic.js +263 -25
- package/dist/src/diagnostic.test.js +220 -1
- package/dist/src/discovery.js +7 -17
- 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/download/progress.js +7 -2
- package/dist/src/execution.js +1 -13
- 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 +3 -6
- package/dist/src/generate.js +4 -8
- 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/plugin.d.ts +2 -1
- package/dist/src/plugin.js +25 -8
- package/dist/src/plugin.test.js +16 -1
- 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 +3 -3
- package/dist/cli-manifest.json +0 -17250
- package/dist/src/browser/discover.d.ts +0 -15
- package/dist/src/browser/discover.js +0 -19
package/dist/src/doctor.js
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { DEFAULT_DAEMON_PORT } from './constants.js';
|
|
8
|
-
import { checkDaemonStatus } from './browser/discover.js';
|
|
9
8
|
import { BrowserBridge } from './browser/index.js';
|
|
10
|
-
import { listSessions } from './browser/daemon-client.js';
|
|
9
|
+
import { getDaemonHealth, listSessions } from './browser/daemon-client.js';
|
|
11
10
|
import { getErrorMessage } from './errors.js';
|
|
12
11
|
import { getRuntimeLabel } from './runtime-detect.js';
|
|
12
|
+
const DOCTOR_LIVE_TIMEOUT_SECONDS = 8;
|
|
13
13
|
/**
|
|
14
14
|
* Test connectivity by attempting a real browser command.
|
|
15
15
|
*/
|
|
@@ -17,7 +17,7 @@ export async function checkConnectivity(opts) {
|
|
|
17
17
|
const start = Date.now();
|
|
18
18
|
try {
|
|
19
19
|
const bridge = new BrowserBridge();
|
|
20
|
-
const page = await bridge.connect({ timeout: opts?.timeout ??
|
|
20
|
+
const page = await bridge.connect({ timeout: opts?.timeout ?? DOCTOR_LIVE_TIMEOUT_SECONDS });
|
|
21
21
|
// Try a simple eval to verify end-to-end connectivity
|
|
22
22
|
await page.evaluate('1 + 1');
|
|
23
23
|
await bridge.close();
|
|
@@ -28,33 +28,48 @@ export async function checkConnectivity(opts) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
export async function runBrowserDoctor(opts = {}) {
|
|
31
|
-
//
|
|
32
|
-
let initialStatus = await checkDaemonStatus();
|
|
33
|
-
if (!initialStatus.running) {
|
|
34
|
-
try {
|
|
35
|
-
const bridge = new BrowserBridge();
|
|
36
|
-
await bridge.connect({ timeout: 5 });
|
|
37
|
-
await bridge.close();
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
// Auto-start failed; we'll report it below.
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
// Run the live connectivity check — it may also auto-start the daemon as a
|
|
44
|
-
// side-effect, so we read daemon status only *after* all side-effects settle.
|
|
31
|
+
// Live connectivity check doubles as auto-start (bridge.connect spawns daemon).
|
|
45
32
|
let connectivity;
|
|
46
33
|
if (opts.live) {
|
|
47
34
|
connectivity = await checkConnectivity();
|
|
48
35
|
}
|
|
49
|
-
|
|
50
|
-
|
|
36
|
+
else {
|
|
37
|
+
// No live probe — daemon may have idle-exited. Do a minimal auto-start
|
|
38
|
+
// so we don't misreport a lazy-lifecycle stop as a real failure.
|
|
39
|
+
const initialHealth = await getDaemonHealth();
|
|
40
|
+
if (initialHealth.state === 'stopped') {
|
|
41
|
+
try {
|
|
42
|
+
const bridge = new BrowserBridge();
|
|
43
|
+
await bridge.connect({ timeout: 5 });
|
|
44
|
+
await bridge.close();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Auto-start failed; we'll report it below.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Single status read *after* all side-effects (live check / auto-start) settle.
|
|
52
|
+
const health = await getDaemonHealth();
|
|
53
|
+
const daemonRunning = health.state !== 'stopped';
|
|
54
|
+
const extensionConnected = health.state === 'ready';
|
|
55
|
+
const daemonFlaky = !!(connectivity?.ok && !daemonRunning);
|
|
56
|
+
const extensionFlaky = !!(connectivity?.ok && daemonRunning && !extensionConnected);
|
|
57
|
+
const sessions = opts.sessions && health.state === 'ready'
|
|
51
58
|
? await listSessions()
|
|
52
59
|
: undefined;
|
|
53
60
|
const issues = [];
|
|
54
|
-
if (
|
|
61
|
+
if (daemonFlaky) {
|
|
62
|
+
issues.push('Daemon connectivity is unstable. The live browser test succeeded, but the daemon was no longer running immediately afterward.\n' +
|
|
63
|
+
'This usually means the daemon crashed or exited right after serving the live probe.');
|
|
64
|
+
}
|
|
65
|
+
else if (!daemonRunning) {
|
|
55
66
|
issues.push('Daemon is not running. It should start automatically when you run an opencli browser command.');
|
|
56
67
|
}
|
|
57
|
-
if (
|
|
68
|
+
if (extensionFlaky) {
|
|
69
|
+
issues.push('Extension connection is unstable. The live browser test succeeded, but the daemon reported the extension disconnected immediately afterward.\n' +
|
|
70
|
+
'This usually means the Browser Bridge service worker is reconnecting slowly or Chrome suspended it.');
|
|
71
|
+
}
|
|
72
|
+
else if (daemonRunning && !extensionConnected) {
|
|
58
73
|
issues.push('Daemon is running but the Chrome/Chromium extension is not connected.\n' +
|
|
59
74
|
'Please install the opencli Browser Bridge extension:\n' +
|
|
60
75
|
' 1. Download from https://github.com/jackwener/opencli/releases\n' +
|
|
@@ -64,19 +79,22 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
64
79
|
if (connectivity && !connectivity.ok) {
|
|
65
80
|
issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
|
|
66
81
|
}
|
|
67
|
-
|
|
68
|
-
|
|
82
|
+
const extensionVersion = health.status?.extensionVersion;
|
|
83
|
+
if (extensionVersion && opts.cliVersion) {
|
|
84
|
+
const extMajor = extensionVersion.split('.')[0];
|
|
69
85
|
const cliMajor = opts.cliVersion.split('.')[0];
|
|
70
86
|
if (extMajor !== cliMajor) {
|
|
71
|
-
issues.push(`Extension major version mismatch: extension v${
|
|
87
|
+
issues.push(`Extension major version mismatch: extension v${extensionVersion} ≠ CLI v${opts.cliVersion}\n` +
|
|
72
88
|
' Download the latest extension from: https://github.com/jackwener/opencli/releases');
|
|
73
89
|
}
|
|
74
90
|
}
|
|
75
91
|
return {
|
|
76
92
|
cliVersion: opts.cliVersion,
|
|
77
|
-
daemonRunning
|
|
78
|
-
|
|
79
|
-
|
|
93
|
+
daemonRunning,
|
|
94
|
+
daemonFlaky,
|
|
95
|
+
extensionConnected,
|
|
96
|
+
extensionFlaky,
|
|
97
|
+
extensionVersion,
|
|
80
98
|
connectivity,
|
|
81
99
|
sessions,
|
|
82
100
|
issues,
|
|
@@ -85,12 +103,22 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
85
103
|
export function renderBrowserDoctorReport(report) {
|
|
86
104
|
const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`) + chalk.dim(` (${getRuntimeLabel()})`), ''];
|
|
87
105
|
// Daemon status
|
|
88
|
-
const daemonIcon = report.
|
|
89
|
-
|
|
106
|
+
const daemonIcon = report.daemonFlaky
|
|
107
|
+
? chalk.yellow('[WARN]')
|
|
108
|
+
: report.daemonRunning ? chalk.green('[OK]') : chalk.red('[MISSING]');
|
|
109
|
+
const daemonLabel = report.daemonFlaky
|
|
110
|
+
? 'unstable (running during live check, then stopped)'
|
|
111
|
+
: report.daemonRunning ? `running on port ${DEFAULT_DAEMON_PORT}` : 'not running';
|
|
112
|
+
lines.push(`${daemonIcon} Daemon: ${daemonLabel}`);
|
|
90
113
|
// Extension status
|
|
91
|
-
const extIcon = report.
|
|
114
|
+
const extIcon = report.extensionFlaky
|
|
115
|
+
? chalk.yellow('[WARN]')
|
|
116
|
+
: report.extensionConnected ? chalk.green('[OK]') : chalk.yellow('[MISSING]');
|
|
92
117
|
const extVersion = report.extensionVersion ? chalk.dim(` (v${report.extensionVersion})`) : '';
|
|
93
|
-
|
|
118
|
+
const extLabel = report.extensionFlaky
|
|
119
|
+
? 'unstable (connected during live check, then disconnected)'
|
|
120
|
+
: report.extensionConnected ? 'connected' : 'not connected';
|
|
121
|
+
lines.push(`${extIcon} Extension: ${extLabel}${extVersion}`);
|
|
94
122
|
// Connectivity
|
|
95
123
|
if (report.connectivity) {
|
|
96
124
|
const connIcon = report.connectivity.ok ? chalk.green('[OK]') : chalk.red('[FAIL]');
|
package/dist/src/doctor.test.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
const {
|
|
3
|
-
|
|
2
|
+
const { mockGetDaemonHealth, mockListSessions, mockConnect, mockClose } = vi.hoisted(() => ({
|
|
3
|
+
mockGetDaemonHealth: vi.fn(),
|
|
4
4
|
mockListSessions: vi.fn(),
|
|
5
5
|
mockConnect: vi.fn(),
|
|
6
6
|
mockClose: vi.fn(),
|
|
7
7
|
}));
|
|
8
|
-
vi.mock('./browser/discover.js', () => ({
|
|
9
|
-
checkDaemonStatus: mockCheckDaemonStatus,
|
|
10
|
-
}));
|
|
11
8
|
vi.mock('./browser/daemon-client.js', () => ({
|
|
9
|
+
getDaemonHealth: mockGetDaemonHealth,
|
|
12
10
|
listSessions: mockListSessions,
|
|
13
11
|
}));
|
|
14
12
|
vi.mock('./browser/index.js', () => ({
|
|
@@ -69,23 +67,98 @@ describe('doctor report rendering', () => {
|
|
|
69
67
|
}));
|
|
70
68
|
expect(text).toContain('[SKIP] Connectivity: skipped (--no-live)');
|
|
71
69
|
});
|
|
72
|
-
it('
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
it('renders unstable extension state when live connectivity and status disagree', () => {
|
|
71
|
+
const text = strip(renderBrowserDoctorReport({
|
|
72
|
+
daemonRunning: true,
|
|
73
|
+
extensionConnected: true,
|
|
74
|
+
extensionFlaky: true,
|
|
75
|
+
connectivity: { ok: true, durationMs: 1234 },
|
|
76
|
+
issues: ['Extension connection is unstable.'],
|
|
77
|
+
}));
|
|
78
|
+
expect(text).toContain('[WARN] Extension: unstable');
|
|
79
|
+
expect(text).toContain('Extension connection is unstable.');
|
|
80
|
+
});
|
|
81
|
+
it('renders unstable daemon state when live connectivity and status disagree', () => {
|
|
82
|
+
const text = strip(renderBrowserDoctorReport({
|
|
83
|
+
daemonRunning: false,
|
|
84
|
+
daemonFlaky: true,
|
|
85
|
+
extensionConnected: false,
|
|
86
|
+
connectivity: { ok: true, durationMs: 1234 },
|
|
87
|
+
issues: ['Daemon connectivity is unstable.'],
|
|
88
|
+
}));
|
|
89
|
+
expect(text).toContain('[WARN] Daemon: unstable');
|
|
90
|
+
expect(text).toContain('Daemon connectivity is unstable.');
|
|
91
|
+
});
|
|
92
|
+
it('reports daemon not running when no-live and auto-start fails', async () => {
|
|
93
|
+
// no-live mode: getDaemonHealth called twice (initial check + final status)
|
|
94
|
+
// Initial: stopped → triggers auto-start attempt
|
|
95
|
+
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
|
|
96
|
+
// Auto-start fails
|
|
77
97
|
mockConnect.mockRejectedValueOnce(new Error('Could not start daemon'));
|
|
78
|
-
//
|
|
79
|
-
|
|
98
|
+
// Final: still stopped
|
|
99
|
+
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
|
|
80
100
|
const report = await runBrowserDoctor({ live: false });
|
|
81
|
-
// Status reflects daemon not running
|
|
82
101
|
expect(report.daemonRunning).toBe(false);
|
|
83
102
|
expect(report.extensionConnected).toBe(false);
|
|
84
|
-
|
|
85
|
-
expect(mockCheckDaemonStatus).toHaveBeenCalledTimes(2);
|
|
86
|
-
// Should report daemon not running
|
|
103
|
+
expect(mockGetDaemonHealth).toHaveBeenCalledTimes(2);
|
|
87
104
|
expect(report.issues).toEqual(expect.arrayContaining([
|
|
88
105
|
expect.stringContaining('Daemon is not running'),
|
|
89
106
|
]));
|
|
90
107
|
});
|
|
108
|
+
it('reports flapping when live check succeeds but final status shows extension disconnected', async () => {
|
|
109
|
+
// Live check succeeds
|
|
110
|
+
mockConnect.mockResolvedValueOnce({
|
|
111
|
+
evaluate: vi.fn().mockResolvedValue(2),
|
|
112
|
+
});
|
|
113
|
+
mockClose.mockResolvedValueOnce(undefined);
|
|
114
|
+
// After live check, getDaemonHealth shows no-extension
|
|
115
|
+
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
|
|
116
|
+
const report = await runBrowserDoctor({ live: true });
|
|
117
|
+
expect(report.daemonRunning).toBe(true);
|
|
118
|
+
expect(report.extensionConnected).toBe(false);
|
|
119
|
+
expect(report.extensionFlaky).toBe(true);
|
|
120
|
+
expect(report.issues).toEqual(expect.arrayContaining([
|
|
121
|
+
expect.stringContaining('Extension connection is unstable'),
|
|
122
|
+
]));
|
|
123
|
+
});
|
|
124
|
+
it('reports daemon flapping when live check succeeds but daemon disappears afterward', async () => {
|
|
125
|
+
// Live check succeeds
|
|
126
|
+
mockConnect.mockResolvedValueOnce({
|
|
127
|
+
evaluate: vi.fn().mockResolvedValue(2),
|
|
128
|
+
});
|
|
129
|
+
mockClose.mockResolvedValueOnce(undefined);
|
|
130
|
+
// After live check, getDaemonHealth shows stopped
|
|
131
|
+
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
|
|
132
|
+
const report = await runBrowserDoctor({ live: true });
|
|
133
|
+
expect(report.daemonRunning).toBe(false);
|
|
134
|
+
expect(report.daemonFlaky).toBe(true);
|
|
135
|
+
expect(report.extensionConnected).toBe(false);
|
|
136
|
+
expect(report.issues).toEqual(expect.arrayContaining([
|
|
137
|
+
expect.stringContaining('Daemon connectivity is unstable'),
|
|
138
|
+
]));
|
|
139
|
+
});
|
|
140
|
+
it('uses the fast default timeout for live connectivity checks', async () => {
|
|
141
|
+
let timeoutSeen;
|
|
142
|
+
mockConnect.mockImplementationOnce(async (opts) => {
|
|
143
|
+
timeoutSeen = opts?.timeout;
|
|
144
|
+
return {
|
|
145
|
+
evaluate: vi.fn().mockResolvedValue(2),
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
mockClose.mockResolvedValueOnce(undefined);
|
|
149
|
+
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'ready', status: { extensionConnected: true } });
|
|
150
|
+
await runBrowserDoctor({ live: true });
|
|
151
|
+
expect(timeoutSeen).toBe(8);
|
|
152
|
+
});
|
|
153
|
+
it('skips auto-start in no-live mode when daemon is already running', async () => {
|
|
154
|
+
// no-live mode but daemon already running (no-extension)
|
|
155
|
+
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
|
|
156
|
+
// Final status: same
|
|
157
|
+
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
|
|
158
|
+
const report = await runBrowserDoctor({ live: false });
|
|
159
|
+
// Should NOT have tried auto-start since daemon was already running
|
|
160
|
+
expect(mockConnect).not.toHaveBeenCalled();
|
|
161
|
+
expect(report.daemonRunning).toBe(true);
|
|
162
|
+
expect(report.extensionConnected).toBe(false);
|
|
163
|
+
});
|
|
91
164
|
});
|
|
@@ -23,8 +23,13 @@ export function formatDuration(ms) {
|
|
|
23
23
|
if (seconds < 60)
|
|
24
24
|
return `${seconds}s`;
|
|
25
25
|
const minutes = Math.floor(seconds / 60);
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
if (minutes < 60) {
|
|
27
|
+
const remainingSeconds = seconds % 60;
|
|
28
|
+
return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
|
29
|
+
}
|
|
30
|
+
const hours = Math.floor(minutes / 60);
|
|
31
|
+
const remainingMinutes = minutes % 60;
|
|
32
|
+
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
30
35
|
* Create a simple progress bar for terminal display.
|
package/dist/src/execution.js
CHANGED
|
@@ -12,12 +12,11 @@
|
|
|
12
12
|
import { Strategy, getRegistry, fullName } from './registry.js';
|
|
13
13
|
import { pathToFileURL } from 'node:url';
|
|
14
14
|
import { executePipeline } from './pipeline/index.js';
|
|
15
|
-
import { AdapterLoadError, ArgumentError,
|
|
15
|
+
import { AdapterLoadError, ArgumentError, CommandExecutionError, getErrorMessage } from './errors.js';
|
|
16
16
|
import { isDiagnosticEnabled, collectDiagnostic, emitDiagnostic } from './diagnostic.js';
|
|
17
17
|
import { shouldUseBrowserSession } from './capabilityRouting.js';
|
|
18
18
|
import { getBrowserFactory, browserSession, runWithTimeout, DEFAULT_BROWSER_COMMAND_TIMEOUT } from './runtime.js';
|
|
19
19
|
import { emitHook } from './hooks.js';
|
|
20
|
-
import { checkDaemonStatus } from './browser/discover.js';
|
|
21
20
|
import { log } from './logger.js';
|
|
22
21
|
import { isElectronApp } from './electron-apps.js';
|
|
23
22
|
import { probeCDP, resolveElectronEndpoint } from './launcher.js';
|
|
@@ -149,17 +148,6 @@ export async function executeCommand(cmd, rawKwargs, debug = false) {
|
|
|
149
148
|
cdpEndpoint = await resolveElectronEndpoint(cmd.site);
|
|
150
149
|
}
|
|
151
150
|
}
|
|
152
|
-
else {
|
|
153
|
-
// Browser Bridge: fail-fast when daemon is up but extension is missing.
|
|
154
|
-
// 300ms timeout avoids a full 2s wait on cold-start.
|
|
155
|
-
const status = await checkDaemonStatus({ timeout: 300 });
|
|
156
|
-
if (status.running && !status.extensionConnected) {
|
|
157
|
-
throw new BrowserConnectError('Browser Bridge extension not connected', 'Install the Browser Bridge:\n' +
|
|
158
|
-
' 1. Download: https://github.com/jackwener/opencli/releases\n' +
|
|
159
|
-
' 2. In Chrome or Chromium, open chrome://extensions → Developer Mode → Load unpacked\n' +
|
|
160
|
-
' Then run: opencli doctor');
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
151
|
ensureRequiredEnv(cmd);
|
|
164
152
|
const BrowserFactory = getBrowserFactory(cmd.site);
|
|
165
153
|
result = await browserSession(BrowserFactory, async (page) => {
|
package/dist/src/explore.d.ts
CHANGED
|
@@ -12,7 +12,6 @@ interface InferredCapability {
|
|
|
12
12
|
name: string;
|
|
13
13
|
description: string;
|
|
14
14
|
strategy: string;
|
|
15
|
-
confidence: number;
|
|
16
15
|
endpoint: string;
|
|
17
16
|
itemPath: string | null;
|
|
18
17
|
recommendedColumns: string[];
|
|
@@ -52,7 +51,6 @@ export interface ExploreEndpointArtifact {
|
|
|
52
51
|
url: string;
|
|
53
52
|
status: number | null;
|
|
54
53
|
contentType: string;
|
|
55
|
-
score: number;
|
|
56
54
|
queryParams: string[];
|
|
57
55
|
itemPath: string | null;
|
|
58
56
|
itemCount: number;
|
package/dist/src/explore.js
CHANGED
|
@@ -13,7 +13,7 @@ import { detectFramework } from './scripts/framework.js';
|
|
|
13
13
|
import { discoverStores } from './scripts/store.js';
|
|
14
14
|
import { interactFuzz } from './scripts/interact.js';
|
|
15
15
|
import { log } from './logger.js';
|
|
16
|
-
import { urlToPattern, findArrayPath, flattenFields, detectFieldRoles, inferCapabilityName, inferStrategy, detectAuthFromHeaders, classifyQueryParams, } from './analysis.js';
|
|
16
|
+
import { urlToPattern, findArrayPath, flattenFields, detectFieldRoles, inferCapabilityName, inferStrategy, detectAuthFromHeaders, classifyQueryParams, isNoiseUrl, } from './analysis.js';
|
|
17
17
|
// ── Site name detection ────────────────────────────────────────────────────
|
|
18
18
|
const KNOWN_SITE_ALIASES = {
|
|
19
19
|
'x.com': 'twitter', 'twitter.com': 'twitter',
|
|
@@ -66,13 +66,29 @@ function parseNetworkRequests(raw) {
|
|
|
66
66
|
return entries;
|
|
67
67
|
}
|
|
68
68
|
if (Array.isArray(raw)) {
|
|
69
|
-
return raw.filter(e => e && typeof e === 'object').map(e =>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
return raw.filter(e => e && typeof e === 'object').map(e => {
|
|
70
|
+
// Handle both legacy shape (status/contentType/responseBody) and
|
|
71
|
+
// extension/CDP capture shape (responseStatus/responseContentType/responsePreview)
|
|
72
|
+
let body = e.responseBody;
|
|
73
|
+
if (body === undefined && e.responsePreview !== undefined) {
|
|
74
|
+
const preview = e.responsePreview;
|
|
75
|
+
if (typeof preview === 'string') {
|
|
76
|
+
try {
|
|
77
|
+
body = JSON.parse(preview);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
body = preview;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
method: (e.method ?? 'GET').toUpperCase(),
|
|
86
|
+
url: String(e.url ?? e.request?.url ?? e.requestUrl ?? ''),
|
|
87
|
+
status: e.status ?? e.responseStatus ?? e.statusCode ?? null,
|
|
88
|
+
contentType: e.contentType ?? e.responseContentType ?? e.response?.contentType ?? '',
|
|
89
|
+
responseBody: body, requestHeaders: e.requestHeaders,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
76
92
|
}
|
|
77
93
|
return [];
|
|
78
94
|
}
|
|
@@ -91,29 +107,32 @@ function isBooleanRecord(value) {
|
|
|
91
107
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
92
108
|
&& Object.values(value).every(v => typeof v === 'boolean');
|
|
93
109
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Deterministic sort key for endpoint ordering — transparent, observable signals only.
|
|
112
|
+
* Used by generate/synthesize to pick a stable default candidate.
|
|
113
|
+
* Not exposed externally; AI agents see the raw metadata and decide for themselves.
|
|
114
|
+
*/
|
|
115
|
+
function endpointSortKey(ep) {
|
|
116
|
+
let k = 0;
|
|
117
|
+
// Prefer endpoints with array data (list APIs are more useful for automation)
|
|
118
|
+
const items = ep.responseAnalysis?.itemCount ?? 0;
|
|
119
|
+
if (items > 0)
|
|
120
|
+
k += 100 + Math.min(items, 50);
|
|
121
|
+
// Prefer endpoints with detected semantic fields
|
|
122
|
+
k += Object.keys(ep.responseAnalysis?.detectedFields ?? {}).length * 10;
|
|
123
|
+
// Prefer API-style paths
|
|
103
124
|
if (ep.pattern.includes('/api/') || ep.pattern.includes('/x/'))
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
s -= 3;
|
|
116
|
-
return s;
|
|
125
|
+
k += 5;
|
|
126
|
+
// Prefer endpoints with query params (more likely to be parameterized APIs)
|
|
127
|
+
if (ep.hasSearchParam || ep.hasPaginationParam || ep.hasLimitParam)
|
|
128
|
+
k += 5;
|
|
129
|
+
return k;
|
|
130
|
+
}
|
|
131
|
+
/** Check whether an endpoint carries useful structured data (any JSON response, not noise). */
|
|
132
|
+
function isUsefulEndpoint(ep) {
|
|
133
|
+
if (isNoiseUrl(ep.url))
|
|
134
|
+
return false;
|
|
135
|
+
return ep.contentType.includes('json');
|
|
117
136
|
}
|
|
118
137
|
// ── Framework detection ────────────────────────────────────────────────────
|
|
119
138
|
const FRAMEWORK_DETECT_JS = detectFramework.toString();
|
|
@@ -122,7 +141,7 @@ const STORE_DISCOVER_JS = discoverStores.toString();
|
|
|
122
141
|
// ── Auto-Interaction (Fuzzing) ─────────────────────────────────────────────
|
|
123
142
|
const INTERACT_FUZZ_JS = interactFuzz.toString();
|
|
124
143
|
// ── Analysis helpers (extracted from exploreUrl) ───────────────────────────
|
|
125
|
-
/** Filter
|
|
144
|
+
/** Filter and deduplicate network endpoints, keeping only useful structured-data APIs. */
|
|
126
145
|
function analyzeEndpoints(networkEntries) {
|
|
127
146
|
const seen = new Map();
|
|
128
147
|
for (const entry of networkEntries) {
|
|
@@ -145,12 +164,13 @@ function analyzeEndpoints(networkEntries) {
|
|
|
145
164
|
hasLimitParam: hasLimit || qp.some(p => LIMIT_PARAMS.has(p)),
|
|
146
165
|
authIndicators: detectAuthFromHeaders(entry.requestHeaders),
|
|
147
166
|
responseAnalysis: entry.responseBody ? analyzeResponseBody(entry.responseBody) : null,
|
|
148
|
-
score: 0,
|
|
149
167
|
};
|
|
150
|
-
ep.score = scoreEndpoint(ep);
|
|
151
168
|
seen.set(key, ep);
|
|
152
169
|
}
|
|
153
|
-
|
|
170
|
+
// Filter to useful endpoints; deterministic ordering by observable metadata signals
|
|
171
|
+
const analyzed = [...seen.values()]
|
|
172
|
+
.filter(isUsefulEndpoint)
|
|
173
|
+
.sort((a, b) => endpointSortKey(b) - endpointSortKey(a));
|
|
154
174
|
return { analyzed, totalCount: seen.size };
|
|
155
175
|
}
|
|
156
176
|
/** Infer CLI capabilities from analyzed endpoints. */
|
|
@@ -192,7 +212,7 @@ function inferCapabilitiesFromEndpoints(endpoints, stores, opts) {
|
|
|
192
212
|
capabilities.push({
|
|
193
213
|
name: capName, description: `${opts.site ?? detectSiteName(opts.url)} ${capName}`,
|
|
194
214
|
strategy: storeHint ? 'store-action' : epStrategy,
|
|
195
|
-
|
|
215
|
+
endpoint: ep.pattern,
|
|
196
216
|
itemPath: ep.responseAnalysis?.itemPath ?? null,
|
|
197
217
|
recommendedColumns: cols.length ? cols : ['title', 'url'],
|
|
198
218
|
recommendedArgs: args,
|
|
@@ -216,7 +236,7 @@ async function writeExploreArtifacts(targetDir, result, analyzedEndpoints, store
|
|
|
216
236
|
}, null, 2)),
|
|
217
237
|
fs.promises.writeFile(path.join(targetDir, 'endpoints.json'), JSON.stringify(analyzedEndpoints.map(ep => ({
|
|
218
238
|
pattern: ep.pattern, method: ep.method, url: ep.url, status: ep.status,
|
|
219
|
-
contentType: ep.contentType,
|
|
239
|
+
contentType: ep.contentType, queryParams: ep.queryParams,
|
|
220
240
|
itemPath: ep.responseAnalysis?.itemPath ?? null, itemCount: ep.responseAnalysis?.itemCount ?? 0,
|
|
221
241
|
detectedFields: ep.responseAnalysis?.detectedFields ?? {}, authIndicators: ep.authIndicators,
|
|
222
242
|
})), null, 2)),
|
|
@@ -237,6 +257,7 @@ export async function exploreUrl(url, opts) {
|
|
|
237
257
|
return browserSession(opts.BrowserFactory, async (page) => {
|
|
238
258
|
return runWithTimeout((async () => {
|
|
239
259
|
// Step 1: Navigate
|
|
260
|
+
await page.startNetworkCapture?.().catch(() => { });
|
|
240
261
|
await page.goto(url);
|
|
241
262
|
await page.wait(waitSeconds);
|
|
242
263
|
// Step 2: Auto-scroll to trigger lazy loading intelligently
|
|
@@ -269,7 +290,9 @@ export async function exploreUrl(url, opts) {
|
|
|
269
290
|
// Step 3: Read page metadata
|
|
270
291
|
const metadata = await readPageMetadata(page);
|
|
271
292
|
// Step 4: Capture network traffic
|
|
272
|
-
const rawNetwork =
|
|
293
|
+
const rawNetwork = page.readNetworkCapture
|
|
294
|
+
? await page.readNetworkCapture()
|
|
295
|
+
: await page.networkRequests(false);
|
|
273
296
|
const networkEntries = parseNetworkRequests(rawNetwork);
|
|
274
297
|
// Step 5: For JSON endpoints missing a body, carefully re-fetch in-browser via a pristine iframe
|
|
275
298
|
const jsonEndpoints = networkEntries.filter(e => e.contentType.includes('json') && e.method === 'GET' && e.status === 200 && !e.responseBody);
|
|
@@ -348,7 +371,7 @@ export function renderExploreSummary(result) {
|
|
|
348
371
|
];
|
|
349
372
|
for (const cap of (result.capabilities ?? []).slice(0, 5)) {
|
|
350
373
|
const storeInfo = cap.storeHint ? ` → ${cap.storeHint.store}.${cap.storeHint.action}()` : '';
|
|
351
|
-
lines.push(` • ${cap.name} (${cap.strategy}
|
|
374
|
+
lines.push(` • ${cap.name} (${cap.strategy})${storeInfo}`);
|
|
352
375
|
}
|
|
353
376
|
const fw = result.framework ?? {};
|
|
354
377
|
const fwNames = Object.entries(fw).filter(([, v]) => v).map(([k]) => k);
|
|
@@ -7,7 +7,6 @@ describe('extension manifest regression', () => {
|
|
|
7
7
|
const raw = await fs.readFile(manifestPath, 'utf8');
|
|
8
8
|
const manifest = JSON.parse(raw);
|
|
9
9
|
expect(manifest.permissions).toContain('cookies');
|
|
10
|
-
expect(manifest.permissions).toContain('scripting');
|
|
11
10
|
expect(manifest.host_permissions).toContain('<all_urls>');
|
|
12
11
|
});
|
|
13
12
|
});
|
package/dist/src/generate.d.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generate: one-shot CLI creation from URL.
|
|
3
3
|
*
|
|
4
|
-
* Orchestrates the
|
|
5
|
-
* explore (Deep Explore) → synthesize (YAML generation
|
|
6
|
-
*
|
|
7
|
-
* Includes Strategy Cascade: if the initial strategy fails,
|
|
8
|
-
* automatically downgrades and retries.
|
|
4
|
+
* Orchestrates the pipeline:
|
|
5
|
+
* explore (Deep Explore) → synthesize (YAML generation + candidate ranking)
|
|
9
6
|
*/
|
|
10
7
|
import type { IBrowserFactory } from './runtime.js';
|
|
11
8
|
import { type SynthesizeCandidateSummary } from './synthesize.js';
|
|
@@ -34,7 +31,7 @@ export interface GenerateCliResult {
|
|
|
34
31
|
};
|
|
35
32
|
synthesize: {
|
|
36
33
|
candidate_count: number;
|
|
37
|
-
candidates: Array<Pick<SynthesizeCandidateSummary, 'name' | 'strategy'
|
|
34
|
+
candidates: Array<Pick<SynthesizeCandidateSummary, 'name' | 'strategy'>>;
|
|
38
35
|
};
|
|
39
36
|
}
|
|
40
37
|
export declare function generateCliFromUrl(opts: GenerateCliOptions): Promise<GenerateCliResult>;
|
package/dist/src/generate.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generate: one-shot CLI creation from URL.
|
|
3
3
|
*
|
|
4
|
-
* Orchestrates the
|
|
5
|
-
* explore (Deep Explore) → synthesize (YAML generation
|
|
6
|
-
*
|
|
7
|
-
* Includes Strategy Cascade: if the initial strategy fails,
|
|
8
|
-
* automatically downgrades and retries.
|
|
4
|
+
* Orchestrates the pipeline:
|
|
5
|
+
* explore (Deep Explore) → synthesize (YAML generation + candidate ranking)
|
|
9
6
|
*/
|
|
10
7
|
import { exploreUrl } from './explore.js';
|
|
11
8
|
import { synthesizeFromExplore } from './synthesize.js';
|
|
@@ -40,7 +37,7 @@ function selectCandidate(candidates, goal) {
|
|
|
40
37
|
if (!candidates.length)
|
|
41
38
|
return null;
|
|
42
39
|
if (!goal)
|
|
43
|
-
return candidates[0];
|
|
40
|
+
return candidates[0];
|
|
44
41
|
const normalized = normalizeGoal(goal);
|
|
45
42
|
if (normalized) {
|
|
46
43
|
const exact = candidates.find(c => c.name === normalized);
|
|
@@ -90,7 +87,6 @@ export async function generateCliFromUrl(opts) {
|
|
|
90
87
|
candidates: (synthesizeResult.candidates ?? []).map((c) => ({
|
|
91
88
|
name: c.name,
|
|
92
89
|
strategy: c.strategy,
|
|
93
|
-
confidence: c.confidence,
|
|
94
90
|
})),
|
|
95
91
|
},
|
|
96
92
|
};
|
|
@@ -111,7 +107,7 @@ export function renderGenerateSummary(r) {
|
|
|
111
107
|
` Candidates: ${r.synthesize?.candidate_count ?? 0}`,
|
|
112
108
|
];
|
|
113
109
|
for (const c of r.synthesize?.candidates ?? []) {
|
|
114
|
-
lines.push(` • ${c.name} (${c.strategy}
|
|
110
|
+
lines.push(` • ${c.name} (${c.strategy})`);
|
|
115
111
|
}
|
|
116
112
|
const fw = r.explore?.framework ?? {};
|
|
117
113
|
const fwNames = Object.entries(fw).filter(([, v]) => v).map(([k]) => k);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface PackageJsonLike {
|
|
2
|
+
bin?: string | Record<string, string>;
|
|
3
|
+
main?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function findPackageRoot(startFile: string, fileExists?: (candidate: string) => boolean): string;
|
|
6
|
+
export declare function getBuiltEntryCandidates(packageRoot: string, readFile?: (filePath: string) => string): string[];
|
|
7
|
+
export declare function getCliManifestPath(clisDir: string): string;
|
|
8
|
+
export declare function getFetchAdaptersScriptPath(packageRoot: string): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export function findPackageRoot(startFile, fileExists = fs.existsSync) {
|
|
4
|
+
let dir = path.dirname(startFile);
|
|
5
|
+
while (true) {
|
|
6
|
+
if (fileExists(path.join(dir, 'package.json')))
|
|
7
|
+
return dir;
|
|
8
|
+
const parent = path.dirname(dir);
|
|
9
|
+
if (parent === dir) {
|
|
10
|
+
throw new Error(`Could not find package.json above ${startFile}`);
|
|
11
|
+
}
|
|
12
|
+
dir = parent;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function getBuiltEntryCandidates(packageRoot, readFile = (filePath) => fs.readFileSync(filePath, 'utf-8')) {
|
|
16
|
+
const candidates = [];
|
|
17
|
+
try {
|
|
18
|
+
const pkg = JSON.parse(readFile(path.join(packageRoot, 'package.json')));
|
|
19
|
+
if (typeof pkg.bin === 'string') {
|
|
20
|
+
candidates.push(path.join(packageRoot, pkg.bin));
|
|
21
|
+
}
|
|
22
|
+
else if (pkg.bin && typeof pkg.bin === 'object' && typeof pkg.bin.opencli === 'string') {
|
|
23
|
+
candidates.push(path.join(packageRoot, pkg.bin.opencli));
|
|
24
|
+
}
|
|
25
|
+
if (typeof pkg.main === 'string') {
|
|
26
|
+
candidates.push(path.join(packageRoot, pkg.main));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Fall through to compatibility candidates below.
|
|
31
|
+
}
|
|
32
|
+
// Compatibility fallback for partially-built trees or older layouts.
|
|
33
|
+
candidates.push(path.join(packageRoot, 'dist', 'src', 'main.js'), path.join(packageRoot, 'dist', 'main.js'));
|
|
34
|
+
return [...new Set(candidates)];
|
|
35
|
+
}
|
|
36
|
+
export function getCliManifestPath(clisDir) {
|
|
37
|
+
return path.resolve(clisDir, '..', 'cli-manifest.json');
|
|
38
|
+
}
|
|
39
|
+
export function getFetchAdaptersScriptPath(packageRoot) {
|
|
40
|
+
return path.join(packageRoot, 'scripts', 'fetch-adapters.js');
|
|
41
|
+
}
|