@apitap/core 1.0.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 (236) hide show
  1. package/LICENSE +60 -0
  2. package/README.md +362 -0
  3. package/SKILL.md +270 -0
  4. package/dist/auth/crypto.d.ts +31 -0
  5. package/dist/auth/crypto.js +66 -0
  6. package/dist/auth/crypto.js.map +1 -0
  7. package/dist/auth/handoff.d.ts +29 -0
  8. package/dist/auth/handoff.js +180 -0
  9. package/dist/auth/handoff.js.map +1 -0
  10. package/dist/auth/manager.d.ts +46 -0
  11. package/dist/auth/manager.js +127 -0
  12. package/dist/auth/manager.js.map +1 -0
  13. package/dist/auth/oauth-refresh.d.ts +16 -0
  14. package/dist/auth/oauth-refresh.js +91 -0
  15. package/dist/auth/oauth-refresh.js.map +1 -0
  16. package/dist/auth/refresh.d.ts +43 -0
  17. package/dist/auth/refresh.js +217 -0
  18. package/dist/auth/refresh.js.map +1 -0
  19. package/dist/capture/anti-bot.d.ts +15 -0
  20. package/dist/capture/anti-bot.js +43 -0
  21. package/dist/capture/anti-bot.js.map +1 -0
  22. package/dist/capture/blocklist.d.ts +6 -0
  23. package/dist/capture/blocklist.js +70 -0
  24. package/dist/capture/blocklist.js.map +1 -0
  25. package/dist/capture/body-diff.d.ts +8 -0
  26. package/dist/capture/body-diff.js +102 -0
  27. package/dist/capture/body-diff.js.map +1 -0
  28. package/dist/capture/body-variables.d.ts +13 -0
  29. package/dist/capture/body-variables.js +142 -0
  30. package/dist/capture/body-variables.js.map +1 -0
  31. package/dist/capture/domain.d.ts +8 -0
  32. package/dist/capture/domain.js +34 -0
  33. package/dist/capture/domain.js.map +1 -0
  34. package/dist/capture/entropy.d.ts +33 -0
  35. package/dist/capture/entropy.js +100 -0
  36. package/dist/capture/entropy.js.map +1 -0
  37. package/dist/capture/filter.d.ts +11 -0
  38. package/dist/capture/filter.js +49 -0
  39. package/dist/capture/filter.js.map +1 -0
  40. package/dist/capture/graphql.d.ts +21 -0
  41. package/dist/capture/graphql.js +99 -0
  42. package/dist/capture/graphql.js.map +1 -0
  43. package/dist/capture/idle.d.ts +23 -0
  44. package/dist/capture/idle.js +44 -0
  45. package/dist/capture/idle.js.map +1 -0
  46. package/dist/capture/monitor.d.ts +26 -0
  47. package/dist/capture/monitor.js +183 -0
  48. package/dist/capture/monitor.js.map +1 -0
  49. package/dist/capture/oauth-detector.d.ts +18 -0
  50. package/dist/capture/oauth-detector.js +96 -0
  51. package/dist/capture/oauth-detector.js.map +1 -0
  52. package/dist/capture/pagination.d.ts +9 -0
  53. package/dist/capture/pagination.js +40 -0
  54. package/dist/capture/pagination.js.map +1 -0
  55. package/dist/capture/parameterize.d.ts +17 -0
  56. package/dist/capture/parameterize.js +63 -0
  57. package/dist/capture/parameterize.js.map +1 -0
  58. package/dist/capture/scrubber.d.ts +5 -0
  59. package/dist/capture/scrubber.js +38 -0
  60. package/dist/capture/scrubber.js.map +1 -0
  61. package/dist/capture/session.d.ts +46 -0
  62. package/dist/capture/session.js +445 -0
  63. package/dist/capture/session.js.map +1 -0
  64. package/dist/capture/token-detector.d.ts +16 -0
  65. package/dist/capture/token-detector.js +62 -0
  66. package/dist/capture/token-detector.js.map +1 -0
  67. package/dist/capture/verifier.d.ts +17 -0
  68. package/dist/capture/verifier.js +147 -0
  69. package/dist/capture/verifier.js.map +1 -0
  70. package/dist/cli.d.ts +2 -0
  71. package/dist/cli.js +930 -0
  72. package/dist/cli.js.map +1 -0
  73. package/dist/discovery/auth.d.ts +17 -0
  74. package/dist/discovery/auth.js +81 -0
  75. package/dist/discovery/auth.js.map +1 -0
  76. package/dist/discovery/fetch.d.ts +17 -0
  77. package/dist/discovery/fetch.js +59 -0
  78. package/dist/discovery/fetch.js.map +1 -0
  79. package/dist/discovery/frameworks.d.ts +11 -0
  80. package/dist/discovery/frameworks.js +249 -0
  81. package/dist/discovery/frameworks.js.map +1 -0
  82. package/dist/discovery/index.d.ts +21 -0
  83. package/dist/discovery/index.js +219 -0
  84. package/dist/discovery/index.js.map +1 -0
  85. package/dist/discovery/openapi.d.ts +13 -0
  86. package/dist/discovery/openapi.js +175 -0
  87. package/dist/discovery/openapi.js.map +1 -0
  88. package/dist/discovery/probes.d.ts +9 -0
  89. package/dist/discovery/probes.js +70 -0
  90. package/dist/discovery/probes.js.map +1 -0
  91. package/dist/index.d.ts +25 -0
  92. package/dist/index.js +25 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/inspect/report.d.ts +52 -0
  95. package/dist/inspect/report.js +191 -0
  96. package/dist/inspect/report.js.map +1 -0
  97. package/dist/mcp.d.ts +8 -0
  98. package/dist/mcp.js +526 -0
  99. package/dist/mcp.js.map +1 -0
  100. package/dist/orchestration/browse.d.ts +38 -0
  101. package/dist/orchestration/browse.js +198 -0
  102. package/dist/orchestration/browse.js.map +1 -0
  103. package/dist/orchestration/cache.d.ts +15 -0
  104. package/dist/orchestration/cache.js +24 -0
  105. package/dist/orchestration/cache.js.map +1 -0
  106. package/dist/plugin.d.ts +17 -0
  107. package/dist/plugin.js +158 -0
  108. package/dist/plugin.js.map +1 -0
  109. package/dist/read/decoders/deepwiki.d.ts +2 -0
  110. package/dist/read/decoders/deepwiki.js +148 -0
  111. package/dist/read/decoders/deepwiki.js.map +1 -0
  112. package/dist/read/decoders/grokipedia.d.ts +2 -0
  113. package/dist/read/decoders/grokipedia.js +210 -0
  114. package/dist/read/decoders/grokipedia.js.map +1 -0
  115. package/dist/read/decoders/hackernews.d.ts +2 -0
  116. package/dist/read/decoders/hackernews.js +168 -0
  117. package/dist/read/decoders/hackernews.js.map +1 -0
  118. package/dist/read/decoders/index.d.ts +2 -0
  119. package/dist/read/decoders/index.js +12 -0
  120. package/dist/read/decoders/index.js.map +1 -0
  121. package/dist/read/decoders/reddit.d.ts +2 -0
  122. package/dist/read/decoders/reddit.js +142 -0
  123. package/dist/read/decoders/reddit.js.map +1 -0
  124. package/dist/read/decoders/twitter.d.ts +12 -0
  125. package/dist/read/decoders/twitter.js +187 -0
  126. package/dist/read/decoders/twitter.js.map +1 -0
  127. package/dist/read/decoders/wikipedia.d.ts +2 -0
  128. package/dist/read/decoders/wikipedia.js +66 -0
  129. package/dist/read/decoders/wikipedia.js.map +1 -0
  130. package/dist/read/decoders/youtube.d.ts +2 -0
  131. package/dist/read/decoders/youtube.js +69 -0
  132. package/dist/read/decoders/youtube.js.map +1 -0
  133. package/dist/read/extract.d.ts +25 -0
  134. package/dist/read/extract.js +320 -0
  135. package/dist/read/extract.js.map +1 -0
  136. package/dist/read/index.d.ts +14 -0
  137. package/dist/read/index.js +66 -0
  138. package/dist/read/index.js.map +1 -0
  139. package/dist/read/peek.d.ts +9 -0
  140. package/dist/read/peek.js +137 -0
  141. package/dist/read/peek.js.map +1 -0
  142. package/dist/read/types.d.ts +44 -0
  143. package/dist/read/types.js +3 -0
  144. package/dist/read/types.js.map +1 -0
  145. package/dist/replay/engine.d.ts +53 -0
  146. package/dist/replay/engine.js +441 -0
  147. package/dist/replay/engine.js.map +1 -0
  148. package/dist/replay/truncate.d.ts +16 -0
  149. package/dist/replay/truncate.js +92 -0
  150. package/dist/replay/truncate.js.map +1 -0
  151. package/dist/serve.d.ts +31 -0
  152. package/dist/serve.js +149 -0
  153. package/dist/serve.js.map +1 -0
  154. package/dist/skill/generator.d.ts +44 -0
  155. package/dist/skill/generator.js +419 -0
  156. package/dist/skill/generator.js.map +1 -0
  157. package/dist/skill/importer.d.ts +26 -0
  158. package/dist/skill/importer.js +80 -0
  159. package/dist/skill/importer.js.map +1 -0
  160. package/dist/skill/search.d.ts +19 -0
  161. package/dist/skill/search.js +51 -0
  162. package/dist/skill/search.js.map +1 -0
  163. package/dist/skill/signing.d.ts +16 -0
  164. package/dist/skill/signing.js +34 -0
  165. package/dist/skill/signing.js.map +1 -0
  166. package/dist/skill/ssrf.d.ts +27 -0
  167. package/dist/skill/ssrf.js +210 -0
  168. package/dist/skill/ssrf.js.map +1 -0
  169. package/dist/skill/store.d.ts +7 -0
  170. package/dist/skill/store.js +93 -0
  171. package/dist/skill/store.js.map +1 -0
  172. package/dist/stats/report.d.ts +26 -0
  173. package/dist/stats/report.js +157 -0
  174. package/dist/stats/report.js.map +1 -0
  175. package/dist/types.d.ts +214 -0
  176. package/dist/types.js +3 -0
  177. package/dist/types.js.map +1 -0
  178. package/package.json +58 -0
  179. package/src/auth/crypto.ts +92 -0
  180. package/src/auth/handoff.ts +229 -0
  181. package/src/auth/manager.ts +140 -0
  182. package/src/auth/oauth-refresh.ts +120 -0
  183. package/src/auth/refresh.ts +300 -0
  184. package/src/capture/anti-bot.ts +63 -0
  185. package/src/capture/blocklist.ts +75 -0
  186. package/src/capture/body-diff.ts +109 -0
  187. package/src/capture/body-variables.ts +156 -0
  188. package/src/capture/domain.ts +34 -0
  189. package/src/capture/entropy.ts +121 -0
  190. package/src/capture/filter.ts +56 -0
  191. package/src/capture/graphql.ts +124 -0
  192. package/src/capture/idle.ts +45 -0
  193. package/src/capture/monitor.ts +224 -0
  194. package/src/capture/oauth-detector.ts +106 -0
  195. package/src/capture/pagination.ts +49 -0
  196. package/src/capture/parameterize.ts +68 -0
  197. package/src/capture/scrubber.ts +49 -0
  198. package/src/capture/session.ts +502 -0
  199. package/src/capture/token-detector.ts +76 -0
  200. package/src/capture/verifier.ts +171 -0
  201. package/src/cli.ts +1031 -0
  202. package/src/discovery/auth.ts +99 -0
  203. package/src/discovery/fetch.ts +85 -0
  204. package/src/discovery/frameworks.ts +231 -0
  205. package/src/discovery/index.ts +256 -0
  206. package/src/discovery/openapi.ts +230 -0
  207. package/src/discovery/probes.ts +76 -0
  208. package/src/index.ts +26 -0
  209. package/src/inspect/report.ts +247 -0
  210. package/src/mcp.ts +618 -0
  211. package/src/orchestration/browse.ts +250 -0
  212. package/src/orchestration/cache.ts +37 -0
  213. package/src/plugin.ts +188 -0
  214. package/src/read/decoders/deepwiki.ts +180 -0
  215. package/src/read/decoders/grokipedia.ts +246 -0
  216. package/src/read/decoders/hackernews.ts +198 -0
  217. package/src/read/decoders/index.ts +15 -0
  218. package/src/read/decoders/reddit.ts +158 -0
  219. package/src/read/decoders/twitter.ts +211 -0
  220. package/src/read/decoders/wikipedia.ts +75 -0
  221. package/src/read/decoders/youtube.ts +75 -0
  222. package/src/read/extract.ts +396 -0
  223. package/src/read/index.ts +78 -0
  224. package/src/read/peek.ts +175 -0
  225. package/src/read/types.ts +37 -0
  226. package/src/replay/engine.ts +559 -0
  227. package/src/replay/truncate.ts +116 -0
  228. package/src/serve.ts +189 -0
  229. package/src/skill/generator.ts +473 -0
  230. package/src/skill/importer.ts +107 -0
  231. package/src/skill/search.ts +76 -0
  232. package/src/skill/signing.ts +36 -0
  233. package/src/skill/ssrf.ts +238 -0
  234. package/src/skill/store.ts +107 -0
  235. package/src/stats/report.ts +208 -0
  236. package/src/types.ts +233 -0
package/src/cli.ts ADDED
@@ -0,0 +1,1031 @@
1
+ #!/usr/bin/env node
2
+ // src/cli.ts
3
+ import { capture } from './capture/monitor.js';
4
+ import { writeSkillFile, readSkillFile, listSkillFiles } from './skill/store.js';
5
+ import { replayEndpoint } from './replay/engine.js';
6
+ import { AuthManager, getMachineId } from './auth/manager.js';
7
+ import { deriveKey } from './auth/crypto.js';
8
+ import { signSkillFile } from './skill/signing.js';
9
+ import { importSkillFile } from './skill/importer.js';
10
+ import { resolveAndValidateUrl } from './skill/ssrf.js';
11
+ import { verifyEndpoints } from './capture/verifier.js';
12
+ import { searchSkills } from './skill/search.js';
13
+ import { refreshTokens } from './auth/refresh.js';
14
+ import { parseJwtClaims } from './capture/entropy.js';
15
+ import { createServeServer, buildServeTools } from './serve.js';
16
+ import { buildInspectReport, formatInspectHuman } from './inspect/report.js';
17
+ import { generateStatsReport, formatStatsHuman } from './stats/report.js';
18
+ import { detectAntiBot, type AntiBotSignal } from './capture/anti-bot.js';
19
+ import { discover } from './discovery/index.js';
20
+ import { peek } from './read/peek.js';
21
+ import { read } from './read/index.js';
22
+ import { homedir } from 'node:os';
23
+ import { join } from 'node:path';
24
+ import { readFileSync } from 'node:fs';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
28
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
29
+ const VERSION = pkg.version as string;
30
+
31
+ interface ParsedArgs {
32
+ command: string;
33
+ positional: string[];
34
+ flags: Record<string, string | boolean>;
35
+ }
36
+
37
+ function parseArgs(argv: string[]): ParsedArgs {
38
+ const [command = 'help', ...rest] = argv;
39
+ const positional: string[] = [];
40
+ const flags: Record<string, string | boolean> = {};
41
+
42
+ for (let i = 0; i < rest.length; i++) {
43
+ if (rest[i].startsWith('--')) {
44
+ const key = rest[i].slice(2);
45
+ const next = rest[i + 1];
46
+ if (next && !next.startsWith('--')) {
47
+ flags[key] = next;
48
+ i++;
49
+ } else {
50
+ flags[key] = true;
51
+ }
52
+ } else {
53
+ positional.push(rest[i]);
54
+ }
55
+ }
56
+
57
+ return { command, positional, flags };
58
+ }
59
+
60
+ function printUsage(): void {
61
+ console.log(`
62
+ apitap — API interception for AI agents
63
+
64
+ Usage:
65
+ apitap capture <url> Capture API traffic from a website
66
+ apitap discover <url> Detect APIs without a browser (fast recon)
67
+ apitap inspect <url> Discover APIs without saving (X-ray vision)
68
+ apitap search <query> Search skill files for a domain or endpoint
69
+ apitap list List available skill files
70
+ apitap show <domain> Show endpoints for a domain
71
+ apitap replay <domain> <endpoint-id> [key=value...]
72
+ Replay an API endpoint
73
+ apitap import <file> Import a skill file with safety validation
74
+ apitap refresh <domain> Refresh auth tokens via browser
75
+ apitap auth [domain] View or manage stored auth
76
+ apitap serve <domain> Serve a skill file as an MCP server
77
+ apitap browse <url> Browse a URL (discover + replay in one step)
78
+ apitap peek <url> Zero-cost triage (HEAD only)
79
+ apitap read <url> Extract content without a browser
80
+ apitap stats Show token savings report
81
+
82
+ Discover options:
83
+ --json Output machine-readable JSON
84
+ --save Save discovered skill file to disk
85
+
86
+ Capture options:
87
+ --json Output machine-readable JSON
88
+ --duration <seconds> Stop capture after N seconds
89
+ --port <port> Connect to specific CDP port
90
+ --launch Always launch a new browser
91
+ --attach Only attach to existing browser
92
+ --all-domains Capture traffic from all domains (default: target only)
93
+ --preview Include response data previews in skill files
94
+ --no-scrub Disable PII scrubbing
95
+ --no-verify Skip auto-verification of GET endpoints
96
+ --verify-posts Also verify POST endpoints by replaying them (may cause side effects)
97
+
98
+ Replay options:
99
+ --json Output machine-readable JSON
100
+ --fresh Force token refresh before replay
101
+ --max-bytes <bytes> Truncate response to fit within byte limit
102
+
103
+ Auth options:
104
+ --list List all domains with stored auth
105
+ --clear Clear auth for a domain
106
+ --json Output machine-readable JSON
107
+
108
+ Browse options:
109
+ --json Output machine-readable JSON
110
+ --max-bytes <bytes> Truncate response to fit within byte limit (default: 50000)
111
+
112
+ Peek options:
113
+ --json Output machine-readable JSON
114
+
115
+ Read options:
116
+ --json Output machine-readable JSON
117
+ --max-bytes <bytes> Truncate content to fit within byte limit
118
+
119
+ Import options:
120
+ --yes Skip confirmation prompt
121
+
122
+ Serve options:
123
+ --json Output tool list as JSON on stderr
124
+ --no-auth Skip loading stored auth
125
+ `.trim());
126
+ }
127
+
128
+ const APITAP_DIR = process.env.APITAP_DIR || join(homedir(), '.apitap');
129
+ const SKILLS_DIR = process.env.APITAP_SKILLS_DIR || undefined;
130
+
131
+ /** Get machine ID, allowing override via env var for testing */
132
+ async function getEffectiveMachineId(): Promise<string> {
133
+ return process.env.APITAP_MACHINE_ID || await getMachineId();
134
+ }
135
+
136
+ const TIER_BADGES: Record<string, string> = {
137
+ green: '[green]',
138
+ yellow: '[yellow]',
139
+ orange: '[orange]',
140
+ red: '[red]',
141
+ unknown: '[ ]',
142
+ };
143
+
144
+ async function handleCapture(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
145
+ const url = positional[0];
146
+ if (!url) {
147
+ console.error('Error: URL required. Usage: apitap capture <url>');
148
+ process.exit(1);
149
+ }
150
+
151
+ const fullUrl = url.startsWith('http') ? url : `https://${url}`;
152
+ const json = flags.json === true;
153
+ const duration = typeof flags.duration === 'string' ? parseInt(flags.duration, 10) : undefined;
154
+ const port = typeof flags.port === 'string' ? parseInt(flags.port, 10) : undefined;
155
+ const skipVerify = flags['no-verify'] === true;
156
+ const verifyPosts = flags['verify-posts'] === true;
157
+
158
+ if (!json) {
159
+ const domainOnly = flags['all-domains'] !== true;
160
+ console.log(`\n 🔍 Capturing ${url}...${duration ? ` (${duration}s)` : ' (Ctrl+C to stop)'}${domainOnly ? ' [domain-only]' : ' [all domains]'}\n`);
161
+ }
162
+
163
+ let endpointCount = 0;
164
+ let filteredCount = 0;
165
+
166
+ const result = await capture({
167
+ url: fullUrl,
168
+ duration,
169
+ port,
170
+ launch: flags.launch === true,
171
+ attach: flags.attach === true,
172
+ allDomains: flags['all-domains'] === true,
173
+ enablePreview: flags.preview === true,
174
+ scrub: flags['no-scrub'] !== true,
175
+ onEndpoint: (ep) => {
176
+ endpointCount++;
177
+ if (!json) {
178
+ console.log(` ✓ ${ep.method.padEnd(6)} ${ep.path}`);
179
+ }
180
+ },
181
+ onFiltered: () => {
182
+ filteredCount++;
183
+ },
184
+ onIdle: () => {
185
+ if (!json) {
186
+ console.log(`\n ⏸ No new endpoints for 15s — looks complete. Ctrl+C to finish.\n`);
187
+ }
188
+ },
189
+ });
190
+
191
+ // Get machine ID for signing and auth storage
192
+ const machineId = await getMachineId();
193
+ const key = deriveKey(machineId);
194
+ const authManager = new AuthManager(APITAP_DIR, machineId);
195
+
196
+ // Write skill files for each domain
197
+ const written: string[] = [];
198
+ for (const [domain, generator] of result.generators) {
199
+ let skill = generator.toSkillFile(domain, {
200
+ domBytes: result.domBytes,
201
+ totalRequests: result.totalRequests,
202
+ });
203
+ if (skill.endpoints.length > 0) {
204
+ // Store extracted auth
205
+ const extractedAuth = generator.getExtractedAuth();
206
+ if (extractedAuth.length > 0) {
207
+ await authManager.store(domain, extractedAuth[0]);
208
+ }
209
+
210
+ // Store OAuth credentials if detected
211
+ const oauthConfig = generator.getOAuthConfig();
212
+ if (oauthConfig) {
213
+ const clientSecret = generator.getOAuthClientSecret();
214
+ if (clientSecret) {
215
+ await authManager.storeOAuthCredentials(domain, { clientSecret });
216
+ }
217
+ }
218
+
219
+ // Auto-verify GET endpoints
220
+ if (!skipVerify) {
221
+ if (!json) {
222
+ console.log(`\n 🔍 Verifying ${domain}...`);
223
+ }
224
+ skill = await verifyEndpoints(skill, { verifyPosts });
225
+ if (!json) {
226
+ for (const ep of skill.endpoints) {
227
+ const tier = ep.replayability?.tier ?? 'unknown';
228
+ const badge = TIER_BADGES[tier];
229
+ const check = ep.replayability?.verified ? ' ✓' : '';
230
+ console.log(` ${badge}${check} ${ep.method.padEnd(6)} ${ep.path}`);
231
+ }
232
+ }
233
+ }
234
+
235
+ // Sign the skill file
236
+ skill = signSkillFile(skill, key);
237
+
238
+ const path = await writeSkillFile(skill);
239
+ written.push(path);
240
+ }
241
+ }
242
+
243
+ if (json) {
244
+ const output = {
245
+ domains: Array.from(result.generators.entries()).map(([domain, gen]) => {
246
+ const skill = gen.toSkillFile(domain, {
247
+ domBytes: result.domBytes,
248
+ totalRequests: result.totalRequests,
249
+ });
250
+ return {
251
+ domain,
252
+ endpoints: skill.endpoints.map(ep => ({
253
+ id: ep.id,
254
+ method: ep.method,
255
+ path: ep.path,
256
+ ...(ep.replayability ? { replayability: ep.replayability } : {}),
257
+ ...(ep.pagination ? { pagination: ep.pagination } : {}),
258
+ })),
259
+ };
260
+ }),
261
+ totalRequests: result.totalRequests,
262
+ filtered: result.filteredRequests,
263
+ skillFiles: written,
264
+ };
265
+ console.log(JSON.stringify(output, null, 2));
266
+ } else {
267
+ console.log(`\n 📋 Capture complete\n`);
268
+ console.log(` Endpoints: ${endpointCount} discovered`);
269
+ console.log(` Requests: ${result.totalRequests} total, ${result.filteredRequests} filtered`);
270
+ for (const path of written) {
271
+ console.log(` Skill file: ${path}`);
272
+ }
273
+ console.log();
274
+ }
275
+ }
276
+
277
+ async function handleSearch(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
278
+ const query = positional.join(' ');
279
+ if (!query) {
280
+ console.error('Error: Query required. Usage: apitap search <query>');
281
+ process.exit(1);
282
+ }
283
+
284
+ const json = flags.json === true;
285
+ const result = await searchSkills(query, SKILLS_DIR);
286
+
287
+ if (json) {
288
+ console.log(JSON.stringify(result, null, 2));
289
+ return;
290
+ }
291
+
292
+ if (!result.found) {
293
+ console.log(`\n ${result.suggestion}\n`);
294
+ return;
295
+ }
296
+
297
+ console.log();
298
+ for (const r of result.results!) {
299
+ const tierBadge = TIER_BADGES[r.tier] || '[?]';
300
+ const verified = r.verified ? ' ✓' : '';
301
+ console.log(` ${tierBadge}${verified} ${r.domain.padEnd(30)} ${r.method.padEnd(6)} ${r.path.padEnd(24)} ${r.endpointId}`);
302
+ }
303
+ console.log();
304
+ }
305
+
306
+ async function handleList(flags: Record<string, string | boolean>): Promise<void> {
307
+ const summaries = await listSkillFiles(SKILLS_DIR);
308
+ const json = flags.json === true;
309
+
310
+ if (json) {
311
+ console.log(JSON.stringify(summaries, null, 2));
312
+ return;
313
+ }
314
+
315
+ if (summaries.length === 0) {
316
+ console.log('\n No skill files found. Run `apitap capture <url>` first.\n');
317
+ return;
318
+ }
319
+
320
+ console.log();
321
+ for (const s of summaries) {
322
+ const ago = timeAgo(s.capturedAt);
323
+ const prov = s.provenance === 'self' ? '✓' : s.provenance === 'imported' ? '⬇' : '?';
324
+ console.log(` ${prov} ${s.domain.padEnd(28)} ${String(s.endpointCount).padStart(3)} endpoints ${ago}`);
325
+ }
326
+ console.log();
327
+ }
328
+
329
+ async function handleShow(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
330
+ const domain = positional[0];
331
+ if (!domain) {
332
+ console.error('Error: Domain required. Usage: apitap show <domain>');
333
+ process.exit(1);
334
+ }
335
+
336
+ const skill = await readSkillFile(domain, SKILLS_DIR);
337
+ if (!skill) {
338
+ console.error(`Error: No skill file found for "${domain}". Run \`apitap capture\` first.`);
339
+ process.exit(1);
340
+ }
341
+
342
+ const json = flags.json === true;
343
+
344
+ if (json) {
345
+ console.log(JSON.stringify(skill, null, 2));
346
+ return;
347
+ }
348
+
349
+ const provLabel = skill.provenance === 'self' ? 'signed ✓' : skill.provenance === 'imported' ? 'imported ⬇' : 'unsigned';
350
+ console.log(`\n ${skill.domain} — ${skill.endpoints.length} endpoints (captured ${timeAgo(skill.capturedAt)}) [${provLabel}]\n`);
351
+ for (const ep of skill.endpoints) {
352
+ const shape = ep.responseShape.type;
353
+ const fields = ep.responseShape.fields?.length ?? 0;
354
+ const hasAuth = Object.values(ep.headers).some(v => v === '[stored]');
355
+ const authBadge = hasAuth ? ' 🔑' : '';
356
+ const tier = ep.replayability?.tier ?? 'unknown';
357
+ const tierBadge = TIER_BADGES[tier];
358
+ const verified = ep.replayability?.verified ? ' ✓' : '';
359
+ const pagBadge = ep.pagination ? ` 📄${ep.pagination.type}` : '';
360
+ const bodyBadge = ep.requestBody ? ` 📦${ep.requestBody.contentType.split('/')[1] || 'body'}` : '';
361
+ console.log(` ${tierBadge}${verified} ${ep.method.padEnd(6)} ${ep.path.padEnd(30)} ${shape}${fields ? ` (${fields} fields)` : ''}${authBadge}${pagBadge}${bodyBadge}`);
362
+ }
363
+ console.log(`\n Replay: apitap replay ${skill.domain} <endpoint-id>\n`);
364
+ }
365
+
366
+ async function handleReplay(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
367
+ const [domain, endpointId, ...paramArgs] = positional;
368
+ if (!domain || !endpointId) {
369
+ console.error('Error: Domain and endpoint required. Usage: apitap replay <domain> <endpoint-id> [key=value...]');
370
+ process.exit(1);
371
+ }
372
+
373
+ const skill = await readSkillFile(domain, SKILLS_DIR);
374
+ if (!skill) {
375
+ console.error(`Error: No skill file found for "${domain}".`);
376
+ process.exit(1);
377
+ }
378
+
379
+ // Parse key=value params
380
+ const params: Record<string, string> = {};
381
+ for (const arg of paramArgs) {
382
+ const eq = arg.indexOf('=');
383
+ if (eq > 0) {
384
+ params[arg.slice(0, eq)] = arg.slice(eq + 1);
385
+ }
386
+ }
387
+
388
+ // Merge stored auth into endpoint headers for replay
389
+ const machineId = await getMachineId();
390
+ const authManager = new AuthManager(APITAP_DIR, machineId);
391
+ const storedAuth = await authManager.retrieve(domain);
392
+
393
+ // Check for [stored] placeholders and warn if auth missing
394
+ const endpoint = skill.endpoints.find(e => e.id === endpointId);
395
+ if (endpoint) {
396
+ const hasStoredPlaceholder = Object.values(endpoint.headers).some(v => v === '[stored]');
397
+ if (hasStoredPlaceholder && !storedAuth) {
398
+ console.error(`Warning: Endpoint requires auth but no stored credentials found for "${domain}".`);
399
+ console.error(` Run \`apitap capture ${domain}\` to capture fresh credentials.\n`);
400
+ }
401
+
402
+ // Inject stored auth into a copy of the skill for replay
403
+ if (storedAuth) {
404
+ endpoint.headers[storedAuth.header] = storedAuth.value;
405
+ }
406
+ }
407
+
408
+ const fresh = flags.fresh === true;
409
+ const json = flags.json === true;
410
+ const maxBytes = typeof flags['max-bytes'] === 'string' ? parseInt(flags['max-bytes'], 10) : undefined;
411
+
412
+ const result = await replayEndpoint(skill, endpointId, {
413
+ params: Object.keys(params).length > 0 ? params : undefined,
414
+ authManager,
415
+ domain,
416
+ fresh,
417
+ maxBytes,
418
+ _skipSsrfCheck: process.env.APITAP_SKIP_SSRF_CHECK === '1',
419
+ });
420
+
421
+ if (json) {
422
+ console.log(JSON.stringify({ status: result.status, data: result.data }, null, 2));
423
+ } else {
424
+ console.log(`\n Status: ${result.status}\n`);
425
+ console.log(JSON.stringify(result.data, null, 2));
426
+ console.log();
427
+ }
428
+ }
429
+
430
+ async function handleImport(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
431
+ const filePath = positional[0];
432
+ if (!filePath) {
433
+ console.error('Error: File path required. Usage: apitap import <file>');
434
+ process.exit(1);
435
+ }
436
+
437
+ const json = flags.json === true;
438
+
439
+ // Get local key for signature verification
440
+ const machineId = await getMachineId();
441
+ const key = deriveKey(machineId);
442
+
443
+ // DNS-resolving SSRF check before importing (prevents DNS rebinding attacks)
444
+ try {
445
+ const raw = JSON.parse(await import('node:fs/promises').then(fs => fs.readFile(filePath, 'utf-8')));
446
+ if (raw.baseUrl) {
447
+ const dnsCheck = await resolveAndValidateUrl(raw.baseUrl);
448
+ if (!dnsCheck.safe) {
449
+ const msg = `DNS rebinding risk: ${dnsCheck.reason}`;
450
+ if (json) {
451
+ console.log(JSON.stringify({ success: false, reason: msg }));
452
+ } else {
453
+ console.error(`Error: ${msg}`);
454
+ }
455
+ process.exit(1);
456
+ }
457
+ }
458
+ } catch {
459
+ // Parse errors will be caught by importSkillFile
460
+ }
461
+
462
+ const result = await importSkillFile(filePath, undefined, key);
463
+
464
+ if (!result.success) {
465
+ if (json) {
466
+ console.log(JSON.stringify({ success: false, reason: result.reason }));
467
+ } else {
468
+ console.error(`Error: ${result.reason}`);
469
+ }
470
+ process.exit(1);
471
+ }
472
+
473
+ if (json) {
474
+ console.log(JSON.stringify({ success: true, skillFile: result.skillFile }));
475
+ } else {
476
+ console.log(`\n ✓ Imported skill file: ${result.skillFile}\n`);
477
+ }
478
+ }
479
+
480
+ async function handleRefresh(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
481
+ const domain = positional[0];
482
+ if (!domain) {
483
+ console.error('Error: Domain required. Usage: apitap refresh <domain>');
484
+ process.exit(1);
485
+ }
486
+
487
+ const skill = await readSkillFile(domain, SKILLS_DIR);
488
+ if (!skill) {
489
+ console.error(`Error: No skill file found for "${domain}".`);
490
+ process.exit(1);
491
+ }
492
+
493
+ const machineId = await getEffectiveMachineId();
494
+ const authManager = new AuthManager(APITAP_DIR, machineId);
495
+ const json = flags.json === true;
496
+
497
+ if (!json) {
498
+ console.log(`\n 🔄 Refreshing tokens for ${domain}...`);
499
+ }
500
+
501
+ const result = await refreshTokens(skill, authManager, {
502
+ domain,
503
+ browserMode: skill.auth?.captchaRisk ? 'visible' : 'headless',
504
+ });
505
+
506
+ if (json) {
507
+ console.log(JSON.stringify(result, null, 2));
508
+ } else if (result.success) {
509
+ if (result.oauthRefreshed) {
510
+ console.log(` ✓ OAuth token refreshed via token endpoint`);
511
+ }
512
+ if (Object.keys(result.tokens).length > 0) {
513
+ console.log(` ✓ Browser tokens refreshed: ${Object.keys(result.tokens).join(', ')}`);
514
+ }
515
+ if (result.captchaDetected) {
516
+ console.log(` (captcha solved: ${result.captchaDetected})`);
517
+ }
518
+ console.log();
519
+ } else {
520
+ console.error(` ✗ Refresh failed: ${result.error || 'no tokens captured'}`);
521
+ process.exit(1);
522
+ }
523
+ }
524
+
525
+ async function handleAuth(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
526
+ const domain = positional[0];
527
+ const json = flags.json === true;
528
+ const machineId = await getEffectiveMachineId();
529
+ const authManager = new AuthManager(APITAP_DIR, machineId);
530
+
531
+ // List all domains
532
+ if (flags.list === true) {
533
+ const domains = await authManager.listDomains();
534
+ if (json) {
535
+ console.log(JSON.stringify({ domains }, null, 2));
536
+ } else {
537
+ if (domains.length === 0) {
538
+ console.log('\n No stored auth\n');
539
+ } else {
540
+ console.log('\n Domains with stored auth:');
541
+ for (const d of domains) {
542
+ console.log(` ${d}`);
543
+ }
544
+ console.log();
545
+ }
546
+ }
547
+ return;
548
+ }
549
+
550
+ // Require domain for other operations
551
+ if (!domain) {
552
+ console.error('Error: Domain required. Usage: apitap auth <domain> [--clear] [--json]');
553
+ console.error(' apitap auth --list [--json]');
554
+ process.exit(1);
555
+ }
556
+
557
+ // Clear auth for domain
558
+ if (flags.clear === true) {
559
+ await authManager.clear(domain);
560
+ if (json) {
561
+ console.log(JSON.stringify({ success: true, domain, cleared: true }));
562
+ } else {
563
+ console.log(`\n ✓ Cleared auth for ${domain}\n`);
564
+ }
565
+ return;
566
+ }
567
+
568
+ // Show auth status for domain
569
+ const auth = await authManager.retrieve(domain);
570
+ const tokens = await authManager.retrieveTokens(domain);
571
+ const session = await authManager.retrieveSession(domain);
572
+ const oauthCreds = await authManager.retrieveOAuthCredentials(domain);
573
+
574
+ // Check for JWT expiry
575
+ let jwtExpiry: string | undefined;
576
+ if (auth?.value) {
577
+ const raw = auth.value.startsWith('Bearer ') ? auth.value.slice(7) : auth.value;
578
+ const jwt = parseJwtClaims(raw);
579
+ if (jwt?.exp) {
580
+ const expDate = new Date(jwt.exp * 1000);
581
+ const isExpired = expDate.getTime() < Date.now();
582
+ jwtExpiry = isExpired ? `expired ${timeAgo(expDate.toISOString())}` : `expires ${expDate.toISOString()}`;
583
+ }
584
+ }
585
+
586
+ // Read skill file for OAuth config (non-secret)
587
+ const skill = await readSkillFile(domain, SKILLS_DIR);
588
+ const oauthConfig = skill?.auth?.oauthConfig;
589
+
590
+ const status = {
591
+ domain,
592
+ hasHeaderAuth: !!auth,
593
+ headerAuthType: auth?.type,
594
+ jwtExpiry,
595
+ tokens: tokens ? Object.keys(tokens) : [],
596
+ tokenRefreshTimes: tokens
597
+ ? Object.fromEntries(
598
+ Object.entries(tokens).map(([k, v]) => [k, v.refreshedAt])
599
+ )
600
+ : {},
601
+ hasSession: !!session,
602
+ sessionSavedAt: session?.savedAt,
603
+ hasOAuth: !!oauthCreds,
604
+ oauthConfig: oauthConfig ?? undefined,
605
+ };
606
+
607
+ if (json) {
608
+ console.log(JSON.stringify(status, null, 2));
609
+ } else {
610
+ console.log(`\n Auth status for ${domain}:`);
611
+ console.log(` Header auth: ${auth ? `${auth.type} (${auth.header})` : 'none'}`);
612
+ if (jwtExpiry) {
613
+ console.log(` JWT: ${jwtExpiry}`);
614
+ }
615
+ if (oauthConfig) {
616
+ console.log(` OAuth: ${oauthConfig.grantType} via ${oauthConfig.tokenEndpoint}`);
617
+ if (oauthCreds?.refreshToken) {
618
+ console.log(` Refresh token: stored`);
619
+ }
620
+ }
621
+ console.log(` Tokens: ${tokens ? Object.keys(tokens).join(', ') || 'none' : 'none'}`);
622
+ if (tokens) {
623
+ for (const [name, info] of Object.entries(tokens)) {
624
+ console.log(` ${name}: refreshed ${timeAgo(info.refreshedAt)}`);
625
+ }
626
+ }
627
+ console.log(` Session cache: ${session ? `saved ${timeAgo(session.savedAt)}` : 'none'}`);
628
+ console.log();
629
+ }
630
+ }
631
+
632
+ async function handleServe(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
633
+ const domain = positional[0];
634
+ if (!domain) {
635
+ console.error('Error: Domain required. Usage: apitap serve <domain>');
636
+ process.exit(1);
637
+ }
638
+
639
+ const noAuth = flags['no-auth'] === true;
640
+ const json = flags.json === true;
641
+
642
+ try {
643
+ const server = await createServeServer(domain, {
644
+ skillsDir: SKILLS_DIR,
645
+ noAuth,
646
+ });
647
+
648
+ // Print tool list to stderr (stdout is the MCP transport)
649
+ const skill = await readSkillFile(domain, SKILLS_DIR);
650
+ const tools = buildServeTools(skill!);
651
+
652
+ if (json) {
653
+ console.error(JSON.stringify(tools.map(t => ({ name: t.name, description: t.description }))));
654
+ } else {
655
+ console.error(`apitap serve: ${domain} (${tools.length} tools)`);
656
+ for (const tool of tools) {
657
+ console.error(` ${tool.name}`);
658
+ }
659
+ }
660
+
661
+ // Start stdio transport
662
+ const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
663
+ const transport = new StdioServerTransport();
664
+ await server.connect(transport);
665
+ } catch (err: any) {
666
+ console.error(`Error: ${err.message}`);
667
+ process.exit(1);
668
+ }
669
+ }
670
+
671
+ function timeAgo(isoDate: string): string {
672
+ const diff = Date.now() - new Date(isoDate).getTime();
673
+ const minutes = Math.floor(diff / 60000);
674
+ if (minutes < 1) return 'just now';
675
+ if (minutes < 60) return `${minutes}m ago`;
676
+ const hours = Math.floor(minutes / 60);
677
+ if (hours < 24) return `${hours}h ago`;
678
+ const days = Math.floor(hours / 24);
679
+ return `${days}d ago`;
680
+ }
681
+
682
+ async function handleInspect(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
683
+ const url = positional[0];
684
+ if (!url) {
685
+ console.error('Usage: apitap inspect <url>');
686
+ process.exit(1);
687
+ }
688
+
689
+ const json = flags.json === true;
690
+ const duration = typeof flags.duration === 'string' ? parseInt(flags.duration, 10) : 30;
691
+
692
+ if (!json) {
693
+ console.log(`\n Inspecting ${url} (${duration}s scan)...\n`);
694
+ }
695
+
696
+ // Track anti-bot signals during capture
697
+ const antiBotSignals = new Set<AntiBotSignal>();
698
+
699
+ const result = await capture({
700
+ url,
701
+ port: typeof flags.port === 'string' ? parseInt(flags.port, 10) : undefined,
702
+ launch: flags.launch === true,
703
+ attach: flags.attach === true,
704
+ duration,
705
+ allDomains: flags['all-domains'] === true,
706
+ enablePreview: false,
707
+ scrub: true,
708
+ onEndpoint: (ep) => {
709
+ if (!json) {
710
+ console.log(` ✓ ${ep.method.padEnd(6)} ${ep.path}`);
711
+ }
712
+ },
713
+ });
714
+
715
+ // Build skill files (without writing to disk), verify endpoints, and detect anti-bot
716
+ const skills = new Map<string, import('./types.js').SkillFile>();
717
+ for (const [domain, generator] of result.generators) {
718
+ let skill = generator.toSkillFile(domain, {
719
+ domBytes: result.domBytes,
720
+ totalRequests: result.totalRequests,
721
+ });
722
+ const verifyPosts = flags['verify-posts'] === true;
723
+ skill = await verifyEndpoints(skill, { verifyPosts });
724
+ if (skill.endpoints.length > 0) {
725
+ skills.set(domain, skill);
726
+ }
727
+ }
728
+
729
+ // Extract target domain from URL
730
+ let targetDomain: string;
731
+ try {
732
+ targetDomain = new URL(url.startsWith('http') ? url : `https://${url}`).hostname;
733
+ } catch {
734
+ targetDomain = url;
735
+ }
736
+
737
+ const report = buildInspectReport({
738
+ skills,
739
+ totalRequests: result.totalRequests,
740
+ filteredRequests: result.filteredRequests,
741
+ duration,
742
+ domBytes: result.domBytes,
743
+ antiBotSignals: [...antiBotSignals],
744
+ targetDomain,
745
+ });
746
+
747
+ if (json) {
748
+ console.log(JSON.stringify(report, null, 2));
749
+ } else {
750
+ console.log(formatInspectHuman(report));
751
+ }
752
+ }
753
+
754
+ async function handleStats(flags: Record<string, string | boolean>): Promise<void> {
755
+ const json = flags.json === true;
756
+ const skillsDir = SKILLS_DIR || join(APITAP_DIR, 'skills');
757
+
758
+ const report = await generateStatsReport(skillsDir);
759
+
760
+ if (json) {
761
+ console.log(JSON.stringify(report, null, 2));
762
+ } else {
763
+ console.log(formatStatsHuman(report));
764
+ }
765
+ }
766
+
767
+ async function handleDiscover(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
768
+ const url = positional[0];
769
+ if (!url) {
770
+ console.error('Error: URL required. Usage: apitap discover <url>');
771
+ process.exit(1);
772
+ }
773
+
774
+ const json = flags.json === true;
775
+ const save = flags.save === true;
776
+
777
+ if (!json) {
778
+ console.log(`\n Discovering APIs for ${url}...\n`);
779
+ }
780
+
781
+ const result = await discover(url);
782
+
783
+ if (json) {
784
+ console.log(JSON.stringify(result, null, 2));
785
+ } else {
786
+ // Confidence summary
787
+ const confidenceLabels: Record<string, string> = {
788
+ high: 'High — API spec or strong framework signals found',
789
+ medium: 'Medium — known framework detected',
790
+ low: 'Low — some API patterns detected',
791
+ none: 'None — no API patterns found',
792
+ };
793
+ console.log(` Confidence: ${confidenceLabels[result.confidence]}`);
794
+ console.log(` Duration: ${result.duration}ms`);
795
+
796
+ if (result.frameworks && result.frameworks.length > 0) {
797
+ console.log(`\n Frameworks:`);
798
+ for (const f of result.frameworks) {
799
+ console.log(` ${f.name} (${f.confidence}) — ${f.apiPatterns.length} predicted patterns`);
800
+ }
801
+ }
802
+
803
+ if (result.specs && result.specs.length > 0) {
804
+ console.log(`\n API Specs:`);
805
+ for (const s of result.specs) {
806
+ console.log(` ${s.type} ${s.version ?? ''} — ${s.url}${s.endpointCount ? ` (${s.endpointCount} endpoints)` : ''}`);
807
+ }
808
+ }
809
+
810
+ if (result.probes && result.probes.length > 0) {
811
+ const apiProbes = result.probes.filter(p => p.isApi);
812
+ if (apiProbes.length > 0) {
813
+ console.log(`\n API Paths:`);
814
+ for (const p of apiProbes) {
815
+ console.log(` ${p.method} ${p.path} → ${p.status} (${p.contentType})`);
816
+ }
817
+ }
818
+ }
819
+
820
+ if (result.hints && result.hints.length > 0) {
821
+ console.log(`\n Hints:`);
822
+ for (const h of result.hints) {
823
+ console.log(` ${h}`);
824
+ }
825
+ }
826
+
827
+ if (result.skillFile) {
828
+ console.log(`\n Skill file: ${result.skillFile.endpoints.length} endpoints predicted`);
829
+ }
830
+
831
+ if (result.confidence === 'none') {
832
+ console.log(`\n Recommendation: Run \`apitap capture ${url}\` for browser-based discovery`);
833
+ }
834
+
835
+ console.log();
836
+ }
837
+
838
+ // Save skill file if requested and available
839
+ if (save && result.skillFile) {
840
+ const { writeSkillFile } = await import('./skill/store.js');
841
+ const { signSkillFile } = await import('./skill/signing.js');
842
+ const { deriveKey } = await import('./auth/crypto.js');
843
+ const machineId = await getMachineId();
844
+ const key = deriveKey(machineId);
845
+
846
+ const signed = signSkillFile(result.skillFile, key);
847
+ const path = await writeSkillFile(signed, SKILLS_DIR);
848
+
849
+ if (json) {
850
+ console.log(JSON.stringify({ saved: path }));
851
+ } else {
852
+ console.log(` Saved: ${path}\n`);
853
+ }
854
+ }
855
+ }
856
+
857
+ async function handleBrowse(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
858
+ const url = positional[0];
859
+ if (!url) {
860
+ console.error('Error: URL required. Usage: apitap browse <url>');
861
+ process.exit(1);
862
+ }
863
+
864
+ const json = flags.json === true;
865
+ const fullUrl = url.startsWith('http') ? url : `https://${url}`;
866
+
867
+ if (!json) {
868
+ console.log(`\n Browsing ${url}...\n`);
869
+ }
870
+
871
+ const maxBytes = typeof flags['max-bytes'] === 'string' ? parseInt(flags['max-bytes'], 10) : 50_000;
872
+
873
+ const { browse } = await import('./orchestration/browse.js');
874
+ const { SessionCache } = await import('./orchestration/cache.js');
875
+
876
+ const result = await browse(fullUrl, {
877
+ skillsDir: SKILLS_DIR,
878
+ cache: new SessionCache(),
879
+ maxBytes,
880
+ _skipSsrfCheck: process.env.APITAP_SKIP_SSRF_CHECK === '1',
881
+ });
882
+
883
+ if (json) {
884
+ console.log(JSON.stringify(result, null, 2));
885
+ return;
886
+ }
887
+
888
+ if (result.success) {
889
+ console.log(` ✓ ${result.domain} → ${result.endpointId} (${result.tier})`);
890
+ console.log(` Status: ${result.status}\n`);
891
+ console.log(JSON.stringify(result.data, null, 2));
892
+ console.log();
893
+ } else {
894
+ console.log(` ✗ ${result.reason}`);
895
+ if (result.suggestion === 'capture_needed') {
896
+ console.log(`\n Recommendation: apitap capture ${result.url}\n`);
897
+ }
898
+ }
899
+ }
900
+
901
+ async function handlePeek(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
902
+ const url = positional[0];
903
+ if (!url) {
904
+ console.error('Error: URL required. Usage: apitap peek <url>');
905
+ process.exit(1);
906
+ }
907
+
908
+ const json = flags.json === true;
909
+ const fullUrl = url.startsWith('http') ? url : `https://${url}`;
910
+
911
+ if (!json) {
912
+ console.log(`\n Peeking at ${url}...\n`);
913
+ }
914
+
915
+ const result = await peek(fullUrl);
916
+
917
+ if (json) {
918
+ console.log(JSON.stringify(result, null, 2));
919
+ return;
920
+ }
921
+
922
+ const icon = result.accessible ? '\u2713' : '\u2717';
923
+ console.log(` ${icon} ${result.recommendation} (${result.status})`);
924
+ if (result.server) console.log(` Server: ${result.server}`);
925
+ if (result.framework) console.log(` Framework: ${result.framework}`);
926
+ if (result.botProtection) console.log(` Bot protection: ${result.botProtection}`);
927
+ if (result.signals.length > 0) console.log(` Signals: ${result.signals.join(', ')}`);
928
+ console.log();
929
+ }
930
+
931
+ async function handleRead(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
932
+ const url = positional[0];
933
+ if (!url) {
934
+ console.error('Error: URL required. Usage: apitap read <url>');
935
+ process.exit(1);
936
+ }
937
+
938
+ const json = flags.json === true;
939
+ const fullUrl = url.startsWith('http') ? url : `https://${url}`;
940
+ const maxBytes = typeof flags['max-bytes'] === 'string' ? parseInt(flags['max-bytes'], 10) : undefined;
941
+
942
+ if (!json) {
943
+ console.log(`\n Reading ${url}...\n`);
944
+ }
945
+
946
+ const result = await read(fullUrl, { maxBytes });
947
+
948
+ if (!result) {
949
+ if (json) {
950
+ console.log(JSON.stringify({ error: 'Failed to read content' }));
951
+ } else {
952
+ console.error(' Failed to read content\n');
953
+ }
954
+ process.exit(1);
955
+ }
956
+
957
+ if (json) {
958
+ console.log(JSON.stringify(result, null, 2));
959
+ return;
960
+ }
961
+
962
+ if (result.title) console.log(` ${result.title}`);
963
+ console.log(` Source: ${result.metadata.source} | ~${result.cost.tokens} tokens\n`);
964
+ console.log(result.content);
965
+ console.log();
966
+ }
967
+
968
+ async function main(): Promise<void> {
969
+ const { command, positional, flags } = parseArgs(process.argv.slice(2));
970
+
971
+ // Handle --version flag before command dispatch
972
+ if (command === '--version' || command === 'version' || flags.version === true) {
973
+ console.log(VERSION);
974
+ return;
975
+ }
976
+
977
+ switch (command) {
978
+ case 'capture':
979
+ await handleCapture(positional, flags);
980
+ break;
981
+ case 'discover':
982
+ await handleDiscover(positional, flags);
983
+ break;
984
+ case 'list':
985
+ await handleList(flags);
986
+ break;
987
+ case 'show':
988
+ await handleShow(positional, flags);
989
+ break;
990
+ case 'replay':
991
+ await handleReplay(positional, flags);
992
+ break;
993
+ case 'search':
994
+ await handleSearch(positional, flags);
995
+ break;
996
+ case 'import':
997
+ await handleImport(positional, flags);
998
+ break;
999
+ case 'refresh':
1000
+ await handleRefresh(positional, flags);
1001
+ break;
1002
+ case 'auth':
1003
+ await handleAuth(positional, flags);
1004
+ break;
1005
+ case 'serve':
1006
+ await handleServe(positional, flags);
1007
+ break;
1008
+ case 'inspect':
1009
+ await handleInspect(positional, flags);
1010
+ break;
1011
+ case 'stats':
1012
+ await handleStats(flags);
1013
+ break;
1014
+ case 'browse':
1015
+ await handleBrowse(positional, flags);
1016
+ break;
1017
+ case 'peek':
1018
+ await handlePeek(positional, flags);
1019
+ break;
1020
+ case 'read':
1021
+ await handleRead(positional, flags);
1022
+ break;
1023
+ default:
1024
+ printUsage();
1025
+ }
1026
+ }
1027
+
1028
+ main().catch((err) => {
1029
+ console.error(`Error: ${err.message}`);
1030
+ process.exit(1);
1031
+ });