@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.
Files changed (122) hide show
  1. package/README.md +5 -1
  2. package/README.zh-CN.md +8 -3
  3. package/dist/clis/1688/assets.d.ts +42 -0
  4. package/dist/clis/1688/assets.js +204 -0
  5. package/dist/clis/1688/assets.test.d.ts +1 -0
  6. package/dist/clis/1688/assets.test.js +39 -0
  7. package/dist/clis/1688/download.d.ts +9 -0
  8. package/dist/clis/1688/download.js +76 -0
  9. package/dist/clis/1688/download.test.d.ts +1 -0
  10. package/dist/clis/1688/download.test.js +31 -0
  11. package/dist/clis/1688/shared.d.ts +10 -0
  12. package/dist/clis/1688/shared.js +43 -0
  13. package/dist/clis/jianyu/search.d.ts +14 -0
  14. package/dist/clis/jianyu/search.js +135 -0
  15. package/dist/clis/jianyu/search.test.d.ts +1 -0
  16. package/dist/clis/jianyu/search.test.js +23 -0
  17. package/dist/clis/linux-do/topic-content.d.ts +35 -0
  18. package/dist/clis/linux-do/topic-content.js +154 -0
  19. package/dist/clis/linux-do/topic-content.test.d.ts +1 -0
  20. package/dist/clis/linux-do/topic-content.test.js +59 -0
  21. package/dist/clis/linux-do/topic.yaml +1 -16
  22. package/dist/clis/quark/ls.d.ts +1 -0
  23. package/dist/clis/quark/ls.js +63 -0
  24. package/dist/clis/quark/mkdir.d.ts +1 -0
  25. package/dist/clis/quark/mkdir.js +36 -0
  26. package/dist/clis/quark/mv.d.ts +1 -0
  27. package/dist/clis/quark/mv.js +53 -0
  28. package/dist/clis/quark/rename.d.ts +1 -0
  29. package/dist/clis/quark/rename.js +26 -0
  30. package/dist/clis/quark/rm.d.ts +1 -0
  31. package/dist/clis/quark/rm.js +24 -0
  32. package/dist/clis/quark/save.d.ts +1 -0
  33. package/dist/clis/quark/save.js +80 -0
  34. package/dist/clis/quark/share-tree.d.ts +1 -0
  35. package/dist/clis/quark/share-tree.js +45 -0
  36. package/dist/clis/quark/utils.d.ts +50 -0
  37. package/dist/clis/quark/utils.js +146 -0
  38. package/dist/clis/quark/utils.test.d.ts +1 -0
  39. package/dist/clis/quark/utils.test.js +58 -0
  40. package/dist/clis/twitter/reply.js +3 -8
  41. package/dist/clis/twitter/reply.test.js +5 -5
  42. package/dist/clis/xiaohongshu/note.js +8 -3
  43. package/dist/clis/xiaohongshu/note.test.js +11 -0
  44. package/dist/clis/xueqiu/groups.yaml +23 -0
  45. package/dist/clis/xueqiu/kline.yaml +65 -0
  46. package/dist/clis/xueqiu/watchlist.yaml +9 -9
  47. package/dist/clis/zhihu/answer.d.ts +1 -0
  48. package/dist/clis/zhihu/answer.js +194 -0
  49. package/dist/clis/zhihu/answer.test.d.ts +1 -0
  50. package/dist/clis/zhihu/answer.test.js +81 -0
  51. package/dist/clis/zhihu/comment.d.ts +1 -0
  52. package/dist/clis/zhihu/comment.js +335 -0
  53. package/dist/clis/zhihu/comment.test.d.ts +1 -0
  54. package/dist/clis/zhihu/comment.test.js +54 -0
  55. package/dist/clis/zhihu/favorite.d.ts +1 -0
  56. package/dist/clis/zhihu/favorite.js +224 -0
  57. package/dist/clis/zhihu/favorite.test.d.ts +1 -0
  58. package/dist/clis/zhihu/favorite.test.js +196 -0
  59. package/dist/clis/zhihu/follow.d.ts +1 -0
  60. package/dist/clis/zhihu/follow.js +80 -0
  61. package/dist/clis/zhihu/follow.test.d.ts +1 -0
  62. package/dist/clis/zhihu/follow.test.js +45 -0
  63. package/dist/clis/zhihu/like.d.ts +1 -0
  64. package/dist/clis/zhihu/like.js +91 -0
  65. package/dist/clis/zhihu/like.test.d.ts +1 -0
  66. package/dist/clis/zhihu/like.test.js +64 -0
  67. package/dist/clis/zhihu/target.d.ts +24 -0
  68. package/dist/clis/zhihu/target.js +91 -0
  69. package/dist/clis/zhihu/target.test.d.ts +1 -0
  70. package/dist/clis/zhihu/target.test.js +77 -0
  71. package/dist/clis/zhihu/write-shared.d.ts +32 -0
  72. package/dist/clis/zhihu/write-shared.js +221 -0
  73. package/dist/clis/zhihu/write-shared.test.d.ts +1 -0
  74. package/dist/clis/zhihu/write-shared.test.js +175 -0
  75. package/dist/src/analysis.d.ts +2 -0
  76. package/dist/src/analysis.js +6 -0
  77. package/dist/src/browser/bridge.d.ts +2 -0
  78. package/dist/src/browser/bridge.js +30 -24
  79. package/dist/src/browser/cdp.js +96 -0
  80. package/dist/src/browser/daemon-client.d.ts +17 -8
  81. package/dist/src/browser/daemon-client.js +12 -13
  82. package/dist/src/browser/daemon-client.test.js +32 -25
  83. package/dist/src/browser/index.d.ts +2 -1
  84. package/dist/src/browser/index.js +1 -1
  85. package/dist/src/browser.test.js +2 -3
  86. package/dist/src/build-manifest.d.ts +3 -1
  87. package/dist/src/build-manifest.js +10 -7
  88. package/dist/src/build-manifest.test.js +8 -4
  89. package/dist/src/cli.d.ts +2 -1
  90. package/dist/src/cli.js +48 -46
  91. package/dist/src/clis/binance/commands.test.d.ts +1 -0
  92. package/dist/src/clis/binance/commands.test.js +54 -0
  93. package/dist/src/commanderAdapter.js +19 -6
  94. package/dist/src/commands/daemon.js +2 -10
  95. package/dist/src/diagnostic.d.ts +28 -2
  96. package/dist/src/diagnostic.js +263 -25
  97. package/dist/src/diagnostic.test.js +220 -1
  98. package/dist/src/discovery.js +7 -17
  99. package/dist/src/doctor.d.ts +2 -0
  100. package/dist/src/doctor.js +59 -31
  101. package/dist/src/doctor.test.js +89 -16
  102. package/dist/src/download/progress.js +7 -2
  103. package/dist/src/execution.js +1 -13
  104. package/dist/src/explore.d.ts +0 -2
  105. package/dist/src/explore.js +61 -38
  106. package/dist/src/extension-manifest-regression.test.js +0 -1
  107. package/dist/src/generate.d.ts +3 -6
  108. package/dist/src/generate.js +4 -8
  109. package/dist/src/package-paths.d.ts +8 -0
  110. package/dist/src/package-paths.js +41 -0
  111. package/dist/src/plugin-scaffold.js +1 -3
  112. package/dist/src/plugin.d.ts +2 -1
  113. package/dist/src/plugin.js +25 -8
  114. package/dist/src/plugin.test.js +16 -1
  115. package/dist/src/record.d.ts +1 -2
  116. package/dist/src/record.js +14 -52
  117. package/dist/src/synthesize.d.ts +0 -2
  118. package/dist/src/synthesize.js +8 -4
  119. package/package.json +3 -3
  120. package/dist/cli-manifest.json +0 -17250
  121. package/dist/src/browser/discover.d.ts +0 -15
  122. package/dist/src/browser/discover.js +0 -19
@@ -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 ?? 8 });
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
- // Try to auto-start daemon if it's not running, so we show accurate status.
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
- const status = await checkDaemonStatus();
50
- const sessions = opts.sessions && status.running && status.extensionConnected
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 (!status.running) {
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 (status.running && !status.extensionConnected) {
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
- if (status.extensionVersion && opts.cliVersion) {
68
- const extMajor = status.extensionVersion.split('.')[0];
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${status.extensionVersion} ≠ CLI v${opts.cliVersion}\n` +
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: status.running,
78
- extensionConnected: status.extensionConnected,
79
- extensionVersion: status.extensionVersion,
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.daemonRunning ? chalk.green('[OK]') : chalk.red('[MISSING]');
89
- lines.push(`${daemonIcon} Daemon: ${report.daemonRunning ? `running on port ${DEFAULT_DAEMON_PORT}` : 'not running'}`);
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.extensionConnected ? chalk.green('[OK]') : chalk.yellow('[MISSING]');
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
- lines.push(`${extIcon} Extension: ${report.extensionConnected ? 'connected' : 'not connected'}${extVersion}`);
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]');
@@ -1,14 +1,12 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- const { mockCheckDaemonStatus, mockListSessions, mockConnect, mockClose } = vi.hoisted(() => ({
3
- mockCheckDaemonStatus: vi.fn(),
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('reports consistent status when live check auto-starts the daemon', async () => {
73
- // checkDaemonStatus is called twice: once for auto-start check, once for final status.
74
- // First call: daemon not running (triggers auto-start attempt)
75
- mockCheckDaemonStatus.mockResolvedValueOnce({ running: false, extensionConnected: false });
76
- // Auto-start attempt via BrowserBridge.connect fails
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
- // Second call: daemon still not running after failed auto-start
79
- mockCheckDaemonStatus.mockResolvedValueOnce({ running: false, extensionConnected: false });
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
- // checkDaemonStatus called twice (initial + final)
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
- const remainingSeconds = seconds % 60;
27
- return `${minutes}m ${remainingSeconds}s`;
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.
@@ -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, BrowserConnectError, CommandExecutionError, getErrorMessage } from './errors.js';
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) => {
@@ -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;
@@ -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
- method: (e.method ?? 'GET').toUpperCase(),
71
- url: String(e.url ?? e.request?.url ?? e.requestUrl ?? ''),
72
- status: e.status ?? e.statusCode ?? null,
73
- contentType: e.contentType ?? e.response?.contentType ?? '',
74
- responseBody: e.responseBody, requestHeaders: e.requestHeaders,
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
- function scoreEndpoint(ep) {
95
- let s = 0;
96
- if (ep.contentType.includes('json'))
97
- s += 10;
98
- if (ep.responseAnalysis) {
99
- s += 5;
100
- s += Math.min(ep.responseAnalysis.itemCount, 10);
101
- s += Object.keys(ep.responseAnalysis.detectedFields).length * 2;
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
- s += 3;
105
- if (ep.hasSearchParam)
106
- s += 3;
107
- if (ep.hasPaginationParam)
108
- s += 2;
109
- if (ep.hasLimitParam)
110
- s += 2;
111
- if (ep.status === 200)
112
- s += 2;
113
- // Anti-Bot Empty Value Detection: penalize JSON endpoints returning empty data
114
- if (ep.responseAnalysis && ep.responseAnalysis.itemCount === 0 && ep.contentType.includes('json'))
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, deduplicate, and score network endpoints. */
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
- const analyzed = [...seen.values()].filter(ep => ep.score >= 5).sort((a, b) => b.score - a.score);
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
- confidence: Math.min(ep.score / 20, 1.0), endpoint: ep.pattern,
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, score: ep.score, queryParams: ep.queryParams,
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 = await page.networkRequests(false);
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}, ${(cap.confidence * 100).toFixed(0)}%)${storeInfo}`);
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
  });
@@ -1,11 +1,8 @@
1
1
  /**
2
2
  * Generate: one-shot CLI creation from URL.
3
3
  *
4
- * Orchestrates the full pipeline:
5
- * explore (Deep Explore) → synthesize (YAML generation) register → verify
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' | 'confidence'>>;
34
+ candidates: Array<Pick<SynthesizeCandidateSummary, 'name' | 'strategy'>>;
38
35
  };
39
36
  }
40
37
  export declare function generateCliFromUrl(opts: GenerateCliOptions): Promise<GenerateCliResult>;
@@ -1,11 +1,8 @@
1
1
  /**
2
2
  * Generate: one-shot CLI creation from URL.
3
3
  *
4
- * Orchestrates the full pipeline:
5
- * explore (Deep Explore) → synthesize (YAML generation) register → verify
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]; // highest confidence first
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}, ${((c.confidence ?? 0) * 100).toFixed(0)}%)`);
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
+ }
@@ -68,9 +68,7 @@ pipeline:
68
68
  - fetch:
69
69
  url: "https://httpbin.org/get?greeting=hello"
70
70
  method: GET
71
- - extract:
72
- type: json
73
- selector: "$.args"
71
+ - select: "args"
74
72
  `;
75
73
  writeFile(targetDir, 'hello.yaml', yamlContent);
76
74
  files.push('hello.yaml');