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.
|
|
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()
|