@jackwener/opencli 1.2.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -88,7 +88,7 @@ function scheduleReconnect(): void {
88
88
  // ─── Automation window isolation ─────────────────────────────────────
89
89
  // All opencli operations happen in a dedicated Chrome window so the
90
90
  // user's active browsing session is never touched.
91
- // The window auto-closes after 30s of idle (no commands).
91
+ // The window auto-closes after 120s of idle (no commands).
92
92
 
93
93
  type AutomationSession = {
94
94
  windowId: number;
@@ -247,7 +247,6 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
247
247
  if (tabId !== undefined) {
248
248
  try {
249
249
  const tab = await chrome.tabs.get(tabId);
250
- console.log(`[opencli] resolveTabId: explicit tabId=${tabId}, url=${tab.url}`);
251
250
  if (isDebuggableUrl(tab.url)) return tabId;
252
251
  // Tab exists but URL is not debuggable — fall through to auto-resolve
253
252
  console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`);
@@ -260,42 +259,27 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
260
259
  // Get (or create) the automation window
261
260
  const windowId = await getAutomationWindow(workspace);
262
261
 
263
- // Prefer an existing debuggable tab (about:blank, http://, https://, etc.)
262
+ // Prefer an existing debuggable tab
264
263
  const tabs = await chrome.tabs.query({ windowId });
265
264
  const debuggableTab = tabs.find(t => t.id && isDebuggableUrl(t.url));
266
- if (debuggableTab?.id) {
267
- console.log(`[opencli] resolveTabId: found debuggable tab ${debuggableTab.id} (${debuggableTab.url})`);
268
- return debuggableTab.id;
269
- }
270
- console.warn(`[opencli] resolveTabId: no debuggable tabs found, tabs: ${tabs.map(t => `${t.id}=${t.url}`).join(', ')}`);
265
+ if (debuggableTab?.id) return debuggableTab.id;
271
266
 
272
- // No debuggable tab found this typically happens when a "New Tab Override"
273
- // extension replaces about:blank with a chrome-extension:// page.
274
- // Reuse the first existing tab by navigating it to about:blank (avoids
275
- // accumulating orphan tabs if chrome.tabs.create is also intercepted).
267
+ // No debuggable tab — another extension may have hijacked the tab URL.
268
+ // Try to reuse by navigating to a data: URI (not interceptable by New Tab Override).
276
269
  const reuseTab = tabs.find(t => t.id);
277
270
  if (reuseTab?.id) {
278
271
  await chrome.tabs.update(reuseTab.id, { url: 'data:text/html,<html></html>' });
279
- // Wait for the navigation to take effect
280
272
  await new Promise(resolve => setTimeout(resolve, 300));
281
- // Verify the URL is actually debuggable (New Tab Override may have intercepted)
282
273
  try {
283
274
  const updated = await chrome.tabs.get(reuseTab.id);
284
275
  if (isDebuggableUrl(updated.url)) return reuseTab.id;
285
- // New Tab Override intercepted about:blank try data: URI instead
286
- console.warn(`[opencli] about:blank was intercepted (${updated.url}), trying data: URI`);
287
- await chrome.tabs.update(reuseTab.id, { url: 'data:text/html,<html></html>' });
288
- await new Promise(resolve => setTimeout(resolve, 300));
289
- const updated2 = await chrome.tabs.get(reuseTab.id);
290
- if (isDebuggableUrl(updated2.url)) return reuseTab.id;
291
- // data: URI also intercepted — create a brand new tab
292
- console.warn(`[opencli] data: URI also intercepted, creating fresh tab`);
276
+ console.warn(`[opencli] data: URI was intercepted (${updated.url}), creating fresh tab`);
293
277
  } catch {
294
278
  // Tab was closed during navigation
295
279
  }
296
280
  }
297
281
 
298
- // Window has no debuggable tabs — create one
282
+ // Fallback: create a new tab
299
283
  const newTab = await chrome.tabs.create({ windowId, url: 'data:text/html,<html></html>', active: true });
300
284
  if (!newTab.id) throw new Error('Failed to create tab in automation window');
301
285
  return newTab.id;
@@ -47,15 +47,18 @@ async function ensureAttached(tabId: number): Promise<void> {
47
47
  await chrome.debugger.attach({ tabId }, '1.3');
48
48
  } catch (e: unknown) {
49
49
  const msg = e instanceof Error ? e.message : String(e);
50
+ const hint = msg.includes('chrome-extension://')
51
+ ? '. Tip: another Chrome extension may be interfering — try disabling other extensions'
52
+ : '';
50
53
  if (msg.includes('Another debugger is already attached')) {
51
54
  try { await chrome.debugger.detach({ tabId }); } catch { /* ignore */ }
52
55
  try {
53
56
  await chrome.debugger.attach({ tabId }, '1.3');
54
57
  } catch {
55
- throw new Error(`attach failed: ${msg}`);
58
+ throw new Error(`attach failed: ${msg}${hint}`);
56
59
  }
57
60
  } else {
58
- throw new Error(`attach failed: ${msg}`);
61
+ throw new Error(`attach failed: ${msg}${hint}`);
59
62
  }
60
63
  }
61
64
  attached.add(tabId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "1.2.5",
3
+ "version": "1.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -20,6 +20,7 @@ import {
20
20
  scrollJs,
21
21
  autoScrollJs,
22
22
  networkRequestsJs,
23
+ waitForDomStableJs,
23
24
  } from './dom-helpers.js';
24
25
 
25
26
  export interface CDPTarget {
@@ -177,10 +178,11 @@ class CDPPage implements IPage {
177
178
  .catch(() => {}); // Don't fail if event times out
178
179
  await this.bridge.send('Page.navigate', { url });
179
180
  await loadPromise;
180
- // Post-load settle: SPA frameworks need extra time to render after load event
181
+ // Smart settle: use DOM stability detection instead of fixed sleep.
182
+ // settleMs is now a timeout cap (default 1000ms), not a fixed wait.
181
183
  if (options?.waitUntil !== 'none') {
182
- const settleMs = options?.settleMs ?? 1000;
183
- await new Promise(resolve => setTimeout(resolve, settleMs));
184
+ const maxMs = options?.settleMs ?? 1000;
185
+ await this.evaluate(waitForDomStableJs(maxMs, Math.min(500, maxMs)));
184
186
  }
185
187
  }
186
188
 
@@ -44,7 +44,10 @@ export async function isDaemonRunning(): Promise<boolean> {
44
44
  try {
45
45
  const controller = new AbortController();
46
46
  const timer = setTimeout(() => controller.abort(), 2000);
47
- const res = await fetch(`${DAEMON_URL}/status`, { signal: controller.signal });
47
+ const res = await fetch(`${DAEMON_URL}/status`, {
48
+ headers: { 'X-OpenCLI': '1' },
49
+ signal: controller.signal,
50
+ });
48
51
  clearTimeout(timer);
49
52
  return res.ok;
50
53
  } catch {
@@ -59,7 +62,10 @@ export async function isExtensionConnected(): Promise<boolean> {
59
62
  try {
60
63
  const controller = new AbortController();
61
64
  const timer = setTimeout(() => controller.abort(), 2000);
62
- const res = await fetch(`${DAEMON_URL}/status`, { signal: controller.signal });
65
+ const res = await fetch(`${DAEMON_URL}/status`, {
66
+ headers: { 'X-OpenCLI': '1' },
67
+ signal: controller.signal,
68
+ });
63
69
  clearTimeout(timer);
64
70
  if (!res.ok) return false;
65
71
  const data = await res.json() as { extensionConnected?: boolean };
@@ -90,7 +96,7 @@ export async function sendCommand(
90
96
 
91
97
  const res = await fetch(`${DAEMON_URL}/command`, {
92
98
  method: 'POST',
93
- headers: { 'Content-Type': 'application/json' },
99
+ headers: { 'Content-Type': 'application/json', 'X-OpenCLI': '1' },
94
100
  body: JSON.stringify(command),
95
101
  signal: controller.signal,
96
102
  });
@@ -18,7 +18,9 @@ export async function checkDaemonStatus(): Promise<{
18
18
  }> {
19
19
  try {
20
20
  const port = parseInt(process.env.OPENCLI_DAEMON_PORT ?? '19825', 10);
21
- const res = await fetch(`http://127.0.0.1:${port}/status`);
21
+ const res = await fetch(`http://127.0.0.1:${port}/status`, {
22
+ headers: { 'X-OpenCLI': '1' },
23
+ });
22
24
  const data = await res.json() as { ok: boolean; extensionConnected: boolean };
23
25
  return { running: true, extensionConnected: data.extensionConnected };
24
26
  } catch {
@@ -145,3 +145,37 @@ export function networkRequestsJs(includeStatic: boolean): string {
145
145
  })()
146
146
  `;
147
147
  }
148
+
149
+ /**
150
+ * Generate JS to wait until the DOM stabilizes (no mutations for `quietMs`),
151
+ * with a hard cap at `maxMs`. Uses MutationObserver in the browser.
152
+ *
153
+ * Returns as soon as the page stops changing, avoiding unnecessary fixed waits.
154
+ * If document.body is not available, falls back to a fixed sleep of maxMs.
155
+ */
156
+ export function waitForDomStableJs(maxMs: number, quietMs: number): string {
157
+ return `
158
+ new Promise(resolve => {
159
+ if (!document.body) {
160
+ setTimeout(() => resolve('nobody'), ${maxMs});
161
+ return;
162
+ }
163
+ let timer = null;
164
+ let cap = null;
165
+ const done = (reason) => {
166
+ clearTimeout(timer);
167
+ clearTimeout(cap);
168
+ obs.disconnect();
169
+ resolve(reason);
170
+ };
171
+ const resetQuiet = () => {
172
+ clearTimeout(timer);
173
+ timer = setTimeout(() => done('quiet'), ${quietMs});
174
+ };
175
+ const obs = new MutationObserver(resetQuiet);
176
+ obs.observe(document.body, { childList: true, subtree: true, attributes: true });
177
+ resetQuiet();
178
+ cap = setTimeout(() => done('capped'), ${maxMs});
179
+ })
180
+ `;
181
+ }
@@ -23,6 +23,7 @@ import {
23
23
  scrollJs,
24
24
  autoScrollJs,
25
25
  networkRequestsJs,
26
+ waitForDomStableJs,
26
27
  } from './dom-helpers.js';
27
28
 
28
29
  /**
@@ -53,11 +54,15 @@ export class Page implements IPage {
53
54
  if (result?.tabId) {
54
55
  this._tabId = result.tabId;
55
56
  }
56
- // Post-load settle: the extension already waits for tab.status === 'complete',
57
- // but SPA frameworks (React/Vue) need extra time to render after DOM load.
57
+ // Smart settle: use DOM stability detection instead of fixed sleep.
58
+ // settleMs is now a timeout cap (default 1000ms), not a fixed wait.
58
59
  if (options?.waitUntil !== 'none') {
59
- const settleMs = options?.settleMs ?? 1000;
60
- await new Promise(resolve => setTimeout(resolve, settleMs));
60
+ const maxMs = options?.settleMs ?? 1000;
61
+ await sendCommand('exec', {
62
+ code: waitForDomStableJs(maxMs, Math.min(500, maxMs)),
63
+ ...this._workspaceOpt(),
64
+ ...this._tabOpt(),
65
+ });
61
66
  }
62
67
  }
63
68
 
package/src/cli.ts CHANGED
@@ -202,12 +202,12 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
202
202
  console.log(renderCascadeResult(result));
203
203
  });
204
204
 
205
- // ── Built-in: doctor / setup / completion ─────────────────────────────────
205
+ // ── Built-in: doctor / completion ──────────────────────────────────────────
206
206
 
207
207
  program
208
208
  .command('doctor')
209
209
  .description('Diagnose opencli browser bridge connectivity')
210
- .option('--live', 'Test browser connectivity (requires Chrome running)', false)
210
+ .option('--no-live', 'Skip live browser connectivity test')
211
211
  .option('--sessions', 'Show active automation sessions', false)
212
212
  .action(async (opts) => {
213
213
  const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
@@ -215,14 +215,6 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
215
215
  console.log(renderBrowserDoctorReport(report));
216
216
  });
217
217
 
218
- program
219
- .command('setup')
220
- .description('Interactive setup: verify browser bridge connectivity')
221
- .action(async () => {
222
- const { runSetup } = await import('./setup.js');
223
- await runSetup({ cliVersion: PKG_VERSION });
224
- });
225
-
226
218
  program
227
219
  .command('completion')
228
220
  .description('Output shell completion script')
package/src/daemon.ts CHANGED
@@ -5,6 +5,14 @@
5
5
  * CLI → HTTP POST /command → daemon → WebSocket → Extension
6
6
  * Extension → WebSocket result → daemon → HTTP response → CLI
7
7
  *
8
+ * Security (defense-in-depth against browser-based CSRF):
9
+ * 1. Origin check — reject HTTP/WS from non chrome-extension:// origins
10
+ * 2. Custom header — require X-OpenCLI header (browsers can't send it
11
+ * without CORS preflight, which we deny)
12
+ * 3. No CORS headers — responses never include Access-Control-Allow-Origin
13
+ * 4. Body size limit — 1 MB max to prevent OOM
14
+ * 5. WebSocket verifyClient — reject upgrade before connection is established
15
+ *
8
16
  * Lifecycle:
9
17
  * - Auto-spawned by opencli on first browser command
10
18
  * - Auto-exits after 5 minutes of idle
@@ -49,25 +57,56 @@ function resetIdleTimer(): void {
49
57
 
50
58
  // ─── HTTP Server ─────────────────────────────────────────────────────
51
59
 
60
+ const MAX_BODY = 1024 * 1024; // 1 MB — commands are tiny; this prevents OOM
61
+
52
62
  function readBody(req: IncomingMessage): Promise<string> {
53
63
  return new Promise((resolve, reject) => {
54
64
  const chunks: Buffer[] = [];
55
- req.on('data', (c: Buffer) => chunks.push(c));
65
+ let size = 0;
66
+ req.on('data', (c: Buffer) => {
67
+ size += c.length;
68
+ if (size > MAX_BODY) { req.destroy(); reject(new Error('Body too large')); return; }
69
+ chunks.push(c);
70
+ });
56
71
  req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
57
72
  req.on('error', reject);
58
73
  });
59
74
  }
60
75
 
61
76
  function jsonResponse(res: ServerResponse, status: number, data: unknown): void {
62
- res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
77
+ res.writeHead(status, { 'Content-Type': 'application/json' });
63
78
  res.end(JSON.stringify(data));
64
79
  }
65
80
 
66
81
  async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
67
- res.setHeader('Access-Control-Allow-Origin', '*');
68
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
69
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
70
- if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
82
+ // ─── Security: Origin & custom-header check ──────────────────────
83
+ // Block browser-based CSRF: browsers always send an Origin header on
84
+ // cross-origin requests. Node.js CLI fetch does NOT send Origin, so
85
+ // legitimate CLI requests pass through. Chrome Extension connects via
86
+ // WebSocket (which bypasses this HTTP handler entirely).
87
+ const origin = req.headers['origin'] as string | undefined;
88
+ if (origin && !origin.startsWith('chrome-extension://')) {
89
+ jsonResponse(res, 403, { ok: false, error: 'Forbidden: cross-origin request blocked' });
90
+ return;
91
+ }
92
+
93
+ // CORS: do NOT send Access-Control-Allow-Origin for normal requests.
94
+ // Only handle preflight so browsers get a definitive "no" answer.
95
+ if (req.method === 'OPTIONS') {
96
+ // No ACAO header → browser will block the actual request.
97
+ res.writeHead(204);
98
+ res.end();
99
+ return;
100
+ }
101
+
102
+ // Require custom header on all HTTP requests. Browsers cannot attach
103
+ // custom headers in "simple" requests, and our preflight returns no
104
+ // Access-Control-Allow-Headers, so scripted fetch() from web pages is
105
+ // blocked even if Origin check is somehow bypassed.
106
+ if (!req.headers['x-opencli']) {
107
+ jsonResponse(res, 403, { ok: false, error: 'Forbidden: missing X-OpenCLI header' });
108
+ return;
109
+ }
71
110
 
72
111
  const url = req.url ?? '/';
73
112
  const pathname = url.split('?')[0];
@@ -136,7 +175,18 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
136
175
  // ─── WebSocket for Extension ─────────────────────────────────────────
137
176
 
138
177
  const httpServer = createServer((req, res) => { handleRequest(req, res).catch(() => { res.writeHead(500); res.end(); }); });
139
- const wss = new WebSocketServer({ server: httpServer, path: '/ext' });
178
+ const wss = new WebSocketServer({
179
+ server: httpServer,
180
+ path: '/ext',
181
+ verifyClient: ({ req }: { req: IncomingMessage }) => {
182
+ // Block browser-originated WebSocket connections. Browsers don't
183
+ // enforce CORS on WebSocket, so a malicious webpage could connect to
184
+ // ws://localhost:19825/ext and impersonate the Extension. Real Chrome
185
+ // Extensions send origin chrome-extension://<id>.
186
+ const origin = req.headers['origin'] as string | undefined;
187
+ return !origin || origin.startsWith('chrome-extension://');
188
+ },
189
+ });
140
190
 
141
191
  wss.on('connection', (ws: WebSocket) => {
142
192
  console.error('[daemon] Extension connected');
@@ -84,36 +84,28 @@ describe('doctor report rendering', () => {
84
84
  issues: [],
85
85
  }));
86
86
 
87
- expect(text).toContain('[SKIP] Connectivity: not tested (use --live)');
87
+ expect(text).toContain('[SKIP] Connectivity: skipped (--no-live)');
88
88
  });
89
89
 
90
90
  it('reports consistent status when live check auto-starts the daemon', async () => {
91
- // With the reordered flow, checkDaemonStatus is called only ONCE after
92
- // the connectivity check that may auto-start the daemon.
93
- mockCheckDaemonStatus.mockResolvedValueOnce({ running: true, extensionConnected: false });
94
- mockConnect.mockRejectedValueOnce(new Error(
95
- 'Daemon is running but the Browser Extension is not connected.\n' +
96
- 'Please install and enable the opencli Browser Bridge extension in Chrome.',
97
- ));
98
-
99
- const report = await runBrowserDoctor({ live: true });
100
-
101
- // Status reflects the post-connectivity state (daemon running)
102
- expect(report.daemonRunning).toBe(true);
91
+ // checkDaemonStatus is called twice: once for auto-start check, once for final status.
92
+ // First call: daemon not running (triggers auto-start attempt)
93
+ mockCheckDaemonStatus.mockResolvedValueOnce({ running: false, extensionConnected: false });
94
+ // Auto-start attempt via BrowserBridge.connect fails
95
+ mockConnect.mockRejectedValueOnce(new Error('Could not start daemon'));
96
+ // Second call: daemon still not running after failed auto-start
97
+ mockCheckDaemonStatus.mockResolvedValueOnce({ running: false, extensionConnected: false });
98
+
99
+ const report = await runBrowserDoctor({ live: false });
100
+
101
+ // Status reflects daemon not running
102
+ expect(report.daemonRunning).toBe(false);
103
103
  expect(report.extensionConnected).toBe(false);
104
- // checkDaemonStatus should only be called once
105
- expect(mockCheckDaemonStatus).toHaveBeenCalledTimes(1);
106
- // Should NOT report "daemon not running" since it IS running after live check
107
- expect(report.issues).not.toContain(
108
- expect.stringContaining('Daemon is not running'),
109
- );
110
- // Should report extension not connected
111
- expect(report.issues).toEqual(expect.arrayContaining([
112
- expect.stringContaining('Chrome extension is not connected'),
113
- ]));
114
- // Should report connectivity failure
104
+ // checkDaemonStatus called twice (initial + final)
105
+ expect(mockCheckDaemonStatus).toHaveBeenCalledTimes(2);
106
+ // Should report daemon not running
115
107
  expect(report.issues).toEqual(expect.arrayContaining([
116
- expect.stringContaining('Browser connectivity test failed'),
108
+ expect.stringContaining('Daemon is not running'),
117
109
  ]));
118
110
  });
119
111
  });
package/src/doctor.ts CHANGED
@@ -51,7 +51,19 @@ export async function checkConnectivity(opts?: { timeout?: number }): Promise<Co
51
51
  }
52
52
 
53
53
  export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<DoctorReport> {
54
- // Run the live connectivity check first it may auto-start the daemon as a
54
+ // Try to auto-start daemon if it's not running, so we show accurate status.
55
+ let initialStatus = await checkDaemonStatus();
56
+ if (!initialStatus.running) {
57
+ try {
58
+ const mcp = new BrowserBridge();
59
+ await mcp.connect({ timeout: 5 });
60
+ await mcp.close();
61
+ } catch {
62
+ // Auto-start failed; we'll report it below.
63
+ }
64
+ }
65
+
66
+ // Run the live connectivity check — it may also auto-start the daemon as a
55
67
  // side-effect, so we read daemon status only *after* all side-effects settle.
56
68
  let connectivity: ConnectivityResult | undefined;
57
69
  if (opts.live) {
@@ -109,7 +121,7 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
109
121
  : `failed (${report.connectivity.error ?? 'unknown'})`;
110
122
  lines.push(`${connIcon} Connectivity: ${detail}`);
111
123
  } else {
112
- lines.push(`${chalk.dim('[SKIP]')} Connectivity: not tested (use --live)`);
124
+ lines.push(`${chalk.dim('[SKIP]')} Connectivity: skipped (--no-live)`);
113
125
  }
114
126
 
115
127
  if (report.sessions) {
package/dist/setup.d.ts DELETED
@@ -1,10 +0,0 @@
1
- /**
2
- * setup.ts — Interactive browser setup for opencli
3
- *
4
- * Simplified for daemon-based architecture. No more token management.
5
- * Just verifies daemon + extension connectivity.
6
- */
7
- export declare function runSetup(opts?: {
8
- cliVersion?: string;
9
- token?: string;
10
- }): Promise<void>;
package/dist/setup.js DELETED
@@ -1,66 +0,0 @@
1
- /**
2
- * setup.ts — Interactive browser setup for opencli
3
- *
4
- * Simplified for daemon-based architecture. No more token management.
5
- * Just verifies daemon + extension connectivity.
6
- */
7
- import chalk from 'chalk';
8
- import { checkDaemonStatus } from './browser/discover.js';
9
- import { checkConnectivity } from './doctor.js';
10
- import { BrowserBridge } from './browser/index.js';
11
- export async function runSetup(opts = {}) {
12
- console.log();
13
- console.log(chalk.bold(' opencli setup') + chalk.dim(' — browser bridge configuration'));
14
- console.log();
15
- // Step 1: Check daemon
16
- console.log(chalk.dim(' Checking daemon status...'));
17
- const status = await checkDaemonStatus();
18
- if (status.running) {
19
- console.log(` ${chalk.green('✓')} Daemon is running on port 19825`);
20
- }
21
- else {
22
- console.log(` ${chalk.yellow('!')} Daemon is not running`);
23
- console.log(chalk.dim(' The daemon starts automatically when you run a browser command.'));
24
- console.log(chalk.dim(' Starting daemon now...'));
25
- // Try to spawn daemon
26
- const mcp = new BrowserBridge();
27
- try {
28
- await mcp.connect({ timeout: 5 });
29
- await mcp.close();
30
- console.log(` ${chalk.green('✓')} Daemon started successfully`);
31
- }
32
- catch {
33
- console.log(` ${chalk.yellow('!')} Could not start daemon automatically`);
34
- }
35
- }
36
- // Step 2: Check extension
37
- const statusAfter = await checkDaemonStatus();
38
- if (statusAfter.extensionConnected) {
39
- console.log(` ${chalk.green('✓')} Chrome extension connected`);
40
- }
41
- else {
42
- console.log(` ${chalk.red('✗')} Chrome extension not connected`);
43
- console.log();
44
- console.log(chalk.dim(' To install the opencli Browser Bridge extension:'));
45
- console.log(chalk.dim(' 1. Download from GitHub Releases'));
46
- console.log(chalk.dim(' 2. Open chrome://extensions/ → Enable Developer Mode'));
47
- console.log(chalk.dim(' 3. Click "Load unpacked" → select the extension folder'));
48
- console.log(chalk.dim(' 4. Make sure Chrome is running'));
49
- console.log();
50
- return;
51
- }
52
- // Step 3: Test connectivity
53
- console.log();
54
- console.log(chalk.dim(' Testing browser connectivity...'));
55
- const conn = await checkConnectivity({ timeout: 5 });
56
- if (conn.ok) {
57
- console.log(` ${chalk.green('✓')} Browser connected in ${(conn.durationMs / 1000).toFixed(1)}s`);
58
- console.log();
59
- console.log(chalk.green.bold(' ✓ Setup complete! You can now use opencli browser commands.'));
60
- }
61
- else {
62
- console.log(` ${chalk.yellow('!')} Connectivity test failed: ${conn.error ?? 'unknown'}`);
63
- console.log(chalk.dim(` Run ${chalk.bold('opencli doctor --live')} to diagnose.`));
64
- }
65
- console.log();
66
- }
package/src/setup.ts DELETED
@@ -1,69 +0,0 @@
1
- /**
2
- * setup.ts — Interactive browser setup for opencli
3
- *
4
- * Simplified for daemon-based architecture. No more token management.
5
- * Just verifies daemon + extension connectivity.
6
- */
7
-
8
- import chalk from 'chalk';
9
- import { checkDaemonStatus } from './browser/discover.js';
10
- import { checkConnectivity } from './doctor.js';
11
- import { BrowserBridge } from './browser/index.js';
12
-
13
- export async function runSetup(opts: { cliVersion?: string; token?: string } = {}) {
14
- console.log();
15
- console.log(chalk.bold(' opencli setup') + chalk.dim(' — browser bridge configuration'));
16
- console.log();
17
-
18
- // Step 1: Check daemon
19
- console.log(chalk.dim(' Checking daemon status...'));
20
- const status = await checkDaemonStatus();
21
-
22
- if (status.running) {
23
- console.log(` ${chalk.green('✓')} Daemon is running on port 19825`);
24
- } else {
25
- console.log(` ${chalk.yellow('!')} Daemon is not running`);
26
- console.log(chalk.dim(' The daemon starts automatically when you run a browser command.'));
27
- console.log(chalk.dim(' Starting daemon now...'));
28
-
29
- // Try to spawn daemon
30
- const mcp = new BrowserBridge();
31
- try {
32
- await mcp.connect({ timeout: 5 });
33
- await mcp.close();
34
- console.log(` ${chalk.green('✓')} Daemon started successfully`);
35
- } catch {
36
- console.log(` ${chalk.yellow('!')} Could not start daemon automatically`);
37
- }
38
- }
39
-
40
- // Step 2: Check extension
41
- const statusAfter = await checkDaemonStatus();
42
- if (statusAfter.extensionConnected) {
43
- console.log(` ${chalk.green('✓')} Chrome extension connected`);
44
- } else {
45
- console.log(` ${chalk.red('✗')} Chrome extension not connected`);
46
- console.log();
47
- console.log(chalk.dim(' To install the opencli Browser Bridge extension:'));
48
- console.log(chalk.dim(' 1. Download from GitHub Releases'));
49
- console.log(chalk.dim(' 2. Open chrome://extensions/ → Enable Developer Mode'));
50
- console.log(chalk.dim(' 3. Click "Load unpacked" → select the extension folder'));
51
- console.log(chalk.dim(' 4. Make sure Chrome is running'));
52
- console.log();
53
- return;
54
- }
55
-
56
- // Step 3: Test connectivity
57
- console.log();
58
- console.log(chalk.dim(' Testing browser connectivity...'));
59
- const conn = await checkConnectivity({ timeout: 5 });
60
- if (conn.ok) {
61
- console.log(` ${chalk.green('✓')} Browser connected in ${(conn.durationMs / 1000).toFixed(1)}s`);
62
- console.log();
63
- console.log(chalk.green.bold(' ✓ Setup complete! You can now use opencli browser commands.'));
64
- } else {
65
- console.log(` ${chalk.yellow('!')} Connectivity test failed: ${conn.error ?? 'unknown'}`);
66
- console.log(chalk.dim(` Run ${chalk.bold('opencli doctor --live')} to diagnose.`));
67
- }
68
- console.log();
69
- }