@nexusbloom/cli 0.1.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 +71 -0
- package/package.json +26 -0
- package/src/index.js +559 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @nexusbloom/cli
|
|
2
|
+
|
|
3
|
+
CLI tool for NexusBloom — run tools and workflows from your terminal.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @nexusbloom/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# List available tools
|
|
15
|
+
nxb list
|
|
16
|
+
|
|
17
|
+
# Search tools
|
|
18
|
+
nxb search "ssl"
|
|
19
|
+
|
|
20
|
+
# Run a tool (remote API, requires API key)
|
|
21
|
+
nxb run ssl-certificate-checker domain=example.com --key YOUR_API_KEY
|
|
22
|
+
|
|
23
|
+
# Run with inline JSON input
|
|
24
|
+
nxb run css-gradient-generator --input '{"colors":["#ff6b6b","#4ecdc4"],"angle":135}'
|
|
25
|
+
|
|
26
|
+
# Run with file input
|
|
27
|
+
nxb run css-gradient-generator --file ./gradient-input.json
|
|
28
|
+
|
|
29
|
+
# Run a tool locally (fetches + caches tool source, executes coreLogic on your machine)
|
|
30
|
+
nxb run ssl-certificate-checker domain=example.com --local
|
|
31
|
+
|
|
32
|
+
# Pipe stdin through a tool
|
|
33
|
+
echo '{"domain":"example.com"}' | nxb transform ssl-certificate-checker
|
|
34
|
+
|
|
35
|
+
# Describe what you want
|
|
36
|
+
nxb chat "check if my website example.com is secure"
|
|
37
|
+
|
|
38
|
+
# Show tool schema
|
|
39
|
+
nxb info ssl-certificate-checker
|
|
40
|
+
|
|
41
|
+
# Manage local tool cache
|
|
42
|
+
nxb cache list
|
|
43
|
+
nxb cache clear
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Options
|
|
47
|
+
|
|
48
|
+
| Flag | Description |
|
|
49
|
+
|------|-------------|
|
|
50
|
+
| `--json` | Output results in JSON format |
|
|
51
|
+
| `--key <key>` | API key (can also set via `NEXUSBLOOM_API_KEY` env var) |
|
|
52
|
+
| `--input <json>` | Inline JSON input (e.g. `'{"domain":"example.com"}'`) |
|
|
53
|
+
| `--file <path>` | Read input from a JSON file |
|
|
54
|
+
| `--local` | Execute coreLogic locally (fetches + caches tool source) |
|
|
55
|
+
|
|
56
|
+
## Environment Variables
|
|
57
|
+
|
|
58
|
+
- `NEXUSBLOOM_API_KEY` — Your API key (can also be passed with `--key`)
|
|
59
|
+
- `NEXUSBLOOM_API_URL` — API base URL (default: https://nexusbloom.dev)
|
|
60
|
+
|
|
61
|
+
## Local Execution
|
|
62
|
+
|
|
63
|
+
The `--local` flag fetches the tool's v2 source code from the NexusBloom API,
|
|
64
|
+
caches it locally, and executes the `coreLogic()` function directly on your
|
|
65
|
+
machine. This is useful for:
|
|
66
|
+
|
|
67
|
+
- Offline use (after initial fetch)
|
|
68
|
+
- Faster repeated execution (cached)
|
|
69
|
+
- Privacy-sensitive inputs (data stays on your machine)
|
|
70
|
+
|
|
71
|
+
Use `nxb cache clear` to clear the local cache.
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nexusbloom/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "NexusBloom CLI — run tools and workflows from your terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nxb": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/",
|
|
11
|
+
"package.json",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"commander": "^12.0.0",
|
|
22
|
+
"chalk": "^5.3.0",
|
|
23
|
+
"ora": "^8.0.0",
|
|
24
|
+
"conf": "^12.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NexusBloom CLI (nxb)
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* nxb list — List available tools
|
|
8
|
+
* nxb search <query> — Search tools
|
|
9
|
+
* nxb run <slug> [args...] — Execute a tool (remote API)
|
|
10
|
+
* nxb run <slug> --input '{}' — Execute with inline JSON input
|
|
11
|
+
* nxb run <slug> --file input.json — Execute with file input
|
|
12
|
+
* nxb run <slug> --local — Execute coreLogic locally (cached)
|
|
13
|
+
* nxb chat <message> — Natural language routing
|
|
14
|
+
* nxb info <slug> — Show tool schema
|
|
15
|
+
* nxb transform <slug> — Pipe stdin through a tool
|
|
16
|
+
* nxb config set <key> <value> — Set config
|
|
17
|
+
* nxb config get [key] — Get config
|
|
18
|
+
* nxb cache clear — Clear local tool cache
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { program } from "commander";
|
|
22
|
+
import chalk from "chalk";
|
|
23
|
+
import { createRequire } from "module";
|
|
24
|
+
import Conf from "conf";
|
|
25
|
+
import fs from "fs";
|
|
26
|
+
import path from "path";
|
|
27
|
+
import { fileURLToPath } from "url";
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const { version } = require("../package.json");
|
|
31
|
+
|
|
32
|
+
const API_BASE = process.env.NEXUSBLOOM_API_URL || "https://nexusbloom.dev";
|
|
33
|
+
const config = new Conf({ projectName: "nexusbloom" });
|
|
34
|
+
|
|
35
|
+
// ─── Cache directory for local tool execution ────────────────────────────────
|
|
36
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const CACHE_DIR = path.resolve(__dirname, "../.cache/tools");
|
|
38
|
+
|
|
39
|
+
function ensureCacheDir() {
|
|
40
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
41
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Program ─────────────────────────────────────────────────────────────────
|
|
46
|
+
program
|
|
47
|
+
.name("nxb")
|
|
48
|
+
.description("NexusBloom CLI — run tools and workflows from your terminal")
|
|
49
|
+
.version(version)
|
|
50
|
+
.option("--json", "Output in JSON format");
|
|
51
|
+
|
|
52
|
+
// ─── `nxb list` — list available tools ───────────────────────────────────────
|
|
53
|
+
program
|
|
54
|
+
.command("list")
|
|
55
|
+
.description("List available tools")
|
|
56
|
+
.option("-s, --search <query>", "Search tools by name or description")
|
|
57
|
+
.action(async (opts, cmd) => {
|
|
58
|
+
const jsonMode = cmd.parent.opts().json || opts.json;
|
|
59
|
+
try {
|
|
60
|
+
const url = new URL("/api/v1/tools", API_BASE);
|
|
61
|
+
if (opts.search) url.searchParams.set("search", opts.search);
|
|
62
|
+
const res = await fetch(url);
|
|
63
|
+
const { tools } = await res.json();
|
|
64
|
+
if (!tools || tools.length === 0) {
|
|
65
|
+
if (jsonMode) return console.log(JSON.stringify({ tools: [] }));
|
|
66
|
+
console.log(chalk.yellow("No tools found."));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (jsonMode) return console.log(JSON.stringify({ tools }, null, 2));
|
|
70
|
+
console.log(chalk.bold(`\n ${tools.length} tool${tools.length > 1 ? "s" : ""} available\n`));
|
|
71
|
+
tools.forEach((t) => {
|
|
72
|
+
console.log(` ${chalk.cyan(t.slug.padEnd(30))} ${chalk.dim((t.description || "").slice(0, 60))}`);
|
|
73
|
+
});
|
|
74
|
+
console.log();
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
|
|
77
|
+
console.error(chalk.red("Failed to fetch tools:"), err.message);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ─── `nxb search <query>` — search tools ─────────────────────────────────────
|
|
82
|
+
program
|
|
83
|
+
.command("search")
|
|
84
|
+
.description("Search for tools")
|
|
85
|
+
.argument("<query>", "Search query")
|
|
86
|
+
.action(async (query, cmd) => {
|
|
87
|
+
const jsonMode = cmd.parent.opts().json;
|
|
88
|
+
try {
|
|
89
|
+
const url = new URL("/api/v1/tools", API_BASE);
|
|
90
|
+
url.searchParams.set("search", query);
|
|
91
|
+
const res = await fetch(url);
|
|
92
|
+
const { tools } = await res.json();
|
|
93
|
+
if (!tools || tools.length === 0) {
|
|
94
|
+
if (jsonMode) return console.log(JSON.stringify({ tools: [] }));
|
|
95
|
+
console.log(chalk.yellow(`No tools matching "${query}".`));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (jsonMode) return console.log(JSON.stringify({ tools, query }, null, 2));
|
|
99
|
+
console.log(chalk.bold(`\n ${tools.length} result${tools.length > 1 ? "s" : ""} for "${query}"\n`));
|
|
100
|
+
tools.forEach((t) => {
|
|
101
|
+
console.log(` ${chalk.cyan(t.slug.padEnd(30))} ${chalk.dim((t.description || "").slice(0, 60))}`);
|
|
102
|
+
});
|
|
103
|
+
console.log();
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
|
|
106
|
+
console.error(chalk.red("Search failed:"), err.message);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ─── `nxb run <slug>` — execute a tool ───────────────────────────────────────
|
|
111
|
+
program
|
|
112
|
+
.command("run")
|
|
113
|
+
.description("Execute a tool")
|
|
114
|
+
.argument("<slug>", "Tool slug")
|
|
115
|
+
.argument("[args...]", "Key=value arguments (e.g. domain=example.com)")
|
|
116
|
+
.option("-k, --key <key>", "API key (or set NEXUSBLOOM_API_KEY env var)")
|
|
117
|
+
.option("-i, --input <json>", "Inline JSON input (e.g. '{\"domain\":\"example.com\"}')")
|
|
118
|
+
.option("-f, --file <path>", "Read input from a JSON file")
|
|
119
|
+
.option("--local", "Execute coreLogic locally (fetches tool source, caches it)")
|
|
120
|
+
.action(async (slug, args, opts, cmd) => {
|
|
121
|
+
const jsonMode = cmd.parent.opts().json;
|
|
122
|
+
|
|
123
|
+
// ── Build input body ────────────────────────────────────────────────
|
|
124
|
+
let body = {};
|
|
125
|
+
|
|
126
|
+
// 1. --input flag takes highest priority
|
|
127
|
+
if (opts.input) {
|
|
128
|
+
try {
|
|
129
|
+
body = JSON.parse(opts.input);
|
|
130
|
+
} catch {
|
|
131
|
+
if (jsonMode) return console.log(JSON.stringify({ error: "Invalid JSON in --input flag" }));
|
|
132
|
+
console.error(chalk.red("Error: --input must be valid JSON"));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 2. --file flag reads from a file
|
|
138
|
+
if (opts.file) {
|
|
139
|
+
try {
|
|
140
|
+
const fileContent = fs.readFileSync(path.resolve(opts.file), "utf-8");
|
|
141
|
+
const fileData = JSON.parse(fileContent);
|
|
142
|
+
// Merge file data, with --input taking precedence
|
|
143
|
+
body = { ...fileData, ...body };
|
|
144
|
+
} catch (err) {
|
|
145
|
+
if (jsonMode) return console.log(JSON.stringify({ error: `Failed to read --file: ${err.message}` }));
|
|
146
|
+
console.error(chalk.red(`Error reading file: ${err.message}`));
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 3. Key=value args (lowest priority, merged in)
|
|
152
|
+
if (!opts.input && !opts.file) {
|
|
153
|
+
(args || []).forEach((arg) => {
|
|
154
|
+
const [k, ...v] = arg.split("=");
|
|
155
|
+
body[k] = v.join("=");
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Local execution mode ────────────────────────────────────────────
|
|
160
|
+
if (opts.local) {
|
|
161
|
+
return await executeLocal(slug, body, jsonMode);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Remote API execution ────────────────────────────────────────────
|
|
165
|
+
const apiKey = opts.key || process.env.NEXUSBLOOM_API_KEY || config.get("apiKey");
|
|
166
|
+
if (!apiKey) {
|
|
167
|
+
if (jsonMode) return console.log(JSON.stringify({ error: "API key required. Set NEXUSBLOOM_API_KEY, pass --key, or run `nxb config set key <your-key>`." }));
|
|
168
|
+
console.error(chalk.red("API key required. Set NEXUSBLOOM_API_KEY, pass --key, or run `nxb config set key <your-key>`."));
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const res = await fetch(`${API_BASE}/api/run/${slug}`, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: {
|
|
176
|
+
"Content-Type": "application/json",
|
|
177
|
+
Authorization: `Bearer ${apiKey}`,
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify(body),
|
|
180
|
+
});
|
|
181
|
+
const data = await res.json();
|
|
182
|
+
if (data.success) {
|
|
183
|
+
if (jsonMode) return console.log(JSON.stringify({ success: true, data: data.data }, null, 2));
|
|
184
|
+
console.log(chalk.green("\n Result:"));
|
|
185
|
+
console.log(` ${JSON.stringify(data.data, null, 2)}`);
|
|
186
|
+
} else {
|
|
187
|
+
if (jsonMode) return console.log(JSON.stringify({ success: false, error: data.error || "Unknown error" }, null, 2));
|
|
188
|
+
console.error(chalk.red(`\n Error: ${data.error || "Unknown error"}`));
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
|
|
193
|
+
console.error(chalk.red("Execution failed:"), err.message);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ─── Local execution helper ──────────────────────────────────────────────────
|
|
199
|
+
async function executeLocal(slug, input, jsonMode) {
|
|
200
|
+
ensureCacheDir();
|
|
201
|
+
const cacheFile = path.join(CACHE_DIR, `${slug}.json`);
|
|
202
|
+
|
|
203
|
+
// Try loading from cache first
|
|
204
|
+
let manifest, coreLogicSource;
|
|
205
|
+
if (fs.existsSync(cacheFile)) {
|
|
206
|
+
try {
|
|
207
|
+
const cached = JSON.parse(fs.readFileSync(cacheFile, "utf-8"));
|
|
208
|
+
manifest = cached.manifest;
|
|
209
|
+
coreLogicSource = cached.coreLogicSource;
|
|
210
|
+
if (!jsonMode) console.log(chalk.dim(` Using cached tool: ${slug}`));
|
|
211
|
+
} catch {
|
|
212
|
+
// Cache corrupted, refetch
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Fetch from API if not cached
|
|
217
|
+
if (!manifest || !coreLogicSource) {
|
|
218
|
+
if (!jsonMode) console.log(chalk.dim(` Fetching tool: ${slug}...`));
|
|
219
|
+
try {
|
|
220
|
+
const res = await fetch(`${API_BASE}/api/run/${slug}`);
|
|
221
|
+
if (!res.ok) {
|
|
222
|
+
if (jsonMode) return console.log(JSON.stringify({ error: `Tool "${slug}" not found` }));
|
|
223
|
+
console.error(chalk.red(`Tool "${slug}" not found.`));
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
const data = await res.json();
|
|
227
|
+
manifest = data.manifest || data;
|
|
228
|
+
|
|
229
|
+
// Fetch the v2 source separately
|
|
230
|
+
const sourceRes = await fetch(`${API_BASE}/api/run/${slug}?source=true`);
|
|
231
|
+
if (sourceRes.ok) {
|
|
232
|
+
const sourceData = await sourceRes.json();
|
|
233
|
+
coreLogicSource = sourceData.data?.v2_source;
|
|
234
|
+
} else {
|
|
235
|
+
coreLogicSource = null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Cache it
|
|
239
|
+
if (manifest) {
|
|
240
|
+
fs.writeFileSync(cacheFile, JSON.stringify({ manifest, coreLogicSource, cachedAt: new Date().toISOString() }), "utf-8");
|
|
241
|
+
}
|
|
242
|
+
} catch (err) {
|
|
243
|
+
if (jsonMode) return console.log(JSON.stringify({ error: `Failed to fetch tool: ${err.message}` }));
|
|
244
|
+
console.error(chalk.red(`Failed to fetch tool: ${err.message}`));
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!coreLogicSource) {
|
|
250
|
+
// Fallback: try to extract coreLogic from the manifest itself
|
|
251
|
+
if (jsonMode) return console.log(JSON.stringify({ error: "No v2 source available for local execution. Try without --local to use remote API." }));
|
|
252
|
+
console.error(chalk.yellow("No v2 source available for local execution. Try without --local to use remote API."));
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Execute coreLogic locally
|
|
257
|
+
try {
|
|
258
|
+
// Extract the coreLogic function from the source
|
|
259
|
+
const fnMatch = coreLogicSource.match(/export\s+(async\s+)?function\s+coreLogic[\s\S]*/);
|
|
260
|
+
if (!fnMatch) {
|
|
261
|
+
if (jsonMode) return console.log(JSON.stringify({ error: "Could not extract coreLogic from source" }));
|
|
262
|
+
console.error(chalk.red("Could not extract coreLogic from source"));
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
const fnSource = fnMatch[0].replace(/export\s+/, "");
|
|
266
|
+
const isAsync = fnMatch[0].startsWith("export async");
|
|
267
|
+
|
|
268
|
+
// Validate input against manifest
|
|
269
|
+
const schema = manifest.input_schema || {};
|
|
270
|
+
const required = schema.required || [];
|
|
271
|
+
const missing = required.filter((k) => input[k] === undefined || input[k] === null);
|
|
272
|
+
if (missing.length > 0) {
|
|
273
|
+
if (jsonMode) return console.log(JSON.stringify({ error: `Missing required fields: ${missing.join(", ")}` }));
|
|
274
|
+
console.error(chalk.red(`Missing required fields: ${missing.join(", ")}`));
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Apply defaults
|
|
279
|
+
if (schema.properties) {
|
|
280
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
281
|
+
if (input[key] === undefined && prop.default !== undefined) {
|
|
282
|
+
input[key] = prop.default;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Create and execute the function
|
|
288
|
+
const coreLogic = new Function("return " + fnSource)();
|
|
289
|
+
const timeoutMs = manifest.timeout_ms || 5000;
|
|
290
|
+
let result;
|
|
291
|
+
|
|
292
|
+
if (isAsync) {
|
|
293
|
+
result = await Promise.race([
|
|
294
|
+
coreLogic(input),
|
|
295
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs)),
|
|
296
|
+
]);
|
|
297
|
+
} else {
|
|
298
|
+
result = coreLogic(input);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (jsonMode) return console.log(JSON.stringify({ success: true, data: result }, null, 2));
|
|
302
|
+
console.log(chalk.green("\n Result (local):"));
|
|
303
|
+
console.log(` ${JSON.stringify(result, null, 2)}`);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
|
|
306
|
+
console.error(chalk.red(`\n Local execution error: ${err.message}`));
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ─── `nxb chat <message>` — describe what you want ───────────────────────────
|
|
312
|
+
program
|
|
313
|
+
.command("chat")
|
|
314
|
+
.description("Describe what you want and let NexusBloom route to the right tools")
|
|
315
|
+
.argument("<message>", "What you want to do")
|
|
316
|
+
.action(async (message, cmd) => {
|
|
317
|
+
const jsonMode = cmd.parent.opts().json;
|
|
318
|
+
try {
|
|
319
|
+
if (!jsonMode) console.log(chalk.dim(" Routing your request..."));
|
|
320
|
+
const res = await fetch(`${API_BASE}/api/v1/route`, {
|
|
321
|
+
method: "POST",
|
|
322
|
+
headers: { "Content-Type": "application/json" },
|
|
323
|
+
body: JSON.stringify({ task: message }),
|
|
324
|
+
});
|
|
325
|
+
const data = await res.json();
|
|
326
|
+
if (jsonMode) return console.log(JSON.stringify(data, null, 2));
|
|
327
|
+
if (data.matched_tools?.length > 0) {
|
|
328
|
+
console.log(chalk.bold(`\n Matched ${data.matched_tools.length} tool${data.matched_tools.length > 1 ? "s" : ""}:\n`));
|
|
329
|
+
data.matched_tools.forEach((t) => {
|
|
330
|
+
console.log(` ${chalk.cyan(t.slug.padEnd(30))} ${chalk.dim(`${Math.round(t.confidence * 100)}% match`)}`);
|
|
331
|
+
});
|
|
332
|
+
console.log();
|
|
333
|
+
if (data.results) {
|
|
334
|
+
console.log(chalk.bold(" Results:\n"));
|
|
335
|
+
data.results.forEach((r) => {
|
|
336
|
+
console.log(` ${chalk.cyan(r.tool)}:`);
|
|
337
|
+
console.log(` ${JSON.stringify(r.data, null, 4)}`);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
console.log(chalk.yellow(" No matching tools found."));
|
|
342
|
+
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
|
|
345
|
+
console.error(chalk.red("Chat failed:"), err.message);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// ─── `nxb info <slug>` — show tool schema ────────────────────────────────────
|
|
350
|
+
program
|
|
351
|
+
.command("info")
|
|
352
|
+
.description("Show tool schema and details")
|
|
353
|
+
.argument("<slug>", "Tool slug")
|
|
354
|
+
.action(async (slug, cmd) => {
|
|
355
|
+
const jsonMode = cmd.parent.opts().json;
|
|
356
|
+
try {
|
|
357
|
+
const res = await fetch(`${API_BASE}/api/run/${slug}`);
|
|
358
|
+
if (!res.ok) {
|
|
359
|
+
if (jsonMode) return console.log(JSON.stringify({ error: `Tool "${slug}" not found` }));
|
|
360
|
+
console.error(chalk.red(`Tool "${slug}" not found.`));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const data = await res.json();
|
|
364
|
+
const manifest = data.manifest || data;
|
|
365
|
+
if (jsonMode) return console.log(JSON.stringify(manifest, null, 2));
|
|
366
|
+
console.log(chalk.bold(`\n ${manifest.name}`));
|
|
367
|
+
console.log(` ${chalk.dim(manifest.short_description || "")}`);
|
|
368
|
+
console.log(` Version: ${chalk.cyan(manifest.version || "1.0.0")}`);
|
|
369
|
+
console.log(` Runtime: ${chalk.cyan(manifest.runtime || "browser")}`);
|
|
370
|
+
console.log(` Pricing: ${chalk.cyan(manifest.pricing || "free")}`);
|
|
371
|
+
if (manifest.input_schema?.properties) {
|
|
372
|
+
console.log(chalk.bold(`\n Input Parameters:`));
|
|
373
|
+
const props = manifest.input_schema.properties;
|
|
374
|
+
const required = manifest.input_schema.required || [];
|
|
375
|
+
for (const [name, prop] of Object.entries(props)) {
|
|
376
|
+
const req = required.includes(name) ? chalk.red(" *required") : "";
|
|
377
|
+
console.log(` ${chalk.cyan(name)} (${prop.type})${req}`);
|
|
378
|
+
if (prop.description) console.log(` ${chalk.dim(prop.description)}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (manifest.output_schema?.properties) {
|
|
382
|
+
console.log(chalk.bold(`\n Output:`));
|
|
383
|
+
for (const [name, prop] of Object.entries(manifest.output_schema.properties)) {
|
|
384
|
+
console.log(` ${chalk.cyan(name)} (${prop.type})`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
console.log();
|
|
388
|
+
} catch (err) {
|
|
389
|
+
if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
|
|
390
|
+
console.error(chalk.red("Failed to fetch tool info:"), err.message);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// ─── `nxb transform <slug>` — pipe stdin through a tool ──────────────────────
|
|
395
|
+
program
|
|
396
|
+
.command("transform")
|
|
397
|
+
.description("Pipe stdin through a tool and write result to stdout")
|
|
398
|
+
.argument("<slug>", "Tool slug")
|
|
399
|
+
.option("-k, --key <key>", "API key (or set NEXUSBLOOM_API_KEY env var)")
|
|
400
|
+
.option("--local", "Execute locally (faster for repeated use)")
|
|
401
|
+
.action(async (slug, opts, cmd) => {
|
|
402
|
+
const jsonMode = cmd.parent.opts().json;
|
|
403
|
+
const apiKey = opts.key || process.env.NEXUSBLOOM_API_KEY || config.get("apiKey");
|
|
404
|
+
|
|
405
|
+
const input = await readStdin();
|
|
406
|
+
if (!input) {
|
|
407
|
+
if (jsonMode) return console.log(JSON.stringify({ error: "No input received from stdin" }));
|
|
408
|
+
console.error(chalk.red("No input received from stdin. Pipe data into this command."));
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let body;
|
|
413
|
+
try {
|
|
414
|
+
body = JSON.parse(input);
|
|
415
|
+
} catch {
|
|
416
|
+
body = { input };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (opts.local) {
|
|
420
|
+
return await executeLocal(slug, body, jsonMode);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!apiKey) {
|
|
424
|
+
if (jsonMode) return console.log(JSON.stringify({ error: "API key required" }));
|
|
425
|
+
console.error(chalk.red("API key required. Set NEXUSBLOOM_API_KEY, pass --key, or use --local mode."));
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
const res = await fetch(`${API_BASE}/api/run/${slug}`, {
|
|
431
|
+
method: "POST",
|
|
432
|
+
headers: {
|
|
433
|
+
"Content-Type": "application/json",
|
|
434
|
+
Authorization: `Bearer ${apiKey}`,
|
|
435
|
+
},
|
|
436
|
+
body: JSON.stringify(body),
|
|
437
|
+
});
|
|
438
|
+
const data = await res.json();
|
|
439
|
+
if (data.success) {
|
|
440
|
+
const output = typeof data.data === "string" ? data.data : JSON.stringify(data.data, null, 2);
|
|
441
|
+
if (jsonMode) return console.log(JSON.stringify({ success: true, data: data.data }, null, 2));
|
|
442
|
+
process.stdout.write(output + "\n");
|
|
443
|
+
} else {
|
|
444
|
+
if (jsonMode) return console.log(JSON.stringify({ success: false, error: data.error || "Unknown error" }, null, 2));
|
|
445
|
+
console.error(chalk.red(`\n Error: ${data.error || "Unknown error"}`));
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
} catch (err) {
|
|
449
|
+
if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
|
|
450
|
+
console.error(chalk.red("Transform failed:"), err.message);
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// ─── `nxb config` — manage configuration ─────────────────────────────────────
|
|
456
|
+
const configCmd = program
|
|
457
|
+
.command("config")
|
|
458
|
+
.description("Manage CLI configuration");
|
|
459
|
+
|
|
460
|
+
configCmd
|
|
461
|
+
.command("set")
|
|
462
|
+
.description("Set a config value")
|
|
463
|
+
.argument("<key>", "Config key (key, default-tool)")
|
|
464
|
+
.argument("<value>", "Config value")
|
|
465
|
+
.action((key, value) => {
|
|
466
|
+
config.set(key, value);
|
|
467
|
+
console.log(chalk.green(` Set ${key} to "${value}"`));
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
configCmd
|
|
471
|
+
.command("get")
|
|
472
|
+
.description("Get a config value")
|
|
473
|
+
.argument("[key]", "Config key (omit to show all)")
|
|
474
|
+
.action((key) => {
|
|
475
|
+
if (key) {
|
|
476
|
+
const value = config.get(key);
|
|
477
|
+
if (value === undefined) {
|
|
478
|
+
console.log(chalk.yellow(` ${key} is not set`));
|
|
479
|
+
} else {
|
|
480
|
+
console.log(` ${key}: ${value}`);
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
const store = config.store;
|
|
484
|
+
if (Object.keys(store).length === 0) {
|
|
485
|
+
console.log(chalk.yellow(" No config values set."));
|
|
486
|
+
} else {
|
|
487
|
+
Object.entries(store).forEach(([k, v]) => {
|
|
488
|
+
console.log(` ${k}: ${v}`);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
configCmd
|
|
495
|
+
.command("list")
|
|
496
|
+
.description("List all config values (alias for config get)")
|
|
497
|
+
.action(() => {
|
|
498
|
+
const store = config.store;
|
|
499
|
+
if (Object.keys(store).length === 0) {
|
|
500
|
+
console.log(chalk.yellow(" No config values set."));
|
|
501
|
+
} else {
|
|
502
|
+
Object.entries(store).forEach(([k, v]) => {
|
|
503
|
+
console.log(` ${k}: ${v}`);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// ─── `nxb cache` — manage local tool cache ─────────────────────────────────
|
|
509
|
+
const cacheCmd = program
|
|
510
|
+
.command("cache")
|
|
511
|
+
.description("Manage local tool cache");
|
|
512
|
+
|
|
513
|
+
cacheCmd
|
|
514
|
+
.command("clear")
|
|
515
|
+
.description("Clear the local tool cache")
|
|
516
|
+
.action(() => {
|
|
517
|
+
ensureCacheDir();
|
|
518
|
+
const files = fs.readdirSync(CACHE_DIR);
|
|
519
|
+
let count = 0;
|
|
520
|
+
for (const file of files) {
|
|
521
|
+
fs.unlinkSync(path.join(CACHE_DIR, file));
|
|
522
|
+
count++;
|
|
523
|
+
}
|
|
524
|
+
console.log(chalk.green(` Cleared ${count} cached tool${count !== 1 ? "s" : ""}.`));
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
cacheCmd
|
|
528
|
+
.command("list")
|
|
529
|
+
.description("List cached tools")
|
|
530
|
+
.action(() => {
|
|
531
|
+
ensureCacheDir();
|
|
532
|
+
const files = fs.readdirSync(CACHE_DIR);
|
|
533
|
+
if (files.length === 0) {
|
|
534
|
+
console.log(chalk.yellow(" No cached tools."));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
console.log(chalk.bold(`\n ${files.length} cached tool${files.length > 1 ? "s" : ""}:\n`));
|
|
538
|
+
files.forEach((f) => {
|
|
539
|
+
const slug = f.replace(/\.json$/, "");
|
|
540
|
+
const stats = fs.statSync(path.join(CACHE_DIR, f));
|
|
541
|
+
const age = Math.round((Date.now() - stats.mtimeMs) / 1000 / 60);
|
|
542
|
+
console.log(` ${chalk.cyan(slug.padEnd(30))} ${chalk.dim(`${age}m ago`)}`);
|
|
543
|
+
});
|
|
544
|
+
console.log();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
program.parse(process.argv);
|
|
548
|
+
|
|
549
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
550
|
+
|
|
551
|
+
function readStdin() {
|
|
552
|
+
return new Promise((resolve) => {
|
|
553
|
+
const chunks = [];
|
|
554
|
+
process.stdin.setEncoding("utf8");
|
|
555
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
556
|
+
process.stdin.on("end", () => resolve(chunks.join("")));
|
|
557
|
+
if (process.stdin.isTTY) resolve("");
|
|
558
|
+
});
|
|
559
|
+
}
|