@fluxa-pay/planner 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/package.json +19 -0
- package/planner.mjs +669 -0
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fluxa-pay/planner",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "FluxA Planner CLI — recommend the right models/APIs/skills for a task, and run paid calls via x402.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"planner": "planner.mjs"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"planner.mjs"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT"
|
|
19
|
+
}
|
package/planner.mjs
ADDED
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// planner — FluxA Planner CLI (LIVE — talks to the real platform)
|
|
4
|
+
//
|
|
5
|
+
// Zero-dependency Node ESM (Node 20+). Nothing to install.
|
|
6
|
+
// node cli/planner.mjs <command>
|
|
7
|
+
//
|
|
8
|
+
// Config (env or ~/.fluxa/config.json):
|
|
9
|
+
// FLUXA_KEY fxa_live_… API key (create at monetize.fluxapay.xyz/keys)
|
|
10
|
+
// FLUXA_PLATFORM default https://monetize.fluxapay.xyz (discovery, models)
|
|
11
|
+
// FLUXA_PROXY default https://proxy-monetize.fluxapay.xyz (wallet, llm)
|
|
12
|
+
//
|
|
13
|
+
// The planner RECOMMENDS — it does not execute or pay. `plan` returns the
|
|
14
|
+
// recommended models/APIs/skills + an estimated budget. It stops there: the
|
|
15
|
+
// agent that called planner executes by hitting those endpoints itself, and
|
|
16
|
+
// FluxA settles each paid call. The planner never calls those APIs or spends Units.
|
|
17
|
+
//
|
|
18
|
+
// What's live:
|
|
19
|
+
// search · models · balance · ledger · whoami → real read-only HTTP
|
|
20
|
+
// plan → calls the server-side /api/planner/plan
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
24
|
+
import { execFile } from "node:child_process";
|
|
25
|
+
import { promisify } from "node:util";
|
|
26
|
+
import { homedir } from "node:os";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { stdout, argv, env, exit } from "node:process";
|
|
29
|
+
|
|
30
|
+
const pexec = promisify(execFile);
|
|
31
|
+
|
|
32
|
+
const PLATFORM = (env.FLUXA_PLATFORM || "https://monetize.fluxapay.xyz").replace(/\/$/, "");
|
|
33
|
+
const PROXY = (env.FLUXA_PROXY || "https://proxy-monetize.fluxapay.xyz").replace(/\/$/, "");
|
|
34
|
+
const AGENTID = (env.FLUXA_AGENTID || "https://agentid.fluxapay.xyz").replace(/\/$/, "");
|
|
35
|
+
const CONFIG = join(homedir(), ".fluxa", "config.json");
|
|
36
|
+
const UNIT_USD = 0.00001;
|
|
37
|
+
|
|
38
|
+
// --- ANSI -------------------------------------------------------------------
|
|
39
|
+
const tty = stdout.isTTY;
|
|
40
|
+
const sgr = (n) => (s) => (tty ? `\x1b[${n}m${s}\x1b[0m` : String(s));
|
|
41
|
+
const c = {
|
|
42
|
+
dim: sgr(2), bold: sgr(1),
|
|
43
|
+
lime: sgr("38;2;166;224;0"), green: sgr("38;2;87;200;120"),
|
|
44
|
+
red: sgr("38;2;229;96;77"), cyan: sgr("36"), gray: sgr("90"),
|
|
45
|
+
};
|
|
46
|
+
const KIND = { model: c.cyan("model"), api: c.lime("api "), skill: c.green("skill") };
|
|
47
|
+
const pad = (s, n) => { s = String(s); return s.length > n ? s.slice(0, n - 1) + "…" : s.padEnd(n); };
|
|
48
|
+
const usd = (units) => { const v = (units || 0) * UNIT_USD; return v < 0.01 ? `$${v.toFixed(4)}` : `$${v.toFixed(2)}`; };
|
|
49
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
50
|
+
|
|
51
|
+
// --- config / auth ----------------------------------------------------------
|
|
52
|
+
function loadConfig() {
|
|
53
|
+
try { return JSON.parse(readFileSync(CONFIG, "utf8")) || {}; } catch { return {}; }
|
|
54
|
+
}
|
|
55
|
+
function saveConfig(patch) {
|
|
56
|
+
mkdirSync(join(homedir(), ".fluxa"), { recursive: true });
|
|
57
|
+
writeFileSync(CONFIG, JSON.stringify({ ...loadConfig(), ...patch }, null, 2));
|
|
58
|
+
}
|
|
59
|
+
function loadKey() {
|
|
60
|
+
if (env.FLUXA_KEY) return env.FLUXA_KEY;
|
|
61
|
+
return loadConfig().key || null;
|
|
62
|
+
}
|
|
63
|
+
function saveKey(key) { saveConfig({ key }); }
|
|
64
|
+
// Auth token: prefer an fxa_live_ key (env/config); otherwise auto-mint a
|
|
65
|
+
// short-lived Agent VC via the local fluxa-wallet CLI. Both are accepted by the
|
|
66
|
+
// proxy's credential resolver (Bearer fxa_live_… or Bearer <agent_vc>).
|
|
67
|
+
// Mint a short-lived Agent VC: get the AgentID login JWT from the local
|
|
68
|
+
// fluxa-wallet, then have the AgentID service ISSUE the VC. Note: the
|
|
69
|
+
// `fluxa-wallet agent-vc` subcommand signs locally with a dev key the proxy
|
|
70
|
+
// can't verify — you must use the AgentID issue endpoint instead.
|
|
71
|
+
async function mintVcOnce() {
|
|
72
|
+
const { stdout: out } = await pexec("fluxa-wallet", ["refreshJWT"]);
|
|
73
|
+
const jwt = JSON.parse(out.slice(out.indexOf("{")))?.data?.jwt;
|
|
74
|
+
if (!jwt) throw new Error("no AgentID JWT from fluxa-wallet");
|
|
75
|
+
const r = await fetch(`${AGENTID}/agent/vc/issue`, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { authorization: `Bearer ${jwt}`, "content-type": "application/json" },
|
|
78
|
+
body: JSON.stringify({ challenge: "wallet-user-info-lookup", ttl_seconds: 300, audience: "fluxa-wallet-service" }),
|
|
79
|
+
});
|
|
80
|
+
const j = await r.json().catch(() => ({}));
|
|
81
|
+
const vc = j.vc || j?.data?.vc;
|
|
82
|
+
if (!vc) throw new Error(`AgentID issue ${r.status}`);
|
|
83
|
+
return vc;
|
|
84
|
+
}
|
|
85
|
+
let _token = null;
|
|
86
|
+
async function authToken() {
|
|
87
|
+
if (_token) return _token;
|
|
88
|
+
const k = loadKey();
|
|
89
|
+
if (k) return (_token = k);
|
|
90
|
+
let lastErr;
|
|
91
|
+
for (let i = 0; i < 2; i++) {
|
|
92
|
+
try {
|
|
93
|
+
const vc = await mintVcOnce();
|
|
94
|
+
console.error(c.dim(`· authed via agent VC (${new URL(AGENTID).host})`));
|
|
95
|
+
return (_token = vc);
|
|
96
|
+
} catch (e) { lastErr = e; }
|
|
97
|
+
}
|
|
98
|
+
die(`could not mint an agent VC (${lastErr?.message}). Set FLUXA_KEY=fxa_live_…, run \`planner login <key>\`, or check fluxa-wallet (\`fluxa-wallet status\`).`);
|
|
99
|
+
}
|
|
100
|
+
// Force an Agent VC, ignoring any fxa_live_ key. Key-management endpoints reject
|
|
101
|
+
// a metered key with 403 (a leaked key must not mint uncapped siblings or revoke
|
|
102
|
+
// others), so `planner keys …` always authenticates with a freshly minted VC.
|
|
103
|
+
let _vc = null;
|
|
104
|
+
async function vcToken() {
|
|
105
|
+
if (_vc) return _vc;
|
|
106
|
+
let lastErr;
|
|
107
|
+
for (let i = 0; i < 2; i++) {
|
|
108
|
+
try {
|
|
109
|
+
const vc = await mintVcOnce();
|
|
110
|
+
console.error(c.dim(`· key mgmt authed via agent VC (${new URL(AGENTID).host})`));
|
|
111
|
+
return (_vc = vc);
|
|
112
|
+
} catch (e) { lastErr = e; }
|
|
113
|
+
}
|
|
114
|
+
die(`could not mint an agent VC (${lastErr?.message}). Key management needs a VC, not an fxa_live_ key — check fluxa-wallet (\`fluxa-wallet status\`).`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- http -------------------------------------------------------------------
|
|
118
|
+
async function http(url, { method = "GET", auth = false, body, accept = "application/json", timeout = 20000 } = {}) {
|
|
119
|
+
const headers = { accept };
|
|
120
|
+
// auth: true → fxa_live_ key if present else a VC; "vc" → always a VC (key mgmt).
|
|
121
|
+
if (auth === "vc") headers.authorization = `Bearer ${await vcToken()}`;
|
|
122
|
+
else if (auth) headers.authorization = `Bearer ${await authToken()}`;
|
|
123
|
+
if (body !== undefined) { headers["content-type"] = "application/json"; body = JSON.stringify(body); }
|
|
124
|
+
const ctrl = new AbortController();
|
|
125
|
+
const to = setTimeout(() => ctrl.abort(), timeout);
|
|
126
|
+
let res;
|
|
127
|
+
try {
|
|
128
|
+
res = await fetch(url, { method, headers, body, signal: ctrl.signal });
|
|
129
|
+
} catch (e) {
|
|
130
|
+
clearTimeout(to);
|
|
131
|
+
die(`network error reaching ${new URL(url).host}: ${e.message}`);
|
|
132
|
+
}
|
|
133
|
+
clearTimeout(to);
|
|
134
|
+
const text = await res.text();
|
|
135
|
+
let data; try { data = JSON.parse(text); } catch { data = text; }
|
|
136
|
+
if (!res.ok) {
|
|
137
|
+
const msg = (data && data.error) || (data && data.hint) || (typeof data === "string" ? data.slice(0, 200) : res.statusText);
|
|
138
|
+
die(`${res.status} ${res.statusText} — ${msg}`, { res, data });
|
|
139
|
+
}
|
|
140
|
+
return { data, res };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- commands ---------------------------------------------------------------
|
|
144
|
+
async function cmdLogin(key) {
|
|
145
|
+
if (!key || !key.startsWith("fxa_")) return die('usage: planner login <fxa_live_…>');
|
|
146
|
+
saveKey(key);
|
|
147
|
+
console.log(c.green("✓") + ` saved key to ${CONFIG}`);
|
|
148
|
+
await cmdWhoami();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function cmdWhoami() {
|
|
152
|
+
const { data } = await http(`${PROXY}/llm/wallet/balances`, { auth: true });
|
|
153
|
+
const n = (data.accounts || []).length;
|
|
154
|
+
console.log(c.green("✓") + ` authenticated · ${n} merchant account${n === 1 ? "" : "s"}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function cmdSearch(query, type) {
|
|
158
|
+
const u = new URL(`${PLATFORM}/api/discover`);
|
|
159
|
+
u.searchParams.set("type", type || "api,skill,model");
|
|
160
|
+
if (query) u.searchParams.set("q", query);
|
|
161
|
+
const { data } = await http(u);
|
|
162
|
+
const rows = [
|
|
163
|
+
...(data.apiServers || []).map((r) => ({ kind: "api", slug: r.slug, desc: r.description || "", price: r.priceUsd ? (r.priceUsd.min === r.priceUsd.max ? `$${r.priceUsd.min}` : `$${r.priceUsd.min}-${r.priceUsd.max}`) : "" })),
|
|
164
|
+
...(data.skills || []).map((r) => ({ kind: "skill", slug: r.slug, desc: r.description || "", price: "" })),
|
|
165
|
+
...(data.models || []).map((r) => ({ kind: "model", slug: `${r.provider}/${r.id}`, desc: r.displayName || "", price: r.inputUnitsPer1M != null ? `${r.inputUnitsPer1M.toLocaleString()}u/Mtok in` : "" })),
|
|
166
|
+
];
|
|
167
|
+
if (!rows.length) return console.log(c.dim(" no matches" + (query ? ` for "${query}"` : "")));
|
|
168
|
+
console.log(c.dim(` ${rows.length} result${rows.length === 1 ? "" : "s"} · ${PLATFORM}/api/discover`));
|
|
169
|
+
for (const r of rows) console.log(` ${KIND[r.kind]} ${c.bold(pad(r.slug, 28))} ${c.gray(pad(r.desc, 42))} ${c.dim(r.price)}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function cmdModels(vendor) {
|
|
173
|
+
const u = new URL(`${PLATFORM}/api/llm/models`);
|
|
174
|
+
if (vendor) u.searchParams.set("vendor", vendor);
|
|
175
|
+
const { data } = await http(u);
|
|
176
|
+
const models = data.models || [];
|
|
177
|
+
if (!models.length) return console.log(c.dim(" no models"));
|
|
178
|
+
for (const m of models) {
|
|
179
|
+
const inU = m.rates_units_per_mtok?.input_tokens, outU = m.rates_units_per_mtok?.output_tokens;
|
|
180
|
+
console.log(` ${c.bold(pad(`${m.provider}/${m.id}`, 32))} ${c.gray(pad(m.display_name || "", 26))} ${c.dim(`${inU ?? "?"}/${outU ?? "?"} Units/Mtok in/out`)}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function cmdBalance(vendor) {
|
|
185
|
+
const url = vendor ? `${PROXY}/llm/wallet/balances/${vendor}` : `${PROXY}/llm/wallet/balances`;
|
|
186
|
+
const { data } = await http(url, { auth: true });
|
|
187
|
+
const accounts = vendor ? [data] : (data.accounts || []);
|
|
188
|
+
if (!accounts.length) return console.log(c.dim(" no merchant balances yet — `planner topup <vendor>` to start"));
|
|
189
|
+
console.log(" " + c.dim(pad("merchant", 16) + pad("balance", 18) + pad("≈ USD", 12) + pad("7d/day", 12) + "status"));
|
|
190
|
+
for (const a of accounts) {
|
|
191
|
+
const low = a.balance < (a.burn7dPerDay || 0) * 3;
|
|
192
|
+
const status = a.balance < 0 ? c.red("owed " + usd(-a.balance)) : low ? c.red("low") : c.green("ok");
|
|
193
|
+
console.log(` ${pad(a.merchant, 16)}${pad((a.balance ?? 0).toLocaleString() + " Units", 18)}${pad(usd(a.balance), 12)}${pad((a.burn7dPerDay ?? 0).toLocaleString(), 12)}${status}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function cmdLedger(vendor) {
|
|
198
|
+
if (!vendor) return die("usage: planner ledger <vendor>");
|
|
199
|
+
const { data } = await http(`${PROXY}/llm/wallet/ledger/${vendor}?limit=20`, { auth: true });
|
|
200
|
+
const entries = data.entries || [];
|
|
201
|
+
if (!entries.length) return console.log(c.dim(" no ledger entries for " + vendor));
|
|
202
|
+
console.log(" " + c.dim(pad("when", 22) + pad("type", 12) + pad("amount", 14) + "balance after"));
|
|
203
|
+
for (const e of entries) {
|
|
204
|
+
const amt = (e.amount > 0 ? c.green("+") : c.red("")) + e.amount.toLocaleString();
|
|
205
|
+
console.log(` ${pad(new Date(e.ts).toISOString().replace("T", " ").slice(0, 19), 22)}${pad(e.type, 12)}${pad(amt, 14 + (tty ? 9 : 0))}${(e.balanceAfter ?? "").toLocaleString()}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- api key management (Agent-VC only) -------------------------------------
|
|
210
|
+
// Provision and rotate the user's fxa_live_ keys programmatically: an agent can
|
|
211
|
+
// hand a fresh, capped key to a sub-process without the human minting one by
|
|
212
|
+
// hand. All four endpoints REQUIRE an Agent VC (a metered fxa_live_ key is 403'd
|
|
213
|
+
// so a leaked key can't mint uncapped siblings or revoke others) and are scoped
|
|
214
|
+
// to the VC owner — so they always auth via `auth: "vc"`.
|
|
215
|
+
const capLabel = (mc) => (mc == null || mc <= 0 ? c.dim("uncapped") : `${mc} MC`);
|
|
216
|
+
|
|
217
|
+
async function cmdKeys(sub, rest, opts) {
|
|
218
|
+
switch (sub || "list") {
|
|
219
|
+
case "list": case "ls": return keysList();
|
|
220
|
+
case "create": case "new": case "add": return keysCreate(opts);
|
|
221
|
+
case "update": case "set": return keysUpdate(rest[0], opts);
|
|
222
|
+
case "revoke": case "rm": case "delete": return keysRevoke(rest[0]);
|
|
223
|
+
default: return die(`unknown: planner keys ${sub}\n keys list | create | update <id> | revoke <id>`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function keysList() {
|
|
228
|
+
const { data } = await http(`${PROXY}/llm/keys`, { auth: "vc" });
|
|
229
|
+
const keys = data.keys || [];
|
|
230
|
+
if (!keys.length) return console.log(c.dim(" no API keys yet — `planner keys create` to mint one"));
|
|
231
|
+
// ids are full UUIDs (never truncated — you need them for update/revoke)
|
|
232
|
+
console.log(" " + c.dim(pad("id", 38) + pad("name", 16) + pad("prefix", 16) + pad("cap", 11) + pad("spent", 11) + "status"));
|
|
233
|
+
for (const k of keys) {
|
|
234
|
+
const status = k.revokedAt ? c.red("revoked") : c.green("active");
|
|
235
|
+
console.log(` ${pad(k.id, 38)}${pad(k.name || "—", 16)}${pad(k.keyPrefix || "", 16)}${pad(capLabel(k.spendCapCredits), 11)}${pad((k.spentCredits ?? 0) + " MC", 11)}${status}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function keysCreate(opts) {
|
|
240
|
+
const body = {};
|
|
241
|
+
if (opts.name) body.name = opts.name;
|
|
242
|
+
if (opts.cap != null) body.spendCapCredits = Number(opts.cap);
|
|
243
|
+
const { data } = await http(`${PROXY}/llm/keys`, { method: "POST", auth: "vc", body });
|
|
244
|
+
console.log(c.green("✓") + ` created key ${c.bold(data.name || data.id)} · cap ${capLabel(data.spendCapCredits)}`);
|
|
245
|
+
console.log(c.dim(` id: ${data.id} ${c.dim("(use it to update/revoke)")}`));
|
|
246
|
+
console.log(`\n ${c.bold("raw key — shown once, store it now:")}\n ${c.lime(data.rawKey)}\n`);
|
|
247
|
+
console.log(c.dim(` use it: planner login ${data.rawKey} (or export FLUXA_KEY=…)`));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function keysUpdate(id, opts) {
|
|
251
|
+
if (!id) return die("usage: planner keys update <id> [--name <n>] [--cap <MC>]");
|
|
252
|
+
const body = {};
|
|
253
|
+
if (opts.name != null) body.name = opts.name;
|
|
254
|
+
if (opts.cap != null) body.spendCapCredits = Number(opts.cap); // --cap 0 clears the cap
|
|
255
|
+
if (!Object.keys(body).length) return die("nothing to update — pass --name and/or --cap");
|
|
256
|
+
const { data } = await http(`${PROXY}/llm/keys/${id}`, { method: "PATCH", auth: "vc", body });
|
|
257
|
+
console.log(c.green("✓") + ` updated ${c.bold(data.name || data.id)} · cap ${capLabel(data.spendCapCredits)}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function keysRevoke(id) {
|
|
261
|
+
if (!id) return die("usage: planner keys revoke <id>");
|
|
262
|
+
await http(`${PROXY}/llm/keys/${id}`, { method: "DELETE", auth: "vc" });
|
|
263
|
+
console.log(c.green("✓") + ` revoked ${id} — it stops authenticating on its next call`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// --- paid call (the x402 v3 dance, wrapped) ---------------------------------
|
|
267
|
+
// `planner call <url>` collapses the 10-step manual flow into one command:
|
|
268
|
+
// register → discover → ensure a signed mandate → call → on 402 settle via
|
|
269
|
+
// `fluxa-wallet x402-v3` → retry with the payment token → return the raw body.
|
|
270
|
+
// Signing the mandate is the one human step; planner reuses a signed, unexpired
|
|
271
|
+
// mandate so you sign once per budget window, not once per call.
|
|
272
|
+
async function walletRaw(args) {
|
|
273
|
+
const { stdout: out } = await pexec("fluxa-wallet", args);
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
function parseWalletJson(out) {
|
|
277
|
+
const i = String(out).indexOf("{");
|
|
278
|
+
if (i < 0) return {};
|
|
279
|
+
try { return JSON.parse(String(out).slice(i)); } catch { return {}; }
|
|
280
|
+
}
|
|
281
|
+
async function walletJson(args) { return parseWalletJson(await walletRaw(args)); }
|
|
282
|
+
const unwrap = (j) => (j && j.data) ? j.data : j; // wallet wraps some payloads in {data}
|
|
283
|
+
|
|
284
|
+
async function ensureAgent() {
|
|
285
|
+
try {
|
|
286
|
+
await walletRaw(["status"]);
|
|
287
|
+
} catch {
|
|
288
|
+
die('no Agent ID — register once: `fluxa-wallet init --name "<agent>" --client "<client>"`, then retry.');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function rawCall(url, method, data, extraHeaders = {}, auth = false) {
|
|
293
|
+
const headers = { accept: "application/json", ...extraHeaders };
|
|
294
|
+
if (auth) headers.authorization = `Bearer ${await authToken()}`;
|
|
295
|
+
let body;
|
|
296
|
+
if (data !== undefined && data !== null) { headers["content-type"] = "application/json"; body = data; }
|
|
297
|
+
let res;
|
|
298
|
+
try { res = await fetch(url, { method, headers, body }); }
|
|
299
|
+
catch (e) { die(`network error reaching ${new URL(url).host}: ${e.message}`); }
|
|
300
|
+
const text = await res.text();
|
|
301
|
+
return { status: res.status, text };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function ensureMandate(currency, budget, seconds, url) {
|
|
305
|
+
const cfg = loadConfig();
|
|
306
|
+
// only reuse a signed mandate of the SAME currency with budget left
|
|
307
|
+
if (cfg.mandateId && cfg.mandateCurrency === currency) {
|
|
308
|
+
const m = unwrap(await walletJson(["mandate-status", "--id", cfg.mandateId]).catch(() => ({})));
|
|
309
|
+
const mm = m.mandate || m;
|
|
310
|
+
if (mm && mm.status === "signed" && Number(mm.remainingAmount ?? 0) >= budget) {
|
|
311
|
+
console.error(c.dim(`· reusing signed mandate ${cfg.mandateId} (remaining ${mm.remainingAmount})`));
|
|
312
|
+
return cfg.mandateId;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const host = new URL(url).host;
|
|
316
|
+
const desc = `Spend up to ${budget} for planner on ${host}`;
|
|
317
|
+
const created = unwrap(await walletJson(["mandate-create", "--desc", desc, "--amount", String(budget), "--seconds", String(seconds), "--currency", currency]));
|
|
318
|
+
const mandateId = created.mandateId || created.id;
|
|
319
|
+
const authUrl = created.authorizationUrl || created.authUrl;
|
|
320
|
+
if (!mandateId) die("mandate-create did not return a mandateId");
|
|
321
|
+
saveConfig({ mandateId, mandateCurrency: currency });
|
|
322
|
+
|
|
323
|
+
console.error(`\n ${c.bold("Sign the spending mandate")} ${c.dim(`(budget ${budget} ${currency}, valid ${seconds}s)`)}`);
|
|
324
|
+
if (authUrl) console.error(` ${c.cyan(authUrl)}`);
|
|
325
|
+
console.error(c.dim(" open the link, approve, then this continues automatically…\n"));
|
|
326
|
+
|
|
327
|
+
for (let i = 0; i < 60; i++) {
|
|
328
|
+
await sleep(2000);
|
|
329
|
+
const s = unwrap(await walletJson(["mandate-status", "--id", mandateId]).catch(() => ({})));
|
|
330
|
+
const mm = s.mandate || s;
|
|
331
|
+
if (mm && mm.status === "signed") { console.error(c.green(" ✓ mandate signed")); return mandateId; }
|
|
332
|
+
}
|
|
333
|
+
die("mandate not signed in time — re-run `planner call` once you've approved the link.");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function descriptorEndpoints(text) {
|
|
337
|
+
try { const j = JSON.parse(text); if (j && Array.isArray(j.endpoints)) return j; } catch {}
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function cmdCall(url, opts = {}) {
|
|
342
|
+
if (!url || !/^https?:\/\//.test(url)) {
|
|
343
|
+
return die("usage: planner call <url> [--endpoint <path>] [--data '<json>'] [--pay credits|usdc] [--method GET|POST] [--budget <units>] [--raw]");
|
|
344
|
+
}
|
|
345
|
+
const pay = (opts.pay || loadConfig().pay || "credits").toLowerCase();
|
|
346
|
+
const currency = pay === "usdc" ? "USDC" : "FLUXA_MONETIZE_CREDITS";
|
|
347
|
+
const budget = opts.budget ? Number(opts.budget) : (pay === "usdc" ? 5000000 : 500); // ~$5 default
|
|
348
|
+
const seconds = opts.seconds ? Number(opts.seconds) : 28800; // 8h
|
|
349
|
+
|
|
350
|
+
await ensureAgent();
|
|
351
|
+
|
|
352
|
+
let targetUrl = url;
|
|
353
|
+
let method = (opts.method || (opts.data ? "POST" : "GET")).toUpperCase();
|
|
354
|
+
const data = opts.data;
|
|
355
|
+
|
|
356
|
+
// proxy endpoints are two-level: GET <base> returns a descriptor listing the
|
|
357
|
+
// real paid sub-endpoints. Resolve it (unless --raw) so the agent doesn't have
|
|
358
|
+
// to read the catalog itself. We never spend without a supplied body.
|
|
359
|
+
if (!opts.raw) {
|
|
360
|
+
const probe = await rawCall(url, "GET");
|
|
361
|
+
const desc = probe.status === 200 ? descriptorEndpoints(probe.text) : null;
|
|
362
|
+
if (desc) {
|
|
363
|
+
const eps = desc.endpoints.filter((e) => e.enabled !== false);
|
|
364
|
+
const norm = (p) => "/" + String(p || "").replace(/^\//, "");
|
|
365
|
+
let ep = opts.endpoint ? eps.find((e) => norm(e.path) === norm(opts.endpoint)) : null;
|
|
366
|
+
if (!ep) {
|
|
367
|
+
const paid = eps.filter((e) => Number(e.price || 0) > 0);
|
|
368
|
+
ep = paid.length === 1 ? paid[0] : null;
|
|
369
|
+
}
|
|
370
|
+
if (!ep) {
|
|
371
|
+
console.error(c.bold(`\n ${desc.name || "endpoint"}`) + c.dim(" — pick one with --endpoint <path>:"));
|
|
372
|
+
for (const e of eps) {
|
|
373
|
+
console.error(` ${pad(e.method || "GET", 5)} ${c.bold(pad(e.path, 22))} ${c.lime(pad(e.price ? `${e.price} ${e.currency || ""}` : "free", 12))} ${c.gray(e.description || "")}`);
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
targetUrl = url.replace(/\/$/, "") + norm(ep.path);
|
|
378
|
+
method = opts.method ? opts.method.toUpperCase() : (ep.method || "GET").toUpperCase();
|
|
379
|
+
const bodyFields = ep.inputSchema && Array.isArray(ep.inputSchema.bodyFields) ? ep.inputSchema.bodyFields : [];
|
|
380
|
+
if (bodyFields.length > 0 && !data) {
|
|
381
|
+
console.error(c.bold(`\n ${method} ${norm(ep.path)}`) + c.dim(` ${ep.price ? `${ep.price} ${ep.currency || ""}` : "free"}`));
|
|
382
|
+
console.error(c.dim(" needs a JSON body — pass it with --data '<json>'. Expected shape:"));
|
|
383
|
+
const ex = bodyFields[0]?.description;
|
|
384
|
+
if (ex) console.error(c.gray(" " + String(ex).replace(/\n/g, "\n ")));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// call the resolved endpoint — pay if it answers 402
|
|
391
|
+
let r = await rawCall(targetUrl, method, data);
|
|
392
|
+
if (r.status !== 402) {
|
|
393
|
+
if (r.status >= 400) die(`${r.status} — ${r.text.slice(0, 300)}`);
|
|
394
|
+
return console.log(r.text); // raw body, no post-processing
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
console.error(c.dim(`· 402 Payment Required — settling via x402 v3 (${currency})`));
|
|
398
|
+
const mandateId = await ensureMandate(currency, budget, seconds, targetUrl);
|
|
399
|
+
|
|
400
|
+
// 2. sign the payment for this exact 402 challenge (pass the FULL body)
|
|
401
|
+
const paid = unwrap(await walletJson(["x402-v3", "--mandate", mandateId, "--payload", r.text]));
|
|
402
|
+
const xPayment = paid.xPaymentB64 || paid.xPayment;
|
|
403
|
+
if (!xPayment) die("x402-v3 did not return a payment token (xPaymentB64).");
|
|
404
|
+
|
|
405
|
+
// 3. retry with the payment token
|
|
406
|
+
r = await rawCall(url, method, opts.data, { "X-Payment": xPayment });
|
|
407
|
+
if (r.status >= 400) die(`paid retry failed: ${r.status} — ${r.text.slice(0, 300)}`);
|
|
408
|
+
console.log(r.text); // raw body, no post-processing
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// --- topup (prepay Units to a merchant) -------------------------------------
|
|
412
|
+
// `POST /llm/topup/initiate` (authed) returns a 402 with an orderId + a
|
|
413
|
+
// `resource` finalize URL; pay it in Monetize Credits via the wallet, then
|
|
414
|
+
// `POST` the finalize URL with the X-Payment token to credit the balance.
|
|
415
|
+
async function cmdTopup(vendor, opts = {}) {
|
|
416
|
+
if (!vendor) return die("usage: planner topup <vendor> [--credits <N>] [--bundle <slug>]");
|
|
417
|
+
await ensureAgent();
|
|
418
|
+
|
|
419
|
+
const body = { vendorSlug: vendor };
|
|
420
|
+
if (opts.bundle) body.packageSlug = opts.bundle;
|
|
421
|
+
else body.costCredits = opts.credits ? Number(opts.credits) : 5; // min 5 MC ($5)
|
|
422
|
+
|
|
423
|
+
// 1. initiate — always answers 402 with the x402 challenge on success
|
|
424
|
+
const init = await rawCall(`${PROXY}/llm/topup/initiate`, "POST", JSON.stringify(body), {}, true);
|
|
425
|
+
if (init.status !== 402) die(`topup initiate ${init.status} — ${init.text.slice(0, 200)}`);
|
|
426
|
+
let ch; try { ch = JSON.parse(init.text); } catch { return die("topup initiate did not return JSON"); }
|
|
427
|
+
const { orderId, resource, costCredits, creditsToGrant } = ch;
|
|
428
|
+
if (!resource || !orderId) die("topup initiate missing orderId/resource");
|
|
429
|
+
console.error(c.dim(`· order ${orderId}: ${costCredits} Monetize Credits → ${Number(creditsToGrant).toLocaleString()} Units to ${vendor}`));
|
|
430
|
+
|
|
431
|
+
// 2. mandate (credits) — budget must cover the cost (wallet credits unit = MC × 100)
|
|
432
|
+
const budget = opts.budget ? Number(opts.budget) : Math.max(500, Math.ceil(Number(costCredits) * 100));
|
|
433
|
+
const seconds = opts.seconds ? Number(opts.seconds) : 28800;
|
|
434
|
+
const mandateId = await ensureMandate("FLUXA_MONETIZE_CREDITS", budget, seconds, `${PROXY}/llm/topup`);
|
|
435
|
+
|
|
436
|
+
// 3. sign the payment over the FULL 402 body
|
|
437
|
+
const paid = unwrap(await walletJson(["x402-v3", "--mandate", mandateId, "--payload", init.text]));
|
|
438
|
+
const xPayment = paid.xPaymentB64 || paid.xPayment;
|
|
439
|
+
if (!xPayment) die("x402-v3 did not return a payment token (xPaymentB64).");
|
|
440
|
+
|
|
441
|
+
// 4. finalize — POST the resource URL with the payment token (no bearer)
|
|
442
|
+
const fin = await rawCall(resource, "POST", undefined, { "X-Payment": xPayment });
|
|
443
|
+
if (fin.status >= 400) die(`finalize ${fin.status} — ${fin.text.slice(0, 200)}`);
|
|
444
|
+
let res; try { res = JSON.parse(fin.text); } catch { res = {}; }
|
|
445
|
+
const added = Number(res.creditsAdded ?? creditsToGrant);
|
|
446
|
+
console.log(c.green("✓") + ` topped up ${vendor} · +${added.toLocaleString()} Units · balance ${Number(res.balance ?? 0).toLocaleString()} Units`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// --- plan (thin call to server-side endpoint) --------------------------------
|
|
450
|
+
async function cmdPlan(task) {
|
|
451
|
+
if (!task) return die('usage: planner plan "<task>"');
|
|
452
|
+
const r = await fetch(`${PROXY}/api/planner/plan`, {
|
|
453
|
+
method: "POST",
|
|
454
|
+
headers: { "content-type": "application/json" },
|
|
455
|
+
body: JSON.stringify({ task }),
|
|
456
|
+
});
|
|
457
|
+
if (!r.ok) return die(`plan failed: HTTP ${r.status}`);
|
|
458
|
+
const result = await r.json().catch(() => null);
|
|
459
|
+
if (!result) return die("plan failed: bad response");
|
|
460
|
+
if (result.kind === "answer") return printAnswer(result);
|
|
461
|
+
return printPlan(result);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function printAnswer(a) {
|
|
465
|
+
console.log("\n " + c.bold(a.answer));
|
|
466
|
+
if (a.command) {
|
|
467
|
+
console.log("\n " + c.dim("run:"));
|
|
468
|
+
console.log(" " + c.cyan(a.command));
|
|
469
|
+
}
|
|
470
|
+
if (a.prompt) {
|
|
471
|
+
console.log("\n " + c.dim("copy this prompt for your agent:"));
|
|
472
|
+
for (const line of a.prompt.split("\n")) console.log(" " + line);
|
|
473
|
+
}
|
|
474
|
+
console.log("");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Render a PlanOrAnswer plan: tools carry kind/slug/name/units/detail.
|
|
478
|
+
function printPlan(plan) {
|
|
479
|
+
const tools = plan.tools || [];
|
|
480
|
+
const heading = plan.reason || plan.task || "Recommended";
|
|
481
|
+
console.log("\n " + c.bold("Recommended") + " " + c.dim(heading));
|
|
482
|
+
let total = 0;
|
|
483
|
+
for (const t of tools) {
|
|
484
|
+
const k = KIND[t.kind] || pad(t.kind, 5);
|
|
485
|
+
total += t.units || 0;
|
|
486
|
+
console.log(` ${k} ${c.bold(pad(t.slug, 36))} ${c.dim(pad(t.detail || "", 30))} ${c.lime("~" + (t.units || 0).toLocaleString() + " Units")}`);
|
|
487
|
+
}
|
|
488
|
+
console.log(c.gray(" " + "─".repeat(78)));
|
|
489
|
+
console.log(` ${c.bold("est")} ${c.lime("~" + total.toLocaleString() + " Units")} ${c.dim("(~" + usd(total) + ")")}`);
|
|
490
|
+
if (plan.instructions) console.log("\n " + c.dim(plan.instructions));
|
|
491
|
+
console.log(c.dim("\n → call these tools directly — FluxA settles each paid call from your wallet. No keys, no subs."));
|
|
492
|
+
return total;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// --- info (self-contained explainer; no network, no auth) -------------------
|
|
496
|
+
const INFO = {
|
|
497
|
+
overview: () => `
|
|
498
|
+
${c.bold("planner")} ${c.dim("— what you're working with")}
|
|
499
|
+
|
|
500
|
+
FluxA is an agent-native task layer. The planner ${c.bold("recommends")} the right tools for a
|
|
501
|
+
task; your agent runs them and FluxA settles each paid call from your wallet.
|
|
502
|
+
|
|
503
|
+
${c.bold("Three kinds of tool")} (all metered in Units):
|
|
504
|
+
${KIND.api} Oneshot APIs — pay-per-call endpoints (scrape, search, video, …)
|
|
505
|
+
${KIND.model} Models — LLM endpoints, billed per token, via /llm/{merchant}
|
|
506
|
+
${KIND.skill} Skills — packaged multi-step routines
|
|
507
|
+
|
|
508
|
+
${c.bold("Money")}
|
|
509
|
+
1 Unit = $0.00001 · 100,000 Units = $1 = 1 Monetize Credit (MC)
|
|
510
|
+
Balances are ${c.bold("per merchant")} (prepaid Units). ${c.cyan("planner topup <merchant>")} to prefund.
|
|
511
|
+
|
|
512
|
+
${c.bold("Paying (x402)")}
|
|
513
|
+
A paid call returns HTTP 402 → planner settles it via ${c.dim("fluxa-wallet")} inside a spending
|
|
514
|
+
mandate you sign once (budget + window), then reuses it. planner never holds keys.
|
|
515
|
+
|
|
516
|
+
${c.bold("Auth")} an ${c.dim("fxa_live_")} key OR an auto-minted agent VC. Nothing to log in.
|
|
517
|
+
${c.bold("Bases")} platform ${c.dim(new URL(PLATFORM).host)} · proxy ${c.dim(new URL(PROXY).host)}
|
|
518
|
+
|
|
519
|
+
${c.bold("Commands")}
|
|
520
|
+
${c.cyan('plan "<task>"')} recommend tools for a task
|
|
521
|
+
${c.cyan("call <url> --data '…'")} make one paid API call (handles the 402)
|
|
522
|
+
${c.cyan("topup <merchant>")} prepay Units
|
|
523
|
+
${c.cyan('search "<q>"')} discover apis/models/skills
|
|
524
|
+
${c.dim("models · balance · ledger · keys · whoami")}
|
|
525
|
+
|
|
526
|
+
More: ${c.cyan("planner info")} ${c.dim("<units|auth|pay|keys|apis|models|skills>")}
|
|
527
|
+
`,
|
|
528
|
+
units: () => `
|
|
529
|
+
${c.bold("Units & credits")}
|
|
530
|
+
1 Unit = $0.00001 (USD). 100,000 Units = $1 = 1 Monetize Credit (MC).
|
|
531
|
+
· Per-call API/skill prices are quoted in USD; model rates in Units per 1M tokens.
|
|
532
|
+
· Your prepaid balance is in Units, held ${c.bold("per merchant")}.
|
|
533
|
+
· Topups are charged in Monetize Credits (min 5 MC = $5); 1 MC grants 100,000 Units.
|
|
534
|
+
`,
|
|
535
|
+
auth: () => `
|
|
536
|
+
${c.bold("Auth")}
|
|
537
|
+
Two accepted Bearer credentials:
|
|
538
|
+
${c.dim("fxa_live_<key>")} create at ${new URL(PLATFORM).host}/keys · ${c.cyan("planner login <key>")}
|
|
539
|
+
${c.dim("agent VC")} short-lived JWT, auto-minted from your local fluxa-wallet
|
|
540
|
+
planner auto-mints an agent VC when no key is set — nothing to log in.
|
|
541
|
+
Discovery (${c.cyan("search")}) is public; everything else is authed.
|
|
542
|
+
Manage keys programmatically with ${c.cyan("planner keys")} ${c.dim("(VC only — see `planner info keys`)")}.
|
|
543
|
+
`,
|
|
544
|
+
keys: () => `
|
|
545
|
+
${c.bold("API keys — programmatic management")} ${c.dim("(Agent VC only)")}
|
|
546
|
+
Provision and rotate your ${c.dim("fxa_live_")} keys so an agent can hand a fresh, capped key
|
|
547
|
+
to a sub-process without you minting one by hand. ${c.bold("Requires an Agent VC")} — a metered
|
|
548
|
+
fxa_live_ key is refused (it must not mint uncapped siblings or revoke others).
|
|
549
|
+
${c.cyan("planner keys")} list your keys (prefixes only)
|
|
550
|
+
${c.cyan("planner keys create --name <n> --cap <MC>")} mint one; raw key shown ONCE
|
|
551
|
+
${c.cyan("planner keys update <id> --cap <MC>")} change name / spend cap (${c.dim("--cap 0")} clears)
|
|
552
|
+
${c.cyan("planner keys revoke <id>")} revoke (immediate, irreversible)
|
|
553
|
+
Spend caps (in MC) gate metered LLM usage on that key only. To rotate: create
|
|
554
|
+
the new key, hand it off, ${c.bold("then")} revoke the old — never leave zero working keys.
|
|
555
|
+
`,
|
|
556
|
+
pay: () => `
|
|
557
|
+
${c.bold("Paying — x402 v3")}
|
|
558
|
+
Prepaid: each merchant has a Units balance; while it's funded, calls just work.
|
|
559
|
+
On a shortfall a paid endpoint returns HTTP 402 with an x402 challenge. planner then:
|
|
560
|
+
1. ensures a signed spending ${c.bold("mandate")} (you pre-approve a budget + time window — once)
|
|
561
|
+
2. signs the 402 via ${c.dim("fluxa-wallet x402-v3")} (planner never holds keys)
|
|
562
|
+
3. retries with the payment token
|
|
563
|
+
It reuses the signed mandate for later calls in its window. ${c.cyan("planner topup")} prefunds instead.
|
|
564
|
+
`,
|
|
565
|
+
apis: () => `
|
|
566
|
+
${c.bold("Oneshot APIs")}
|
|
567
|
+
${c.dim("/api/{config}")} is two-level: ${c.dim("GET <base>")} returns a descriptor of sub-endpoints
|
|
568
|
+
(path, method, price, input schema). The paid action is a sub-endpoint.
|
|
569
|
+
${c.cyan("planner call <base>")} show the paid endpoint + its required body
|
|
570
|
+
${c.cyan("planner call <base> --data '<json>'")} call it; pay the 402 if needed
|
|
571
|
+
${c.dim("--endpoint <path>")} picks when there are several; ${c.dim("--raw")} hits a URL directly.
|
|
572
|
+
`,
|
|
573
|
+
models: () => `
|
|
574
|
+
${c.bold("Models — merchant-centric")}
|
|
575
|
+
A ${c.bold("merchant")} (provider) exposes many models; billing + balance are per merchant.
|
|
576
|
+
An offering is ${c.dim("(merchant, model)")}; the lane is ${c.dim("POST /llm/{merchant}/v1/chat/completions")}
|
|
577
|
+
(OpenAI wire format), billed per token.
|
|
578
|
+
${c.cyan("planner models [--vendor <slug>]")} list models + Units rates
|
|
579
|
+
${c.cyan("planner topup <merchant>")} fund that merchant's balance
|
|
580
|
+
`,
|
|
581
|
+
skills: () => `
|
|
582
|
+
${c.bold("Skills")}
|
|
583
|
+
Packaged multi-step routines that wrap several tools into one capability.
|
|
584
|
+
${c.cyan('planner search "<q>" --type skill')} discover skills
|
|
585
|
+
Install via the skills tool: ${c.dim("npx -y skills add <platform> -s <slug>")}.
|
|
586
|
+
`,
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
function cmdInfo(topic) {
|
|
590
|
+
const key = (topic || "overview").toLowerCase();
|
|
591
|
+
const render = INFO[key];
|
|
592
|
+
if (!render) {
|
|
593
|
+
return die(`unknown topic: ${topic}\n topics: ${Object.keys(INFO).filter((k) => k !== "overview").join(", ")}`);
|
|
594
|
+
}
|
|
595
|
+
console.log(render());
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function help() {
|
|
599
|
+
console.log(`
|
|
600
|
+
${c.bold("planner")} ${c.dim("— FluxA Planner CLI (live)")}
|
|
601
|
+
|
|
602
|
+
${c.bold("Headline")}
|
|
603
|
+
planner plan "<task>" recommend the models, APIs & skills for a task
|
|
604
|
+
planner call <url> make a paid API call — handles the x402 mandate + payment
|
|
605
|
+
[--pay credits|usdc] [--method GET|POST] [--data '<json>']
|
|
606
|
+
|
|
607
|
+
${c.bold("Primitives")} ${c.dim("(all live)")}
|
|
608
|
+
planner search "<q>" [--type api,skill,model] discover resources
|
|
609
|
+
planner models [--vendor <slug>] model catalog + Units rates
|
|
610
|
+
planner balance [vendor] prepaid Units per merchant
|
|
611
|
+
planner topup <vendor> [--credits <N>|--bundle <slug>] prepay Units (x402, via wallet)
|
|
612
|
+
planner ledger <vendor> spend/topup history
|
|
613
|
+
planner keys [create|update <id>|revoke <id>] manage fxa_live_ API keys (Agent VC only)
|
|
614
|
+
planner login <fxa_live_…> / planner whoami auth
|
|
615
|
+
planner info [units|auth|pay|keys|apis|models|skills] explain how the platform works
|
|
616
|
+
|
|
617
|
+
${c.dim("Config: FLUXA_KEY, FLUXA_PLATFORM (" + PLATFORM + "), FLUXA_PROXY (" + PROXY + ")")}
|
|
618
|
+
`);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function die(msg, ctx) {
|
|
622
|
+
console.error(c.red("error: ") + msg);
|
|
623
|
+
exit(1);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// --- arg parse --------------------------------------------------------------
|
|
627
|
+
async function main() {
|
|
628
|
+
const args = argv.slice(2);
|
|
629
|
+
const cmd = args[0];
|
|
630
|
+
const FLAGS = ["--type", "--vendor", "--pay", "--method", "--data", "--budget", "--seconds", "--endpoint", "--credits", "--bundle", "--name", "--cap"];
|
|
631
|
+
const BOOLS = ["--raw"];
|
|
632
|
+
const getFlag = (n) => { const i = args.indexOf(n); return i !== -1 ? args[i + 1] : null; };
|
|
633
|
+
const type = getFlag("--type");
|
|
634
|
+
const vendor = getFlag("--vendor");
|
|
635
|
+
const rest = [];
|
|
636
|
+
for (let i = 1; i < args.length; i++) {
|
|
637
|
+
if (FLAGS.includes(args[i])) { i++; continue; }
|
|
638
|
+
if (BOOLS.includes(args[i])) continue;
|
|
639
|
+
rest.push(args[i]);
|
|
640
|
+
}
|
|
641
|
+
const arg = rest.join(" ").trim();
|
|
642
|
+
|
|
643
|
+
switch (cmd) {
|
|
644
|
+
case "plan": return cmdPlan(arg);
|
|
645
|
+
case "call":
|
|
646
|
+
return cmdCall(rest[0], {
|
|
647
|
+
pay: getFlag("--pay"), method: getFlag("--method"), data: getFlag("--data"),
|
|
648
|
+
budget: getFlag("--budget"), seconds: getFlag("--seconds"),
|
|
649
|
+
endpoint: getFlag("--endpoint"), raw: args.includes("--raw"),
|
|
650
|
+
});
|
|
651
|
+
case "search": return cmdSearch(arg, type);
|
|
652
|
+
case "models": return cmdModels(vendor);
|
|
653
|
+
case "balance": case "bal": return cmdBalance(rest[0]);
|
|
654
|
+
case "topup":
|
|
655
|
+
return cmdTopup(rest[0], {
|
|
656
|
+
credits: getFlag("--credits"), bundle: getFlag("--bundle"),
|
|
657
|
+
budget: getFlag("--budget"), seconds: getFlag("--seconds"),
|
|
658
|
+
});
|
|
659
|
+
case "ledger": return cmdLedger(rest[0]);
|
|
660
|
+
case "keys": return cmdKeys(rest[0], rest.slice(1), { name: getFlag("--name"), cap: getFlag("--cap") });
|
|
661
|
+
case "login": return cmdLogin(rest[0]);
|
|
662
|
+
case "whoami": return cmdWhoami();
|
|
663
|
+
case "info": return cmdInfo(rest[0]);
|
|
664
|
+
case undefined: case "-h": case "--help": case "help": return help();
|
|
665
|
+
default: return die(`unknown command: ${cmd}\n run \`planner --help\``);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
main();
|