imperium-crawl 2.5.2 → 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.
- package/README.md +18 -16
- package/dist/batch/job-store.js +1 -1
- package/dist/batch/job-store.js.map +1 -1
- package/dist/brave-api/index.js +1 -1
- package/dist/brave-api/index.js.map +1 -1
- package/dist/cli/config.d.ts +21 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +51 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/engine.d.ts +17 -0
- package/dist/cli/engine.d.ts.map +1 -0
- package/dist/cli/engine.js +440 -0
- package/dist/cli/engine.js.map +1 -0
- package/dist/cli/explore.d.ts +30 -0
- package/dist/cli/explore.d.ts.map +1 -0
- package/dist/cli/explore.js +427 -0
- package/dist/cli/explore.js.map +1 -0
- package/dist/cli/onboarding.d.ts +10 -0
- package/dist/cli/onboarding.d.ts.map +1 -0
- package/dist/cli/onboarding.js +128 -0
- package/dist/cli/onboarding.js.map +1 -0
- package/dist/cli/recorder.d.ts +44 -0
- package/dist/cli/recorder.d.ts.map +1 -0
- package/dist/cli/recorder.js +67 -0
- package/dist/cli/recorder.js.map +1 -0
- package/dist/cli/tui.d.ts +12 -0
- package/dist/cli/tui.d.ts.map +1 -0
- package/dist/cli/tui.js +945 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/cli/ui.d.ts +26 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +58 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/core/action-executor.d.ts +66 -0
- package/dist/core/action-executor.d.ts.map +1 -0
- package/dist/core/action-executor.js +403 -0
- package/dist/core/action-executor.js.map +1 -0
- package/dist/core/config.d.ts +16 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +56 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/constants.d.ts +40 -0
- package/dist/core/constants.d.ts.map +1 -0
- package/dist/core/constants.js +86 -0
- package/dist/core/constants.js.map +1 -0
- package/dist/core/formatters.d.ts +36 -0
- package/dist/core/formatters.d.ts.map +1 -0
- package/dist/core/formatters.js +147 -0
- package/dist/core/formatters.js.map +1 -0
- package/dist/engines/camofox.d.ts +27 -0
- package/dist/engines/camofox.d.ts.map +1 -0
- package/dist/engines/camofox.js +432 -0
- package/dist/engines/camofox.js.map +1 -0
- package/dist/engines/index.d.ts +13 -0
- package/dist/engines/index.d.ts.map +1 -0
- package/dist/engines/index.js +41 -0
- package/dist/engines/index.js.map +1 -0
- package/dist/engines/types.d.ts +63 -0
- package/dist/engines/types.d.ts.map +1 -0
- package/dist/engines/types.js +8 -0
- package/dist/engines/types.js.map +1 -0
- package/dist/flows/engine.js +3 -3
- package/dist/flows/engine.js.map +1 -1
- package/dist/flows/storage.js +1 -1
- package/dist/flows/storage.js.map +1 -1
- package/dist/flows/templates.js +1 -1
- package/dist/flows/templates.js.map +1 -1
- package/dist/flows/types.d.ts +405 -405
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/knowledge/store.js +1 -1
- package/dist/knowledge/store.js.map +1 -1
- package/dist/network/index.d.ts +3 -0
- package/dist/network/index.d.ts.map +1 -0
- package/dist/network/index.js +2 -0
- package/dist/network/index.js.map +1 -0
- package/dist/recipes/data/crypto-websocket.json +11 -0
- package/dist/recipes/data/ecommerce-product.json +25 -0
- package/dist/recipes/data/github-trending.json +19 -0
- package/dist/recipes/data/hn-top-stories.json +22 -0
- package/dist/recipes/data/influencer-competitor-spy.json +14 -0
- package/dist/recipes/data/influencer-content-scout.json +14 -0
- package/dist/recipes/data/influencer-hashtag-scout.json +14 -0
- package/dist/recipes/data/influencer-niche-discovery.json +14 -0
- package/dist/recipes/data/job-listings-greenhouse.json +17 -0
- package/dist/recipes/data/news-article-reader.json +9 -0
- package/dist/recipes/data/product-reviews.json +33 -0
- package/dist/recipes/data/reddit-posts.json +8 -0
- package/dist/recipes/data/seo-page-audit.json +26 -0
- package/dist/recipes/data/social-media-mentions.json +31 -0
- package/dist/recipes/index.d.ts +1 -1
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +14 -14
- package/dist/recipes/index.js.map +1 -1
- package/dist/security/auth-vault.js +1 -1
- package/dist/security/auth-vault.js.map +1 -1
- package/dist/security/index.d.ts +6 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +4 -0
- package/dist/security/index.js.map +1 -0
- package/dist/sessions/browser-state.js +1 -1
- package/dist/sessions/browser-state.js.map +1 -1
- package/dist/sessions/encryption.js +1 -1
- package/dist/sessions/encryption.js.map +1 -1
- package/dist/sessions/manager.js +1 -1
- package/dist/sessions/manager.js.map +1 -1
- package/dist/skills/detector.js +1 -1
- package/dist/skills/detector.js.map +1 -1
- package/dist/skills/index.d.ts +9 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +6 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/manager.js +1 -1
- package/dist/skills/manager.js.map +1 -1
- package/dist/snapshot/store.js +1 -1
- package/dist/snapshot/store.js.map +1 -1
- package/dist/social/index.d.ts +7 -0
- package/dist/social/index.d.ts.map +1 -0
- package/dist/social/index.js +4 -0
- package/dist/social/index.js.map +1 -0
- package/dist/stealth/browser-pool.js +1 -1
- package/dist/stealth/browser-pool.js.map +1 -1
- package/dist/stealth/browser.js +2 -2
- package/dist/stealth/browser.js.map +1 -1
- package/dist/stealth/chrome-profile.js +2 -2
- package/dist/stealth/chrome-profile.js.map +1 -1
- package/dist/stealth/index.js +2 -2
- package/dist/stealth/index.js.map +1 -1
- package/dist/tools/ai-extract.js +1 -1
- package/dist/tools/ai-extract.js.map +1 -1
- package/dist/tools/batch-download.d.ts +1 -1
- package/dist/tools/batch-download.js +1 -1
- package/dist/tools/batch-download.js.map +1 -1
- package/dist/tools/batch-scrape.d.ts +2 -2
- package/dist/tools/batch-scrape.js +1 -1
- package/dist/tools/batch-scrape.js.map +1 -1
- package/dist/tools/browser.d.ts +8 -8
- package/dist/tools/browser.js +4 -4
- package/dist/tools/browser.js.map +1 -1
- package/dist/tools/camofox-status.d.ts +14 -0
- package/dist/tools/camofox-status.d.ts.map +1 -0
- package/dist/tools/camofox-status.js +61 -0
- package/dist/tools/camofox-status.js.map +1 -0
- package/dist/tools/camofox-update.d.ts +29 -0
- package/dist/tools/camofox-update.d.ts.map +1 -0
- package/dist/tools/camofox-update.js +108 -0
- package/dist/tools/camofox-update.js.map +1 -0
- package/dist/tools/crawl.d.ts +2 -2
- package/dist/tools/crawl.js +1 -1
- package/dist/tools/crawl.js.map +1 -1
- package/dist/tools/create-skill.js +3 -3
- package/dist/tools/create-skill.js.map +1 -1
- package/dist/tools/discover-apis.d.ts +1 -1
- package/dist/tools/discover-apis.js +1 -1
- package/dist/tools/discover-apis.js.map +1 -1
- package/dist/tools/download.d.ts +7 -7
- package/dist/tools/download.js +1 -1
- package/dist/tools/download.js.map +1 -1
- package/dist/tools/extract.d.ts +1 -1
- package/dist/tools/extract.js +1 -1
- package/dist/tools/extract.js.map +1 -1
- package/dist/tools/image-search.d.ts +1 -1
- package/dist/tools/image-search.js +2 -2
- package/dist/tools/image-search.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/instagram.d.ts +1 -1
- package/dist/tools/instagram.js +3 -3
- package/dist/tools/instagram.js.map +1 -1
- package/dist/tools/interact.d.ts +86 -86
- package/dist/tools/interact.js +5 -5
- package/dist/tools/interact.js.map +1 -1
- package/dist/tools/knowledge.js +1 -1
- package/dist/tools/knowledge.js.map +1 -1
- package/dist/tools/list-skills.js +1 -1
- package/dist/tools/list-skills.js.map +1 -1
- package/dist/tools/manifest.d.ts.map +1 -1
- package/dist/tools/manifest.js +9 -0
- package/dist/tools/manifest.js.map +1 -1
- package/dist/tools/map.js +1 -1
- package/dist/tools/map.js.map +1 -1
- package/dist/tools/monitor-websocket.d.ts +1 -1
- package/dist/tools/monitor-websocket.js +1 -1
- package/dist/tools/monitor-websocket.js.map +1 -1
- package/dist/tools/monitor.d.ts +2 -2
- package/dist/tools/news-search.d.ts +1 -1
- package/dist/tools/news-search.js +2 -2
- package/dist/tools/news-search.js.map +1 -1
- package/dist/tools/query-api.d.ts +5 -5
- package/dist/tools/query-api.js +1 -1
- package/dist/tools/query-api.js.map +1 -1
- package/dist/tools/readability.js +1 -1
- package/dist/tools/readability.js.map +1 -1
- package/dist/tools/record-flow.d.ts +2 -2
- package/dist/tools/record-flow.js +3 -3
- package/dist/tools/record-flow.js.map +1 -1
- package/dist/tools/reddit.d.ts +2 -2
- package/dist/tools/reddit.js +3 -3
- package/dist/tools/reddit.js.map +1 -1
- package/dist/tools/rss.js +1 -1
- package/dist/tools/rss.js.map +1 -1
- package/dist/tools/run-flow.d.ts +6 -6
- package/dist/tools/run-skill.d.ts +6 -6
- package/dist/tools/run-skill.js +2 -2
- package/dist/tools/run-skill.js.map +1 -1
- package/dist/tools/scrape.d.ts +4 -4
- package/dist/tools/scrape.js +1 -1
- package/dist/tools/scrape.js.map +1 -1
- package/dist/tools/screenshot.js +1 -1
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/search.d.ts +1 -1
- package/dist/tools/search.js +2 -2
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/snapshot.d.ts +5 -5
- package/dist/tools/snapshot.js +2 -2
- package/dist/tools/snapshot.js.map +1 -1
- package/dist/tools/video-search.d.ts +1 -1
- package/dist/tools/video-search.js +2 -2
- package/dist/tools/video-search.js.map +1 -1
- package/dist/tools/watch.d.ts +2 -2
- package/dist/tools/watch.js +1 -1
- package/dist/tools/watch.js.map +1 -1
- package/dist/tools/youtube.d.ts +1 -1
- package/dist/tools/youtube.js +4 -4
- package/dist/tools/youtube.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/fetcher.js +1 -1
- package/dist/utils/fetcher.js.map +1 -1
- package/dist/utils/robots.js +1 -1
- package/dist/utils/robots.js.map +1 -1
- package/package.json +7 -3
package/dist/cli/tui.js
ADDED
|
@@ -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
|