@fluxa-pay/planner 0.2.2 → 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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/planner.mjs +32 -186
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fluxa-pay/planner",
3
- "version": "0.2.2",
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.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
@@ -47,6 +47,12 @@ const KIND = { model: c.cyan("model"), api: c.lime("api "), skill: c.green("ski
47
47
  const pad = (s, n) => { s = String(s); return s.length > n ? s.slice(0, n - 1) + "…" : s.padEnd(n); };
48
48
  const usd = (units) => { const v = (units || 0) * UNIT_USD; return v < 0.01 ? `$${v.toFixed(4)}` : `$${v.toFixed(2)}`; };
49
49
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
50
+ // The planner brain occasionally HTML-escapes angle brackets in free text
51
+ // (e.g. "&lt;merchant&gt;"); un-escape the common entities for terminal output.
52
+ // &amp; is undone LAST so "&amp;lt;" doesn't collapse into "<".
53
+ const unesc = (s) => typeof s === "string"
54
+ ? s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#3?9;/g, "'").replace(/&amp;/g, "&")
55
+ : s;
50
56
 
51
57
  // --- config / auth ----------------------------------------------------------
52
58
  function loadConfig() {
@@ -275,12 +281,9 @@ async function keysRevoke(id) {
275
281
  console.log(c.green("✓") + ` revoked ${id} — it stops authenticating on its next call`);
276
282
  }
277
283
 
278
- // --- paid call (the x402 v3 dance, wrapped) ---------------------------------
279
- // `planner call <url>` collapses the 10-step manual flow into one command:
280
- // register discover ensure a signed mandate call → on 402 settle via
281
- // `fluxa-wallet x402-v3` → retry with the payment token → return the raw body.
282
- // Signing the mandate is the one human step; planner reuses a signed, unexpired
283
- // 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.
284
287
  async function walletRaw(args) {
285
288
  const { stdout: out } = await pexec("fluxa-wallet", args);
286
289
  return out;
@@ -313,39 +316,6 @@ async function rawCall(url, method, data, extraHeaders = {}, auth = false) {
313
316
  return { status: res.status, text };
314
317
  }
315
318
 
316
- // The proxy's API/MCP lanes bill the caller identified by the X-Agent-ID header
317
- // and reject every non-discovery request without it. Default to the agent_id
318
- // (UUID) from `fluxa-wallet status`; configs with require_agent_auth need a JWT
319
- // instead, so agentCall() retries with a freshly minted wallet JWT on the
320
- // "valid JWT" 401. Both values are cached for the process.
321
- let _agentId = null;
322
- async function agentId() {
323
- if (_agentId) return _agentId;
324
- const s = unwrap(await walletJson(["status"]).catch(() => ({})));
325
- const id = s.agent_id || s.agentId || s.id;
326
- if (!id) die("could not read agent_id from `fluxa-wallet status` — register once with `fluxa-wallet init`.");
327
- return (_agentId = id);
328
- }
329
- let _walletJwt = null;
330
- async function walletJwt() {
331
- if (_walletJwt) return _walletJwt;
332
- const j = unwrap(await walletJson(["refreshJWT"]).catch(() => ({})));
333
- const jwt = j.jwt || j.token;
334
- if (!jwt) die("could not mint a wallet JWT (`fluxa-wallet refreshJWT`).");
335
- return (_walletJwt = jwt);
336
- }
337
- // rawCall + X-Agent-ID, with the UUID→JWT fallback for require_agent_auth configs.
338
- // Auto-attached headers go FIRST so a caller-supplied `extra` (e.g. a user's
339
- // `--header "X-Agent-ID: …"`) can override — collisions are surfaced upstream
340
- // in cmdCall so the override isn't silent.
341
- async function agentCall(url, method, data, extra = {}) {
342
- const r = await rawCall(url, method, data, { "X-Agent-ID": await agentId(), ...extra });
343
- if (r.status === 401 && /valid JWT/i.test(r.text)) {
344
- return rawCall(url, method, data, { "X-Agent-ID": await walletJwt(), ...extra });
345
- }
346
- return r;
347
- }
348
-
349
319
  async function ensureMandate(currency, budget, seconds, url) {
350
320
  const cfg = loadConfig();
351
321
  // only reuse a signed mandate of the SAME currency with budget left
@@ -375,101 +345,7 @@ async function ensureMandate(currency, budget, seconds, url) {
375
345
  const mm = s.mandate || s;
376
346
  if (mm && mm.status === "signed") { console.error(c.green(" ✓ mandate signed")); return mandateId; }
377
347
  }
378
- die("mandate not signed in time — re-run `planner call` once you've approved the link.");
379
- }
380
-
381
- function descriptorEndpoints(text) {
382
- try { const j = JSON.parse(text); if (j && Array.isArray(j.endpoints)) return j; } catch {}
383
- return null;
384
- }
385
-
386
- async function cmdCall(url, opts = {}) {
387
- if (!url) {
388
- return die("usage: planner call <url|slug> [--endpoint <path>] [--data '<json>'] [--header 'K: V']… [--pay credits|usdc] [--method GET|POST] [--budget <units>] [--raw]");
389
- }
390
- // accept a bare AgentMarket slug (e.g. `web-search-api`) → the proxy API base.
391
- // full http(s) URLs are used as-is.
392
- if (!/^https?:\/\//.test(url)) url = `${PROXY}/api/${encodeURIComponent(url)}`;
393
- const pay = (opts.pay || loadConfig().pay || "credits").toLowerCase();
394
- const currency = pay === "usdc" ? "USDC" : "FLUXA_MONETIZE_CREDITS";
395
- const budget = opts.budget ? Number(opts.budget) : (pay === "usdc" ? 5000000 : 500); // ~$5 default
396
- const seconds = opts.seconds ? Number(opts.seconds) : 28800; // 8h
397
-
398
- // user-supplied request headers (repeatable --header "K: V") are merged
399
- // into every outbound call below. A user value wins over the auto-attached
400
- // X-Agent-ID; X-Payment is the one exception, see the retry step. We warn
401
- // up front when a user header would override an auto-attached one so the
402
- // override isn't silent.
403
- const userHeaders = opts.headers || {};
404
- const reservedNames = ["x-agent-id", "x-payment"];
405
- for (const k of Object.keys(userHeaders)) {
406
- if (reservedNames.includes(k.toLowerCase())) {
407
- console.error(c.dim(`! --header ${k} overrides the auto-attached value`));
408
- }
409
- }
410
-
411
- await ensureAgent();
412
-
413
- let targetUrl = url;
414
- let method = (opts.method || (opts.data ? "POST" : "GET")).toUpperCase();
415
- const data = opts.data;
416
-
417
- // proxy endpoints are two-level: GET <base> returns a descriptor listing the
418
- // real paid sub-endpoints. Resolve it (unless --raw) so the agent doesn't have
419
- // to read the catalog itself. We never spend without a supplied body.
420
- if (!opts.raw) {
421
- const probe = await agentCall(url, "GET", undefined, userHeaders);
422
- const desc = probe.status === 200 ? descriptorEndpoints(probe.text) : null;
423
- if (desc) {
424
- const eps = desc.endpoints.filter((e) => e.enabled !== false);
425
- const norm = (p) => "/" + String(p || "").replace(/^\//, "");
426
- let ep = opts.endpoint ? eps.find((e) => norm(e.path) === norm(opts.endpoint)) : null;
427
- if (!ep) {
428
- const paid = eps.filter((e) => Number(e.price || 0) > 0);
429
- ep = paid.length === 1 ? paid[0] : null;
430
- }
431
- if (!ep) {
432
- console.error(c.bold(`\n ${desc.name || "endpoint"}`) + c.dim(" — pick one with --endpoint <path>:"));
433
- for (const e of eps) {
434
- 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 || "")}`);
435
- }
436
- return;
437
- }
438
- targetUrl = url.replace(/\/$/, "") + norm(ep.path);
439
- method = opts.method ? opts.method.toUpperCase() : (ep.method || "GET").toUpperCase();
440
- const bodyFields = ep.inputSchema && Array.isArray(ep.inputSchema.bodyFields) ? ep.inputSchema.bodyFields : [];
441
- if (bodyFields.length > 0 && !data) {
442
- console.error(c.bold(`\n ${method} ${norm(ep.path)}`) + c.dim(` ${ep.price ? `${ep.price} ${ep.currency || ""}` : "free"}`));
443
- console.error(c.dim(" needs a JSON body — pass it with --data '<json>'. Expected shape:"));
444
- const ex = bodyFields[0]?.description;
445
- if (ex) console.error(c.gray(" " + String(ex).replace(/\n/g, "\n ")));
446
- return;
447
- }
448
- }
449
- }
450
-
451
- // call the resolved endpoint — pay if it answers 402
452
- let r = await agentCall(targetUrl, method, data, userHeaders);
453
- if (r.status !== 402) {
454
- if (r.status >= 400) die(`${r.status} — ${r.text.slice(0, 300)}`);
455
- return console.log(r.text); // raw body, no post-processing
456
- }
457
-
458
- console.error(c.dim(`· 402 Payment Required — settling via x402 v3 (${currency})`));
459
- const mandateId = await ensureMandate(currency, budget, seconds, targetUrl);
460
-
461
- // 2. sign the payment for this exact 402 challenge (pass the FULL body)
462
- const paid = unwrap(await walletJson(["x402-v3", "--mandate", mandateId, "--payload", r.text]));
463
- const xPayment = paid.xPaymentB64 || paid.xPayment;
464
- if (!xPayment) die("x402-v3 did not return a payment token (xPaymentB64).");
465
-
466
- // 3. retry with the payment token (against the resolved endpoint, with
467
- // X-Agent-ID + user headers). The freshly-minted X-Payment is appended
468
- // LAST so it wins over any user-supplied X-Payment — preserving the x402
469
- // settlement that the CLI just signed for this exact 402 challenge.
470
- r = await agentCall(targetUrl, method, opts.data, { ...userHeaders, "X-Payment": xPayment });
471
- if (r.status >= 400) die(`paid retry failed: ${r.status} — ${r.text.slice(0, 300)}`);
472
- console.log(r.text); // raw body, no post-processing
348
+ die("mandate not signed in time — re-run once you've approved the link.");
473
349
  }
474
350
 
475
351
  // --- topup (prepay Units to a merchant) -------------------------------------
@@ -571,7 +447,7 @@ async function cmdApiCreate(opts) {
571
447
  if (data.proxyUrl) console.log(c.dim(` proxy: ${data.proxyUrl}`));
572
448
  console.log(c.dim(
573
449
  data.status === "published"
574
- ? ` test it: planner call ${data.proxyUrl || data.slug}`
450
+ ? ` test it: curl ${data.proxyUrl || `${PROXY}/api/${data.slug}`}`
575
451
  : ` publish: planner api edit ${data.slug || data.configId} --publish`,
576
452
  ));
577
453
  }
@@ -629,14 +505,14 @@ async function cmdPlan(task) {
629
505
  }
630
506
 
631
507
  function printAnswer(a) {
632
- console.log("\n " + c.bold(a.answer));
508
+ console.log("\n " + c.bold(unesc(a.answer)));
633
509
  if (a.command) {
634
510
  console.log("\n " + c.dim("run:"));
635
- console.log(" " + c.cyan(a.command));
511
+ console.log(" " + c.cyan(unesc(a.command)));
636
512
  }
637
513
  if (a.prompt) {
638
514
  console.log("\n " + c.dim("copy this prompt for your agent:"));
639
- for (const line of a.prompt.split("\n")) console.log(" " + line);
515
+ for (const line of unesc(a.prompt).split("\n")) console.log(" " + line);
640
516
  }
641
517
  console.log("");
642
518
  }
@@ -644,17 +520,17 @@ function printAnswer(a) {
644
520
  // Render a PlanOrAnswer plan: tools carry kind/slug/name/units/detail.
645
521
  function printPlan(plan) {
646
522
  const tools = plan.tools || [];
647
- const heading = plan.reason || plan.task || "Recommended";
523
+ const heading = unesc(plan.reason || plan.task || "Recommended");
648
524
  console.log("\n " + c.bold("Recommended") + " " + c.dim(heading));
649
525
  let total = 0;
650
526
  for (const t of tools) {
651
527
  const k = KIND[t.kind] || pad(t.kind, 5);
652
528
  total += t.units || 0;
653
- console.log(` ${k} ${c.bold(pad(t.slug, 36))} ${c.dim(pad(t.detail || "", 30))} ${c.lime("~" + (t.units || 0).toLocaleString() + " Units")}`);
529
+ console.log(` ${k} ${c.bold(pad(t.slug, 36))} ${c.dim(pad(unesc(t.detail || ""), 30))} ${c.lime("~" + (t.units || 0).toLocaleString() + " Units")}`);
654
530
  }
655
531
  console.log(c.gray(" " + "─".repeat(78)));
656
532
  console.log(` ${c.bold("est")} ${c.lime("~" + total.toLocaleString() + " Units")} ${c.dim("(~" + usd(total) + ")")}`);
657
- if (plan.instructions) console.log("\n " + c.dim(plan.instructions));
533
+ if (plan.instructions) console.log("\n " + c.dim(unesc(plan.instructions)));
658
534
  console.log(c.dim("\n → call these tools directly — FluxA settles each paid call from your wallet. No keys, no subs."));
659
535
  return total;
660
536
  }
@@ -677,15 +553,14 @@ ${c.bold("planner")} ${c.dim("— what you're working with")}
677
553
  Balances are ${c.bold("per merchant")} (prepaid Units). ${c.cyan("planner topup <merchant>")} to prefund.
678
554
 
679
555
  ${c.bold("Paying (x402)")}
680
- A paid call returns HTTP 402 → planner settles it via ${c.dim("fluxa-wallet")} inside a spending
681
- mandate you sign once (budget + window), then reuses it. planner never holds keys.
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.
682
558
 
683
559
  ${c.bold("Auth")} an ${c.dim("fxa_live_")} key OR an auto-minted agent VC. Nothing to log in.
684
560
  ${c.bold("Bases")} platform ${c.dim(new URL(PLATFORM).host)} · proxy ${c.dim(new URL(PROXY).host)}
685
561
 
686
562
  ${c.bold("Commands")}
687
563
  ${c.cyan('plan "<task>"')} recommend tools for a task
688
- ${c.cyan("call <url> --data '…'")} make one paid API call (handles the 402)
689
564
  ${c.cyan("topup <merchant>")} prepay Units
690
565
  ${c.cyan('search "<q>"')} discover apis/models/skills
691
566
  ${c.dim("models · balance · ledger · keys · whoami")}
@@ -723,19 +598,21 @@ ${c.bold("API keys — programmatic management")} ${c.dim("(Agent VC only)")}
723
598
  pay: () => `
724
599
  ${c.bold("Paying — x402 v3")}
725
600
  Prepaid: each merchant has a Units balance; while it's funded, calls just work.
726
- On a shortfall a paid endpoint returns HTTP 402 with an x402 challenge. planner then:
727
- 1. ensures a signed spending ${c.bold("mandate")} (you pre-approve a budget + time window — once)
728
- 2. signs the 402 via ${c.dim("fluxa-wallet x402-v3")} (planner never holds keys)
729
- 3. retries with the payment token
730
- It reuses the signed mandate for later calls in its window. ${c.cyan("planner topup")} prefunds instead.
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.
731
608
  `,
732
609
  apis: () => `
733
610
  ${c.bold("Oneshot APIs")}
734
611
  ${c.dim("/api/{config}")} is two-level: ${c.dim("GET <base>")} returns a descriptor of sub-endpoints
735
612
  (path, method, price, input schema). The paid action is a sub-endpoint.
736
- ${c.cyan("planner call <base>")} show the paid endpoint + its required body
737
- ${c.cyan("planner call <base> --data '<json>'")} call it; pay the 402 if needed
738
- ${c.dim("--endpoint <path>")} picks when there are several; ${c.dim("--raw")} hits a URL directly.
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).
739
616
  ${c.bold("Manage your APIs")} ${c.dim("(fxa_live_ key or auto-minted Agent VC)")}
740
617
  ${c.cyan("planner api create --file <spec.json> [--publish]")} create (draft unless --publish)
741
618
  ${c.cyan("planner api list")} your api configs
@@ -775,9 +652,7 @@ ${c.bold("planner")} ${c.dim("— FluxA Planner CLI (live)")}
775
652
 
776
653
  ${c.bold("Headline")}
777
654
  planner plan "<task>" recommend the models, APIs & skills for a task
778
- planner call <url> make a paid API call handles the x402 mandate + payment
779
- [--pay credits|usdc] [--method GET|POST] [--data '<json>']
780
- [--header "Name: Value"] (repeatable; e.g. an upstream Bearer token)
655
+ (run the recommended calls yourself; pay 402s via fluxa-wallet)
781
656
 
782
657
  ${c.bold("Primitives")} ${c.dim("(all live)")}
783
658
  planner search "<q>" [--type api,skill,model] discover resources
@@ -805,31 +680,9 @@ function die(msg, ctx) {
805
680
  async function main() {
806
681
  const args = argv.slice(2);
807
682
  const cmd = args[0];
808
- const FLAGS = ["--type", "--vendor", "--pay", "--method", "--data", "--budget", "--seconds", "--endpoint", "--credits", "--bundle", "--name", "--cap", "--file", "--upstream", "--wallet", "--header"];
809
- const BOOLS = ["--raw", "--publish", "--private", "--require-agent-auth", "--dry-run"];
683
+ const FLAGS = ["--type", "--vendor", "--budget", "--seconds", "--credits", "--bundle", "--name", "--cap", "--file", "--upstream", "--wallet"];
684
+ const BOOLS = ["--publish", "--private", "--require-agent-auth", "--dry-run"];
810
685
  const getFlag = (n) => { const i = args.indexOf(n); return i !== -1 ? args[i + 1] : null; };
811
- // repeatable flag → array of all values (in order)
812
- const getFlagAll = (n) => {
813
- const out = [];
814
- for (let i = 0; i < args.length - 1; i++) if (args[i] === n) out.push(args[i + 1]);
815
- return out;
816
- };
817
- // parse `--header "Name: Value"` (repeatable) into a headers object. Splits
818
- // on the FIRST colon so JWTs/URLs in the value are preserved. Malformed
819
- // entries are warned and skipped, never thrown — a broken header shouldn't
820
- // poison the whole call.
821
- const parseHeaders = (specs) => {
822
- const out = {};
823
- for (const s of specs) {
824
- const i = s.indexOf(":");
825
- if (i <= 0) { console.error(c.dim(`! ignoring malformed --header "${s}" (expected "Name: Value")`)); continue; }
826
- const name = s.slice(0, i).trim();
827
- const value = s.slice(i + 1).trim();
828
- if (!name) continue;
829
- out[name] = value;
830
- }
831
- return out;
832
- };
833
686
  const type = getFlag("--type");
834
687
  const vendor = getFlag("--vendor");
835
688
  const rest = [];
@@ -842,13 +695,6 @@ async function main() {
842
695
 
843
696
  switch (cmd) {
844
697
  case "plan": return cmdPlan(arg);
845
- case "call":
846
- return cmdCall(rest[0], {
847
- pay: getFlag("--pay"), method: getFlag("--method"), data: getFlag("--data"),
848
- budget: getFlag("--budget"), seconds: getFlag("--seconds"),
849
- endpoint: getFlag("--endpoint"), raw: args.includes("--raw"),
850
- headers: parseHeaders(getFlagAll("--header")),
851
- });
852
698
  case "search": return cmdSearch(arg, type);
853
699
  case "models": return cmdModels(vendor);
854
700
  case "vendors": return cmdVendors();