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,144 @@
1
+ /**
2
+ * GENERATED by `imprint emit` — DO NOT EDIT BY HAND.
3
+ *
4
+ * Tool: search_namecheap_domains
5
+ * Site: namecheap-domains
6
+ * Intent: 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.
7
+ *
8
+ * To regenerate: imprint emit ~/.imprint/namecheap-domains/search_namecheap_domains/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_namecheap_domains",
21
+ "intent": {
22
+ "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.",
23
+ "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"
24
+ },
25
+ "parameters": [
26
+ {
27
+ "name": "query",
28
+ "type": "string",
29
+ "description": "The SLD (second-level-domain) or search term to look up (e.g. 'imprint').",
30
+ "default": "imprint"
31
+ },
32
+ {
33
+ "name": "category",
34
+ "type": "string",
35
+ "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.",
36
+ "default": ""
37
+ }
38
+ ],
39
+ "requests": [
40
+ {
41
+ "method": "GET",
42
+ "url": "https://www.namecheap.com/domains/tlds.ashx",
43
+ "headers": {
44
+ "Accept": "application/json, text/plain, */*",
45
+ "Accept-Language": "en-US,en;q=0.9",
46
+ "Referer": "https://www.namecheap.com/domains/registration/results/?domain=${param.query}",
47
+ "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",
48
+ "sec-ch-ua": "\"Chromium\";v=\"147\", \"Not.A/Brand\";v=\"8\"",
49
+ "sec-ch-ua-mobile": "?0",
50
+ "sec-ch-ua-platform": "\"macOS\""
51
+ },
52
+ "effect": "safe"
53
+ },
54
+ {
55
+ "method": "GET",
56
+ "url": "https://rtb.namecheapapi.com/api/search/${param.query}?session_id=1000000000000&search=false",
57
+ "headers": {
58
+ "Accept": "application/json, text/plain, */*",
59
+ "Accept-Language": "en-US,en;q=0.9",
60
+ "Origin": "https://www.namecheap.com",
61
+ "Referer": "https://www.namecheap.com/",
62
+ "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",
63
+ "sec-ch-ua": "\"Chromium\";v=\"147\", \"Not.A/Brand\";v=\"8\"",
64
+ "sec-ch-ua-mobile": "?0",
65
+ "sec-ch-ua-platform": "\"macOS\""
66
+ },
67
+ "effect": "safe"
68
+ },
69
+ {
70
+ "method": "GET",
71
+ "url": "https://rtb.namecheapapi.com/api/picks/${param.query}?session_id=1000000000000",
72
+ "headers": {
73
+ "Accept": "application/json, text/plain, */*",
74
+ "Accept-Language": "en-US,en;q=0.9",
75
+ "Origin": "https://www.namecheap.com",
76
+ "Referer": "https://www.namecheap.com/",
77
+ "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",
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
+ "effect": "safe"
83
+ },
84
+ {
85
+ "method": "POST",
86
+ "url": "https://www.namecheap.com/api/v1/ncpl/domainsearchgateway/domain/readBySld",
87
+ "headers": {
88
+ "Accept": "application/json, text/plain, */*",
89
+ "Accept-Language": "en-US,en;q=0.9",
90
+ "Content-Type": "application/json; charset=UTF-8",
91
+ "Origin": "https://www.namecheap.com",
92
+ "Referer": "https://www.namecheap.com/domains/registration/results/?domain=${param.query}",
93
+ "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",
94
+ "X-Requested-With": "XMLHttpRequest",
95
+ "sec-ch-ua": "\"Chromium\";v=\"147\", \"Not.A/Brand\";v=\"8\"",
96
+ "sec-ch-ua-mobile": "?0",
97
+ "sec-ch-ua-platform": "\"macOS\""
98
+ },
99
+ "body": "{\"request\":{\"sld\":\"${param.query}\",\"sources\":[\"sellerhub\",\"brandstore\"]}}",
100
+ "effect": "safe"
101
+ },
102
+ {
103
+ "method": "GET",
104
+ "url": "https://domains.revved.com/v1/domainStatus?whois=true&domains=__PLACEHOLDER__",
105
+ "headers": {
106
+ "Accept": "application/json, text/plain, */*",
107
+ "Referer": "https://www.namecheap.com/"
108
+ },
109
+ "effect": "safe"
110
+ }
111
+ ],
112
+ "site": "namecheap-domains",
113
+ "parserModule": "./parser.ts",
114
+ "requestTransformModule": "./request-transform.ts"
115
+ };
116
+
117
+ export interface SearchNamecheapDomainsInput {
118
+ /** The SLD (second-level-domain) or search term to look up (e.g. 'imprint'). */
119
+ query?: string;
120
+ /** 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. */
121
+ category?: string;
122
+ }
123
+
124
+ export async function searchNamecheapDomains(
125
+ input: SearchNamecheapDomainsInput,
126
+ opts: { credentials?: CredentialStore; fetchImpl?: typeof fetch; initialState?: Record<string, unknown> } = {},
127
+ ): Promise<ToolResult> {
128
+ const __dirname = dirname(fileURLToPath(import.meta.url));
129
+ const params: Record<string, string | number | boolean> = {
130
+ query: input.query ?? "imprint",
131
+ category: input.category ?? "",
132
+
133
+ };
134
+ return executeWorkflow({
135
+ workflow: WORKFLOW,
136
+ params,
137
+ credentials: opts.credentials,
138
+ fetchImpl: opts.fetchImpl,
139
+ initialState: opts.initialState,
140
+ workflowPath: join(__dirname, 'workflow.json'),
141
+ });
142
+ }
143
+
144
+ export { WORKFLOW };
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Parser for the Namecheap domain-search workflow.
3
+ *
4
+ * Input (context.responses):
5
+ * [0] tlds.ashx → TLD catalog with category & pricing metadata
6
+ * [1] /api/search/{sld} → exact_match + first 5 picks + 25 ranks
7
+ * [2] /api/picks/{sld} → expanded picks with aftermarket pricing
8
+ * [3] /readBySld → marketplace listings (Items[]) from Spaceship
9
+ * [4] /v1/domainStatus → availability + whois for all suggested domains
10
+ *
11
+ * Returns a clean object that merges all four sources into agent-usable shape.
12
+ * If `params.category` is provided, picks/ranks are filtered to TLDs that
13
+ * carry that CategoryName in the TLD catalog.
14
+ */
15
+
16
+ interface TldCategory {
17
+ CategoryName: string;
18
+ SeqNoOfProduct?: number;
19
+ }
20
+
21
+ interface TldPricing {
22
+ Price?: number;
23
+ Regular?: number;
24
+ Renewal?: number;
25
+ Hint?: string;
26
+ Duration?: number;
27
+ DurationType?: string;
28
+ }
29
+
30
+ interface TldEntry {
31
+ Name: string;
32
+ Type?: string;
33
+ Categories?: TldCategory[] | null;
34
+ Pricing?: TldPricing;
35
+ TldsState?: string;
36
+ WhoisguardCompatibile?: boolean;
37
+ }
38
+
39
+ interface SearchAftermarket {
40
+ domain?: string;
41
+ fast_transfer?: boolean;
42
+ price?: number;
43
+ status?: string;
44
+ type?: string;
45
+ username?: string;
46
+ }
47
+
48
+ interface SearchStatus {
49
+ available?: boolean;
50
+ lookupType?: string;
51
+ name?: string;
52
+ premium?: boolean;
53
+ reason?: string;
54
+ whois?: { createdYear?: number };
55
+ }
56
+
57
+ interface SearchPick {
58
+ domain: string;
59
+ tld: string;
60
+ type?: string;
61
+ priority?: number;
62
+ info?: string;
63
+ enable_cart_verification?: boolean;
64
+ aftermarket?: SearchAftermarket;
65
+ status?: SearchStatus;
66
+ }
67
+
68
+ interface SearchResponse {
69
+ exact_match?: {
70
+ domain?: string;
71
+ tld?: string;
72
+ is_supported?: boolean;
73
+ enable_cart_verification?: boolean;
74
+ campaignType?: string | null;
75
+ status?: SearchStatus;
76
+ };
77
+ picks?: SearchPick[];
78
+ ranks?: Array<{ domain: string; tld: string; enable_cart_verification?: boolean }>;
79
+ hasNextPage?: boolean;
80
+ type?: string;
81
+ }
82
+
83
+ interface PicksResponse {
84
+ type?: string;
85
+ picks?: SearchPick[];
86
+ }
87
+
88
+ interface MarketplaceMoney {
89
+ OriginalAmount?: number;
90
+ Amount?: number;
91
+ Currency?: string;
92
+ }
93
+
94
+ interface MarketplaceSource {
95
+ EntryPoints?: Record<string, string | null>;
96
+ DomainDisplayName?: string;
97
+ LogoUrl?: string | null;
98
+ BuyItNow?: MarketplaceMoney | null;
99
+ MinOffer?: MarketplaceMoney | null;
100
+ LeaseToOwn?: {
101
+ DownPayment?: MarketplaceMoney | null;
102
+ InstallmentPayment?: { Amount?: MarketplaceMoney; Cycles?: number } | null;
103
+ FinalPayment?: MarketplaceMoney | null;
104
+ Total?: MarketplaceMoney | null;
105
+ } | null;
106
+ Description?: string | null;
107
+ Keywords?: string[] | null;
108
+ }
109
+
110
+ interface MarketplaceItem {
111
+ DomainName: string;
112
+ DomainDisplayName?: string;
113
+ Tld?: string;
114
+ Sld?: string;
115
+ DomainHasSalePageEnabled?: boolean;
116
+ ResponseStatus?: string;
117
+ EntryPoints?: Record<string, string | null>;
118
+ Source?: { brandStore?: MarketplaceSource | null; sellerHub?: MarketplaceSource | null };
119
+ }
120
+
121
+ interface MarketplaceResponse {
122
+ Items?: MarketplaceItem[];
123
+ TotalCount?: number;
124
+ }
125
+
126
+ function buildTldIndex(tlds: TldEntry[]): Map<string, TldEntry> {
127
+ const map = new Map<string, TldEntry>();
128
+ for (const t of tlds) {
129
+ if (t && typeof t.Name === 'string') map.set(t.Name.toLowerCase(), t);
130
+ }
131
+ return map;
132
+ }
133
+
134
+ function enrichDomain(
135
+ pick: SearchPick | { domain: string; tld: string; enable_cart_verification?: boolean },
136
+ tldIndex: Map<string, TldEntry>,
137
+ statusByDomain: Map<string, { available: boolean; reason?: string; createdYear?: number; registrar?: string }>,
138
+ ) {
139
+ const tldKey = (pick.tld || '').toLowerCase();
140
+ const tldMeta = tldIndex.get(tldKey);
141
+ const aftermarket = (pick as SearchPick).aftermarket;
142
+ const aftermarketActive = !!(aftermarket && aftermarket.status && aftermarket.domain);
143
+ const domainStatus = statusByDomain.get((pick.domain || '').toLowerCase());
144
+
145
+ const available = domainStatus?.available ?? null;
146
+ const isPremium = aftermarketActive && (aftermarket?.price ?? 0) > 0;
147
+
148
+ return {
149
+ domain: pick.domain,
150
+ tld: pick.tld,
151
+ available,
152
+ is_premium: isPremium,
153
+ registered_year: domainStatus?.createdYear ?? null,
154
+ registrar: domainStatus?.registrar ?? null,
155
+ status_reason: domainStatus?.reason ?? null,
156
+ aftermarket: aftermarketActive
157
+ ? {
158
+ domain: aftermarket?.domain,
159
+ price: aftermarket?.price,
160
+ status: aftermarket?.status,
161
+ type: aftermarket?.type,
162
+ fast_transfer: aftermarket?.fast_transfer,
163
+ }
164
+ : null,
165
+ registration_price: available !== false && tldMeta?.Pricing
166
+ ? {
167
+ price: tldMeta.Pricing.Price ?? null,
168
+ regular: tldMeta.Pricing.Regular ?? null,
169
+ renewal: tldMeta.Pricing.Renewal ?? null,
170
+ hint: tldMeta.Pricing.Hint ?? null,
171
+ }
172
+ : null,
173
+ categories: (tldMeta?.Categories ?? []).map((c) => c.CategoryName),
174
+ };
175
+ }
176
+
177
+ function pickHasCategory(tld: string, category: string, tldIndex: Map<string, TldEntry>): boolean {
178
+ const meta = tldIndex.get(tld.toLowerCase());
179
+ if (!meta || !Array.isArray(meta.Categories)) return false;
180
+ const target = category.toLowerCase();
181
+ return meta.Categories.some((c) => (c.CategoryName || '').toLowerCase() === target);
182
+ }
183
+
184
+ function dedupeByDomain<T extends { domain: string }>(items: T[]): T[] {
185
+ const seen = new Set<string>();
186
+ const out: T[] = [];
187
+ for (const item of items) {
188
+ const key = (item.domain || '').toLowerCase();
189
+ if (!key || seen.has(key)) continue;
190
+ seen.add(key);
191
+ out.push(item);
192
+ }
193
+ return out;
194
+ }
195
+
196
+ function mergePicks(
197
+ searchPicks: SearchPick[],
198
+ picksDetail: SearchPick[],
199
+ ): SearchPick[] {
200
+ // Index detail picks by domain so we can prefer the richer record.
201
+ const detailIndex = new Map<string, SearchPick>();
202
+ for (const p of picksDetail) {
203
+ if (p && p.type === 'domain' && p.domain) detailIndex.set(p.domain.toLowerCase(), p);
204
+ }
205
+ const merged: SearchPick[] = [];
206
+ const seen = new Set<string>();
207
+ for (const p of searchPicks) {
208
+ if (!p || !p.domain) continue;
209
+ const key = p.domain.toLowerCase();
210
+ seen.add(key);
211
+ const detail = detailIndex.get(key);
212
+ merged.push(detail ? { ...p, ...detail } : p);
213
+ }
214
+ // Add any picksDetail entries not already represented (e.g. from /api/picks).
215
+ for (const [key, p] of detailIndex.entries()) {
216
+ if (!seen.has(key)) merged.push(p);
217
+ }
218
+ return merged;
219
+ }
220
+
221
+ function summariseMarketplace(item: MarketplaceItem) {
222
+ const sources: Array<{
223
+ name: string;
224
+ entry_points: Record<string, string | null>;
225
+ buy_it_now: number | null;
226
+ min_offer: number | null;
227
+ currency: string | null;
228
+ description: string | null;
229
+ keywords: string[] | null;
230
+ lease_to_own: {
231
+ down_payment: number | null;
232
+ installment: { amount: number | null; cycles: number | null } | null;
233
+ total: number | null;
234
+ } | null;
235
+ }> = [];
236
+ const src = item.Source ?? {};
237
+ for (const [name, info] of Object.entries(src)) {
238
+ if (!info) continue;
239
+ const lto = info.LeaseToOwn ?? null;
240
+ sources.push({
241
+ name,
242
+ entry_points: info.EntryPoints ?? {},
243
+ buy_it_now: info.BuyItNow?.Amount ?? null,
244
+ min_offer: info.MinOffer?.Amount ?? null,
245
+ currency:
246
+ info.BuyItNow?.Currency ??
247
+ info.MinOffer?.Currency ??
248
+ info.LeaseToOwn?.Total?.Currency ??
249
+ null,
250
+ description: info.Description ?? null,
251
+ keywords: info.Keywords ?? null,
252
+ lease_to_own: lto
253
+ ? {
254
+ down_payment: lto.DownPayment?.Amount ?? null,
255
+ installment: lto.InstallmentPayment
256
+ ? {
257
+ amount: lto.InstallmentPayment.Amount?.Amount ?? null,
258
+ cycles: lto.InstallmentPayment.Cycles ?? null,
259
+ }
260
+ : null,
261
+ total: lto.Total?.Amount ?? null,
262
+ }
263
+ : null,
264
+ });
265
+ }
266
+ return {
267
+ domain: item.DomainName,
268
+ tld: item.Tld,
269
+ sld: item.Sld,
270
+ sale_page_enabled: !!item.DomainHasSalePageEnabled,
271
+ entry_points: item.EntryPoints ?? {},
272
+ sources,
273
+ };
274
+ }
275
+
276
+ export function extract(
277
+ rawResponse: unknown,
278
+ context?: {
279
+ params: Record<string, string | number | boolean>;
280
+ responses: unknown[];
281
+ },
282
+ ): unknown {
283
+ const responses = context?.responses ?? [rawResponse];
284
+ const params = context?.params ?? {};
285
+ const query = String(params.query ?? '').trim();
286
+ const category = String(params.category ?? '').trim().toLowerCase();
287
+
288
+ const tldsRaw = responses[0];
289
+ const searchRaw = responses[1] as SearchResponse | undefined;
290
+ const picksRaw = responses[2] as PicksResponse | undefined;
291
+ const marketplaceRaw = responses[3] as MarketplaceResponse | undefined;
292
+ const domainStatusRaw = responses[4] as {
293
+ status?: Array<{
294
+ name: string;
295
+ available: boolean;
296
+ lookupType?: string;
297
+ reason?: string;
298
+ whois?: { createdYear?: number };
299
+ extra?: { createdYear?: number; registrar?: string; extensionsTaken?: number; ns?: string[] };
300
+ }>;
301
+ } | undefined;
302
+
303
+ const statusByDomain = new Map<string, { available: boolean; reason?: string; createdYear?: number; registrar?: string }>();
304
+ for (const s of domainStatusRaw?.status ?? []) {
305
+ statusByDomain.set(s.name.toLowerCase(), {
306
+ available: s.available,
307
+ reason: s.reason,
308
+ createdYear: s.whois?.createdYear ?? s.extra?.createdYear,
309
+ registrar: s.extra?.registrar,
310
+ });
311
+ }
312
+
313
+ const tldList: TldEntry[] = Array.isArray(tldsRaw) ? (tldsRaw as TldEntry[]) : [];
314
+ const tldIndex = buildTldIndex(tldList);
315
+
316
+ const exactMatchRaw = searchRaw?.exact_match ?? null;
317
+ const exactMatch = exactMatchRaw
318
+ ? (() => {
319
+ const ds = statusByDomain.get((exactMatchRaw.domain || '').toLowerCase());
320
+ const meta = tldIndex.get((exactMatchRaw.tld || '').toLowerCase());
321
+ return {
322
+ domain: exactMatchRaw.domain,
323
+ tld: exactMatchRaw.tld,
324
+ available: ds?.available ?? null,
325
+ registered_year: ds?.createdYear ?? null,
326
+ registrar: ds?.registrar ?? null,
327
+ status_reason: ds?.reason ?? null,
328
+ registration_price: ds?.available !== false && meta?.Pricing
329
+ ? {
330
+ price: meta.Pricing.Price ?? null,
331
+ regular: meta.Pricing.Regular ?? null,
332
+ renewal: meta.Pricing.Renewal ?? null,
333
+ hint: meta.Pricing.Hint ?? null,
334
+ }
335
+ : null,
336
+ };
337
+ })()
338
+ : null;
339
+
340
+ const detailPicks = (picksRaw?.picks ?? []).filter(
341
+ (p) => p && (p.type === 'domain' || !p.type) && p.domain,
342
+ );
343
+ const searchPicks = (searchRaw?.picks ?? []).filter((p) => p && p.domain);
344
+ const merged = mergePicks(searchPicks, detailPicks);
345
+ let picks = merged.map((p) => enrichDomain(p, tldIndex, statusByDomain));
346
+
347
+ let ranks = (searchRaw?.ranks ?? []).map((r) => enrichDomain(r, tldIndex, statusByDomain));
348
+
349
+ if (category) {
350
+ picks = picks.filter((p) => pickHasCategory(p.tld, category, tldIndex));
351
+ ranks = ranks.filter((r) => pickHasCategory(r.tld, category, tldIndex));
352
+ }
353
+
354
+ picks = dedupeByDomain(picks);
355
+ ranks = dedupeByDomain(ranks);
356
+
357
+ const marketplace = (marketplaceRaw?.Items ?? []).map(summariseMarketplace);
358
+
359
+ // Compose a flat list of TLDs that match the requested category, useful when
360
+ // the agent wants to know what other TLDs are filed under a filter button.
361
+ const categoryTlds: string[] = category
362
+ ? tldList
363
+ .filter((t) =>
364
+ (t.Categories ?? []).some((c) => (c.CategoryName || '').toLowerCase() === category),
365
+ )
366
+ .map((t) => t.Name)
367
+ : [];
368
+
369
+ return {
370
+ query,
371
+ category: category || null,
372
+ exact_match: exactMatch,
373
+ picks,
374
+ ranks,
375
+ marketplace,
376
+ marketplace_count: marketplaceRaw?.TotalCount ?? marketplace.length,
377
+ category_tlds: categoryTlds,
378
+ tld_catalog_count: tldList.length,
379
+ };
380
+ }
@@ -0,0 +1,50 @@
1
+ toolName: search_namecheap_domains
2
+ summary: Search Namecheap for available domains matching a query, optionally filtering by TLD category.
3
+ parameters:
4
+ - name: query
5
+ type: string
6
+ description: The SLD or search term to look up (e.g. 'imprint')
7
+ - name: category
8
+ type: string
9
+ description: "Optional filter category slug to narrow TLD suggestions (e.g. 'international', 'finance', 'technology', 'short', 'new', 'orless', 'businesses', 'professional', 'organizations', 'shoppingsales', 'mediamusic', 'fun', 'sportshobbies', 'transport', 'personal', 'sociallifestyle', 'fooddrink', 'cities', 'beauty', 'travel', 'healthfitness', 'environment', 'colors', 'tradesconstruction', 'nonenglish', 'religion', 'academiceducation', 'audiovideo', 'artsculture', 'marketing', 'products', 'services', 'adult', 'realestate', 'politics'). Omit or leave empty for the default Popular view."
10
+ default: ""
11
+ - name: include_marketplace
12
+ type: boolean
13
+ description: Whether to also wait for aftermarket/marketplace results (default true). Currently informational; the page fetches marketplace data automatically.
14
+ default: true
15
+ steps:
16
+ - action: navigate
17
+ url: https://www.namecheap.com/
18
+ wait_for: networkidle
19
+ - action: type
20
+ locators:
21
+ - by: id
22
+ value: static-domain-search-domain-search-input
23
+ - by: css
24
+ value: "input[name=\"domain\"][type=\"search\"]"
25
+ value: ${query}
26
+ - action: click
27
+ locators:
28
+ - by: aria_label
29
+ value: Search
30
+ - by: role
31
+ value: button
32
+ name: Search
33
+ - by: css
34
+ value: button.search-domain-btn.nchp-search-form__submit
35
+ wait_for:
36
+ xhr: /api/search/${query}
37
+ - action: wait
38
+ wait_for: networkidle
39
+ - action: click
40
+ locators:
41
+ - by: css
42
+ value: "main.results.filtering ul.filters li.filter-${category} > button"
43
+ wait_for:
44
+ xhr: /api/search/${query}
45
+ result:
46
+ source: xhr
47
+ url_pattern: /api/search/${query}
48
+ extract: picks[].domain
49
+ return_as: picks
50
+ notes: "The final filter-click step is only meaningful when ${category} is supplied; with an empty category the playbook should skip it (default Popular view is already loaded by the search submit). Each filter click triggers the same /api/search/{query} XHR; the load-bearing response shape is {exact_match, picks[], ranks[], hasNextPage, type}. Marketplace/aftermarket data is returned by a separate POST to /api/v1/ncpl/domainsearchgateway/domain/readBySld which fires automatically on the results page. Session bootstrap (SessionHandler.ashx, refid) happens via the cookies/page load — no explicit auth step required."