imperium-crawl 2.5.3 → 2.6.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 (235) hide show
  1. package/README.md +15 -14
  2. package/dist/batch/job-store.js +1 -1
  3. package/dist/batch/job-store.js.map +1 -1
  4. package/dist/brave-api/index.js +1 -1
  5. package/dist/brave-api/index.js.map +1 -1
  6. package/dist/cli/config.d.ts +21 -0
  7. package/dist/cli/config.d.ts.map +1 -0
  8. package/dist/cli/config.js +51 -0
  9. package/dist/cli/config.js.map +1 -0
  10. package/dist/cli/engine.d.ts +17 -0
  11. package/dist/cli/engine.d.ts.map +1 -0
  12. package/dist/cli/engine.js +440 -0
  13. package/dist/cli/engine.js.map +1 -0
  14. package/dist/cli/explore.d.ts +30 -0
  15. package/dist/cli/explore.d.ts.map +1 -0
  16. package/dist/cli/explore.js +427 -0
  17. package/dist/cli/explore.js.map +1 -0
  18. package/dist/cli/onboarding.d.ts +10 -0
  19. package/dist/cli/onboarding.d.ts.map +1 -0
  20. package/dist/cli/onboarding.js +128 -0
  21. package/dist/cli/onboarding.js.map +1 -0
  22. package/dist/cli/recorder.d.ts +44 -0
  23. package/dist/cli/recorder.d.ts.map +1 -0
  24. package/dist/cli/recorder.js +67 -0
  25. package/dist/cli/recorder.js.map +1 -0
  26. package/dist/cli/tui.d.ts +12 -0
  27. package/dist/cli/tui.d.ts.map +1 -0
  28. package/dist/cli/tui.js +945 -0
  29. package/dist/cli/tui.js.map +1 -0
  30. package/dist/cli/ui.d.ts +26 -0
  31. package/dist/cli/ui.d.ts.map +1 -0
  32. package/dist/cli/ui.js +58 -0
  33. package/dist/cli/ui.js.map +1 -0
  34. package/dist/core/action-executor.d.ts +66 -0
  35. package/dist/core/action-executor.d.ts.map +1 -0
  36. package/dist/core/action-executor.js +403 -0
  37. package/dist/core/action-executor.js.map +1 -0
  38. package/dist/core/config.d.ts +16 -0
  39. package/dist/core/config.d.ts.map +1 -0
  40. package/dist/core/config.js +56 -0
  41. package/dist/core/config.js.map +1 -0
  42. package/dist/core/constants.d.ts +40 -0
  43. package/dist/core/constants.d.ts.map +1 -0
  44. package/dist/core/constants.js +86 -0
  45. package/dist/core/constants.js.map +1 -0
  46. package/dist/core/formatters.d.ts +36 -0
  47. package/dist/core/formatters.d.ts.map +1 -0
  48. package/dist/core/formatters.js +147 -0
  49. package/dist/core/formatters.js.map +1 -0
  50. package/dist/engines/camofox.d.ts +27 -0
  51. package/dist/engines/camofox.d.ts.map +1 -0
  52. package/dist/engines/camofox.js +432 -0
  53. package/dist/engines/camofox.js.map +1 -0
  54. package/dist/engines/index.d.ts +13 -0
  55. package/dist/engines/index.d.ts.map +1 -0
  56. package/dist/engines/index.js +41 -0
  57. package/dist/engines/index.js.map +1 -0
  58. package/dist/engines/types.d.ts +63 -0
  59. package/dist/engines/types.d.ts.map +1 -0
  60. package/dist/engines/types.js +8 -0
  61. package/dist/engines/types.js.map +1 -0
  62. package/dist/flows/engine.js +3 -3
  63. package/dist/flows/engine.js.map +1 -1
  64. package/dist/flows/storage.js +1 -1
  65. package/dist/flows/storage.js.map +1 -1
  66. package/dist/flows/templates.js +1 -1
  67. package/dist/flows/templates.js.map +1 -1
  68. package/dist/flows/types.d.ts +405 -405
  69. package/dist/index.js +4 -4
  70. package/dist/index.js.map +1 -1
  71. package/dist/knowledge/store.js +1 -1
  72. package/dist/knowledge/store.js.map +1 -1
  73. package/dist/network/index.d.ts +3 -0
  74. package/dist/network/index.d.ts.map +1 -0
  75. package/dist/network/index.js +2 -0
  76. package/dist/network/index.js.map +1 -0
  77. package/dist/recipes/data/crypto-websocket.json +11 -0
  78. package/dist/recipes/data/ecommerce-product.json +25 -0
  79. package/dist/recipes/data/github-trending.json +19 -0
  80. package/dist/recipes/data/hn-top-stories.json +22 -0
  81. package/dist/recipes/data/influencer-competitor-spy.json +14 -0
  82. package/dist/recipes/data/influencer-content-scout.json +14 -0
  83. package/dist/recipes/data/influencer-hashtag-scout.json +14 -0
  84. package/dist/recipes/data/influencer-niche-discovery.json +14 -0
  85. package/dist/recipes/data/job-listings-greenhouse.json +17 -0
  86. package/dist/recipes/data/news-article-reader.json +9 -0
  87. package/dist/recipes/data/product-reviews.json +33 -0
  88. package/dist/recipes/data/reddit-posts.json +8 -0
  89. package/dist/recipes/data/seo-page-audit.json +26 -0
  90. package/dist/recipes/data/social-media-mentions.json +31 -0
  91. package/dist/recipes/index.d.ts +1 -1
  92. package/dist/recipes/index.d.ts.map +1 -1
  93. package/dist/recipes/index.js +14 -14
  94. package/dist/recipes/index.js.map +1 -1
  95. package/dist/security/auth-vault.js +1 -1
  96. package/dist/security/auth-vault.js.map +1 -1
  97. package/dist/security/index.d.ts +6 -0
  98. package/dist/security/index.d.ts.map +1 -0
  99. package/dist/security/index.js +4 -0
  100. package/dist/security/index.js.map +1 -0
  101. package/dist/sessions/browser-state.js +1 -1
  102. package/dist/sessions/browser-state.js.map +1 -1
  103. package/dist/sessions/encryption.js +1 -1
  104. package/dist/sessions/encryption.js.map +1 -1
  105. package/dist/sessions/manager.js +1 -1
  106. package/dist/sessions/manager.js.map +1 -1
  107. package/dist/skills/detector.js +1 -1
  108. package/dist/skills/detector.js.map +1 -1
  109. package/dist/skills/index.d.ts +9 -0
  110. package/dist/skills/index.d.ts.map +1 -0
  111. package/dist/skills/index.js +6 -0
  112. package/dist/skills/index.js.map +1 -0
  113. package/dist/skills/manager.js +1 -1
  114. package/dist/skills/manager.js.map +1 -1
  115. package/dist/snapshot/store.js +1 -1
  116. package/dist/snapshot/store.js.map +1 -1
  117. package/dist/social/index.d.ts +7 -0
  118. package/dist/social/index.d.ts.map +1 -0
  119. package/dist/social/index.js +4 -0
  120. package/dist/social/index.js.map +1 -0
  121. package/dist/stealth/browser-pool.js +1 -1
  122. package/dist/stealth/browser-pool.js.map +1 -1
  123. package/dist/stealth/browser.js +2 -2
  124. package/dist/stealth/browser.js.map +1 -1
  125. package/dist/stealth/chrome-profile.js +2 -2
  126. package/dist/stealth/chrome-profile.js.map +1 -1
  127. package/dist/stealth/index.js +2 -2
  128. package/dist/stealth/index.js.map +1 -1
  129. package/dist/tools/ai-extract.js +1 -1
  130. package/dist/tools/ai-extract.js.map +1 -1
  131. package/dist/tools/batch-download.d.ts +1 -1
  132. package/dist/tools/batch-download.js +1 -1
  133. package/dist/tools/batch-download.js.map +1 -1
  134. package/dist/tools/batch-scrape.d.ts +2 -2
  135. package/dist/tools/batch-scrape.js +1 -1
  136. package/dist/tools/batch-scrape.js.map +1 -1
  137. package/dist/tools/browser.d.ts +8 -8
  138. package/dist/tools/browser.js +4 -4
  139. package/dist/tools/browser.js.map +1 -1
  140. package/dist/tools/camofox-status.d.ts +14 -0
  141. package/dist/tools/camofox-status.d.ts.map +1 -0
  142. package/dist/tools/camofox-status.js +61 -0
  143. package/dist/tools/camofox-status.js.map +1 -0
  144. package/dist/tools/camofox-update.d.ts +29 -0
  145. package/dist/tools/camofox-update.d.ts.map +1 -0
  146. package/dist/tools/camofox-update.js +108 -0
  147. package/dist/tools/camofox-update.js.map +1 -0
  148. package/dist/tools/crawl.d.ts +2 -2
  149. package/dist/tools/crawl.js +1 -1
  150. package/dist/tools/crawl.js.map +1 -1
  151. package/dist/tools/create-skill.js +3 -3
  152. package/dist/tools/create-skill.js.map +1 -1
  153. package/dist/tools/discover-apis.d.ts +1 -1
  154. package/dist/tools/discover-apis.js +1 -1
  155. package/dist/tools/discover-apis.js.map +1 -1
  156. package/dist/tools/download.d.ts +7 -7
  157. package/dist/tools/download.js +1 -1
  158. package/dist/tools/download.js.map +1 -1
  159. package/dist/tools/extract.d.ts +1 -1
  160. package/dist/tools/extract.js +1 -1
  161. package/dist/tools/extract.js.map +1 -1
  162. package/dist/tools/image-search.d.ts +1 -1
  163. package/dist/tools/image-search.js +2 -2
  164. package/dist/tools/image-search.js.map +1 -1
  165. package/dist/tools/index.d.ts.map +1 -1
  166. package/dist/tools/index.js +5 -0
  167. package/dist/tools/index.js.map +1 -1
  168. package/dist/tools/instagram.d.ts +1 -1
  169. package/dist/tools/instagram.js +3 -3
  170. package/dist/tools/instagram.js.map +1 -1
  171. package/dist/tools/interact.d.ts +86 -86
  172. package/dist/tools/interact.js +5 -5
  173. package/dist/tools/interact.js.map +1 -1
  174. package/dist/tools/knowledge.js +1 -1
  175. package/dist/tools/knowledge.js.map +1 -1
  176. package/dist/tools/list-skills.js +1 -1
  177. package/dist/tools/list-skills.js.map +1 -1
  178. package/dist/tools/manifest.d.ts.map +1 -1
  179. package/dist/tools/manifest.js +9 -0
  180. package/dist/tools/manifest.js.map +1 -1
  181. package/dist/tools/map.js +1 -1
  182. package/dist/tools/map.js.map +1 -1
  183. package/dist/tools/monitor-websocket.d.ts +1 -1
  184. package/dist/tools/monitor-websocket.js +1 -1
  185. package/dist/tools/monitor-websocket.js.map +1 -1
  186. package/dist/tools/monitor.d.ts +2 -2
  187. package/dist/tools/news-search.d.ts +1 -1
  188. package/dist/tools/news-search.js +2 -2
  189. package/dist/tools/news-search.js.map +1 -1
  190. package/dist/tools/query-api.d.ts +5 -5
  191. package/dist/tools/query-api.js +1 -1
  192. package/dist/tools/query-api.js.map +1 -1
  193. package/dist/tools/readability.js +1 -1
  194. package/dist/tools/readability.js.map +1 -1
  195. package/dist/tools/record-flow.d.ts +2 -2
  196. package/dist/tools/record-flow.js +3 -3
  197. package/dist/tools/record-flow.js.map +1 -1
  198. package/dist/tools/reddit.d.ts +2 -2
  199. package/dist/tools/reddit.js +3 -3
  200. package/dist/tools/reddit.js.map +1 -1
  201. package/dist/tools/rss.js +1 -1
  202. package/dist/tools/rss.js.map +1 -1
  203. package/dist/tools/run-flow.d.ts +6 -6
  204. package/dist/tools/run-skill.d.ts +6 -6
  205. package/dist/tools/run-skill.js +2 -2
  206. package/dist/tools/run-skill.js.map +1 -1
  207. package/dist/tools/scrape.d.ts +4 -4
  208. package/dist/tools/scrape.js +1 -1
  209. package/dist/tools/scrape.js.map +1 -1
  210. package/dist/tools/screenshot.js +1 -1
  211. package/dist/tools/screenshot.js.map +1 -1
  212. package/dist/tools/search.d.ts +1 -1
  213. package/dist/tools/search.js +2 -2
  214. package/dist/tools/search.js.map +1 -1
  215. package/dist/tools/snapshot.d.ts +5 -5
  216. package/dist/tools/snapshot.js +2 -2
  217. package/dist/tools/snapshot.js.map +1 -1
  218. package/dist/tools/video-search.d.ts +1 -1
  219. package/dist/tools/video-search.js +2 -2
  220. package/dist/tools/video-search.js.map +1 -1
  221. package/dist/tools/watch.d.ts +2 -2
  222. package/dist/tools/watch.js +1 -1
  223. package/dist/tools/watch.js.map +1 -1
  224. package/dist/tools/youtube.d.ts +1 -1
  225. package/dist/tools/youtube.js +4 -4
  226. package/dist/tools/youtube.js.map +1 -1
  227. package/dist/types.d.ts +14 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +5 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/utils/fetcher.js +1 -1
  232. package/dist/utils/fetcher.js.map +1 -1
  233. package/dist/utils/robots.js +1 -1
  234. package/dist/utils/robots.js.map +1 -1
  235. package/package.json +7 -3
@@ -0,0 +1,945 @@
1
+ /**
2
+ * Full TUI (Terminal User Interface) for imperium-crawl — v3
3
+ *
4
+ * Slash-command-driven UX (Claude Code aesthetic).
5
+ * readline for main prompt, @clack/prompts for param collection only.
6
+ *
7
+ * Activated when: no CLI args AND process.stdout.isTTY.
8
+ * Non-TTY mode (pipe/CI/agents) shows --help output.
9
+ * CLI subcommands (scrape, crawl, etc.) are unaffected — they bypass this.
10
+ */
11
+ import chalk from "chalk";
12
+ import Table from "cli-table3";
13
+ import readline from "node:readline";
14
+ import { readdirSync, writeFileSync } from "node:fs";
15
+ import { text, select, multiselect, confirm, spinner as clackSpinner, isCancel, } from "@clack/prompts";
16
+ import { loadCliConfig } from "./config.js";
17
+ import { PACKAGE_VERSION } from "../core/constants.js";
18
+ import { parseToolOutput } from "../core/formatters.js";
19
+ import { getSkillsDir, getJobsDir } from "../core/config.js";
20
+ // ── Cancel Signal ────────────────────────────────────────────────────
21
+ class TuiCancelError extends Error {
22
+ constructor() {
23
+ super("cancelled");
24
+ this.name = "TuiCancelError";
25
+ }
26
+ }
27
+ /** Throw TuiCancelError if the prompt result is a cancel symbol. */
28
+ function cc(val) {
29
+ if (isCancel(val))
30
+ throw new TuiCancelError();
31
+ return val;
32
+ }
33
+ const SLASH_COMMANDS = [
34
+ // Scraping
35
+ { cmd: "/scrape", tool: "scrape", argField: "url", desc: "Scrape a web page", category: "Scraping" },
36
+ { cmd: "/crawl", tool: "crawl", argField: "url", desc: "Crawl a website", category: "Scraping" },
37
+ { cmd: "/map", tool: "map", argField: "url", desc: "Discover all URLs", category: "Scraping" },
38
+ { cmd: "/extract", tool: "extract", argField: "url", desc: "CSS selector extraction", category: "Scraping" },
39
+ { cmd: "/read", tool: "readability", argField: "url", desc: "Article content (readability)", category: "Scraping" },
40
+ { cmd: "/screenshot", tool: "screenshot", argField: "url", desc: "Page screenshot", category: "Scraping" },
41
+ // Search
42
+ { cmd: "/search", tool: "search", argField: "query", desc: "Web search", category: "Search" },
43
+ { cmd: "/news", tool: "news-search", argField: "query", desc: "News search", category: "Search" },
44
+ { cmd: "/images", tool: "image-search", argField: "query", desc: "Image search", category: "Search" },
45
+ { cmd: "/videos", tool: "video-search", argField: "query", desc: "Video search", category: "Search" },
46
+ // AI & Automation
47
+ { cmd: "/ai", tool: "ai-extract", argField: "url", desc: "AI data extraction", category: "AI & Automation" },
48
+ { cmd: "/interact", tool: "interact", desc: "Browser automation", category: "AI & Automation" },
49
+ // Batch & Jobs
50
+ { cmd: "/batch", tool: "batch-scrape", desc: "Parallel batch scraping", category: "Batch & Jobs" },
51
+ { cmd: "/jobs", tool: "list-jobs", desc: "List batch jobs", category: "Batch & Jobs" },
52
+ { cmd: "/job", tool: "job-status", argField: "job_id", desc: "Check job status", category: "Batch & Jobs" },
53
+ { cmd: "/delete-job", tool: "delete-job", argField: "job_id", desc: "Delete a batch job", category: "Batch & Jobs" },
54
+ // Skills
55
+ { cmd: "/skills", tool: "list-skills", desc: "List saved skills", category: "Skills" },
56
+ { cmd: "/create-skill", tool: "create-skill", argField: "url", desc: "Create a reusable scraper", category: "Skills" },
57
+ { cmd: "/run-skill", tool: "run-skill", argField: "name", desc: "Run a saved skill", category: "Skills" },
58
+ // API Discovery
59
+ { cmd: "/discover", tool: "discover-apis", argField: "url", desc: "Find APIs on a page", category: "API Discovery" },
60
+ { cmd: "/query-api", tool: "query-api", argField: "url", desc: "Query an API endpoint", category: "API Discovery" },
61
+ { cmd: "/ws", tool: "monitor-websocket", argField: "url", desc: "Monitor WebSocket", category: "API Discovery" },
62
+ ];
63
+ // System commands (not tool-backed)
64
+ const SYSTEM_COMMANDS = ["/help", "/save", "/again", "/clear", "/setup", "/exit"];
65
+ // ── Text Utilities ───────────────────────────────────────────────────
66
+ /** Word-wrap text to `width` cols. Preserves existing newlines. */
67
+ function wrapText(text, width) {
68
+ if (width <= 0)
69
+ return text;
70
+ return text
71
+ .split("\n")
72
+ .map((line) => {
73
+ if (line.length <= width)
74
+ return line;
75
+ const words = line.split(" ");
76
+ const lines = [];
77
+ let current = "";
78
+ for (const word of words) {
79
+ if (current.length + word.length + 1 > width) {
80
+ if (current)
81
+ lines.push(current);
82
+ current = word;
83
+ }
84
+ else {
85
+ current = current ? `${current} ${word}` : word;
86
+ }
87
+ }
88
+ if (current)
89
+ lines.push(current);
90
+ return lines.join("\n");
91
+ })
92
+ .join("\n");
93
+ }
94
+ /** Minimal markdown → chalk rendering for terminal display. */
95
+ function renderMarkdown(text) {
96
+ const cols = process.stdout.columns ?? 80;
97
+ const lines = text.split("\n");
98
+ const out = [];
99
+ let inCodeBlock = false;
100
+ let codeLines = [];
101
+ for (const line of lines) {
102
+ // Fenced code block
103
+ if (line.trimStart().startsWith("```")) {
104
+ if (inCodeBlock) {
105
+ // Close block
106
+ const blockContent = codeLines.join("\n");
107
+ out.push(chalk.bgBlack.cyan(" " + blockContent.replace(/\n/g, "\n ") + " "));
108
+ codeLines = [];
109
+ inCodeBlock = false;
110
+ }
111
+ else {
112
+ inCodeBlock = true;
113
+ }
114
+ continue;
115
+ }
116
+ if (inCodeBlock) {
117
+ codeLines.push(line);
118
+ continue;
119
+ }
120
+ // Headings
121
+ const h3 = line.match(/^###\s+(.+)/);
122
+ if (h3) {
123
+ out.push(chalk.bold(h3[1]));
124
+ continue;
125
+ }
126
+ const h2 = line.match(/^##\s+(.+)/);
127
+ if (h2) {
128
+ out.push(chalk.bold.underline(h2[1]));
129
+ continue;
130
+ }
131
+ const h1 = line.match(/^#\s+(.+)/);
132
+ if (h1) {
133
+ out.push(chalk.bold.underline(h1[1]));
134
+ continue;
135
+ }
136
+ // List items
137
+ const listItem = line.match(/^(\s*)[*-]\s+(.+)/);
138
+ if (listItem) {
139
+ const indent = listItem[1];
140
+ let content = listItem[2];
141
+ content = applyInlineMarkdown(content);
142
+ out.push(`${indent} • ${content}`);
143
+ continue;
144
+ }
145
+ // Numbered list
146
+ const numItem = line.match(/^(\s*)\d+\.\s+(.+)/);
147
+ if (numItem) {
148
+ const indent = numItem[1];
149
+ const content = applyInlineMarkdown(numItem[2]);
150
+ out.push(`${indent} ${content}`);
151
+ continue;
152
+ }
153
+ // Normal line — apply inline transforms + word wrap
154
+ const processed = applyInlineMarkdown(line);
155
+ out.push(wrapText(processed, cols - 4));
156
+ }
157
+ // Unclosed code block
158
+ if (inCodeBlock && codeLines.length) {
159
+ out.push(chalk.bgBlack.cyan(" " + codeLines.join("\n ") + " "));
160
+ }
161
+ return out.join("\n");
162
+ }
163
+ function applyInlineMarkdown(text) {
164
+ // Bold: **text** or __text__
165
+ text = text.replace(/\*\*(.+?)\*\*/g, (_, t) => chalk.bold(t));
166
+ text = text.replace(/__(.+?)__/g, (_, t) => chalk.bold(t));
167
+ // Italic: *text* or _text_
168
+ text = text.replace(/\*([^*]+?)\*/g, (_, t) => chalk.italic(t));
169
+ text = text.replace(/_([^_]+?)_/g, (_, t) => chalk.italic(t));
170
+ // Inline code: `code`
171
+ text = text.replace(/`([^`]+?)`/g, (_, t) => chalk.cyan(t));
172
+ return text;
173
+ }
174
+ // ── Header ───────────────────────────────────────────────────────────
175
+ function showHeader() {
176
+ const hasBrave = !!process.env.BRAVE_API_KEY;
177
+ const hasCaptcha = !!(process.env.TWOCAPTCHA_API_KEY || process.env.TWO_CAPTCHA_API_KEY);
178
+ // Count jobs and skills
179
+ let jobCount = 0;
180
+ let skillCount = 0;
181
+ try {
182
+ jobCount = readdirSync(getJobsDir()).filter(f => f.endsWith(".json")).length;
183
+ }
184
+ catch { /* none */ }
185
+ try {
186
+ skillCount = readdirSync(getSkillsDir()).filter(f => f.endsWith(".json")).length;
187
+ }
188
+ catch { /* none */ }
189
+ // Status checks
190
+ const checks = [];
191
+ if (hasBrave)
192
+ checks.push(chalk.green("✓") + " " + chalk.white("Brave Search"));
193
+ if (hasCaptcha)
194
+ checks.push(chalk.green("✓") + " " + chalk.white("2Captcha"));
195
+ const statsStr = chalk.dim(`${jobCount} job${jobCount !== 1 ? "s" : ""} · ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
196
+ console.log();
197
+ console.log(` ${chalk.bold.magenta("✻")} ${chalk.bold("imperiumcrawl")} ${chalk.dim(`v${PACKAGE_VERSION}`)}`);
198
+ if (checks.length > 0) {
199
+ console.log(` ${checks.join(" ")} ${statsStr}`);
200
+ }
201
+ else {
202
+ console.log(` ${chalk.dim("No API keys configured")} ${statsStr}`);
203
+ }
204
+ console.log();
205
+ console.log(chalk.dim(" /help for commands"));
206
+ console.log();
207
+ }
208
+ // ── Help Screen ──────────────────────────────────────────────────────
209
+ function showHelp() {
210
+ // Group commands by category
211
+ const categories = new Map();
212
+ for (const cmd of SLASH_COMMANDS) {
213
+ const existing = categories.get(cmd.category) ?? [];
214
+ existing.push(cmd);
215
+ categories.set(cmd.category, existing);
216
+ }
217
+ console.log();
218
+ for (const [category, commands] of categories) {
219
+ const sep = chalk.dim("─".repeat(Math.max(0, 42 - category.length - 1)));
220
+ console.log(` ${chalk.bold(category)} ${sep}`);
221
+ for (const cmd of commands) {
222
+ const padded = (cmd.cmd + " ").padEnd(20);
223
+ console.log(` ${chalk.cyan(padded)}${chalk.dim(cmd.desc)}`);
224
+ }
225
+ console.log();
226
+ }
227
+ console.log(chalk.dim(` /save · /again · /setup · /clear · /exit`));
228
+ console.log();
229
+ console.log();
230
+ }
231
+ function unwrapZod(schema) {
232
+ let isOptional = false;
233
+ let hasDefault = false;
234
+ let defaultValue = undefined;
235
+ const description = schema.description;
236
+ let current = schema;
237
+ for (let i = 0; i < 10; i++) {
238
+ const typeName = current._def.typeName;
239
+ if (typeName === "ZodDefault") {
240
+ hasDefault = true;
241
+ defaultValue = current._def.defaultValue();
242
+ current = current._def.innerType;
243
+ }
244
+ else if (typeName === "ZodOptional" || typeName === "ZodNullable") {
245
+ isOptional = true;
246
+ current = current._def.innerType;
247
+ }
248
+ else {
249
+ break;
250
+ }
251
+ }
252
+ return { base: current, isOptional, hasDefault, defaultValue, description };
253
+ }
254
+ function getZodTypeName(schema) {
255
+ return schema._def.typeName ?? "";
256
+ }
257
+ // ── Interact Action Wizard ───────────────────────────────────────────
258
+ async function collectInteractActions() {
259
+ const actions = [];
260
+ for (let idx = 1;; idx++) {
261
+ const actionType = cc(await select({
262
+ message: `Add action #${idx}:`,
263
+ options: [
264
+ { value: "navigate", label: "navigate", hint: "Go to a URL" },
265
+ { value: "click", label: "click", hint: "Click an element" },
266
+ { value: "type", label: "type", hint: "Type text into a field" },
267
+ { value: "scroll", label: "scroll", hint: "Scroll the page" },
268
+ { value: "wait", label: "wait", hint: "Wait N milliseconds" },
269
+ { value: "screenshot", label: "screenshot", hint: "Take a screenshot" },
270
+ { value: "evaluate", label: "evaluate", hint: "Run JavaScript on the page" },
271
+ { value: "press", label: "press", hint: "Press a keyboard key" },
272
+ { value: "select", label: "select", hint: "Select a dropdown option" },
273
+ { value: "hover", label: "hover", hint: "Hover over an element" },
274
+ ],
275
+ }));
276
+ const action = { type: actionType };
277
+ if (actionType === "navigate") {
278
+ action.url = cc(await text({
279
+ message: "URL to navigate to:",
280
+ placeholder: "https://example.com",
281
+ validate: (v) => ((v ?? "").trim() ? undefined : "URL is required"),
282
+ }));
283
+ }
284
+ else if (actionType === "click" || actionType === "hover") {
285
+ action.selector = cc(await text({
286
+ message: "CSS selector:",
287
+ placeholder: "button.submit, #login-btn",
288
+ validate: (v) => ((v ?? "").trim() ? undefined : "Selector is required"),
289
+ }));
290
+ }
291
+ else if (actionType === "type") {
292
+ action.selector = cc(await text({
293
+ message: "CSS selector (input field):",
294
+ placeholder: "input[name='q']",
295
+ validate: (v) => ((v ?? "").trim() ? undefined : "Selector is required"),
296
+ }));
297
+ action.text = cc(await text({
298
+ message: "Text to type:",
299
+ validate: (v) => ((v ?? "").trim() ? undefined : "Text is required"),
300
+ }));
301
+ }
302
+ else if (actionType === "scroll") {
303
+ const dir = cc(await select({
304
+ message: "Scroll direction:",
305
+ options: [
306
+ { value: "down", label: "down" },
307
+ { value: "up", label: "up" },
308
+ ],
309
+ }));
310
+ const amount = cc(await text({
311
+ message: "Amount (pixels):",
312
+ placeholder: "500",
313
+ validate: (v) => (isNaN(Number(v ?? "")) ? "Enter a number" : undefined),
314
+ }));
315
+ action.direction = dir;
316
+ action.amount = Number(amount);
317
+ }
318
+ else if (actionType === "wait") {
319
+ const ms = cc(await text({
320
+ message: "Wait milliseconds:",
321
+ placeholder: "1000",
322
+ validate: (v) => (isNaN(Number(v ?? "")) ? "Enter a number" : undefined),
323
+ }));
324
+ action.milliseconds = Number(ms);
325
+ }
326
+ else if (actionType === "evaluate") {
327
+ action.code = cc(await text({
328
+ message: "JavaScript to run:",
329
+ placeholder: "document.title",
330
+ validate: (v) => ((v ?? "").trim() ? undefined : "Code is required"),
331
+ }));
332
+ }
333
+ else if (actionType === "press") {
334
+ action.key = cc(await text({
335
+ message: "Key to press:",
336
+ placeholder: "Enter, Escape, Tab, ArrowDown",
337
+ validate: (v) => ((v ?? "").trim() ? undefined : "Key is required"),
338
+ }));
339
+ }
340
+ else if (actionType === "select") {
341
+ action.selector = cc(await text({
342
+ message: "CSS selector (select element):",
343
+ validate: (v) => ((v ?? "").trim() ? undefined : "Selector is required"),
344
+ }));
345
+ action.value = cc(await text({
346
+ message: "Option value to select:",
347
+ validate: (v) => ((v ?? "").trim() ? undefined : "Value is required"),
348
+ }));
349
+ }
350
+ // screenshot: no extra params
351
+ actions.push(action);
352
+ const addMore = cc(await confirm({ message: "Add another action?", initialValue: true }));
353
+ if (!addMore)
354
+ break;
355
+ }
356
+ return actions;
357
+ }
358
+ // ── Param Collector ──────────────────────────────────────────────────
359
+ async function collectParamField(cmd, key, fieldSchema, isRequired) {
360
+ // Special case: interact tool's actions array
361
+ if (cmd === "interact" && key === "actions") {
362
+ const value = await collectInteractActions();
363
+ return { value, skip: false };
364
+ }
365
+ const { base, hasDefault, defaultValue, description } = unwrapZod(fieldSchema);
366
+ const typeName = getZodTypeName(base);
367
+ const label = description ?? key.replace(/_/g, " ");
368
+ const msgRequired = chalk.white(label);
369
+ const msgOptional = chalk.dim(label) + chalk.dim(" (optional)");
370
+ const msg = isRequired ? msgRequired : msgOptional;
371
+ if (typeName === "ZodString") {
372
+ if (isRequired) {
373
+ const val = cc(await text({
374
+ message: `${msg}:`,
375
+ validate: (v) => ((v ?? "").trim() ? undefined : `${key} is required`),
376
+ }));
377
+ return { value: val, skip: false };
378
+ }
379
+ else {
380
+ const val = cc(await text({
381
+ message: `${msg}:`,
382
+ placeholder: hasDefault ? String(defaultValue) : "press Enter to skip",
383
+ }));
384
+ if (val.trim())
385
+ return { value: val, skip: false };
386
+ if (hasDefault)
387
+ return { value: defaultValue, skip: false };
388
+ return { value: undefined, skip: true };
389
+ }
390
+ }
391
+ else if (typeName === "ZodNumber") {
392
+ if (isRequired) {
393
+ const val = cc(await text({
394
+ message: `${msg}:`,
395
+ validate: (v) => (isNaN(Number(v)) ? "Enter a valid number" : undefined),
396
+ }));
397
+ return { value: parseFloat(val), skip: false };
398
+ }
399
+ else {
400
+ const val = cc(await text({
401
+ message: `${msg}:`,
402
+ placeholder: hasDefault ? String(defaultValue) : "optional number",
403
+ }));
404
+ if (val.trim() && !isNaN(Number(val)))
405
+ return { value: parseFloat(val), skip: false };
406
+ if (hasDefault)
407
+ return { value: defaultValue, skip: false };
408
+ return { value: undefined, skip: true };
409
+ }
410
+ }
411
+ else if (typeName === "ZodBoolean") {
412
+ const val = cc(await confirm({
413
+ message: `${msg}?`,
414
+ initialValue: hasDefault ? defaultValue : false,
415
+ }));
416
+ return { value: val, skip: false };
417
+ }
418
+ else if (typeName === "ZodEnum") {
419
+ const values = base._def.values;
420
+ const val = cc(await select({
421
+ message: `${msg}:`,
422
+ options: values.map((v) => ({ value: v, label: v })),
423
+ ...(hasDefault ? { initialValue: String(defaultValue) } : {}),
424
+ }));
425
+ return { value: val, skip: false };
426
+ }
427
+ else if (typeName === "ZodArray") {
428
+ const innerUnwrapped = unwrapZod(base._def.type);
429
+ const innerTypeName = getZodTypeName(innerUnwrapped.base);
430
+ if (innerTypeName === "ZodEnum") {
431
+ const values = innerUnwrapped.base._def.values;
432
+ const val = cc(await multiselect({
433
+ message: `${msg}:`,
434
+ options: values.map((v) => ({ value: v, label: v })),
435
+ required: isRequired,
436
+ }));
437
+ return { value: val, skip: false };
438
+ }
439
+ else {
440
+ const val = cc(await text({
441
+ message: `${msg} (comma-separated):`,
442
+ placeholder: hasDefault ? String(defaultValue) : "press Enter to skip",
443
+ }));
444
+ if (val.trim()) {
445
+ return {
446
+ value: val.split(",").map((s) => s.trim()).filter(Boolean),
447
+ skip: false,
448
+ };
449
+ }
450
+ if (hasDefault)
451
+ return { value: defaultValue, skip: false };
452
+ return { value: [], skip: false };
453
+ }
454
+ }
455
+ else if (typeName === "ZodRecord") {
456
+ const val = cc(await text({
457
+ message: `${msg} (JSON object):`,
458
+ placeholder: hasDefault ? JSON.stringify(defaultValue) : '{"key": "value"}',
459
+ validate: (v) => {
460
+ if (!(v ?? "").trim())
461
+ return undefined;
462
+ try {
463
+ JSON.parse(v ?? "");
464
+ return undefined;
465
+ }
466
+ catch {
467
+ return "Invalid JSON";
468
+ }
469
+ },
470
+ }));
471
+ if (val.trim())
472
+ return { value: JSON.parse(val), skip: false };
473
+ if (hasDefault)
474
+ return { value: defaultValue, skip: false };
475
+ return { value: undefined, skip: true };
476
+ }
477
+ else {
478
+ // Fallback: treat as optional text
479
+ const val = cc(await text({
480
+ message: `${msg}:`,
481
+ placeholder: "optional — press Enter to skip",
482
+ }));
483
+ if (val.trim())
484
+ return { value: val, skip: false };
485
+ return { value: undefined, skip: true };
486
+ }
487
+ }
488
+ async function collectParams(cmd, toolModule, prefilled = {}) {
489
+ if (getZodTypeName(toolModule.schema) !== "ZodObject")
490
+ return { ...prefilled };
491
+ const shape = toolModule.schema.shape;
492
+ const entries = Object.entries(shape);
493
+ const required = [];
494
+ const optional = [];
495
+ for (const [key, fieldSchema] of entries) {
496
+ const { isOptional, hasDefault } = unwrapZod(fieldSchema);
497
+ if (isOptional || hasDefault) {
498
+ optional.push([key, fieldSchema]);
499
+ }
500
+ else {
501
+ required.push([key, fieldSchema]);
502
+ }
503
+ }
504
+ const params = { ...prefilled };
505
+ // Collect required fields (skip if already prefilled)
506
+ for (const [key, fieldSchema] of required) {
507
+ if (params[key] !== undefined)
508
+ continue;
509
+ const { value, skip } = await collectParamField(cmd, key, fieldSchema, true);
510
+ if (!skip)
511
+ params[key] = value;
512
+ }
513
+ // Ask if user wants optional fields
514
+ if (optional.length > 0) {
515
+ const showOptional = cc(await confirm({
516
+ message: `Show ${optional.length} advanced option${optional.length !== 1 ? "s" : ""}?`,
517
+ initialValue: false,
518
+ }));
519
+ if (showOptional) {
520
+ for (const [key, fieldSchema] of optional) {
521
+ if (params[key] !== undefined)
522
+ continue;
523
+ const { value, skip } = await collectParamField(cmd, key, fieldSchema, false);
524
+ if (!skip && value !== undefined)
525
+ params[key] = value;
526
+ }
527
+ }
528
+ }
529
+ return params;
530
+ }
531
+ // ── Execute with Progress ────────────────────────────────────────────
532
+ async function executeWithProgress(tool, params) {
533
+ const s = clackSpinner();
534
+ s.start(`Running ${tool.name}…`);
535
+ const start = Date.now();
536
+ try {
537
+ const result = await tool.execute(params);
538
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
539
+ s.stop(`Done in ${elapsed}s`);
540
+ return parseToolOutput(result);
541
+ }
542
+ catch (err) {
543
+ const msg = err instanceof Error ? err.message : String(err);
544
+ s.stop(chalk.red("✗ Failed"));
545
+ const isApiKeyError = msg.includes("API_KEY") ||
546
+ msg.toLowerCase().includes("api key") ||
547
+ msg.includes("401") ||
548
+ msg.includes("403") ||
549
+ msg.includes("unauthorized");
550
+ console.log();
551
+ console.log(chalk.red(` ✗ ${msg}`));
552
+ if (isApiKeyError) {
553
+ console.log(chalk.yellow(` Hint: Run /setup to configure API keys.`));
554
+ }
555
+ console.log();
556
+ return null;
557
+ }
558
+ }
559
+ // ── Pretty Printer ───────────────────────────────────────────────────
560
+ function prettyPrint(data, indent = 0) {
561
+ const pad = " ".repeat(indent);
562
+ const maxDepth = 3;
563
+ if (data === null || data === undefined) {
564
+ process.stdout.write(chalk.dim("null"));
565
+ return;
566
+ }
567
+ if (typeof data === "string") {
568
+ process.stdout.write(chalk.white(JSON.stringify(data)));
569
+ return;
570
+ }
571
+ if (typeof data === "number") {
572
+ process.stdout.write(chalk.yellow(String(data)));
573
+ return;
574
+ }
575
+ if (typeof data === "boolean") {
576
+ process.stdout.write(chalk.cyan(String(data)));
577
+ return;
578
+ }
579
+ if (Array.isArray(data)) {
580
+ if (data.length === 0) {
581
+ process.stdout.write(chalk.dim("[]"));
582
+ return;
583
+ }
584
+ // Compact short arrays of primitives
585
+ const allPrimitive = data.every((v) => typeof v !== "object" || v === null);
586
+ if (allPrimitive && data.length <= 6) {
587
+ const items = data.map((v) => typeof v === "string"
588
+ ? chalk.white(JSON.stringify(v))
589
+ : typeof v === "number"
590
+ ? chalk.yellow(String(v))
591
+ : chalk.cyan(String(v)));
592
+ process.stdout.write(`[${items.join(chalk.dim(", "))}]`);
593
+ return;
594
+ }
595
+ if (indent >= maxDepth) {
596
+ process.stdout.write(chalk.dim(`[… ${data.length} items]`));
597
+ return;
598
+ }
599
+ process.stdout.write("[\n");
600
+ data.slice(0, 20).forEach((item, i) => {
601
+ process.stdout.write(`${pad} `);
602
+ prettyPrint(item, indent + 1);
603
+ if (i < data.length - 1)
604
+ process.stdout.write(chalk.dim(","));
605
+ process.stdout.write("\n");
606
+ });
607
+ if (data.length > 20) {
608
+ process.stdout.write(`${pad} ${chalk.dim(`… ${data.length - 20} more items`)}\n`);
609
+ }
610
+ process.stdout.write(`${pad}]`);
611
+ return;
612
+ }
613
+ if (typeof data === "object") {
614
+ const obj = data;
615
+ const keys = Object.keys(obj);
616
+ if (keys.length === 0) {
617
+ process.stdout.write(chalk.dim("{}"));
618
+ return;
619
+ }
620
+ if (indent >= maxDepth) {
621
+ process.stdout.write(chalk.dim(`{… ${keys.length} keys}`));
622
+ return;
623
+ }
624
+ process.stdout.write("{\n");
625
+ keys.forEach((key, i) => {
626
+ process.stdout.write(`${pad} ${chalk.dim(key)}: `);
627
+ prettyPrint(obj[key], indent + 1);
628
+ if (i < keys.length - 1)
629
+ process.stdout.write(chalk.dim(","));
630
+ process.stdout.write("\n");
631
+ });
632
+ process.stdout.write(`${pad}}`);
633
+ return;
634
+ }
635
+ process.stdout.write(String(data));
636
+ }
637
+ // ── Table Renderer ───────────────────────────────────────────────────
638
+ function renderBox(headers, rows) {
639
+ const table = new Table({
640
+ head: headers.map((h) => chalk.cyan(h)),
641
+ style: { head: [], border: [] },
642
+ });
643
+ for (const row of rows)
644
+ table.push(row);
645
+ return table.toString();
646
+ }
647
+ // ── Result Display ───────────────────────────────────────────────────
648
+ function displayResults(data, cmd) {
649
+ if (data === null || data === undefined)
650
+ return;
651
+ const toolName = cmd.replace(/-/g, "_");
652
+ const obj = typeof data === "object" && data !== null
653
+ ? data
654
+ : null;
655
+ const sep = chalk.dim("─".repeat(Math.min(process.stdout.columns ?? 80, 60)));
656
+ console.log();
657
+ // list_jobs → table
658
+ if (toolName === "list_jobs" && obj) {
659
+ const jobs = Array.isArray(obj.jobs) ? obj.jobs : [];
660
+ if (jobs.length === 0) {
661
+ console.log(chalk.dim(" No jobs found."));
662
+ console.log();
663
+ return;
664
+ }
665
+ const rows = jobs.map((j) => {
666
+ const job = j;
667
+ const total = Number(job.urls_total ?? 0);
668
+ const done = Number(job.urls_completed ?? 0);
669
+ const failed = Number(job.urls_failed ?? 0);
670
+ const pct = total > 0 ? Math.round(((done + failed) / total) * 100) : 0;
671
+ return [
672
+ String(job.id ?? ""),
673
+ String(job.status ?? ""),
674
+ `${done + failed}/${total}`,
675
+ `${pct}%`,
676
+ ];
677
+ });
678
+ console.log(renderBox(["Job ID", "Status", "URLs", "Progress"], rows));
679
+ console.log();
680
+ return;
681
+ }
682
+ // list_skills → table
683
+ if (toolName === "list_skills" && obj) {
684
+ const skills = Array.isArray(obj.skills) ? obj.skills : [];
685
+ if (skills.length === 0) {
686
+ console.log(chalk.dim(" No skills found. Use /create-skill to create one."));
687
+ console.log();
688
+ return;
689
+ }
690
+ const rows = skills.map((s) => {
691
+ const skill = s;
692
+ const fields = Array.isArray(skill.fields)
693
+ ? skill.fields.join(", ")
694
+ : "";
695
+ const created = typeof skill.created_at === "string" ? skill.created_at.split("T")[0] : "";
696
+ return [
697
+ String(skill.name ?? ""),
698
+ chalk.cyan(String(skill.url ?? "")),
699
+ fields,
700
+ created,
701
+ ];
702
+ });
703
+ console.log(renderBox(["Name", "URL", "Fields", "Created"], rows));
704
+ console.log();
705
+ return;
706
+ }
707
+ // search / news / image / video → numbered table
708
+ if (["search", "news_search", "image_search", "video_search"].includes(toolName) && obj) {
709
+ const results = Array.isArray(obj.results) ? obj.results : [];
710
+ if (results.length === 0) {
711
+ console.log(chalk.dim(" No results found."));
712
+ console.log();
713
+ return;
714
+ }
715
+ const rows = results.slice(0, 20).map((r, i) => {
716
+ const result = r;
717
+ const title = chalk.white(String(result.title ?? result.name ?? "").slice(0, 50));
718
+ const url = chalk.dim(String(result.url ?? result.source ?? "").slice(0, 60));
719
+ return [chalk.dim(String(i + 1)), title, url];
720
+ });
721
+ console.log(renderBox(["#", "Title", "URL"], rows));
722
+ console.log();
723
+ return;
724
+ }
725
+ // scrape / readability / crawl → content preview
726
+ if (["scrape", "readability", "crawl"].includes(toolName) && obj) {
727
+ const content = typeof obj.markdown === "string"
728
+ ? obj.markdown
729
+ : typeof obj.content === "string"
730
+ ? obj.content
731
+ : typeof obj.text === "string"
732
+ ? obj.text
733
+ : null;
734
+ console.log(sep);
735
+ if (typeof obj.title === "string" && obj.title)
736
+ console.log(` ${chalk.bold.white(obj.title)}`);
737
+ if (typeof obj.url === "string" && obj.url)
738
+ console.log(` ${chalk.dim.cyan(obj.url)}`);
739
+ console.log(sep);
740
+ if (content) {
741
+ const cols = process.stdout.columns ?? 80;
742
+ const preview = content.slice(0, 600);
743
+ const remaining = content.length - 600;
744
+ console.log(wrapText(preview, cols - 4).split("\n").map((l) => ` ${l}`).join("\n"));
745
+ if (remaining > 0) {
746
+ console.log(chalk.dim(`\n … ${remaining.toLocaleString()} more chars · /save to export`));
747
+ }
748
+ }
749
+ console.log();
750
+ return;
751
+ }
752
+ // batch_scrape → summary table + failed URLs
753
+ if (toolName === "batch_scrape" && obj) {
754
+ const total = Number(obj.urls_total ?? 0);
755
+ const completed = Number(obj.urls_completed ?? 0);
756
+ const failed = Number(obj.urls_failed ?? 0);
757
+ const duration = typeof obj.duration_ms === "number"
758
+ ? `${(obj.duration_ms / 1000).toFixed(1)}s`
759
+ : "—";
760
+ console.log(renderBox(["Total", "Completed", "Failed", "Duration"], [[String(total), String(completed), String(failed), duration]]));
761
+ const failedUrls = Array.isArray(obj.failed_urls) ? obj.failed_urls : [];
762
+ if (failedUrls.length > 0) {
763
+ console.log(chalk.red(`\n Failed URLs (top 5):`));
764
+ failedUrls.slice(0, 5).forEach((u, i) => {
765
+ console.log(chalk.dim(` ${i + 1}. ${u}`));
766
+ });
767
+ }
768
+ console.log();
769
+ return;
770
+ }
771
+ // Default: pretty-printed structured output
772
+ console.log(sep);
773
+ console.log();
774
+ process.stdout.write(" ");
775
+ prettyPrint(data, 0);
776
+ console.log();
777
+ console.log();
778
+ }
779
+ // ── Save to File ─────────────────────────────────────────────────────
780
+ function saveToFile(data, filename) {
781
+ const fname = filename?.trim() || `results-${Date.now()}.json`;
782
+ writeFileSync(fname, JSON.stringify(data, null, 2) + "\n", "utf-8");
783
+ console.log(chalk.green(` ✓ Saved to ${fname}`));
784
+ console.log();
785
+ }
786
+ async function handleSlashCommand(input, state) {
787
+ const parts = input.trim().split(/\s+/);
788
+ const cmdName = parts[0].toLowerCase();
789
+ const argValue = parts.slice(1).join(" ").trim() || undefined;
790
+ // ── System commands ──────────────────────────────────────────────
791
+ if (cmdName === "/help" || cmdName === "/h" || cmdName === "/?") {
792
+ showHelp();
793
+ return;
794
+ }
795
+ if (cmdName === "/exit" || cmdName === "/quit" || cmdName === "/q") {
796
+ console.log(chalk.dim("\n Bye!\n"));
797
+ process.exit(0);
798
+ }
799
+ if (cmdName === "/clear" || cmdName === "/cls") {
800
+ process.stdout.write("\x1b[2J\x1b[H");
801
+ showHeader();
802
+ return;
803
+ }
804
+ if (cmdName === "/setup") {
805
+ const { runSetup } = await import("./onboarding.js");
806
+ await runSetup();
807
+ return;
808
+ }
809
+ if (cmdName === "/save") {
810
+ if (state.lastResult === null) {
811
+ console.log(chalk.dim(" No results to save.\n"));
812
+ return;
813
+ }
814
+ saveToFile(state.lastResult, argValue);
815
+ return;
816
+ }
817
+ if (cmdName === "/again") {
818
+ if (!state.lastToolModule || !state.lastParams) {
819
+ console.log(chalk.dim(" No previous command to repeat.\n"));
820
+ return;
821
+ }
822
+ state.lastResult = await executeWithProgress(state.lastToolModule, state.lastParams);
823
+ if (state.lastResult !== null)
824
+ displayResults(state.lastResult, state.lastCmd);
825
+ return;
826
+ }
827
+ // ── Tool slash commands ──────────────────────────────────────────
828
+ const slashCmd = SLASH_COMMANDS.find((sc) => sc.cmd === cmdName);
829
+ if (!slashCmd) {
830
+ console.log(chalk.dim(` Unknown command: ${cmdName}. Type /help for commands.\n`));
831
+ return;
832
+ }
833
+ // Load tool module
834
+ let toolModule;
835
+ try {
836
+ toolModule = (await import(`../tools/${slashCmd.tool}.js`));
837
+ }
838
+ catch {
839
+ console.log(chalk.red(` ✗ Could not load tool: ${slashCmd.tool}\n`));
840
+ return;
841
+ }
842
+ // Pre-fill inline arg
843
+ const prefilled = {};
844
+ if (argValue && slashCmd.argField) {
845
+ prefilled[slashCmd.argField] = argValue;
846
+ }
847
+ // Collect params using @clack (readline will be paused)
848
+ let params;
849
+ try {
850
+ params = await collectParams(slashCmd.tool, toolModule, prefilled);
851
+ }
852
+ catch (e) {
853
+ if (e instanceof TuiCancelError) {
854
+ console.log();
855
+ return;
856
+ }
857
+ throw e;
858
+ }
859
+ // Execute
860
+ state.lastResult = await executeWithProgress(toolModule, params);
861
+ state.lastCmd = slashCmd.tool;
862
+ state.lastParams = params;
863
+ state.lastToolModule = toolModule;
864
+ if (state.lastResult !== null) {
865
+ displayResults(state.lastResult, slashCmd.tool);
866
+ }
867
+ }
868
+ // ── Readline Prompt Helper ───────────────────────────────────────────
869
+ function createPrompt() {
870
+ return readline.createInterface({
871
+ input: process.stdin,
872
+ output: process.stdout,
873
+ terminal: true,
874
+ });
875
+ }
876
+ function askLine(rl) {
877
+ return new Promise((resolve) => {
878
+ rl.question(chalk.bold.cyan("❯ "), (answer) => {
879
+ resolve(answer);
880
+ });
881
+ rl.once("close", () => resolve(null));
882
+ });
883
+ }
884
+ // ── Main Loop ────────────────────────────────────────────────────────
885
+ async function mainLoop() {
886
+ showHeader();
887
+ const state = {
888
+ lastResult: null,
889
+ lastCmd: null,
890
+ lastParams: null,
891
+ lastToolModule: null,
892
+ };
893
+ while (true) {
894
+ const rl = createPrompt();
895
+ const input = await askLine(rl);
896
+ rl.close();
897
+ // Ctrl+D or stream end
898
+ if (input === null) {
899
+ console.log(chalk.dim("\n Bye!\n"));
900
+ process.exit(0);
901
+ }
902
+ const trimmed = input.trim();
903
+ if (!trimmed)
904
+ continue;
905
+ // ── Slash command ────────────────────────────────────────────
906
+ if (trimmed.startsWith("/")) {
907
+ try {
908
+ await handleSlashCommand(trimmed, state);
909
+ }
910
+ catch (e) {
911
+ if (e instanceof TuiCancelError) {
912
+ console.log();
913
+ continue;
914
+ }
915
+ throw e;
916
+ }
917
+ continue;
918
+ }
919
+ // ── Unknown input (text without /) ─────────────────────────
920
+ console.log(chalk.dim(" Unknown input. Type /help for commands.\n"));
921
+ }
922
+ }
923
+ // ── Entry Point ──────────────────────────────────────────────────────
924
+ export async function runTui() {
925
+ // First-run check: no config AND no env API keys
926
+ const config = loadCliConfig();
927
+ const hasAnyKey = config.BRAVE_API_KEY ||
928
+ process.env.BRAVE_API_KEY;
929
+ if (!hasAnyKey && Object.keys(config).length === 0) {
930
+ console.log();
931
+ console.log(chalk.dim(" No API keys configured."));
932
+ console.log(chalk.dim(" Run /setup after startup to configure, or continue without.\n"));
933
+ }
934
+ try {
935
+ await mainLoop();
936
+ }
937
+ catch (e) {
938
+ if (e instanceof TuiCancelError) {
939
+ console.log(chalk.dim("\n Bye!\n"));
940
+ process.exit(0);
941
+ }
942
+ throw e;
943
+ }
944
+ }
945
+ //# sourceMappingURL=tui.js.map