imprint-mcp 0.3.0 → 0.4.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.
@@ -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 { existsSync, readdirSync, statSync } from 'node:fs';
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 = findChromium();
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 { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
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 extendsPath = normalizeTsconfigPath(pathRelative(dir, rootTsconfig));
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,
@@ -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) {
@@ -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: 'no chromium-* install under ~/Library/Caches/ms-playwright or ~/.cache/ms-playwright',
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 = pathJoin(homedir(), '.hermes', 'config.yaml');
218
+ const configPath = defaultHermesConfigPath();
213
219
  if (!existsSync(configPath)) {
214
220
  return {
215
221
  name: 'Hermes Agent',