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 +2 -0
- package/package.json +3 -1
- package/src/cli.ts +74 -3
- package/src/imprint/backend-ladder.ts +43 -16
- package/src/imprint/cdp-browser-fetch.ts +277 -170
- package/src/imprint/cron.ts +14 -1
- package/src/imprint/doctor.ts +19 -1
- package/src/imprint/mcp-maintenance.ts +71 -6
- package/src/imprint/mcp-server.ts +29 -4
- package/src/imprint/probe-backends.ts +346 -63
- package/src/imprint/types.ts +12 -0
- package/src/imprint/update.ts +73 -0
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.
|
|
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: [
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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,
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
1204
|
-
|
|
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
|
|
1240
|
+
bootstrapUrl,
|
|
1214
1241
|
});
|
|
1215
|
-
cache.set(
|
|
1242
|
+
cache.set(cacheKey, sf);
|
|
1216
1243
|
return sf;
|
|
1217
1244
|
}
|
|
1218
1245
|
|