@fluxa-pay/planner 0.1.1 → 0.2.2
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 +186 -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.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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"planner": "planner.mjs"
|
package/planner.mjs
CHANGED
|
@@ -181,6 +181,18 @@ async function cmdModels(vendor) {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
async function cmdVendors() {
|
|
185
|
+
const { data } = await http(`${PLATFORM}/api/llm/vendors`);
|
|
186
|
+
const vendors = data.vendors || [];
|
|
187
|
+
if (!vendors.length) return console.log(c.dim(" no vendors"));
|
|
188
|
+
console.log(" " + c.dim(pad("slug", 22) + pad("name", 22) + pad("models", 9) + pad("bundles", 9) + "price (Units/Mtok)"));
|
|
189
|
+
for (const v of vendors) {
|
|
190
|
+
const pr = v.priceRange;
|
|
191
|
+
const price = pr && Number.isFinite(pr.min) ? (pr.min === pr.max ? `${pr.min}` : `${pr.min}-${pr.max}`) : c.dim("—");
|
|
192
|
+
console.log(` ${c.bold(pad(v.slug, 22))}${c.gray(pad(v.name || "", 22))}${pad(String(v.modelCount ?? 0), 9)}${pad(String(v.bundleCount ?? 0), 9)}${price}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
184
196
|
async function cmdBalance(vendor) {
|
|
185
197
|
const url = vendor ? `${PROXY}/llm/wallet/balances/${vendor}` : `${PROXY}/llm/wallet/balances`;
|
|
186
198
|
const { data } = await http(url, { auth: true });
|
|
@@ -323,10 +335,13 @@ async function walletJwt() {
|
|
|
323
335
|
return (_walletJwt = jwt);
|
|
324
336
|
}
|
|
325
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.
|
|
326
341
|
async function agentCall(url, method, data, extra = {}) {
|
|
327
|
-
const r = await rawCall(url, method, data, {
|
|
342
|
+
const r = await rawCall(url, method, data, { "X-Agent-ID": await agentId(), ...extra });
|
|
328
343
|
if (r.status === 401 && /valid JWT/i.test(r.text)) {
|
|
329
|
-
return rawCall(url, method, data, {
|
|
344
|
+
return rawCall(url, method, data, { "X-Agent-ID": await walletJwt(), ...extra });
|
|
330
345
|
}
|
|
331
346
|
return r;
|
|
332
347
|
}
|
|
@@ -370,7 +385,7 @@ function descriptorEndpoints(text) {
|
|
|
370
385
|
|
|
371
386
|
async function cmdCall(url, opts = {}) {
|
|
372
387
|
if (!url) {
|
|
373
|
-
return die("usage: planner call <url|slug> [--endpoint <path>] [--data '<json>'] [--pay credits|usdc] [--method GET|POST] [--budget <units>] [--raw]");
|
|
388
|
+
return die("usage: planner call <url|slug> [--endpoint <path>] [--data '<json>'] [--header 'K: V']… [--pay credits|usdc] [--method GET|POST] [--budget <units>] [--raw]");
|
|
374
389
|
}
|
|
375
390
|
// accept a bare AgentMarket slug (e.g. `web-search-api`) → the proxy API base.
|
|
376
391
|
// full http(s) URLs are used as-is.
|
|
@@ -380,6 +395,19 @@ async function cmdCall(url, opts = {}) {
|
|
|
380
395
|
const budget = opts.budget ? Number(opts.budget) : (pay === "usdc" ? 5000000 : 500); // ~$5 default
|
|
381
396
|
const seconds = opts.seconds ? Number(opts.seconds) : 28800; // 8h
|
|
382
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
|
+
|
|
383
411
|
await ensureAgent();
|
|
384
412
|
|
|
385
413
|
let targetUrl = url;
|
|
@@ -390,7 +418,7 @@ async function cmdCall(url, opts = {}) {
|
|
|
390
418
|
// real paid sub-endpoints. Resolve it (unless --raw) so the agent doesn't have
|
|
391
419
|
// to read the catalog itself. We never spend without a supplied body.
|
|
392
420
|
if (!opts.raw) {
|
|
393
|
-
const probe = await agentCall(url, "GET");
|
|
421
|
+
const probe = await agentCall(url, "GET", undefined, userHeaders);
|
|
394
422
|
const desc = probe.status === 200 ? descriptorEndpoints(probe.text) : null;
|
|
395
423
|
if (desc) {
|
|
396
424
|
const eps = desc.endpoints.filter((e) => e.enabled !== false);
|
|
@@ -421,7 +449,7 @@ async function cmdCall(url, opts = {}) {
|
|
|
421
449
|
}
|
|
422
450
|
|
|
423
451
|
// call the resolved endpoint — pay if it answers 402
|
|
424
|
-
let r = await agentCall(targetUrl, method, data);
|
|
452
|
+
let r = await agentCall(targetUrl, method, data, userHeaders);
|
|
425
453
|
if (r.status !== 402) {
|
|
426
454
|
if (r.status >= 400) die(`${r.status} — ${r.text.slice(0, 300)}`);
|
|
427
455
|
return console.log(r.text); // raw body, no post-processing
|
|
@@ -435,8 +463,11 @@ async function cmdCall(url, opts = {}) {
|
|
|
435
463
|
const xPayment = paid.xPaymentB64 || paid.xPayment;
|
|
436
464
|
if (!xPayment) die("x402-v3 did not return a payment token (xPaymentB64).");
|
|
437
465
|
|
|
438
|
-
// 3. retry with the payment token (against the resolved endpoint, with
|
|
439
|
-
|
|
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 });
|
|
440
471
|
if (r.status >= 400) die(`paid retry failed: ${r.status} — ${r.text.slice(0, 300)}`);
|
|
441
472
|
console.log(r.text); // raw body, no post-processing
|
|
442
473
|
}
|
|
@@ -479,6 +510,109 @@ async function cmdTopup(vendor, opts = {}) {
|
|
|
479
510
|
console.log(c.green("✓") + ` topped up ${vendor} · +${added.toLocaleString()} Units · balance ${Number(res.balance ?? 0).toLocaleString()} Units`);
|
|
480
511
|
}
|
|
481
512
|
|
|
513
|
+
// --- api (manage your oneshot API configs) ----------------------------------
|
|
514
|
+
// `planner api <verb>` — resource-first, like `planner keys`. All verbs auth
|
|
515
|
+
// with auth:true (fxa_live_ key if present, else an auto-minted Agent VC) and
|
|
516
|
+
// are owner-scoped server-side.
|
|
517
|
+
function readSpec(file) {
|
|
518
|
+
let raw;
|
|
519
|
+
try {
|
|
520
|
+
raw = file ? readFileSync(file, "utf8") : readFileSync(0, "utf8");
|
|
521
|
+
} catch (e) {
|
|
522
|
+
return die(`could not read spec ${file ? file : "from stdin"}: ${e.message}`);
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
return JSON.parse(raw);
|
|
526
|
+
} catch (e) {
|
|
527
|
+
return die(`spec is not valid JSON${file ? ` (${file})` : " (stdin)"}: ${e.message}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Overlay --name/--upstream/--wallet/--private/--require-agent-auth onto a spec.
|
|
532
|
+
function applyOverrides(spec, opts) {
|
|
533
|
+
if (opts.name != null) spec.serverName = opts.name;
|
|
534
|
+
if (opts.upstream != null) spec.upstreamBaseUrl = opts.upstream;
|
|
535
|
+
if (opts.wallet != null) spec.yourWallet = opts.wallet;
|
|
536
|
+
if (opts.private) spec.isPrivate = true;
|
|
537
|
+
if (opts.requireAgentAuth) spec.requireAgentAuth = true;
|
|
538
|
+
return spec;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function cmdApi(sub, rest, opts) {
|
|
542
|
+
switch (sub) {
|
|
543
|
+
case "create": case "new": return cmdApiCreate(opts);
|
|
544
|
+
case "list": case "ls": return cmdApiList();
|
|
545
|
+
case "get": case "show": return cmdApiGet(rest[0]);
|
|
546
|
+
case "edit": case "update": return cmdApiEdit(rest[0], opts);
|
|
547
|
+
default:
|
|
548
|
+
return die(
|
|
549
|
+
"usage: planner api <create|list|get|edit>\n" +
|
|
550
|
+
" api create --file <spec.json> [--publish] [--dry-run]\n" +
|
|
551
|
+
" api list\n" +
|
|
552
|
+
" api get <idOrSlug>\n" +
|
|
553
|
+
" api edit <idOrSlug> [--file <spec.json>] [--publish] [--dry-run]",
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function cmdApiCreate(opts) {
|
|
559
|
+
const spec = applyOverrides(readSpec(opts.file), opts);
|
|
560
|
+
spec.status = opts.publish ? "published" : "draft";
|
|
561
|
+
|
|
562
|
+
if (opts.dryRun) {
|
|
563
|
+
console.error(c.dim(`· dry-run — would POST ${spec.status} config to ${PROXY}/configs/api:`));
|
|
564
|
+
console.log(JSON.stringify(spec, null, 2));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const { data } = await http(`${PROXY}/configs/api`, { method: "POST", auth: true, body: spec });
|
|
569
|
+
const tag = data.status === "published" ? c.green("published") : c.dim("draft");
|
|
570
|
+
console.log(c.green("✓") + ` created ${c.bold(data.slug || data.configId)} · ${tag}`);
|
|
571
|
+
if (data.proxyUrl) console.log(c.dim(` proxy: ${data.proxyUrl}`));
|
|
572
|
+
console.log(c.dim(
|
|
573
|
+
data.status === "published"
|
|
574
|
+
? ` test it: planner call ${data.proxyUrl || data.slug}`
|
|
575
|
+
: ` publish: planner api edit ${data.slug || data.configId} --publish`,
|
|
576
|
+
));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function cmdApiList() {
|
|
580
|
+
const { data } = await http(`${PROXY}/configs/api`, { auth: true });
|
|
581
|
+
const rows = data.configs || [];
|
|
582
|
+
if (!rows.length) return console.log(c.dim(" no api configs yet — `planner api create --file <spec.json>`"));
|
|
583
|
+
console.log(" " + c.dim(pad("slug", 26) + pad("name", 22) + pad("status", 10) + pad("vis", 9) + pad("eps", 5) + "created"));
|
|
584
|
+
for (const r of rows) {
|
|
585
|
+
const st = r.status === "published" ? c.green(pad("published", 10)) : c.dim(pad("draft", 10));
|
|
586
|
+
const vis = r.isPrivate ? "private" : "public";
|
|
587
|
+
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)}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function cmdApiGet(idOrSlug) {
|
|
592
|
+
if (!idOrSlug) return die("usage: planner api get <idOrSlug>");
|
|
593
|
+
const { data } = await http(`${PROXY}/configs/api/${encodeURIComponent(idOrSlug)}`, { auth: true });
|
|
594
|
+
console.log(JSON.stringify(data, null, 2));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function cmdApiEdit(idOrSlug, opts) {
|
|
598
|
+
if (!idOrSlug) return die("usage: planner api edit <idOrSlug> [--file <spec.json>] [--publish] [--dry-run]");
|
|
599
|
+
const spec = applyOverrides(opts.file ? readSpec(opts.file) : {}, opts);
|
|
600
|
+
if (opts.publish) spec.status = "published";
|
|
601
|
+
if (Object.keys(spec).length === 0) {
|
|
602
|
+
return die("nothing to edit — pass --file and/or flags (--name, --upstream, --publish, …)");
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (opts.dryRun) {
|
|
606
|
+
console.error(c.dim(`· dry-run — would PATCH ${PROXY}/configs/api/${idOrSlug}:`));
|
|
607
|
+
console.log(JSON.stringify(spec, null, 2));
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const { data } = await http(`${PROXY}/configs/api/${encodeURIComponent(idOrSlug)}`, { method: "PATCH", auth: true, body: spec });
|
|
612
|
+
const tag = data.status === "published" ? c.green("published") : c.dim("draft");
|
|
613
|
+
console.log(c.green("✓") + ` updated ${c.bold(data.slug || data.configId)} · ${tag}`);
|
|
614
|
+
}
|
|
615
|
+
|
|
482
616
|
// --- plan (thin call to server-side endpoint) --------------------------------
|
|
483
617
|
async function cmdPlan(task) {
|
|
484
618
|
if (!task) return die('usage: planner plan "<task>"');
|
|
@@ -602,6 +736,13 @@ ${c.bold("Oneshot APIs")}
|
|
|
602
736
|
${c.cyan("planner call <base>")} show the paid endpoint + its required body
|
|
603
737
|
${c.cyan("planner call <base> --data '<json>'")} call it; pay the 402 if needed
|
|
604
738
|
${c.dim("--endpoint <path>")} picks when there are several; ${c.dim("--raw")} hits a URL directly.
|
|
739
|
+
${c.bold("Manage your APIs")} ${c.dim("(fxa_live_ key or auto-minted Agent VC)")}
|
|
740
|
+
${c.cyan("planner api create --file <spec.json> [--publish]")} create (draft unless --publish)
|
|
741
|
+
${c.cyan("planner api list")} your api configs
|
|
742
|
+
${c.cyan("planner api get <idOrSlug>")} print the round-trippable spec
|
|
743
|
+
${c.cyan("planner api edit <idOrSlug> [--file <s>] [--publish]")} update fields / replace endpoints
|
|
744
|
+
${c.dim("Spec JSON: serverName, serverDescription, upstreamBaseUrl, endpoints[]. Wallet defaults from your account.")}
|
|
745
|
+
${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
746
|
`,
|
|
606
747
|
models: () => `
|
|
607
748
|
${c.bold("Models — merchant-centric")}
|
|
@@ -636,14 +777,18 @@ ${c.bold("Headline")}
|
|
|
636
777
|
planner plan "<task>" recommend the models, APIs & skills for a task
|
|
637
778
|
planner call <url> make a paid API call — handles the x402 mandate + payment
|
|
638
779
|
[--pay credits|usdc] [--method GET|POST] [--data '<json>']
|
|
780
|
+
[--header "Name: Value"] (repeatable; e.g. an upstream Bearer token)
|
|
639
781
|
|
|
640
782
|
${c.bold("Primitives")} ${c.dim("(all live)")}
|
|
641
783
|
planner search "<q>" [--type api,skill,model] discover resources
|
|
642
784
|
planner models [--vendor <slug>] model catalog + Units rates
|
|
785
|
+
planner vendors list fundable merchants (slug, models, price)
|
|
643
786
|
planner balance [vendor] prepaid Units per merchant
|
|
644
787
|
planner topup <vendor> [--credits <N>|--bundle <slug>] prepay Units (x402, via wallet)
|
|
645
788
|
planner ledger <vendor> spend/topup history
|
|
646
789
|
planner keys [create|update <id>|revoke <id>] manage fxa_live_ API keys (Agent VC only)
|
|
790
|
+
planner api create --file <spec.json> [--publish] create a oneshot API (draft by default)
|
|
791
|
+
planner api list | get <idOrSlug> | edit <idOrSlug> list / inspect / update your APIs
|
|
647
792
|
planner login <fxa_live_…> / planner whoami auth
|
|
648
793
|
planner info [units|auth|pay|keys|apis|models|skills] explain how the platform works
|
|
649
794
|
|
|
@@ -660,9 +805,31 @@ function die(msg, ctx) {
|
|
|
660
805
|
async function main() {
|
|
661
806
|
const args = argv.slice(2);
|
|
662
807
|
const cmd = args[0];
|
|
663
|
-
const FLAGS = ["--type", "--vendor", "--pay", "--method", "--data", "--budget", "--seconds", "--endpoint", "--credits", "--bundle", "--name", "--cap"];
|
|
664
|
-
const BOOLS = ["--raw"];
|
|
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"];
|
|
665
810
|
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
|
+
};
|
|
666
833
|
const type = getFlag("--type");
|
|
667
834
|
const vendor = getFlag("--vendor");
|
|
668
835
|
const rest = [];
|
|
@@ -680,9 +847,11 @@ async function main() {
|
|
|
680
847
|
pay: getFlag("--pay"), method: getFlag("--method"), data: getFlag("--data"),
|
|
681
848
|
budget: getFlag("--budget"), seconds: getFlag("--seconds"),
|
|
682
849
|
endpoint: getFlag("--endpoint"), raw: args.includes("--raw"),
|
|
850
|
+
headers: parseHeaders(getFlagAll("--header")),
|
|
683
851
|
});
|
|
684
852
|
case "search": return cmdSearch(arg, type);
|
|
685
853
|
case "models": return cmdModels(vendor);
|
|
854
|
+
case "vendors": return cmdVendors();
|
|
686
855
|
case "balance": case "bal": return cmdBalance(rest[0]);
|
|
687
856
|
case "topup":
|
|
688
857
|
return cmdTopup(rest[0], {
|
|
@@ -691,6 +860,14 @@ async function main() {
|
|
|
691
860
|
});
|
|
692
861
|
case "ledger": return cmdLedger(rest[0]);
|
|
693
862
|
case "keys": return cmdKeys(rest[0], rest.slice(1), { name: getFlag("--name"), cap: getFlag("--cap") });
|
|
863
|
+
case "api":
|
|
864
|
+
return cmdApi(rest[0], rest.slice(1), {
|
|
865
|
+
file: getFlag("--file"), name: getFlag("--name"),
|
|
866
|
+
upstream: getFlag("--upstream"), wallet: getFlag("--wallet"),
|
|
867
|
+
publish: args.includes("--publish"), private: args.includes("--private"),
|
|
868
|
+
requireAgentAuth: args.includes("--require-agent-auth"),
|
|
869
|
+
dryRun: args.includes("--dry-run"),
|
|
870
|
+
});
|
|
694
871
|
case "login": return cmdLogin(rest[0]);
|
|
695
872
|
case "whoami": return cmdWhoami();
|
|
696
873
|
case "info": return cmdInfo(rest[0]);
|