imprint-mcp 0.2.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/LICENSE +21 -0
  3. package/README.md +322 -0
  4. package/examples/discoverandgo/README.md +57 -0
  5. package/examples/discoverandgo/book_discoverandgo_museum_pass/cron.json +8 -0
  6. package/examples/discoverandgo/book_discoverandgo_museum_pass/index.ts +89 -0
  7. package/examples/discoverandgo/book_discoverandgo_museum_pass/workflow.json +39 -0
  8. package/examples/echo/README.md +37 -0
  9. package/examples/echo/echo_test/index.ts +31 -0
  10. package/examples/google-flights/search_google_flights/index.ts +101 -0
  11. package/examples/google-flights/search_google_flights/parser.test.ts +140 -0
  12. package/examples/google-flights/search_google_flights/parser.ts +189 -0
  13. package/examples/google-flights/search_google_flights/playbook.yaml +130 -0
  14. package/examples/google-flights/search_google_flights/workflow.json +48 -0
  15. package/examples/google-hotels/search_google_hotels/index.ts +194 -0
  16. package/examples/google-hotels/search_google_hotels/parser.test.ts +168 -0
  17. package/examples/google-hotels/search_google_hotels/parser.ts +330 -0
  18. package/examples/google-hotels/search_google_hotels/playbook.yaml +125 -0
  19. package/examples/google-hotels/search_google_hotels/workflow.json +111 -0
  20. package/examples/namecheap-domains/search_namecheap_domains/index.ts +144 -0
  21. package/examples/namecheap-domains/search_namecheap_domains/parser.ts +380 -0
  22. package/examples/namecheap-domains/search_namecheap_domains/playbook.yaml +50 -0
  23. package/examples/namecheap-domains/search_namecheap_domains/request-transform.ts +136 -0
  24. package/examples/namecheap-domains/search_namecheap_domains/workflow.json +97 -0
  25. package/examples/southwest/README.md +81 -0
  26. package/examples/southwest/search_southwest_flights/backends.json +23 -0
  27. package/examples/southwest/search_southwest_flights/cron.json +19 -0
  28. package/examples/southwest/search_southwest_flights/index.ts +110 -0
  29. package/examples/southwest/search_southwest_flights/playbook.yaml +46 -0
  30. package/examples/southwest/search_southwest_flights/workflow.json +54 -0
  31. package/package.json +78 -0
  32. package/prompts/compile-agent.md +580 -0
  33. package/prompts/intent-detection.md +198 -0
  34. package/prompts/playbook-compilation.md +279 -0
  35. package/prompts/request-triage.md +74 -0
  36. package/prompts/tool-candidate-detection.md +104 -0
  37. package/src/cli.ts +1287 -0
  38. package/src/imprint/agent.ts +468 -0
  39. package/src/imprint/app-api-hosts.ts +53 -0
  40. package/src/imprint/backend-ladder.ts +568 -0
  41. package/src/imprint/check.ts +136 -0
  42. package/src/imprint/chromium.ts +211 -0
  43. package/src/imprint/claude-cli-compile.ts +640 -0
  44. package/src/imprint/cli-credential.ts +394 -0
  45. package/src/imprint/codex-cli-compile.ts +712 -0
  46. package/src/imprint/compile-agent-types.ts +40 -0
  47. package/src/imprint/compile-agent.ts +404 -0
  48. package/src/imprint/compile-tools.ts +1389 -0
  49. package/src/imprint/compile.ts +720 -0
  50. package/src/imprint/cookie-jar.ts +246 -0
  51. package/src/imprint/credential-bundle.ts +195 -0
  52. package/src/imprint/credential-extract.ts +290 -0
  53. package/src/imprint/credential-store.ts +707 -0
  54. package/src/imprint/cron.ts +312 -0
  55. package/src/imprint/doctor.ts +223 -0
  56. package/src/imprint/emit.ts +154 -0
  57. package/src/imprint/etld.ts +134 -0
  58. package/src/imprint/freeform-redact.ts +216 -0
  59. package/src/imprint/inject-listener.ts +137 -0
  60. package/src/imprint/install.ts +795 -0
  61. package/src/imprint/integrations.ts +385 -0
  62. package/src/imprint/is-compiled.ts +2 -0
  63. package/src/imprint/json-path.ts +100 -0
  64. package/src/imprint/llm.ts +998 -0
  65. package/src/imprint/load-json.ts +54 -0
  66. package/src/imprint/log.ts +33 -0
  67. package/src/imprint/login.ts +166 -0
  68. package/src/imprint/mcp-compile-server.ts +282 -0
  69. package/src/imprint/mcp-maintenance.ts +1790 -0
  70. package/src/imprint/mcp-server.ts +350 -0
  71. package/src/imprint/multi-progress.ts +69 -0
  72. package/src/imprint/notify.ts +155 -0
  73. package/src/imprint/paths.ts +64 -0
  74. package/src/imprint/playbook-parser.ts +21 -0
  75. package/src/imprint/playbook-runner.ts +465 -0
  76. package/src/imprint/probe-backends.ts +251 -0
  77. package/src/imprint/progress.ts +28 -0
  78. package/src/imprint/record.ts +470 -0
  79. package/src/imprint/redact.ts +550 -0
  80. package/src/imprint/replay-capture.ts +387 -0
  81. package/src/imprint/request-context.ts +66 -0
  82. package/src/imprint/runtime-link.ts +73 -0
  83. package/src/imprint/runtime.ts +942 -0
  84. package/src/imprint/sensitive-keys.ts +156 -0
  85. package/src/imprint/session-diff.ts +409 -0
  86. package/src/imprint/session-merge.ts +198 -0
  87. package/src/imprint/session-writer.ts +149 -0
  88. package/src/imprint/sites.ts +27 -0
  89. package/src/imprint/stealth-fetch.ts +434 -0
  90. package/src/imprint/teach-state.ts +235 -0
  91. package/src/imprint/teach.ts +2120 -0
  92. package/src/imprint/tool-candidates.ts +423 -0
  93. package/src/imprint/tool-loader.ts +186 -0
  94. package/src/imprint/tool-selection.ts +70 -0
  95. package/src/imprint/tracing.ts +508 -0
  96. package/src/imprint/types.ts +472 -0
  97. package/src/imprint/version.ts +21 -0
@@ -0,0 +1,251 @@
1
+ /**
2
+ * `imprint probe-backends <site>` — try each backend once and write the
3
+ * ranked working order to <IMPRINT_HOME>/<site>/<toolName>/backends.json. cron + MCP
4
+ * read it at startup so they skip futile rungs every tick for sites
5
+ * where one backend is known-blocked.
6
+ */
7
+
8
+ import { createHash } from 'node:crypto';
9
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
10
+ import { resolve as pathResolve } from 'node:path';
11
+ import { runWithLadder } from './backend-ladder.ts';
12
+ import { createLog } from './log.ts';
13
+ import { imprintHomeDir } from './paths.ts';
14
+ import { availableSitesHint } from './sites.ts';
15
+ import type { StealthFetch } from './stealth-fetch.ts';
16
+ import { type ResolvedTool, discoverTools } from './tool-loader.ts';
17
+ import { selectGeneratedTool } from './tool-selection.ts';
18
+ import {
19
+ type BackendsCache,
20
+ BackendsCacheSchema,
21
+ type ConcreteBackend,
22
+ CronConfigSchema,
23
+ WorkflowSchema,
24
+ } from './types.ts';
25
+ import { VERSION } from './version.ts';
26
+
27
+ interface ProbeBackendsOptions {
28
+ site: string;
29
+ /** Override generated asset root. Defaults to IMPRINT_HOME (~/.imprint). */
30
+ assetRoot?: string;
31
+ /** Override params instead of reading cron.json / workflow defaults. */
32
+ paramOverrides?: Record<string, string | number | boolean>;
33
+ /** Where to write backends.json. Defaults to <assetRoot>/<site>/<toolName>/backends.json. */
34
+ outPath?: string;
35
+ /** Select a specific generated tool when a site has more than one. */
36
+ toolName?: string;
37
+ }
38
+
39
+ interface ProbeBackendsResult {
40
+ cache: BackendsCache;
41
+ outPath: string;
42
+ }
43
+
44
+ const log = createLog('probe');
45
+
46
+ export async function probeBackends(opts: ProbeBackendsOptions): Promise<ProbeBackendsResult> {
47
+ const assetRoot = opts.assetRoot ?? imprintHomeDir();
48
+ const discovered = await discoverTools(assetRoot, opts.site, '[imprint probe]');
49
+ const tool = selectGeneratedTool({
50
+ site: opts.site,
51
+ tools: discovered,
52
+ purpose: 'probe',
53
+ toolName: opts.toolName,
54
+ pathHint: opts.outPath,
55
+ pathHintLabel: '--out',
56
+ });
57
+ if (!tool) {
58
+ throw new Error(
59
+ `No generated tool found for site "${opts.site}".\n${availableSitesHint(assetRoot, opts.site)}\n→ run \`imprint teach ${opts.site}\` or \`imprint emit ~/.imprint/${opts.site}/<toolName>/workflow.json\` first.`,
60
+ );
61
+ }
62
+ const outPath = opts.outPath ?? pathResolve(tool.dir, 'backends.json');
63
+
64
+ const params = resolveParams(tool, opts.paramOverrides);
65
+
66
+ log(`probing fetch / fetch-bootstrap / stealth-fetch / playbook for ${tool.workflow.toolName}…`);
67
+ log(` params: ${JSON.stringify(params)}`);
68
+
69
+ // Try every backend (single-rung ladders) — operators want the full
70
+ // matrix, not just the first that worked.
71
+ const stealthCache = new Map<string, StealthFetch>();
72
+ const allBackends: ConcreteBackend[] = workflowNeedsBootstrap(tool.workflow)
73
+ ? ['fetch', 'fetch-bootstrap', 'stealth-fetch', 'playbook']
74
+ : ['fetch', 'stealth-fetch', 'playbook'];
75
+ const results: BackendsCache['results'] = {};
76
+ const working: ConcreteBackend[] = [];
77
+
78
+ for (const backend of allBackends) {
79
+ log(`probing ${backend}…`);
80
+ const t0 = Date.now();
81
+ const { result, attempts } = await runWithLadder(
82
+ [backend],
83
+ tool,
84
+ params,
85
+ assetRoot,
86
+ stealthCache,
87
+ );
88
+ const durationMs = Date.now() - t0;
89
+ const attempt = attempts[0];
90
+
91
+ if (!attempt) {
92
+ results[backend] = { outcome: 'skipped', detail: 'no attempt recorded' };
93
+ continue;
94
+ }
95
+
96
+ if (attempt.outcome === 'unavailable') {
97
+ results[backend] = { outcome: 'unavailable', detail: attempt.detail };
98
+ log(` ${backend}: unavailable (${attempt.detail})`);
99
+ continue;
100
+ }
101
+
102
+ if (result.ok) {
103
+ results[backend] = { outcome: 'ok', durationMs };
104
+ working.push(backend);
105
+ log(` ${backend}: OK in ${durationMs}ms`);
106
+ continue;
107
+ }
108
+
109
+ if (result.error === 'FORBIDDEN') {
110
+ results[backend] = {
111
+ outcome: 'forbidden',
112
+ durationMs,
113
+ detail: result.message.slice(0, 200),
114
+ };
115
+ log(` ${backend}: FORBIDDEN`);
116
+ } else {
117
+ results[backend] = {
118
+ outcome: 'failed',
119
+ durationMs,
120
+ error: result.error,
121
+ detail: result.message.slice(0, 200),
122
+ };
123
+ log(` ${backend}: ${result.error} — ${result.message.slice(0, 100)}`);
124
+ }
125
+ }
126
+
127
+ if (working.length === 0) {
128
+ const hint =
129
+ 'For bot-protected sites, ensure stealth-fetch can reach the site (try `imprint cron <site> --once` with replayBackend: stealth-fetch). For sites that need DOM walks, ensure `imprint compile-playbook` produced a working playbook.yaml.';
130
+ throw new Error(
131
+ `No backend succeeded for ${opts.site}. Results:\n${JSON.stringify(results, null, 2)}\n${hint}`,
132
+ );
133
+ }
134
+
135
+ const cache: BackendsCache = {
136
+ probedAt: new Date().toISOString(),
137
+ imprintVersion: VERSION,
138
+ schemaVersion: 2,
139
+ workflowHash: workflowHash(tool.workflow),
140
+ capabilityHash: capabilityHash(tool.workflow),
141
+ preferredOrder: working,
142
+ results,
143
+ };
144
+ BackendsCacheSchema.parse(cache); // catch schema drift early
145
+
146
+ writeFileSync(outPath, `${JSON.stringify(cache, null, 2)}\n`);
147
+ log(`wrote ${outPath} — preferred: ${working.join(' → ')}`);
148
+
149
+ return { cache, outPath };
150
+ }
151
+
152
+ function workflowNeedsBootstrap(workflow: ResolvedTool['workflow']): boolean {
153
+ if (workflow.bootstrap) return true;
154
+ return workflow.requests.some((r) =>
155
+ (r.captures ?? []).some(
156
+ (c) => c.capability === 'browser_bootstrap' || c.capability === 'stealth_bootstrap',
157
+ ),
158
+ );
159
+ }
160
+
161
+ function workflowHash(workflow: ResolvedTool['workflow']): string {
162
+ return createHash('sha256')
163
+ .update(JSON.stringify(WorkflowSchema.parse(workflow)))
164
+ .digest('hex');
165
+ }
166
+
167
+ function capabilityHash(workflow: ResolvedTool['workflow']): string {
168
+ const caps = {
169
+ bootstrap: Boolean(workflow.bootstrap),
170
+ captures: workflow.requests.flatMap((r) =>
171
+ (r.captures ?? []).map((c) => `${c.source}:${c.name}:${c.capability}`),
172
+ ),
173
+ };
174
+ return createHash('sha256').update(JSON.stringify(caps)).digest('hex');
175
+ }
176
+
177
+ /** Read backends.json. Returns null on missing/malformed — runtime
178
+ * falls back to the default ladder; a stale cache must never break cron. */
179
+ export function loadBackendsCache(
180
+ site: string,
181
+ _assetRoot: string,
182
+ toolDir?: string,
183
+ ): BackendsCache | null {
184
+ if (!toolDir) return null;
185
+ const path = pathResolve(toolDir, 'backends.json');
186
+ if (!existsSync(path)) return null;
187
+ try {
188
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
189
+ const parsed = BackendsCacheSchema.parse(raw);
190
+ if (parsed.schemaVersion && parsed.schemaVersion >= 2 && parsed.workflowHash) {
191
+ const workflowPath = pathResolve(toolDir, 'workflow.json');
192
+ if (existsSync(workflowPath)) {
193
+ const currentHash = workflowHashSync(readFileSync(workflowPath, 'utf8'));
194
+ if (currentHash !== parsed.workflowHash) {
195
+ process.stderr.write(
196
+ `[imprint] backends.json at ${path} is stale for current workflow — ignoring (run \`imprint probe-backends ${site}\` to regenerate)\n`,
197
+ );
198
+ return null;
199
+ }
200
+ }
201
+ }
202
+ return parsed;
203
+ } catch (err) {
204
+ process.stderr.write(
205
+ `[imprint] backends.json at ${path} failed to parse — ignoring (run \`imprint probe-backends ${site}\` to regenerate): ${err instanceof Error ? err.message : String(err)}\n`,
206
+ );
207
+ return null;
208
+ }
209
+ }
210
+
211
+ function workflowHashSync(workflowJson: string): string {
212
+ return createHash('sha256')
213
+ .update(JSON.stringify(WorkflowSchema.parse(JSON.parse(workflowJson))))
214
+ .digest('hex');
215
+ }
216
+
217
+ /** Param priority: caller overrides → cron.json → workflow defaults. */
218
+ function resolveParams(
219
+ tool: ResolvedTool,
220
+ overrides?: Record<string, string | number | boolean>,
221
+ ): Record<string, string | number | boolean> {
222
+ const cronPath = pathResolve(tool.dir, 'cron.json');
223
+ let cronParams: Record<string, string | number | boolean> = {};
224
+ if (existsSync(cronPath)) {
225
+ try {
226
+ const raw = JSON.parse(readFileSync(cronPath, 'utf8'));
227
+ const parsed = CronConfigSchema.safeParse(raw);
228
+ if (parsed.success) cronParams = parsed.data.params;
229
+ } catch {
230
+ // Ignore — fall through to workflow defaults
231
+ }
232
+ }
233
+
234
+ const out: Record<string, string | number | boolean> = {};
235
+ for (const p of tool.workflow.parameters) {
236
+ if (overrides && p.name in overrides) {
237
+ const v = overrides[p.name];
238
+ if (v !== undefined) out[p.name] = v;
239
+ } else if (p.name in cronParams) {
240
+ const v = cronParams[p.name];
241
+ if (v !== undefined) out[p.name] = v;
242
+ } else if (p.default !== undefined) {
243
+ out[p.name] = p.default as string | number | boolean;
244
+ } else {
245
+ throw new Error(
246
+ `Probe needs a value for required param "${p.name}". Either set it in cron.json, give it a default in workflow.json, or pass --param ${p.name}=<value>.`,
247
+ );
248
+ }
249
+ }
250
+ return out;
251
+ }
@@ -0,0 +1,28 @@
1
+ /** Shared rendering helpers for compile-agent progress events.
2
+ * Used by the `imprint generate` CLI handler and `imprint teach` spinner
3
+ * so both surfaces describe the agent's activity the same human-friendly way. */
4
+
5
+ import type { CompileAgentProgress } from './compile-agent.ts';
6
+
7
+ const FRIENDLY_TOOL_NAMES: Record<string, string> = {
8
+ read_session_summary: 'inspecting session',
9
+ read_request: 'examining a request',
10
+ read_response_body: 'reading API response',
11
+ search_response_body: 'searching response for anchors',
12
+ write_file: 'writing artifact',
13
+ read_file: 'reading file',
14
+ run_bash: 'running command',
15
+ run_tests: 'running tests',
16
+ };
17
+
18
+ export function describeAgentActivity(p: CompileAgentProgress): string {
19
+ if (p.phase === 'thinking') return 'thinking';
20
+ return FRIENDLY_TOOL_NAMES[p.toolName ?? ''] ?? `using ${p.toolName ?? 'tool'}`;
21
+ }
22
+
23
+ export function formatElapsed(ms: number): string {
24
+ const totalSec = Math.floor(ms / 1000);
25
+ const m = Math.floor(totalSec / 60);
26
+ const s = totalSec % 60;
27
+ return `${m}:${String(s).padStart(2, '0')}`;
28
+ }