@fluxa-pay/planner 0.2.3 → 0.2.4
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 +2 -2
- package/planner.mjs +20 -180
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluxa-pay/planner",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "FluxA Planner CLI. Recommend the right models, APIs and skills for a task,
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"description": "FluxA Planner CLI. Recommend the right models, APIs and skills for a task, and manage your oneshot APIs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"planner": "planner.mjs"
|
package/planner.mjs
CHANGED
|
@@ -281,12 +281,9 @@ async function keysRevoke(id) {
|
|
|
281
281
|
console.log(c.green("✓") + ` revoked ${id} — it stops authenticating on its next call`);
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
// ---
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
// `fluxa-wallet x402-v3` → retry with the payment token → return the raw body.
|
|
288
|
-
// Signing the mandate is the one human step; planner reuses a signed, unexpired
|
|
289
|
-
// mandate so you sign once per budget window, not once per call.
|
|
284
|
+
// --- wallet helpers (x402 v3 settlement, shared by topup) --------------------
|
|
285
|
+
// The wallet CLI does the signing; planner reuses a signed, unexpired mandate so
|
|
286
|
+
// the user signs once per budget window, not once per settlement.
|
|
290
287
|
async function walletRaw(args) {
|
|
291
288
|
const { stdout: out } = await pexec("fluxa-wallet", args);
|
|
292
289
|
return out;
|
|
@@ -319,39 +316,6 @@ async function rawCall(url, method, data, extraHeaders = {}, auth = false) {
|
|
|
319
316
|
return { status: res.status, text };
|
|
320
317
|
}
|
|
321
318
|
|
|
322
|
-
// The proxy's API/MCP lanes bill the caller identified by the X-Agent-ID header
|
|
323
|
-
// and reject every non-discovery request without it. Default to the agent_id
|
|
324
|
-
// (UUID) from `fluxa-wallet status`; configs with require_agent_auth need a JWT
|
|
325
|
-
// instead, so agentCall() retries with a freshly minted wallet JWT on the
|
|
326
|
-
// "valid JWT" 401. Both values are cached for the process.
|
|
327
|
-
let _agentId = null;
|
|
328
|
-
async function agentId() {
|
|
329
|
-
if (_agentId) return _agentId;
|
|
330
|
-
const s = unwrap(await walletJson(["status"]).catch(() => ({})));
|
|
331
|
-
const id = s.agent_id || s.agentId || s.id;
|
|
332
|
-
if (!id) die("could not read agent_id from `fluxa-wallet status` — register once with `fluxa-wallet init`.");
|
|
333
|
-
return (_agentId = id);
|
|
334
|
-
}
|
|
335
|
-
let _walletJwt = null;
|
|
336
|
-
async function walletJwt() {
|
|
337
|
-
if (_walletJwt) return _walletJwt;
|
|
338
|
-
const j = unwrap(await walletJson(["refreshJWT"]).catch(() => ({})));
|
|
339
|
-
const jwt = j.jwt || j.token;
|
|
340
|
-
if (!jwt) die("could not mint a wallet JWT (`fluxa-wallet refreshJWT`).");
|
|
341
|
-
return (_walletJwt = jwt);
|
|
342
|
-
}
|
|
343
|
-
// rawCall + X-Agent-ID, with the UUID→JWT fallback for require_agent_auth configs.
|
|
344
|
-
// Auto-attached headers go FIRST so a caller-supplied `extra` (e.g. a user's
|
|
345
|
-
// `--header "X-Agent-ID: …"`) can override — collisions are surfaced upstream
|
|
346
|
-
// in cmdCall so the override isn't silent.
|
|
347
|
-
async function agentCall(url, method, data, extra = {}) {
|
|
348
|
-
const r = await rawCall(url, method, data, { "X-Agent-ID": await agentId(), ...extra });
|
|
349
|
-
if (r.status === 401 && /valid JWT/i.test(r.text)) {
|
|
350
|
-
return rawCall(url, method, data, { "X-Agent-ID": await walletJwt(), ...extra });
|
|
351
|
-
}
|
|
352
|
-
return r;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
319
|
async function ensureMandate(currency, budget, seconds, url) {
|
|
356
320
|
const cfg = loadConfig();
|
|
357
321
|
// only reuse a signed mandate of the SAME currency with budget left
|
|
@@ -381,101 +345,7 @@ async function ensureMandate(currency, budget, seconds, url) {
|
|
|
381
345
|
const mm = s.mandate || s;
|
|
382
346
|
if (mm && mm.status === "signed") { console.error(c.green(" ✓ mandate signed")); return mandateId; }
|
|
383
347
|
}
|
|
384
|
-
die("mandate not signed in time — re-run
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function descriptorEndpoints(text) {
|
|
388
|
-
try { const j = JSON.parse(text); if (j && Array.isArray(j.endpoints)) return j; } catch {}
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
async function cmdCall(url, opts = {}) {
|
|
393
|
-
if (!url) {
|
|
394
|
-
return die("usage: planner call <url|slug> [--endpoint <path>] [--data '<json>'] [--header 'K: V']… [--pay credits|usdc] [--method GET|POST] [--budget <units>] [--raw]");
|
|
395
|
-
}
|
|
396
|
-
// accept a bare AgentMarket slug (e.g. `web-search-api`) → the proxy API base.
|
|
397
|
-
// full http(s) URLs are used as-is.
|
|
398
|
-
if (!/^https?:\/\//.test(url)) url = `${PROXY}/api/${encodeURIComponent(url)}`;
|
|
399
|
-
const pay = (opts.pay || loadConfig().pay || "credits").toLowerCase();
|
|
400
|
-
const currency = pay === "usdc" ? "USDC" : "FLUXA_MONETIZE_CREDITS";
|
|
401
|
-
const budget = opts.budget ? Number(opts.budget) : (pay === "usdc" ? 5000000 : 500); // ~$5 default
|
|
402
|
-
const seconds = opts.seconds ? Number(opts.seconds) : 28800; // 8h
|
|
403
|
-
|
|
404
|
-
// user-supplied request headers (repeatable --header "K: V") are merged
|
|
405
|
-
// into every outbound call below. A user value wins over the auto-attached
|
|
406
|
-
// X-Agent-ID; X-Payment is the one exception, see the retry step. We warn
|
|
407
|
-
// up front when a user header would override an auto-attached one so the
|
|
408
|
-
// override isn't silent.
|
|
409
|
-
const userHeaders = opts.headers || {};
|
|
410
|
-
const reservedNames = ["x-agent-id", "x-payment"];
|
|
411
|
-
for (const k of Object.keys(userHeaders)) {
|
|
412
|
-
if (reservedNames.includes(k.toLowerCase())) {
|
|
413
|
-
console.error(c.dim(`! --header ${k} overrides the auto-attached value`));
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
await ensureAgent();
|
|
418
|
-
|
|
419
|
-
let targetUrl = url;
|
|
420
|
-
let method = (opts.method || (opts.data ? "POST" : "GET")).toUpperCase();
|
|
421
|
-
const data = opts.data;
|
|
422
|
-
|
|
423
|
-
// proxy endpoints are two-level: GET <base> returns a descriptor listing the
|
|
424
|
-
// real paid sub-endpoints. Resolve it (unless --raw) so the agent doesn't have
|
|
425
|
-
// to read the catalog itself. We never spend without a supplied body.
|
|
426
|
-
if (!opts.raw) {
|
|
427
|
-
const probe = await agentCall(url, "GET", undefined, userHeaders);
|
|
428
|
-
const desc = probe.status === 200 ? descriptorEndpoints(probe.text) : null;
|
|
429
|
-
if (desc) {
|
|
430
|
-
const eps = desc.endpoints.filter((e) => e.enabled !== false);
|
|
431
|
-
const norm = (p) => "/" + String(p || "").replace(/^\//, "");
|
|
432
|
-
let ep = opts.endpoint ? eps.find((e) => norm(e.path) === norm(opts.endpoint)) : null;
|
|
433
|
-
if (!ep) {
|
|
434
|
-
const paid = eps.filter((e) => Number(e.price || 0) > 0);
|
|
435
|
-
ep = paid.length === 1 ? paid[0] : null;
|
|
436
|
-
}
|
|
437
|
-
if (!ep) {
|
|
438
|
-
console.error(c.bold(`\n ${desc.name || "endpoint"}`) + c.dim(" — pick one with --endpoint <path>:"));
|
|
439
|
-
for (const e of eps) {
|
|
440
|
-
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 || "")}`);
|
|
441
|
-
}
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
targetUrl = url.replace(/\/$/, "") + norm(ep.path);
|
|
445
|
-
method = opts.method ? opts.method.toUpperCase() : (ep.method || "GET").toUpperCase();
|
|
446
|
-
const bodyFields = ep.inputSchema && Array.isArray(ep.inputSchema.bodyFields) ? ep.inputSchema.bodyFields : [];
|
|
447
|
-
if (bodyFields.length > 0 && !data) {
|
|
448
|
-
console.error(c.bold(`\n ${method} ${norm(ep.path)}`) + c.dim(` ${ep.price ? `${ep.price} ${ep.currency || ""}` : "free"}`));
|
|
449
|
-
console.error(c.dim(" needs a JSON body — pass it with --data '<json>'. Expected shape:"));
|
|
450
|
-
const ex = bodyFields[0]?.description;
|
|
451
|
-
if (ex) console.error(c.gray(" " + String(ex).replace(/\n/g, "\n ")));
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// call the resolved endpoint — pay if it answers 402
|
|
458
|
-
let r = await agentCall(targetUrl, method, data, userHeaders);
|
|
459
|
-
if (r.status !== 402) {
|
|
460
|
-
if (r.status >= 400) die(`${r.status} — ${r.text.slice(0, 300)}`);
|
|
461
|
-
return console.log(r.text); // raw body, no post-processing
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
console.error(c.dim(`· 402 Payment Required — settling via x402 v3 (${currency})`));
|
|
465
|
-
const mandateId = await ensureMandate(currency, budget, seconds, targetUrl);
|
|
466
|
-
|
|
467
|
-
// 2. sign the payment for this exact 402 challenge (pass the FULL body)
|
|
468
|
-
const paid = unwrap(await walletJson(["x402-v3", "--mandate", mandateId, "--payload", r.text]));
|
|
469
|
-
const xPayment = paid.xPaymentB64 || paid.xPayment;
|
|
470
|
-
if (!xPayment) die("x402-v3 did not return a payment token (xPaymentB64).");
|
|
471
|
-
|
|
472
|
-
// 3. retry with the payment token (against the resolved endpoint, with
|
|
473
|
-
// X-Agent-ID + user headers). The freshly-minted X-Payment is appended
|
|
474
|
-
// LAST so it wins over any user-supplied X-Payment — preserving the x402
|
|
475
|
-
// settlement that the CLI just signed for this exact 402 challenge.
|
|
476
|
-
r = await agentCall(targetUrl, method, opts.data, { ...userHeaders, "X-Payment": xPayment });
|
|
477
|
-
if (r.status >= 400) die(`paid retry failed: ${r.status} — ${r.text.slice(0, 300)}`);
|
|
478
|
-
console.log(r.text); // raw body, no post-processing
|
|
348
|
+
die("mandate not signed in time — re-run once you've approved the link.");
|
|
479
349
|
}
|
|
480
350
|
|
|
481
351
|
// --- topup (prepay Units to a merchant) -------------------------------------
|
|
@@ -577,7 +447,7 @@ async function cmdApiCreate(opts) {
|
|
|
577
447
|
if (data.proxyUrl) console.log(c.dim(` proxy: ${data.proxyUrl}`));
|
|
578
448
|
console.log(c.dim(
|
|
579
449
|
data.status === "published"
|
|
580
|
-
? ` test it:
|
|
450
|
+
? ` test it: curl ${data.proxyUrl || `${PROXY}/api/${data.slug}`}`
|
|
581
451
|
: ` publish: planner api edit ${data.slug || data.configId} --publish`,
|
|
582
452
|
));
|
|
583
453
|
}
|
|
@@ -683,15 +553,14 @@ ${c.bold("planner")} ${c.dim("— what you're working with")}
|
|
|
683
553
|
Balances are ${c.bold("per merchant")} (prepaid Units). ${c.cyan("planner topup <merchant>")} to prefund.
|
|
684
554
|
|
|
685
555
|
${c.bold("Paying (x402)")}
|
|
686
|
-
A paid call returns HTTP 402 →
|
|
687
|
-
mandate you sign once (budget + window), then
|
|
556
|
+
A paid call returns HTTP 402 → settle it with the ${c.dim("fluxa-wallet")} CLI inside a spending
|
|
557
|
+
mandate you sign once (budget + window), then reuse it. ${c.cyan("planner topup")} prefunds instead.
|
|
688
558
|
|
|
689
559
|
${c.bold("Auth")} an ${c.dim("fxa_live_")} key OR an auto-minted agent VC. Nothing to log in.
|
|
690
560
|
${c.bold("Bases")} platform ${c.dim(new URL(PLATFORM).host)} · proxy ${c.dim(new URL(PROXY).host)}
|
|
691
561
|
|
|
692
562
|
${c.bold("Commands")}
|
|
693
563
|
${c.cyan('plan "<task>"')} recommend tools for a task
|
|
694
|
-
${c.cyan("call <url> --data '…'")} make one paid API call (handles the 402)
|
|
695
564
|
${c.cyan("topup <merchant>")} prepay Units
|
|
696
565
|
${c.cyan('search "<q>"')} discover apis/models/skills
|
|
697
566
|
${c.dim("models · balance · ledger · keys · whoami")}
|
|
@@ -729,19 +598,21 @@ ${c.bold("API keys — programmatic management")} ${c.dim("(Agent VC only)")}
|
|
|
729
598
|
pay: () => `
|
|
730
599
|
${c.bold("Paying — x402 v3")}
|
|
731
600
|
Prepaid: each merchant has a Units balance; while it's funded, calls just work.
|
|
732
|
-
On a shortfall a paid endpoint returns HTTP 402 with an x402 challenge.
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
601
|
+
On a shortfall a paid endpoint returns HTTP 402 with an x402 challenge. Settle it with
|
|
602
|
+
the ${c.dim("fluxa-wallet")} CLI:
|
|
603
|
+
1. sign a spending ${c.bold("mandate")} once (you pre-approve a budget + time window)
|
|
604
|
+
${c.dim("fluxa-wallet mandate-create --amount <units> --seconds <ttl> --currency <…>")}
|
|
605
|
+
2. settle the 402 challenge ${c.dim("fluxa-wallet x402-v3 --mandate <id> --payload <402 body>")}
|
|
606
|
+
3. retry the call with the returned payment token in the ${c.dim("X-Payment")} header
|
|
607
|
+
Reuse the signed mandate for later calls in its window. ${c.cyan("planner topup")} prefunds instead.
|
|
737
608
|
`,
|
|
738
609
|
apis: () => `
|
|
739
610
|
${c.bold("Oneshot APIs")}
|
|
740
611
|
${c.dim("/api/{config}")} is two-level: ${c.dim("GET <base>")} returns a descriptor of sub-endpoints
|
|
741
612
|
(path, method, price, input schema). The paid action is a sub-endpoint.
|
|
742
|
-
${c.
|
|
743
|
-
|
|
744
|
-
${c.dim("
|
|
613
|
+
${c.dim(`curl ${new URL(PROXY).host}/api/<slug>`)} list the sub-endpoints + their input schema
|
|
614
|
+
Call a sub-endpoint; on a 402, settle with ${c.dim("fluxa-wallet")} (see ${c.cyan("planner info pay")}) and
|
|
615
|
+
retry with the ${c.dim("X-Payment")} token. ${c.dim("X-Agent-ID")} is optional (attribution only).
|
|
745
616
|
${c.bold("Manage your APIs")} ${c.dim("(fxa_live_ key or auto-minted Agent VC)")}
|
|
746
617
|
${c.cyan("planner api create --file <spec.json> [--publish]")} create (draft unless --publish)
|
|
747
618
|
${c.cyan("planner api list")} your api configs
|
|
@@ -781,9 +652,7 @@ ${c.bold("planner")} ${c.dim("— FluxA Planner CLI (live)")}
|
|
|
781
652
|
|
|
782
653
|
${c.bold("Headline")}
|
|
783
654
|
planner plan "<task>" recommend the models, APIs & skills for a task
|
|
784
|
-
|
|
785
|
-
[--pay credits|usdc] [--method GET|POST] [--data '<json>']
|
|
786
|
-
[--header "Name: Value"] (repeatable; e.g. an upstream Bearer token)
|
|
655
|
+
(run the recommended calls yourself; pay 402s via fluxa-wallet)
|
|
787
656
|
|
|
788
657
|
${c.bold("Primitives")} ${c.dim("(all live)")}
|
|
789
658
|
planner search "<q>" [--type api,skill,model] discover resources
|
|
@@ -811,31 +680,9 @@ function die(msg, ctx) {
|
|
|
811
680
|
async function main() {
|
|
812
681
|
const args = argv.slice(2);
|
|
813
682
|
const cmd = args[0];
|
|
814
|
-
const FLAGS = ["--type", "--vendor", "--
|
|
815
|
-
const BOOLS = ["--
|
|
683
|
+
const FLAGS = ["--type", "--vendor", "--budget", "--seconds", "--credits", "--bundle", "--name", "--cap", "--file", "--upstream", "--wallet"];
|
|
684
|
+
const BOOLS = ["--publish", "--private", "--require-agent-auth", "--dry-run"];
|
|
816
685
|
const getFlag = (n) => { const i = args.indexOf(n); return i !== -1 ? args[i + 1] : null; };
|
|
817
|
-
// repeatable flag → array of all values (in order)
|
|
818
|
-
const getFlagAll = (n) => {
|
|
819
|
-
const out = [];
|
|
820
|
-
for (let i = 0; i < args.length - 1; i++) if (args[i] === n) out.push(args[i + 1]);
|
|
821
|
-
return out;
|
|
822
|
-
};
|
|
823
|
-
// parse `--header "Name: Value"` (repeatable) into a headers object. Splits
|
|
824
|
-
// on the FIRST colon so JWTs/URLs in the value are preserved. Malformed
|
|
825
|
-
// entries are warned and skipped, never thrown — a broken header shouldn't
|
|
826
|
-
// poison the whole call.
|
|
827
|
-
const parseHeaders = (specs) => {
|
|
828
|
-
const out = {};
|
|
829
|
-
for (const s of specs) {
|
|
830
|
-
const i = s.indexOf(":");
|
|
831
|
-
if (i <= 0) { console.error(c.dim(`! ignoring malformed --header "${s}" (expected "Name: Value")`)); continue; }
|
|
832
|
-
const name = s.slice(0, i).trim();
|
|
833
|
-
const value = s.slice(i + 1).trim();
|
|
834
|
-
if (!name) continue;
|
|
835
|
-
out[name] = value;
|
|
836
|
-
}
|
|
837
|
-
return out;
|
|
838
|
-
};
|
|
839
686
|
const type = getFlag("--type");
|
|
840
687
|
const vendor = getFlag("--vendor");
|
|
841
688
|
const rest = [];
|
|
@@ -848,13 +695,6 @@ async function main() {
|
|
|
848
695
|
|
|
849
696
|
switch (cmd) {
|
|
850
697
|
case "plan": return cmdPlan(arg);
|
|
851
|
-
case "call":
|
|
852
|
-
return cmdCall(rest[0], {
|
|
853
|
-
pay: getFlag("--pay"), method: getFlag("--method"), data: getFlag("--data"),
|
|
854
|
-
budget: getFlag("--budget"), seconds: getFlag("--seconds"),
|
|
855
|
-
endpoint: getFlag("--endpoint"), raw: args.includes("--raw"),
|
|
856
|
-
headers: parseHeaders(getFlagAll("--header")),
|
|
857
|
-
});
|
|
858
698
|
case "search": return cmdSearch(arg, type);
|
|
859
699
|
case "models": return cmdModels(vendor);
|
|
860
700
|
case "vendors": return cmdVendors();
|