@muxoai/cli 0.1.0 → 0.1.2

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 (3) hide show
  1. package/README.md +12 -0
  2. package/dist/index.js +643 -234
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -34,6 +34,7 @@ muxo plan Server-side diff → ordered op list
34
34
  muxo apply Push manifest, snapshot version
35
35
  muxo add <bundle> Append a bundle stanza
36
36
  muxo run <workflow> Execute a workflow one-shot
37
+ muxo call <capability> Invoke a capability directly
37
38
  muxo status Project, bundles, budget
38
39
  muxo keys list|create|rotate|revoke
39
40
  muxo credits balance|packs|buy <pack>
@@ -42,10 +43,21 @@ muxo logs [workflow] Execution history
42
43
  muxo steps <instanceId> Per-step log for one execution
43
44
  muxo login Store your key in the OS keychain
44
45
  muxo mcp Stdio MCP proxy for agents
46
+ muxo ui Launch the interactive TUI dashboard
45
47
  ```
46
48
 
47
49
  Global flags: `--json`, `--quiet`, `--debug`.
48
50
 
51
+ ## Dashboard
52
+
53
+ ```bash
54
+ muxo ui
55
+ ```
56
+
57
+ A full-screen TUI (from [`@muxoai/tui`](https://www.npmjs.com/package/@muxoai/tui))
58
+ with tabs for overview + budget, capabilities, workflows, runs, keys, and
59
+ credits. Keyboard-first, mouse-friendly.
60
+
49
61
  ## Agent-first
50
62
 
51
63
  Point any MCP client at `https://api.muxo.ai/mcp` (Bearer auth), or use the
package/dist/index.js CHANGED
@@ -4,8 +4,148 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/init.ts
7
- import { access, writeFile } from "node:fs/promises";
7
+ import { access, mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
8
8
  import { basename } from "node:path";
9
+ import { validateManifest } from "@muxoai/core";
10
+
11
+ // src/lib/api.ts
12
+ async function request(opts, method, path, body) {
13
+ if (opts.debug) {
14
+ console.error(`muxo: ${method} ${opts.baseUrl}${path}`);
15
+ }
16
+ let res;
17
+ try {
18
+ res = await fetch(`${opts.baseUrl}${path}`, {
19
+ method,
20
+ headers: {
21
+ ...opts.key ? { authorization: `Bearer ${opts.key}` } : {},
22
+ "content-type": "application/json"
23
+ },
24
+ body: body === void 0 ? void 0 : JSON.stringify(body)
25
+ });
26
+ } catch (err) {
27
+ const reason = err instanceof Error ? err.message : String(err);
28
+ return {
29
+ ok: false,
30
+ error: {
31
+ code: "internal",
32
+ message: `network error reaching ${opts.baseUrl}${path}: ${reason}. Check your connection and retry (default endpoint: https://api.muxo.ai/v1)`
33
+ }
34
+ };
35
+ }
36
+ try {
37
+ const envelope = await res.json();
38
+ if (!envelope.ok && res.status === 401 && opts.key === void 0) {
39
+ return {
40
+ ok: false,
41
+ error: {
42
+ code: envelope.error.code,
43
+ message: `${envelope.error.message} \u2014 no key found: run \`muxo keys create\` for a free one, or set MUXO_KEY`
44
+ }
45
+ };
46
+ }
47
+ return envelope;
48
+ } catch {
49
+ return {
50
+ ok: false,
51
+ error: { code: "internal", message: `muxo returned HTTP ${res.status} with a non-JSON body` }
52
+ };
53
+ }
54
+ }
55
+ function makeApi(opts) {
56
+ return {
57
+ get: (path) => request(opts, "GET", path),
58
+ post: (path, body) => request(opts, "POST", path, body),
59
+ put: (path, body) => request(opts, "PUT", path, body),
60
+ del: (path) => request(opts, "DELETE", path)
61
+ };
62
+ }
63
+
64
+ // src/lib/config.ts
65
+ import { readFile as readFile2 } from "node:fs/promises";
66
+
67
+ // src/lib/keychain.ts
68
+ import { execFile } from "node:child_process";
69
+ import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
70
+ import { homedir } from "node:os";
71
+ import { dirname, join } from "node:path";
72
+ import { promisify } from "node:util";
73
+ var run = promisify(execFile);
74
+ var SERVICE = "muxo";
75
+ var ACCOUNT = "muxo-key";
76
+ var KEY_FILE = join(homedir(), ".muxo", "key");
77
+ async function keychainGet() {
78
+ if (process.platform === "darwin") {
79
+ try {
80
+ const { stdout } = await run("security", [
81
+ "find-generic-password",
82
+ "-s",
83
+ SERVICE,
84
+ "-a",
85
+ ACCOUNT,
86
+ "-w"
87
+ ]);
88
+ const key = stdout.trim();
89
+ if (key) return key;
90
+ } catch {
91
+ }
92
+ }
93
+ try {
94
+ const key = (await readFile(KEY_FILE, "utf8")).trim();
95
+ return key || void 0;
96
+ } catch {
97
+ return void 0;
98
+ }
99
+ }
100
+ async function keychainSet(key) {
101
+ if (process.platform === "darwin") {
102
+ try {
103
+ await run("security", [
104
+ "add-generic-password",
105
+ "-U",
106
+ "-s",
107
+ SERVICE,
108
+ "-a",
109
+ ACCOUNT,
110
+ "-w",
111
+ key
112
+ ]);
113
+ return "keychain";
114
+ } catch {
115
+ }
116
+ }
117
+ await mkdir(dirname(KEY_FILE), { recursive: true });
118
+ await writeFile(KEY_FILE, `${key}
119
+ `, { mode: 384 });
120
+ await chmod(KEY_FILE, 384);
121
+ return "file";
122
+ }
123
+
124
+ // src/lib/config.ts
125
+ var DEFAULT_BASE_URL = "https://api.muxo.ai/v1";
126
+ function normalizeBaseUrl(raw) {
127
+ const trimmed = raw.trim().replace(/\/+$/, "");
128
+ if (trimmed === "") return DEFAULT_BASE_URL;
129
+ return /\/v1$/.test(trimmed) ? trimmed : `${trimmed}/v1`;
130
+ }
131
+ function baseUrl() {
132
+ return normalizeBaseUrl(process.env.MUXO_API_BASE || DEFAULT_BASE_URL);
133
+ }
134
+ async function resolveKey() {
135
+ if (process.env.MUXO_KEY) return process.env.MUXO_KEY;
136
+ return keychainGet();
137
+ }
138
+ var MANIFEST_FILES = ["muxo.yaml", "muxo.muxo", ".muxo/muxo.yaml"];
139
+ async function readManifestText() {
140
+ for (const file of MANIFEST_FILES) {
141
+ try {
142
+ return await readFile2(file, "utf8");
143
+ } catch {
144
+ continue;
145
+ }
146
+ }
147
+ return void 0;
148
+ }
9
149
 
10
150
  // src/lib/templates.ts
11
151
  import { stringify } from "yaml";
@@ -18,6 +158,8 @@ var KNOWN_BUNDLES = [
18
158
  { name: "compute", capabilities: ["compute.sandbox", "compute.container"] },
19
159
  { name: "deploy", capabilities: ["deploy.site", "deploy.container", "deploy.edge"] },
20
160
  { name: "domains", capabilities: ["domains.register", "dns.manage"] },
161
+ { name: "data", capabilities: ["db.write", "db.query"] },
162
+ { name: "kv", capabilities: ["kv.store"] },
21
163
  { name: "storage", capabilities: ["storage.object", "vector.search"] },
22
164
  { name: "ops", capabilities: ["observability.log", "queue.emit", "email.send"] }
23
165
  ];
@@ -30,28 +172,50 @@ var webIntel = {
30
172
  var llm = {
31
173
  bundles: { llm: { capabilities: ["llm.chat", "llm.embed"] } }
32
174
  };
175
+ var deploySite = {
176
+ bundles: { deploy: { capabilities: ["deploy.site", "deploy.edge"] } }
177
+ };
178
+ var storage = {
179
+ bundles: { storage: { capabilities: ["storage.object", "vector.search"] } }
180
+ };
181
+ var data = {
182
+ bundles: { data: { capabilities: ["db.write", "db.query"] } }
183
+ };
184
+ var kv = {
185
+ bundles: { kv: { capabilities: ["kv.store"] } }
186
+ };
187
+ var ops = {
188
+ bundles: { ops: { capabilities: ["observability.log", "queue.emit", "email.send"] } }
189
+ };
190
+ var compute = {
191
+ bundles: { compute: { capabilities: ["compute.sandbox", "compute.container"] } }
192
+ };
193
+ var browser = {
194
+ bundles: { browser: { capabilities: ["browser.drive"] } }
195
+ };
196
+ var domains = {
197
+ bundles: { domains: { capabilities: ["domains.register", "dns.manage"] } }
198
+ };
33
199
  var priceRadar = {
34
- bundles: { "web-intel": { capabilities: ["web.search", "web.scrape", "web.extract"] } },
200
+ bundles: {
201
+ "web-intel": { capabilities: ["web.search", "web.scrape"] }
202
+ },
35
203
  workflows: {
36
204
  "daily-prices": {
37
205
  on: { schedule: "0 6 * * *" },
38
206
  steps: [
39
- { search: { cap: "web.search", query: "competitor pricing", into: "results" } },
40
- { scrape: { cap: "web.scrape", urls: "${results.urls}", into: "pages" } },
207
+ // web.search resolves to an array of {url, title, markdown}
208
+ { search: { cap: "web.search", query: "competitor pricing", limit: 5, into: "results" } },
209
+ // fan out: scrape each result page
41
210
  {
42
- extract: {
43
- cap: "web.extract",
44
- from: "pages",
45
- schema: { product: "str", price: "num" },
46
- into: "prices"
211
+ each: {
212
+ over: "${results}",
213
+ as: "item",
214
+ steps: [{ scrape: { cap: "web.scrape", url: "${item.url}", into: "page" } }]
47
215
  }
48
- },
49
- { store: { cap: "db.write", into: "stack.database.main.prices" } }
216
+ }
50
217
  ]
51
218
  }
52
- },
53
- stack: {
54
- database: { main: { provider: "neon", service: "postgres" } }
55
219
  }
56
220
  };
57
221
  function slugify(name) {
@@ -77,34 +241,63 @@ function matchTemplate(intent) {
77
241
  const lower = intent.toLowerCase();
78
242
  const bundles = {};
79
243
  let workflows;
80
- let stack;
81
- let schedule = detectSchedule(intent);
244
+ const schedule = detectSchedule(intent);
82
245
  if (/\bprice|pricing\b/.test(lower)) {
83
246
  return priceRadar;
84
247
  }
85
- if (/track|monitor|scrape|search|extract|crawl|web|news|hacker|hn/.test(lower)) {
248
+ if (/\bhost(ing)?\b|\bdeploy|\blanding\b|\bwebsite\b|web ?site|static site|\bpublish\b|\bserve\b/.test(lower)) {
249
+ Object.assign(bundles, deploySite.bundles);
250
+ }
251
+ if (/\btrack|monitor|scrape|search|extract|crawl|\bweb\b|news|hacker|\bhn\b/.test(lower)) {
86
252
  Object.assign(bundles, webIntel.bundles);
87
253
  }
88
- if (/chat|llm|summar|embed|\bai\b/.test(lower)) {
254
+ if (/\bchat|llm|summar|embed|\bai\b|assistant|generate\b/.test(lower)) {
89
255
  Object.assign(bundles, llm.bundles);
90
256
  }
91
- if (/store|database|db|postgres|save\b/.test(lower)) {
92
- stack = { database: { main: { provider: "neon", service: "postgres" } } };
257
+ if (/\bstore|save|database|postgres|sql|record|table\b/.test(lower)) {
258
+ Object.assign(bundles, data.bundles);
259
+ }
260
+ if (/\bkv|cache|config|settings|feature flag\b/.test(lower)) {
261
+ Object.assign(bundles, kv.bundles);
262
+ }
263
+ if (/\bupload|\bfile|asset|object|bucket\b/.test(lower)) {
264
+ Object.assign(bundles, storage.bundles);
265
+ }
266
+ if (/\bvector|embedding|semantic|similarity|\brag\b|recommend\b/.test(lower)) {
267
+ Object.assign(bundles, storage.bundles);
268
+ }
269
+ if (/\bemail|notif|alert|notify|\bmail\b/.test(lower)) {
270
+ Object.assign(bundles, ops.bundles);
93
271
  }
94
- if (schedule !== void 0 && Object.keys(bundles).length > 0) {
272
+ if (/\bsandbox|execute|run code|compute|container|docker|\bcode\b/.test(lower)) {
273
+ Object.assign(bundles, compute.bundles);
274
+ }
275
+ if (/\bbrowser|automate|click|login form|\bform\b/.test(lower)) {
276
+ Object.assign(bundles, browser.bundles);
277
+ }
278
+ if (/\bdomain|\bdns\b/.test(lower)) {
279
+ Object.assign(bundles, domains.bundles);
280
+ }
281
+ if (schedule !== void 0 && bundles["web-intel"] !== void 0) {
95
282
  const topic = (lower.match(/[a-z][a-z-]{3,}/)?.[0] ?? "watch").slice(0, 20);
96
283
  workflows = {
97
284
  [`${topic.replace(/-+$/, "")}-watch`]: {
98
285
  on: { schedule },
99
286
  steps: [
100
287
  { search: { cap: "web.search", query: intent.slice(0, 80), limit: 3, into: "results" } },
101
- { log: { cap: "observability.log", level: "info", message: "found ${results.count} results" } }
288
+ {
289
+ each: {
290
+ over: "${results}",
291
+ as: "item",
292
+ steps: [{ scrape: { cap: "web.scrape", url: "${item.url}", into: "page" } }]
293
+ }
294
+ }
102
295
  ]
103
296
  }
104
297
  };
105
298
  }
106
299
  if (Object.keys(bundles).length === 0) return { bundles: {} };
107
- return { bundles, workflows, stack };
300
+ return { bundles, workflows };
108
301
  }
109
302
  function renderManifest(project, description, tpl) {
110
303
  const doc = {
@@ -177,102 +370,114 @@ async function exists(path) {
177
370
  return false;
178
371
  }
179
372
  }
373
+ function landingPage(title) {
374
+ return `<!doctype html>
375
+ <html lang="en">
376
+ <head>
377
+ <meta charset="utf-8" />
378
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
379
+ <title>${title}</title>
380
+ <style>
381
+ :root { color-scheme: dark; }
382
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center;
383
+ font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
384
+ background: #0b0b12; color: #f5f5f7; }
385
+ main { text-align: center; padding: 2rem; }
386
+ h1 { font-size: clamp(2rem, 6vw, 4rem); margin: 0 0 0.5rem; }
387
+ p { color: #a1a1aa; margin: 0; }
388
+ </style>
389
+ </head>
390
+ <body>
391
+ <main>
392
+ <h1>${title}</h1>
393
+ <p>Deployed with muxo.</p>
394
+ </main>
395
+ </body>
396
+ </html>
397
+ `;
398
+ }
399
+ async function harnessManifest(intent, project) {
400
+ const base = process.env.MUXO_HARNESS_URL;
401
+ if (base === void 0 || base.trim() === "") return void 0;
402
+ try {
403
+ const res = await fetch(`${base.replace(/\/+$/, "")}/architect`, {
404
+ method: "POST",
405
+ headers: { "content-type": "application/json" },
406
+ body: JSON.stringify({ intent, project }),
407
+ signal: AbortSignal.timeout(18e4)
408
+ });
409
+ if (!res.ok) return void 0;
410
+ const body = await res.json();
411
+ const yaml = body.data?.yaml;
412
+ if (typeof yaml !== "string" || yaml.trim() === "") return void 0;
413
+ if (!validateManifest(yaml).ok) return void 0;
414
+ return { yaml, source: "harness" };
415
+ } catch {
416
+ return void 0;
417
+ }
418
+ }
419
+ async function architectManifest(intent, project, opts) {
420
+ const key = await resolveKey();
421
+ if (key === void 0) return void 0;
422
+ const api = makeApi({ baseUrl: baseUrl(), key, debug: opts.debug });
423
+ const res = await api.post("/architect", { intent, project });
424
+ if (!res.ok) return void 0;
425
+ const data2 = res.data;
426
+ if (typeof data2?.yaml !== "string" || data2.yaml.trim() === "") return void 0;
427
+ return { yaml: data2.yaml, source: "architect" };
428
+ }
180
429
  async function init(intent, opts) {
181
430
  if (await exists("muxo.yaml")) {
182
431
  console.error("error: muxo.yaml already exists in this directory");
183
432
  return 1;
184
433
  }
185
- const it = intent ?? "";
186
- const tpl = matchTemplate(it);
434
+ const it = (intent ?? "").trim();
187
435
  const project = slugify(basename(process.cwd()));
188
- const text = renderManifest(project, it, tpl);
189
- await writeFile("muxo.yaml", text, "utf8");
190
- info("wrote muxo.yaml", opts);
191
- nextSteps(["muxo validate", "muxo plan", "muxo apply", "muxo run <workflow> # or wait for the cron schedule", "muxo logs # watch executions"], opts);
192
- return 0;
193
- }
194
-
195
- // src/lib/config.ts
196
- import { readFile as readFile2 } from "node:fs/promises";
197
-
198
- // src/lib/keychain.ts
199
- import { execFile } from "node:child_process";
200
- import { chmod, mkdir, readFile, unlink, writeFile as writeFile2 } from "node:fs/promises";
201
- import { homedir } from "node:os";
202
- import { dirname, join } from "node:path";
203
- import { promisify } from "node:util";
204
- var run = promisify(execFile);
205
- var SERVICE = "muxo";
206
- var ACCOUNT = "muxo-key";
207
- var KEY_FILE = join(homedir(), ".muxo", "key");
208
- async function keychainGet() {
209
- if (process.platform === "darwin") {
436
+ let text;
437
+ let source = "template";
438
+ if (it !== "") {
210
439
  try {
211
- const { stdout } = await run("security", [
212
- "find-generic-password",
213
- "-s",
214
- SERVICE,
215
- "-a",
216
- ACCOUNT,
217
- "-w"
218
- ]);
219
- const key = stdout.trim();
220
- if (key) return key;
440
+ const designed = await harnessManifest(it, project) ?? await architectManifest(it, project, opts);
441
+ if (designed !== void 0) {
442
+ text = designed.yaml;
443
+ source = designed.source;
444
+ }
221
445
  } catch {
222
446
  }
223
447
  }
224
- try {
225
- const key = (await readFile(KEY_FILE, "utf8")).trim();
226
- return key || void 0;
227
- } catch {
228
- return void 0;
448
+ const tpl = matchTemplate(it);
449
+ if (text === void 0) text = renderManifest(project, it, tpl);
450
+ await writeFile2("muxo.yaml", text, "utf8");
451
+ const designedBy = source === "harness" ? " (designed by the muxo harness)" : source === "architect" ? " (designed by the muxo architect)" : "";
452
+ info(`wrote muxo.yaml${designedBy}`, opts);
453
+ const parsed = validateManifest(text);
454
+ const bundles = parsed.ok && parsed.manifest !== void 0 ? Object.keys(parsed.manifest.bundles ?? {}) : Object.keys(tpl.bundles);
455
+ if (bundles.length > 0) {
456
+ info(`bundles: ${bundles.join(", ")}`, opts);
457
+ } else {
458
+ info("no bundles yet \u2014 add one with `muxo add <bundle>`", opts);
229
459
  }
230
- }
231
- async function keychainSet(key) {
232
- if (process.platform === "darwin") {
233
- try {
234
- await run("security", [
235
- "add-generic-password",
236
- "-U",
237
- "-s",
238
- SERVICE,
239
- "-a",
240
- ACCOUNT,
241
- "-w",
242
- key
243
- ]);
244
- return "keychain";
245
- } catch {
246
- }
460
+ const hasDeploy = parsed.ok && parsed.manifest !== void 0 ? Object.values(parsed.manifest.bundles ?? {}).some((b) => b.capabilities.includes("deploy.site")) : tpl.bundles.deploy !== void 0;
461
+ let scaffolded = false;
462
+ if (hasDeploy && !await exists("public/index.html")) {
463
+ await mkdir2("public", { recursive: true });
464
+ await writeFile2("public/index.html", landingPage(project), "utf8");
465
+ info("wrote public/index.html", opts);
466
+ scaffolded = true;
247
467
  }
248
- await mkdir(dirname(KEY_FILE), { recursive: true });
249
- await writeFile2(KEY_FILE, `${key}
250
- `, { mode: 384 });
251
- await chmod(KEY_FILE, 384);
252
- return "file";
253
- }
254
-
255
- // src/lib/config.ts
256
- var DEFAULT_BASE_URL = "https://api.muxo.ai/v1";
257
- var LOCAL_BASE_URL = "http://localhost:8787/v1";
258
- function baseUrl(local) {
259
- if (local) return LOCAL_BASE_URL;
260
- return process.env.MUXO_API_BASE || DEFAULT_BASE_URL;
261
- }
262
- async function resolveKey() {
263
- if (process.env.MUXO_KEY) return process.env.MUXO_KEY;
264
- return keychainGet();
265
- }
266
- var MANIFEST_FILES = ["muxo.yaml", "muxo.muxo", ".muxo/muxo.yaml"];
267
- async function readManifestText() {
268
- for (const file of MANIFEST_FILES) {
269
- try {
270
- return await readFile2(file, "utf8");
271
- } catch {
272
- continue;
273
- }
468
+ const steps = ["muxo validate", "muxo plan", "muxo apply"];
469
+ const workflowNames = parsed.ok && parsed.manifest !== void 0 ? Object.keys(parsed.manifest.workflows ?? {}) : Object.keys(tpl.workflows ?? {});
470
+ if (workflowNames.length > 0) {
471
+ steps.push(`muxo run ${workflowNames[0]}`, "muxo logs # watch executions");
274
472
  }
275
- return void 0;
473
+ if (hasDeploy) {
474
+ steps.push("muxo keys create # a key scoped to this manifest");
475
+ steps.push(`muxo deploy ${scaffolded ? "public" : "<dir>"} # upload the site \u2192 live URL`);
476
+ } else if (workflowNames.length === 0) {
477
+ steps.push("muxo status # confirm the applied state");
478
+ }
479
+ nextSteps(steps, opts);
480
+ return 0;
276
481
  }
277
482
 
278
483
  // src/commands/validate.ts
@@ -330,7 +535,7 @@ var manifestSchema = z.object({
330
535
  }).optional(),
331
536
  workflows: z.record(workflow).optional()
332
537
  }).strict();
333
- function validateManifest(text) {
538
+ function validateManifest2(text) {
334
539
  let doc;
335
540
  try {
336
541
  doc = parseYaml(text);
@@ -381,20 +586,32 @@ function validateManifest(text) {
381
586
  }
382
587
 
383
588
  // src/commands/validate.ts
589
+ var ZERO_USAGE = { credits: 0, provider: "muxo", latency_ms: 0 };
384
590
  async function validate(opts) {
385
591
  const text = await readManifestText();
386
592
  if (text === void 0) {
387
- const err = [{ path: "", message: "no muxo.yaml found in this directory" }];
388
- if (opts.json) printJson(err);
389
- else console.error(`error: ${err[0].message}`);
593
+ const message = "no muxo.yaml found in this directory";
594
+ if (opts.json) {
595
+ printJson({ ok: false, error: { code: "not_found", message } });
596
+ } else {
597
+ console.error(`error: ${message}`);
598
+ }
390
599
  return 1;
391
600
  }
392
601
  const issues = [
393
602
  ...coreValidate(text).errors.map((e) => ({ path: e.path, message: e.message + (e.hint ? ` (${e.hint})` : "") })),
394
- ...validateManifest(text)
603
+ ...validateManifest2(text)
395
604
  ];
396
605
  if (opts.json) {
397
- printJson(issues);
606
+ if (issues.length === 0) {
607
+ printJson({ ok: true, data: { valid: true, issues: [] }, usage: ZERO_USAGE });
608
+ } else {
609
+ printJson({
610
+ ok: false,
611
+ error: { code: "invalid_manifest", message: `muxo.yaml is invalid (${issues.length} issue${issues.length === 1 ? "" : "s"})` },
612
+ data: { valid: false, issues }
613
+ });
614
+ }
398
615
  } else if (issues.length === 0) {
399
616
  if (!opts.quiet) console.log("muxo.yaml is valid");
400
617
  } else {
@@ -405,49 +622,6 @@ async function validate(opts) {
405
622
  return issues.length > 0 ? 1 : 0;
406
623
  }
407
624
 
408
- // src/lib/api.ts
409
- async function request(opts, method, path, body) {
410
- if (opts.debug) {
411
- console.error(`muxo: ${method} ${opts.baseUrl}${path}`);
412
- }
413
- let res;
414
- try {
415
- res = await fetch(`${opts.baseUrl}${path}`, {
416
- method,
417
- headers: {
418
- ...opts.key ? { authorization: `Bearer ${opts.key}` } : {},
419
- "content-type": "application/json"
420
- },
421
- body: body === void 0 ? void 0 : JSON.stringify(body)
422
- });
423
- } catch (err) {
424
- const reason = err instanceof Error ? err.message : String(err);
425
- return {
426
- ok: false,
427
- error: {
428
- code: "internal",
429
- message: `network error reaching ${opts.baseUrl}${path}: ${reason}. If this is your first run, set MUXO_API_BASE to your runtime URL (e.g. export MUXO_API_BASE=https://api.muxo.ai/v1)`
430
- }
431
- };
432
- }
433
- try {
434
- return await res.json();
435
- } catch {
436
- return {
437
- ok: false,
438
- error: { code: "internal", message: `muxo returned HTTP ${res.status} with a non-JSON body` }
439
- };
440
- }
441
- }
442
- function makeApi(opts) {
443
- return {
444
- get: (path) => request(opts, "GET", path),
445
- post: (path, body) => request(opts, "POST", path, body),
446
- put: (path, body) => request(opts, "PUT", path, body),
447
- del: (path) => request(opts, "DELETE", path)
448
- };
449
- }
450
-
451
625
  // src/commands/plan.ts
452
626
  async function plan(opts) {
453
627
  const text = await readManifestText();
@@ -456,15 +630,15 @@ async function plan(opts) {
456
630
  return 1;
457
631
  }
458
632
  const api = makeApi({
459
- baseUrl: baseUrl(opts.local),
633
+ baseUrl: baseUrl(),
460
634
  key: await resolveKey(),
461
635
  debug: opts.debug
462
636
  });
463
637
  const env = await api.post("/plan", { yaml: text });
464
- return renderEnvelope(env, !!opts.json, (data) => {
465
- const ops = data?.ops;
466
- if (Array.isArray(ops)) {
467
- if (ops.length === 0) {
638
+ return renderEnvelope(env, !!opts.json, (data2) => {
639
+ const ops2 = data2?.ops;
640
+ if (Array.isArray(ops2)) {
641
+ if (ops2.length === 0) {
468
642
  console.log("no changes \u2014 manifest matches the deployed state");
469
643
  return;
470
644
  }
@@ -477,16 +651,16 @@ async function plan(opts) {
477
651
  deprovision: "-",
478
652
  noop: "!"
479
653
  };
480
- ops.forEach((op, i) => {
654
+ ops2.forEach((op, i) => {
481
655
  const icon = ICONS[op.kind] ?? "\xB7";
482
656
  const label = op.detail !== void 0 && op.detail !== "" ? `${op.ref}: ${op.detail}` : op.ref;
483
657
  console.log(`${i + 1}. ${icon} ${label}`);
484
658
  });
485
- const removes = ops.filter((o) => o.kind === "deprovision").length;
659
+ const removes = ops2.filter((o) => o.kind === "deprovision").length;
486
660
  if (removes > 0) console.log(`
487
661
  \u26A0 ${removes} deprovision op(s) \u2014 apply will remove these resources`);
488
662
  } else {
489
- console.log(JSON.stringify(data, null, 2));
663
+ console.log(JSON.stringify(data2, null, 2));
490
664
  }
491
665
  });
492
666
  }
@@ -497,16 +671,16 @@ async function apply(opts) {
497
671
  return 1;
498
672
  }
499
673
  const api = makeApi({
500
- baseUrl: baseUrl(opts.local),
674
+ baseUrl: baseUrl(),
501
675
  key: await resolveKey(),
502
676
  debug: opts.debug
503
677
  });
504
678
  const env = await api.put("/manifest", { yaml: text });
505
- return renderEnvelope(env, !!opts.json, (data) => {
506
- const d = data;
679
+ return renderEnvelope(env, !!opts.json, (data2) => {
680
+ const d = data2;
507
681
  const version = d?.version !== void 0 ? ` version ${d.version}` : "";
508
- const ops = Array.isArray(d?.ops) ? ` (${d.ops.length} ops)` : "";
509
- console.log(`applied${version}${ops}`);
682
+ const ops2 = Array.isArray(d?.ops) ? ` (${d.ops.length} ops)` : "";
683
+ console.log(`applied${version}${ops2}`);
510
684
  if (Array.isArray(d?.ops)) {
511
685
  d.ops.forEach((op, i) => {
512
686
  const label = typeof op === "string" ? op : JSON.stringify(op);
@@ -559,21 +733,7 @@ async function add(bundleName, opts) {
559
733
  return 0;
560
734
  }
561
735
 
562
- // src/commands/run.ts
563
- function printSteps(data) {
564
- const d = data;
565
- if (d?.execution_id !== void 0) console.log(`execution ${String(d.execution_id)}`);
566
- if (Array.isArray(d?.steps)) {
567
- for (const step of d.steps) {
568
- const name = typeof step?.name === "string" ? step.name : "step";
569
- const status2 = typeof step?.status === "string" ? step.status : JSON.stringify(step);
570
- console.log(` ${name}: ${status2}`);
571
- if (step?.error !== void 0) console.log(` ${JSON.stringify(step.error)}`);
572
- }
573
- return;
574
- }
575
- console.log(JSON.stringify(data, null, 2));
576
- }
736
+ // src/lib/params.ts
577
737
  function coerceParam(value) {
578
738
  if (value === "true") return true;
579
739
  if (value === "false") return false;
@@ -589,9 +749,125 @@ function parseParams(raw) {
589
749
  }
590
750
  return params;
591
751
  }
752
+
753
+ // src/commands/call.ts
754
+ async function call(capability, opts) {
755
+ let input = {};
756
+ if (opts.data !== void 0) {
757
+ try {
758
+ const parsed = JSON.parse(opts.data);
759
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
760
+ console.error("error: --data must be a JSON object");
761
+ return 1;
762
+ }
763
+ input = parsed;
764
+ } catch (e) {
765
+ console.error(`error: --data is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
766
+ return 1;
767
+ }
768
+ }
769
+ try {
770
+ input = { ...input, ...parseParams(opts.param) };
771
+ } catch (e) {
772
+ console.error(`error: ${e instanceof Error ? e.message : String(e)}`);
773
+ return 1;
774
+ }
775
+ const api = makeApi({
776
+ baseUrl: baseUrl(),
777
+ key: await resolveKey(),
778
+ debug: opts.debug
779
+ });
780
+ const env = await api.post(`/capabilities/${capability}`, input);
781
+ return renderEnvelope(env, !!opts.json, (data2) => printJson(data2));
782
+ }
783
+
784
+ // src/commands/deploy.ts
785
+ import { access as access2, readdir, readFile as readFile4 } from "node:fs/promises";
786
+ import { join as join2, relative, sep } from "node:path";
787
+ async function exists2(path) {
788
+ try {
789
+ await access2(path);
790
+ return true;
791
+ } catch {
792
+ return false;
793
+ }
794
+ }
795
+ async function collectFiles(dir) {
796
+ const out = {};
797
+ async function walk(current) {
798
+ for (const entry of await readdir(current, { withFileTypes: true })) {
799
+ if (entry.name.startsWith(".")) continue;
800
+ const full = join2(current, entry.name);
801
+ if (entry.isDirectory()) {
802
+ await walk(full);
803
+ continue;
804
+ }
805
+ if (!entry.isFile()) continue;
806
+ const rel = relative(dir, full).split(sep).join("/");
807
+ out[`/${rel}`] = await readFile4(full, "utf8");
808
+ }
809
+ }
810
+ await walk(dir);
811
+ return out;
812
+ }
813
+ async function deploy(dirArg, opts) {
814
+ let dir = dirArg;
815
+ if (dir === void 0) {
816
+ for (const candidate of ["public", "dist", "build", "out"]) {
817
+ if (await exists2(candidate)) {
818
+ dir = candidate;
819
+ break;
820
+ }
821
+ }
822
+ }
823
+ if (dir === void 0 || !await exists2(dir)) {
824
+ console.error(`error: no site directory found${dir !== void 0 ? ` at "${dir}"` : ""} \u2014 pass one: muxo deploy <dir>`);
825
+ return 1;
826
+ }
827
+ const files = await collectFiles(dir);
828
+ const count = Object.keys(files).length;
829
+ if (count === 0) {
830
+ console.error(`error: "${dir}" is empty`);
831
+ return 1;
832
+ }
833
+ if (!opts.quiet && !opts.json) console.error(`deploying ${count} file(s) from ${dir}\u2026`);
834
+ const api = makeApi({
835
+ baseUrl: baseUrl(),
836
+ key: await resolveKey(),
837
+ debug: opts.debug
838
+ });
839
+ const env = await api.post("/capabilities/deploy.site", {
840
+ files,
841
+ ...opts.target !== void 0 ? { target: opts.target } : {}
842
+ });
843
+ return renderEnvelope(env, !!opts.json, (data2) => {
844
+ const d = data2;
845
+ if (typeof d?.url === "string" && d.url !== "") {
846
+ console.log(`deployed: ${d.url}`);
847
+ return;
848
+ }
849
+ console.log(JSON.stringify(data2, null, 2));
850
+ });
851
+ }
852
+
853
+ // src/commands/run.ts
854
+ function printSteps(data2) {
855
+ const d = data2;
856
+ if (d?.execution_id !== void 0) console.log(`execution ${String(d.execution_id)}`);
857
+ if (Array.isArray(d?.steps)) {
858
+ for (const step of d.steps) {
859
+ const name = typeof step?.name === "string" ? step.name : "step";
860
+ const status2 = typeof step?.status === "string" ? step.status : JSON.stringify(step);
861
+ console.log(` ${name}: ${status2}`);
862
+ if (step?.error !== void 0) console.log(` ${JSON.stringify(step.error)}`);
863
+ }
864
+ return;
865
+ }
866
+ console.log(JSON.stringify(data2, null, 2));
867
+ }
592
868
  async function run2(workflow2, opts) {
593
869
  const api = makeApi({
594
- baseUrl: baseUrl(opts.local),
870
+ baseUrl: baseUrl(),
595
871
  key: await resolveKey(),
596
872
  debug: opts.debug
597
873
  });
@@ -621,13 +897,13 @@ async function run2(workflow2, opts) {
621
897
  }
622
898
  async function status(opts) {
623
899
  const api = makeApi({
624
- baseUrl: baseUrl(opts.local),
900
+ baseUrl: baseUrl(),
625
901
  key: await resolveKey(),
626
902
  debug: opts.debug
627
903
  });
628
904
  const env = await api.get("/status");
629
- return renderEnvelope(env, !!opts.json, (data) => {
630
- const d = data;
905
+ return renderEnvelope(env, !!opts.json, (data2) => {
906
+ const d = data2;
631
907
  if (d && typeof d === "object" && !Array.isArray(d)) {
632
908
  if (d.project !== void 0) console.log(`project: ${String(d.project)}`);
633
909
  if (d.version !== void 0) console.log(`version: ${String(d.version)}`);
@@ -640,31 +916,31 @@ async function status(opts) {
640
916
  if (d.drift !== void 0) console.log(`drift: ${JSON.stringify(d.drift)}`);
641
917
  return;
642
918
  }
643
- console.log(JSON.stringify(data, null, 2));
919
+ console.log(JSON.stringify(data2, null, 2));
644
920
  });
645
921
  }
646
922
  async function rollback(version, opts) {
647
923
  const api = makeApi({
648
- baseUrl: baseUrl(opts.local),
924
+ baseUrl: baseUrl(),
649
925
  key: await resolveKey(),
650
926
  debug: opts.debug
651
927
  });
652
928
  const body = version === void 0 ? {} : { version: Number(version) };
653
929
  const env = await api.post("/rollback", body);
654
- return renderEnvelope(env, !!opts.json, (data) => {
655
- console.log(`rolled back to ${JSON.stringify(data)}`);
930
+ return renderEnvelope(env, !!opts.json, (data2) => {
931
+ console.log(`rolled back to ${JSON.stringify(data2)}`);
656
932
  });
657
933
  }
658
934
  async function logs(workflow2, opts) {
659
935
  const api = makeApi({
660
- baseUrl: baseUrl(opts.local),
936
+ baseUrl: baseUrl(),
661
937
  key: await resolveKey(),
662
938
  debug: opts.debug
663
939
  });
664
940
  const qs = workflow2 ? `?workflow=${encodeURIComponent(workflow2)}` : "";
665
941
  const env = await api.get(`/workflows/${encodeURIComponent(workflow2 ?? "_all")}/executions${qs}`);
666
- return renderEnvelope(env, !!opts.json, (data) => {
667
- const d = data;
942
+ return renderEnvelope(env, !!opts.json, (data2) => {
943
+ const d = data2;
668
944
  if (Array.isArray(d?.executions)) {
669
945
  for (const e of d.executions) {
670
946
  const when = new Date(e.started_at).toISOString().slice(0, 19).replace("T", " ");
@@ -674,18 +950,18 @@ async function logs(workflow2, opts) {
674
950
  }
675
951
  return;
676
952
  }
677
- console.log(JSON.stringify(data, null, 2));
953
+ console.log(JSON.stringify(data2, null, 2));
678
954
  });
679
955
  }
680
956
  async function stepLogs(instanceId, opts) {
681
957
  const api = makeApi({
682
- baseUrl: baseUrl(opts.local),
958
+ baseUrl: baseUrl(),
683
959
  key: await resolveKey(),
684
960
  debug: opts.debug
685
961
  });
686
962
  const env = await api.get(`/executions/${encodeURIComponent(instanceId)}/steps`);
687
- return renderEnvelope(env, !!opts.json, (data) => {
688
- const rows = data?.steps ?? [];
963
+ return renderEnvelope(env, !!opts.json, (data2) => {
964
+ const rows = data2?.steps ?? [];
689
965
  for (const s of rows) {
690
966
  const mark = s.status === "ok" ? "\u2713" : "\u2717";
691
967
  const err = s.error ? ` \u2014 ${s.error}` : "";
@@ -695,10 +971,25 @@ async function stepLogs(instanceId, opts) {
695
971
  }
696
972
 
697
973
  // src/commands/keys.ts
974
+ import { parseManifest } from "@muxoai/core";
975
+ async function bootstrapBody() {
976
+ const text = await readManifestText();
977
+ if (text === void 0) return {};
978
+ const parsed = parseManifest(text);
979
+ if (!parsed.ok || parsed.manifest === void 0) return {};
980
+ const capabilities2 = [
981
+ ...new Set(Object.values(parsed.manifest.bundles ?? {}).flatMap((b) => b.capabilities ?? []))
982
+ ];
983
+ return {
984
+ project: parsed.manifest.project,
985
+ ...capabilities2.length > 0 ? { scopes: capabilities2 } : {}
986
+ };
987
+ }
698
988
  async function keysCommand(action, id, opts) {
989
+ const existingKey = await resolveKey();
699
990
  const api = makeApi({
700
- baseUrl: baseUrl(opts.local),
701
- key: await resolveKey(),
991
+ baseUrl: baseUrl(),
992
+ key: existingKey,
702
993
  debug: opts.debug
703
994
  });
704
995
  if (action === "revoke" && !id) {
@@ -722,7 +1013,7 @@ async function keysCommand(action, id, opts) {
722
1013
  env = await api.get("/keys");
723
1014
  break;
724
1015
  case "create":
725
- env = await api.post("/keys");
1016
+ env = await api.post("/keys", existingKey === void 0 ? await bootstrapBody() : void 0);
726
1017
  break;
727
1018
  case "rotate":
728
1019
  env = await api.post(`/keys/${id}/rotate`);
@@ -731,32 +1022,62 @@ async function keysCommand(action, id, opts) {
731
1022
  env = await api.del(`/keys/${id}`);
732
1023
  break;
733
1024
  }
1025
+ let storedIn;
1026
+ if (env.ok && action === "create" && existingKey === void 0) {
1027
+ const data2 = env.data;
1028
+ if (typeof data2?.key === "string") {
1029
+ storedIn = await keychainSet(data2.key);
1030
+ if (!opts.json && storedIn === "file") {
1031
+ console.error("warning: keychain unavailable; key stored in ~/.muxo/key (chmod 600)");
1032
+ }
1033
+ }
1034
+ }
734
1035
  if (env.ok && (action === "create" || action === "rotate") && !opts.json) {
735
- const data = env.data;
736
- if (data?.key !== void 0) {
737
- const id2 = data.keyId ?? data.id;
738
- console.log(`${action === "rotate" ? "rotated" : "created"} key ${id2 !== void 0 ? String(id2) : ""}`);
739
- console.log(String(data.key));
740
- console.log("store it now \u2014 it will not be shown again");
1036
+ const data2 = env.data;
1037
+ if (data2?.key !== void 0) {
1038
+ const keyId = data2.keyId ?? data2.key_id ?? data2.id;
1039
+ console.log(`${action === "rotate" ? "rotated" : "created"} key ${keyId !== void 0 ? String(keyId) : ""}`);
1040
+ console.log(String(data2.key));
1041
+ if (data2.project_id !== void 0) {
1042
+ console.log(`project ${String(data2.project_id)}${data2.credits !== void 0 ? ` \xB7 ${String(data2.credits)} credits` : ""}`);
1043
+ }
1044
+ console.log(
1045
+ storedIn !== void 0 ? `stored in ${storedIn === "keychain" ? "OS keychain" : "~/.muxo/key"} \u2014 ready to use` : "store it now \u2014 it will not be shown again"
1046
+ );
741
1047
  return 0;
742
1048
  }
743
1049
  }
744
- return renderEnvelope(env, !!opts.json, (data) => {
745
- if (Array.isArray(data)) {
746
- for (const key of data) {
1050
+ return renderEnvelope(env, !!opts.json, (data2) => {
1051
+ if (Array.isArray(data2)) {
1052
+ for (const key of data2) {
747
1053
  const k = key;
748
1054
  console.log(`${String(k?.id ?? "?")}${k?.revoked ? " (revoked)" : ""}`);
749
1055
  }
750
1056
  return;
751
1057
  }
752
- printJson(data);
1058
+ printJson(data2);
1059
+ });
1060
+ }
1061
+
1062
+ // src/commands/capabilities.ts
1063
+ async function capabilities(opts) {
1064
+ const api = makeApi({ baseUrl: baseUrl(), key: await resolveKey(), debug: opts.debug });
1065
+ const env = await api.get("/capabilities");
1066
+ return renderEnvelope(env, !!opts.json, (data2) => {
1067
+ const rows = data2.capabilities ?? [];
1068
+ for (const c of rows) {
1069
+ const providers = c.backends.map((b) => b.configured ? b.provider : `${b.provider}*`).join(", ");
1070
+ console.log(`${c.available ? "ok " : "-- "}${(c.friendly ?? c.name).padEnd(18)} ${c.name.padEnd(22)} ${providers}`);
1071
+ }
1072
+ console.log("\n* = not configured (missing runtime secrets)");
1073
+ console.log("friendly names work anywhere a capability name does (REST, MCP, CLI, SDK)");
753
1074
  });
754
1075
  }
755
1076
 
756
1077
  // src/commands/credits.ts
757
1078
  async function creditsCommand(action, packId, opts) {
758
1079
  const api = makeApi({
759
- baseUrl: baseUrl(opts.local),
1080
+ baseUrl: baseUrl(),
760
1081
  key: await resolveKey(),
761
1082
  debug: opts.debug
762
1083
  });
@@ -766,8 +1087,8 @@ async function creditsCommand(action, packId, opts) {
766
1087
  return 1;
767
1088
  }
768
1089
  const env2 = await api.post("/credits/checkout", { pack: packId });
769
- return renderEnvelope(env2, !!opts.json, (data) => {
770
- const d = data;
1090
+ return renderEnvelope(env2, !!opts.json, (data2) => {
1091
+ const d = data2;
771
1092
  if (d?.url !== void 0) {
772
1093
  console.log(`checkout: ${d.url}`);
773
1094
  if (d.stubbed) console.log("(stub \u2014 set STRIPE_SECRET_KEY on the runtime for real checkout)");
@@ -775,8 +1096,8 @@ async function creditsCommand(action, packId, opts) {
775
1096
  });
776
1097
  }
777
1098
  const env = await api.get("/credits");
778
- return renderEnvelope(env, !!opts.json, (data) => {
779
- const d = data;
1099
+ return renderEnvelope(env, !!opts.json, (data2) => {
1100
+ const d = data2;
780
1101
  if (action === "packs") {
781
1102
  for (const p of d?.packs ?? []) {
782
1103
  console.log(`${p.id} ${p.credits} credits $${(p.price_cents / 100).toFixed(2)}`);
@@ -807,7 +1128,7 @@ function promptHidden(promptText) {
807
1128
  });
808
1129
  return;
809
1130
  }
810
- process.stdout.write(promptText);
1131
+ process.stderr.write(promptText);
811
1132
  const wasRaw = process.stdin.isRaw ?? false;
812
1133
  process.stdin.setRawMode(true);
813
1134
  process.stdin.resume();
@@ -818,7 +1139,7 @@ function promptHidden(promptText) {
818
1139
  process.stdin.setRawMode(wasRaw);
819
1140
  process.stdin.pause();
820
1141
  process.stdin.removeListener("data", onData);
821
- process.stdout.write("\n");
1142
+ process.stderr.write("\n");
822
1143
  resolve(buf);
823
1144
  return;
824
1145
  }
@@ -828,7 +1149,7 @@ function promptHidden(promptText) {
828
1149
  process.stdin.setRawMode(wasRaw);
829
1150
  process.stdin.pause();
830
1151
  process.stdin.removeListener("data", onData);
831
- process.stdout.write("\n");
1152
+ process.stderr.write("\n");
832
1153
  process.exit(130);
833
1154
  } else {
834
1155
  buf += ch;
@@ -841,14 +1162,41 @@ function promptHidden(promptText) {
841
1162
  async function login(opts) {
842
1163
  const key = await promptHidden("Paste your muxo key: ");
843
1164
  if (!key) {
844
- console.error("error: no key provided");
1165
+ if (opts.json) {
1166
+ printJson({ ok: false, error: { code: "invalid_input", message: "no key provided" } });
1167
+ } else {
1168
+ console.error("error: no key provided");
1169
+ }
845
1170
  return 1;
846
1171
  }
1172
+ const api = makeApi({ baseUrl: baseUrl(), key, debug: opts.debug });
1173
+ const check = await api.get("/status");
1174
+ if (!check.ok && check.error.code === "forbidden") {
1175
+ if (opts.json) {
1176
+ printJson(check);
1177
+ } else {
1178
+ console.error(`error: ${check.error.message}`);
1179
+ }
1180
+ return 2;
1181
+ }
847
1182
  const where = await keychainSet(key);
848
1183
  if (where === "file") {
849
1184
  console.error("warning: keychain unavailable; key stored in ~/.muxo/key (chmod 600)");
850
1185
  }
1186
+ if (opts.json) {
1187
+ printJson({
1188
+ ok: true,
1189
+ data: { stored: where, validated: check.ok },
1190
+ usage: { credits: 0, provider: "muxo", latency_ms: 0 }
1191
+ });
1192
+ return 0;
1193
+ }
851
1194
  info(where === "keychain" ? "key stored in OS keychain" : "key stored in ~/.muxo/key", opts);
1195
+ if (!check.ok) {
1196
+ console.error(`warning: could not verify key against the runtime (${check.error.message}) \u2014 stored anyway`);
1197
+ } else {
1198
+ info("key verified against the runtime", opts);
1199
+ }
852
1200
  nextSteps(["muxo status"], opts);
853
1201
  return 0;
854
1202
  }
@@ -867,13 +1215,29 @@ var STATIC_TOOLS = [
867
1215
  { name: "llm_tts", description: "Text to speech. Inputs: text (required), voice?." },
868
1216
  { name: "compute_sandbox", description: "Run code in a sandbox. Inputs: code (required), runtime?, timeout?." },
869
1217
  { name: "kv_store", description: "Key-value store. Inputs: key (required), value (to write) or get (to read)." },
1218
+ { name: "db_write", description: "Write rows to a stack database. Inputs: into (stack address, required), rows (required)." },
1219
+ { name: "db_query", description: "Query a stack database. Inputs: sql (required)." },
1220
+ { name: "dns_manage", description: "Manage DNS records for a domain. Inputs: domain (required), records." },
870
1221
  { name: "storage_object", description: "Object storage. Inputs: key (required), file (write) or get (read)." },
871
1222
  { name: "vector_search", description: "Vector search. Inputs: query (required), k?." },
872
1223
  { name: "email_send", description: "Send an email. Inputs: to, subject, body (all required)." },
873
1224
  { name: "observability_log", description: "Log an event. Inputs: level (required), message (required)." },
874
- { name: "queue_emit", description: "Emit an event to a queue. Inputs: topic (required), payload (required)." }
1225
+ { name: "queue_emit", description: "Emit an event to a queue. Inputs: topic (required), payload (required)." },
1226
+ { name: "llm_image", description: "Generate an image from a prompt. Inputs: prompt (required), size?, model?." },
1227
+ { name: "llm_stt", description: "Transcribe audio to text. Inputs: audio_url or audio_base64 (required), model?." },
1228
+ { name: "sms_send", description: "Send an SMS. Inputs: to (required), body (required), from?." },
1229
+ { name: "analytics_track", description: "Track an analytics event. Inputs: event (required), distinct_id?, properties?." },
1230
+ { name: "appsearch_query", description: "Full-text search an app index. Inputs: query (required), index?, limit?." },
1231
+ { name: "memory_store", description: "Store a memory. Inputs: content (required), metadata?." },
1232
+ { name: "memory_search", description: "Search stored memories. Inputs: query (required), k?." },
1233
+ { name: "featureflags_evaluate", description: "Evaluate a feature flag. Inputs: flag (required), context?." },
1234
+ { name: "video_generate", description: "Generate a video from a prompt. Inputs: prompt (required), avatar_id?, voice_id?." },
1235
+ { name: "auth_users", description: "Manage users. Inputs: action (create|get|list|delete), id?, email?, password?, metadata?." },
1236
+ { name: "payments_charge", description: "Accept a payment from an agent (Stripe PaymentIntents / MPP). Inputs: amount (required), currency?, payment_method_types?, payment_method?, confirm?, metadata?." },
1237
+ { name: "payments_refund", description: "Refund a payment. Inputs: payment_intent (required), amount?, reason?." }
875
1238
  ];
876
1239
  function excluded(tool) {
1240
+ if (process.env.MUXO_MCP_EXPOSE_ALL === "true") return false;
877
1241
  return tool.startsWith("deploy_") || tool === "domains_register" || tool === "compute_container";
878
1242
  }
879
1243
  function toToolName(capability) {
@@ -889,10 +1253,10 @@ async function fetchTools(baseUrl2, key) {
889
1253
  const api = makeApi({ baseUrl: baseUrl2, key });
890
1254
  const env = await api.get("/status");
891
1255
  if (!env.ok) return STATIC_TOOLS.map((t) => toolShape(t.name, t.description));
892
- const data = env.data;
893
- if (Array.isArray(data?.tools)) {
1256
+ const data2 = env.data;
1257
+ if (Array.isArray(data2?.tools)) {
894
1258
  const tools = [];
895
- for (const t of data.tools) {
1259
+ for (const t of data2.tools) {
896
1260
  if (typeof t === "string") {
897
1261
  tools.push(toolShape(toToolName(t), ""));
898
1262
  continue;
@@ -908,15 +1272,15 @@ async function fetchTools(baseUrl2, key) {
908
1272
  const filtered = tools.filter((t) => !excluded(t.name));
909
1273
  return filtered.length > 0 ? filtered : STATIC_TOOLS.map((t) => toolShape(t.name, t.description));
910
1274
  }
911
- if (Array.isArray(data?.capabilities)) {
912
- const tools = data.capabilities.filter((c) => typeof c === "string").map((c) => toolShape(toToolName(c), "")).filter((t) => !excluded(t.name));
1275
+ if (Array.isArray(data2?.capabilities)) {
1276
+ const tools = data2.capabilities.filter((c) => typeof c === "string").map((c) => toolShape(toToolName(c), "")).filter((t) => !excluded(t.name));
913
1277
  return tools.length > 0 ? tools : STATIC_TOOLS.map((t) => toolShape(t.name, t.description));
914
1278
  }
915
1279
  return STATIC_TOOLS.map((t) => toolShape(t.name, t.description));
916
1280
  }
917
- function successResult(data, usage) {
1281
+ function successResult(data2, usage) {
918
1282
  return {
919
- content: [{ type: "text", text: JSON.stringify({ data, usage }) }],
1283
+ content: [{ type: "text", text: JSON.stringify({ data: data2, usage }) }],
920
1284
  isError: false
921
1285
  };
922
1286
  }
@@ -931,7 +1295,7 @@ function errorResult(error) {
931
1295
  };
932
1296
  }
933
1297
  async function mcp(opts) {
934
- const baseUrl2 = opts.local ? LOCAL_BASE_URL : process.env.MUXO_API_BASE || DEFAULT_BASE_URL;
1298
+ const baseUrl2 = process.env.MUXO_API_BASE || DEFAULT_BASE_URL;
935
1299
  const key = await resolveKey();
936
1300
  const api = makeApi({ baseUrl: baseUrl2, key });
937
1301
  let tools = null;
@@ -953,7 +1317,7 @@ async function mcp(opts) {
953
1317
  reply(id, {
954
1318
  protocolVersion: version,
955
1319
  capabilities: { tools: { listChanged: false } },
956
- serverInfo: { name: "muxo", version: "0.1.0" }
1320
+ serverInfo: { name: "muxo", version: "0.1.1" }
957
1321
  });
958
1322
  return;
959
1323
  }
@@ -1001,18 +1365,41 @@ async function mcp(opts) {
1001
1365
  return 0;
1002
1366
  }
1003
1367
 
1368
+ // src/commands/ui.ts
1369
+ async function ui(opts) {
1370
+ if (process.stdout.isTTY !== true) {
1371
+ console.error("error: `muxo ui` needs an interactive terminal (TTY)");
1372
+ return 1;
1373
+ }
1374
+ let mod;
1375
+ try {
1376
+ mod = await import("@muxoai/tui");
1377
+ } catch (err) {
1378
+ const message = err instanceof Error ? err.message : String(err);
1379
+ if (/Cannot find (module|package)|ERR_MODULE_NOT_FOUND/.test(message)) {
1380
+ console.error("error: @muxoai/tui is not installed \u2014 run `npm i -g @muxoai/tui`");
1381
+ return 1;
1382
+ }
1383
+ throw err;
1384
+ }
1385
+ await mod.run({ baseUrl: baseUrl(), key: await resolveKey(), debug: opts.debug });
1386
+ return 0;
1387
+ }
1388
+
1004
1389
  // src/index.ts
1005
1390
  function flags(cmd) {
1006
1391
  cmd.option("--json", "structured JSON output").option("--quiet", "suppress non-essential output").option("--debug", "log requests to stderr");
1007
1392
  }
1008
1393
  async function main() {
1009
1394
  const program = new Command();
1010
- program.name("muxo").description("Muxo CLI \u2014 one key for your whole stack").version("0.1.0").addHelpText("after", `
1395
+ program.name("muxo").description("Muxo CLI \u2014 one key for your whole stack").version("0.1.1").addHelpText("after", `
1011
1396
  Examples:
1012
1397
  muxo init "scrape hacker news every hour and store titles"
1013
1398
  muxo validate && muxo plan && muxo apply
1014
1399
  muxo run hn-scraper --param max_items=3
1400
+ muxo call web.search --data '{"query":"agent infra","limit":3}'
1015
1401
  muxo logs && muxo steps <instanceId>
1402
+ muxo ui # interactive dashboard
1016
1403
 
1017
1404
  Auth: export MUXO_KEY=mk_... Endpoint: export MUXO_API_BASE=https://api.muxo.ai/v1
1018
1405
  Docs: https://api.muxo.ai/docs/quickstart`);
@@ -1047,11 +1434,29 @@ Docs: https://api.muxo.ai/docs/quickstart`);
1047
1434
  runCmd.description("Execute a workflow one-shot").action(async (workflow2, opts) => {
1048
1435
  process.exitCode = await run2(workflow2, opts);
1049
1436
  });
1437
+ const callCmd = program.command("call <capability>");
1438
+ flags(callCmd);
1439
+ callCmd.option("--data <json>", `JSON object of capability inputs, e.g. --data '{"query":"x"}'`).option("--param <key=value>", "input field, e.g. --param limit=5 (repeatable)", (v, prev) => [...prev, v], []);
1440
+ callCmd.description("Invoke a capability directly (web.search, deploy.site, llm.chat, \u2026)").action(
1441
+ async (capability, opts) => {
1442
+ process.exitCode = await call(capability, opts);
1443
+ }
1444
+ );
1445
+ const deployCmd = program.command("deploy [dir]");
1446
+ flags(deployCmd);
1447
+ deployCmd.option("--target <target>", "deployment target, e.g. production").description("Deploy a local directory as a static site (default: public/ or dist/)").action(async (dir, opts) => {
1448
+ process.exitCode = await deploy(dir, opts);
1449
+ });
1050
1450
  const statusCmd = program.command("status");
1051
1451
  flags(statusCmd);
1052
1452
  statusCmd.description("Project health, bundles, budget state").action(async (opts) => {
1053
1453
  process.exitCode = await status(opts);
1054
1454
  });
1455
+ const capabilitiesCmd = program.command("capabilities");
1456
+ flags(capabilitiesCmd);
1457
+ capabilitiesCmd.description("Show which capabilities this deployment can serve").action(async (opts) => {
1458
+ process.exitCode = await capabilities(opts);
1459
+ });
1055
1460
  const keys = program.command("keys");
1056
1461
  flags(keys);
1057
1462
  keys.description("Manage project keys");
@@ -1110,15 +1515,19 @@ Docs: https://api.muxo.ai/docs/quickstart`);
1110
1515
  });
1111
1516
  const loginCmd = program.command("login");
1112
1517
  flags(loginCmd);
1113
- loginCmd.description("Store your muxo key in the OS keychain").action(async (opts) => {
1518
+ loginCmd.description("Store your muxo key in the OS keychain (validated against the runtime)").action(async (opts) => {
1114
1519
  process.exitCode = await login(opts);
1115
1520
  });
1116
1521
  const mcpCmd = program.command("mcp");
1117
1522
  flags(mcpCmd);
1118
- mcpCmd.option("--local", "target local runtime (http://localhost:8787/v1)");
1119
- mcpCmd.description("Stdio MCP proxy wrapping the muxo API (SPEC \xA712)").action(async (opts) => {
1523
+ mcpCmd.description("Stdio MCP proxy wrapping the muxo API").action(async (opts) => {
1120
1524
  process.exitCode = await mcp(opts);
1121
1525
  });
1526
+ const uiCmd = program.command("ui");
1527
+ flags(uiCmd);
1528
+ uiCmd.description("Launch the interactive TUI dashboard (status, capabilities, workflows, runs, keys, credits)").action(async (opts) => {
1529
+ process.exitCode = await ui(opts);
1530
+ });
1122
1531
  await program.parseAsync(process.argv);
1123
1532
  }
1124
1533
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muxoai/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Muxo CLI: one key for your whole stack — scaffold, validate, apply, run, and monitor agent workflows.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,7 +39,8 @@
39
39
  "prepublishOnly": "npm run build"
40
40
  },
41
41
  "dependencies": {
42
- "@muxoai/core": "^0.1.0",
42
+ "@muxoai/core": "^0.1.2",
43
+ "@muxoai/tui": "^0.1.0",
43
44
  "commander": "^12.0.0",
44
45
  "yaml": "^2.4.0",
45
46
  "zod": "^3.23.0"