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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imprint-mcp",
3
- "version": "0.3.1",
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: 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
- }
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, params, credentials, [])
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(params, {
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, params, credentials, [])
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(params, {
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) {