imprint-mcp 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -180,6 +180,8 @@ When an API call gets blocked, Imprint doesn't jump to DOM replay. It escalates
180
180
 
181
181
  The full order is `fetch → fetch-bootstrap → cdp-replay → stealth-fetch → playbook`; `auto` mode walks it and stops at the first backend that works.
182
182
 
183
+ For bot-protected sites, `imprint probe-backends <site> --tool <toolName>` writes a `backends.json` preference cache so cron and MCP start from the known-good backend instead of rediscovering blocked rungs. Use `imprint probe-backends <site> --all` to refresh every tool in a multi-tool site; `imprint mcp status` reports stale or invalid backend caches before they quietly fall back to the default ladder. CDP replay records both cold and warm timings when it succeeds: a timeout-safe cold start may rank by its fast warm runtime, but a cold start above the preferred threshold stays behind cold-safe backends in durable cache order.
184
+
183
185
  Every recording compiles to *both* `workflow.json` and `playbook.yaml`, so the ladder always has a DOM fallback.
184
186
 
185
187
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imprint-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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": {
@@ -63,6 +63,7 @@
63
63
  "playwright-extra": "^4.3.6",
64
64
  "puppeteer-extra-plugin-stealth": "^2.11.2",
65
65
  "redactum": "^1.1.0",
66
+ "semver": "^7.8.4",
66
67
  "yaml": "^2.8.4",
67
68
  "zod": "^3.24.0"
68
69
  },
@@ -72,6 +73,7 @@
72
73
  "@types/chrome-remote-interface": "^0.31.14",
73
74
  "@types/node": "^22.10.0",
74
75
  "@types/node-cron": "^3.0.11",
76
+ "@types/semver": "^7.7.1",
75
77
  "knip": "^5.0.0",
76
78
  "madge": "^8.0.0",
77
79
  "typescript": "^5.7.0"
package/src/cli.ts CHANGED
@@ -346,9 +346,13 @@ export const VERB_HELP: Record<string, VerbHelp> = {
346
346
  },
347
347
  'probe-backends': {
348
348
  summary: 'Try each backend once and cache the working order to backends.json.',
349
- usage: ['imprint probe-backends <site> [--tool <toolName>] [--out <path>] [--param k=v]…'],
349
+ usage: [
350
+ 'imprint probe-backends <site> [--tool <toolName>] [--out <path>] [--param k=v]…',
351
+ 'imprint probe-backends <site> --all [--param k=v]…',
352
+ ],
350
353
  flags: [
351
354
  { name: '--tool <toolName>', description: 'Select a generated tool for multi-tool sites.' },
355
+ { name: '--all', description: 'Probe every generated tool for the site.' },
352
356
  { name: '--out <path>', description: 'Override backends.json output path.' },
353
357
  { name: '--param k=v', description: 'Override a workflow parameter (repeatable).' },
354
358
  ],
@@ -451,6 +455,17 @@ export const VERB_HELP: Record<string, VerbHelp> = {
451
455
  ],
452
456
  example: 'imprint mcp status',
453
457
  },
458
+ update: {
459
+ summary: 'Check for updates and install the latest version of imprint.',
460
+ usage: ['imprint update [--check]'],
461
+ flags: [
462
+ {
463
+ name: '--check',
464
+ description: 'Only check whether an update is available; do not install.',
465
+ },
466
+ ],
467
+ example: 'imprint update',
468
+ },
454
469
  };
455
470
 
456
471
  function printVerbHelp(verb: string): void {
@@ -621,7 +636,7 @@ async function main(argv: string[]): Promise<number> {
621
636
 
622
637
  case 'doctor': {
623
638
  const { doctor, reportDoctor } = await import('./imprint/doctor.ts');
624
- const report = reportDoctor(doctor());
639
+ const report = reportDoctor(await doctor());
625
640
  for (const line of report.lines) console.log(line);
626
641
  return report.ok ? 0 : 1;
627
642
  }
@@ -1201,6 +1216,7 @@ async function main(argv: string[]): Promise<number> {
1201
1216
  const { values } = parseArgs({
1202
1217
  args: argv.slice(2),
1203
1218
  options: {
1219
+ all: { type: 'boolean' },
1204
1220
  out: { type: 'string' },
1205
1221
  tool: { type: 'string' },
1206
1222
  param: { type: 'string', multiple: true },
@@ -1209,7 +1225,30 @@ async function main(argv: string[]): Promise<number> {
1209
1225
  });
1210
1226
  const overrides = tryParseParamKV(values.param);
1211
1227
  if (overrides === null) return 2;
1212
- const { probeBackends } = await import('./imprint/probe-backends.ts');
1228
+ if (values.all && values.tool) {
1229
+ console.error('error: --all cannot be combined with --tool');
1230
+ return 2;
1231
+ }
1232
+ if (values.all && values.out) {
1233
+ console.error('error: --all cannot be combined with --out');
1234
+ return 2;
1235
+ }
1236
+ const { probeAllBackends, probeBackends } = await import('./imprint/probe-backends.ts');
1237
+ if (values.all) {
1238
+ const results = await probeAllBackends({
1239
+ site,
1240
+ paramOverrides: Object.keys(overrides).length > 0 ? overrides : undefined,
1241
+ });
1242
+ for (const result of results) {
1243
+ console.log(`[imprint] probed → ${result.outPath}`);
1244
+ console.log(`[imprint] preferred order: ${result.cache.preferredOrder.join(' → ')}`);
1245
+ }
1246
+ console.log('');
1247
+ console.log(
1248
+ '[imprint] cron + mcp-server now skip futile rungs at startup using these caches.',
1249
+ );
1250
+ return 0;
1251
+ }
1213
1252
  const result = await probeBackends({
1214
1253
  site,
1215
1254
  outPath: values.out,
@@ -1482,6 +1521,38 @@ async function main(argv: string[]): Promise<number> {
1482
1521
  return 0;
1483
1522
  }
1484
1523
 
1524
+ case 'update': {
1525
+ const { checkForUpdate, performUpdate } = await import('./imprint/update.ts');
1526
+ const checkOnly = argv.slice(1).includes('--check');
1527
+ if (checkOnly) {
1528
+ const result = await checkForUpdate();
1529
+ if (!result) {
1530
+ console.error('Could not reach npm registry.');
1531
+ return 1;
1532
+ }
1533
+ console.log(`Current: v${result.current}`);
1534
+ console.log(`Latest: v${result.latest}`);
1535
+ if (result.updateAvailable) {
1536
+ console.log('\nUpdate available — run `imprint update` to install.');
1537
+ } else {
1538
+ console.log('\nAlready up to date.');
1539
+ }
1540
+ return 0;
1541
+ }
1542
+ console.log('Checking for updates...');
1543
+ const result = await performUpdate();
1544
+ if (result.from === result.to && result.ok) {
1545
+ console.log(`imprint v${result.from} is already the latest version.`);
1546
+ return 0;
1547
+ }
1548
+ if (result.ok) {
1549
+ console.log(`Updated imprint: v${result.from} → v${result.to}`);
1550
+ return 0;
1551
+ }
1552
+ console.error(`Update failed: ${result.error}`);
1553
+ return 1;
1554
+ }
1555
+
1485
1556
  default: {
1486
1557
  const suggestion = closestVerb(verb);
1487
1558
  const tail = suggestion ? `did you mean \`imprint ${suggestion}\`?` : 'run `imprint --help`';
@@ -181,6 +181,20 @@ const compileLastRequestAt = new Map<string, number>();
181
181
  function sleepMs(ms: number): Promise<void> {
182
182
  return new Promise((r) => setTimeout(r, ms));
183
183
  }
184
+
185
+ function withWorkflowDefaults(
186
+ workflow: Workflow,
187
+ params: Record<string, string | number | boolean>,
188
+ ): Record<string, string | number | boolean> {
189
+ const paramsWithDefaults: Record<string, string | number | boolean> = { ...params };
190
+ for (const p of workflow.parameters) {
191
+ if (!(p.name in paramsWithDefaults) && p.default !== undefined) {
192
+ paramsWithDefaults[p.name] = p.default;
193
+ }
194
+ }
195
+ return paramsWithDefaults;
196
+ }
197
+
184
198
  /** Await the per-origin min spacing before a compile-path live request. The
185
199
  * first call to an origin never waits (last=0); subsequent ones within the
186
200
  * window are delayed so the suite paces itself under the rate-flag. */
@@ -303,7 +317,8 @@ export async function runWithLadder(
303
317
  result = await runCdpReplay(tool, params, options?.cdpPool);
304
318
  break;
305
319
  case 'stealth-fetch': {
306
- const sf = ensureStealthFetch(tool, stealthCache);
320
+ const paramsWithDefaults = withWorkflowDefaults(tool.workflow, params);
321
+ const sf = await ensureStealthFetch(tool, stealthCache, paramsWithDefaults);
307
322
  // When the workflow declares a bootstrap block, mint its declared
308
323
  // session-token state (CSRF cookies etc.) from the SAME stealth
309
324
  // session that provides the transport cookies. Without this, a
@@ -313,19 +328,14 @@ export async function runWithLadder(
313
328
  const initialState = tool.workflow.bootstrap
314
329
  ? await stealthBootstrapState(sf, tool.workflow.bootstrap)
315
330
  : undefined;
316
- result = await tool.toolFn(params, { fetchImpl: sf.fetchImpl, initialState });
331
+ result = await tool.toolFn(paramsWithDefaults, { fetchImpl: sf.fetchImpl, initialState });
317
332
  break;
318
333
  }
319
334
  case 'playbook': {
320
335
  // DOM-walk last resort (the anti-bot API path is fetch-bootstrap, above).
321
336
  // Apply workflow.json's declared parameter defaults — runPlaybook
322
337
  // validates and throws on absent values regardless of declared defaults.
323
- const paramsWithDefaults: typeof params = { ...params };
324
- for (const p of tool.workflow.parameters) {
325
- if (!(p.name in paramsWithDefaults) && p.default !== undefined) {
326
- paramsWithDefaults[p.name] = p.default;
327
- }
328
- }
338
+ const paramsWithDefaults = withWorkflowDefaults(tool.workflow, params);
329
339
  result = await runPlaybook({
330
340
  playbook: playbookPath(assetRoot, tool.site, tool.dir),
331
341
  params: paramsWithDefaults,
@@ -726,8 +736,9 @@ async function runFetchBootstrap(
726
736
  values: {},
727
737
  storage: [],
728
738
  };
739
+ const paramsWithDefaults = withWorkflowDefaults(tool.workflow, params);
729
740
  const bootstrapUrl = tool.workflow.bootstrap
730
- ? substituteString(tool.workflow.bootstrap.url, params, credentials, [])
741
+ ? substituteString(tool.workflow.bootstrap.url, paramsWithDefaults, credentials, [])
731
742
  : undefined;
732
743
  const siteDir = pathResolve(tool.dir, '..');
733
744
 
@@ -799,7 +810,7 @@ async function runFetchBootstrap(
799
810
  );
800
811
  if (!captureResult.ok) return captureResult.result;
801
812
 
802
- const result = await tool.toolFn(params, {
813
+ const result = await tool.toolFn(paramsWithDefaults, {
803
814
  credentials: bootstrappedCredentials,
804
815
  initialState: captureResult.state,
805
816
  fetchImpl: makeJarUaFetch(jar.ua),
@@ -860,8 +871,9 @@ async function runCdpReplay(
860
871
  values: {},
861
872
  storage: [],
862
873
  };
874
+ const paramsWithDefaults = withWorkflowDefaults(tool.workflow, params);
863
875
  const bootstrapUrl = tool.workflow.bootstrap
864
- ? substituteString(tool.workflow.bootstrap.url, params, credentials, [])
876
+ ? substituteString(tool.workflow.bootstrap.url, paramsWithDefaults, credentials, [])
865
877
  : undefined;
866
878
 
867
879
  const siteDir = pathResolve(tool.dir, '..');
@@ -921,7 +933,7 @@ async function runCdpReplay(
921
933
  return captureResult.result;
922
934
  }
923
935
 
924
- const result = await tool.toolFn(params, {
936
+ const result = await tool.toolFn(paramsWithDefaults, {
925
937
  credentials: bootstrappedCredentials,
926
938
  initialState: captureResult.state,
927
939
  fetchImpl: cf.fetchImpl,
@@ -934,6 +946,8 @@ async function runCdpReplay(
934
946
  saveJar(siteDir, postJar);
935
947
  } catch {
936
948
  // best-effort
949
+ } finally {
950
+ if (!cdpPool && ownsSession) await cf.close();
937
951
  }
938
952
  } else {
939
953
  if (ownsSession) {
@@ -1200,8 +1214,21 @@ async function stealthBootstrapState(
1200
1214
  return state;
1201
1215
  }
1202
1216
 
1203
- function ensureStealthFetch(tool: ResolvedTool, cache: Map<string, StealthFetch>): StealthFetch {
1204
- const cached = cache.get(tool.site);
1217
+ async function ensureStealthFetch(
1218
+ tool: ResolvedTool,
1219
+ cache: Map<string, StealthFetch>,
1220
+ params: Record<string, string | number | boolean>,
1221
+ ): Promise<StealthFetch> {
1222
+ const credentials = (await loadCredentialStore(tool.site)) ?? {
1223
+ site: tool.site,
1224
+ cookies: [],
1225
+ values: {},
1226
+ };
1227
+ const bootstrapUrl = tool.workflow.bootstrap?.url
1228
+ ? substituteString(tool.workflow.bootstrap.url, params, credentials, [], 'url')
1229
+ : undefined;
1230
+ const cacheKey = bootstrapUrl ? `${tool.site}:${bootstrapUrl}` : tool.site;
1231
+ const cached = cache.get(cacheKey);
1205
1232
  if (cached) return cached;
1206
1233
  const sf = createStealthFetch({
1207
1234
  baseUrl: pickBaseUrl(tool),
@@ -1210,9 +1237,9 @@ function ensureStealthFetch(tool: ResolvedTool, cache: Map<string, StealthFetch>
1210
1237
  // minted in the same session as the anti-bot cookies. Otherwise the
1211
1238
  // stealth rung can't satisfy a `${state.X}` the workflow bootstrap was
1212
1239
  // supposed to provide, and escalation from fetch-bootstrap dead-ends.
1213
- bootstrapUrl: tool.workflow.bootstrap?.url,
1240
+ bootstrapUrl,
1214
1241
  });
1215
- cache.set(tool.site, sf);
1242
+ cache.set(cacheKey, sf);
1216
1243
  return sf;
1217
1244
  }
1218
1245