imprint-mcp 0.3.1 → 0.4.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/package.json +1 -1
- package/src/cli.ts +145 -0
- package/src/imprint/backend-ladder.ts +23 -10
- package/src/imprint/cdp-browser-fetch.ts +277 -170
- package/src/imprint/export-archive.ts +355 -0
- package/src/imprint/mcp-maintenance.ts +6 -12
- package/src/imprint/teach-state.ts +37 -0
- package/src/imprint/teach.ts +62 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imprint-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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
|
@@ -62,6 +62,10 @@ INSTALL
|
|
|
62
62
|
install [<site>] Install an emitted MCP server into an AI platform.
|
|
63
63
|
uninstall [<site>] Remove an installed Imprint MCP server from an AI platform.
|
|
64
64
|
|
|
65
|
+
SHARE
|
|
66
|
+
export <site> [<site2>] Bundle site tools into a portable .tar.gz archive.
|
|
67
|
+
import <archive.tar.gz> Unpack an archive into ~/.imprint and set up tools.
|
|
68
|
+
|
|
65
69
|
RUN
|
|
66
70
|
mcp-server <site> Serve one site's tools as MCP (stdio default).
|
|
67
71
|
cron <site> Polling daemon for ~/.imprint/<site>/<toolName>/cron.json.
|
|
@@ -284,6 +288,39 @@ export const VERB_HELP: Record<string, VerbHelp> = {
|
|
|
284
288
|
],
|
|
285
289
|
example: 'imprint uninstall google-flights --platform claude-desktop',
|
|
286
290
|
},
|
|
291
|
+
export: {
|
|
292
|
+
summary:
|
|
293
|
+
'Bundle one or more site tool sets into a portable .tar.gz archive for sharing across machines.',
|
|
294
|
+
usage: ['imprint export <site> [<site2> ...] [--out <path>] [--include-credentials]'],
|
|
295
|
+
flags: [
|
|
296
|
+
{
|
|
297
|
+
name: '--out <path>',
|
|
298
|
+
description:
|
|
299
|
+
'Output path. Defaults to ./imprint-export-<site>.tar.gz (single) or ./imprint-export-<timestamp>.tar.gz (multi).',
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: '--include-credentials',
|
|
303
|
+
description: 'Embed encrypted credential bundles (prompts for a passphrase per site).',
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
example: 'imprint export avis southwest marriott --out tools.tar.gz --include-credentials',
|
|
307
|
+
},
|
|
308
|
+
import: {
|
|
309
|
+
summary: 'Unpack an imprint export archive into ~/.imprint and set up tools for use.',
|
|
310
|
+
usage: ['imprint import <archive.tar.gz> [--force] [--platform <name>]'],
|
|
311
|
+
flags: [
|
|
312
|
+
{
|
|
313
|
+
name: '--force',
|
|
314
|
+
description: 'Overwrite existing sites instead of skipping them.',
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: '--platform <name>',
|
|
318
|
+
description:
|
|
319
|
+
'Auto-install MCP servers after import: claude-code, codex, claude-desktop, openclaw, hermes.',
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
example: 'imprint import tools.tar.gz --force --platform claude-code',
|
|
323
|
+
},
|
|
287
324
|
login: {
|
|
288
325
|
summary: 'Persist auth cookies for <site> from a captured session.',
|
|
289
326
|
usage: ['imprint login <site> --from-session <session.json>'],
|
|
@@ -896,6 +933,114 @@ async function main(argv: string[]): Promise<number> {
|
|
|
896
933
|
return 0;
|
|
897
934
|
}
|
|
898
935
|
|
|
936
|
+
case 'export': {
|
|
937
|
+
const sites: string[] = [];
|
|
938
|
+
let i = 1;
|
|
939
|
+
for (; i < argv.length; i++) {
|
|
940
|
+
const arg = argv[i];
|
|
941
|
+
if (!arg || arg.startsWith('-')) break;
|
|
942
|
+
sites.push(arg);
|
|
943
|
+
}
|
|
944
|
+
if (sites.length === 0) {
|
|
945
|
+
console.error('error: `imprint export` requires at least one <site> argument.');
|
|
946
|
+
return 2;
|
|
947
|
+
}
|
|
948
|
+
const { values } = parseArgs({
|
|
949
|
+
args: argv.slice(i),
|
|
950
|
+
options: {
|
|
951
|
+
out: { type: 'string' },
|
|
952
|
+
'include-credentials': { type: 'boolean' },
|
|
953
|
+
},
|
|
954
|
+
allowPositionals: false,
|
|
955
|
+
});
|
|
956
|
+
const defaultOut =
|
|
957
|
+
sites.length === 1
|
|
958
|
+
? `imprint-export-${sites[0]}.tar.gz`
|
|
959
|
+
: `imprint-export-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.tar.gz`;
|
|
960
|
+
const out = values.out ?? defaultOut;
|
|
961
|
+
const { exportArchive } = await import('./imprint/export-archive.ts');
|
|
962
|
+
const result = await exportArchive({
|
|
963
|
+
sites,
|
|
964
|
+
out,
|
|
965
|
+
includeCredentials: values['include-credentials'],
|
|
966
|
+
});
|
|
967
|
+
console.log(`[imprint] exported → ${result.archivePath}`);
|
|
968
|
+
for (const s of result.sites) {
|
|
969
|
+
console.log(
|
|
970
|
+
`[imprint] ${s.name}: ${s.tools.length} tool${s.tools.length === 1 ? '' : 's'} (${s.tools.join(', ')})`,
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
const kb = (result.byteSize / 1024).toFixed(1);
|
|
974
|
+
console.log(`[imprint] archive size: ${kb} KB`);
|
|
975
|
+
console.log('');
|
|
976
|
+
console.log('next step:');
|
|
977
|
+
console.log(` imprint import ${out} # on the target machine`);
|
|
978
|
+
return 0;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
case 'import': {
|
|
982
|
+
const archivePath = requirePositional(argv, 'import', 'an <archive.tar.gz> argument');
|
|
983
|
+
if (archivePath === null) return 2;
|
|
984
|
+
const { values } = parseArgs({
|
|
985
|
+
args: argv.slice(2),
|
|
986
|
+
options: {
|
|
987
|
+
force: { type: 'boolean' },
|
|
988
|
+
platform: { type: 'string' },
|
|
989
|
+
},
|
|
990
|
+
allowPositionals: false,
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
if (values.platform) {
|
|
994
|
+
const { PLATFORMS } = await import('./imprint/integrations.ts');
|
|
995
|
+
if (!PLATFORMS.includes(values.platform as (typeof PLATFORMS)[number])) {
|
|
996
|
+
console.error(
|
|
997
|
+
`error: unknown platform '${values.platform}' — valid: ${PLATFORMS.join(', ')}`,
|
|
998
|
+
);
|
|
999
|
+
return 2;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const { importArchive } = await import('./imprint/export-archive.ts');
|
|
1004
|
+
const result = await importArchive({
|
|
1005
|
+
archivePath,
|
|
1006
|
+
force: values.force,
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
for (const s of result.sites) {
|
|
1010
|
+
if (s.skipped) {
|
|
1011
|
+
console.log(`[imprint] ${s.name}: skipped (already exists)`);
|
|
1012
|
+
} else {
|
|
1013
|
+
console.log(
|
|
1014
|
+
`[imprint] ${s.name}: imported ${s.tools.length} tool${s.tools.length === 1 ? '' : 's'} (${s.tools.join(', ')})${s.credentialsImported ? ' + credentials' : ''}`,
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const imported = result.sites.filter((s) => !s.skipped);
|
|
1020
|
+
if (imported.length > 0 && !values.platform) {
|
|
1021
|
+
console.log('');
|
|
1022
|
+
console.log('next steps:');
|
|
1023
|
+
for (const s of imported) {
|
|
1024
|
+
console.log(` imprint install ${s.name} # register MCP server`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (values.platform) {
|
|
1029
|
+
const { install } = await import('./imprint/install.ts');
|
|
1030
|
+
const { PLATFORMS } = await import('./imprint/integrations.ts');
|
|
1031
|
+
for (const s of imported) {
|
|
1032
|
+
const installResult = await install({
|
|
1033
|
+
site: s.name,
|
|
1034
|
+
platform: values.platform as (typeof PLATFORMS)[number],
|
|
1035
|
+
noInteractive: true,
|
|
1036
|
+
});
|
|
1037
|
+
console.log(`[imprint] ${installResult.message}`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
return 0;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
899
1044
|
case 'login': {
|
|
900
1045
|
const site = requirePositional(argv, 'login', 'a <site> argument');
|
|
901
1046
|
if (site === null) return 2;
|
|
@@ -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. */
|
|
@@ -320,12 +334,7 @@ export async function runWithLadder(
|
|
|
320
334
|
// DOM-walk last resort (the anti-bot API path is fetch-bootstrap, above).
|
|
321
335
|
// Apply workflow.json's declared parameter defaults — runPlaybook
|
|
322
336
|
// 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
|
-
}
|
|
337
|
+
const paramsWithDefaults = withWorkflowDefaults(tool.workflow, params);
|
|
329
338
|
result = await runPlaybook({
|
|
330
339
|
playbook: playbookPath(assetRoot, tool.site, tool.dir),
|
|
331
340
|
params: paramsWithDefaults,
|
|
@@ -726,8 +735,9 @@ async function runFetchBootstrap(
|
|
|
726
735
|
values: {},
|
|
727
736
|
storage: [],
|
|
728
737
|
};
|
|
738
|
+
const paramsWithDefaults = withWorkflowDefaults(tool.workflow, params);
|
|
729
739
|
const bootstrapUrl = tool.workflow.bootstrap
|
|
730
|
-
? substituteString(tool.workflow.bootstrap.url,
|
|
740
|
+
? substituteString(tool.workflow.bootstrap.url, paramsWithDefaults, credentials, [])
|
|
731
741
|
: undefined;
|
|
732
742
|
const siteDir = pathResolve(tool.dir, '..');
|
|
733
743
|
|
|
@@ -799,7 +809,7 @@ async function runFetchBootstrap(
|
|
|
799
809
|
);
|
|
800
810
|
if (!captureResult.ok) return captureResult.result;
|
|
801
811
|
|
|
802
|
-
const result = await tool.toolFn(
|
|
812
|
+
const result = await tool.toolFn(paramsWithDefaults, {
|
|
803
813
|
credentials: bootstrappedCredentials,
|
|
804
814
|
initialState: captureResult.state,
|
|
805
815
|
fetchImpl: makeJarUaFetch(jar.ua),
|
|
@@ -860,8 +870,9 @@ async function runCdpReplay(
|
|
|
860
870
|
values: {},
|
|
861
871
|
storage: [],
|
|
862
872
|
};
|
|
873
|
+
const paramsWithDefaults = withWorkflowDefaults(tool.workflow, params);
|
|
863
874
|
const bootstrapUrl = tool.workflow.bootstrap
|
|
864
|
-
? substituteString(tool.workflow.bootstrap.url,
|
|
875
|
+
? substituteString(tool.workflow.bootstrap.url, paramsWithDefaults, credentials, [])
|
|
865
876
|
: undefined;
|
|
866
877
|
|
|
867
878
|
const siteDir = pathResolve(tool.dir, '..');
|
|
@@ -921,7 +932,7 @@ async function runCdpReplay(
|
|
|
921
932
|
return captureResult.result;
|
|
922
933
|
}
|
|
923
934
|
|
|
924
|
-
const result = await tool.toolFn(
|
|
935
|
+
const result = await tool.toolFn(paramsWithDefaults, {
|
|
925
936
|
credentials: bootstrappedCredentials,
|
|
926
937
|
initialState: captureResult.state,
|
|
927
938
|
fetchImpl: cf.fetchImpl,
|
|
@@ -934,6 +945,8 @@ async function runCdpReplay(
|
|
|
934
945
|
saveJar(siteDir, postJar);
|
|
935
946
|
} catch {
|
|
936
947
|
// best-effort
|
|
948
|
+
} finally {
|
|
949
|
+
if (!cdpPool && ownsSession) await cf.close();
|
|
937
950
|
}
|
|
938
951
|
} else {
|
|
939
952
|
if (ownsSession) {
|