@fluxa-pay/planner 0.1.1 → 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 +172 -9
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fluxa-pay/planner",
3
- "version": "0.1.1",
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
@@ -323,10 +323,13 @@ async function walletJwt() {
323
323
  return (_walletJwt = jwt);
324
324
  }
325
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.
326
329
  async function agentCall(url, method, data, extra = {}) {
327
- const r = await rawCall(url, method, data, { ...extra, "X-Agent-ID": await agentId() });
330
+ const r = await rawCall(url, method, data, { "X-Agent-ID": await agentId(), ...extra });
328
331
  if (r.status === 401 && /valid JWT/i.test(r.text)) {
329
- return rawCall(url, method, data, { ...extra, "X-Agent-ID": await walletJwt() });
332
+ return rawCall(url, method, data, { "X-Agent-ID": await walletJwt(), ...extra });
330
333
  }
331
334
  return r;
332
335
  }
@@ -370,7 +373,7 @@ function descriptorEndpoints(text) {
370
373
 
371
374
  async function cmdCall(url, opts = {}) {
372
375
  if (!url) {
373
- return die("usage: planner call <url|slug> [--endpoint <path>] [--data '<json>'] [--pay credits|usdc] [--method GET|POST] [--budget <units>] [--raw]");
376
+ return die("usage: planner call <url|slug> [--endpoint <path>] [--data '<json>'] [--header 'K: V']… [--pay credits|usdc] [--method GET|POST] [--budget <units>] [--raw]");
374
377
  }
375
378
  // accept a bare AgentMarket slug (e.g. `web-search-api`) → the proxy API base.
376
379
  // full http(s) URLs are used as-is.
@@ -380,6 +383,19 @@ async function cmdCall(url, opts = {}) {
380
383
  const budget = opts.budget ? Number(opts.budget) : (pay === "usdc" ? 5000000 : 500); // ~$5 default
381
384
  const seconds = opts.seconds ? Number(opts.seconds) : 28800; // 8h
382
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
+
383
399
  await ensureAgent();
384
400
 
385
401
  let targetUrl = url;
@@ -390,7 +406,7 @@ async function cmdCall(url, opts = {}) {
390
406
  // real paid sub-endpoints. Resolve it (unless --raw) so the agent doesn't have
391
407
  // to read the catalog itself. We never spend without a supplied body.
392
408
  if (!opts.raw) {
393
- const probe = await agentCall(url, "GET");
409
+ const probe = await agentCall(url, "GET", undefined, userHeaders);
394
410
  const desc = probe.status === 200 ? descriptorEndpoints(probe.text) : null;
395
411
  if (desc) {
396
412
  const eps = desc.endpoints.filter((e) => e.enabled !== false);
@@ -421,7 +437,7 @@ async function cmdCall(url, opts = {}) {
421
437
  }
422
438
 
423
439
  // call the resolved endpoint — pay if it answers 402
424
- let r = await agentCall(targetUrl, method, data);
440
+ let r = await agentCall(targetUrl, method, data, userHeaders);
425
441
  if (r.status !== 402) {
426
442
  if (r.status >= 400) die(`${r.status} — ${r.text.slice(0, 300)}`);
427
443
  return console.log(r.text); // raw body, no post-processing
@@ -435,8 +451,11 @@ async function cmdCall(url, opts = {}) {
435
451
  const xPayment = paid.xPaymentB64 || paid.xPayment;
436
452
  if (!xPayment) die("x402-v3 did not return a payment token (xPaymentB64).");
437
453
 
438
- // 3. retry with the payment token (against the resolved endpoint, with X-Agent-ID)
439
- r = await agentCall(targetUrl, 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 });
440
459
  if (r.status >= 400) die(`paid retry failed: ${r.status} — ${r.text.slice(0, 300)}`);
441
460
  console.log(r.text); // raw body, no post-processing
442
461
  }
@@ -479,6 +498,109 @@ async function cmdTopup(vendor, opts = {}) {
479
498
  console.log(c.green("✓") + ` topped up ${vendor} · +${added.toLocaleString()} Units · balance ${Number(res.balance ?? 0).toLocaleString()} Units`);
480
499
  }
481
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
+
482
604
  // --- plan (thin call to server-side endpoint) --------------------------------
483
605
  async function cmdPlan(task) {
484
606
  if (!task) return die('usage: planner plan "<task>"');
@@ -602,6 +724,13 @@ ${c.bold("Oneshot APIs")}
602
724
  ${c.cyan("planner call <base>")} show the paid endpoint + its required body
603
725
  ${c.cyan("planner call <base> --data '<json>'")} call it; pay the 402 if needed
604
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")}
605
734
  `,
606
735
  models: () => `
607
736
  ${c.bold("Models — merchant-centric")}
@@ -636,6 +765,7 @@ ${c.bold("Headline")}
636
765
  planner plan "<task>" recommend the models, APIs & skills for a task
637
766
  planner call <url> make a paid API call — handles the x402 mandate + payment
638
767
  [--pay credits|usdc] [--method GET|POST] [--data '<json>']
768
+ [--header "Name: Value"] (repeatable; e.g. an upstream Bearer token)
639
769
 
640
770
  ${c.bold("Primitives")} ${c.dim("(all live)")}
641
771
  planner search "<q>" [--type api,skill,model] discover resources
@@ -644,6 +774,8 @@ ${c.bold("Primitives")} ${c.dim("(all live)")}
644
774
  planner topup <vendor> [--credits <N>|--bundle <slug>] prepay Units (x402, via wallet)
645
775
  planner ledger <vendor> spend/topup history
646
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
647
779
  planner login <fxa_live_…> / planner whoami auth
648
780
  planner info [units|auth|pay|keys|apis|models|skills] explain how the platform works
649
781
 
@@ -660,9 +792,31 @@ function die(msg, ctx) {
660
792
  async function main() {
661
793
  const args = argv.slice(2);
662
794
  const cmd = args[0];
663
- const FLAGS = ["--type", "--vendor", "--pay", "--method", "--data", "--budget", "--seconds", "--endpoint", "--credits", "--bundle", "--name", "--cap"];
664
- 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"];
665
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
+ };
666
820
  const type = getFlag("--type");
667
821
  const vendor = getFlag("--vendor");
668
822
  const rest = [];
@@ -680,6 +834,7 @@ async function main() {
680
834
  pay: getFlag("--pay"), method: getFlag("--method"), data: getFlag("--data"),
681
835
  budget: getFlag("--budget"), seconds: getFlag("--seconds"),
682
836
  endpoint: getFlag("--endpoint"), raw: args.includes("--raw"),
837
+ headers: parseHeaders(getFlagAll("--header")),
683
838
  });
684
839
  case "search": return cmdSearch(arg, type);
685
840
  case "models": return cmdModels(vendor);
@@ -691,6 +846,14 @@ async function main() {
691
846
  });
692
847
  case "ledger": return cmdLedger(rest[0]);
693
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
+ });
694
857
  case "login": return cmdLogin(rest[0]);
695
858
  case "whoami": return cmdWhoami();
696
859
  case "info": return cmdInfo(rest[0]);