@fluxa-pay/planner 0.2.3 → 0.2.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fluxa-pay/planner",
3
- "version": "0.2.3",
4
- "description": "FluxA Planner CLI. Recommend the right models, APIs and skills for a task, run paid calls via x402, and manage your oneshot APIs.",
3
+ "version": "0.2.5",
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"
@@ -10,7 +10,8 @@
10
10
  "node": ">=20"
11
11
  },
12
12
  "files": [
13
- "planner.mjs"
13
+ "planner.mjs",
14
+ "plan-format.mjs"
14
15
  ],
15
16
  "publishConfig": {
16
17
  "access": "public"
@@ -0,0 +1,49 @@
1
+ // Pure formatting of a `planner plan` result into terminal lines. No I/O and no
2
+ // color of its own — the caller passes a helpers bag (h) so planner.mjs injects
3
+ // ANSI while tests pass identity helpers and assert on plain text.
4
+ //
5
+ // When the plan carries capability `groups`, each capability prints its options
6
+ // (the recommended one first, tagged) so the agent sees the alternatives. Older
7
+ // / template plans without groups fall back to the flat `tools` list.
8
+ export function planLines(plan, h) {
9
+ const lines = [];
10
+ const heading = h.unesc(plan.reason || plan.task || "Recommended");
11
+ lines.push("");
12
+ lines.push(" " + h.bold("Recommended") + " " + h.dim(heading));
13
+
14
+ const groups = Array.isArray(plan.groups) ? plan.groups : null;
15
+ let total = 0;
16
+
17
+ if (groups && groups.length) {
18
+ for (const g of groups) {
19
+ lines.push(" " + h.dim(h.unesc(g.capability || "")));
20
+ const lead = g.options.find((o) => o.recommended) || g.options[0];
21
+ const ordered = lead ? [lead, ...g.options.filter((o) => o !== lead)] : g.options;
22
+ if (lead) total += lead.units || 0;
23
+ for (const o of ordered) {
24
+ const k = h.kind(o.kind);
25
+ const tag = o === lead ? " " + h.lime("recommended") : "";
26
+ lines.push(
27
+ ` ${k} ${h.bold(h.pad(o.slug, 34))} ${h.dim(h.pad(h.unesc(o.detail || ""), 24))} ${h.lime("~" + (o.units || 0).toLocaleString() + " Units")}${tag}`,
28
+ );
29
+ }
30
+ }
31
+ } else {
32
+ for (const t of plan.tools || []) {
33
+ total += t.units || 0;
34
+ const k = h.kind(t.kind);
35
+ lines.push(
36
+ ` ${k} ${h.bold(h.pad(t.slug, 36))} ${h.dim(h.pad(h.unesc(t.detail || ""), 30))} ${h.lime("~" + (t.units || 0).toLocaleString() + " Units")}`,
37
+ );
38
+ }
39
+ }
40
+
41
+ lines.push(h.gray(" " + "─".repeat(78)));
42
+ lines.push(` ${h.bold("est")} ${h.lime("~" + total.toLocaleString() + " Units")} ${h.dim("(~" + h.usd(total) + ")")}`);
43
+ if (plan.instructions) {
44
+ lines.push("");
45
+ lines.push(" " + h.dim(h.unesc(plan.instructions)));
46
+ }
47
+ lines.push(h.dim("\n → Run these yourself. Paid endpoints answer HTTP 402; settle with fluxa-wallet (planner info pay)."));
48
+ return { lines, total };
49
+ }
package/planner.mjs CHANGED
@@ -26,6 +26,7 @@ import { promisify } from "node:util";
26
26
  import { homedir } from "node:os";
27
27
  import { join } from "node:path";
28
28
  import { stdout, argv, env, exit } from "node:process";
29
+ import { planLines } from "./plan-format.mjs";
29
30
 
30
31
  const pexec = promisify(execFile);
31
32
 
@@ -281,12 +282,9 @@ async function keysRevoke(id) {
281
282
  console.log(c.green("✓") + ` revoked ${id} — it stops authenticating on its next call`);
282
283
  }
283
284
 
284
- // --- paid call (the x402 v3 dance, wrapped) ---------------------------------
285
- // `planner call <url>` collapses the 10-step manual flow into one command:
286
- // register discover ensure a signed mandate call → on 402 settle via
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.
285
+ // --- wallet helpers (x402 v3 settlement, shared by topup) --------------------
286
+ // The wallet CLI does the signing; planner reuses a signed, unexpired mandate so
287
+ // the user signs once per budget window, not once per settlement.
290
288
  async function walletRaw(args) {
291
289
  const { stdout: out } = await pexec("fluxa-wallet", args);
292
290
  return out;
@@ -319,39 +317,6 @@ async function rawCall(url, method, data, extraHeaders = {}, auth = false) {
319
317
  return { status: res.status, text };
320
318
  }
321
319
 
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
320
  async function ensureMandate(currency, budget, seconds, url) {
356
321
  const cfg = loadConfig();
357
322
  // only reuse a signed mandate of the SAME currency with budget left
@@ -381,101 +346,7 @@ async function ensureMandate(currency, budget, seconds, url) {
381
346
  const mm = s.mandate || s;
382
347
  if (mm && mm.status === "signed") { console.error(c.green(" ✓ mandate signed")); return mandateId; }
383
348
  }
384
- die("mandate not signed in time — re-run `planner call` once you've approved the link.");
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
349
+ die("mandate not signed in time — re-run once you've approved the link.");
479
350
  }
480
351
 
481
352
  // --- topup (prepay Units to a merchant) -------------------------------------
@@ -577,7 +448,7 @@ async function cmdApiCreate(opts) {
577
448
  if (data.proxyUrl) console.log(c.dim(` proxy: ${data.proxyUrl}`));
578
449
  console.log(c.dim(
579
450
  data.status === "published"
580
- ? ` test it: planner call ${data.proxyUrl || data.slug}`
451
+ ? ` test it: curl ${data.proxyUrl || `${PROXY}/api/${data.slug}`}`
581
452
  : ` publish: planner api edit ${data.slug || data.configId} --publish`,
582
453
  ));
583
454
  }
@@ -649,19 +520,12 @@ function printAnswer(a) {
649
520
 
650
521
  // Render a PlanOrAnswer plan: tools carry kind/slug/name/units/detail.
651
522
  function printPlan(plan) {
652
- const tools = plan.tools || [];
653
- const heading = unesc(plan.reason || plan.task || "Recommended");
654
- console.log("\n " + c.bold("Recommended") + " " + c.dim(heading));
655
- let total = 0;
656
- for (const t of tools) {
657
- const k = KIND[t.kind] || pad(t.kind, 5);
658
- total += t.units || 0;
659
- console.log(` ${k} ${c.bold(pad(t.slug, 36))} ${c.dim(pad(unesc(t.detail || ""), 30))} ${c.lime("~" + (t.units || 0).toLocaleString() + " Units")}`);
660
- }
661
- console.log(c.gray(" " + "─".repeat(78)));
662
- console.log(` ${c.bold("est")} ${c.lime("~" + total.toLocaleString() + " Units")} ${c.dim("(~" + usd(total) + ")")}`);
663
- if (plan.instructions) console.log("\n " + c.dim(unesc(plan.instructions)));
664
- console.log(c.dim("\n → call these tools directly — FluxA settles each paid call from your wallet. No keys, no subs."));
523
+ const { lines, total } = planLines(plan, {
524
+ bold: c.bold, dim: c.dim, lime: c.lime, gray: c.gray,
525
+ kind: (k) => KIND[k] || pad(k, 5),
526
+ pad, usd, unesc,
527
+ });
528
+ for (const l of lines) console.log(l);
665
529
  return total;
666
530
  }
667
531
 
@@ -683,15 +547,14 @@ ${c.bold("planner")} ${c.dim("— what you're working with")}
683
547
  Balances are ${c.bold("per merchant")} (prepaid Units). ${c.cyan("planner topup <merchant>")} to prefund.
684
548
 
685
549
  ${c.bold("Paying (x402)")}
686
- A paid call returns HTTP 402 → planner settles it via ${c.dim("fluxa-wallet")} inside a spending
687
- mandate you sign once (budget + window), then reuses it. planner never holds keys.
550
+ A paid call returns HTTP 402 → settle it with the ${c.dim("fluxa-wallet")} CLI inside a spending
551
+ mandate you sign once (budget + window), then reuse it. ${c.cyan("planner topup")} prefunds instead.
688
552
 
689
553
  ${c.bold("Auth")} an ${c.dim("fxa_live_")} key OR an auto-minted agent VC. Nothing to log in.
690
554
  ${c.bold("Bases")} platform ${c.dim(new URL(PLATFORM).host)} · proxy ${c.dim(new URL(PROXY).host)}
691
555
 
692
556
  ${c.bold("Commands")}
693
557
  ${c.cyan('plan "<task>"')} recommend tools for a task
694
- ${c.cyan("call <url> --data '…'")} make one paid API call (handles the 402)
695
558
  ${c.cyan("topup <merchant>")} prepay Units
696
559
  ${c.cyan('search "<q>"')} discover apis/models/skills
697
560
  ${c.dim("models · balance · ledger · keys · whoami")}
@@ -729,19 +592,21 @@ ${c.bold("API keys — programmatic management")} ${c.dim("(Agent VC only)")}
729
592
  pay: () => `
730
593
  ${c.bold("Paying — x402 v3")}
731
594
  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. planner then:
733
- 1. ensures a signed spending ${c.bold("mandate")} (you pre-approve a budget + time window — once)
734
- 2. signs the 402 via ${c.dim("fluxa-wallet x402-v3")} (planner never holds keys)
735
- 3. retries with the payment token
736
- It reuses the signed mandate for later calls in its window. ${c.cyan("planner topup")} prefunds instead.
595
+ On a shortfall a paid endpoint returns HTTP 402 with an x402 challenge. Settle it with
596
+ the ${c.dim("fluxa-wallet")} CLI:
597
+ 1. sign a spending ${c.bold("mandate")} once (you pre-approve a budget + time window)
598
+ ${c.dim("fluxa-wallet mandate-create --amount <units> --seconds <ttl> --currency <…>")}
599
+ 2. settle the 402 challenge ${c.dim("fluxa-wallet x402-v3 --mandate <id> --payload <402 body>")}
600
+ 3. retry the call with the returned payment token in the ${c.dim("X-Payment")} header
601
+ Reuse the signed mandate for later calls in its window. ${c.cyan("planner topup")} prefunds instead.
737
602
  `,
738
603
  apis: () => `
739
604
  ${c.bold("Oneshot APIs")}
740
605
  ${c.dim("/api/{config}")} is two-level: ${c.dim("GET <base>")} returns a descriptor of sub-endpoints
741
606
  (path, method, price, input schema). The paid action is a sub-endpoint.
742
- ${c.cyan("planner call <base>")} show the paid endpoint + its required body
743
- ${c.cyan("planner call <base> --data '<json>'")} call it; pay the 402 if needed
744
- ${c.dim("--endpoint <path>")} picks when there are several; ${c.dim("--raw")} hits a URL directly.
607
+ ${c.dim(`curl ${new URL(PROXY).host}/api/<slug>`)} list the sub-endpoints + their input schema
608
+ Call a sub-endpoint; on a 402, settle with ${c.dim("fluxa-wallet")} (see ${c.cyan("planner info pay")}) and
609
+ retry with the ${c.dim("X-Payment")} token. ${c.dim("X-Agent-ID")} is optional (attribution only).
745
610
  ${c.bold("Manage your APIs")} ${c.dim("(fxa_live_ key or auto-minted Agent VC)")}
746
611
  ${c.cyan("planner api create --file <spec.json> [--publish]")} create (draft unless --publish)
747
612
  ${c.cyan("planner api list")} your api configs
@@ -781,9 +646,7 @@ ${c.bold("planner")} ${c.dim("— FluxA Planner CLI (live)")}
781
646
 
782
647
  ${c.bold("Headline")}
783
648
  planner plan "<task>" recommend the models, APIs & skills for a task
784
- planner call <url> make a paid API call handles the x402 mandate + payment
785
- [--pay credits|usdc] [--method GET|POST] [--data '<json>']
786
- [--header "Name: Value"] (repeatable; e.g. an upstream Bearer token)
649
+ (run the recommended calls yourself; pay 402s via fluxa-wallet)
787
650
 
788
651
  ${c.bold("Primitives")} ${c.dim("(all live)")}
789
652
  planner search "<q>" [--type api,skill,model] discover resources
@@ -811,31 +674,9 @@ function die(msg, ctx) {
811
674
  async function main() {
812
675
  const args = argv.slice(2);
813
676
  const cmd = args[0];
814
- const FLAGS = ["--type", "--vendor", "--pay", "--method", "--data", "--budget", "--seconds", "--endpoint", "--credits", "--bundle", "--name", "--cap", "--file", "--upstream", "--wallet", "--header"];
815
- const BOOLS = ["--raw", "--publish", "--private", "--require-agent-auth", "--dry-run"];
677
+ const FLAGS = ["--type", "--vendor", "--budget", "--seconds", "--credits", "--bundle", "--name", "--cap", "--file", "--upstream", "--wallet"];
678
+ const BOOLS = ["--publish", "--private", "--require-agent-auth", "--dry-run"];
816
679
  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
680
  const type = getFlag("--type");
840
681
  const vendor = getFlag("--vendor");
841
682
  const rest = [];
@@ -848,13 +689,6 @@ async function main() {
848
689
 
849
690
  switch (cmd) {
850
691
  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
692
  case "search": return cmdSearch(arg, type);
859
693
  case "models": return cmdModels(vendor);
860
694
  case "vendors": return cmdVendors();