@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.
- package/package.json +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.
|
|
4
|
-
"description": "FluxA Planner CLI
|
|
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, {
|
|
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, {
|
|
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
|
|
439
|
-
|
|
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]);
|