@nexusbloom/cli 0.2.4 → 0.2.6

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +317 -171
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexusbloom/cli",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "NexusBloom CLI — run tools and workflows from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -101,26 +101,29 @@ program
101
101
  .option("-s, --search <query>", "Search tools by name or description")
102
102
  .action(async (opts, cmd) => {
103
103
  const jsonMode = cmd.parent.opts().json || opts.json;
104
- try {
105
- const url = new URL("/api/tools", API_BASE);
106
- if (opts.search) url.searchParams.set("search", opts.search);
107
- const res = await fetch(url);
108
- const { tools } = await res.json();
109
- if (!tools || tools.length === 0) {
110
- if (jsonMode) return console.log(JSON.stringify({ tools: [] }));
111
- console.log(chalk.yellow("No tools found."));
112
- return;
113
- }
114
- if (jsonMode) return console.log(JSON.stringify({ tools }, null, 2));
115
- console.log(chalk.bold(`\n ${tools.length} tool${tools.length > 1 ? "s" : ""} available\n`));
116
- tools.forEach((t) => {
117
- console.log(` ${chalk.cyan(t.slug.padEnd(30))} ${chalk.dim((t.description || "").slice(0, 60))}`);
118
- });
119
- console.log();
120
- } catch (err) {
121
- if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
122
- console.error(chalk.red("Failed to fetch tools:"), err.message);
104
+ const tools = await fetchToolsWithFallback();
105
+ if (!tools || tools.length === 0) {
106
+ if (jsonMode) return console.log(JSON.stringify({ tools: [] }));
107
+ console.log(chalk.yellow("No tools found. Set NEXUSBLOOM_SUPABASE_ANON_KEY or deploy the API."));
108
+ return;
123
109
  }
110
+ let filtered = tools;
111
+ if (opts.search) {
112
+ const q = opts.search.toLowerCase();
113
+ filtered = tools.filter(t =>
114
+ (t.slug || "").includes(q) ||
115
+ (t.name || "").toLowerCase().includes(q) ||
116
+ (t.description || "").toLowerCase().includes(q) ||
117
+ (t.category || "").includes(q)
118
+ );
119
+ }
120
+ if (jsonMode) return console.log(JSON.stringify({ tools: filtered }, null, 2));
121
+ console.log(chalk.bold(`\n ${filtered.length} tool${filtered.length > 1 ? "s" : ""} available\n`));
122
+ filtered.forEach((t) => {
123
+ const typeBadge = t.type === "community" ? chalk.dim("[community]") : "";
124
+ console.log(` ${chalk.cyan((t.slug || "").padEnd(30))} ${chalk.dim((t.description || "").slice(0, 50))} ${typeBadge}`);
125
+ });
126
+ console.log();
124
127
  });
125
128
 
126
129
  // ─── `nxb search <query>` — search tools ─────────────────────────────────────
@@ -130,26 +133,30 @@ program
130
133
  .argument("<query>", "Search query")
131
134
  .action(async (query, opts, cmd) => {
132
135
  const jsonMode = (cmd?.parent?.opts().json) || false;
133
- try {
134
- const url = new URL("/api/tools", API_BASE);
135
- url.searchParams.set("search", query);
136
- const res = await fetch(url);
137
- const { tools } = await res.json();
138
- if (!tools || tools.length === 0) {
139
- if (jsonMode) return console.log(JSON.stringify({ tools: [] }));
140
- console.log(chalk.yellow(`No tools matching "${query}".`));
141
- return;
142
- }
143
- if (jsonMode) return console.log(JSON.stringify({ tools, query }, null, 2));
144
- console.log(chalk.bold(`\n ${tools.length} result${tools.length > 1 ? "s" : ""} for "${query}"\n`));
145
- tools.forEach((t) => {
146
- console.log(` ${chalk.cyan(t.slug.padEnd(30))} ${chalk.dim((t.description || "").slice(0, 60))}`);
147
- });
148
- console.log();
149
- } catch (err) {
150
- if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
151
- console.error(chalk.red("Search failed:"), err.message);
136
+ const tools = await fetchToolsWithFallback();
137
+ if (!tools || tools.length === 0) {
138
+ if (jsonMode) return console.log(JSON.stringify({ tools: [], query }));
139
+ console.log(chalk.yellow(`No tools found matching "${query}".`));
140
+ return;
152
141
  }
142
+ const q = query.toLowerCase();
143
+ const filtered = tools.filter(t =>
144
+ (t.slug || "").includes(q) ||
145
+ (t.name || "").toLowerCase().includes(q) ||
146
+ (t.description || "").toLowerCase().includes(q) ||
147
+ (t.category || "").includes(q)
148
+ );
149
+ if (filtered.length === 0) {
150
+ if (jsonMode) return console.log(JSON.stringify({ tools: [], query }));
151
+ console.log(chalk.yellow(`No tools matching "${query}".`));
152
+ return;
153
+ }
154
+ if (jsonMode) return console.log(JSON.stringify({ tools: filtered, query }, null, 2));
155
+ console.log(chalk.bold(`\n ${filtered.length} result${filtered.length > 1 ? "s" : ""} for "${query}"\n`));
156
+ filtered.forEach((t) => {
157
+ console.log(` ${chalk.cyan((t.slug || "").padEnd(30))} ${chalk.dim((t.description || "").slice(0, 60))}`);
158
+ });
159
+ console.log();
153
160
  });
154
161
 
155
162
  // ─── `nxb run <slug>` — execute a tool ───────────────────────────────────────
@@ -166,17 +173,12 @@ program
166
173
  const jsonMode = cmd.parent.opts().json;
167
174
 
168
175
  // ── Resolve abbreviation ────────────────────────────────────────────
169
- try {
170
- const res = await fetch(`${API_BASE}/api/tools`);
171
- const { tools } = await res.json();
172
- const slugs = (tools || []).map((t) => t.slug);
173
- const abbrMap = generateAbbreviations(slugs);
174
- if (abbrMap[slug] && slugs.includes(abbrMap[slug])) {
175
- if (!jsonMode) console.log(chalk.dim(` Resolved: ${slug} → ${abbrMap[slug]}`));
176
- slug = abbrMap[slug];
177
- }
178
- } catch {
179
- // Silently ignore abbreviation resolution failures
176
+ const allTools = await fetchToolsWithFallback();
177
+ const slugs = (allTools || []).map((t) => t.slug);
178
+ const abbrMap = generateAbbreviations(slugs);
179
+ if (abbrMap[slug] && slugs.includes(abbrMap[slug])) {
180
+ if (!jsonMode) console.log(chalk.dim(` Resolved: ${slug} → ${abbrMap[slug]}`));
181
+ slug = abbrMap[slug];
180
182
  }
181
183
 
182
184
  // ── Build input body ────────────────────────────────────────────────
@@ -238,9 +240,8 @@ program
238
240
  // ── Remote API execution ────────────────────────────────────────────
239
241
  const apiKey = opts.key || process.env.NEXUSBLOOM_API_KEY || configGet("apiKey") || configGet("key");
240
242
  if (!apiKey) {
241
- 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>`." }));
242
- console.error(chalk.red("API key required. Set NEXUSBLOOM_API_KEY, pass --key, or run `nxb config set key <your-key>`."));
243
- process.exit(1);
243
+ // No API key — try local mode automatically
244
+ return await executeLocal(slug, body, jsonMode);
244
245
  }
245
246
 
246
247
  try {
@@ -254,11 +255,9 @@ program
254
255
  });
255
256
 
256
257
  if (!res.ok && !res.headers.get("content-type")?.includes("json")) {
257
- const text = await res.text();
258
- if (jsonMode) return console.log(JSON.stringify({ error: `API returned ${res.status}: ${text.slice(0, 200)}` }));
259
- console.error(chalk.red(`API returned ${res.status} (non-JSON response). The endpoint may not be deployed.`));
260
- console.error(chalk.dim(` Raw response: ${text.slice(0, 200)}`));
261
- process.exit(1);
258
+ // API returned non-JSON (likely not deployed) — fall back to local
259
+ if (!jsonMode) console.log(chalk.dim(` API not deployed, falling back to local execution...`));
260
+ return await executeLocal(slug, body, jsonMode);
262
261
  }
263
262
 
264
263
  const data = await res.json();
@@ -276,9 +275,9 @@ program
276
275
  process.exit(1);
277
276
  }
278
277
  } catch (err) {
279
- if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
280
- console.error(chalk.red("Execution failed:"), err.message);
281
- process.exit(1);
278
+ // Network error — fall back to local
279
+ if (!jsonMode) console.log(chalk.dim(` API unreachable, falling back to local execution...`));
280
+ return await executeLocal(slug, body, jsonMode);
282
281
  }
283
282
  });
284
283
 
@@ -323,39 +322,14 @@ async function executeLocal(slug, input, jsonMode) {
323
322
  // API failed, try Supabase fallback
324
323
  }
325
324
 
326
- // Fallback: query Supabase directly for v2_source
325
+ // Fallback: query Supabase directly via shared helper
327
326
  if (!apiSuccess) {
328
327
  try {
329
- const supabaseUrl = process.env.NEXUSBLOOM_SUPABASE_URL || "https://dhxaqatmuprehrkszxzs.supabase.co";
330
- const supabaseKey = process.env.NEXUSBLOOM_SUPABASE_ANON_KEY;
331
- if (supabaseKey) {
332
- // Get tool from tools table
333
- const toolRes = await fetch(`${supabaseUrl}/rest/v1/tools?slug=eq.${slug}&select=id`, {
334
- headers: { apikey: supabaseKey, Authorization: `Bearer ${supabaseKey}` }
335
- });
336
- const tools = await toolRes.json();
337
- const toolId = tools?.[0]?.id;
338
- if (toolId) {
339
- // Get v2_source from tool_versions
340
- const versionRes = await fetch(
341
- `${supabaseUrl}/rest/v1/tool_versions?tool_id=eq.${toolId}&tool_type=eq.builtin&status=eq.published&select=v2_source&order=created_at.desc&limit=1`,
342
- { headers: { apikey: supabaseKey, Authorization: `Bearer ${supabaseKey}` } }
343
- );
344
- const versions = await versionRes.json();
345
- const v2Source = versions?.[0]?.v2_source;
346
- if (v2Source) {
347
- coreLogicSource = v2Source;
348
- const manifestMatch = v2Source.match(/export\s+const\s+MANIFEST\s*=\s*({[\s\S]*?});\s*\n/);
349
- if (manifestMatch) {
350
- try {
351
- manifest = JSON.parse(manifestMatch[1].replace(/(\w+):/g, '"$1":').replace(/'/g, '"'));
352
- } catch {
353
- manifest = eval("(" + manifestMatch[1] + ")");
354
- }
355
- }
356
- apiSuccess = true;
357
- }
358
- }
328
+ const toolInfo = await fetchToolFromSupabase(slug);
329
+ if (toolInfo) {
330
+ manifest = toolInfo.manifest;
331
+ coreLogicSource = toolInfo.coreLogicSource;
332
+ apiSuccess = true;
359
333
  }
360
334
  } catch {
361
335
  // Supabase fallback also failed
@@ -363,8 +337,8 @@ async function executeLocal(slug, input, jsonMode) {
363
337
  }
364
338
 
365
339
  if (!apiSuccess) {
366
- if (jsonMode) return console.log(JSON.stringify({ error: `Tool "${slug}" not found. Ensure API is deployed or set NEXUSBLOOM_SUPABASE_ANON_KEY for local mode.` }));
367
- console.error(chalk.red(`Tool "${slug}" not found. Ensure API is deployed or set NEXUSBLOOM_SUPABASE_ANON_KEY for local mode.`));
340
+ if (jsonMode) return console.log(JSON.stringify({ error: `Tool "${slug}" not found. Set NEXUSBLOOM_SUPABASE_ANON_KEY or run \`nxb config set supabaseKey <your-anon-key>\` for offline mode.` }));
341
+ console.error(chalk.red(`Tool "${slug}" not found. Set NEXUSBLOOM_SUPABASE_ANON_KEY or run \`nxb config set supabaseKey <your-anon-key>\` for offline mode.`));
368
342
  process.exit(1);
369
343
  }
370
344
 
@@ -376,22 +350,21 @@ async function executeLocal(slug, input, jsonMode) {
376
350
 
377
351
  if (!coreLogicSource) {
378
352
  // Fallback: try to extract coreLogic from the manifest itself
379
- if (jsonMode) return console.log(JSON.stringify({ error: "No v2 source available for local execution. Try without --local to use remote API." }));
380
- console.error(chalk.yellow("No v2 source available for local execution. Try without --local to use remote API."));
353
+ if (jsonMode) return console.log(JSON.stringify({ error: "No v2 source available for local execution. Set NEXUSBLOOM_SUPABASE_ANON_KEY or run `nxb config set supabaseKey <your-key>`." }));
354
+ console.error(chalk.yellow("No v2 source available for local execution. Set NEXUSBLOOM_SUPABASE_ANON_KEY or run `nxb config set supabaseKey <your-key>`."));
381
355
  process.exit(1);
382
356
  }
383
357
 
384
358
  // Execute coreLogic locally
385
359
  try {
386
- // Extract the coreLogic function from the source
387
- const fnMatch = coreLogicSource.match(/export\s+(async\s+)?function\s+coreLogic[\s\S]*/);
388
- if (!fnMatch) {
389
- if (jsonMode) return console.log(JSON.stringify({ error: "Could not extract coreLogic from source" }));
390
- console.error(chalk.red("Could not extract coreLogic from source"));
391
- process.exit(1);
392
- }
393
- const fnSource = fnMatch[0].replace(/export\s+/, "");
394
- const isAsync = fnMatch[0].startsWith("export async");
360
+ // Strip imports and exports, then wrap the full module scope in an IIFE
361
+ // that returns coreLogic — this preserves module-scoped helpers (e.g. generateField)
362
+ const cleanSource = coreLogicSource
363
+ .replace(/^import\s+.*?;?\s*$/gm, "") // Remove import statements
364
+ .replace(/^export\s+/gm, ""); // Remove export keywords
365
+ const wrappedSource = `(function() {\n${cleanSource}\nreturn coreLogic;\n})()`;
366
+ const coreLogicFn = new Function("return " + wrappedSource)();
367
+ const isAsync = /export\s+async\s+function\s+coreLogic/.test(coreLogicSource);
395
368
 
396
369
  // Validate input against manifest
397
370
  const schema = manifest.input_schema || {};
@@ -413,17 +386,16 @@ async function executeLocal(slug, input, jsonMode) {
413
386
  }
414
387
 
415
388
  // Create and execute the function
416
- const coreLogic = new Function("return " + fnSource)();
417
389
  const timeoutMs = manifest.timeout_ms || 5000;
418
390
  let result;
419
391
 
420
392
  if (isAsync) {
421
393
  result = await Promise.race([
422
- coreLogic(input),
394
+ coreLogicFn(input),
423
395
  new Promise((_, reject) => setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs)),
424
396
  ]);
425
397
  } else {
426
- result = coreLogic(input);
398
+ result = coreLogicFn(input);
427
399
  }
428
400
 
429
401
  if (jsonMode) return console.log(JSON.stringify({ success: true, data: result }, null, 2));
@@ -445,53 +417,62 @@ program
445
417
  const jsonMode = (cmd?.parent?.opts().json) || false;
446
418
 
447
419
  // Resolve abbreviation
448
- try {
449
- const res = await fetch(`${API_BASE}/api/tools`);
450
- const { tools } = await res.json();
451
- const slugs = (tools || []).map((t) => t.slug);
452
- const abbrMap = generateAbbreviations(slugs);
453
- if (abbrMap[slug] && slugs.includes(abbrMap[slug])) {
454
- if (!jsonMode) console.log(chalk.dim(` Resolved: ${slug} → ${abbrMap[slug]}`));
455
- slug = abbrMap[slug];
456
- }
457
- } catch { /* ignore */ }
420
+ const allTools = await fetchToolsWithFallback();
421
+ const slugs = (allTools || []).map((t) => t.slug);
422
+ const abbrMap = generateAbbreviations(slugs);
423
+ if (abbrMap[slug] && slugs.includes(abbrMap[slug])) {
424
+ if (!jsonMode) console.log(chalk.dim(` Resolved: ${slug} → ${abbrMap[slug]}`));
425
+ slug = abbrMap[slug];
426
+ }
427
+
428
+ // Try to get manifest — API first, then Supabase fallback
429
+ let manifest = null;
458
430
  try {
459
431
  const res = await fetch(`${API_BASE}/api/run/${slug}`);
460
- if (!res.ok) {
461
- if (jsonMode) return console.log(JSON.stringify({ error: `Tool "${slug}" not found` }));
462
- console.error(chalk.red(`Tool "${slug}" not found.`));
463
- return;
432
+ if (res.ok) {
433
+ const data = await res.json();
434
+ manifest = data.data?.manifest || data.manifest || data;
464
435
  }
465
- const data = await res.json();
466
- const manifest = data.data?.manifest || data.manifest || data;
467
- if (jsonMode) return console.log(JSON.stringify(manifest, null, 2));
468
- const toolName = manifest.name || manifest.slug || slug;
469
- console.log(chalk.bold(`\n ${toolName}`));
470
- console.log(` ${chalk.dim(manifest.short_description || "")}`);
471
- console.log(` Version: ${chalk.cyan(manifest.version || "1.0.0")}`);
472
- console.log(` Runtime: ${chalk.cyan(manifest.runtime || "browser")}`);
473
- console.log(` Pricing: ${chalk.cyan(manifest.pricing || "free")}`);
474
- if (manifest.input_schema?.properties) {
475
- console.log(chalk.bold(`\n Input Parameters:`));
476
- const props = manifest.input_schema.properties;
477
- const required = manifest.input_schema.required || [];
478
- for (const [name, prop] of Object.entries(props)) {
479
- const req = required.includes(name) ? chalk.red(" *required") : "";
480
- console.log(` ${chalk.cyan(name)} (${prop.type})${req}`);
481
- if (prop.description) console.log(` ${chalk.dim(prop.description)}`);
482
- }
436
+ } catch { /* API failed */ }
437
+
438
+ if (!manifest) {
439
+ // Try Supabase fallback
440
+ try {
441
+ const toolInfo = await fetchToolFromSupabase(slug);
442
+ if (toolInfo) manifest = toolInfo.manifest;
443
+ } catch { /* fallback failed */ }
444
+ }
445
+
446
+ if (!manifest) {
447
+ if (jsonMode) return console.log(JSON.stringify({ error: `Tool "${slug}" not found.` }));
448
+ console.error(chalk.red(`Tool "${slug}" not found.`));
449
+ return;
450
+ }
451
+
452
+ if (jsonMode) return console.log(JSON.stringify(manifest, null, 2));
453
+ const toolName = manifest.name || manifest.slug || slug;
454
+ console.log(chalk.bold(`\n ${toolName}`));
455
+ console.log(` ${chalk.dim(manifest.short_description || "")}`);
456
+ console.log(` Version: ${chalk.cyan(manifest.version || "1.0.0")}`);
457
+ console.log(` Runtime: ${chalk.cyan(manifest.runtime || "browser")}`);
458
+ console.log(` Pricing: ${chalk.cyan(manifest.pricing || "free")}`);
459
+ if (manifest.input_schema?.properties) {
460
+ console.log(chalk.bold(`\n Input Parameters:`));
461
+ const props = manifest.input_schema.properties;
462
+ const required = manifest.input_schema.required || [];
463
+ for (const [name, prop] of Object.entries(props)) {
464
+ const req = required.includes(name) ? chalk.red(" *required") : "";
465
+ console.log(` ${chalk.cyan(name)} (${prop.type})${req}`);
466
+ if (prop.description) console.log(` ${chalk.dim(prop.description)}`);
483
467
  }
484
- if (manifest.output_schema?.properties) {
485
- console.log(chalk.bold(`\n Output:`));
486
- for (const [name, prop] of Object.entries(manifest.output_schema.properties)) {
487
- console.log(` ${chalk.cyan(name)} (${prop.type})`);
488
- }
468
+ }
469
+ if (manifest.output_schema?.properties) {
470
+ console.log(chalk.bold(`\n Output:`));
471
+ for (const [name, prop] of Object.entries(manifest.output_schema.properties)) {
472
+ console.log(` ${chalk.cyan(name)} (${prop.type})`);
489
473
  }
490
- console.log();
491
- } catch (err) {
492
- if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
493
- console.error(chalk.red("Failed to fetch tool info:"), err.message);
494
474
  }
475
+ console.log();
495
476
  });
496
477
 
497
478
  // ─── `nxb transform <slug>` — pipe stdin through a tool ──────────────────────
@@ -538,6 +519,10 @@ program
538
519
  },
539
520
  body: JSON.stringify(body),
540
521
  });
522
+ if (!res.ok && !res.headers.get("content-type")?.includes("json")) {
523
+ if (!jsonMode) console.log(chalk.dim(` API not deployed, falling back to local execution...`));
524
+ return await executeLocal(slug, body, jsonMode);
525
+ }
541
526
  const data = await res.json();
542
527
  if (data.success) {
543
528
  const output = typeof data.data === "string" ? data.data : JSON.stringify(data.data, null, 2);
@@ -549,9 +534,8 @@ program
549
534
  process.exit(1);
550
535
  }
551
536
  } catch (err) {
552
- if (jsonMode) return console.log(JSON.stringify({ error: err.message }));
553
- console.error(chalk.red("Transform failed:"), err.message);
554
- process.exit(1);
537
+ if (!jsonMode) console.log(chalk.dim(` API unreachable, falling back to local execution...`));
538
+ return await executeLocal(slug, body, jsonMode);
555
539
  }
556
540
  });
557
541
 
@@ -688,11 +672,13 @@ program
688
672
  .description("Show tool abbreviations")
689
673
  .argument("[query]", "Search/filter abbreviations (e.g. 'ip' shows all IP-related tools)")
690
674
  .action(async (query, opts, cmd) => {
691
- try {
692
- const res = await fetch(`${API_BASE}/api/tools`);
693
- const { tools } = await res.json();
694
- const slugs = (tools || []).map((t) => t.slug);
695
- const abbrMap = generateAbbreviations(slugs);
675
+ const tools = await fetchToolsWithFallback();
676
+ if (!tools || tools.length === 0) {
677
+ console.log(chalk.yellow(" No tools available. Set NEXUSBLOOM_SUPABASE_ANON_KEY or deploy the API."));
678
+ return;
679
+ }
680
+ const slugs = tools.map((t) => t.slug);
681
+ const abbrMap = generateAbbreviations(slugs);
696
682
 
697
683
  // Filter by query if provided
698
684
  let filtered = slugs;
@@ -708,15 +694,12 @@ program
708
694
  }
709
695
 
710
696
  console.log(chalk.bold(`\n Tool Abbreviations${query ? ` matching "${query}"` : ""}:\n`));
711
- filtered.forEach((slug) => {
712
- const tool = (tools || []).find(t => t.slug === slug);
713
- const name = tool?.name || slug;
714
- console.log(` ${chalk.cyan(abbrMap[slug].padEnd(15))} → ${chalk.dim(slug.padEnd(30))} ${chalk.gray(name)}`);
715
- });
716
- console.log();
717
- } catch (err) {
718
- console.error(chalk.red("Failed to fetch tools:"), err.message);
719
- }
697
+ filtered.forEach((slug) => {
698
+ const tool = (tools || []).find(t => t.slug === slug);
699
+ const name = tool?.name || slug;
700
+ console.log(` ${chalk.cyan(abbrMap[slug].padEnd(15))} → ${chalk.dim(slug.padEnd(30))} ${chalk.gray(name)}`);
701
+ });
702
+ console.log();
720
703
  });
721
704
 
722
705
  // ─── `nxb json` — create JSON input for tools ───────────────────────────────
@@ -732,14 +715,25 @@ program
732
715
  const jsonMode = cmd.parent.opts().json;
733
716
 
734
717
  if (opts.template) {
735
- // Fetch tool schema
736
- const res = await fetch(`${API_BASE}/api/run/${opts.template}`);
737
- if (!res.ok) {
738
- console.error(chalk.red(`Tool "${opts.template}" not found`));
718
+ // Fetch tool schema — try API first, then Supabase
719
+ let manifest = null;
720
+ try {
721
+ const res = await fetch(`${API_BASE}/api/run/${opts.template}`);
722
+ if (res.ok) {
723
+ const data = await res.json();
724
+ manifest = data.data?.manifest || data.manifest || data;
725
+ }
726
+ } catch { /* API failed */ }
727
+
728
+ if (!manifest) {
729
+ const toolInfo = await fetchToolFromSupabase(opts.template);
730
+ if (toolInfo) manifest = toolInfo.manifest;
731
+ }
732
+
733
+ if (!manifest) {
734
+ console.error(chalk.red(`Tool "${opts.template}" not found. Set NEXUSBLOOM_SUPABASE_ANON_KEY or deploy the API.`));
739
735
  process.exit(1);
740
736
  }
741
- const data = await res.json();
742
- const manifest = data.data?.manifest || data.manifest || data;
743
737
  const schema = manifest.input_schema || {};
744
738
 
745
739
  let result = {};
@@ -828,6 +822,158 @@ program
828
822
  }
829
823
  });
830
824
 
825
+ // ─── Supabase Helpers ─────────────────────────────────────────────────────────
826
+
827
+ const SUPABASE_URL_DEFAULT = "https://dhxaqatmuprehrkszxzs.supabase.co";
828
+ const SUPABASE_ANON_KEY_DEFAULT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRoeGFxYXRtdXByZWhya3N6eHpzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzMxNjIzNTQsImV4cCI6MjA4ODczODM1NH0.pq7DxW66ETQt6QlIAu2WiLO08tfA7JHQVPd2SvvA3sM";
829
+
830
+ function getSupabaseKey() {
831
+ return process.env.NEXUSBLOOM_SUPABASE_ANON_KEY
832
+ || configGet("supabaseKey")
833
+ || SUPABASE_ANON_KEY_DEFAULT;
834
+ }
835
+
836
+ function getSupabaseUrl() {
837
+ return process.env.NEXUSBLOOM_SUPABASE_URL
838
+ || SUPABASE_URL_DEFAULT;
839
+ }
840
+
841
+ function supabaseHeaders() {
842
+ const key = getSupabaseKey();
843
+ return { apikey: key, Authorization: `Bearer ${key}` };
844
+ }
845
+
846
+ /**
847
+ * Fetch tool list from Supabase directly (fallback when API is down).
848
+ * Returns array of {slug, name, short_description, category, runtime, version, icon} or null.
849
+ */
850
+ async function fetchToolsFromSupabase() {
851
+ const headers = supabaseHeaders();
852
+ const supabaseUrl = getSupabaseUrl();
853
+ try {
854
+ const res = await fetch(
855
+ `${supabaseUrl}/rest/v1/tools?select=slug,name,short_description,tool_type,runtime,version,icon&published=eq.true&order=name.asc`,
856
+ { headers }
857
+ );
858
+ if (!res.ok) return null;
859
+ const tools = await res.json();
860
+ if (!Array.isArray(tools)) return null;
861
+
862
+ // Also fetch community tools
863
+ const communityRes = await fetch(
864
+ `${supabaseUrl}/rest/v1/community_tools?select=slug,name,short_description,category,version,icon,author_id&status=eq.published&order=name.asc`,
865
+ { headers }
866
+ );
867
+ const communityTools = communityRes.ok ? (await communityRes.json()) : [];
868
+
869
+ const allTools = [
870
+ ...tools.map(t => ({
871
+ slug: t.slug,
872
+ name: t.name,
873
+ description: t.short_description || "",
874
+ category: t.tool_type || "uncategorized",
875
+ runtime: t.runtime || "both",
876
+ version: t.version || "1.0.0",
877
+ icon: t.icon || "🔧",
878
+ type: "builtin",
879
+ })),
880
+ ...communityTools.map(t => ({
881
+ slug: t.slug,
882
+ name: t.name,
883
+ description: t.short_description || "",
884
+ category: t.category || "uncategorized",
885
+ runtime: "both",
886
+ version: t.version || "1.0.0",
887
+ icon: t.icon || "🛠️",
888
+ type: "community",
889
+ author_id: t.author_id,
890
+ })),
891
+ ];
892
+
893
+ return allTools;
894
+ } catch {
895
+ return null;
896
+ }
897
+ }
898
+
899
+ /**
900
+ * Fetch a single tool's v2_source from Supabase (fallback).
901
+ * Returns {manifest, coreLogicSource, v2Source} or null.
902
+ */
903
+ async function fetchToolFromSupabase(slug) {
904
+ const headers = supabaseHeaders();
905
+ const supabaseUrl = getSupabaseUrl();
906
+ try {
907
+ // Try builtin tools first
908
+ const toolRes = await fetch(
909
+ `${supabaseUrl}/rest/v1/tools?slug=eq.${slug}&select=id&published=eq.true`,
910
+ { headers }
911
+ );
912
+ let tools = await toolRes.json();
913
+ let toolId = tools?.[0]?.id;
914
+ let toolType = "builtin";
915
+
916
+ // Fall back to community tools
917
+ if (!toolId) {
918
+ const comRes = await fetch(
919
+ `${supabaseUrl}/rest/v1/community_tools?slug=eq.${slug}&select=id&status=eq.published`,
920
+ { headers }
921
+ );
922
+ const comTools = await comRes.json();
923
+ toolId = comTools?.[0]?.id;
924
+ toolType = "community";
925
+ }
926
+
927
+ if (!toolId) return null;
928
+
929
+ const versionRes = await fetch(
930
+ `${supabaseUrl}/rest/v1/tool_versions?tool_id=eq.${toolId}&tool_type=eq.${toolType}&status=eq.published&select=v2_source&order=created_at.desc&limit=1`,
931
+ { headers }
932
+ );
933
+ const versions = await versionRes.json();
934
+ const v2Source = versions?.[0]?.v2_source;
935
+ if (!v2Source) return null;
936
+
937
+ const manifestMatch = v2Source.match(/export\s+const\s+MANIFEST\s*=\s*({[\s\S]*?});\s*\n/);
938
+ let manifest = null;
939
+ if (manifestMatch) {
940
+ try {
941
+ manifest = new Function("return " + manifestMatch[1])();
942
+ } catch {
943
+ try {
944
+ manifest = JSON.parse(manifestMatch[1].replace(/(\w+):/g, '"$1":').replace(/'/g, '"'));
945
+ } catch {
946
+ manifest = eval("(" + manifestMatch[1] + ")");
947
+ }
948
+ }
949
+ }
950
+ return { manifest, coreLogicSource: v2Source, v2Source };
951
+ } catch {
952
+ return null;
953
+ }
954
+ }
955
+
956
+ /**
957
+ * Try API first, fall back to Supabase. Returns array or null.
958
+ */
959
+ async function fetchToolsWithFallback() {
960
+ // Try API first
961
+ try {
962
+ const url = new URL("/api/tools", API_BASE);
963
+ const res = await fetch(url);
964
+ const ct = res.headers.get("content-type") || "";
965
+ if (ct.includes("json")) {
966
+ const { tools } = await res.json();
967
+ if (Array.isArray(tools) && tools.length > 0) return tools;
968
+ }
969
+ } catch { /* API failed */ }
970
+
971
+ // Fall back to Supabase
972
+ const supabaseTools = await fetchToolsFromSupabase();
973
+ if (supabaseTools) return supabaseTools;
974
+ return null;
975
+ }
976
+
831
977
  // ─── Helpers ─────────────────────────────────────────────────────────────────
832
978
 
833
979
  function readStdin() {