clementine-agent 1.6.0 → 1.6.2

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.
@@ -20,5 +20,17 @@
20
20
  export declare function cmdBrowserStatus(): Promise<void>;
21
21
  export declare function cmdBrowserInstall(): Promise<void>;
22
22
  export declare function cmdBrowserEnable(): Promise<void>;
23
+ /**
24
+ * Auto-prompt during `clementine update`. Stays silent unless there's
25
+ * something actionable — mirrors the keychain wizard's behavior.
26
+ *
27
+ * Skips prompting when:
28
+ * - Not in an interactive TTY
29
+ * - The MCP wrapper isn't shipped with this version
30
+ * - Browser harness is already installed AND enabled
31
+ * - User previously dismissed the prompt
32
+ */
33
+ export declare function maybePromptBrowserHarness(): Promise<void>;
23
34
  export declare function cmdBrowserDisable(): Promise<void>;
35
+ export declare function cmdBrowserConnect(): Promise<void>;
24
36
  //# sourceMappingURL=browser.d.ts.map
@@ -19,9 +19,11 @@
19
19
  */
20
20
  import { execSync, spawnSync } from 'node:child_process';
21
21
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
22
+ import http from 'node:http';
22
23
  import os from 'node:os';
23
24
  import path from 'node:path';
24
25
  import { fileURLToPath } from 'node:url';
26
+ import { confirm } from '@inquirer/prompts';
25
27
  const BOLD = '\x1b[1m';
26
28
  const DIM = '\x1b[0;90m';
27
29
  const GREEN = '\x1b[0;32m';
@@ -40,10 +42,36 @@ const VENV_PYTHON = path.join(VENV_DIR, 'bin', 'python3');
40
42
  const MCP_SERVERS_FILE = path.join(BASE_DIR, 'mcp-servers.json');
41
43
  const HARNESS_REPO = 'https://github.com/browser-use/browser-harness.git';
42
44
  const SERVER_NAME = 'browser-harness';
45
+ const DISMISSED_MARKER = path.join(BASE_DIR, '.browser-harness-dismissed');
43
46
  function commandExists(cmd) {
44
47
  const result = spawnSync('which', [cmd], { stdio: 'pipe' });
45
48
  return result.status === 0;
46
49
  }
50
+ /** Probe the CDP socket — returns true if Chrome is listening on :9222. */
51
+ function probeCdp() {
52
+ return new Promise(resolve => {
53
+ const req = http.get('http://localhost:9222/json/version', { timeout: 1500 }, res => {
54
+ resolve(res.statusCode === 200);
55
+ res.resume();
56
+ });
57
+ req.on('error', () => resolve(false));
58
+ req.on('timeout', () => { req.destroy(); resolve(false); });
59
+ });
60
+ }
61
+ /** True if a Google Chrome process is currently running. */
62
+ function isChromeRunning() {
63
+ if (process.platform === 'darwin') {
64
+ const r = spawnSync('pgrep', ['-x', 'Google Chrome'], { stdio: 'pipe' });
65
+ return r.status === 0;
66
+ }
67
+ // Linux: chrome / chromium / google-chrome
68
+ for (const name of ['google-chrome', 'chromium', 'chrome']) {
69
+ const r = spawnSync('pgrep', ['-x', name], { stdio: 'pipe' });
70
+ if (r.status === 0)
71
+ return true;
72
+ }
73
+ return false;
74
+ }
47
75
  function pythonVersion() {
48
76
  try {
49
77
  const out = execSync('python3 --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
@@ -75,6 +103,7 @@ export async function cmdBrowserStatus() {
75
103
  const harnessOk = existsSync(path.join(HARNESS_HOME, 'src'));
76
104
  const servers = loadMcpServers();
77
105
  const enabled = Object.prototype.hasOwnProperty.call(servers, SERVER_NAME);
106
+ const cdpOk = await probeCdp();
78
107
  console.log();
79
108
  console.log(` ${BOLD}Browser Harness${RESET} ${DIM}(beta)${RESET}`);
80
109
  console.log();
@@ -83,6 +112,7 @@ export async function cmdBrowserStatus() {
83
112
  console.log(` ${venvOk ? GREEN + '✓' : YELLOW + '○'}${RESET} venv installed ${DIM}${VENV_DIR}${RESET}`);
84
113
  console.log(` ${harnessOk ? GREEN + '✓' : YELLOW + '○'}${RESET} harness cloned ${DIM}${HARNESS_HOME}${RESET}`);
85
114
  console.log(` ${enabled ? GREEN + '✓' : DIM + '○'}${RESET} MCP entry ${DIM}${enabled ? 'enabled' : 'disabled'} in mcp-servers.json${RESET}`);
115
+ console.log(` ${cdpOk ? GREEN + '✓' : YELLOW + '○'}${RESET} Chrome CDP ${DIM}${cdpOk ? 'connected on :9222' : 'not connected — run: clementine browser connect'}${RESET}`);
86
116
  console.log();
87
117
  if (!py) {
88
118
  console.log(` ${YELLOW}Install Python 3.10+ first:${RESET}`);
@@ -97,12 +127,20 @@ export async function cmdBrowserStatus() {
97
127
  console.log(` Next: ${BOLD}clementine browser enable${RESET}`);
98
128
  console.log();
99
129
  }
130
+ else if (!cdpOk) {
131
+ console.log(` Next: ${BOLD}clementine browser connect${RESET}`);
132
+ console.log();
133
+ }
100
134
  else {
101
- console.log(` ${GREEN}Ready.${RESET} ${DIM}Restart the daemon to pick up changes: clementine restart${RESET}`);
135
+ console.log(` ${GREEN}Ready.${RESET} ${DIM}Browser harness is fully connected.${RESET}`);
102
136
  console.log();
103
137
  }
104
138
  }
105
- export async function cmdBrowserInstall() {
139
+ /**
140
+ * Core install logic. Returns true on success, false on any failure.
141
+ * Prints progress + errors to stdout/stderr but never calls process.exit.
142
+ */
143
+ async function runInstall() {
106
144
  console.log();
107
145
  console.log(` ${BOLD}Installing browser-harness${RESET} ${DIM}(beta)${RESET}`);
108
146
  console.log();
@@ -110,13 +148,13 @@ export async function cmdBrowserInstall() {
110
148
  console.error(` ${RED}python3 not found.${RESET} Install Python 3.10+ first:`);
111
149
  console.error(` ${CYAN}brew install python@3.12${RESET}`);
112
150
  console.error();
113
- process.exit(1);
151
+ return false;
114
152
  }
115
153
  if (!existsSync(MCP_SCRIPT)) {
116
154
  console.error(` ${RED}MCP wrapper not found at:${RESET} ${MCP_SCRIPT}`);
117
155
  console.error(` ${DIM}This means the package was installed without vendor/ files. Reinstall:${RESET}`);
118
156
  console.error(` ${CYAN}npm install -g clementine-agent@latest${RESET}`);
119
- process.exit(1);
157
+ return false;
120
158
  }
121
159
  if (!existsSync(BASE_DIR))
122
160
  mkdirSync(BASE_DIR, { recursive: true });
@@ -124,7 +162,7 @@ export async function cmdBrowserInstall() {
124
162
  if (!existsSync(HARNESS_HOME)) {
125
163
  if (!commandExists('git')) {
126
164
  console.error(` ${RED}git not found.${RESET} Install git, then re-run.`);
127
- process.exit(1);
165
+ return false;
128
166
  }
129
167
  console.log(` ${DIM}→ cloning ${HARNESS_REPO}${RESET}`);
130
168
  try {
@@ -132,7 +170,7 @@ export async function cmdBrowserInstall() {
132
170
  }
133
171
  catch {
134
172
  console.error(` ${RED}Clone failed.${RESET} Check network / git access and try again.`);
135
- process.exit(1);
173
+ return false;
136
174
  }
137
175
  }
138
176
  else {
@@ -146,7 +184,7 @@ export async function cmdBrowserInstall() {
146
184
  }
147
185
  catch {
148
186
  console.error(` ${RED}venv creation failed.${RESET}`);
149
- process.exit(1);
187
+ return false;
150
188
  }
151
189
  }
152
190
  else {
@@ -165,25 +203,21 @@ export async function cmdBrowserInstall() {
165
203
  }
166
204
  catch {
167
205
  console.error(` ${RED}pip install failed.${RESET} Inspect output above and re-run when fixed.`);
168
- process.exit(1);
206
+ return false;
169
207
  }
170
208
  console.log();
171
209
  console.log(` ${GREEN}✓${RESET} Install complete.`);
172
- console.log();
173
- console.log(` ${BOLD}Next steps:${RESET}`);
174
- console.log(` 1. Enable Chrome remote debugging — open Chrome with:`);
175
- console.log(` ${CYAN}/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome \\${RESET}`);
176
- console.log(` ${CYAN}--remote-debugging-port=9222${RESET}`);
177
- console.log(` 2. Enable the MCP server: ${BOLD}clementine browser enable${RESET}`);
178
- console.log(` 3. Restart the daemon: ${BOLD}clementine restart${RESET}`);
179
- console.log();
210
+ return true;
180
211
  }
181
- export async function cmdBrowserEnable() {
212
+ /**
213
+ * Core enable logic. Returns true on success, false on any failure.
214
+ */
215
+ function runEnable() {
182
216
  if (!existsSync(VENV_PYTHON) || !existsSync(MCP_SCRIPT)) {
183
217
  console.error();
184
218
  console.error(` ${RED}Not installed yet.${RESET} Run ${BOLD}clementine browser install${RESET} first.`);
185
219
  console.error();
186
- process.exit(1);
220
+ return false;
187
221
  }
188
222
  const servers = loadMcpServers();
189
223
  servers[SERVER_NAME] = {
@@ -201,9 +235,107 @@ export async function cmdBrowserEnable() {
201
235
  saveMcpServers(servers);
202
236
  console.log();
203
237
  console.log(` ${GREEN}✓${RESET} Registered ${BOLD}${SERVER_NAME}${RESET} in mcp-servers.json`);
238
+ return true;
239
+ }
240
+ export async function cmdBrowserInstall() {
241
+ const ok = await runInstall();
242
+ if (!ok)
243
+ process.exit(1);
244
+ console.log();
245
+ console.log(` ${BOLD}Next:${RESET} ${BOLD}clementine browser enable${RESET} — register the MCP server`);
246
+ console.log(` ${DIM}Then connect Chrome with: clementine browser connect${RESET}`);
247
+ console.log();
248
+ }
249
+ export async function cmdBrowserEnable() {
250
+ const ok = runEnable();
251
+ if (!ok)
252
+ process.exit(1);
204
253
  console.log(` ${DIM}Restart the daemon to pick up the change: clementine restart${RESET}`);
205
254
  console.log();
206
255
  }
256
+ /**
257
+ * Auto-prompt during `clementine update`. Stays silent unless there's
258
+ * something actionable — mirrors the keychain wizard's behavior.
259
+ *
260
+ * Skips prompting when:
261
+ * - Not in an interactive TTY
262
+ * - The MCP wrapper isn't shipped with this version
263
+ * - Browser harness is already installed AND enabled
264
+ * - User previously dismissed the prompt
265
+ */
266
+ export async function maybePromptBrowserHarness() {
267
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
268
+ return;
269
+ if (!existsSync(MCP_SCRIPT))
270
+ return;
271
+ const servers = loadMcpServers();
272
+ const enabled = Object.prototype.hasOwnProperty.call(servers, SERVER_NAME);
273
+ const installed = existsSync(VENV_PYTHON);
274
+ if (enabled && installed)
275
+ return;
276
+ if (existsSync(DISMISSED_MARKER))
277
+ return;
278
+ console.log();
279
+ console.log(` ${BOLD}Browser Harness available${RESET} ${DIM}(beta, opt-in)${RESET}`);
280
+ console.log(` ${DIM}Lets Clementine drive your real Chrome — fill forms, post on LinkedIn,${RESET}`);
281
+ console.log(` ${DIM}book appointments — using your live browser session.${RESET}`);
282
+ console.log();
283
+ let answer;
284
+ try {
285
+ answer = await confirm({
286
+ message: 'Install Browser Harness now?',
287
+ default: false,
288
+ });
289
+ }
290
+ catch {
291
+ // User Ctrl+C'd or terminal closed — treat as decline, don't dismiss permanently
292
+ return;
293
+ }
294
+ if (!answer) {
295
+ try {
296
+ writeFileSync(DISMISSED_MARKER, new Date().toISOString() + '\n');
297
+ }
298
+ catch { /* non-fatal */ }
299
+ console.log(` ${DIM}Skipped. To install later: clementine browser install${RESET}`);
300
+ console.log();
301
+ return;
302
+ }
303
+ // User said yes — run install + enable inline
304
+ const installOk = await runInstall();
305
+ if (!installOk) {
306
+ console.error(` ${YELLOW}Install failed.${RESET} ${DIM}You can retry with: clementine browser install${RESET}`);
307
+ console.log();
308
+ return;
309
+ }
310
+ const enableOk = runEnable();
311
+ if (!enableOk) {
312
+ console.error(` ${YELLOW}Enable failed.${RESET} ${DIM}Retry with: clementine browser enable${RESET}`);
313
+ console.log();
314
+ return;
315
+ }
316
+ console.log();
317
+ console.log(` ${GREEN}✓${RESET} Browser Harness installed and enabled.`);
318
+ console.log();
319
+ // Offer to connect Chrome right now
320
+ let connectNow = false;
321
+ try {
322
+ connectNow = await confirm({
323
+ message: 'Connect Chrome now? (relaunches Chrome with debugging — will close current windows)',
324
+ default: false,
325
+ });
326
+ }
327
+ catch {
328
+ // Ctrl+C — bail without dismissing
329
+ return;
330
+ }
331
+ if (connectNow) {
332
+ await runConnect({ confirmQuit: false });
333
+ }
334
+ else {
335
+ console.log(` ${DIM}Connect later with: ${BOLD}clementine browser connect${RESET}`);
336
+ console.log();
337
+ }
338
+ }
207
339
  export async function cmdBrowserDisable() {
208
340
  const servers = loadMcpServers();
209
341
  if (!Object.prototype.hasOwnProperty.call(servers, SERVER_NAME)) {
@@ -221,4 +353,121 @@ export async function cmdBrowserDisable() {
221
353
  console.log(` ${DIM}Restart the daemon: clementine restart${RESET}`);
222
354
  console.log();
223
355
  }
356
+ /**
357
+ * Core connect logic — quits any running Chrome and relaunches with
358
+ * --remote-debugging-port=9222 so browser-harness can connect.
359
+ *
360
+ * Returns true when CDP is reachable on :9222 at the end, false otherwise.
361
+ * Never calls process.exit so it's safe to call from the auto-prompt flow.
362
+ */
363
+ async function runConnect(opts = {}) {
364
+ // 1. Already connected? Done.
365
+ if (await probeCdp()) {
366
+ console.log();
367
+ console.log(` ${GREEN}✓${RESET} Already connected — Chrome is running with remote debugging on :9222`);
368
+ console.log();
369
+ return true;
370
+ }
371
+ // 2. Platform check — auto-launch is currently macOS only
372
+ if (process.platform !== 'darwin' && process.platform !== 'linux') {
373
+ console.error();
374
+ console.error(` ${YELLOW}Auto-connect is only supported on macOS and Linux.${RESET}`);
375
+ console.error(` Launch Chrome manually with the flag: ${BOLD}--remote-debugging-port=9222${RESET}`);
376
+ console.error();
377
+ return false;
378
+ }
379
+ // 3. Chrome already running without the flag? Need to quit first.
380
+ if (isChromeRunning()) {
381
+ console.log();
382
+ console.log(` ${YELLOW}Chrome is running, but without remote debugging.${RESET}`);
383
+ console.log(` ${DIM}To connect, Chrome needs to be quit and relaunched with the flag.${RESET}`);
384
+ console.log(` ${DIM}This will close your current Chrome windows.${RESET}`);
385
+ console.log();
386
+ let confirmed = !opts.confirmQuit; // skip prompt when caller already confirmed
387
+ if (opts.confirmQuit) {
388
+ try {
389
+ confirmed = await confirm({
390
+ message: 'Quit Chrome and relaunch with debugging?',
391
+ default: false,
392
+ });
393
+ }
394
+ catch {
395
+ return false;
396
+ }
397
+ }
398
+ if (!confirmed) {
399
+ console.log(` ${DIM}Skipped. To do it yourself: quit Chrome (Cmd+Q), then run:${RESET}`);
400
+ console.log(` ${BOLD}clementine browser connect${RESET}`);
401
+ console.log();
402
+ return false;
403
+ }
404
+ console.log(` ${DIM}→ quitting Chrome...${RESET}`);
405
+ try {
406
+ if (process.platform === 'darwin') {
407
+ execSync('osascript -e \'tell application "Google Chrome" to quit\'', { stdio: 'pipe' });
408
+ }
409
+ else {
410
+ // Linux: graceful TERM, then KILL if needed
411
+ try {
412
+ execSync('pkill -TERM -x "google-chrome|chromium|chrome"', { stdio: 'pipe' });
413
+ }
414
+ catch { /* ok */ }
415
+ }
416
+ // Wait briefly for Chrome to actually exit
417
+ for (let i = 0; i < 15; i++) {
418
+ if (!isChromeRunning())
419
+ break;
420
+ await new Promise(r => setTimeout(r, 300));
421
+ }
422
+ }
423
+ catch {
424
+ console.error(` ${RED}Failed to quit Chrome.${RESET} Please quit it manually and re-run.`);
425
+ return false;
426
+ }
427
+ }
428
+ // 4. Launch Chrome with the debugging flag
429
+ console.log(` ${DIM}→ launching Chrome with --remote-debugging-port=9222${RESET}`);
430
+ try {
431
+ if (process.platform === 'darwin') {
432
+ execSync('open -na "Google Chrome" --args --remote-debugging-port=9222', { stdio: 'pipe' });
433
+ }
434
+ else {
435
+ // Linux — find a chrome binary in PATH
436
+ const candidates = ['google-chrome', 'chromium', 'chrome'];
437
+ const bin = candidates.find(commandExists);
438
+ if (!bin) {
439
+ console.error(` ${RED}No Chrome / Chromium binary found in PATH.${RESET}`);
440
+ return false;
441
+ }
442
+ // Launch detached so this command returns immediately
443
+ execSync(`nohup ${bin} --remote-debugging-port=9222 >/dev/null 2>&1 &`, { stdio: 'pipe' });
444
+ }
445
+ }
446
+ catch (e) {
447
+ console.error(` ${RED}Failed to launch Chrome:${RESET} ${String(e).slice(0, 200)}`);
448
+ return false;
449
+ }
450
+ // 5. Poll for CDP availability (up to ~6s)
451
+ for (let i = 0; i < 24; i++) {
452
+ await new Promise(r => setTimeout(r, 250));
453
+ if (await probeCdp()) {
454
+ console.log();
455
+ console.log(` ${GREEN}✓${RESET} Connected — Chrome is running with remote debugging on :9222`);
456
+ console.log(` ${DIM}Browser harness can now control your live session.${RESET}`);
457
+ console.log();
458
+ return true;
459
+ }
460
+ }
461
+ console.error();
462
+ console.error(` ${YELLOW}Chrome launched, but CDP socket isn't responding yet.${RESET}`);
463
+ console.error(` ${DIM}Check that Chrome started, then verify with:${RESET}`);
464
+ console.error(` ${CYAN}curl http://localhost:9222/json/version${RESET}`);
465
+ console.error();
466
+ return false;
467
+ }
468
+ export async function cmdBrowserConnect() {
469
+ const ok = await runConnect({ confirmQuit: true });
470
+ if (!ok)
471
+ process.exit(1);
472
+ }
224
473
  //# sourceMappingURL=browser.js.map
package/dist/cli/index.js CHANGED
@@ -24,7 +24,7 @@ import { cmdCronList, cmdCronRun, cmdCronRunDue, cmdCronRuns, cmdCronAdd, cmdCro
24
24
  import { cmdDashboard } from './dashboard.js';
25
25
  import { cmdChat } from './chat.js';
26
26
  import { cmdIngestSeed, cmdIngestRun, cmdIngestList, cmdIngestStatus } from './ingest.js';
27
- import { cmdBrowserStatus, cmdBrowserInstall, cmdBrowserEnable, cmdBrowserDisable } from './browser.js';
27
+ import { cmdBrowserStatus, cmdBrowserInstall, cmdBrowserEnable, cmdBrowserDisable, cmdBrowserConnect, maybePromptBrowserHarness } from './browser.js';
28
28
  import { isSensitiveEnvKey } from '../secrets/sensitivity.js';
29
29
  const __filename = fileURLToPath(import.meta.url);
30
30
  const __dirname = path.dirname(__filename);
@@ -3171,6 +3171,8 @@ async function cmdUpdate(options) {
3171
3171
  console.log(` ${DIM}Restart your daemon to pick up the new code:${RESET}`);
3172
3172
  console.log(` clementine restart`);
3173
3173
  }
3174
+ // Surface new opt-in integrations (silent unless action needed)
3175
+ await maybePromptBrowserHarness();
3174
3176
  return;
3175
3177
  }
3176
3178
  let step = 0;
@@ -3830,6 +3832,8 @@ async function cmdUpdate(options) {
3830
3832
  }
3831
3833
  console.log(` ${DIM}Config backup: ${backupDir}${RESET}`);
3832
3834
  console.log();
3835
+ // Surface new opt-in integrations (silent unless action needed)
3836
+ await maybePromptBrowserHarness();
3833
3837
  }
3834
3838
  // ── Cron commands ───────────────────────────────────────────────────
3835
3839
  const cronCmd = program
@@ -4566,6 +4570,10 @@ browserCmd
4566
4570
  .command('enable')
4567
4571
  .description('Register the browser harness MCP server in mcp-servers.json')
4568
4572
  .action(cmdBrowserEnable);
4573
+ browserCmd
4574
+ .command('connect')
4575
+ .description('Quit Chrome and relaunch it with --remote-debugging-port=9222 (one-shot)')
4576
+ .action(cmdBrowserConnect);
4569
4577
  browserCmd
4570
4578
  .command('disable')
4571
4579
  .description('Remove the browser harness MCP entry (keeps installed files)')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",