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