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,472 @@
1
+ /**
2
+ * Zod schemas + types shared across imprint. Capture (Session), workflow
3
+ * (Workflow + Request), runtime (ToolResult), config (Cron, NotifyWhen,
4
+ * BackendsCache), and the playbook DOM-replay schema (Locator/Step/etc).
5
+ *
6
+ * For the data-flow diagram (record → generate → emit → MCP), see
7
+ * docs/architecture.md.
8
+ */
9
+
10
+ import { z } from 'zod';
11
+
12
+ // ─── Captured session (output of `imprint record`) ──────────────────────────
13
+
14
+ const CapturedRequestSchema = z.object({
15
+ seq: z.number().int().nonnegative(),
16
+ /** ms since recording started */
17
+ timestamp: z.number(),
18
+ method: z.string(),
19
+ url: z.string(),
20
+ headers: z.record(z.string()),
21
+ body: z.string().optional(),
22
+ resourceType: z.string(),
23
+ response: z
24
+ .object({
25
+ status: z.number(),
26
+ headers: z.record(z.string()),
27
+ body: z.string().optional(),
28
+ mimeType: z.string().optional(),
29
+ })
30
+ .optional(),
31
+ });
32
+ export type CapturedRequest = z.infer<typeof CapturedRequestSchema>;
33
+
34
+ const CapturedEventSchema = z.object({
35
+ seq: z.number().int().nonnegative(),
36
+ timestamp: z.number(),
37
+ type: z.enum([
38
+ 'navigation',
39
+ 'click',
40
+ 'input',
41
+ 'change',
42
+ 'submit',
43
+ 'dom-snapshot',
44
+ 'ws-sent',
45
+ 'ws-received',
46
+ ]),
47
+ /**
48
+ * For navigation: the URL.
49
+ * For click/input/change: JSON of { selector, tag, id, name, value?, text? }.
50
+ * For submit: JSON of { selector, action, method, fields[] }.
51
+ * For ws-sent/ws-received: JSON of { url, opcode, payloadDataPreview }.
52
+ */
53
+ detail: z.string(),
54
+ });
55
+ export type CapturedEvent = z.infer<typeof CapturedEventSchema>;
56
+
57
+ const CookieSnapshotSchema = z.object({
58
+ takenAt: z.string(),
59
+ timestamp: z.number(),
60
+ label: z.enum(['start', 'end', 'manual']),
61
+ cookies: z.array(
62
+ z.object({
63
+ name: z.string(),
64
+ value: z.string(),
65
+ domain: z.string(),
66
+ path: z.string(),
67
+ expires: z.number().optional(),
68
+ httpOnly: z.boolean().optional(),
69
+ secure: z.boolean().optional(),
70
+ sameSite: z.string().optional(),
71
+ hostOnly: z.boolean().optional(),
72
+ creationIndex: z.number().optional(),
73
+ }),
74
+ ),
75
+ });
76
+ export type CookieSnapshot = z.infer<typeof CookieSnapshotSchema>;
77
+
78
+ const StorageSnapshotSchema = z.object({
79
+ takenAt: z.string(),
80
+ timestamp: z.number(),
81
+ label: z.enum(['start', 'end', 'manual']),
82
+ origin: z.string(),
83
+ localStorage: z.record(z.string()).default({}),
84
+ sessionStorage: z.record(z.string()).default({}),
85
+ });
86
+ export type StorageSnapshot = z.infer<typeof StorageSnapshotSchema>;
87
+
88
+ const NarrationSchema = z.object({
89
+ seq: z.number().int().nonnegative(),
90
+ timestamp: z.number(),
91
+ text: z.string(),
92
+ });
93
+ export type Narration = z.infer<typeof NarrationSchema>;
94
+
95
+ export const SessionSchema = z.object({
96
+ site: z.string(),
97
+ startedAt: z.string(),
98
+ url: z.string(),
99
+ imprintVersion: z.string(),
100
+ requests: z.array(CapturedRequestSchema),
101
+ events: z.array(CapturedEventSchema),
102
+ narration: z.array(NarrationSchema),
103
+ cookieSnapshots: z.array(CookieSnapshotSchema).default([]),
104
+ storageSnapshots: z.array(StorageSnapshotSchema).default([]),
105
+ });
106
+ export type Session = z.infer<typeof SessionSchema>;
107
+
108
+ // ─── Workflow (output of `imprint generate`) ────────────────────────────────
109
+
110
+ const WorkflowParameterSchema = z.object({
111
+ name: z.string(),
112
+ type: z.enum(['string', 'number', 'boolean']),
113
+ description: z.string(),
114
+ /** Optional with this default if set. */
115
+ default: z.union([z.string(), z.number(), z.boolean()]).optional(),
116
+ });
117
+ export type WorkflowParameter = z.infer<typeof WorkflowParameterSchema>;
118
+
119
+ const StateCapabilitySchema = z.enum([
120
+ 'ordinary_http',
121
+ 'browser_bootstrap',
122
+ 'stealth_bootstrap',
123
+ 'credential_required',
124
+ 'unsupported',
125
+ ]);
126
+ export type StateCapability = z.infer<typeof StateCapabilitySchema>;
127
+
128
+ const CaptureCommonSchema = z.object({
129
+ name: z.string(),
130
+ required: z.boolean().optional().default(true),
131
+ capability: StateCapabilitySchema.optional().default('ordinary_http'),
132
+ });
133
+
134
+ const CookieCaptureSchema = CaptureCommonSchema.extend({
135
+ source: z.literal('cookie'),
136
+ cookie: z.string(),
137
+ url: z.string().optional(),
138
+ domain: z.string().optional(),
139
+ path: z.string().optional(),
140
+ sameSite: z.string().optional(),
141
+ allowHttpOnlyProjection: z.boolean().optional().default(false),
142
+ });
143
+
144
+ const RequestCaptureSchema = z.discriminatedUnion('source', [
145
+ CaptureCommonSchema.extend({
146
+ source: z.literal('json'),
147
+ path: z.string(),
148
+ }),
149
+ CaptureCommonSchema.extend({
150
+ source: z.literal('response_header'),
151
+ header: z.string(),
152
+ mode: z.enum(['first', 'last', 'all']).optional().default('last'),
153
+ }),
154
+ CaptureCommonSchema.extend({
155
+ source: z.literal('text_regex'),
156
+ pattern: z.string(),
157
+ group: z.number().int().nonnegative().optional().default(1),
158
+ }),
159
+ CookieCaptureSchema,
160
+ ]);
161
+ export type RequestCapture = z.infer<typeof RequestCaptureSchema>;
162
+
163
+ const BootstrapCaptureSchema = z.discriminatedUnion('source', [
164
+ CookieCaptureSchema,
165
+ CaptureCommonSchema.extend({
166
+ source: z.literal('local_storage'),
167
+ origin: z.string(),
168
+ key: z.string(),
169
+ }),
170
+ CaptureCommonSchema.extend({
171
+ source: z.literal('session_storage'),
172
+ origin: z.string(),
173
+ key: z.string(),
174
+ }),
175
+ CaptureCommonSchema.extend({
176
+ source: z.literal('html_regex'),
177
+ pattern: z.string(),
178
+ group: z.number().int().nonnegative().optional().default(1),
179
+ }),
180
+ CaptureCommonSchema.extend({
181
+ source: z.literal('dom_attribute'),
182
+ selector: z.string(),
183
+ attribute: z.string(),
184
+ timeoutMs: z.number().int().positive().optional(),
185
+ }),
186
+ CaptureCommonSchema.extend({
187
+ source: z.literal('dom_text'),
188
+ selector: z.string(),
189
+ timeoutMs: z.number().int().positive().optional(),
190
+ }),
191
+ ]);
192
+ export type BootstrapCapture = z.infer<typeof BootstrapCaptureSchema>;
193
+
194
+ const WorkflowRequestSchema = z.object({
195
+ method: z.string(),
196
+ /** Template; ${param.X} substitutes a parameter, ${response[N].path} an
197
+ * earlier extracted value. */
198
+ url: z.string(),
199
+ headers: z.record(z.string()),
200
+ body: z.string().optional(),
201
+ /** Names → jsonpath expressions; later requests reference via ${response[N].name}. */
202
+ extract: z.record(z.string()).optional(),
203
+ captures: z.array(RequestCaptureSchema).optional(),
204
+ effect: z.enum(['safe', 'idempotent', 'unsafe']).optional(),
205
+ });
206
+ export type WorkflowRequest = z.infer<typeof WorkflowRequestSchema>;
207
+
208
+ export const WorkflowSchema = z.object({
209
+ toolName: z.string(),
210
+ intent: z.object({
211
+ description: z.string(),
212
+ /** Concatenated narration the user spoke while recording. */
213
+ userSaid: z.string().optional(),
214
+ }),
215
+ parameters: z.array(WorkflowParameterSchema),
216
+ requests: z.array(WorkflowRequestSchema),
217
+ site: z.string(),
218
+ bootstrap: z
219
+ .object({
220
+ url: z.string(),
221
+ waitUntil: z.enum(['domcontentloaded', 'load', 'networkidle']).optional(),
222
+ waitMs: z.number().int().nonnegative().optional(),
223
+ timeoutMs: z.number().int().positive().optional(),
224
+ captures: z.array(BootstrapCaptureSchema).optional(),
225
+ })
226
+ .optional(),
227
+ /** Path to a sibling parser module (relative to the workflow.json file)
228
+ * exporting `extract(rawResponse): unknown`. Applied by the runtime
229
+ * to transform the raw API response into structured agent output. */
230
+ parserModule: z.string().optional(),
231
+ /** Path to a sibling request-transform module (relative to workflow.json)
232
+ * exporting `transform(method, url, responses, params?)`.
233
+ *
234
+ * Return value:
235
+ * - `string` — the transformed URL (backward-compatible).
236
+ * - `{ url: string; body?: string; headers?: Record<string, string> }` —
237
+ * URL plus optional body and header overrides for complex body formats
238
+ * (JSPB, nested JSON-in-form) where placeholder substitution alone
239
+ * cannot handle the encoding.
240
+ *
241
+ * The optional 4th arg `params` carries the resolved workflow parameters
242
+ * so the transform can construct request bodies programmatically. */
243
+ requestTransformModule: z.string().optional(),
244
+ });
245
+ export type Workflow = z.infer<typeof WorkflowSchema>;
246
+
247
+ // ─── Generated tool runtime contract ─────────────────────────────────────────
248
+
249
+ export type StateMissingFailure =
250
+ | 'producer_unavailable'
251
+ | 'producer_ran_value_absent'
252
+ | 'ambiguous_cookie'
253
+ | 'credential_missing'
254
+ | 'unsupported_workflow';
255
+
256
+ export interface StateMissingItem {
257
+ name: string;
258
+ source: 'credential' | 'cookie' | 'state' | 'storage' | 'response' | 'workflow';
259
+ capability: StateCapability;
260
+ required: boolean;
261
+ failure: StateMissingFailure;
262
+ message: string;
263
+ }
264
+
265
+ /** Discriminated union returned by every generated tool. */
266
+ export type ToolResult<T = unknown> =
267
+ | { ok: true; data: T }
268
+ | {
269
+ ok: false;
270
+ error:
271
+ | 'AUTH_EXPIRED' // 401 — run `imprint login`
272
+ | 'FORBIDDEN' // 403 — bot detection, geo, ToS, capability mismatch
273
+ | 'NETWORK' // fetch threw / timed out
274
+ | 'RATE_LIMITED' // 429
275
+ | 'BAD_RESPONSE' // other 4xx/5xx
276
+ | 'STATE_MISSING' // required cookie/state could not be produced
277
+ | 'UNKNOWN';
278
+ message: string;
279
+ remediation?: string;
280
+ missing?: StateMissingItem[];
281
+ };
282
+
283
+ // ─── Cron config (input to `imprint cron`) ───────────────────────────────────
284
+
285
+ /** Push-on-success predicate. Without one, cron only pushes on failure.
286
+ * See docs/architecture.md for the predicate language. */
287
+ const NotifyWhenSchema = z.discriminatedUnion('type', [
288
+ z.object({
289
+ type: z.literal('price_below'),
290
+ threshold: z.number(),
291
+ /** Dot-path with [] for array iteration; see json-path.ts.
292
+ * Accepts an array of paths to try in order — useful when a tool
293
+ * returns different shapes from different backends (e.g. raw API
294
+ * shape from stealth-fetch vs. reshaped output from playbook).
295
+ * The union of values from every matching path is taken. */
296
+ pricePath: z.union([z.string(), z.array(z.string()).min(1)]),
297
+ }),
298
+ ]);
299
+ export type NotifyWhen = z.infer<typeof NotifyWhenSchema>;
300
+
301
+ /** fetch (plain API replay) → gated fetch-bootstrap (browser state init +
302
+ * API replay) → stealth-fetch (bot-defense state + API replay) → playbook
303
+ * (full DOM walk). 'auto' only inserts fetch-bootstrap for declared or
304
+ * satisfiable browser-minted state. */
305
+ const ReplayBackendSchema = z.enum([
306
+ 'fetch',
307
+ 'fetch-bootstrap',
308
+ 'stealth-fetch',
309
+ 'playbook',
310
+ 'auto',
311
+ ]);
312
+ export type ReplayBackend = z.infer<typeof ReplayBackendSchema>;
313
+
314
+ const ConcreteBackendSchema = ReplayBackendSchema.exclude(['auto']);
315
+ /** ReplayBackend without the 'auto' meta-value — what the ladder actually walks. */
316
+ export type ConcreteBackend = Exclude<ReplayBackend, 'auto'>;
317
+
318
+ /** Per-backend probe result. Written to <IMPRINT_HOME>/<site>/<toolName>/backends.json
319
+ * by `imprint probe-backends`; cron + MCP read it at startup so they
320
+ * start with the cheapest known-working backend. */
321
+ const BackendProbeResultSchema = z.discriminatedUnion('outcome', [
322
+ z.object({
323
+ outcome: z.literal('ok'),
324
+ durationMs: z.number(),
325
+ }),
326
+ z.object({
327
+ outcome: z.literal('forbidden'),
328
+ durationMs: z.number(),
329
+ detail: z.string().optional(),
330
+ }),
331
+ z.object({
332
+ outcome: z.literal('failed'),
333
+ durationMs: z.number(),
334
+ error: z.string(),
335
+ detail: z.string().optional(),
336
+ }),
337
+ z.object({
338
+ outcome: z.literal('unavailable'),
339
+ detail: z.string(),
340
+ }),
341
+ z.object({
342
+ outcome: z.literal('skipped'),
343
+ detail: z.string(),
344
+ }),
345
+ ]);
346
+
347
+ export const BackendsCacheSchema = z.object({
348
+ probedAt: z.string(),
349
+ /** Schema-bump invalidator. */
350
+ imprintVersion: z.string(),
351
+ workflowHash: z.string().optional(),
352
+ schemaVersion: z.number().optional(),
353
+ capabilityHash: z.string().optional(),
354
+ /** Ladder for runtime — preferredOrder[0] cheapest, rest fall back on
355
+ * FORBIDDEN. Excludes 'auto'. */
356
+ preferredOrder: z.array(ConcreteBackendSchema).min(1),
357
+ results: z.record(z.string(), BackendProbeResultSchema),
358
+ });
359
+ export type BackendsCache = z.infer<typeof BackendsCacheSchema>;
360
+
361
+ export const CronConfigSchema = z.object({
362
+ schedule: z.string(),
363
+ params: z.record(z.union([z.string(), z.number(), z.boolean()])).default({}),
364
+ notifyWhen: NotifyWhenSchema.optional(),
365
+ replayBackend: ReplayBackendSchema.optional().default('auto'),
366
+ });
367
+ export type CronConfig = z.infer<typeof CronConfigSchema>;
368
+
369
+ // ─── Playbook (DOM-replay artifact) ─────────────────────────────────────────
370
+
371
+ /** Locator strategies, in priority order: role+name → aria_label → text → id → css. */
372
+ const LocatorSchema = z.discriminatedUnion('by', [
373
+ z.object({
374
+ by: z.literal('role'),
375
+ value: z.string(),
376
+ name: z.string().optional(),
377
+ }),
378
+ z.object({
379
+ by: z.literal('aria_label'),
380
+ value: z.string().optional(),
381
+ value_pattern: z.string().optional(),
382
+ }),
383
+ z.object({
384
+ by: z.literal('text'),
385
+ value: z.string().optional(),
386
+ value_pattern: z.string().optional(),
387
+ }),
388
+ z.object({ by: z.literal('id'), value: z.string() }),
389
+ z.object({ by: z.literal('css'), value: z.string() }),
390
+ ]);
391
+ export type Locator = z.infer<typeof LocatorSchema>;
392
+
393
+ const WaitForSchema = z.union([
394
+ z.literal('networkidle'),
395
+ z.literal('load'),
396
+ z.literal('visible'),
397
+ z.literal('hidden'),
398
+ z.object({
399
+ xhr: z.string(),
400
+ method: z.string().optional(),
401
+ timeout_ms: z.number().int().positive().optional(),
402
+ }),
403
+ z.object({ sleep_ms: z.number().int().positive() }),
404
+ ]);
405
+ export type WaitFor = z.infer<typeof WaitForSchema>;
406
+
407
+ const PlaybookStepSchema = z.discriminatedUnion('action', [
408
+ z.object({
409
+ action: z.literal('navigate'),
410
+ url: z.string(),
411
+ wait_for: WaitForSchema.optional(),
412
+ }),
413
+ z.object({
414
+ action: z.literal('click'),
415
+ locators: z.array(LocatorSchema).min(1),
416
+ wait_for: WaitForSchema.optional(),
417
+ }),
418
+ z.object({
419
+ action: z.literal('type'),
420
+ locators: z.array(LocatorSchema).min(1),
421
+ value: z.string(),
422
+ clear: z.boolean().optional(),
423
+ wait_for: WaitForSchema.optional(),
424
+ }),
425
+ z.object({
426
+ action: z.literal('submit'),
427
+ locators: z.array(LocatorSchema).min(1),
428
+ wait_for: WaitForSchema.optional(),
429
+ }),
430
+ z.object({
431
+ action: z.literal('press'),
432
+ key: z.string(),
433
+ locators: z.array(LocatorSchema).optional(),
434
+ wait_for: WaitForSchema.optional(),
435
+ }),
436
+ z.object({
437
+ action: z.literal('wait'),
438
+ wait_for: WaitForSchema,
439
+ }),
440
+ ]);
441
+ export type PlaybookStep = z.infer<typeof PlaybookStepSchema>;
442
+
443
+ const PlaybookResultSchema = z.discriminatedUnion('source', [
444
+ z.object({
445
+ source: z.literal('xhr'),
446
+ url_pattern: z.string(),
447
+ method: z.string().optional(),
448
+ /** Dot-path with [] for array iteration (see json-path.ts). */
449
+ extract: z.string(),
450
+ return_as: z.string().default('result'),
451
+ }),
452
+ z.object({
453
+ source: z.literal('dom'),
454
+ locators: z.array(LocatorSchema).min(1),
455
+ /** "text" (innerText) or attribute name (e.g. "value", "href"). */
456
+ extract: z.string(),
457
+ return_as: z.string().default('result'),
458
+ }),
459
+ ]);
460
+ export type PlaybookResult = z.infer<typeof PlaybookResultSchema>;
461
+
462
+ // Playbook params are structurally identical to workflow params — reuse
463
+ // the same schema directly to stay in sync.
464
+ export const PlaybookSchema = z.object({
465
+ toolName: z.string(),
466
+ summary: z.string(),
467
+ parameters: z.array(WorkflowParameterSchema),
468
+ steps: z.array(PlaybookStepSchema).min(1),
469
+ result: PlaybookResultSchema,
470
+ notes: z.string().optional(),
471
+ });
472
+ export type Playbook = z.infer<typeof PlaybookSchema>;
@@ -0,0 +1,21 @@
1
+ /** Single source of truth for the imprint version — read once from
2
+ * package.json so cli.ts, record.ts, probe-backends.ts can't drift.
3
+ * Compiled binaries receive the version via --define at build time. */
4
+
5
+ import { readFileSync } from 'node:fs';
6
+ import { resolve as pathResolve } from 'node:path';
7
+
8
+ const compiledVersion = (globalThis as Record<string, unknown>).__IMPRINT_VERSION__ as
9
+ | string
10
+ | undefined;
11
+
12
+ let version: string;
13
+ if (compiledVersion) {
14
+ version = compiledVersion;
15
+ } else {
16
+ const pkgPath = pathResolve(import.meta.dir, '..', '..', 'package.json');
17
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version: string };
18
+ version = pkg.version;
19
+ }
20
+
21
+ export const VERSION = version;