@muxoai/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/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/index.js +1127 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Muxo AI, LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @muxoai/cli
|
|
2
|
+
|
|
3
|
+
**One key. One file. Your agent's entire toolkit.**
|
|
4
|
+
|
|
5
|
+
The Muxo CLI is a thin client for the [Muxo runtime](https://api.muxo.ai) — a
|
|
6
|
+
declarative `muxo.yaml` manifest plus a managed runtime that gives AI agents and
|
|
7
|
+
developers unified access to web intelligence, LLMs, compute, and 60+ cloud
|
|
8
|
+
services behind a single key and a single bill.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g @muxoai/cli
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quickstart
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
export MUXO_KEY=mk_test_... # free test key, or: muxo login
|
|
20
|
+
export MUXO_API_BASE=https://api.muxo.ai/v1
|
|
21
|
+
|
|
22
|
+
muxo init "scrape hacker news every hour and store titles"
|
|
23
|
+
muxo validate && muxo plan && muxo apply
|
|
24
|
+
muxo run hn-scraper --param max_items=3
|
|
25
|
+
muxo logs && muxo steps <instanceId>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Commands
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
muxo init [intent] Scaffold muxo.yaml (from intent string or interactive)
|
|
32
|
+
muxo validate Schema + catalog validation
|
|
33
|
+
muxo plan Server-side diff → ordered op list
|
|
34
|
+
muxo apply Push manifest, snapshot version
|
|
35
|
+
muxo add <bundle> Append a bundle stanza
|
|
36
|
+
muxo run <workflow> Execute a workflow one-shot
|
|
37
|
+
muxo status Project, bundles, budget
|
|
38
|
+
muxo keys list|create|rotate|revoke
|
|
39
|
+
muxo credits balance|packs|buy <pack>
|
|
40
|
+
muxo rollback [version] Re-apply a previous manifest snapshot
|
|
41
|
+
muxo logs [workflow] Execution history
|
|
42
|
+
muxo steps <instanceId> Per-step log for one execution
|
|
43
|
+
muxo login Store your key in the OS keychain
|
|
44
|
+
muxo mcp Stdio MCP proxy for agents
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Global flags: `--json`, `--quiet`, `--debug`.
|
|
48
|
+
|
|
49
|
+
## Agent-first
|
|
50
|
+
|
|
51
|
+
Point any MCP client at `https://api.muxo.ai/mcp` (Bearer auth), or use the
|
|
52
|
+
stdio proxy: `MUXO_KEY=mk_... muxo mcp`.
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT © Muxo AI, LLC
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,1127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import { access, writeFile } from "node:fs/promises";
|
|
8
|
+
import { basename } from "node:path";
|
|
9
|
+
|
|
10
|
+
// src/lib/templates.ts
|
|
11
|
+
import { stringify } from "yaml";
|
|
12
|
+
var KNOWN_BUNDLES = [
|
|
13
|
+
{ name: "web-intel", capabilities: ["web.search", "web.scrape", "web.extract"] },
|
|
14
|
+
{ name: "web-scale", capabilities: ["web.crawl", "web.monitor"] },
|
|
15
|
+
{ name: "llm", capabilities: ["llm.chat", "llm.embed"] },
|
|
16
|
+
{ name: "voice", capabilities: ["llm.tts"] },
|
|
17
|
+
{ name: "browser", capabilities: ["browser.drive"] },
|
|
18
|
+
{ name: "compute", capabilities: ["compute.sandbox", "compute.container"] },
|
|
19
|
+
{ name: "deploy", capabilities: ["deploy.site", "deploy.container", "deploy.edge"] },
|
|
20
|
+
{ name: "domains", capabilities: ["domains.register", "dns.manage"] },
|
|
21
|
+
{ name: "storage", capabilities: ["storage.object", "vector.search"] },
|
|
22
|
+
{ name: "ops", capabilities: ["observability.log", "queue.emit", "email.send"] }
|
|
23
|
+
];
|
|
24
|
+
function findBundle(name) {
|
|
25
|
+
return KNOWN_BUNDLES.find((b) => b.name === name);
|
|
26
|
+
}
|
|
27
|
+
var webIntel = {
|
|
28
|
+
bundles: { "web-intel": { capabilities: ["web.search", "web.scrape", "web.extract"] } }
|
|
29
|
+
};
|
|
30
|
+
var llm = {
|
|
31
|
+
bundles: { llm: { capabilities: ["llm.chat", "llm.embed"] } }
|
|
32
|
+
};
|
|
33
|
+
var priceRadar = {
|
|
34
|
+
bundles: { "web-intel": { capabilities: ["web.search", "web.scrape", "web.extract"] } },
|
|
35
|
+
workflows: {
|
|
36
|
+
"daily-prices": {
|
|
37
|
+
on: { schedule: "0 6 * * *" },
|
|
38
|
+
steps: [
|
|
39
|
+
{ search: { cap: "web.search", query: "competitor pricing", into: "results" } },
|
|
40
|
+
{ scrape: { cap: "web.scrape", urls: "${results.urls}", into: "pages" } },
|
|
41
|
+
{
|
|
42
|
+
extract: {
|
|
43
|
+
cap: "web.extract",
|
|
44
|
+
from: "pages",
|
|
45
|
+
schema: { product: "str", price: "num" },
|
|
46
|
+
into: "prices"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{ store: { cap: "db.write", into: "stack.database.main.prices" } }
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
stack: {
|
|
54
|
+
database: { main: { provider: "neon", service: "postgres" } }
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function slugify(name) {
|
|
58
|
+
const slug2 = name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
59
|
+
return slug2.length >= 3 ? slug2 : `${slug2}-project`.slice(0, 40);
|
|
60
|
+
}
|
|
61
|
+
function detectSchedule(intent) {
|
|
62
|
+
const lower = intent.toLowerCase();
|
|
63
|
+
if (/\bevery (hour|hr)\b|\bhourly\b/.test(lower)) return "0 * * * *";
|
|
64
|
+
if (/\bevery (\d+) hours?\b/.test(lower)) {
|
|
65
|
+
const n = Math.max(1, Math.min(23, parseInt(/\bevery (\d+) hours?\b/.exec(lower)?.[1] ?? "1", 10)));
|
|
66
|
+
return `0 */${n} * * *`;
|
|
67
|
+
}
|
|
68
|
+
if (/\bevery (\d+) minutes?\b/.test(lower)) {
|
|
69
|
+
const n = Math.max(1, Math.min(59, parseInt(/\bevery (\d+) minutes?\b/.exec(lower)?.[1] ?? "15", 10)));
|
|
70
|
+
return `*/${n} * * * *`;
|
|
71
|
+
}
|
|
72
|
+
if (/\bdaily\b|\bevery day\b|\b(?:each|every) morning\b/.test(lower)) return "0 6 * * *";
|
|
73
|
+
if (/\bweekly\b|\bevery week\b/.test(lower)) return "0 6 * * 1";
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
function matchTemplate(intent) {
|
|
77
|
+
const lower = intent.toLowerCase();
|
|
78
|
+
const bundles = {};
|
|
79
|
+
let workflows;
|
|
80
|
+
let stack;
|
|
81
|
+
let schedule = detectSchedule(intent);
|
|
82
|
+
if (/\bprice|pricing\b/.test(lower)) {
|
|
83
|
+
return priceRadar;
|
|
84
|
+
}
|
|
85
|
+
if (/track|monitor|scrape|search|extract|crawl|web|news|hacker|hn/.test(lower)) {
|
|
86
|
+
Object.assign(bundles, webIntel.bundles);
|
|
87
|
+
}
|
|
88
|
+
if (/chat|llm|summar|embed|\bai\b/.test(lower)) {
|
|
89
|
+
Object.assign(bundles, llm.bundles);
|
|
90
|
+
}
|
|
91
|
+
if (/store|database|db|postgres|save\b/.test(lower)) {
|
|
92
|
+
stack = { database: { main: { provider: "neon", service: "postgres" } } };
|
|
93
|
+
}
|
|
94
|
+
if (schedule !== void 0 && Object.keys(bundles).length > 0) {
|
|
95
|
+
const topic = (lower.match(/[a-z][a-z-]{3,}/)?.[0] ?? "watch").slice(0, 20);
|
|
96
|
+
workflows = {
|
|
97
|
+
[`${topic.replace(/-+$/, "")}-watch`]: {
|
|
98
|
+
on: { schedule },
|
|
99
|
+
steps: [
|
|
100
|
+
{ 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" } }
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (Object.keys(bundles).length === 0) return { bundles: {} };
|
|
107
|
+
return { bundles, workflows, stack };
|
|
108
|
+
}
|
|
109
|
+
function renderManifest(project, description, tpl) {
|
|
110
|
+
const doc = {
|
|
111
|
+
muxo: "0.1",
|
|
112
|
+
project,
|
|
113
|
+
...description ? { description } : {}
|
|
114
|
+
};
|
|
115
|
+
if (tpl.stack && Object.keys(tpl.stack).length > 0) doc.stack = tpl.stack;
|
|
116
|
+
if (Object.keys(tpl.bundles).length > 0) doc.bundles = tpl.bundles;
|
|
117
|
+
if (tpl.workflows) doc.workflows = tpl.workflows;
|
|
118
|
+
doc.budget = { monthly_credits: 1e3, hard_stop: true };
|
|
119
|
+
return stringify(doc);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/lib/output.ts
|
|
123
|
+
function printJson(value) {
|
|
124
|
+
console.log(JSON.stringify(value, null, 2));
|
|
125
|
+
}
|
|
126
|
+
function exitCodeFor(env) {
|
|
127
|
+
if (env.ok) return 0;
|
|
128
|
+
return env.error.code === "forbidden" ? 2 : 1;
|
|
129
|
+
}
|
|
130
|
+
function printError(env) {
|
|
131
|
+
if (env.ok) return;
|
|
132
|
+
console.error(`error: ${env.error.code}: ${env.error.message}`);
|
|
133
|
+
if (env.error.retry_after_ms !== void 0) {
|
|
134
|
+
console.error(`retry after ${env.error.retry_after_ms}ms`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function humanData(env) {
|
|
138
|
+
if (!env.ok) return;
|
|
139
|
+
if (typeof env.data === "string") {
|
|
140
|
+
console.log(env.data);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
printJson(env.data);
|
|
144
|
+
}
|
|
145
|
+
function renderEnvelope(env, json, human) {
|
|
146
|
+
if (json) {
|
|
147
|
+
printJson(env);
|
|
148
|
+
return exitCodeFor(env);
|
|
149
|
+
}
|
|
150
|
+
if (!env.ok) {
|
|
151
|
+
printError(env);
|
|
152
|
+
return exitCodeFor(env);
|
|
153
|
+
}
|
|
154
|
+
if (human) {
|
|
155
|
+
human(env.data);
|
|
156
|
+
} else {
|
|
157
|
+
humanData(env);
|
|
158
|
+
}
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
function info(msg, opts) {
|
|
162
|
+
if (opts.json || opts.quiet) return;
|
|
163
|
+
console.log(msg);
|
|
164
|
+
}
|
|
165
|
+
function nextSteps(msgs, opts) {
|
|
166
|
+
if (opts.json || opts.quiet) return;
|
|
167
|
+
console.error("Next steps:");
|
|
168
|
+
for (const m of msgs) console.error(` ${m}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/commands/init.ts
|
|
172
|
+
async function exists(path) {
|
|
173
|
+
try {
|
|
174
|
+
await access(path);
|
|
175
|
+
return true;
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function init(intent, opts) {
|
|
181
|
+
if (await exists("muxo.yaml")) {
|
|
182
|
+
console.error("error: muxo.yaml already exists in this directory");
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
const it = intent ?? "";
|
|
186
|
+
const tpl = matchTemplate(it);
|
|
187
|
+
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") {
|
|
210
|
+
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;
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const key = (await readFile(KEY_FILE, "utf8")).trim();
|
|
226
|
+
return key || void 0;
|
|
227
|
+
} catch {
|
|
228
|
+
return void 0;
|
|
229
|
+
}
|
|
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
|
+
}
|
|
247
|
+
}
|
|
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
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return void 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/commands/validate.ts
|
|
279
|
+
import { validateManifest as coreValidate } from "@muxoai/core";
|
|
280
|
+
|
|
281
|
+
// src/lib/validate.ts
|
|
282
|
+
import { parse as parseYaml } from "yaml";
|
|
283
|
+
import { z } from "zod";
|
|
284
|
+
var slug = z.string().regex(/^[a-z0-9-]{3,40}$/);
|
|
285
|
+
var stackEntry = z.object({
|
|
286
|
+
provider: z.string().min(1),
|
|
287
|
+
service: z.string().min(1)
|
|
288
|
+
}).passthrough();
|
|
289
|
+
var routing = z.object({
|
|
290
|
+
prefer: z.enum(["cost", "latency", "reliability"]).optional(),
|
|
291
|
+
fallback: z.array(z.string()).optional(),
|
|
292
|
+
locked: z.boolean().optional()
|
|
293
|
+
});
|
|
294
|
+
var bundle = z.object({
|
|
295
|
+
capabilities: z.array(z.string().min(1)).min(1),
|
|
296
|
+
routing: routing.optional()
|
|
297
|
+
});
|
|
298
|
+
var workflow = z.object({
|
|
299
|
+
on: z.object({
|
|
300
|
+
schedule: z.string().optional(),
|
|
301
|
+
event: z.string().optional()
|
|
302
|
+
}).optional(),
|
|
303
|
+
params: z.record(
|
|
304
|
+
z.object({
|
|
305
|
+
type: z.enum(["string", "number", "boolean"]),
|
|
306
|
+
required: z.boolean().optional(),
|
|
307
|
+
default: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
308
|
+
description: z.string().optional()
|
|
309
|
+
})
|
|
310
|
+
).optional(),
|
|
311
|
+
retries: z.number().int().min(0).max(10).optional(),
|
|
312
|
+
timeout_seconds: z.number().int().min(1).max(3600).optional(),
|
|
313
|
+
on_error: z.enum(["halt", "continue"]).optional(),
|
|
314
|
+
steps: z.array(z.record(z.unknown())).optional()
|
|
315
|
+
});
|
|
316
|
+
var manifestSchema = z.object({
|
|
317
|
+
muxo: z.string().regex(/^\d+\.\d+(\.\d+)?$/),
|
|
318
|
+
project: slug,
|
|
319
|
+
description: z.string().optional(),
|
|
320
|
+
env: z.record(
|
|
321
|
+
z.string().regex(/^[A-Z_][A-Z0-9_]*$/, "env keys must be UPPER_SNAKE_CASE"),
|
|
322
|
+
z.union([z.string(), z.number(), z.boolean()])
|
|
323
|
+
).optional(),
|
|
324
|
+
stack: z.record(z.record(stackEntry)).optional(),
|
|
325
|
+
bundles: z.record(bundle).optional(),
|
|
326
|
+
budget: z.object({
|
|
327
|
+
monthly_credits: z.number().int().positive(),
|
|
328
|
+
hard_stop: z.boolean().optional(),
|
|
329
|
+
per_workflow: z.record(z.number().int().positive()).optional()
|
|
330
|
+
}).optional(),
|
|
331
|
+
workflows: z.record(workflow).optional()
|
|
332
|
+
}).strict();
|
|
333
|
+
function validateManifest(text) {
|
|
334
|
+
let doc;
|
|
335
|
+
try {
|
|
336
|
+
doc = parseYaml(text);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
339
|
+
return [{ path: "", message: `yaml parse error: ${message}` }];
|
|
340
|
+
}
|
|
341
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
|
342
|
+
return [{ path: "", message: "manifest must be a YAML mapping" }];
|
|
343
|
+
}
|
|
344
|
+
const issues = [];
|
|
345
|
+
const parsed = manifestSchema.safeParse(doc);
|
|
346
|
+
if (!parsed.success) {
|
|
347
|
+
for (const issue of parsed.error.issues) {
|
|
348
|
+
issues.push({ path: issue.path.join("."), message: issue.message });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const m = doc;
|
|
352
|
+
if (typeof m.muxo === "string") {
|
|
353
|
+
const major = Number.parseInt(m.muxo.split(".")[0] ?? "", 10);
|
|
354
|
+
if (Number.isFinite(major) && major > 0) {
|
|
355
|
+
issues.push({
|
|
356
|
+
path: "muxo",
|
|
357
|
+
message: `unsupported format version "${m.muxo}"; upgrade your muxo client to read v${major} manifests`
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (m.bundles && typeof m.bundles === "object" && !Array.isArray(m.bundles)) {
|
|
362
|
+
const seen = /* @__PURE__ */ new Map();
|
|
363
|
+
for (const [name, b] of Object.entries(m.bundles)) {
|
|
364
|
+
const caps = b?.capabilities;
|
|
365
|
+
if (!Array.isArray(caps)) continue;
|
|
366
|
+
for (const cap of caps) {
|
|
367
|
+
if (typeof cap !== "string") continue;
|
|
368
|
+
const owner = seen.get(cap);
|
|
369
|
+
if (owner) {
|
|
370
|
+
issues.push({
|
|
371
|
+
path: `bundles.${name}.capabilities`,
|
|
372
|
+
message: `capability "${cap}" already declared in bundle "${owner}"`
|
|
373
|
+
});
|
|
374
|
+
} else {
|
|
375
|
+
seen.set(cap, name);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return issues;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/commands/validate.ts
|
|
384
|
+
async function validate(opts) {
|
|
385
|
+
const text = await readManifestText();
|
|
386
|
+
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}`);
|
|
390
|
+
return 1;
|
|
391
|
+
}
|
|
392
|
+
const issues = [
|
|
393
|
+
...coreValidate(text).errors.map((e) => ({ path: e.path, message: e.message + (e.hint ? ` (${e.hint})` : "") })),
|
|
394
|
+
...validateManifest(text)
|
|
395
|
+
];
|
|
396
|
+
if (opts.json) {
|
|
397
|
+
printJson(issues);
|
|
398
|
+
} else if (issues.length === 0) {
|
|
399
|
+
if (!opts.quiet) console.log("muxo.yaml is valid");
|
|
400
|
+
} else {
|
|
401
|
+
for (const issue of issues) {
|
|
402
|
+
console.error(`error: ${issue.path ? `${issue.path}: ` : ""}${issue.message}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return issues.length > 0 ? 1 : 0;
|
|
406
|
+
}
|
|
407
|
+
|
|
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
|
+
// src/commands/plan.ts
|
|
452
|
+
async function plan(opts) {
|
|
453
|
+
const text = await readManifestText();
|
|
454
|
+
if (text === void 0) {
|
|
455
|
+
console.error("error: no muxo.yaml found in this directory");
|
|
456
|
+
return 1;
|
|
457
|
+
}
|
|
458
|
+
const api = makeApi({
|
|
459
|
+
baseUrl: baseUrl(opts.local),
|
|
460
|
+
key: await resolveKey(),
|
|
461
|
+
debug: opts.debug
|
|
462
|
+
});
|
|
463
|
+
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) {
|
|
468
|
+
console.log("no changes \u2014 manifest matches the deployed state");
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const ICONS = {
|
|
472
|
+
provision: "+",
|
|
473
|
+
register_bundle: "+",
|
|
474
|
+
register_workflow: "+",
|
|
475
|
+
upgrade: "~",
|
|
476
|
+
downgrade: "~",
|
|
477
|
+
deprovision: "-",
|
|
478
|
+
noop: "!"
|
|
479
|
+
};
|
|
480
|
+
ops.forEach((op, i) => {
|
|
481
|
+
const icon = ICONS[op.kind] ?? "\xB7";
|
|
482
|
+
const label = op.detail !== void 0 && op.detail !== "" ? `${op.ref}: ${op.detail}` : op.ref;
|
|
483
|
+
console.log(`${i + 1}. ${icon} ${label}`);
|
|
484
|
+
});
|
|
485
|
+
const removes = ops.filter((o) => o.kind === "deprovision").length;
|
|
486
|
+
if (removes > 0) console.log(`
|
|
487
|
+
\u26A0 ${removes} deprovision op(s) \u2014 apply will remove these resources`);
|
|
488
|
+
} else {
|
|
489
|
+
console.log(JSON.stringify(data, null, 2));
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
async function apply(opts) {
|
|
494
|
+
const text = await readManifestText();
|
|
495
|
+
if (text === void 0) {
|
|
496
|
+
console.error("error: no muxo.yaml found in this directory");
|
|
497
|
+
return 1;
|
|
498
|
+
}
|
|
499
|
+
const api = makeApi({
|
|
500
|
+
baseUrl: baseUrl(opts.local),
|
|
501
|
+
key: await resolveKey(),
|
|
502
|
+
debug: opts.debug
|
|
503
|
+
});
|
|
504
|
+
const env = await api.put("/manifest", { yaml: text });
|
|
505
|
+
return renderEnvelope(env, !!opts.json, (data) => {
|
|
506
|
+
const d = data;
|
|
507
|
+
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}`);
|
|
510
|
+
if (Array.isArray(d?.ops)) {
|
|
511
|
+
d.ops.forEach((op, i) => {
|
|
512
|
+
const label = typeof op === "string" ? op : JSON.stringify(op);
|
|
513
|
+
console.log(` ${i + 1}. ${label}`);
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/commands/add.ts
|
|
520
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
521
|
+
import { parse as parseYaml2, stringify as stringify2 } from "yaml";
|
|
522
|
+
async function add(bundleName, opts) {
|
|
523
|
+
const def = findBundle(bundleName);
|
|
524
|
+
if (!def) {
|
|
525
|
+
const known = KNOWN_BUNDLES.map((b) => b.name).join(", ");
|
|
526
|
+
console.error(`error: unknown bundle "${bundleName}" (known: ${known})`);
|
|
527
|
+
return 1;
|
|
528
|
+
}
|
|
529
|
+
let text;
|
|
530
|
+
try {
|
|
531
|
+
text = await readFile3("muxo.yaml", "utf8");
|
|
532
|
+
} catch {
|
|
533
|
+
console.error("error: no muxo.yaml found in this directory");
|
|
534
|
+
return 1;
|
|
535
|
+
}
|
|
536
|
+
let doc;
|
|
537
|
+
try {
|
|
538
|
+
doc = parseYaml2(text);
|
|
539
|
+
} catch (err) {
|
|
540
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
541
|
+
console.error(`error: yaml parse error: ${msg}`);
|
|
542
|
+
return 1;
|
|
543
|
+
}
|
|
544
|
+
if (!doc || typeof doc !== "object") {
|
|
545
|
+
console.error("error: muxo.yaml must be a YAML mapping");
|
|
546
|
+
return 1;
|
|
547
|
+
}
|
|
548
|
+
const bundles = doc.bundles ?? {};
|
|
549
|
+
if (bundles[def.name]) {
|
|
550
|
+
console.error(`error: bundle "${def.name}" already exists in muxo.yaml`);
|
|
551
|
+
return 1;
|
|
552
|
+
}
|
|
553
|
+
bundles[def.name] = { capabilities: def.capabilities };
|
|
554
|
+
doc.bundles = bundles;
|
|
555
|
+
await writeFile3("muxo.yaml", stringify2(doc), "utf8");
|
|
556
|
+
if (!opts.json && !opts.quiet) {
|
|
557
|
+
console.log(`added bundle ${def.name} (${def.capabilities.join(", ")})`);
|
|
558
|
+
}
|
|
559
|
+
return 0;
|
|
560
|
+
}
|
|
561
|
+
|
|
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
|
+
}
|
|
577
|
+
function coerceParam(value) {
|
|
578
|
+
if (value === "true") return true;
|
|
579
|
+
if (value === "false") return false;
|
|
580
|
+
if (value !== "" && !Number.isNaN(Number(value))) return Number(value);
|
|
581
|
+
return value;
|
|
582
|
+
}
|
|
583
|
+
function parseParams(raw) {
|
|
584
|
+
const params = {};
|
|
585
|
+
for (const entry of raw ?? []) {
|
|
586
|
+
const eq = entry.indexOf("=");
|
|
587
|
+
if (eq === -1) throw new Error(`--param expects key=value, got "${entry}"`);
|
|
588
|
+
params[entry.slice(0, eq)] = coerceParam(entry.slice(eq + 1));
|
|
589
|
+
}
|
|
590
|
+
return params;
|
|
591
|
+
}
|
|
592
|
+
async function run2(workflow2, opts) {
|
|
593
|
+
const api = makeApi({
|
|
594
|
+
baseUrl: baseUrl(opts.local),
|
|
595
|
+
key: await resolveKey(),
|
|
596
|
+
debug: opts.debug
|
|
597
|
+
});
|
|
598
|
+
const params = parseParams(opts.param);
|
|
599
|
+
const env = await api.post(`/workflows/${workflow2}/run`, Object.keys(params).length > 0 ? { params } : {});
|
|
600
|
+
const instanceId = (env.ok ? env.data : void 0)?.instanceId;
|
|
601
|
+
const result = renderEnvelope(env, !!opts.json, printSteps);
|
|
602
|
+
if (!opts.json && typeof instanceId === "string" && instanceId.length > 0) {
|
|
603
|
+
for (let i = 0; i < 30; i++) {
|
|
604
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
605
|
+
const execs = await api.get(`/workflows/${encodeURIComponent(workflow2)}/executions`);
|
|
606
|
+
const row = execs.data?.executions?.find((e) => e.id === instanceId);
|
|
607
|
+
if (row === void 0 || row.status !== "running") break;
|
|
608
|
+
}
|
|
609
|
+
const steps = await api.get(`/executions/${encodeURIComponent(instanceId)}/steps`);
|
|
610
|
+
const rows = steps.data?.steps ?? [];
|
|
611
|
+
if (rows.length > 0) {
|
|
612
|
+
console.log("steps:");
|
|
613
|
+
for (const s of rows) {
|
|
614
|
+
const mark = s.status === "ok" ? "\u2713" : "\u2717";
|
|
615
|
+
const err = s.error ? ` \u2014 ${s.error}` : "";
|
|
616
|
+
console.log(` ${mark} ${s.step} (${s.capability}) \u2192 ${s.provider}, ${s.credits} credits, ${s.latency_ms}ms${err}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return result;
|
|
621
|
+
}
|
|
622
|
+
async function status(opts) {
|
|
623
|
+
const api = makeApi({
|
|
624
|
+
baseUrl: baseUrl(opts.local),
|
|
625
|
+
key: await resolveKey(),
|
|
626
|
+
debug: opts.debug
|
|
627
|
+
});
|
|
628
|
+
const env = await api.get("/status");
|
|
629
|
+
return renderEnvelope(env, !!opts.json, (data) => {
|
|
630
|
+
const d = data;
|
|
631
|
+
if (d && typeof d === "object" && !Array.isArray(d)) {
|
|
632
|
+
if (d.project !== void 0) console.log(`project: ${String(d.project)}`);
|
|
633
|
+
if (d.version !== void 0) console.log(`version: ${String(d.version)}`);
|
|
634
|
+
if (d.bundles !== void 0) {
|
|
635
|
+
const bundles = Array.isArray(d.bundles) ? d.bundles.map(String).join(", ") : Object.keys(d.bundles).join(", ");
|
|
636
|
+
console.log(`bundles: ${bundles || "(none)"}`);
|
|
637
|
+
}
|
|
638
|
+
if (Array.isArray(d.workflows)) console.log(`workflows: ${d.workflows.map(String).join(", ") || "(none)"}`);
|
|
639
|
+
if (d.budget !== void 0) console.log(`budget: ${JSON.stringify(d.budget)}`);
|
|
640
|
+
if (d.drift !== void 0) console.log(`drift: ${JSON.stringify(d.drift)}`);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
console.log(JSON.stringify(data, null, 2));
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
async function rollback(version, opts) {
|
|
647
|
+
const api = makeApi({
|
|
648
|
+
baseUrl: baseUrl(opts.local),
|
|
649
|
+
key: await resolveKey(),
|
|
650
|
+
debug: opts.debug
|
|
651
|
+
});
|
|
652
|
+
const body = version === void 0 ? {} : { version: Number(version) };
|
|
653
|
+
const env = await api.post("/rollback", body);
|
|
654
|
+
return renderEnvelope(env, !!opts.json, (data) => {
|
|
655
|
+
console.log(`rolled back to ${JSON.stringify(data)}`);
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
async function logs(workflow2, opts) {
|
|
659
|
+
const api = makeApi({
|
|
660
|
+
baseUrl: baseUrl(opts.local),
|
|
661
|
+
key: await resolveKey(),
|
|
662
|
+
debug: opts.debug
|
|
663
|
+
});
|
|
664
|
+
const qs = workflow2 ? `?workflow=${encodeURIComponent(workflow2)}` : "";
|
|
665
|
+
const env = await api.get(`/workflows/${encodeURIComponent(workflow2 ?? "_all")}/executions${qs}`);
|
|
666
|
+
return renderEnvelope(env, !!opts.json, (data) => {
|
|
667
|
+
const d = data;
|
|
668
|
+
if (Array.isArray(d?.executions)) {
|
|
669
|
+
for (const e of d.executions) {
|
|
670
|
+
const when = new Date(e.started_at).toISOString().slice(0, 19).replace("T", " ");
|
|
671
|
+
const err = e.error ? ` \u2014 ${e.error}` : "";
|
|
672
|
+
console.log(`${when} ${e.workflow} ${e.status}${err}`);
|
|
673
|
+
console.log(` steps: muxo steps ${e.id}`);
|
|
674
|
+
}
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
console.log(JSON.stringify(data, null, 2));
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
async function stepLogs(instanceId, opts) {
|
|
681
|
+
const api = makeApi({
|
|
682
|
+
baseUrl: baseUrl(opts.local),
|
|
683
|
+
key: await resolveKey(),
|
|
684
|
+
debug: opts.debug
|
|
685
|
+
});
|
|
686
|
+
const env = await api.get(`/executions/${encodeURIComponent(instanceId)}/steps`);
|
|
687
|
+
return renderEnvelope(env, !!opts.json, (data) => {
|
|
688
|
+
const rows = data?.steps ?? [];
|
|
689
|
+
for (const s of rows) {
|
|
690
|
+
const mark = s.status === "ok" ? "\u2713" : "\u2717";
|
|
691
|
+
const err = s.error ? ` \u2014 ${s.error}` : "";
|
|
692
|
+
console.log(`${s.seq}. ${mark} ${s.step} (${s.capability}) \u2192 ${s.provider}, ${s.credits} credits, ${s.latency_ms}ms${err}`);
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/commands/keys.ts
|
|
698
|
+
async function keysCommand(action, id, opts) {
|
|
699
|
+
const api = makeApi({
|
|
700
|
+
baseUrl: baseUrl(opts.local),
|
|
701
|
+
key: await resolveKey(),
|
|
702
|
+
debug: opts.debug
|
|
703
|
+
});
|
|
704
|
+
if (action === "revoke" && !id) {
|
|
705
|
+
console.error("error: revoke requires a key id");
|
|
706
|
+
return 1;
|
|
707
|
+
}
|
|
708
|
+
if (action === "rotate" && !id) {
|
|
709
|
+
const listed = await api.get("/keys");
|
|
710
|
+
if (!listed.ok) return renderEnvelope(listed, !!opts.json);
|
|
711
|
+
const keys = listed.data ?? [];
|
|
712
|
+
const active = keys.find((k) => k?.revoked !== true);
|
|
713
|
+
if (active === void 0 || typeof active.id !== "string") {
|
|
714
|
+
console.error("error: no active key to rotate");
|
|
715
|
+
return 1;
|
|
716
|
+
}
|
|
717
|
+
id = active.id;
|
|
718
|
+
}
|
|
719
|
+
let env;
|
|
720
|
+
switch (action) {
|
|
721
|
+
case "list":
|
|
722
|
+
env = await api.get("/keys");
|
|
723
|
+
break;
|
|
724
|
+
case "create":
|
|
725
|
+
env = await api.post("/keys");
|
|
726
|
+
break;
|
|
727
|
+
case "rotate":
|
|
728
|
+
env = await api.post(`/keys/${id}/rotate`);
|
|
729
|
+
break;
|
|
730
|
+
case "revoke":
|
|
731
|
+
env = await api.del(`/keys/${id}`);
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
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");
|
|
741
|
+
return 0;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return renderEnvelope(env, !!opts.json, (data) => {
|
|
745
|
+
if (Array.isArray(data)) {
|
|
746
|
+
for (const key of data) {
|
|
747
|
+
const k = key;
|
|
748
|
+
console.log(`${String(k?.id ?? "?")}${k?.revoked ? " (revoked)" : ""}`);
|
|
749
|
+
}
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
printJson(data);
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/commands/credits.ts
|
|
757
|
+
async function creditsCommand(action, packId, opts) {
|
|
758
|
+
const api = makeApi({
|
|
759
|
+
baseUrl: baseUrl(opts.local),
|
|
760
|
+
key: await resolveKey(),
|
|
761
|
+
debug: opts.debug
|
|
762
|
+
});
|
|
763
|
+
if (action === "buy") {
|
|
764
|
+
if (!packId) {
|
|
765
|
+
console.error("error: buy requires a pack id (muxo credits packs)");
|
|
766
|
+
return 1;
|
|
767
|
+
}
|
|
768
|
+
const env2 = await api.post("/credits/checkout", { pack: packId });
|
|
769
|
+
return renderEnvelope(env2, !!opts.json, (data) => {
|
|
770
|
+
const d = data;
|
|
771
|
+
if (d?.url !== void 0) {
|
|
772
|
+
console.log(`checkout: ${d.url}`);
|
|
773
|
+
if (d.stubbed) console.log("(stub \u2014 set STRIPE_SECRET_KEY on the runtime for real checkout)");
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
const env = await api.get("/credits");
|
|
778
|
+
return renderEnvelope(env, !!opts.json, (data) => {
|
|
779
|
+
const d = data;
|
|
780
|
+
if (action === "packs") {
|
|
781
|
+
for (const p of d?.packs ?? []) {
|
|
782
|
+
console.log(`${p.id} ${p.credits} credits $${(p.price_cents / 100).toFixed(2)}`);
|
|
783
|
+
}
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
console.log(`account: ${d?.account_id ?? "?"}`);
|
|
787
|
+
console.log(`balance: ${d?.balance_credits ?? 0} muxo credits`);
|
|
788
|
+
if (Array.isArray(d?.ledger) && d.ledger.length > 0) {
|
|
789
|
+
console.log("recent:");
|
|
790
|
+
for (const entry of d.ledger.slice(0, 10)) {
|
|
791
|
+
const when = new Date(entry.created_at).toISOString().slice(0, 19).replace("T", " ");
|
|
792
|
+
console.log(` ${when} ${entry.delta > 0 ? "+" : ""}${entry.delta} ${entry.reason}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/commands/login.ts
|
|
799
|
+
import { createInterface } from "node:readline";
|
|
800
|
+
function promptHidden(promptText) {
|
|
801
|
+
return new Promise((resolve) => {
|
|
802
|
+
if (!process.stdin.isTTY) {
|
|
803
|
+
const rl = createInterface({ input: process.stdin });
|
|
804
|
+
rl.once("line", (line) => {
|
|
805
|
+
rl.close();
|
|
806
|
+
resolve(line.trim());
|
|
807
|
+
});
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
process.stdout.write(promptText);
|
|
811
|
+
const wasRaw = process.stdin.isRaw ?? false;
|
|
812
|
+
process.stdin.setRawMode(true);
|
|
813
|
+
process.stdin.resume();
|
|
814
|
+
let buf = "";
|
|
815
|
+
const onData = (chunk) => {
|
|
816
|
+
for (const ch of chunk.toString("utf8")) {
|
|
817
|
+
if (ch === "\r" || ch === "\n") {
|
|
818
|
+
process.stdin.setRawMode(wasRaw);
|
|
819
|
+
process.stdin.pause();
|
|
820
|
+
process.stdin.removeListener("data", onData);
|
|
821
|
+
process.stdout.write("\n");
|
|
822
|
+
resolve(buf);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (ch === "\x7F" || ch === "\b") {
|
|
826
|
+
buf = buf.slice(0, -1);
|
|
827
|
+
} else if (ch === "") {
|
|
828
|
+
process.stdin.setRawMode(wasRaw);
|
|
829
|
+
process.stdin.pause();
|
|
830
|
+
process.stdin.removeListener("data", onData);
|
|
831
|
+
process.stdout.write("\n");
|
|
832
|
+
process.exit(130);
|
|
833
|
+
} else {
|
|
834
|
+
buf += ch;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
process.stdin.on("data", onData);
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
async function login(opts) {
|
|
842
|
+
const key = await promptHidden("Paste your muxo key: ");
|
|
843
|
+
if (!key) {
|
|
844
|
+
console.error("error: no key provided");
|
|
845
|
+
return 1;
|
|
846
|
+
}
|
|
847
|
+
const where = await keychainSet(key);
|
|
848
|
+
if (where === "file") {
|
|
849
|
+
console.error("warning: keychain unavailable; key stored in ~/.muxo/key (chmod 600)");
|
|
850
|
+
}
|
|
851
|
+
info(where === "keychain" ? "key stored in OS keychain" : "key stored in ~/.muxo/key", opts);
|
|
852
|
+
nextSteps(["muxo status"], opts);
|
|
853
|
+
return 0;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// src/commands/mcp.ts
|
|
857
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
858
|
+
var STATIC_TOOLS = [
|
|
859
|
+
{ name: "web_search", description: "Search the web. Inputs: query (required), limit?, freshness?." },
|
|
860
|
+
{ name: "web_scrape", description: "Scrape URL(s) to markdown. Inputs: url or urls, formats?." },
|
|
861
|
+
{ name: "web_extract", description: "Extract structured data from pages. Inputs: from (required), schema (required)." },
|
|
862
|
+
{ name: "web_crawl", description: "Crawl a site. Inputs: url (required), depth?, limit?." },
|
|
863
|
+
{ name: "web_monitor", description: "Poll a URL for change. Inputs: url (required), on: change." },
|
|
864
|
+
{ name: "browser_drive", description: "Drive a browser session. Inputs: session (required), actions (required)." },
|
|
865
|
+
{ name: "llm_chat", description: "Chat completion. Inputs: prompt (required), model?, context?." },
|
|
866
|
+
{ name: "llm_embed", description: "Generate embeddings. Inputs: input (required)." },
|
|
867
|
+
{ name: "llm_tts", description: "Text to speech. Inputs: text (required), voice?." },
|
|
868
|
+
{ name: "compute_sandbox", description: "Run code in a sandbox. Inputs: code (required), runtime?, timeout?." },
|
|
869
|
+
{ name: "kv_store", description: "Key-value store. Inputs: key (required), value (to write) or get (to read)." },
|
|
870
|
+
{ name: "storage_object", description: "Object storage. Inputs: key (required), file (write) or get (read)." },
|
|
871
|
+
{ name: "vector_search", description: "Vector search. Inputs: query (required), k?." },
|
|
872
|
+
{ name: "email_send", description: "Send an email. Inputs: to, subject, body (all required)." },
|
|
873
|
+
{ 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)." }
|
|
875
|
+
];
|
|
876
|
+
function excluded(tool) {
|
|
877
|
+
return tool.startsWith("deploy_") || tool === "domains_register" || tool === "compute_container";
|
|
878
|
+
}
|
|
879
|
+
function toToolName(capability) {
|
|
880
|
+
return capability.replace(/\./g, "_");
|
|
881
|
+
}
|
|
882
|
+
function toCapability(tool) {
|
|
883
|
+
return tool.replace(/_/g, ".");
|
|
884
|
+
}
|
|
885
|
+
function toolShape(name, description) {
|
|
886
|
+
return { name, description, inputSchema: { type: "object" } };
|
|
887
|
+
}
|
|
888
|
+
async function fetchTools(baseUrl2, key) {
|
|
889
|
+
const api = makeApi({ baseUrl: baseUrl2, key });
|
|
890
|
+
const env = await api.get("/status");
|
|
891
|
+
if (!env.ok) return STATIC_TOOLS.map((t) => toolShape(t.name, t.description));
|
|
892
|
+
const data = env.data;
|
|
893
|
+
if (Array.isArray(data?.tools)) {
|
|
894
|
+
const tools = [];
|
|
895
|
+
for (const t of data.tools) {
|
|
896
|
+
if (typeof t === "string") {
|
|
897
|
+
tools.push(toolShape(toToolName(t), ""));
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
if (t !== null && typeof t === "object") {
|
|
901
|
+
const obj = t;
|
|
902
|
+
if (typeof obj.name !== "string") continue;
|
|
903
|
+
const name = obj.name.includes(".") ? toToolName(obj.name) : obj.name;
|
|
904
|
+
const description = typeof obj.description === "string" ? obj.description : "";
|
|
905
|
+
tools.push(toolShape(name, description));
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
const filtered = tools.filter((t) => !excluded(t.name));
|
|
909
|
+
return filtered.length > 0 ? filtered : STATIC_TOOLS.map((t) => toolShape(t.name, t.description));
|
|
910
|
+
}
|
|
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));
|
|
913
|
+
return tools.length > 0 ? tools : STATIC_TOOLS.map((t) => toolShape(t.name, t.description));
|
|
914
|
+
}
|
|
915
|
+
return STATIC_TOOLS.map((t) => toolShape(t.name, t.description));
|
|
916
|
+
}
|
|
917
|
+
function successResult(data, usage) {
|
|
918
|
+
return {
|
|
919
|
+
content: [{ type: "text", text: JSON.stringify({ data, usage }) }],
|
|
920
|
+
isError: false
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
function errorResult(error) {
|
|
924
|
+
const payload = {};
|
|
925
|
+
if (typeof error.code === "string") payload.code = error.code;
|
|
926
|
+
if (typeof error.message === "string") payload.message = error.message;
|
|
927
|
+
if (error.retry_after_ms !== void 0) payload.retry_after_ms = error.retry_after_ms;
|
|
928
|
+
return {
|
|
929
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
930
|
+
isError: true
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
async function mcp(opts) {
|
|
934
|
+
const baseUrl2 = opts.local ? LOCAL_BASE_URL : process.env.MUXO_API_BASE || DEFAULT_BASE_URL;
|
|
935
|
+
const key = await resolveKey();
|
|
936
|
+
const api = makeApi({ baseUrl: baseUrl2, key });
|
|
937
|
+
let tools = null;
|
|
938
|
+
const write = (msg) => process.stdout.write(`${JSON.stringify(msg)}
|
|
939
|
+
`);
|
|
940
|
+
const reply = (id, result) => write({ jsonrpc: "2.0", id, result });
|
|
941
|
+
const rpcError = (id, code, message) => write({ jsonrpc: "2.0", id, error: { code, message } });
|
|
942
|
+
async function handle(msg) {
|
|
943
|
+
const { id, method } = msg;
|
|
944
|
+
if (!method) {
|
|
945
|
+
if (id !== void 0) rpcError(id, -32600, "Invalid Request");
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (method.startsWith("notifications/")) return;
|
|
949
|
+
switch (method) {
|
|
950
|
+
case "initialize": {
|
|
951
|
+
const params = msg.params ?? {};
|
|
952
|
+
const version = typeof params.protocolVersion === "string" ? params.protocolVersion : "2024-11-05";
|
|
953
|
+
reply(id, {
|
|
954
|
+
protocolVersion: version,
|
|
955
|
+
capabilities: { tools: { listChanged: false } },
|
|
956
|
+
serverInfo: { name: "muxo", version: "0.1.0" }
|
|
957
|
+
});
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
case "ping": {
|
|
961
|
+
reply(id, {});
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
case "tools/list": {
|
|
965
|
+
if (!tools) tools = await fetchTools(baseUrl2, key);
|
|
966
|
+
reply(id, { tools });
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
case "tools/call": {
|
|
970
|
+
const params = msg.params ?? {};
|
|
971
|
+
if (typeof params.name !== "string") {
|
|
972
|
+
rpcError(id, -32602, "Invalid params: name is required");
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const capability = toCapability(params.name);
|
|
976
|
+
const input = params.arguments ?? {};
|
|
977
|
+
const env = await api.post(`/capabilities/${capability}`, input);
|
|
978
|
+
reply(id, env.ok ? successResult(env.data, env.usage) : errorResult(env.error));
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
default:
|
|
982
|
+
rpcError(id, -32601, `Method not found: ${method}`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const rl = createInterface2({ input: process.stdin, terminal: false });
|
|
986
|
+
const pending = [];
|
|
987
|
+
rl.on("line", (line) => {
|
|
988
|
+
const trimmed = line.trim();
|
|
989
|
+
if (!trimmed) return;
|
|
990
|
+
let msg;
|
|
991
|
+
try {
|
|
992
|
+
msg = JSON.parse(trimmed);
|
|
993
|
+
} catch {
|
|
994
|
+
rpcError(null, -32700, "Parse error");
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
pending.push(handle(msg));
|
|
998
|
+
});
|
|
999
|
+
await new Promise((resolve) => rl.once("close", resolve));
|
|
1000
|
+
await Promise.all(pending);
|
|
1001
|
+
return 0;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/index.ts
|
|
1005
|
+
function flags(cmd) {
|
|
1006
|
+
cmd.option("--json", "structured JSON output").option("--quiet", "suppress non-essential output").option("--debug", "log requests to stderr");
|
|
1007
|
+
}
|
|
1008
|
+
async function main() {
|
|
1009
|
+
const program = new Command();
|
|
1010
|
+
program.name("muxo").description("Muxo CLI \u2014 one key for your whole stack").version("0.1.0").addHelpText("after", `
|
|
1011
|
+
Examples:
|
|
1012
|
+
muxo init "scrape hacker news every hour and store titles"
|
|
1013
|
+
muxo validate && muxo plan && muxo apply
|
|
1014
|
+
muxo run hn-scraper --param max_items=3
|
|
1015
|
+
muxo logs && muxo steps <instanceId>
|
|
1016
|
+
|
|
1017
|
+
Auth: export MUXO_KEY=mk_... Endpoint: export MUXO_API_BASE=https://api.muxo.ai/v1
|
|
1018
|
+
Docs: https://api.muxo.ai/docs/quickstart`);
|
|
1019
|
+
const initCmd = program.command("init [intent]");
|
|
1020
|
+
flags(initCmd);
|
|
1021
|
+
initCmd.description("Scaffold muxo.yaml from an intent string").action(async (intent, opts) => {
|
|
1022
|
+
process.exitCode = await init(intent, opts);
|
|
1023
|
+
});
|
|
1024
|
+
const validateCmd = program.command("validate");
|
|
1025
|
+
flags(validateCmd);
|
|
1026
|
+
validateCmd.description("Validate muxo.yaml locally (structure, slug, budget)").action(async (opts) => {
|
|
1027
|
+
process.exitCode = await validate(opts);
|
|
1028
|
+
});
|
|
1029
|
+
const planCmd = program.command("plan");
|
|
1030
|
+
flags(planCmd);
|
|
1031
|
+
planCmd.description("Server-side diff of muxo.yaml \u2192 ordered op list").action(async (opts) => {
|
|
1032
|
+
process.exitCode = await plan(opts);
|
|
1033
|
+
});
|
|
1034
|
+
const applyCmd = program.command("apply");
|
|
1035
|
+
flags(applyCmd);
|
|
1036
|
+
applyCmd.description("Push manifest, execute plan, snapshot version").action(async (opts) => {
|
|
1037
|
+
process.exitCode = await apply(opts);
|
|
1038
|
+
});
|
|
1039
|
+
const addCmd = program.command("add <bundle>");
|
|
1040
|
+
flags(addCmd);
|
|
1041
|
+
addCmd.description("Append a bundle stanza to muxo.yaml").action(async (bundle2, opts) => {
|
|
1042
|
+
process.exitCode = await add(bundle2, opts);
|
|
1043
|
+
});
|
|
1044
|
+
const runCmd = program.command("run <workflow>");
|
|
1045
|
+
flags(runCmd);
|
|
1046
|
+
runCmd.option("--param <key=value>", "workflow parameter, e.g. --param count=3 (repeatable)", (v, prev) => [...prev, v], []);
|
|
1047
|
+
runCmd.description("Execute a workflow one-shot").action(async (workflow2, opts) => {
|
|
1048
|
+
process.exitCode = await run2(workflow2, opts);
|
|
1049
|
+
});
|
|
1050
|
+
const statusCmd = program.command("status");
|
|
1051
|
+
flags(statusCmd);
|
|
1052
|
+
statusCmd.description("Project health, bundles, budget state").action(async (opts) => {
|
|
1053
|
+
process.exitCode = await status(opts);
|
|
1054
|
+
});
|
|
1055
|
+
const keys = program.command("keys");
|
|
1056
|
+
flags(keys);
|
|
1057
|
+
keys.description("Manage project keys");
|
|
1058
|
+
const keysList = keys.command("list");
|
|
1059
|
+
flags(keysList);
|
|
1060
|
+
keysList.action(async (opts, cmd) => {
|
|
1061
|
+
process.exitCode = await keysCommand("list", void 0, { ...cmd.parent.opts(), ...opts });
|
|
1062
|
+
});
|
|
1063
|
+
const keysCreate = keys.command("create");
|
|
1064
|
+
flags(keysCreate);
|
|
1065
|
+
keysCreate.action(async (opts, cmd) => {
|
|
1066
|
+
process.exitCode = await keysCommand("create", void 0, { ...cmd.parent.opts(), ...opts });
|
|
1067
|
+
});
|
|
1068
|
+
const keysRotate = keys.command("rotate [id]");
|
|
1069
|
+
flags(keysRotate);
|
|
1070
|
+
keysRotate.action(async (id, opts, cmd) => {
|
|
1071
|
+
process.exitCode = await keysCommand("rotate", id, { ...cmd.parent.opts(), ...opts });
|
|
1072
|
+
});
|
|
1073
|
+
const keysRevoke = keys.command("revoke <id>");
|
|
1074
|
+
flags(keysRevoke);
|
|
1075
|
+
keysRevoke.action(async (id, opts, cmd) => {
|
|
1076
|
+
process.exitCode = await keysCommand("revoke", id, { ...cmd.parent.opts(), ...opts });
|
|
1077
|
+
});
|
|
1078
|
+
const credits = program.command("credits");
|
|
1079
|
+
flags(credits);
|
|
1080
|
+
credits.description("Account credit balance, packs, and top-up");
|
|
1081
|
+
const creditsBalance = credits.command("balance");
|
|
1082
|
+
flags(creditsBalance);
|
|
1083
|
+
creditsBalance.action(async (opts, cmd) => {
|
|
1084
|
+
process.exitCode = await creditsCommand("balance", void 0, { ...cmd.parent.opts(), ...opts });
|
|
1085
|
+
});
|
|
1086
|
+
const creditsPacks = credits.command("packs");
|
|
1087
|
+
flags(creditsPacks);
|
|
1088
|
+
creditsPacks.action(async (opts, cmd) => {
|
|
1089
|
+
process.exitCode = await creditsCommand("packs", void 0, { ...cmd.parent.opts(), ...opts });
|
|
1090
|
+
});
|
|
1091
|
+
const creditsBuy = credits.command("buy <pack>");
|
|
1092
|
+
flags(creditsBuy);
|
|
1093
|
+
creditsBuy.action(async (pack, opts, cmd) => {
|
|
1094
|
+
process.exitCode = await creditsCommand("buy", pack, { ...cmd.parent.opts(), ...opts });
|
|
1095
|
+
});
|
|
1096
|
+
const rollbackCmd = program.command("rollback [version]");
|
|
1097
|
+
flags(rollbackCmd);
|
|
1098
|
+
rollbackCmd.description("Re-apply a previous manifest snapshot").action(async (version, opts) => {
|
|
1099
|
+
process.exitCode = await rollback(version, opts);
|
|
1100
|
+
});
|
|
1101
|
+
const logsCmd = program.command("logs [workflow]");
|
|
1102
|
+
flags(logsCmd);
|
|
1103
|
+
logsCmd.description("Fetch execution logs").action(async (workflow2, opts) => {
|
|
1104
|
+
process.exitCode = await logs(workflow2, opts);
|
|
1105
|
+
});
|
|
1106
|
+
const stepsCmd = program.command("steps <instanceId>");
|
|
1107
|
+
flags(stepsCmd);
|
|
1108
|
+
stepsCmd.description("Per-step log for one execution (step, capability, provider, credits, error)").action(async (instanceId, opts) => {
|
|
1109
|
+
process.exitCode = await stepLogs(instanceId, opts);
|
|
1110
|
+
});
|
|
1111
|
+
const loginCmd = program.command("login");
|
|
1112
|
+
flags(loginCmd);
|
|
1113
|
+
loginCmd.description("Store your muxo key in the OS keychain").action(async (opts) => {
|
|
1114
|
+
process.exitCode = await login(opts);
|
|
1115
|
+
});
|
|
1116
|
+
const mcpCmd = program.command("mcp");
|
|
1117
|
+
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) => {
|
|
1120
|
+
process.exitCode = await mcp(opts);
|
|
1121
|
+
});
|
|
1122
|
+
await program.parseAsync(process.argv);
|
|
1123
|
+
}
|
|
1124
|
+
main().catch((err) => {
|
|
1125
|
+
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1126
|
+
process.exit(1);
|
|
1127
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@muxoai/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Muxo CLI: one key for your whole stack — scaffold, validate, apply, run, and monitor agent workflows.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"muxo": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/muxoai/muxo.git",
|
|
20
|
+
"directory": "packages/cli"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://api.muxo.ai",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/muxoai/muxo/issues"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"muxo",
|
|
28
|
+
"cli",
|
|
29
|
+
"agents",
|
|
30
|
+
"capabilities",
|
|
31
|
+
"ai"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node18 --outfile=dist/index.js --packages=external --log-level=warning",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"prepublishOnly": "npm run build"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@muxoai/core": "^0.1.0",
|
|
43
|
+
"commander": "^12.0.0",
|
|
44
|
+
"yaml": "^2.4.0",
|
|
45
|
+
"zod": "^3.23.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.0.0",
|
|
49
|
+
"esbuild": "^0.21.5",
|
|
50
|
+
"typescript": "^5.6.0"
|
|
51
|
+
}
|
|
52
|
+
}
|