@fluxa-pay/planner 0.1.0 → 0.2.1

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 +204 -8
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
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.",
3
+ "version": "0.2.1",
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.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "planner": "planner.mjs"
package/planner.mjs CHANGED
@@ -301,6 +301,39 @@ async function rawCall(url, method, data, extraHeaders = {}, auth = false) {
301
301
  return { status: res.status, text };
302
302
  }
303
303
 
304
+ // The proxy's API/MCP lanes bill the caller identified by the X-Agent-ID header
305
+ // and reject every non-discovery request without it. Default to the agent_id
306
+ // (UUID) from `fluxa-wallet status`; configs with require_agent_auth need a JWT
307
+ // instead, so agentCall() retries with a freshly minted wallet JWT on the
308
+ // "valid JWT" 401. Both values are cached for the process.
309
+ let _agentId = null;
310
+ async function agentId() {
311
+ if (_agentId) return _agentId;
312
+ const s = unwrap(await walletJson(["status"]).catch(() => ({})));
313
+ const id = s.agent_id || s.agentId || s.id;
314
+ if (!id) die("could not read agent_id from `fluxa-wallet status` — register once with `fluxa-wallet init`.");
315
+ return (_agentId = id);
316
+ }
317
+ let _walletJwt = null;
318
+ async function walletJwt() {
319
+ if (_walletJwt) return _walletJwt;
320
+ const j = unwrap(await walletJson(["refreshJWT"]).catch(() => ({})));
321
+ const jwt = j.jwt || j.token;
322
+ if (!jwt) die("could not mint a wallet JWT (`fluxa-wallet refreshJWT`).");
323
+ return (_walletJwt = jwt);
324
+ }
325
+ // rawCall + X-Agent-ID, with the UUID→JWT fallback for require_agent_auth configs.
326
+ // Auto-attached headers go FIRST so a caller-supplied `extra` (e.g. a user's
327
+ // `--header "X-Agent-ID: …"`) can override — collisions are surfaced upstream
328
+ // in cmdCall so the override isn't silent.
329
+ async function agentCall(url, method, data, extra = {}) {
330
+ const r = await rawCall(url, method, data, { "X-Agent-ID": await agentId(), ...extra });
331
+ if (r.status === 401 && /valid JWT/i.test(r.text)) {
332
+ return rawCall(url, method, data, { "X-Agent-ID": await walletJwt(), ...extra });
333
+ }
334
+ return r;
335
+ }
336
+
304
337
  async function ensureMandate(currency, budget, seconds, url) {
305
338
  const cfg = loadConfig();
306
339
  // only reuse a signed mandate of the SAME currency with budget left
@@ -339,14 +372,30 @@ function descriptorEndpoints(text) {
339
372
  }
340
373
 
341
374
  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]");
375
+ if (!url) {
376
+ return die("usage: planner call <url|slug> [--endpoint <path>] [--data '<json>'] [--header 'K: V']… [--pay credits|usdc] [--method GET|POST] [--budget <units>] [--raw]");
344
377
  }
378
+ // accept a bare AgentMarket slug (e.g. `web-search-api`) → the proxy API base.
379
+ // full http(s) URLs are used as-is.
380
+ if (!/^https?:\/\//.test(url)) url = `${PROXY}/api/${encodeURIComponent(url)}`;
345
381
  const pay = (opts.pay || loadConfig().pay || "credits").toLowerCase();
346
382
  const currency = pay === "usdc" ? "USDC" : "FLUXA_MONETIZE_CREDITS";
347
383
  const budget = opts.budget ? Number(opts.budget) : (pay === "usdc" ? 5000000 : 500); // ~$5 default
348
384
  const seconds = opts.seconds ? Number(opts.seconds) : 28800; // 8h
349
385
 
386
+ // user-supplied request headers (repeatable --header "K: V") are merged
387
+ // into every outbound call below. A user value wins over the auto-attached
388
+ // X-Agent-ID; X-Payment is the one exception, see the retry step. We warn
389
+ // up front when a user header would override an auto-attached one so the
390
+ // override isn't silent.
391
+ const userHeaders = opts.headers || {};
392
+ const reservedNames = ["x-agent-id", "x-payment"];
393
+ for (const k of Object.keys(userHeaders)) {
394
+ if (reservedNames.includes(k.toLowerCase())) {
395
+ console.error(c.dim(`! --header ${k} overrides the auto-attached value`));
396
+ }
397
+ }
398
+
350
399
  await ensureAgent();
351
400
 
352
401
  let targetUrl = url;
@@ -357,7 +406,7 @@ async function cmdCall(url, opts = {}) {
357
406
  // real paid sub-endpoints. Resolve it (unless --raw) so the agent doesn't have
358
407
  // to read the catalog itself. We never spend without a supplied body.
359
408
  if (!opts.raw) {
360
- const probe = await rawCall(url, "GET");
409
+ const probe = await agentCall(url, "GET", undefined, userHeaders);
361
410
  const desc = probe.status === 200 ? descriptorEndpoints(probe.text) : null;
362
411
  if (desc) {
363
412
  const eps = desc.endpoints.filter((e) => e.enabled !== false);
@@ -388,7 +437,7 @@ async function cmdCall(url, opts = {}) {
388
437
  }
389
438
 
390
439
  // call the resolved endpoint — pay if it answers 402
391
- let r = await rawCall(targetUrl, method, data);
440
+ let r = await agentCall(targetUrl, method, data, userHeaders);
392
441
  if (r.status !== 402) {
393
442
  if (r.status >= 400) die(`${r.status} — ${r.text.slice(0, 300)}`);
394
443
  return console.log(r.text); // raw body, no post-processing
@@ -402,8 +451,11 @@ async function cmdCall(url, opts = {}) {
402
451
  const xPayment = paid.xPaymentB64 || paid.xPayment;
403
452
  if (!xPayment) die("x402-v3 did not return a payment token (xPaymentB64).");
404
453
 
405
- // 3. retry with the payment token
406
- r = await rawCall(url, method, opts.data, { "X-Payment": xPayment });
454
+ // 3. retry with the payment token (against the resolved endpoint, with
455
+ // X-Agent-ID + user headers). The freshly-minted X-Payment is appended
456
+ // LAST so it wins over any user-supplied X-Payment — preserving the x402
457
+ // settlement that the CLI just signed for this exact 402 challenge.
458
+ r = await agentCall(targetUrl, method, opts.data, { ...userHeaders, "X-Payment": xPayment });
407
459
  if (r.status >= 400) die(`paid retry failed: ${r.status} — ${r.text.slice(0, 300)}`);
408
460
  console.log(r.text); // raw body, no post-processing
409
461
  }
@@ -446,6 +498,109 @@ async function cmdTopup(vendor, opts = {}) {
446
498
  console.log(c.green("✓") + ` topped up ${vendor} · +${added.toLocaleString()} Units · balance ${Number(res.balance ?? 0).toLocaleString()} Units`);
447
499
  }
448
500
 
501
+ // --- api (manage your oneshot API configs) ----------------------------------
502
+ // `planner api <verb>` — resource-first, like `planner keys`. All verbs auth
503
+ // with auth:true (fxa_live_ key if present, else an auto-minted Agent VC) and
504
+ // are owner-scoped server-side.
505
+ function readSpec(file) {
506
+ let raw;
507
+ try {
508
+ raw = file ? readFileSync(file, "utf8") : readFileSync(0, "utf8");
509
+ } catch (e) {
510
+ return die(`could not read spec ${file ? file : "from stdin"}: ${e.message}`);
511
+ }
512
+ try {
513
+ return JSON.parse(raw);
514
+ } catch (e) {
515
+ return die(`spec is not valid JSON${file ? ` (${file})` : " (stdin)"}: ${e.message}`);
516
+ }
517
+ }
518
+
519
+ // Overlay --name/--upstream/--wallet/--private/--require-agent-auth onto a spec.
520
+ function applyOverrides(spec, opts) {
521
+ if (opts.name != null) spec.serverName = opts.name;
522
+ if (opts.upstream != null) spec.upstreamBaseUrl = opts.upstream;
523
+ if (opts.wallet != null) spec.yourWallet = opts.wallet;
524
+ if (opts.private) spec.isPrivate = true;
525
+ if (opts.requireAgentAuth) spec.requireAgentAuth = true;
526
+ return spec;
527
+ }
528
+
529
+ async function cmdApi(sub, rest, opts) {
530
+ switch (sub) {
531
+ case "create": case "new": return cmdApiCreate(opts);
532
+ case "list": case "ls": return cmdApiList();
533
+ case "get": case "show": return cmdApiGet(rest[0]);
534
+ case "edit": case "update": return cmdApiEdit(rest[0], opts);
535
+ default:
536
+ return die(
537
+ "usage: planner api <create|list|get|edit>\n" +
538
+ " api create --file <spec.json> [--publish] [--dry-run]\n" +
539
+ " api list\n" +
540
+ " api get <idOrSlug>\n" +
541
+ " api edit <idOrSlug> [--file <spec.json>] [--publish] [--dry-run]",
542
+ );
543
+ }
544
+ }
545
+
546
+ async function cmdApiCreate(opts) {
547
+ const spec = applyOverrides(readSpec(opts.file), opts);
548
+ spec.status = opts.publish ? "published" : "draft";
549
+
550
+ if (opts.dryRun) {
551
+ console.error(c.dim(`· dry-run — would POST ${spec.status} config to ${PROXY}/configs/api:`));
552
+ console.log(JSON.stringify(spec, null, 2));
553
+ return;
554
+ }
555
+
556
+ const { data } = await http(`${PROXY}/configs/api`, { method: "POST", auth: true, body: spec });
557
+ const tag = data.status === "published" ? c.green("published") : c.dim("draft");
558
+ console.log(c.green("✓") + ` created ${c.bold(data.slug || data.configId)} · ${tag}`);
559
+ if (data.proxyUrl) console.log(c.dim(` proxy: ${data.proxyUrl}`));
560
+ console.log(c.dim(
561
+ data.status === "published"
562
+ ? ` test it: planner call ${data.proxyUrl || data.slug}`
563
+ : ` publish: planner api edit ${data.slug || data.configId} --publish`,
564
+ ));
565
+ }
566
+
567
+ async function cmdApiList() {
568
+ const { data } = await http(`${PROXY}/configs/api`, { auth: true });
569
+ const rows = data.configs || [];
570
+ if (!rows.length) return console.log(c.dim(" no api configs yet — `planner api create --file <spec.json>`"));
571
+ console.log(" " + c.dim(pad("slug", 26) + pad("name", 22) + pad("status", 10) + pad("vis", 9) + pad("eps", 5) + "created"));
572
+ for (const r of rows) {
573
+ const st = r.status === "published" ? c.green(pad("published", 10)) : c.dim(pad("draft", 10));
574
+ const vis = r.isPrivate ? "private" : "public";
575
+ console.log(` ${pad(r.slug || r.configId, 26)}${pad(r.serverName || "", 22)}${st}${pad(vis, 9)}${pad(String(r.endpointCount ?? 0), 5)}${(r.createdAt || "").slice(0, 10)}`);
576
+ }
577
+ }
578
+
579
+ async function cmdApiGet(idOrSlug) {
580
+ if (!idOrSlug) return die("usage: planner api get <idOrSlug>");
581
+ const { data } = await http(`${PROXY}/configs/api/${encodeURIComponent(idOrSlug)}`, { auth: true });
582
+ console.log(JSON.stringify(data, null, 2));
583
+ }
584
+
585
+ async function cmdApiEdit(idOrSlug, opts) {
586
+ if (!idOrSlug) return die("usage: planner api edit <idOrSlug> [--file <spec.json>] [--publish] [--dry-run]");
587
+ const spec = applyOverrides(opts.file ? readSpec(opts.file) : {}, opts);
588
+ if (opts.publish) spec.status = "published";
589
+ if (Object.keys(spec).length === 0) {
590
+ return die("nothing to edit — pass --file and/or flags (--name, --upstream, --publish, …)");
591
+ }
592
+
593
+ if (opts.dryRun) {
594
+ console.error(c.dim(`· dry-run — would PATCH ${PROXY}/configs/api/${idOrSlug}:`));
595
+ console.log(JSON.stringify(spec, null, 2));
596
+ return;
597
+ }
598
+
599
+ const { data } = await http(`${PROXY}/configs/api/${encodeURIComponent(idOrSlug)}`, { method: "PATCH", auth: true, body: spec });
600
+ const tag = data.status === "published" ? c.green("published") : c.dim("draft");
601
+ console.log(c.green("✓") + ` updated ${c.bold(data.slug || data.configId)} · ${tag}`);
602
+ }
603
+
449
604
  // --- plan (thin call to server-side endpoint) --------------------------------
450
605
  async function cmdPlan(task) {
451
606
  if (!task) return die('usage: planner plan "<task>"');
@@ -569,6 +724,13 @@ ${c.bold("Oneshot APIs")}
569
724
  ${c.cyan("planner call <base>")} show the paid endpoint + its required body
570
725
  ${c.cyan("planner call <base> --data '<json>'")} call it; pay the 402 if needed
571
726
  ${c.dim("--endpoint <path>")} picks when there are several; ${c.dim("--raw")} hits a URL directly.
727
+ ${c.bold("Manage your APIs")} ${c.dim("(fxa_live_ key or auto-minted Agent VC)")}
728
+ ${c.cyan("planner api create --file <spec.json> [--publish]")} create (draft unless --publish)
729
+ ${c.cyan("planner api list")} your api configs
730
+ ${c.cyan("planner api get <idOrSlug>")} print the round-trippable spec
731
+ ${c.cyan("planner api edit <idOrSlug> [--file <s>] [--publish]")} update fields / replace endpoints
732
+ ${c.dim("Spec JSON: serverName, serverDescription, upstreamBaseUrl, endpoints[]. Wallet defaults from your account.")}
733
+ ${c.dim("get emits a spec you can edit and feed back: planner api get foo > s.json; planner api edit foo --file s.json")}
572
734
  `,
573
735
  models: () => `
574
736
  ${c.bold("Models — merchant-centric")}
@@ -603,6 +765,7 @@ ${c.bold("Headline")}
603
765
  planner plan "<task>" recommend the models, APIs & skills for a task
604
766
  planner call <url> make a paid API call — handles the x402 mandate + payment
605
767
  [--pay credits|usdc] [--method GET|POST] [--data '<json>']
768
+ [--header "Name: Value"] (repeatable; e.g. an upstream Bearer token)
606
769
 
607
770
  ${c.bold("Primitives")} ${c.dim("(all live)")}
608
771
  planner search "<q>" [--type api,skill,model] discover resources
@@ -611,6 +774,8 @@ ${c.bold("Primitives")} ${c.dim("(all live)")}
611
774
  planner topup <vendor> [--credits <N>|--bundle <slug>] prepay Units (x402, via wallet)
612
775
  planner ledger <vendor> spend/topup history
613
776
  planner keys [create|update <id>|revoke <id>] manage fxa_live_ API keys (Agent VC only)
777
+ planner api create --file <spec.json> [--publish] create a oneshot API (draft by default)
778
+ planner api list | get <idOrSlug> | edit <idOrSlug> list / inspect / update your APIs
614
779
  planner login <fxa_live_…> / planner whoami auth
615
780
  planner info [units|auth|pay|keys|apis|models|skills] explain how the platform works
616
781
 
@@ -627,9 +792,31 @@ function die(msg, ctx) {
627
792
  async function main() {
628
793
  const args = argv.slice(2);
629
794
  const cmd = args[0];
630
- const FLAGS = ["--type", "--vendor", "--pay", "--method", "--data", "--budget", "--seconds", "--endpoint", "--credits", "--bundle", "--name", "--cap"];
631
- const BOOLS = ["--raw"];
795
+ const FLAGS = ["--type", "--vendor", "--pay", "--method", "--data", "--budget", "--seconds", "--endpoint", "--credits", "--bundle", "--name", "--cap", "--file", "--upstream", "--wallet", "--header"];
796
+ const BOOLS = ["--raw", "--publish", "--private", "--require-agent-auth", "--dry-run"];
632
797
  const getFlag = (n) => { const i = args.indexOf(n); return i !== -1 ? args[i + 1] : null; };
798
+ // repeatable flag → array of all values (in order)
799
+ const getFlagAll = (n) => {
800
+ const out = [];
801
+ for (let i = 0; i < args.length - 1; i++) if (args[i] === n) out.push(args[i + 1]);
802
+ return out;
803
+ };
804
+ // parse `--header "Name: Value"` (repeatable) into a headers object. Splits
805
+ // on the FIRST colon so JWTs/URLs in the value are preserved. Malformed
806
+ // entries are warned and skipped, never thrown — a broken header shouldn't
807
+ // poison the whole call.
808
+ const parseHeaders = (specs) => {
809
+ const out = {};
810
+ for (const s of specs) {
811
+ const i = s.indexOf(":");
812
+ if (i <= 0) { console.error(c.dim(`! ignoring malformed --header "${s}" (expected "Name: Value")`)); continue; }
813
+ const name = s.slice(0, i).trim();
814
+ const value = s.slice(i + 1).trim();
815
+ if (!name) continue;
816
+ out[name] = value;
817
+ }
818
+ return out;
819
+ };
633
820
  const type = getFlag("--type");
634
821
  const vendor = getFlag("--vendor");
635
822
  const rest = [];
@@ -647,6 +834,7 @@ async function main() {
647
834
  pay: getFlag("--pay"), method: getFlag("--method"), data: getFlag("--data"),
648
835
  budget: getFlag("--budget"), seconds: getFlag("--seconds"),
649
836
  endpoint: getFlag("--endpoint"), raw: args.includes("--raw"),
837
+ headers: parseHeaders(getFlagAll("--header")),
650
838
  });
651
839
  case "search": return cmdSearch(arg, type);
652
840
  case "models": return cmdModels(vendor);
@@ -658,6 +846,14 @@ async function main() {
658
846
  });
659
847
  case "ledger": return cmdLedger(rest[0]);
660
848
  case "keys": return cmdKeys(rest[0], rest.slice(1), { name: getFlag("--name"), cap: getFlag("--cap") });
849
+ case "api":
850
+ return cmdApi(rest[0], rest.slice(1), {
851
+ file: getFlag("--file"), name: getFlag("--name"),
852
+ upstream: getFlag("--upstream"), wallet: getFlag("--wallet"),
853
+ publish: args.includes("--publish"), private: args.includes("--private"),
854
+ requireAgentAuth: args.includes("--require-agent-auth"),
855
+ dryRun: args.includes("--dry-run"),
856
+ });
661
857
  case "login": return cmdLogin(rest[0]);
662
858
  case "whoami": return cmdWhoami();
663
859
  case "info": return cmdInfo(rest[0]);