clementine-agent 1.5.1 → 1.6.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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Clementine CLI — browser-harness integration (Phase 1, beta).
3
+ *
4
+ * Subcommands:
5
+ * clementine browser status — show install / enable / CDP state
6
+ * clementine browser install — set up Python venv + clone harness
7
+ * clementine browser enable — register MCP server in mcp-servers.json
8
+ * clementine browser disable — remove the MCP entry (keeps files)
9
+ *
10
+ * Safety:
11
+ * - All subcommands fail soft. Missing Python or unsupported OS just prints
12
+ * a clear message and exits 0 (or 1 for hard errors); the daemon never
13
+ * auto-installs anything.
14
+ * - Enabling only writes a single entry to ~/.clementine/mcp-servers.json
15
+ * that mcp-bridge.ts already understands. No changes to assistant.ts.
16
+ * - If Python or the MCP server fail at runtime, the SDK logs the error and
17
+ * the rest of Clementine keeps running (every other MCP server is
18
+ * unaffected).
19
+ */
20
+ export declare function cmdBrowserStatus(): Promise<void>;
21
+ export declare function cmdBrowserInstall(): Promise<void>;
22
+ export declare function cmdBrowserEnable(): Promise<void>;
23
+ export declare function cmdBrowserDisable(): Promise<void>;
24
+ //# sourceMappingURL=browser.d.ts.map
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Clementine CLI — browser-harness integration (Phase 1, beta).
3
+ *
4
+ * Subcommands:
5
+ * clementine browser status — show install / enable / CDP state
6
+ * clementine browser install — set up Python venv + clone harness
7
+ * clementine browser enable — register MCP server in mcp-servers.json
8
+ * clementine browser disable — remove the MCP entry (keeps files)
9
+ *
10
+ * Safety:
11
+ * - All subcommands fail soft. Missing Python or unsupported OS just prints
12
+ * a clear message and exits 0 (or 1 for hard errors); the daemon never
13
+ * auto-installs anything.
14
+ * - Enabling only writes a single entry to ~/.clementine/mcp-servers.json
15
+ * that mcp-bridge.ts already understands. No changes to assistant.ts.
16
+ * - If Python or the MCP server fail at runtime, the SDK logs the error and
17
+ * the rest of Clementine keeps running (every other MCP server is
18
+ * unaffected).
19
+ */
20
+ import { execSync, spawnSync } from 'node:child_process';
21
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
22
+ import os from 'node:os';
23
+ import path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+ const BOLD = '\x1b[1m';
26
+ const DIM = '\x1b[0;90m';
27
+ const GREEN = '\x1b[0;32m';
28
+ const YELLOW = '\x1b[1;33m';
29
+ const RED = '\x1b[0;31m';
30
+ const CYAN = '\x1b[0;36m';
31
+ const RESET = '\x1b[0m';
32
+ const __filename = fileURLToPath(import.meta.url);
33
+ const __dirname = path.dirname(__filename);
34
+ const BASE_DIR = process.env.CLEMENTINE_HOME || path.join(os.homedir(), '.clementine');
35
+ const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
36
+ const MCP_SCRIPT = path.join(PACKAGE_ROOT, 'vendor', 'browser-harness-mcp', 'server.py');
37
+ const HARNESS_HOME = path.join(BASE_DIR, 'browser-harness');
38
+ const VENV_DIR = path.join(BASE_DIR, 'browser-harness-mcp-venv');
39
+ const VENV_PYTHON = path.join(VENV_DIR, 'bin', 'python3');
40
+ const MCP_SERVERS_FILE = path.join(BASE_DIR, 'mcp-servers.json');
41
+ const HARNESS_REPO = 'https://github.com/browser-use/browser-harness.git';
42
+ const SERVER_NAME = 'browser-harness';
43
+ function commandExists(cmd) {
44
+ const result = spawnSync('which', [cmd], { stdio: 'pipe' });
45
+ return result.status === 0;
46
+ }
47
+ function pythonVersion() {
48
+ try {
49
+ const out = execSync('python3 --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
50
+ return out.trim();
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ function loadMcpServers() {
57
+ if (!existsSync(MCP_SERVERS_FILE))
58
+ return {};
59
+ try {
60
+ return JSON.parse(readFileSync(MCP_SERVERS_FILE, 'utf-8'));
61
+ }
62
+ catch {
63
+ return {};
64
+ }
65
+ }
66
+ function saveMcpServers(servers) {
67
+ if (!existsSync(BASE_DIR))
68
+ mkdirSync(BASE_DIR, { recursive: true });
69
+ writeFileSync(MCP_SERVERS_FILE, JSON.stringify(servers, null, 2) + '\n');
70
+ }
71
+ export async function cmdBrowserStatus() {
72
+ const py = pythonVersion();
73
+ const mcpScriptOk = existsSync(MCP_SCRIPT);
74
+ const venvOk = existsSync(VENV_PYTHON);
75
+ const harnessOk = existsSync(path.join(HARNESS_HOME, 'src'));
76
+ const servers = loadMcpServers();
77
+ const enabled = Object.prototype.hasOwnProperty.call(servers, SERVER_NAME);
78
+ console.log();
79
+ console.log(` ${BOLD}Browser Harness${RESET} ${DIM}(beta)${RESET}`);
80
+ console.log();
81
+ console.log(` ${py ? GREEN + '✓' : RED + '✗'}${RESET} python3 ${DIM}${py ?? 'not found'}${RESET}`);
82
+ console.log(` ${mcpScriptOk ? GREEN + '✓' : RED + '✗'}${RESET} MCP wrapper ${DIM}${MCP_SCRIPT}${RESET}`);
83
+ console.log(` ${venvOk ? GREEN + '✓' : YELLOW + '○'}${RESET} venv installed ${DIM}${VENV_DIR}${RESET}`);
84
+ console.log(` ${harnessOk ? GREEN + '✓' : YELLOW + '○'}${RESET} harness cloned ${DIM}${HARNESS_HOME}${RESET}`);
85
+ console.log(` ${enabled ? GREEN + '✓' : DIM + '○'}${RESET} MCP entry ${DIM}${enabled ? 'enabled' : 'disabled'} in mcp-servers.json${RESET}`);
86
+ console.log();
87
+ if (!py) {
88
+ console.log(` ${YELLOW}Install Python 3.10+ first:${RESET}`);
89
+ console.log(` ${CYAN}brew install python@3.12${RESET}`);
90
+ console.log();
91
+ }
92
+ else if (!venvOk || !harnessOk) {
93
+ console.log(` Next: ${BOLD}clementine browser install${RESET}`);
94
+ console.log();
95
+ }
96
+ else if (!enabled) {
97
+ console.log(` Next: ${BOLD}clementine browser enable${RESET}`);
98
+ console.log();
99
+ }
100
+ else {
101
+ console.log(` ${GREEN}Ready.${RESET} ${DIM}Restart the daemon to pick up changes: clementine restart${RESET}`);
102
+ console.log();
103
+ }
104
+ }
105
+ export async function cmdBrowserInstall() {
106
+ console.log();
107
+ console.log(` ${BOLD}Installing browser-harness${RESET} ${DIM}(beta)${RESET}`);
108
+ console.log();
109
+ if (!pythonVersion()) {
110
+ console.error(` ${RED}python3 not found.${RESET} Install Python 3.10+ first:`);
111
+ console.error(` ${CYAN}brew install python@3.12${RESET}`);
112
+ console.error();
113
+ process.exit(1);
114
+ }
115
+ if (!existsSync(MCP_SCRIPT)) {
116
+ console.error(` ${RED}MCP wrapper not found at:${RESET} ${MCP_SCRIPT}`);
117
+ console.error(` ${DIM}This means the package was installed without vendor/ files. Reinstall:${RESET}`);
118
+ console.error(` ${CYAN}npm install -g clementine-agent@latest${RESET}`);
119
+ process.exit(1);
120
+ }
121
+ if (!existsSync(BASE_DIR))
122
+ mkdirSync(BASE_DIR, { recursive: true });
123
+ // Step 1: clone the harness if missing
124
+ if (!existsSync(HARNESS_HOME)) {
125
+ if (!commandExists('git')) {
126
+ console.error(` ${RED}git not found.${RESET} Install git, then re-run.`);
127
+ process.exit(1);
128
+ }
129
+ console.log(` ${DIM}→ cloning ${HARNESS_REPO}${RESET}`);
130
+ try {
131
+ execSync(`git clone --depth 1 ${HARNESS_REPO} "${HARNESS_HOME}"`, { stdio: 'inherit' });
132
+ }
133
+ catch {
134
+ console.error(` ${RED}Clone failed.${RESET} Check network / git access and try again.`);
135
+ process.exit(1);
136
+ }
137
+ }
138
+ else {
139
+ console.log(` ${GREEN}✓${RESET} harness already cloned at ${DIM}${HARNESS_HOME}${RESET}`);
140
+ }
141
+ // Step 2: create venv if missing
142
+ if (!existsSync(VENV_PYTHON)) {
143
+ console.log(` ${DIM}→ creating venv at ${VENV_DIR}${RESET}`);
144
+ try {
145
+ execSync(`python3 -m venv "${VENV_DIR}"`, { stdio: 'inherit' });
146
+ }
147
+ catch {
148
+ console.error(` ${RED}venv creation failed.${RESET}`);
149
+ process.exit(1);
150
+ }
151
+ }
152
+ else {
153
+ console.log(` ${GREEN}✓${RESET} venv already exists`);
154
+ }
155
+ // Step 3: install MCP deps + harness deps
156
+ console.log(` ${DIM}→ installing python deps (mcp, websockets, browser-harness)${RESET}`);
157
+ try {
158
+ execSync(`"${VENV_PYTHON}" -m pip install --upgrade pip --quiet`, { stdio: 'inherit' });
159
+ execSync(`"${VENV_PYTHON}" -m pip install --quiet "mcp>=1.0.0" "websockets>=12.0"`, { stdio: 'inherit' });
160
+ // Install harness deps from its pyproject.toml if present
161
+ const harnessPyproject = path.join(HARNESS_HOME, 'pyproject.toml');
162
+ if (existsSync(harnessPyproject)) {
163
+ execSync(`"${VENV_PYTHON}" -m pip install --quiet -e "${HARNESS_HOME}"`, { stdio: 'inherit' });
164
+ }
165
+ }
166
+ catch {
167
+ console.error(` ${RED}pip install failed.${RESET} Inspect output above and re-run when fixed.`);
168
+ process.exit(1);
169
+ }
170
+ console.log();
171
+ 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();
180
+ }
181
+ export async function cmdBrowserEnable() {
182
+ if (!existsSync(VENV_PYTHON) || !existsSync(MCP_SCRIPT)) {
183
+ console.error();
184
+ console.error(` ${RED}Not installed yet.${RESET} Run ${BOLD}clementine browser install${RESET} first.`);
185
+ console.error();
186
+ process.exit(1);
187
+ }
188
+ const servers = loadMcpServers();
189
+ servers[SERVER_NAME] = {
190
+ type: 'stdio',
191
+ command: VENV_PYTHON,
192
+ args: [MCP_SCRIPT],
193
+ env: {
194
+ BROWSER_HARNESS_HOME: HARNESS_HOME,
195
+ BROWSER_CDP_URL: process.env.BROWSER_CDP_URL || 'ws://localhost:9222',
196
+ },
197
+ description: 'Drive the user\'s real Chrome via CDP (browser-use/browser-harness)',
198
+ enabled: true,
199
+ source: 'user',
200
+ };
201
+ saveMcpServers(servers);
202
+ console.log();
203
+ console.log(` ${GREEN}✓${RESET} Registered ${BOLD}${SERVER_NAME}${RESET} in mcp-servers.json`);
204
+ console.log(` ${DIM}Restart the daemon to pick up the change: clementine restart${RESET}`);
205
+ console.log();
206
+ }
207
+ export async function cmdBrowserDisable() {
208
+ const servers = loadMcpServers();
209
+ if (!Object.prototype.hasOwnProperty.call(servers, SERVER_NAME)) {
210
+ console.log();
211
+ console.log(` ${DIM}${SERVER_NAME} is already disabled.${RESET}`);
212
+ console.log();
213
+ return;
214
+ }
215
+ delete servers[SERVER_NAME];
216
+ saveMcpServers(servers);
217
+ console.log();
218
+ console.log(` ${GREEN}✓${RESET} Removed ${BOLD}${SERVER_NAME}${RESET} from mcp-servers.json`);
219
+ console.log(` ${DIM}venv and harness clone are kept. To fully remove:${RESET}`);
220
+ console.log(` ${CYAN}rm -rf "${VENV_DIR}" "${HARNESS_HOME}"${RESET}`);
221
+ console.log(` ${DIM}Restart the daemon: clementine restart${RESET}`);
222
+ console.log();
223
+ }
224
+ //# sourceMappingURL=browser.js.map
package/dist/cli/index.js CHANGED
@@ -24,6 +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
28
  import { isSensitiveEnvKey } from '../secrets/sensitivity.js';
28
29
  const __filename = fileURLToPath(import.meta.url);
29
30
  const __dirname = path.dirname(__filename);
@@ -4549,5 +4550,25 @@ ingestCmd
4549
4550
  .command('status <slug>')
4550
4551
  .description('Show recent runs and metadata for a source')
4551
4552
  .action(cmdIngestStatus);
4553
+ // ── Browser harness (beta) ──────────────────────────────────────────
4554
+ const browserCmd = program
4555
+ .command('browser')
4556
+ .description('Browser harness — drive your real Chrome via CDP (beta, opt-in)');
4557
+ browserCmd
4558
+ .command('status')
4559
+ .description('Show install and enable state of the browser harness MCP')
4560
+ .action(cmdBrowserStatus);
4561
+ browserCmd
4562
+ .command('install')
4563
+ .description('Clone browser-harness and install Python deps into a private venv')
4564
+ .action(cmdBrowserInstall);
4565
+ browserCmd
4566
+ .command('enable')
4567
+ .description('Register the browser harness MCP server in mcp-servers.json')
4568
+ .action(cmdBrowserEnable);
4569
+ browserCmd
4570
+ .command('disable')
4571
+ .description('Remove the browser harness MCP entry (keeps installed files)')
4572
+ .action(cmdBrowserDisable);
4552
4573
  program.parse();
4553
4574
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -86,6 +86,7 @@
86
86
  "dist/",
87
87
  "vault/",
88
88
  "scripts/",
89
+ "vendor/",
89
90
  "install.sh",
90
91
  "README.md",
91
92
  ".env.example"
@@ -0,0 +1,27 @@
1
+ # Browser Harness MCP Bridge
2
+
3
+ Stdio MCP server that wraps [browser-use/browser-harness](https://github.com/browser-use/browser-harness) so Clementine can drive your real Chrome via CDP.
4
+
5
+ **Status:** Phase 1 plumbing — tools return `[stub]` placeholders until the harness primitives are wired in.
6
+
7
+ ## Setup
8
+
9
+ ```bash
10
+ clementine browser install # clone harness + install Python deps
11
+ clementine browser enable # register MCP server in ~/.clementine/mcp-servers.json
12
+ clementine restart
13
+ ```
14
+
15
+ To remove: `clementine browser disable` (the venv and harness clone are kept; delete `~/.clementine/browser-harness*` to fully remove).
16
+
17
+ ## Tools
18
+
19
+ | Tool | Tier | Description |
20
+ |------|------|-------------|
21
+ | `browser_status` | 1 | Diagnostic: install state + CDP URL |
22
+ | `browser_screenshot` | 1 | Capture active tab |
23
+ | `browser_inspect` | 1 | Read page HTML or selector |
24
+ | `browser_navigate` | 2 | Open a URL in connected Chrome |
25
+ | `browser_run_python` | 3 | Execute Python in `agent-workspace/` (approval required) |
26
+
27
+ Tier policies are enforced by Clementine's `src/agent/hooks.ts`. Tier 3 actions require explicit approval and run only with a per-domain allowlist (Phase 2).
@@ -0,0 +1,13 @@
1
+ [project]
2
+ name = "clementine-browser-harness-mcp"
3
+ version = "0.1.0"
4
+ description = "MCP bridge between Clementine and browser-use/browser-harness"
5
+ requires-python = ">=3.10"
6
+ dependencies = [
7
+ "mcp>=1.0.0",
8
+ "websockets>=12.0",
9
+ ]
10
+
11
+ [build-system]
12
+ requires = ["hatchling"]
13
+ build-backend = "hatchling.build"
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Clementine ↔ browser-harness MCP bridge.
4
+
5
+ Stdio MCP server that exposes browser-harness primitives to the Claude Agent
6
+ SDK. Fails gracefully: if browser-harness or its deps aren't installed, the
7
+ server still starts and every tool returns a clear "not installed" message
8
+ so the rest of Clementine keeps working.
9
+
10
+ Wire-up:
11
+ mcpServers in ~/.clementine/mcp-servers.json:
12
+ {
13
+ "browser-harness": {
14
+ "type": "stdio",
15
+ "command": "<venv>/bin/python3",
16
+ "args": ["<package>/vendor/browser-harness-mcp/server.py"],
17
+ "env": {
18
+ "BROWSER_HARNESS_HOME": "~/.clementine/browser-harness",
19
+ "BROWSER_CDP_URL": "ws://localhost:9222"
20
+ }
21
+ }
22
+ }
23
+
24
+ Run `clementine browser install` and `clementine browser enable` to set up.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import os
29
+ import sys
30
+ from pathlib import Path
31
+
32
+ # Best-effort: load browser-harness from the user's data home.
33
+ HARNESS_HOME = Path(
34
+ os.environ.get(
35
+ "BROWSER_HARNESS_HOME",
36
+ Path.home() / ".clementine" / "browser-harness",
37
+ )
38
+ ).expanduser()
39
+
40
+ CDP_URL = os.environ.get("BROWSER_CDP_URL", "ws://localhost:9222")
41
+
42
+ _HARNESS_AVAILABLE = False
43
+ _HARNESS_ERROR: str | None = None
44
+
45
+ try:
46
+ if (HARNESS_HOME / "src").is_dir():
47
+ sys.path.insert(0, str(HARNESS_HOME / "src"))
48
+ # The actual harness module — import is lazy to keep startup cheap.
49
+ import browser_harness # type: ignore # noqa: F401
50
+ _HARNESS_AVAILABLE = True
51
+ except Exception as e: # noqa: BLE001
52
+ _HARNESS_ERROR = f"{type(e).__name__}: {e}"
53
+
54
+ try:
55
+ from mcp.server.fastmcp import FastMCP
56
+ except Exception as e: # noqa: BLE001
57
+ sys.stderr.write(
58
+ "browser-harness MCP: 'mcp' package not installed. "
59
+ "Run `clementine browser install` to set up.\n"
60
+ f"Underlying error: {e}\n"
61
+ )
62
+ sys.exit(1)
63
+
64
+
65
+ server = FastMCP("browser-harness")
66
+
67
+
68
+ def _not_ready_message() -> str:
69
+ if _HARNESS_AVAILABLE:
70
+ return ""
71
+ return (
72
+ "browser-harness is not installed. Run `clementine browser install` "
73
+ "to clone the harness into ~/.clementine/browser-harness and install "
74
+ f"Python dependencies. (Underlying: {_HARNESS_ERROR})"
75
+ )
76
+
77
+
78
+ @server.tool()
79
+ def browser_status() -> str:
80
+ """Report whether browser-harness is installed and the CDP target it's pointed at."""
81
+ parts = [
82
+ f"harness_installed: {_HARNESS_AVAILABLE}",
83
+ f"harness_home: {HARNESS_HOME}",
84
+ f"cdp_url: {CDP_URL}",
85
+ ]
86
+ if not _HARNESS_AVAILABLE and _HARNESS_ERROR:
87
+ parts.append(f"error: {_HARNESS_ERROR}")
88
+ return "\n".join(parts)
89
+
90
+
91
+ @server.tool()
92
+ def browser_navigate(url: str) -> str:
93
+ """Open a URL in the connected Chrome via CDP. Tier 2 (logged)."""
94
+ msg = _not_ready_message()
95
+ if msg:
96
+ return msg
97
+ # TODO: implement via browser_harness CDP helpers
98
+ return f"[stub] would navigate to {url} via {CDP_URL}"
99
+
100
+
101
+ @server.tool()
102
+ def browser_screenshot() -> str:
103
+ """Capture a screenshot of the active tab and return its file path. Tier 1."""
104
+ msg = _not_ready_message()
105
+ if msg:
106
+ return msg
107
+ # TODO: implement via browser_harness CDP helpers
108
+ return f"[stub] would screenshot active tab via {CDP_URL}"
109
+
110
+
111
+ @server.tool()
112
+ def browser_inspect(selector: str = "body") -> str:
113
+ """Read the current page HTML or a specific selector. Tier 1 (read-only)."""
114
+ msg = _not_ready_message()
115
+ if msg:
116
+ return msg
117
+ # TODO: implement via browser_harness CDP helpers
118
+ return f"[stub] would inspect '{selector}' via {CDP_URL}"
119
+
120
+
121
+ @server.tool()
122
+ def browser_run_python(code: str) -> str:
123
+ """Run Python in the harness workspace. Tier 3 (autonomous-blocked, requires approval)."""
124
+ msg = _not_ready_message()
125
+ if msg:
126
+ return msg
127
+ # TODO: thread through agent-workspace/agent_helpers.py — see SKILL.md
128
+ return f"[stub] would run python ({len(code)} bytes) in {HARNESS_HOME}/agent-workspace"
129
+
130
+
131
+ if __name__ == "__main__":
132
+ # FastMCP handles stdio transport automatically.
133
+ server.run()