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,136 @@
1
+ /**
2
+ * Request transform for Namecheap RTB endpoints.
3
+ *
4
+ * The namecheap.com domain-search UI signs every request to its real-time
5
+ * bidding APIs (rtb.namecheapapi.com, etc.) with an `rcs` query param
6
+ * computed by client-side JavaScript. The signature is:
7
+ * 1. Build a canonical string:
8
+ * "<secret> <nonce32hex> <METHOD> <pathname> <key1>=<encodedValue1>&<key2>=..."
9
+ * where query pairs come from the URL's raw search string (encoded values),
10
+ * excluding any existing `rcs` key, sorted alphabetically by key. The
11
+ * values are then re-encoded with encodeURIComponent (potentially
12
+ * double-encoding), per the original JS implementation.
13
+ * 2. Compute CRC32 (signed 32-bit) of that string.
14
+ * 3. JSON.stringify({val: <crc>, n: <nonce>}).
15
+ * 4. XOR each character of the JSON with 73, then base64 the resulting
16
+ * binary string. That value, URL-encoded, becomes the rcs param.
17
+ *
18
+ * The two static "secrets" (one for *.namecheapapi.com, one for *.revved.com)
19
+ * are app-level constants embedded in the public domain-search bundle — not
20
+ * per-user secrets — and gate access to real availability/pricing data.
21
+ * Without a valid rcs the API returns sentinel "domain.com" mock data.
22
+ */
23
+
24
+ const NC_SECRET = '815e7ef93be85bebe5959f6f72d7e542';
25
+ const REVVED_SECRET = '8f6c7d5691eebd3b5090dc6b06755d58';
26
+
27
+ const NC_HOST_RE =
28
+ /(sb[.-])?(rtb|aftermarket|premiums|pricerequest|business-lookup|domain-suggestion)?\.namecheapapi\.com$/;
29
+ const REVVED_HOST_RE = /(sb-)?domains?\.revved\.com$/;
30
+
31
+ // CRC32 lookup table (matching the `crc-32` npm module v1.2.2 used by the site).
32
+ const CRC32_TABLE: Int32Array = (() => {
33
+ const t = new Int32Array(256);
34
+ for (let n = 0; n < 256; n++) {
35
+ let e = n;
36
+ for (let k = 0; k < 8; k++) {
37
+ e = e & 1 ? -306674912 ^ (e >>> 1) : e >>> 1;
38
+ }
39
+ t[n] = e;
40
+ }
41
+ return t;
42
+ })();
43
+
44
+ function crc32Str(str: string, seed = 0): number {
45
+ let r = ~seed;
46
+ for (let a = 0; a < str.length; a++) {
47
+ let i = str.charCodeAt(a);
48
+ if (i < 128) {
49
+ r = (r >>> 8) ^ CRC32_TABLE[(r ^ i) & 255]!;
50
+ } else if (i < 2048) {
51
+ r = (r >>> 8) ^ CRC32_TABLE[(r ^ (192 | ((i >> 6) & 31))) & 255]!;
52
+ r = (r >>> 8) ^ CRC32_TABLE[(r ^ (128 | (63 & i))) & 255]!;
53
+ } else if (i >= 55296 && i < 57344) {
54
+ i = 64 + (1023 & i);
55
+ const s = 1023 & str.charCodeAt(++a);
56
+ r = (r >>> 8) ^ CRC32_TABLE[(r ^ (240 | ((i >> 8) & 7))) & 255]!;
57
+ r = (r >>> 8) ^ CRC32_TABLE[(r ^ (128 | ((i >> 2) & 63))) & 255]!;
58
+ r = (r >>> 8) ^ CRC32_TABLE[(r ^ (128 | ((s >> 6) & 15) | ((3 & i) << 4))) & 255]!;
59
+ r = (r >>> 8) ^ CRC32_TABLE[(r ^ (128 | (63 & s))) & 255]!;
60
+ } else {
61
+ r = (r >>> 8) ^ CRC32_TABLE[(r ^ (224 | ((i >> 12) & 15))) & 255]!;
62
+ r = (r >>> 8) ^ CRC32_TABLE[(r ^ (128 | ((i >> 6) & 63))) & 255]!;
63
+ r = (r >>> 8) ^ CRC32_TABLE[(r ^ (128 | (63 & i))) & 255]!;
64
+ }
65
+ }
66
+ return ~r;
67
+ }
68
+
69
+ function randomNonce32Hex(): string {
70
+ let s = '';
71
+ for (let i = 0; i < 32; i++) s += Math.floor(Math.random() * 16).toString(16);
72
+ return s;
73
+ }
74
+
75
+ function chooseSecret(host: string): string | null {
76
+ if (REVVED_HOST_RE.test(host)) return REVVED_SECRET;
77
+ if (NC_HOST_RE.test(host)) return NC_SECRET;
78
+ return null;
79
+ }
80
+
81
+ export function transform(method: string, urlStr: string, responses?: unknown[]): string {
82
+ let url: URL;
83
+ try {
84
+ url = new URL(urlStr);
85
+ } catch {
86
+ return urlStr;
87
+ }
88
+
89
+ // domainStatus placeholder: replace __PLACEHOLDER__ with actual domain
90
+ // names extracted from the search response (responses[1]).
91
+ // Use string replacement instead of searchParams.set to avoid encoding commas.
92
+ if (url.pathname === '/v1/domainStatus' && urlStr.includes('__PLACEHOLDER__') && responses) {
93
+ const search = responses[1] as { picks?: Array<{ domain?: string }>; ranks?: Array<{ domain?: string }>; exact_match?: { domain?: string } } | undefined;
94
+ const domains = new Set<string>();
95
+ if (search?.exact_match?.domain) domains.add(search.exact_match.domain);
96
+ for (const p of search?.picks ?? []) { if (p?.domain) domains.add(p.domain); }
97
+ for (const r of search?.ranks ?? []) { if (r?.domain) domains.add(r.domain); }
98
+ urlStr = urlStr.replace('__PLACEHOLDER__', [...domains].join(','));
99
+ try { url = new URL(urlStr); } catch { return urlStr; }
100
+ }
101
+
102
+ const secret = chooseSecret(url.host);
103
+ if (!secret) return urlStr;
104
+
105
+ const nonce = randomNonce32Hex();
106
+ let canonical = `${secret} ${nonce} ${method.toUpperCase()} ${url.pathname} `;
107
+
108
+ const pairs: Array<[string, string]> = [];
109
+ if (url.search && url.search.startsWith('?')) {
110
+ for (const part of url.search.slice(1).split('&')) {
111
+ const idx = part.indexOf('=');
112
+ if (idx === -1) continue;
113
+ const k = part.slice(0, idx);
114
+ const v = part.slice(idx + 1);
115
+ if (k !== 'rcs') pairs.push([k, v]);
116
+ }
117
+ }
118
+ pairs.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
119
+ canonical += pairs.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
120
+
121
+ const val = crc32Str(canonical, 0);
122
+ const payload = JSON.stringify({ val, n: nonce });
123
+
124
+ let xored = '';
125
+ for (let i = 0; i < payload.length; i++) {
126
+ xored += String.fromCharCode(73 ^ payload.charCodeAt(i));
127
+ }
128
+
129
+ // Buffer is available in Bun/Node; encode the XOR'd binary string as base64.
130
+ const rcs = Buffer.from(xored, 'binary').toString('base64');
131
+
132
+ // Strip any pre-existing rcs param the caller may have placed in the URL.
133
+ const filtered = pairs.map(([k, v]) => `${k}=${v}`).join('&');
134
+ const newSearch = filtered ? `?${filtered}&rcs=${encodeURIComponent(rcs)}` : `?rcs=${encodeURIComponent(rcs)}`;
135
+ return `${url.origin}${url.pathname}${newSearch}${url.hash}`;
136
+ }
@@ -0,0 +1,97 @@
1
+ {
2
+ "toolName": "search_namecheap_domains",
3
+ "intent": {
4
+ "description": "Search Namecheap for available domains matching a query, returning the exact-match availability + pricing, suggested domain picks across many TLDs, the TLD catalog, and Spaceship/brandstore aftermarket marketplace listings.",
5
+ "userSaid": "i searched for imprint on namecheap. njow there are a lot of filters, by default i am on \"Popular\". there were about 30 filters. i cycled through all of them"
6
+ },
7
+ "parameters": [
8
+ {
9
+ "name": "query",
10
+ "type": "string",
11
+ "description": "The SLD (second-level-domain) or search term to look up (e.g. 'imprint').",
12
+ "default": "imprint"
13
+ },
14
+ {
15
+ "name": "category",
16
+ "type": "string",
17
+ "description": "Optional TLD category filter to narrow returned suggestions. One of the CategoryName values exposed by the TLD catalog: 'popular', 'international', 'new', '088domains'. Leave empty for unfiltered results.",
18
+ "default": ""
19
+ }
20
+ ],
21
+ "site": "namecheap-domains",
22
+ "requests": [
23
+ {
24
+ "method": "GET",
25
+ "url": "https://www.namecheap.com/domains/tlds.ashx",
26
+ "headers": {
27
+ "Accept": "application/json, text/plain, */*",
28
+ "Accept-Language": "en-US,en;q=0.9",
29
+ "Referer": "https://www.namecheap.com/domains/registration/results/?domain=${param.query}",
30
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36",
31
+ "sec-ch-ua": "\"Chromium\";v=\"147\", \"Not.A/Brand\";v=\"8\"",
32
+ "sec-ch-ua-mobile": "?0",
33
+ "sec-ch-ua-platform": "\"macOS\""
34
+ },
35
+ "effect": "safe"
36
+ },
37
+ {
38
+ "method": "GET",
39
+ "url": "https://rtb.namecheapapi.com/api/search/${param.query}?session_id=1000000000000&search=false",
40
+ "headers": {
41
+ "Accept": "application/json, text/plain, */*",
42
+ "Accept-Language": "en-US,en;q=0.9",
43
+ "Origin": "https://www.namecheap.com",
44
+ "Referer": "https://www.namecheap.com/",
45
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36",
46
+ "sec-ch-ua": "\"Chromium\";v=\"147\", \"Not.A/Brand\";v=\"8\"",
47
+ "sec-ch-ua-mobile": "?0",
48
+ "sec-ch-ua-platform": "\"macOS\""
49
+ },
50
+ "effect": "safe"
51
+ },
52
+ {
53
+ "method": "GET",
54
+ "url": "https://rtb.namecheapapi.com/api/picks/${param.query}?session_id=1000000000000",
55
+ "headers": {
56
+ "Accept": "application/json, text/plain, */*",
57
+ "Accept-Language": "en-US,en;q=0.9",
58
+ "Origin": "https://www.namecheap.com",
59
+ "Referer": "https://www.namecheap.com/",
60
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36",
61
+ "sec-ch-ua": "\"Chromium\";v=\"147\", \"Not.A/Brand\";v=\"8\"",
62
+ "sec-ch-ua-mobile": "?0",
63
+ "sec-ch-ua-platform": "\"macOS\""
64
+ },
65
+ "effect": "safe"
66
+ },
67
+ {
68
+ "method": "POST",
69
+ "url": "https://www.namecheap.com/api/v1/ncpl/domainsearchgateway/domain/readBySld",
70
+ "headers": {
71
+ "Accept": "application/json, text/plain, */*",
72
+ "Accept-Language": "en-US,en;q=0.9",
73
+ "Content-Type": "application/json; charset=UTF-8",
74
+ "Origin": "https://www.namecheap.com",
75
+ "Referer": "https://www.namecheap.com/domains/registration/results/?domain=${param.query}",
76
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36",
77
+ "X-Requested-With": "XMLHttpRequest",
78
+ "sec-ch-ua": "\"Chromium\";v=\"147\", \"Not.A/Brand\";v=\"8\"",
79
+ "sec-ch-ua-mobile": "?0",
80
+ "sec-ch-ua-platform": "\"macOS\""
81
+ },
82
+ "body": "{\"request\":{\"sld\":\"${param.query}\",\"sources\":[\"sellerhub\",\"brandstore\"]}}",
83
+ "effect": "safe"
84
+ },
85
+ {
86
+ "method": "GET",
87
+ "url": "https://domains.revved.com/v1/domainStatus?whois=true&domains=__PLACEHOLDER__",
88
+ "headers": {
89
+ "Accept": "application/json, text/plain, */*",
90
+ "Referer": "https://www.namecheap.com/"
91
+ },
92
+ "effect": "safe"
93
+ }
94
+ ],
95
+ "parserModule": "./parser.ts",
96
+ "requestTransformModule": "./request-transform.ts"
97
+ }
@@ -0,0 +1,81 @@
1
+ # Southwest fare-drop watcher
2
+
3
+ > Watch a Southwest route, push when the lowest fare drops below your threshold. Defeats Akamai bot detection via `stealth-fetch`.
4
+
5
+ ## What this shows off
6
+
7
+ - **The full backend ladder in action.** `fetch` returns 403 (Akamai), `stealth-fetch` mints sensor tokens via a brief Playwright bootstrap then succeeds in ~10s/call.
8
+ - **`probe-backends` skipping the futile rung.** The cached `backends.json` orders the ladder `stealth-fetch → playbook` so cron doesn't burn 200ms on a fetch attempt every tick.
9
+ - **`notifyWhen: price_below`** pushing only on real drops, with the `pricePath` extracting from real Southwest response shape.
10
+ - **The fresh-UUID header trick** — Southwest rejects stale `X-User-Experience-ID`; stealth-fetch regenerates per call.
11
+ - **Multi-path `pricePath`** — `cron.json`'s notifyWhen lists both the raw API shape (when stealth-fetch wins) and the playbook's reshaped output, so the push fires regardless of which backend produced the result.
12
+
13
+ ## Run it
14
+
15
+ ```bash
16
+ # One-time setup (if you haven't already)
17
+ bunx playwright install chromium
18
+
19
+ # Run a single tick (verifies everything still works)
20
+ imprint cron southwest --once
21
+
22
+ # Production: foreground daemon
23
+ NTFY_URL=https://ntfy.sh/your-secret-topic imprint cron southwest
24
+
25
+ # Production: OS scheduler (cron / systemd timer / launchd) — wraps --once
26
+ NTFY_URL=https://ntfy.sh/your-secret-topic imprint cron southwest --once
27
+ ```
28
+
29
+ ## What you should see
30
+
31
+ ```
32
+ [imprint cron] config: examples/southwest/search_southwest_flights/cron.json
33
+ [imprint cron] backends.json: probed 2026-05-03T22:23Z, preferred order: stealth-fetch → playbook
34
+ [imprint cron] tool: search_southwest_flights (5 param(s))
35
+ [imprint cron] schedule: 0 9 * * *
36
+ [imprint cron] notifyWhen: price_below
37
+ [imprint cron] replayBackend: auto (ladder: stealth-fetch → playbook)
38
+ [imprint backend] trying stealth-fetch…
39
+ [imprint stealth] bootstrapping…
40
+ [imprint stealth] bootstrapped in ~13s — 21 cookies, 6 sensor headers
41
+ [imprint backend] stealth-fetch: OK in ~15s
42
+ [imprint cron] OK in ~15s via stealth-fetch: {"data":{"searchResults":{...,"value":"108.40"}}}
43
+ ```
44
+
45
+ Real Southwest data, real $108.40 lowest WGA fare. Bootstrap is one-time per process; subsequent ticks reuse the stealth-fetch session.
46
+
47
+ ## Files
48
+
49
+ | File | What |
50
+ |---|---|
51
+ | `~/.imprint/southwest/sessions/<ts>.{jsonl,json}` | Raw recording (local only — may contain cookies) |
52
+ | `~/.imprint/southwest/sessions/<ts>.redacted.json` | Scrubbed for LLM analysis |
53
+ | `workflow.json` | API workflow used by stealth-fetch backend |
54
+ | `index.ts` | Generated tool function (`opts.fetchImpl` is what stealth-fetch injects into) |
55
+ | `playbook.yaml` | DOM playbook fallback — single navigate to the URL-prefilled search + XHR result extraction |
56
+ | `cron.json` | Daily 9am tick; `replayBackend: "auto"`; `price_below: 99` |
57
+ | `backends.json` | Probe artifact — `preferredOrder: ["stealth-fetch", "playbook"]` |
58
+
59
+ ## Tuning
60
+
61
+ - **Threshold**: currently `$99`. Today's lowest is $108.40 so nothing fires; lower for noisy alerts, raise to wait for a deeper drop.
62
+ - **Date**: polls one date (`departure_date: 2026-06-20`). Multiple dates = multiple `cron.json` files for now.
63
+ - **Re-probe**: re-run `imprint probe-backends southwest` if the cron starts erroring — Akamai's sensor schema changes occasionally.
64
+
65
+ ## Notes (gotchas this demo handles)
66
+
67
+ These came up during bring-up; documented so the next person knows they're handled:
68
+
69
+ - **Hidden duplicate elements.** Southwest renders both a custom dropdown and a hidden native `<select>`. Runner filters to visible.
70
+ - **Wrapper intercepts pointer events.** Clickable `<strong>` inside `role=checkbox`. Runner retries with `force: true`.
71
+ - **Date input is non-typeable.** URL-prefilled navigation sidesteps the calendar widget.
72
+ - **Vanilla Playwright gets a 403.** `navigator.webdriver` is the tell. Stealth plugin patches it.
73
+ - **Token GC race.** Playwright GCs response bodies aggressively. Runner reads inside the response handler and drains pending text() promises before extracting.
74
+ - **`networkidle` hangs on SPAs.** Persistent connections never go idle. Runner uses `domcontentloaded` + the explicit `wait_for`.
75
+ - **Stale `X-User-Experience-ID`.** stealth-fetch regenerates a fresh UUID per call AND auto-injects if the workflow dropped it.
76
+
77
+ ## Not in this demo
78
+
79
+ - Auto-booking the cheaper flight on a drop (read-only watcher).
80
+ - Multi-date sweep (one `cron.json` per date).
81
+ - Hosted execution (run foreground or wire to your OS scheduler).
@@ -0,0 +1,23 @@
1
+ {
2
+ "probedAt": "2026-05-03T22:23:26.619Z",
3
+ "imprintVersion": "0.1.0",
4
+ "preferredOrder": [
5
+ "stealth-fetch",
6
+ "playbook"
7
+ ],
8
+ "results": {
9
+ "fetch": {
10
+ "outcome": "forbidden",
11
+ "durationMs": 579,
12
+ "detail": "Request 0 returned 403: {\n \"code\": 403050700\n}"
13
+ },
14
+ "stealth-fetch": {
15
+ "outcome": "ok",
16
+ "durationMs": 12419
17
+ },
18
+ "playbook": {
19
+ "outcome": "ok",
20
+ "durationMs": 13597
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "schedule": "0 9 * * *",
3
+ "replayBackend": "auto",
4
+ "params": {
5
+ "origin_airport_code": "SJC",
6
+ "destination_airport_code": "SAN",
7
+ "departure_date": "2026-06-20",
8
+ "adult_passengers_count": 1,
9
+ "fare_type": "USD"
10
+ },
11
+ "notifyWhen": {
12
+ "type": "price_below",
13
+ "threshold": 99,
14
+ "pricePath": [
15
+ "data.searchResults.airProducts[].lowestFare.value",
16
+ "prices[]"
17
+ ]
18
+ }
19
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * GENERATED by `imprint emit` — DO NOT EDIT BY HAND.
3
+ *
4
+ * Tool: search_southwest_flights
5
+ * Site: southwest
6
+ * Intent: Search for one-way Southwest Airlines flights between two airports on a given departure date, returning available flights and fares.
7
+ *
8
+ * To regenerate: imprint emit ~/.imprint/southwest/search_southwest_flights/workflow.json --force
9
+ */
10
+
11
+ import { fileURLToPath } from 'node:url';
12
+ import { dirname, join } from 'node:path';
13
+ import {
14
+ executeWorkflow,
15
+ type CredentialStore,
16
+ } from 'imprint/runtime';
17
+ import type { ToolResult, Workflow } from 'imprint/types';
18
+
19
+ const WORKFLOW: Workflow = {
20
+ "toolName": "search_southwest_flights",
21
+ "intent": {
22
+ "description": "Search for one-way Southwest Airlines flights between two airports on a given departure date, returning available flights and fares.",
23
+ "userSaid": "i just specified the departure and arrival airports (SJC and SAN) i set teh depart date now i changed to one way trip only now i clicked search now i clicked hte low fare calendar i clicked 26 june from the low fare calendar now i clicked continue to flight times on the low fare calendar, and now it's showing me the flight prices for 26 june"
24
+ },
25
+ "parameters": [
26
+ {
27
+ "name": "origin_airport_code",
28
+ "type": "string",
29
+ "description": "IATA code for the departure airport (e.g. SJC)",
30
+ "default": "SJC"
31
+ },
32
+ {
33
+ "name": "destination_airport_code",
34
+ "type": "string",
35
+ "description": "IATA code for the arrival airport (e.g. SAN)",
36
+ "default": "SAN"
37
+ },
38
+ {
39
+ "name": "departure_date",
40
+ "type": "string",
41
+ "description": "Departure date in YYYY-MM-DD format",
42
+ "default": "2026-06-23"
43
+ },
44
+ {
45
+ "name": "adult_passengers_count",
46
+ "type": "number",
47
+ "description": "Number of adult passengers",
48
+ "default": 1
49
+ },
50
+ {
51
+ "name": "fare_type",
52
+ "type": "string",
53
+ "description": "Fare currency type: USD for dollars, POINTS for Rapid Rewards points",
54
+ "default": "USD"
55
+ }
56
+ ],
57
+ "requests": [
58
+ {
59
+ "method": "POST",
60
+ "url": "https://www.southwest.com/api/air-booking/v1/air-booking/page/air/booking/shopping",
61
+ "headers": {
62
+ "Content-Type": "application/json",
63
+ "Accept": "application/json, text/javascript, */*; q=0.01",
64
+ "X-API-Key": "${env.SOUTHWEST_API_KEY}",
65
+ "X-App-ID": "air-booking",
66
+ "X-Channel-ID": "southwest"
67
+ },
68
+ "body": "{\"adultPassengersCount\":\"${param.adult_passengers_count}\",\"adultsCount\":\"${param.adult_passengers_count}\",\"departureDate\":\"${param.departure_date}\",\"departureTimeOfDay\":\"ALL_DAY\",\"destinationAirportCode\":\"${param.destination_airport_code}\",\"fareType\":\"${param.fare_type}\",\"int\":\"HOMEQBOMAIR\",\"originationAirportCode\":\"${param.origin_airport_code}\",\"passengerType\":\"ADULT\",\"promoCode\":\"\",\"returnDate\":\"\",\"returnTimeOfDay\":\"ALL_DAY\",\"tripType\":\"oneway\",\"application\":\"air-booking\",\"site\":\"southwest\"}"
69
+ }
70
+ ],
71
+ "site": "southwest"
72
+ };
73
+
74
+ export interface SearchSouthwestFlightsInput {
75
+ /** IATA code for the departure airport (e.g. SJC) */
76
+ origin_airport_code?: string;
77
+ /** IATA code for the arrival airport (e.g. SAN) */
78
+ destination_airport_code?: string;
79
+ /** Departure date in YYYY-MM-DD format */
80
+ departure_date?: string;
81
+ /** Number of adult passengers */
82
+ adult_passengers_count?: number;
83
+ /** Fare currency type: USD for dollars, POINTS for Rapid Rewards points */
84
+ fare_type?: string;
85
+ }
86
+
87
+ export async function searchSouthwestFlights(
88
+ input: SearchSouthwestFlightsInput,
89
+ opts: { credentials?: CredentialStore; fetchImpl?: typeof fetch; initialState?: Record<string, unknown> } = {},
90
+ ): Promise<ToolResult> {
91
+ const __dirname = dirname(fileURLToPath(import.meta.url));
92
+ const params: Record<string, string | number | boolean> = {
93
+ origin_airport_code: input.origin_airport_code ?? "SJC",
94
+ destination_airport_code: input.destination_airport_code ?? "SAN",
95
+ departure_date: input.departure_date ?? "2026-06-23",
96
+ adult_passengers_count: input.adult_passengers_count ?? 1,
97
+ fare_type: input.fare_type ?? "USD",
98
+
99
+ };
100
+ return executeWorkflow({
101
+ workflow: WORKFLOW,
102
+ params,
103
+ credentials: opts.credentials,
104
+ fetchImpl: opts.fetchImpl,
105
+ initialState: opts.initialState,
106
+ workflowPath: join(__dirname, 'workflow.json'),
107
+ });
108
+ }
109
+
110
+ export { WORKFLOW };
@@ -0,0 +1,46 @@
1
+ toolName: search_southwest_flights
2
+ summary: |
3
+ Search Southwest for one-way fares between two airports on a given date.
4
+ Uses the URL-prefilled search shortcut so a single navigation triggers the
5
+ shopping XHR — no DOM walk needed. Stealth Chromium (default in the runner)
6
+ defeats Akamai's bot detection that blocks naive HTTP clients.
7
+ parameters:
8
+ - name: origin_airport_code
9
+ type: string
10
+ description: IATA origin airport code, e.g. SJC
11
+ - name: destination_airport_code
12
+ type: string
13
+ description: IATA destination airport code, e.g. SAN
14
+ - name: departure_date
15
+ type: string
16
+ description: YYYY-MM-DD
17
+ - name: adult_passengers_count
18
+ type: number
19
+ description: Number of adult passengers
20
+ default: 1
21
+ - name: fare_type
22
+ type: string
23
+ description: "USD or POINTS"
24
+ default: USD
25
+ steps:
26
+ - action: navigate
27
+ url: https://www.southwest.com/air/booking/select-depart.html?adultsCount=${adult_passengers_count}&adultPassengersCount=${adult_passengers_count}&originationAirportCode=${origin_airport_code}&destinationAirportCode=${destination_airport_code}&departureDate=${departure_date}&departureTimeOfDay=ALL_DAY&fareType=${fare_type}&int=HOMEQBOMAIR&passengerType=ADULT&promoCode=&returnDate=&returnTimeOfDay=ALL_DAY&tripType=oneway
28
+ wait_for:
29
+ xhr: /api/air-booking/v1/air-booking/page/air/booking/shopping
30
+ result:
31
+ source: xhr
32
+ url_pattern: /api/air-booking/v1/air-booking/page/air/booking/shopping
33
+ extract: data.searchResults.airProducts[].lowestFare.value
34
+ return_as: prices
35
+ notes: |
36
+ Param names match workflow.json exactly (origin_airport_code etc., not the
37
+ friendlier origin) so cron.json's params block is shared across the
38
+ fetch / stealth-fetch / playbook backends and the auto-ladder can hot-swap
39
+ between them without rewriting params.
40
+
41
+ The original recording walked through the form (autocompletes, date picker,
42
+ trip-type dropdown, search submit, Low Fare Calendar drilldown). Six
43
+ iterations revealed Southwest's date input is non-typeable (zero input
44
+ events captured on #departureDate — the user clicked the input + clicked
45
+ a calendar cell). The URL-param shortcut sidesteps every form-fill quirk
46
+ in one navigation.
@@ -0,0 +1,54 @@
1
+ {
2
+ "toolName": "search_southwest_flights",
3
+ "intent": {
4
+ "description": "Search for one-way Southwest Airlines flights between two airports on a given departure date, returning available flights and fares.",
5
+ "userSaid": "i just specified the departure and arrival airports (SJC and SAN) i set teh depart date now i changed to one way trip only now i clicked search now i clicked hte low fare calendar i clicked 26 june from the low fare calendar now i clicked continue to flight times on the low fare calendar, and now it's showing me the flight prices for 26 june"
6
+ },
7
+ "parameters": [
8
+ {
9
+ "name": "origin_airport_code",
10
+ "type": "string",
11
+ "description": "IATA code for the departure airport (e.g. SJC)",
12
+ "default": "SJC"
13
+ },
14
+ {
15
+ "name": "destination_airport_code",
16
+ "type": "string",
17
+ "description": "IATA code for the arrival airport (e.g. SAN)",
18
+ "default": "SAN"
19
+ },
20
+ {
21
+ "name": "departure_date",
22
+ "type": "string",
23
+ "description": "Departure date in YYYY-MM-DD format",
24
+ "default": "2026-06-23"
25
+ },
26
+ {
27
+ "name": "adult_passengers_count",
28
+ "type": "number",
29
+ "description": "Number of adult passengers",
30
+ "default": 1
31
+ },
32
+ {
33
+ "name": "fare_type",
34
+ "type": "string",
35
+ "description": "Fare currency type: USD for dollars, POINTS for Rapid Rewards points",
36
+ "default": "USD"
37
+ }
38
+ ],
39
+ "requests": [
40
+ {
41
+ "method": "POST",
42
+ "url": "https://www.southwest.com/api/air-booking/v1/air-booking/page/air/booking/shopping",
43
+ "headers": {
44
+ "Content-Type": "application/json",
45
+ "Accept": "application/json, text/javascript, */*; q=0.01",
46
+ "X-API-Key": "${env.SOUTHWEST_API_KEY}",
47
+ "X-App-ID": "air-booking",
48
+ "X-Channel-ID": "southwest"
49
+ },
50
+ "body": "{\"adultPassengersCount\":\"${param.adult_passengers_count}\",\"adultsCount\":\"${param.adult_passengers_count}\",\"departureDate\":\"${param.departure_date}\",\"departureTimeOfDay\":\"ALL_DAY\",\"destinationAirportCode\":\"${param.destination_airport_code}\",\"fareType\":\"${param.fare_type}\",\"int\":\"HOMEQBOMAIR\",\"originationAirportCode\":\"${param.origin_airport_code}\",\"passengerType\":\"ADULT\",\"promoCode\":\"\",\"returnDate\":\"\",\"returnTimeOfDay\":\"ALL_DAY\",\"tripType\":\"oneway\",\"application\":\"air-booking\",\"site\":\"southwest\"}"
51
+ }
52
+ ],
53
+ "site": "southwest"
54
+ }
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "imprint-mcp",
3
+ "version": "0.2.0",
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
+ "type": "module",
6
+ "exports": {
7
+ "./runtime": "./src/imprint/runtime.ts",
8
+ "./types": "./src/imprint/types.ts"
9
+ },
10
+ "license": "MIT",
11
+ "author": "Ashay Changwani",
12
+ "homepage": "https://github.com/ashaychangwani/imprint",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/ashaychangwani/imprint.git"
16
+ },
17
+ "files": [
18
+ "src/",
19
+ "prompts/",
20
+ "examples/",
21
+ "README.md",
22
+ "LICENSE",
23
+ "CHANGELOG.md"
24
+ ],
25
+ "engines": {
26
+ "bun": ">=1.3.0"
27
+ },
28
+ "bin": {
29
+ "imprint": "./src/cli.ts"
30
+ },
31
+ "scripts": {
32
+ "dev": "bun run src/cli.ts",
33
+ "imprint": "bun run src/cli.ts",
34
+ "doctor": "bun run src/cli.ts doctor",
35
+ "test": "bun test",
36
+ "lint": "biome check src test",
37
+ "lint:fix": "biome check --write src test",
38
+ "typecheck": "tsc --noEmit",
39
+ "knip": "knip --no-progress",
40
+ "deadcode": "bun run knip && bun run circular",
41
+ "circular": "madge --circular --extensions ts src/",
42
+ "check": "bun run typecheck && bun run lint && bun run test && bun run deadcode",
43
+ "bench:redact": "bun run scripts/redact-benchmark.ts",
44
+ "build:binary": "bun run scripts/build-binary.ts",
45
+ "build:binary:all": "bun run scripts/build-binary.ts --all-targets",
46
+ "changelog": "bunx git-cliff --config cliff.toml --unreleased --strip header",
47
+ "prepare": "git config core.hooksPath .githooks"
48
+ },
49
+ "dependencies": {
50
+ "@anthropic-ai/sdk": "^0.52.0",
51
+ "@arizeai/phoenix-otel": "^1.0.2",
52
+ "@clack/prompts": "^1.3.0",
53
+ "@modelcontextprotocol/sdk": "^1.0.0",
54
+ "@napi-rs/keyring": "^1.3.0",
55
+ "@noble/hashes": "^2.2.0",
56
+ "@opentelemetry/api": "^1.9.1",
57
+ "chrome-remote-interface": "^0.33.0",
58
+ "env-paths": "^3.0.0",
59
+ "libsodium-wrappers": "^0.8.4",
60
+ "node-cron": "^3.0.3",
61
+ "playwright": "^1.49.0",
62
+ "playwright-extra": "^4.3.6",
63
+ "puppeteer-extra-plugin-stealth": "^2.11.2",
64
+ "redactum": "^1.1.0",
65
+ "yaml": "^2.8.4",
66
+ "zod": "^3.24.0"
67
+ },
68
+ "devDependencies": {
69
+ "@biomejs/biome": "^1.9.4",
70
+ "@types/bun": "^1.1.0",
71
+ "@types/chrome-remote-interface": "^0.31.14",
72
+ "@types/node": "^22.10.0",
73
+ "@types/node-cron": "^3.0.11",
74
+ "knip": "^5.0.0",
75
+ "madge": "^8.0.0",
76
+ "typescript": "^5.7.0"
77
+ }
78
+ }