imprint-mcp 0.3.0 → 0.3.1
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.
- package/README.md +42 -2
- package/examples/google-flights/get_flight_calendar_prices/index.ts +2 -2
- package/examples/google-flights/get_flight_calendar_prices/workflow.json +4 -2
- package/examples/southwest/README.md +3 -2
- package/examples/southwest/search_southwest_flights/index.ts +18 -1
- package/examples/southwest/search_southwest_flights/workflow.json +18 -1
- package/package.json +1 -1
- package/src/cli.ts +7 -1
- package/src/imprint/cdp-browser-fetch.ts +4 -1
- package/src/imprint/chromium.ts +279 -8
- package/src/imprint/compile-tools.ts +11 -3
- package/src/imprint/cron.ts +3 -0
- package/src/imprint/doctor.ts +9 -3
- package/src/imprint/install.ts +79 -4
- package/src/imprint/integrations.ts +1 -1
- package/src/imprint/mcp-maintenance.ts +9 -1
- package/src/imprint/mcp-server.ts +1 -1
- package/src/imprint/stealth-chromium.ts +6 -8
package/README.md
CHANGED
|
@@ -29,6 +29,24 @@ imprint teach southwest --url https://www.southwest.com
|
|
|
29
29
|
|
|
30
30
|
A browser opens. You drive the workflow and narrate what you're doing. Imprint records every request and interaction, then compiles a deterministic **MCP tool** your agent can call forever.
|
|
31
31
|
|
|
32
|
+
Want to try a finished MCP before recording anything?
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
imprint install google-flights --source examples --platform claude-desktop
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
That registers the checked-in Google Flights example as an MCP server in your client. Swap `claude-desktop` for `claude-code`, `codex`, `openclaw`, or `hermes`, or add `--print` to see the config without changing anything.
|
|
39
|
+
|
|
40
|
+
On a Hermes agent or Docker host, register the examples directly into Hermes:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
for site in google-flights google-hotels southwest discoverandgo echo; do
|
|
44
|
+
imprint install "$site" --source examples --platform hermes --no-interactive
|
|
45
|
+
done
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
When `HERMES_HOME` is set, Imprint writes Hermes MCP entries to `$HERMES_HOME/config.yaml`; outside Hermes it uses `~/.hermes/config.yaml`. For browser-backed MCPs, `imprint install` also installs Playwright Chromium into `$HERMES_HOME/.cache/ms-playwright` and writes `PLAYWRIGHT_BROWSERS_PATH` into the MCP config so Hermes can find it. Use `--skip-browser-install` only for offline builds where you preinstall the browser yourself. In a fresh Linux image that is missing browser system libraries, install those during image build with `bunx playwright install --with-deps chromium`.
|
|
49
|
+
|
|
32
50
|
---
|
|
33
51
|
|
|
34
52
|
## See It in Action
|
|
@@ -118,7 +136,7 @@ bun install && bun link
|
|
|
118
136
|
|
|
119
137
|
<br>
|
|
120
138
|
|
|
121
|
-
**Browser commands** (`teach`, `record`, `login`, `playbook`)
|
|
139
|
+
**Browser commands** (`teach`, `record`, `login`, `playbook`) and browser-backed `imprint install` targets auto-install Playwright Chromium when it is missing. For offline CI or prebuilt Linux images where you pass `--skip-browser-install`, preinstall it ahead of time:
|
|
122
140
|
|
|
123
141
|
```bash
|
|
124
142
|
bunx playwright install chromium
|
|
@@ -207,6 +225,21 @@ Install any example into your MCP client:
|
|
|
207
225
|
imprint install google-flights --source examples --platform claude-desktop
|
|
208
226
|
```
|
|
209
227
|
|
|
228
|
+
Examples are real generated MCPs, not handwritten SDK samples. `imprint install <site> --source examples` points the MCP server at this repo's `examples/` directory with `IMPRINT_HOME`, ensures Playwright Chromium for browser-backed tools, and lets your client list and call the checked-in tools immediately:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
imprint install google-hotels --source examples --platform codex
|
|
232
|
+
imprint install southwest --source examples --platform claude-code
|
|
233
|
+
imprint install echo --source examples --platform claude-desktop --print
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
For your own generated tools, leave off `--source examples`:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
imprint install mysite --platform claude-code
|
|
240
|
+
imprint install mysite --platform codex
|
|
241
|
+
```
|
|
242
|
+
|
|
210
243
|
---
|
|
211
244
|
|
|
212
245
|
## CLI Reference
|
|
@@ -227,7 +260,14 @@ imprint <command> --help # per-command options
|
|
|
227
260
|
|
|
228
261
|
## Sharing Skills
|
|
229
262
|
|
|
230
|
-
Teach on your laptop, ship to a remote agent.
|
|
263
|
+
Teach on your laptop, ship to a remote agent. Generated MCP folders contain the portable tool artifacts: `workflow.json`, `playbook.yaml`, `index.ts`, optional shared modules, and cron/backend metadata. Copy `~/.imprint/<site>` into the receiver's `~/.imprint/<site>` or commit it to a private repo, install Imprint there, then register it:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
bun install -g imprint-mcp
|
|
267
|
+
imprint install mysite --platform claude-code
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Credentials stay separate. Skill folders contain **zero plaintext credentials** — only `${credential.NAME}` placeholders and a manifest listing what the receiver must provision.
|
|
231
271
|
|
|
232
272
|
```bash
|
|
233
273
|
# Export (encrypted with libsodium + argon2id)
|
|
@@ -75,7 +75,7 @@ const WORKFLOW: Workflow = {
|
|
|
75
75
|
{
|
|
76
76
|
"name": "f_sid",
|
|
77
77
|
"required": false,
|
|
78
|
-
"capability": "
|
|
78
|
+
"capability": "browser_bootstrap",
|
|
79
79
|
"source": "html_regex",
|
|
80
80
|
"pattern": "\"FdrFJe\":\"([^\"]+)\"",
|
|
81
81
|
"group": 1
|
|
@@ -83,7 +83,7 @@ const WORKFLOW: Workflow = {
|
|
|
83
83
|
{
|
|
84
84
|
"name": "bl",
|
|
85
85
|
"required": false,
|
|
86
|
-
"capability": "
|
|
86
|
+
"capability": "browser_bootstrap",
|
|
87
87
|
"source": "html_regex",
|
|
88
88
|
"pattern": "\"cfb2h\":\"([^\"]+)\"",
|
|
89
89
|
"group": 1
|
|
@@ -44,14 +44,16 @@
|
|
|
44
44
|
"name": "f_sid",
|
|
45
45
|
"pattern": "\"FdrFJe\":\"([^\"]+)\"",
|
|
46
46
|
"group": 1,
|
|
47
|
-
"required": false
|
|
47
|
+
"required": false,
|
|
48
|
+
"capability": "browser_bootstrap"
|
|
48
49
|
},
|
|
49
50
|
{
|
|
50
51
|
"source": "html_regex",
|
|
51
52
|
"name": "bl",
|
|
52
53
|
"pattern": "\"cfb2h\":\"([^\"]+)\"",
|
|
53
54
|
"group": 1,
|
|
54
|
-
"required": false
|
|
55
|
+
"required": false,
|
|
56
|
+
"capability": "browser_bootstrap"
|
|
55
57
|
}
|
|
56
58
|
]
|
|
57
59
|
},
|
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
- **`probe-backends` skipping the futile rung.** The cached `backends.json` orders the ladder `stealth-fetch → playbook` so cron doesn't burn 200ms on a fetch attempt every tick.
|
|
9
9
|
- **`notifyWhen: price_below`** pushing only on real drops, with the `pricePath` extracting from real Southwest response shape.
|
|
10
10
|
- **The fresh-UUID header trick** — Southwest rejects stale `X-User-Experience-ID`; stealth-fetch regenerates per call.
|
|
11
|
+
- **Public bootstrap config capture.** The workflow fetches Southwest's public bootstrap `data.js` and captures the current `api-keys.prod` value, so installed examples do not require a `SOUTHWEST_API_KEY` environment variable.
|
|
11
12
|
- **Multi-path `pricePath`** — `cron.json`'s notifyWhen lists both the raw API shape (when stealth-fetch wins) and the playbook's reshaped output, so the push fires regardless of which backend produced the result.
|
|
12
13
|
|
|
13
14
|
## Run it
|
|
14
15
|
|
|
15
16
|
```bash
|
|
16
|
-
# One-time setup
|
|
17
|
-
|
|
17
|
+
# One-time setup: registers the example MCP and installs Playwright Chromium if missing
|
|
18
|
+
imprint install southwest --source examples --platform claude-desktop
|
|
18
19
|
|
|
19
20
|
# Run a single tick (verifies everything still works)
|
|
20
21
|
imprint cron southwest --once
|
|
@@ -55,13 +55,30 @@ const WORKFLOW: Workflow = {
|
|
|
55
55
|
}
|
|
56
56
|
],
|
|
57
57
|
"requests": [
|
|
58
|
+
{
|
|
59
|
+
"method": "GET",
|
|
60
|
+
"url": "https://www.southwest.com/swa-ui/bootstrap/landing-home-page-v2/1/data.js",
|
|
61
|
+
"headers": {
|
|
62
|
+
"Accept": "application/javascript, text/javascript, */*; q=0.01"
|
|
63
|
+
},
|
|
64
|
+
"captures": [
|
|
65
|
+
{
|
|
66
|
+
"name": "southwest_api_key",
|
|
67
|
+
"source": "text_regex",
|
|
68
|
+
"pattern": "\"swa-bootstrap-landing-home-page-v2/api-keys\":\\[function\\(require,module,exports\\)\\{\\s*module\\.exports = \\{[^}]*\"prod\":\"([^\"]+)\"",
|
|
69
|
+
"group": 1,
|
|
70
|
+
"required": true,
|
|
71
|
+
"capability": "ordinary_http"
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
},
|
|
58
75
|
{
|
|
59
76
|
"method": "POST",
|
|
60
77
|
"url": "https://www.southwest.com/api/air-booking/v1/air-booking/page/air/booking/shopping",
|
|
61
78
|
"headers": {
|
|
62
79
|
"Content-Type": "application/json",
|
|
63
80
|
"Accept": "application/json, text/javascript, */*; q=0.01",
|
|
64
|
-
"X-API-Key": "${
|
|
81
|
+
"X-API-Key": "${state.southwest_api_key}",
|
|
65
82
|
"X-App-ID": "air-booking",
|
|
66
83
|
"X-Channel-ID": "southwest"
|
|
67
84
|
},
|
|
@@ -37,13 +37,30 @@
|
|
|
37
37
|
}
|
|
38
38
|
],
|
|
39
39
|
"requests": [
|
|
40
|
+
{
|
|
41
|
+
"method": "GET",
|
|
42
|
+
"url": "https://www.southwest.com/swa-ui/bootstrap/landing-home-page-v2/1/data.js",
|
|
43
|
+
"headers": {
|
|
44
|
+
"Accept": "application/javascript, text/javascript, */*; q=0.01"
|
|
45
|
+
},
|
|
46
|
+
"captures": [
|
|
47
|
+
{
|
|
48
|
+
"name": "southwest_api_key",
|
|
49
|
+
"source": "text_regex",
|
|
50
|
+
"pattern": "\"swa-bootstrap-landing-home-page-v2/api-keys\":\\[function\\(require,module,exports\\)\\{\\s*module\\.exports = \\{[^}]*\"prod\":\"([^\"]+)\"",
|
|
51
|
+
"group": 1,
|
|
52
|
+
"required": true,
|
|
53
|
+
"capability": "ordinary_http"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
},
|
|
40
57
|
{
|
|
41
58
|
"method": "POST",
|
|
42
59
|
"url": "https://www.southwest.com/api/air-booking/v1/air-booking/page/air/booking/shopping",
|
|
43
60
|
"headers": {
|
|
44
61
|
"Content-Type": "application/json",
|
|
45
62
|
"Accept": "application/json, text/javascript, */*; q=0.01",
|
|
46
|
-
"X-API-Key": "${
|
|
63
|
+
"X-API-Key": "${state.southwest_api_key}",
|
|
47
64
|
"X-App-ID": "air-booking",
|
|
48
65
|
"X-Channel-ID": "southwest"
|
|
49
66
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imprint-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Teach an AI agent how to use any website. Once. Records a real browser session + narration; generates a deterministic MCP tool plus a DOM-replay playbook fallback.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/cli.ts
CHANGED
|
@@ -238,7 +238,7 @@ export const VERB_HELP: Record<string, VerbHelp> = {
|
|
|
238
238
|
summary:
|
|
239
239
|
'Install an already-emitted MCP server into Claude Code, Codex, Claude Desktop, OpenClaw, or Hermes.',
|
|
240
240
|
usage: [
|
|
241
|
-
'imprint install [<site>] [--platform <name>] [--source local|examples] [--print] [--no-interactive]',
|
|
241
|
+
'imprint install [<site>] [--platform <name>] [--source local|examples] [--print] [--no-interactive] [--skip-browser-install]',
|
|
242
242
|
],
|
|
243
243
|
flags: [
|
|
244
244
|
{
|
|
@@ -257,6 +257,10 @@ export const VERB_HELP: Record<string, VerbHelp> = {
|
|
|
257
257
|
name: '--no-interactive',
|
|
258
258
|
description: 'Do not prompt; requires <site> and --platform.',
|
|
259
259
|
},
|
|
260
|
+
{
|
|
261
|
+
name: '--skip-browser-install',
|
|
262
|
+
description: 'Do not auto-install Playwright Chromium for browser-backed tools.',
|
|
263
|
+
},
|
|
260
264
|
],
|
|
261
265
|
example: 'imprint install google-flights --source examples --platform claude-desktop',
|
|
262
266
|
},
|
|
@@ -822,6 +826,7 @@ async function main(argv: string[]): Promise<number> {
|
|
|
822
826
|
source: { type: 'string' },
|
|
823
827
|
print: { type: 'boolean' },
|
|
824
828
|
'no-interactive': { type: 'boolean' },
|
|
829
|
+
'skip-browser-install': { type: 'boolean' },
|
|
825
830
|
},
|
|
826
831
|
allowPositionals: false,
|
|
827
832
|
});
|
|
@@ -850,6 +855,7 @@ async function main(argv: string[]): Promise<number> {
|
|
|
850
855
|
source: values.source as (typeof sources)[number] | undefined,
|
|
851
856
|
print: values.print,
|
|
852
857
|
noInteractive: values['no-interactive'],
|
|
858
|
+
skipBrowserInstall: values['skip-browser-install'],
|
|
853
859
|
});
|
|
854
860
|
console.log(`[imprint] ${result.message}`);
|
|
855
861
|
if ('source' in result)
|
|
@@ -283,7 +283,10 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
|
|
|
283
283
|
throw new Error('navigate timeout');
|
|
284
284
|
}),
|
|
285
285
|
]);
|
|
286
|
-
await
|
|
286
|
+
await Promise.race([
|
|
287
|
+
Page.loadEventFired(),
|
|
288
|
+
sleep(Math.min(abckWaitMs, 5000)).then(() => undefined),
|
|
289
|
+
]).catch(() => {});
|
|
287
290
|
} catch (err) {
|
|
288
291
|
log(`navigation issue (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
289
292
|
}
|
package/src/imprint/chromium.ts
CHANGED
|
@@ -2,11 +2,20 @@
|
|
|
2
2
|
* bundled Chromium (unmanaged) over system Chrome (corporate policy
|
|
3
3
|
* often blocks --remote-debugging-port). $CHROMIUM_PATH overrides. */
|
|
4
4
|
|
|
5
|
-
import { type ChildProcess, spawn } from 'node:child_process';
|
|
6
|
-
import {
|
|
5
|
+
import { type ChildProcess, spawnSync as nodeSpawnSync, spawn } from 'node:child_process';
|
|
6
|
+
import {
|
|
7
|
+
closeSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
openSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
statSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
} from 'node:fs';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
7
16
|
import { createServer } from 'node:net';
|
|
8
17
|
import { homedir, tmpdir } from 'node:os';
|
|
9
|
-
import { join as pathJoin } from 'node:path';
|
|
18
|
+
import { dirname as pathDirname, join as pathJoin } from 'node:path';
|
|
10
19
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
11
20
|
import { isDebug } from './log.ts';
|
|
12
21
|
|
|
@@ -62,6 +71,13 @@ export function chromeProxyArg(proxy: string): string | null {
|
|
|
62
71
|
return /^[\w.-]+:\d+$/.test(proxy) ? proxy : null;
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
export function shouldDisableChromiumSandbox(): boolean {
|
|
75
|
+
const override = process.env.IMPRINT_CHROMIUM_NO_SANDBOX?.trim().toLowerCase();
|
|
76
|
+
if (override === '1' || override === 'true' || override === 'yes') return true;
|
|
77
|
+
if (override === '0' || override === 'false' || override === 'no') return false;
|
|
78
|
+
return process.platform === 'linux' && existsSync('/.dockerenv');
|
|
79
|
+
}
|
|
80
|
+
|
|
65
81
|
interface LaunchedChromium {
|
|
66
82
|
process: ChildProcess;
|
|
67
83
|
port: number;
|
|
@@ -77,14 +93,36 @@ const LINUX_CANDIDATES = [
|
|
|
77
93
|
'/usr/bin/chromium',
|
|
78
94
|
'/usr/bin/chromium-browser',
|
|
79
95
|
];
|
|
96
|
+
const require = createRequire(import.meta.url);
|
|
97
|
+
|
|
98
|
+
export function defaultPlaywrightBrowsersPath(): string | undefined {
|
|
99
|
+
const hermesHome = process.env.HERMES_HOME?.trim();
|
|
100
|
+
if (hermesHome) return pathJoin(hermesHome, '.cache', 'ms-playwright');
|
|
101
|
+
const explicit = process.env.PLAYWRIGHT_BROWSERS_PATH?.trim();
|
|
102
|
+
if (explicit) return explicit;
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function playwrightInstallEnv(): NodeJS.ProcessEnv {
|
|
107
|
+
const env = { ...process.env };
|
|
108
|
+
const browsersPath = defaultPlaywrightBrowsersPath();
|
|
109
|
+
if (browsersPath) env.PLAYWRIGHT_BROWSERS_PATH = browsersPath;
|
|
110
|
+
return env;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function playwrightChromiumCacheRoots(): string[] {
|
|
114
|
+
const roots = [
|
|
115
|
+
defaultPlaywrightBrowsersPath(),
|
|
116
|
+
pathJoin(homedir(), 'Library/Caches/ms-playwright'),
|
|
117
|
+
pathJoin(homedir(), '.cache/ms-playwright'),
|
|
118
|
+
].filter((root): root is string => Boolean(root));
|
|
119
|
+
return [...new Set(roots)];
|
|
120
|
+
}
|
|
80
121
|
|
|
81
122
|
/** Find Playwright's "Google Chrome for Testing" — newest version wins
|
|
82
123
|
* if multiple are installed. */
|
|
83
124
|
function findPlaywrightChromium(): string | null {
|
|
84
|
-
const cacheRoots =
|
|
85
|
-
pathJoin(homedir(), 'Library/Caches/ms-playwright'),
|
|
86
|
-
pathJoin(homedir(), '.cache/ms-playwright'),
|
|
87
|
-
];
|
|
125
|
+
const cacheRoots = playwrightChromiumCacheRoots();
|
|
88
126
|
for (const root of cacheRoots) {
|
|
89
127
|
if (!existsSync(root)) continue;
|
|
90
128
|
let dirs: string[];
|
|
@@ -122,6 +160,7 @@ function findPlaywrightChromium(): string | null {
|
|
|
122
160
|
'Google Chrome for Testing',
|
|
123
161
|
),
|
|
124
162
|
// Linux layout
|
|
163
|
+
pathJoin(root, dir, 'chrome-linux64', 'chrome'),
|
|
125
164
|
pathJoin(root, dir, 'chrome-linux', 'chrome'),
|
|
126
165
|
];
|
|
127
166
|
for (const c of candidates) {
|
|
@@ -136,7 +175,236 @@ function findPlaywrightChromium(): string | null {
|
|
|
136
175
|
return null;
|
|
137
176
|
}
|
|
138
177
|
|
|
178
|
+
function playwrightInstallCommand(): string[] {
|
|
179
|
+
const playwrightCli = resolvePlaywrightCli();
|
|
180
|
+
if (playwrightCli) return ['node', playwrightCli, 'install', 'chromium'];
|
|
181
|
+
if (process.versions.bun) return [process.execPath, 'x', 'playwright', 'install', 'chromium'];
|
|
182
|
+
return ['bunx', 'playwright', 'install', 'chromium'];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function resolvePlaywrightCli(): string | null {
|
|
186
|
+
try {
|
|
187
|
+
const packageJson = require.resolve('playwright/package.json');
|
|
188
|
+
const cli = pathJoin(pathDirname(packageJson), 'cli.js');
|
|
189
|
+
return existsSync(cli) ? cli : null;
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function quoteShellArg(value: string): string {
|
|
196
|
+
if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) return value;
|
|
197
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function commandText(command: string[], env: NodeJS.ProcessEnv): string {
|
|
201
|
+
const prefix = env.PLAYWRIGHT_BROWSERS_PATH
|
|
202
|
+
? `PLAYWRIGHT_BROWSERS_PATH=${quoteShellArg(env.PLAYWRIGHT_BROWSERS_PATH)} `
|
|
203
|
+
: '';
|
|
204
|
+
return `${prefix}${command.map(quoteShellArg).join(' ')}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
interface InstallerResult {
|
|
208
|
+
exitCode: number | null;
|
|
209
|
+
stdout?: string;
|
|
210
|
+
stderr?: string;
|
|
211
|
+
signal?: NodeJS.Signals | null;
|
|
212
|
+
error?: string;
|
|
213
|
+
timedOut?: boolean;
|
|
214
|
+
timeoutMs?: number;
|
|
215
|
+
logPath?: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
type ChromiumInstaller = (command: string[], env: NodeJS.ProcessEnv) => InstallerResult;
|
|
219
|
+
|
|
220
|
+
let chromiumInstallerForTest: ChromiumInstaller | null = null;
|
|
221
|
+
let chromiumFinderForTest: (() => string | null) | null = null;
|
|
222
|
+
let verifiedChromiumPath: string | null = null;
|
|
223
|
+
|
|
224
|
+
export function __setPlaywrightChromiumInstallerForTest(installer: ChromiumInstaller | null): void {
|
|
225
|
+
chromiumInstallerForTest = installer;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function __setChromiumFinderForTest(finder: (() => string | null) | null): void {
|
|
229
|
+
chromiumFinderForTest = finder;
|
|
230
|
+
verifiedChromiumPath = null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function runPlaywrightChromiumInstall(command: string[], env: NodeJS.ProcessEnv): InstallerResult {
|
|
234
|
+
if (chromiumInstallerForTest) return chromiumInstallerForTest(command, env);
|
|
235
|
+
const timeoutMs = playwrightInstallTimeoutMs();
|
|
236
|
+
const logPath = pathJoin(tmpdir(), `imprint-playwright-install-${process.pid}-${Date.now()}.log`);
|
|
237
|
+
let logFd: number | null = null;
|
|
238
|
+
try {
|
|
239
|
+
logFd = openSync(logPath, 'w');
|
|
240
|
+
const result = nodeSpawnSync(command[0] ?? '', command.slice(1), {
|
|
241
|
+
env,
|
|
242
|
+
// Playwright emits frequent progress lines. Send them to a file so parent
|
|
243
|
+
// command runners that capture stderr without draining it cannot block.
|
|
244
|
+
stdio: ['ignore', logFd, logFd],
|
|
245
|
+
timeout: timeoutMs,
|
|
246
|
+
});
|
|
247
|
+
const failed = result.status !== 0 || Boolean(result.error);
|
|
248
|
+
if (!failed) unlinkInstallerLog(logPath);
|
|
249
|
+
return formatSpawnResult(
|
|
250
|
+
result,
|
|
251
|
+
timeoutMs,
|
|
252
|
+
failed ? readInstallerLog(logPath) : undefined,
|
|
253
|
+
logPath,
|
|
254
|
+
);
|
|
255
|
+
} finally {
|
|
256
|
+
if (logFd !== null) closeSync(logFd);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function formatSpawnResult(
|
|
261
|
+
result: ReturnType<typeof nodeSpawnSync>,
|
|
262
|
+
timeoutMs: number,
|
|
263
|
+
output: string | undefined,
|
|
264
|
+
logPath: string,
|
|
265
|
+
): InstallerResult {
|
|
266
|
+
const error = result.error as (Error & { code?: string }) | undefined;
|
|
267
|
+
return {
|
|
268
|
+
exitCode: result.status,
|
|
269
|
+
stderr: output,
|
|
270
|
+
signal: result.signal,
|
|
271
|
+
error: error?.message,
|
|
272
|
+
timedOut: error?.code === 'ETIMEDOUT',
|
|
273
|
+
timeoutMs,
|
|
274
|
+
logPath,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function readInstallerLog(logPath: string): string | undefined {
|
|
279
|
+
try {
|
|
280
|
+
const output = readFileSync(logPath, 'utf8').trim();
|
|
281
|
+
const maxChars = 50_000;
|
|
282
|
+
if (output.length <= maxChars) return output;
|
|
283
|
+
return `[last ${maxChars} chars of ${logPath}]\n${output.slice(-maxChars)}`;
|
|
284
|
+
} catch {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function unlinkInstallerLog(logPath: string): void {
|
|
290
|
+
try {
|
|
291
|
+
unlinkSync(logPath);
|
|
292
|
+
} catch {
|
|
293
|
+
// best effort cleanup
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function playwrightInstallTimeoutMs(): number {
|
|
298
|
+
const raw = process.env.IMPRINT_PLAYWRIGHT_INSTALL_TIMEOUT_MS?.trim();
|
|
299
|
+
if (!raw) return 10 * 60 * 1000;
|
|
300
|
+
const parsed = Number(raw);
|
|
301
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 10 * 60 * 1000;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function formatInstallerFailure(result: InstallerResult): string | undefined {
|
|
305
|
+
const lines: string[] = [];
|
|
306
|
+
if (result.timedOut) {
|
|
307
|
+
lines.push(`Timed out after ${Math.round((result.timeoutMs ?? 0) / 1000)}s.`);
|
|
308
|
+
}
|
|
309
|
+
if (result.signal) lines.push(`Terminated by signal: ${result.signal}`);
|
|
310
|
+
if (result.error) lines.push(`Error: ${result.error}`);
|
|
311
|
+
const output = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
|
|
312
|
+
if (output) lines.push(`Output:\n${output}`);
|
|
313
|
+
else if (result.logPath) lines.push(`Installer log: ${result.logPath}`);
|
|
314
|
+
return lines.length > 0 ? lines.join('\n') : undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
interface EnsureChromiumResult {
|
|
318
|
+
path: string;
|
|
319
|
+
installed: boolean;
|
|
320
|
+
command?: string;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function verifyChromiumExecutable(path: string): void {
|
|
324
|
+
if (verifiedChromiumPath === path) return;
|
|
325
|
+
const result = Bun.spawnSync([path, '--version'], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
326
|
+
if (result.exitCode === 0) {
|
|
327
|
+
verifiedChromiumPath = path;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const output = `${result.stderr.toString()}\n${result.stdout.toString()}`.trim();
|
|
331
|
+
throw new Error(
|
|
332
|
+
[
|
|
333
|
+
`Chromium was found at ${path}, but it could not start.`,
|
|
334
|
+
output ? `Output:\n${output}` : undefined,
|
|
335
|
+
process.platform === 'linux'
|
|
336
|
+
? 'Install missing Linux browser libraries with: bunx playwright install --with-deps chromium'
|
|
337
|
+
: undefined,
|
|
338
|
+
]
|
|
339
|
+
.filter((line): line is string => Boolean(line))
|
|
340
|
+
.join('\n'),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function ensurePlaywrightChromiumInstalled(
|
|
345
|
+
opts: {
|
|
346
|
+
log?: (message: string) => void;
|
|
347
|
+
} = {},
|
|
348
|
+
): EnsureChromiumResult {
|
|
349
|
+
let existingPath: string | null = null;
|
|
350
|
+
try {
|
|
351
|
+
existingPath = findChromium();
|
|
352
|
+
} catch {
|
|
353
|
+
// Install below.
|
|
354
|
+
}
|
|
355
|
+
if (existingPath) {
|
|
356
|
+
verifyChromiumExecutable(existingPath);
|
|
357
|
+
return { path: existingPath, installed: false };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const env = playwrightInstallEnv();
|
|
361
|
+
if (env.PLAYWRIGHT_BROWSERS_PATH) {
|
|
362
|
+
process.env.PLAYWRIGHT_BROWSERS_PATH = env.PLAYWRIGHT_BROWSERS_PATH;
|
|
363
|
+
}
|
|
364
|
+
const command = playwrightInstallCommand();
|
|
365
|
+
const displayCommand = commandText(command, env);
|
|
366
|
+
opts.log?.(`Chromium not found; installing Playwright Chromium with: ${displayCommand}`);
|
|
367
|
+
const result = runPlaywrightChromiumInstall(command, env);
|
|
368
|
+
if (result.exitCode !== 0 || result.error) {
|
|
369
|
+
const failure = formatInstallerFailure(result);
|
|
370
|
+
throw new Error(
|
|
371
|
+
[
|
|
372
|
+
'Could not install Playwright Chromium automatically.',
|
|
373
|
+
`Command: ${displayCommand}`,
|
|
374
|
+
failure,
|
|
375
|
+
'',
|
|
376
|
+
'Retry manually with the command above.',
|
|
377
|
+
process.platform === 'linux'
|
|
378
|
+
? 'If Chromium is installed but cannot launch in a fresh Linux image, install OS browser libraries with: bunx playwright install --with-deps chromium'
|
|
379
|
+
: undefined,
|
|
380
|
+
]
|
|
381
|
+
.filter((line): line is string => Boolean(line))
|
|
382
|
+
.join('\n'),
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
const path = findChromium();
|
|
388
|
+
verifyChromiumExecutable(path);
|
|
389
|
+
return { path, installed: true, command: displayCommand };
|
|
390
|
+
} catch (err) {
|
|
391
|
+
throw new Error(
|
|
392
|
+
[
|
|
393
|
+
'Playwright Chromium install completed, but Imprint still could not locate or start the Chromium binary.',
|
|
394
|
+
`Command: ${displayCommand}`,
|
|
395
|
+
err instanceof Error ? err.message : String(err),
|
|
396
|
+
].join('\n'),
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
139
401
|
export function findChromium(): string {
|
|
402
|
+
if (chromiumFinderForTest) {
|
|
403
|
+
const path = chromiumFinderForTest();
|
|
404
|
+
if (path) return path;
|
|
405
|
+
throw new Error('Could not locate Chromium.');
|
|
406
|
+
}
|
|
407
|
+
|
|
140
408
|
const explicit = process.env.CHROMIUM_PATH;
|
|
141
409
|
if (explicit && existsSync(explicit)) return explicit;
|
|
142
410
|
|
|
@@ -272,7 +540,9 @@ async function waitForCdp(port: number, timeoutMs = 10_000): Promise<void> {
|
|
|
272
540
|
}
|
|
273
541
|
|
|
274
542
|
export async function launchChromium(opts: LaunchOptions = {}): Promise<LaunchedChromium> {
|
|
275
|
-
const exe =
|
|
543
|
+
const exe = ensurePlaywrightChromiumInstalled({
|
|
544
|
+
log: (message) => process.stderr.write(`[imprint] ${message}\n`),
|
|
545
|
+
}).path;
|
|
276
546
|
const port = opts.port ?? (await pickFreePort());
|
|
277
547
|
const userDataDir =
|
|
278
548
|
opts.userDataDir ?? pathJoin(tmpdir(), `imprint-chrome-${Date.now()}-${process.pid}`);
|
|
@@ -286,6 +556,7 @@ export async function launchChromium(opts: LaunchOptions = {}): Promise<Launched
|
|
|
286
556
|
'--disable-popup-blocking',
|
|
287
557
|
'--use-mock-keychain',
|
|
288
558
|
];
|
|
559
|
+
if (shouldDisableChromiumSandbox()) args.push('--no-sandbox');
|
|
289
560
|
if (opts.headless) args.push('--headless=new');
|
|
290
561
|
const proxy = opts.proxy ?? proxyUrl();
|
|
291
562
|
if (proxy) {
|
|
@@ -7,7 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
existsSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
realpathSync,
|
|
15
|
+
unlinkSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from 'node:fs';
|
|
11
18
|
import { basename, dirname, join as pathJoin, relative as pathRelative } from 'node:path';
|
|
12
19
|
import type { AgentTool } from './agent.ts';
|
|
13
20
|
import { inferAppApiHosts } from './app-api-hosts.ts';
|
|
@@ -1234,8 +1241,9 @@ export async function typecheckArtifacts(
|
|
|
1234
1241
|
includes: string[],
|
|
1235
1242
|
): Promise<{ stdout: string; stderr: string; exitCode: number; timedOut: boolean }> {
|
|
1236
1243
|
const configPath = pathJoin(dir, '.imprint-typecheck.tsconfig.json');
|
|
1237
|
-
const rootTsconfig = pathJoin(REPO_ROOT, 'tsconfig.json');
|
|
1238
|
-
const
|
|
1244
|
+
const rootTsconfig = realpathSync(pathJoin(REPO_ROOT, 'tsconfig.json'));
|
|
1245
|
+
const configDir = realpathSync(dir);
|
|
1246
|
+
const extendsPath = normalizeTsconfigPath(pathRelative(configDir, rootTsconfig));
|
|
1239
1247
|
|
|
1240
1248
|
writeFileSync(
|
|
1241
1249
|
configPath,
|
package/src/imprint/cron.ts
CHANGED
|
@@ -77,6 +77,7 @@ async function runOnce(
|
|
|
77
77
|
ladder: ConcreteBackend[],
|
|
78
78
|
assetRoot: string,
|
|
79
79
|
stealthCache: Map<string, StealthFetch>,
|
|
80
|
+
skipBootstrapSplice: boolean,
|
|
80
81
|
): Promise<ToolResult> {
|
|
81
82
|
const startedAt = new Date();
|
|
82
83
|
log(
|
|
@@ -90,6 +91,7 @@ async function runOnce(
|
|
|
90
91
|
params,
|
|
91
92
|
assetRoot,
|
|
92
93
|
stealthCache,
|
|
94
|
+
{ skipBootstrapSplice },
|
|
93
95
|
);
|
|
94
96
|
|
|
95
97
|
const elapsed = Date.now() - t0;
|
|
@@ -274,6 +276,7 @@ async function runCronImpl(opts: RunCronOptions): Promise<void> {
|
|
|
274
276
|
ladder,
|
|
275
277
|
assetRoot,
|
|
276
278
|
stealthCache,
|
|
279
|
+
Boolean(cached?.preferredOrder.length),
|
|
277
280
|
] as const;
|
|
278
281
|
|
|
279
282
|
if (opts.once) {
|
package/src/imprint/doctor.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { join as pathJoin } from 'node:path';
|
|
8
8
|
import { findChromium } from './chromium.ts';
|
|
9
|
+
import { defaultHermesConfigPath } from './install.ts';
|
|
9
10
|
import { getProviderStatuses } from './llm.ts';
|
|
10
11
|
import { VERSION } from './version.ts';
|
|
11
12
|
|
|
@@ -63,9 +64,13 @@ function checkPlaywrightChromium(): CheckResult {
|
|
|
63
64
|
// useful as a separate line so users see whether the Playwright path
|
|
64
65
|
// specifically is set up (matters for stealth-fetch + playbook backends).
|
|
65
66
|
const cacheRoots = [
|
|
67
|
+
process.env.PLAYWRIGHT_BROWSERS_PATH,
|
|
68
|
+
process.env.HERMES_HOME
|
|
69
|
+
? pathJoin(process.env.HERMES_HOME, '.cache', 'ms-playwright')
|
|
70
|
+
: undefined,
|
|
66
71
|
pathJoin(homedir(), 'Library/Caches/ms-playwright'),
|
|
67
72
|
pathJoin(homedir(), '.cache/ms-playwright'),
|
|
68
|
-
];
|
|
73
|
+
].filter((root): root is string => Boolean(root));
|
|
69
74
|
for (const root of cacheRoots) {
|
|
70
75
|
if (!existsSync(root)) continue;
|
|
71
76
|
try {
|
|
@@ -84,7 +89,8 @@ function checkPlaywrightChromium(): CheckResult {
|
|
|
84
89
|
return {
|
|
85
90
|
name: 'Playwright Chromium',
|
|
86
91
|
ok: false,
|
|
87
|
-
detail:
|
|
92
|
+
detail:
|
|
93
|
+
'no chromium-* install under PLAYWRIGHT_BROWSERS_PATH, $HERMES_HOME/.cache/ms-playwright, ~/Library/Caches/ms-playwright, or ~/.cache/ms-playwright',
|
|
88
94
|
fix: 'run: bunx playwright install chromium (needed for stealth-fetch + playbook)',
|
|
89
95
|
};
|
|
90
96
|
}
|
|
@@ -209,7 +215,7 @@ function checkClaudeCode(): CheckResult {
|
|
|
209
215
|
}
|
|
210
216
|
|
|
211
217
|
function checkHermes(): CheckResult {
|
|
212
|
-
const configPath =
|
|
218
|
+
const configPath = defaultHermesConfigPath();
|
|
213
219
|
if (!existsSync(configPath)) {
|
|
214
220
|
return {
|
|
215
221
|
name: 'Hermes Agent',
|
package/src/imprint/install.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'node:path';
|
|
4
4
|
import * as p from '@clack/prompts';
|
|
5
5
|
import { parse as yamlParse, stringify as yamlStringify } from 'yaml';
|
|
6
|
+
import { defaultPlaywrightBrowsersPath, ensurePlaywrightChromiumInstalled } from './chromium.ts';
|
|
6
7
|
import {
|
|
7
8
|
type McpServerConfig,
|
|
8
9
|
PLATFORMS,
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
shellQuote,
|
|
17
18
|
} from './integrations.ts';
|
|
18
19
|
import { imprintHomeDir } from './paths.ts';
|
|
19
|
-
import { discoverTools } from './tool-loader.ts';
|
|
20
|
+
import { type ResolvedTool, discoverTools } from './tool-loader.ts';
|
|
20
21
|
import type { Workflow } from './types.ts';
|
|
21
22
|
|
|
22
23
|
type InstallSource = 'local' | 'examples';
|
|
@@ -28,6 +29,7 @@ interface InstallOptions {
|
|
|
28
29
|
source?: InstallSource;
|
|
29
30
|
print?: boolean;
|
|
30
31
|
noInteractive?: boolean;
|
|
32
|
+
skipBrowserInstall?: boolean;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
interface UninstallOptions {
|
|
@@ -70,6 +72,7 @@ interface InstallTarget {
|
|
|
70
72
|
source: InstallSource;
|
|
71
73
|
assetRoot: string;
|
|
72
74
|
site: string;
|
|
75
|
+
tools: ResolvedTool[];
|
|
73
76
|
workflows: Workflow[];
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -114,7 +117,11 @@ function defaultOpenClawConfigPath(): string {
|
|
|
114
117
|
return pathJoin(homedir(), '.openclaw', 'openclaw.json');
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
function defaultHermesConfigPath(): string {
|
|
120
|
+
export function defaultHermesConfigPath(): string {
|
|
121
|
+
const explicit = process.env.HERMES_CONFIG?.trim();
|
|
122
|
+
if (explicit) return explicit;
|
|
123
|
+
const hermesHome = process.env.HERMES_HOME?.trim();
|
|
124
|
+
if (hermesHome) return pathJoin(hermesHome, 'config.yaml');
|
|
118
125
|
return pathJoin(homedir(), '.hermes', 'config.yaml');
|
|
119
126
|
}
|
|
120
127
|
|
|
@@ -255,7 +262,7 @@ export async function install(opts: InstallOptions = {}): Promise<InstallResult>
|
|
|
255
262
|
const imprintCommand = configFilePlatform(platform)
|
|
256
263
|
? detectDirectBunImprintCommand()
|
|
257
264
|
: detectImprintCommand();
|
|
258
|
-
const env =
|
|
265
|
+
const env = buildInstallEnvironment(target);
|
|
259
266
|
const workflow = target.workflows[0];
|
|
260
267
|
if (!workflow) {
|
|
261
268
|
throw new Error(`No emitted workflows found for ${target.site}. Run \`imprint emit\` first.`);
|
|
@@ -287,6 +294,10 @@ export async function install(opts: InstallOptions = {}): Promise<InstallResult>
|
|
|
287
294
|
};
|
|
288
295
|
}
|
|
289
296
|
|
|
297
|
+
if (!opts.skipBrowserInstall) {
|
|
298
|
+
ensureBrowserRuntimeForInstall(target);
|
|
299
|
+
}
|
|
300
|
+
|
|
290
301
|
const regCommand = buildRegistrationCommand({
|
|
291
302
|
site: target.site,
|
|
292
303
|
platform,
|
|
@@ -422,10 +433,68 @@ async function resolveInstallTarget(opts: InstallOptions): Promise<InstallTarget
|
|
|
422
433
|
source: selected.source,
|
|
423
434
|
assetRoot: selected.assetRoot,
|
|
424
435
|
site: selected.site,
|
|
436
|
+
tools,
|
|
425
437
|
workflows,
|
|
426
438
|
};
|
|
427
439
|
}
|
|
428
440
|
|
|
441
|
+
function buildInstallEnvironment(target: InstallTarget): Record<string, string> {
|
|
442
|
+
const env: Record<string, string> = { IMPRINT_HOME: target.assetRoot };
|
|
443
|
+
const browsersPath = defaultPlaywrightBrowsersPath();
|
|
444
|
+
if (browsersPath && installTargetNeedsBrowserRuntime(target)) {
|
|
445
|
+
env.PLAYWRIGHT_BROWSERS_PATH = browsersPath;
|
|
446
|
+
}
|
|
447
|
+
return env;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function ensureBrowserRuntimeForInstall(target: InstallTarget): void {
|
|
451
|
+
if (!installTargetNeedsBrowserRuntime(target)) return;
|
|
452
|
+
const result = ensurePlaywrightChromiumInstalled({
|
|
453
|
+
log: (message) => process.stderr.write(`[imprint install] ${message}\n`),
|
|
454
|
+
});
|
|
455
|
+
if (result.installed) {
|
|
456
|
+
process.stderr.write(`[imprint install] installed Playwright Chromium at ${result.path}\n`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function installTargetNeedsBrowserRuntime(target: InstallTarget): boolean {
|
|
461
|
+
return target.tools.some(
|
|
462
|
+
(tool) => workflowNeedsBrowserRuntime(tool.workflow) || toolDirNeedsBrowserRuntime(tool.dir),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function workflowNeedsBrowserRuntime(workflow: Workflow): boolean {
|
|
467
|
+
if (workflow.bootstrap) return true;
|
|
468
|
+
if (workflow.liveVerified === false && workflow.liveVerifiedWaiver?.kind === 'waived-bot') {
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
if (workflow.requests.some((request) => request.url.includes('${state.'))) return true;
|
|
472
|
+
return workflow.requests.some((request) =>
|
|
473
|
+
(request.captures ?? []).some((capture) => captureNeedsBrowserRuntime(capture.capability)),
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function toolDirNeedsBrowserRuntime(toolDir: string): boolean {
|
|
478
|
+
if (existsSync(pathJoin(toolDir, 'playbook.yaml'))) return true;
|
|
479
|
+
const backendsPath = pathJoin(toolDir, 'backends.json');
|
|
480
|
+
if (!existsSync(backendsPath)) return false;
|
|
481
|
+
try {
|
|
482
|
+
const parsed = JSON.parse(readFileSync(backendsPath, 'utf8')) as { preferredOrder?: unknown };
|
|
483
|
+
if (!Array.isArray(parsed.preferredOrder)) return false;
|
|
484
|
+
return parsed.preferredOrder.some(
|
|
485
|
+
(backend) =>
|
|
486
|
+
typeof backend === 'string' &&
|
|
487
|
+
['fetch-bootstrap', 'cdp-replay', 'stealth-fetch', 'playbook'].includes(backend),
|
|
488
|
+
);
|
|
489
|
+
} catch {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function captureNeedsBrowserRuntime(capability: string | undefined): boolean {
|
|
495
|
+
return capability === 'browser_bootstrap' || capability === 'stealth_bootstrap';
|
|
496
|
+
}
|
|
497
|
+
|
|
429
498
|
async function resolveInstallPlatform(
|
|
430
499
|
opts: Pick<InstallOptions, 'platform' | 'noInteractive'>,
|
|
431
500
|
message = 'Install this MCP server where?',
|
|
@@ -658,7 +727,13 @@ function isPlatformDetected(platform: Platform): boolean {
|
|
|
658
727
|
case 'openclaw':
|
|
659
728
|
return commandExists('openclaw') || existsSync(pathJoin(homedir(), '.openclaw'));
|
|
660
729
|
case 'hermes':
|
|
661
|
-
return
|
|
730
|
+
return (
|
|
731
|
+
commandExists('hermes') ||
|
|
732
|
+
Boolean(process.env.HERMES_CONFIG?.trim()) ||
|
|
733
|
+
Boolean(process.env.HERMES_HOME?.trim()) ||
|
|
734
|
+
existsSync(defaultHermesConfigPath()) ||
|
|
735
|
+
existsSync(pathJoin(homedir(), '.hermes'))
|
|
736
|
+
);
|
|
662
737
|
}
|
|
663
738
|
}
|
|
664
739
|
|
|
@@ -105,7 +105,7 @@ export function generatePasteSnippet(opts: {
|
|
|
105
105
|
This gives your agent a tool that ${descLower}. Parameters: ${paramList}.`;
|
|
106
106
|
|
|
107
107
|
case 'hermes':
|
|
108
|
-
return `Add the ${toolName} tool: add to ~/.hermes/config.yaml under mcp_servers:
|
|
108
|
+
return `Add the ${toolName} tool: add to $HERMES_HOME/config.yaml (or ~/.hermes/config.yaml outside Hermes) under mcp_servers:
|
|
109
109
|
|
|
110
110
|
${toolName}:
|
|
111
111
|
command: "${ic.command}"
|
|
@@ -144,6 +144,14 @@ function defaultContext(opts: Partial<MaintenanceContext> = {}): MaintenanceCont
|
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
function hermesConfigPath(ctx: MaintenanceContext): string {
|
|
148
|
+
const explicit = process.env.HERMES_CONFIG?.trim();
|
|
149
|
+
if (explicit) return explicit;
|
|
150
|
+
const hermesHome = process.env.HERMES_HOME?.trim();
|
|
151
|
+
if (hermesHome) return pathJoin(hermesHome, 'config.yaml');
|
|
152
|
+
return pathJoin(ctx.homeDir, '.hermes', 'config.yaml');
|
|
153
|
+
}
|
|
154
|
+
|
|
147
155
|
function parseSubArgs(argv: string[]): ParsedArgs {
|
|
148
156
|
const positionals: string[] = [];
|
|
149
157
|
const flags: Record<string, string | boolean> = {};
|
|
@@ -1419,7 +1427,7 @@ function scanJsonMap(
|
|
|
1419
1427
|
}
|
|
1420
1428
|
|
|
1421
1429
|
function scanHermes(ctx: MaintenanceContext): McpRegistration[] {
|
|
1422
|
-
const configPath =
|
|
1430
|
+
const configPath = hermesConfigPath(ctx);
|
|
1423
1431
|
if (!existsSync(configPath)) return [];
|
|
1424
1432
|
const doc = YAML.parseDocument(readFileSync(configPath, 'utf8'));
|
|
1425
1433
|
const servers = doc.get('mcp_servers', true);
|
|
@@ -179,7 +179,7 @@ function buildServer(
|
|
|
179
179
|
args,
|
|
180
180
|
assetRoot,
|
|
181
181
|
stealthCache,
|
|
182
|
-
{ cdpPool, winnerCache },
|
|
182
|
+
{ cdpPool, winnerCache, skipBootstrapSplice: Boolean(tool.preferredOrder?.length) },
|
|
183
183
|
);
|
|
184
184
|
// Reset the idle timer for this site's pooled Chrome.
|
|
185
185
|
if (result.ok && usedBackend === 'cdp-replay' && cdpPool.has(tool.site)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ensurePlaywrightChromiumInstalled } from './chromium.ts';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Shared loader for Playwright's chromium with the stealth plugin applied.
|
|
@@ -69,13 +69,11 @@ export async function isStealthPluginAvailable(): Promise<boolean> {
|
|
|
69
69
|
* replay browser using chrome-headless-shell looks like a bot. Using the
|
|
70
70
|
* SAME binary for both eliminates the binary asymmetry.
|
|
71
71
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
72
|
+
* Throws if Chromium cannot be installed or started; callers translate the
|
|
73
|
+
* error into their own result shape.
|
|
74
74
|
*/
|
|
75
75
|
export function getStealthExecutablePath(): string | undefined {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
76
|
+
return ensurePlaywrightChromiumInstalled({
|
|
77
|
+
log: (message) => process.stderr.write(`[imprint] ${message}\n`),
|
|
78
|
+
}).path;
|
|
81
79
|
}
|