@fiscalmindset/blindfold 0.4.1 β†’ 0.4.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/bin/blindfold.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  */
6
6
  import { type Argv, parseArgv } from "./cli-shared.ts";
7
7
  import { c, bad } from "../src/color.ts";
8
- import { bannerBox, commandBox, nearest } from "../src/tui.ts";
8
+ import { nearest } from "../src/tui.ts";
9
+ import { renderMainHelp, renderCommandHelp, findCommand } from "../src/help.ts";
9
10
  import { handleAuth } from "./cmd-auth.ts";
10
11
  import { handleSecrets } from "./cmd-secrets.ts";
11
12
  import { handleLifecycle } from "./cmd-lifecycle.ts";
@@ -35,9 +36,21 @@ async function main(): Promise<void> {
35
36
  const cmdArgs = ddIdx >= 0 ? raw.slice(ddIdx + 1) : [];
36
37
  const argv = parseArgv(ddIdx >= 0 ? raw.slice(0, ddIdx) : raw);
37
38
  const cmd = argv._[0] ?? "help";
39
+
40
+ // `blindfold help` / `blindfold help <cmd>`
41
+ if (cmd === "help" || cmd === "--help" || cmd === "-h") {
42
+ const sub = String(argv._[1] ?? "");
43
+ console.log(sub && findCommand(sub) ? renderCommandHelp(sub) : renderMainHelp());
44
+ return;
45
+ }
46
+ // `blindfold <cmd> --help` / `-h` β†’ per-command help instead of running it.
47
+ if ((argv.flags.help || argv.flags.h) && findCommand(cmd)) {
48
+ console.log(renderCommandHelp(cmd));
49
+ return;
50
+ }
51
+
38
52
  const handler = ROUTES[cmd];
39
53
  if (handler) await handler(cmd, argv, cmdArgs);
40
- else if (cmd === "help" || cmd === "--help" || cmd === "-h") printHelp();
41
54
  else printUnknown(cmd);
42
55
  }
43
56
 
@@ -54,74 +67,6 @@ function printUnknown(cmd: string): void {
54
67
  process.exit(1);
55
68
  }
56
69
 
57
- function printHelp(): void {
58
- const groups: Array<[string, Array<[string, string]>]> = [
59
- ["πŸš€ Get started", [
60
- ["signup", "Self-serve: mint a funded Terminal 3 testnet tenant (key generated locally, email-verified)."],
61
- ["init", "Guided zero-knowledge setup: .env, build, auth, publish, seed; can auto-start the proxy."],
62
- ["doctor", "Show mode + config and run a live tenant health check."],
63
- ["credit", "Show the tenant's Terminal 3 token balance (costs nothing)."],
64
- ["verify", "Handshake + authenticate against T3 (smoke test)."],
65
- ]],
66
- ["πŸ”‘ Secrets", [
67
- ["register", "Seal a secret into the enclave (hidden prompt; never touches disk)."],
68
- ["use", "Release a sealed secret into one command as $ENV β€” never back in your env."],
69
- ["export", "CI: release a sealed secret into $GITHUB_ENV (masked in logs)."],
70
- ["rotate", "Replace a sealed secret's value (snapshots the old one for rollback)."],
71
- ["rollback", "Restore a previous value snapshotted by rotate."],
72
- ["versions", "List the snapshots available to roll back to (metadata only)."],
73
- ["migrate", "Seal every secret in .env at once, then remove the plaintext lines."],
74
- ]],
75
- ["🌐 Proxy & serve", [
76
- ["proxy", "Run the local sentinel proxy. --auth mints a session token; --socket binds a 0600 unix socket."],
77
- ["attest", "Verify the enclave's TDX attestation (Intel root CA). --pin gates seal/proxy on the code measurement."],
78
- ["dashboard", "Live HTML dashboard of proxy usage (default :8799)."],
79
- ["stats", "CLI summary of proxy usage (stats:clear wipes it)."],
80
- ]],
81
- ["πŸ‘₯ Team & sharing", [
82
- ["grant", "Authorize the contract to call these hosts (e.g. --host api.openai.com)."],
83
- ["share", "Let a teammate's agent USE your sealed keys for a host β€” forward only, no plaintext."],
84
- ["revoke", "Remove a teammate's access β€” immediate and complete."],
85
- ]],
86
- ["πŸ“¦ Enclave & admin", [
87
- ["publish", "Publish the Rust→WASM contract to your tenant (one-time)."],
88
- ["status", "One-glance: mode, tenant health, and sealed secrets."],
89
- ["sealed", "List sealed keys β€” metadata only, never the value."],
90
- ["audit", "Verify the ledger hash-chain and reconcile it against the enclave."],
91
- ["compat", "Scan this machine for AI agent tools + print the env-var swap for each."],
92
- ["update", "Update the global install (from npm, or --from <repo>)."],
93
- ]],
94
- ["πŸ‘€ Account", [
95
- ["login", "Store existing Terminal 3 credentials (key β†’ OS keychain)."],
96
- ["logout", "Remove stored credentials."],
97
- ["whoami", "Show tenant, env, and key source (never the value)."],
98
- ]],
99
- ["πŸ€– Agent skill", [
100
- ["skill", "install [--global|--cursor|--opencode|--cline|--all] / uninstall β€” the agent skill for your coding agent."],
101
- ]],
102
- ];
103
-
104
- const out: string[] = [
105
- "",
106
- bannerBox("πŸ›‘οΈ Blindfold", "Protect your AI agent's API keys with Terminal 3 enclaves. The agent only ever holds a placeholder β€” the real key is substituted inside the TDX enclave."),
107
- "",
108
- ];
109
- for (const [title, rows] of groups) {
110
- out.push(commandBox(title, rows));
111
- out.push("");
112
- }
113
- out.push(
114
- c.bold("Quick start"),
115
- ` ${c.cyan("blindfold signup --email you@x.com")} ${c.gray("# create a funded testnet tenant")}`,
116
- ` ${c.cyan("blindfold register --name openai_api_key")}`,
117
- ` ${c.cyan("blindfold proxy")} ${c.gray("# point your agent at http://127.0.0.1:8787")}`,
118
- "",
119
- `${c.gray("Docs:")} ${c.cyan("https://www.npmjs.com/package/@fiscalmindset/blindfold")} ${c.gray("Β·")} ${c.gray("Run")} ${c.cyan("blindfold <command> --help")} ${c.gray("for details")}`,
120
- "",
121
- );
122
- console.log(out.join("\n"));
123
- }
124
-
125
70
  main().catch((e) => {
126
71
  console.error("βœ–", (e as Error).message);
127
72
  process.exit(1);
package/dist/cli.mjs CHANGED
@@ -1395,9 +1395,10 @@ function vlen(s) {
1395
1395
  }
1396
1396
  return w;
1397
1397
  }
1398
- function padEnd(s, w) {
1398
+ function pad(s, w) {
1399
1399
  return s + " ".repeat(Math.max(0, w - vlen(s)));
1400
1400
  }
1401
+ var padEnd = pad;
1401
1402
  function wrapText(text, width) {
1402
1403
  if (width <= 4) return [text];
1403
1404
  const out = [];
@@ -1429,31 +1430,23 @@ function bannerBox(title, subtitle) {
1429
1430
  lines.push(bot);
1430
1431
  return lines.join("\n");
1431
1432
  }
1432
- function commandBox(title, rows) {
1433
+ function boxLines(title, lines) {
1433
1434
  const w = termWidth();
1434
1435
  const inner = w - 4;
1435
- const gap = 2;
1436
- const longest = rows.reduce((m, [cmd]) => Math.max(m, vlen(cmd)), 0);
1437
- const cmdW = Math.min(longest, Math.max(10, Math.floor(inner * 0.34)));
1438
- const descW = Math.max(12, inner - cmdW - gap);
1439
1436
  const prefix = B.tl + B.h + " ";
1440
- const fill = Math.max(
1441
- 0,
1442
- w - vlen(prefix) - vlen(title) - 1 - 1
1443
- /* tr */
1444
- );
1437
+ const fill = Math.max(0, w - vlen(prefix) - vlen(title) - 2);
1445
1438
  const out = [dim(prefix) + c.bold(title) + dim(" " + B.h.repeat(fill) + B.tr)];
1446
- for (const [cmd, desc] of rows) {
1447
- const dLines = wrapText(desc, descW);
1448
- const cmdCell = padEnd(c.cyan(cmd), cmdW);
1449
- out.push(dim(B.v) + " " + cmdCell + " ".repeat(gap) + padEnd(dLines[0] ?? "", descW) + " " + dim(B.v));
1450
- for (let i = 1; i < dLines.length; i++) {
1451
- out.push(dim(B.v) + " " + " ".repeat(cmdW + gap) + padEnd(dim(dLines[i] ?? ""), descW) + " " + dim(B.v));
1452
- }
1453
- }
1439
+ for (const l of lines) out.push(dim(B.v) + " " + pad(l, inner) + " " + dim(B.v));
1454
1440
  out.push(dim(B.bl + B.h.repeat(w - 2) + B.br));
1455
1441
  return out.join("\n");
1456
1442
  }
1443
+ function rule(label = "") {
1444
+ const w = termWidth();
1445
+ if (!label) return dim(B.h.repeat(w));
1446
+ const left = B.h + B.h + " ";
1447
+ const fill = Math.max(0, w - vlen(left) - vlen(label) - 1);
1448
+ return dim(left) + c.bold(label) + dim(" " + B.h.repeat(fill));
1449
+ }
1457
1450
  function nearest(input, candidates) {
1458
1451
  let best = null;
1459
1452
  let bestD = Infinity;
@@ -1478,6 +1471,267 @@ function editDistance(a, b) {
1478
1471
  return dp[a.length][b.length];
1479
1472
  }
1480
1473
 
1474
+ // src/help.ts
1475
+ var GROUP_ORDER = [
1476
+ "\u{1F680} Get started",
1477
+ "\u{1F511} Secrets",
1478
+ "\u{1F310} Proxy & serve",
1479
+ "\u{1F465} Team & sharing",
1480
+ "\u{1F4E6} Enclave & admin",
1481
+ "\u{1F464} Account",
1482
+ "\u{1F916} Agent skill"
1483
+ ];
1484
+ var COMMANDS = [
1485
+ // ── Get started ──────────────────────────────────────────────────────────
1486
+ {
1487
+ name: "signup",
1488
+ group: "\u{1F680} Get started",
1489
+ usage: "[--email <addr>] [--first <name>] [--last <name>]",
1490
+ summary: "Self-serve: mint a funded Terminal 3 testnet tenant (key generated locally, email-verified).",
1491
+ flags: [
1492
+ ["--email <addr>", "Email for the tenant; a verification code is sent there. Prompts if omitted."],
1493
+ ["--first <name>", "First name for the profile (default: from the email local-part)."],
1494
+ ["--last <name>", 'Last name for the profile (default: "Tenant").'],
1495
+ ["--otp <code>", "Supply the emailed code non-interactively (for scripts)."]
1496
+ ],
1497
+ examples: ["blindfold signup --email you@example.com"],
1498
+ notes: "Testnet-only. One email binds to one tenant \u2014 Gmail +aliases give fresh identities. Already have credentials? Use `blindfold login`."
1499
+ },
1500
+ {
1501
+ name: "init",
1502
+ group: "\u{1F680} Get started",
1503
+ usage: "[--seed <KV:ENV>]... [--start]",
1504
+ summary: "Guided zero-knowledge setup: .env, build, auth, publish, seed; can auto-start the proxy.",
1505
+ flags: [
1506
+ ["--seed <KV:ENV>", "Seal <ENV>'s value into secret <KV> as the last setup step (repeatable)."],
1507
+ ["--start", "Launch the proxy when setup finishes."]
1508
+ ],
1509
+ examples: ["blindfold init", "blindfold init --seed openai_api_key:OPENAI_API_KEY --start"]
1510
+ },
1511
+ { name: "doctor", group: "\u{1F680} Get started", usage: "", summary: "Show mode + config and run a live tenant health check (handshake + authenticate + me).", examples: ["blindfold doctor"] },
1512
+ {
1513
+ name: "credit",
1514
+ group: "\u{1F680} Get started",
1515
+ usage: "[--json]",
1516
+ aliases: ["balance"],
1517
+ summary: "Show the tenant's Terminal 3 token balance (costs nothing; works even at zero).",
1518
+ flags: [["--json", "Print the raw balance object instead of the formatted view."]],
1519
+ examples: ["blindfold credit"]
1520
+ },
1521
+ { name: "verify", group: "\u{1F680} Get started", usage: "", summary: "Handshake + authenticate against Terminal 3 (a smoke test).", examples: ["blindfold verify"] },
1522
+ // ── Secrets ──────────────────────────────────────────────────────────────
1523
+ {
1524
+ name: "register",
1525
+ group: "\u{1F511} Secrets",
1526
+ usage: "--name <KEY> [--from-env <ENV>]",
1527
+ summary: "Seal a secret into the enclave (one-time). Hidden prompt by default \u2014 never touches disk.",
1528
+ flags: [
1529
+ ["--name <KEY>", "Logical name for the sealed secret (required)."],
1530
+ ["--from-env <ENV>", "Read the value from process.env.<ENV> instead of prompting."]
1531
+ ],
1532
+ examples: ["blindfold register --name openai_api_key", 'echo "$KEY" | blindfold register --name openai_api_key'],
1533
+ notes: "Without --from-env it prompts with no echo (preferred). Piped stdin also works."
1534
+ },
1535
+ {
1536
+ name: "use",
1537
+ group: "\u{1F511} Secrets",
1538
+ usage: "--name <secret> [--as <ENV>] -- <cmd...> | --name <secret> --url <https>",
1539
+ summary: "Release a sealed secret into ONE command as $ENV \u2014 never back in your environment.",
1540
+ flags: [
1541
+ ["--name <secret>", "The sealed secret to release (required)."],
1542
+ ["--as <ENV>", "Env var to inject it as. Auto-detected for known tools (gh\u2192GH_TOKEN, psql\u2192PGPASSWORD\u2026)."],
1543
+ ["--url <https>", "Instead of running a command, do a quick authenticated GET to this URL."]
1544
+ ],
1545
+ examples: ["blindfold use --name gh_token -- gh api user", "blindfold use --name openai_api_key --as OPENAI_API_KEY -- node agent.js"]
1546
+ },
1547
+ {
1548
+ name: "export",
1549
+ group: "\u{1F511} Secrets",
1550
+ usage: "--name <secret> [--as <ENV>]",
1551
+ summary: "CI: release a sealed secret into $GITHUB_ENV for later steps (masked in logs).",
1552
+ flags: [["--name <secret>", "The sealed secret to export (required)."], ["--as <ENV>", "Env var name to write (default: the secret name upper-cased)."]],
1553
+ examples: ["blindfold export --name openai_api_key --as OPENAI_API_KEY"]
1554
+ },
1555
+ {
1556
+ name: "rotate",
1557
+ group: "\u{1F511} Secrets",
1558
+ usage: "--name <secret> [--from-env <ENV>]",
1559
+ summary: "Replace a sealed secret's value; snapshots the old one for rollback (fingerprints only).",
1560
+ flags: [["--name <secret>", "The sealed secret to rotate (required)."], ["--from-env <ENV>", "Read the new value from an env var instead of prompting."]],
1561
+ examples: ["blindfold rotate --name stripe_secret_key"]
1562
+ },
1563
+ {
1564
+ name: "rollback",
1565
+ group: "\u{1F511} Secrets",
1566
+ usage: "--name <secret> [--to <fp|iso-ts>]",
1567
+ summary: "Restore a previous value snapshotted by rotate (most recent by default).",
1568
+ flags: [["--name <secret>", "The sealed secret to roll back (required)."], ["--to <fp|iso-ts>", "Target a specific snapshot by fingerprint or timestamp."]],
1569
+ examples: ["blindfold rollback --name stripe_secret_key"]
1570
+ },
1571
+ { name: "versions", group: "\u{1F511} Secrets", usage: "[--name <secret>]", summary: "List the snapshots available to roll back to (metadata only).", flags: [["--name <secret>", "Limit to one secret."]], examples: ["blindfold versions --name stripe_secret_key"] },
1572
+ {
1573
+ name: "migrate",
1574
+ group: "\u{1F511} Secrets",
1575
+ usage: "[--dry-run] [--keep]",
1576
+ summary: "Seal every secret in .env at once, then remove the plaintext lines (backup kept).",
1577
+ flags: [["--dry-run", "Preview what would be sealed, change nothing."], ["--keep", "Comment out the .env lines instead of deleting them."]],
1578
+ examples: ["blindfold migrate --dry-run", "blindfold migrate"],
1579
+ notes: "Skips T3 creds + config (T3N_API_KEY / DID)."
1580
+ },
1581
+ // ── Proxy & serve ────────────────────────────────────────────────────────
1582
+ {
1583
+ name: "proxy",
1584
+ group: "\u{1F310} Proxy & serve",
1585
+ usage: "[--port <n>] [--auth] [--socket [path]]",
1586
+ summary: "Run the local sentinel proxy your agent points at. Substitution happens in the enclave.",
1587
+ flags: [
1588
+ ["--port <n>", "TCP port to listen on (default 8787)."],
1589
+ ["--auth", "Mint a per-session token so only the wrapped agent can use the proxy."],
1590
+ ["--socket [path]", "Bind a 0600 unix-domain socket (only your OS user can connect)."]
1591
+ ],
1592
+ examples: ["blindfold proxy", "blindfold proxy --auth", "blindfold proxy --socket"],
1593
+ notes: "Point your agent at http://127.0.0.1:8787 and send Authorization: Bearer __BLINDFOLD__."
1594
+ },
1595
+ {
1596
+ name: "attest",
1597
+ group: "\u{1F310} Proxy & serve",
1598
+ usage: "[--expect-rtmr3 <b64>] [--pin] [--json]",
1599
+ summary: "Verify the enclave's TDX attestation (chains to Intel's root CA).",
1600
+ flags: [
1601
+ ["--expect-rtmr3 <b64>", "Assert the code measurement equals this value."],
1602
+ ["--pin", "Record the RTMR3 so seal/proxy auto-verify the enclave first."],
1603
+ ["--json", "Machine-readable output."]
1604
+ ],
1605
+ examples: ["blindfold attest", "blindfold attest --pin"]
1606
+ },
1607
+ { name: "dashboard", group: "\u{1F310} Proxy & serve", usage: "[--port <n>]", summary: "Live HTML dashboard of proxy usage (default :8799).", flags: [["--port <n>", "Port for the dashboard (default 8799)."]], examples: ["blindfold dashboard"] },
1608
+ { name: "stats", group: "\u{1F310} Proxy & serve", usage: "", summary: "CLI summary of proxy usage. `stats:clear` wipes the log.", examples: ["blindfold stats", "blindfold stats:clear"] },
1609
+ // ── Team & sharing ───────────────────────────────────────────────────────
1610
+ {
1611
+ name: "grant",
1612
+ group: "\u{1F465} Team & sharing",
1613
+ usage: "--host <host>[,<host2>...]",
1614
+ summary: "Authorize the contract to call these hosts (required before proxy/in-enclave can reach them).",
1615
+ flags: [["--host <host,...>", "Comma-separated egress hosts to allow (additive)."]],
1616
+ examples: ["blindfold grant --host api.openai.com", "blindfold grant --host api.github.com,api.stripe.com"]
1617
+ },
1618
+ {
1619
+ name: "share",
1620
+ group: "\u{1F465} Team & sharing",
1621
+ usage: "--to <agent-did> --host <host>[,...]",
1622
+ summary: "Let a teammate's agent USE your sealed keys for a host \u2014 forward only, never the plaintext.",
1623
+ flags: [["--to <agent-did>", "The teammate's agent DID to authorize."], ["--host <host,...>", "Hosts they may reach through the enclave."]],
1624
+ examples: ["blindfold share --to did:t3n:\u2026 --host api.openai.com"]
1625
+ },
1626
+ { name: "revoke", group: "\u{1F465} Team & sharing", usage: "--to <agent-did>", summary: "Remove a teammate's access \u2014 immediate and complete.", flags: [["--to <agent-did>", "The teammate's agent DID to revoke."]], examples: ["blindfold revoke --to did:t3n:\u2026"] },
1627
+ // ── Enclave & admin ──────────────────────────────────────────────────────
1628
+ { name: "publish", group: "\u{1F4E6} Enclave & admin", usage: "[--wasm <path>]", summary: "Publish the Rust\u2192WASM contract to your tenant (one-time).", flags: [["--wasm <path>", "Path to blindfold_proxy.wasm (defaults to the bundled build)."]], examples: ["blindfold publish"] },
1629
+ { name: "status", group: "\u{1F4E6} Enclave & admin", usage: "", summary: "One-glance overview: mode, tenant health, and the list of sealed secrets.", examples: ["blindfold status"] },
1630
+ { name: "sealed", group: "\u{1F4E6} Enclave & admin", usage: "", summary: "List sealed keys \u2014 metadata only (name, byte-length, when, where). Never the value.", examples: ["blindfold sealed"] },
1631
+ { name: "audit", group: "\u{1F4E6} Enclave & admin", usage: "", summary: "Verify the ledger hash-chain and reconcile it against the enclave (flags drift/tampering).", examples: ["blindfold audit"] },
1632
+ { name: "compat", group: "\u{1F4E6} Enclave & admin", usage: "[--json]", summary: "Scan this machine for AI agent tools and print the exact env-var swap for each.", flags: [["--json", "Machine-readable output."]], examples: ["blindfold compat"] },
1633
+ { name: "update", group: "\u{1F4E6} Enclave & admin", usage: "[--from <path>]", aliases: ["upgrade"], summary: "Update the global install (from npm, or a local repo with --from).", flags: [["--from <path>", "Build + install from a local repo checkout instead of npm."]], examples: ["blindfold update"] },
1634
+ // ── Account ──────────────────────────────────────────────────────────────
1635
+ {
1636
+ name: "login",
1637
+ group: "\u{1F464} Account",
1638
+ usage: "[--did <did>] [--key <0x\u2026>] [--env <net>] [--file]",
1639
+ summary: "Store EXISTING Terminal 3 credentials (tenant key \u2192 OS keychain).",
1640
+ flags: [
1641
+ ["--did <did>", "Tenant DID (prompts if omitted)."],
1642
+ ["--key <0x\u2026>", "Tenant key (prompts hidden if omitted)."],
1643
+ ["--env <net>", "testnet (default) or production."],
1644
+ ["--file", "Force a 0600 config file instead of the OS keychain."]
1645
+ ],
1646
+ examples: ["blindfold login", "blindfold login --did did:t3n:\u2026 --env testnet"]
1647
+ },
1648
+ { name: "logout", group: "\u{1F464} Account", usage: "", summary: "Remove stored credentials (keychain entry + config file).", examples: ["blindfold logout"] },
1649
+ { name: "whoami", group: "\u{1F464} Account", usage: "", summary: "Show tenant, env, and key source \u2014 never the value.", examples: ["blindfold whoami"] },
1650
+ // ── Agent skill ──────────────────────────────────────────────────────────
1651
+ {
1652
+ name: "skill",
1653
+ group: "\u{1F916} Agent skill",
1654
+ usage: "install [--global|--cursor|--opencode|--cline|--all] | uninstall",
1655
+ summary: "Install the Blindfold agent skill so your coding agent handles secrets safely.",
1656
+ flags: [
1657
+ ["--global", "Install for every Claude Code session on this machine."],
1658
+ ["--cursor / --opencode / --cline", "Install for that agent instead of Claude Code."],
1659
+ ["--all", "Install everywhere at once."]
1660
+ ],
1661
+ examples: ["blindfold skill install", "blindfold skill install --all"]
1662
+ }
1663
+ ];
1664
+ function findCommand(name) {
1665
+ return COMMANDS.find((c2) => c2.name === name || c2.aliases?.includes(name));
1666
+ }
1667
+ function renderMainHelp() {
1668
+ const w = termWidth();
1669
+ const out = [
1670
+ "",
1671
+ bannerBox("\u{1F6E1}\uFE0F Blindfold", "Protect your AI agent's API keys with Terminal 3 enclaves. The agent only ever holds a placeholder \u2014 the real key is substituted inside the TDX enclave."),
1672
+ ""
1673
+ ];
1674
+ const nameW = Math.min(12, COMMANDS.reduce((m, cmd) => Math.max(m, vlen(cmd.name)), 0));
1675
+ const descW = Math.max(16, w - 4 - nameW - 2);
1676
+ for (const group of GROUP_ORDER) {
1677
+ const cmds = COMMANDS.filter((cmd) => cmd.group === group);
1678
+ const lines = [];
1679
+ for (const cmd of cmds) {
1680
+ const dl = wrapText(cmd.summary, descW);
1681
+ lines.push(pad(c.cyan(cmd.name), nameW) + " " + (dl[0] ?? ""));
1682
+ for (let i = 1; i < dl.length; i++) lines.push(pad("", nameW) + " " + c.gray(dl[i] ?? ""));
1683
+ if (cmd.usage) {
1684
+ lines.push(pad("", nameW) + " " + c.gray("\u21B3 blindfold " + cmd.name + " " + cmd.usage));
1685
+ }
1686
+ }
1687
+ out.push(boxLines(group, lines));
1688
+ out.push("");
1689
+ }
1690
+ out.push(
1691
+ c.bold("Quick start"),
1692
+ ` ${c.cyan("blindfold signup --email you@x.com")} ${c.gray("# create a funded testnet tenant")}`,
1693
+ ` ${c.cyan("blindfold register --name openai_api_key")}`,
1694
+ ` ${c.cyan("blindfold proxy")} ${c.gray("# point your agent at http://127.0.0.1:8787")}`,
1695
+ "",
1696
+ `${c.gray("Details for any command:")} ${c.cyan("blindfold <command> --help")} ${c.gray("\xB7")} ${c.gray("Docs:")} ${c.cyan("npmjs.com/package/@fiscalmindset/blindfold")}`,
1697
+ ""
1698
+ );
1699
+ return out.join("\n");
1700
+ }
1701
+ function renderCommandHelp(name) {
1702
+ const cmd = findCommand(name);
1703
+ if (!cmd) return renderMainHelp();
1704
+ const w = termWidth();
1705
+ const out = ["", boxLines(`blindfold ${cmd.name}`, wrapText(cmd.summary, w - 4)), ""];
1706
+ out.push(rule("Usage"));
1707
+ out.push(" " + c.cyan(`blindfold ${cmd.name}${cmd.usage ? " " + cmd.usage : ""}`));
1708
+ if (cmd.aliases?.length) out.push(" " + c.gray(`alias: ${cmd.aliases.map((a) => "blindfold " + a).join(", ")}`));
1709
+ out.push("");
1710
+ if (cmd.flags?.length) {
1711
+ out.push(rule("Flags"));
1712
+ const flagW = Math.min(24, cmd.flags.reduce((m, [f]) => Math.max(m, vlen(f)), 0));
1713
+ const dW = Math.max(16, w - 2 - flagW - 2);
1714
+ for (const [flag, desc] of cmd.flags) {
1715
+ const dl = wrapText(desc, dW);
1716
+ out.push(" " + pad(c.yellow(flag), flagW) + " " + (dl[0] ?? ""));
1717
+ for (let i = 1; i < dl.length; i++) out.push(" " + pad("", flagW) + " " + c.gray(dl[i] ?? ""));
1718
+ }
1719
+ out.push("");
1720
+ }
1721
+ if (cmd.examples?.length) {
1722
+ out.push(rule("Examples"));
1723
+ for (const ex of cmd.examples) out.push(" " + c.cyan(ex));
1724
+ out.push("");
1725
+ }
1726
+ if (cmd.notes) {
1727
+ out.push(rule("Notes"));
1728
+ for (const nl of wrapText(cmd.notes, w - 2)) out.push(" " + c.gray(nl));
1729
+ out.push("");
1730
+ }
1731
+ out.push(c.gray("See all commands: ") + c.cyan("blindfold help"));
1732
+ return out.join("\n");
1733
+ }
1734
+
1481
1735
  // bin/cmd-auth.ts
1482
1736
  init_env();
1483
1737
  init_keychain();
@@ -4367,9 +4621,17 @@ async function main() {
4367
4621
  const cmdArgs = ddIdx >= 0 ? raw.slice(ddIdx + 1) : [];
4368
4622
  const argv = parseArgv(ddIdx >= 0 ? raw.slice(0, ddIdx) : raw);
4369
4623
  const cmd = argv._[0] ?? "help";
4624
+ if (cmd === "help" || cmd === "--help" || cmd === "-h") {
4625
+ const sub = String(argv._[1] ?? "");
4626
+ console.log(sub && findCommand(sub) ? renderCommandHelp(sub) : renderMainHelp());
4627
+ return;
4628
+ }
4629
+ if ((argv.flags.help || argv.flags.h) && findCommand(cmd)) {
4630
+ console.log(renderCommandHelp(cmd));
4631
+ return;
4632
+ }
4370
4633
  const handler = ROUTES[cmd];
4371
4634
  if (handler) await handler(cmd, argv, cmdArgs);
4372
- else if (cmd === "help" || cmd === "--help" || cmd === "-h") printHelp();
4373
4635
  else printUnknown(cmd);
4374
4636
  }
4375
4637
  function printUnknown(cmd) {
@@ -4381,72 +4643,6 @@ function printUnknown(cmd) {
4381
4643
  console.error(c.gray(" Run ") + c.cyan("blindfold help") + c.gray(" to see all commands."));
4382
4644
  process.exit(1);
4383
4645
  }
4384
- function printHelp() {
4385
- const groups = [
4386
- ["\u{1F680} Get started", [
4387
- ["signup", "Self-serve: mint a funded Terminal 3 testnet tenant (key generated locally, email-verified)."],
4388
- ["init", "Guided zero-knowledge setup: .env, build, auth, publish, seed; can auto-start the proxy."],
4389
- ["doctor", "Show mode + config and run a live tenant health check."],
4390
- ["credit", "Show the tenant's Terminal 3 token balance (costs nothing)."],
4391
- ["verify", "Handshake + authenticate against T3 (smoke test)."]
4392
- ]],
4393
- ["\u{1F511} Secrets", [
4394
- ["register", "Seal a secret into the enclave (hidden prompt; never touches disk)."],
4395
- ["use", "Release a sealed secret into one command as $ENV \u2014 never back in your env."],
4396
- ["export", "CI: release a sealed secret into $GITHUB_ENV (masked in logs)."],
4397
- ["rotate", "Replace a sealed secret's value (snapshots the old one for rollback)."],
4398
- ["rollback", "Restore a previous value snapshotted by rotate."],
4399
- ["versions", "List the snapshots available to roll back to (metadata only)."],
4400
- ["migrate", "Seal every secret in .env at once, then remove the plaintext lines."]
4401
- ]],
4402
- ["\u{1F310} Proxy & serve", [
4403
- ["proxy", "Run the local sentinel proxy. --auth mints a session token; --socket binds a 0600 unix socket."],
4404
- ["attest", "Verify the enclave's TDX attestation (Intel root CA). --pin gates seal/proxy on the code measurement."],
4405
- ["dashboard", "Live HTML dashboard of proxy usage (default :8799)."],
4406
- ["stats", "CLI summary of proxy usage (stats:clear wipes it)."]
4407
- ]],
4408
- ["\u{1F465} Team & sharing", [
4409
- ["grant", "Authorize the contract to call these hosts (e.g. --host api.openai.com)."],
4410
- ["share", "Let a teammate's agent USE your sealed keys for a host \u2014 forward only, no plaintext."],
4411
- ["revoke", "Remove a teammate's access \u2014 immediate and complete."]
4412
- ]],
4413
- ["\u{1F4E6} Enclave & admin", [
4414
- ["publish", "Publish the Rust\u2192WASM contract to your tenant (one-time)."],
4415
- ["status", "One-glance: mode, tenant health, and sealed secrets."],
4416
- ["sealed", "List sealed keys \u2014 metadata only, never the value."],
4417
- ["audit", "Verify the ledger hash-chain and reconcile it against the enclave."],
4418
- ["compat", "Scan this machine for AI agent tools + print the env-var swap for each."],
4419
- ["update", "Update the global install (from npm, or --from <repo>)."]
4420
- ]],
4421
- ["\u{1F464} Account", [
4422
- ["login", "Store existing Terminal 3 credentials (key \u2192 OS keychain)."],
4423
- ["logout", "Remove stored credentials."],
4424
- ["whoami", "Show tenant, env, and key source (never the value)."]
4425
- ]],
4426
- ["\u{1F916} Agent skill", [
4427
- ["skill", "install [--global|--cursor|--opencode|--cline|--all] / uninstall \u2014 the agent skill for your coding agent."]
4428
- ]]
4429
- ];
4430
- const out = [
4431
- "",
4432
- bannerBox("\u{1F6E1}\uFE0F Blindfold", "Protect your AI agent's API keys with Terminal 3 enclaves. The agent only ever holds a placeholder \u2014 the real key is substituted inside the TDX enclave."),
4433
- ""
4434
- ];
4435
- for (const [title, rows] of groups) {
4436
- out.push(commandBox(title, rows));
4437
- out.push("");
4438
- }
4439
- out.push(
4440
- c.bold("Quick start"),
4441
- ` ${c.cyan("blindfold signup --email you@x.com")} ${c.gray("# create a funded testnet tenant")}`,
4442
- ` ${c.cyan("blindfold register --name openai_api_key")}`,
4443
- ` ${c.cyan("blindfold proxy")} ${c.gray("# point your agent at http://127.0.0.1:8787")}`,
4444
- "",
4445
- `${c.gray("Docs:")} ${c.cyan("https://www.npmjs.com/package/@fiscalmindset/blindfold")} ${c.gray("\xB7")} ${c.gray("Run")} ${c.cyan("blindfold <command> --help")} ${c.gray("for details")}`,
4446
- ""
4447
- );
4448
- console.log(out.join("\n"));
4449
- }
4450
4646
  main().catch((e) => {
4451
4647
  console.error("\u2716", e.message);
4452
4648
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiscalmindset/blindfold",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "Make any AI agent's API keys un-leakable, by sealing them inside a Terminal 3 TDX enclave.",
6
6
  "license": "MIT",
package/src/help.ts ADDED
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Command registry + help renderers. Drives both the grouped `blindfold help`
3
+ * overview (with per-command usage) and the detailed `blindfold <cmd> --help`
4
+ * (usage + flags + examples). All output reflows to the terminal width.
5
+ */
6
+ import { c } from "./color.ts";
7
+ import { termWidth, vlen, wrapText, pad, bannerBox, boxLines, rule } from "./tui.ts";
8
+
9
+ interface CmdDef {
10
+ name: string;
11
+ group: string;
12
+ usage: string; // args/flags after the command name, e.g. "[--email <addr>]"
13
+ summary: string;
14
+ flags?: Array<[string, string]>;
15
+ examples?: string[];
16
+ notes?: string;
17
+ aliases?: string[];
18
+ }
19
+
20
+ const GROUP_ORDER = [
21
+ "πŸš€ Get started",
22
+ "πŸ”‘ Secrets",
23
+ "🌐 Proxy & serve",
24
+ "πŸ‘₯ Team & sharing",
25
+ "πŸ“¦ Enclave & admin",
26
+ "πŸ‘€ Account",
27
+ "πŸ€– Agent skill",
28
+ ];
29
+
30
+ export const COMMANDS: CmdDef[] = [
31
+ // ── Get started ──────────────────────────────────────────────────────────
32
+ {
33
+ name: "signup", group: "πŸš€ Get started",
34
+ usage: "[--email <addr>] [--first <name>] [--last <name>]",
35
+ summary: "Self-serve: mint a funded Terminal 3 testnet tenant (key generated locally, email-verified).",
36
+ flags: [
37
+ ["--email <addr>", "Email for the tenant; a verification code is sent there. Prompts if omitted."],
38
+ ["--first <name>", "First name for the profile (default: from the email local-part)."],
39
+ ["--last <name>", "Last name for the profile (default: \"Tenant\")."],
40
+ ["--otp <code>", "Supply the emailed code non-interactively (for scripts)."],
41
+ ],
42
+ examples: ["blindfold signup --email you@example.com"],
43
+ notes: "Testnet-only. One email binds to one tenant β€” Gmail +aliases give fresh identities. Already have credentials? Use `blindfold login`.",
44
+ },
45
+ {
46
+ name: "init", group: "πŸš€ Get started",
47
+ usage: "[--seed <KV:ENV>]... [--start]",
48
+ summary: "Guided zero-knowledge setup: .env, build, auth, publish, seed; can auto-start the proxy.",
49
+ flags: [
50
+ ["--seed <KV:ENV>", "Seal <ENV>'s value into secret <KV> as the last setup step (repeatable)."],
51
+ ["--start", "Launch the proxy when setup finishes."],
52
+ ],
53
+ examples: ["blindfold init", "blindfold init --seed openai_api_key:OPENAI_API_KEY --start"],
54
+ },
55
+ { name: "doctor", group: "πŸš€ Get started", usage: "", summary: "Show mode + config and run a live tenant health check (handshake + authenticate + me).", examples: ["blindfold doctor"] },
56
+ {
57
+ name: "credit", group: "πŸš€ Get started", usage: "[--json]", aliases: ["balance"],
58
+ summary: "Show the tenant's Terminal 3 token balance (costs nothing; works even at zero).",
59
+ flags: [["--json", "Print the raw balance object instead of the formatted view."]],
60
+ examples: ["blindfold credit"],
61
+ },
62
+ { name: "verify", group: "πŸš€ Get started", usage: "", summary: "Handshake + authenticate against Terminal 3 (a smoke test).", examples: ["blindfold verify"] },
63
+
64
+ // ── Secrets ──────────────────────────────────────────────────────────────
65
+ {
66
+ name: "register", group: "πŸ”‘ Secrets",
67
+ usage: "--name <KEY> [--from-env <ENV>]",
68
+ summary: "Seal a secret into the enclave (one-time). Hidden prompt by default β€” never touches disk.",
69
+ flags: [
70
+ ["--name <KEY>", "Logical name for the sealed secret (required)."],
71
+ ["--from-env <ENV>", "Read the value from process.env.<ENV> instead of prompting."],
72
+ ],
73
+ examples: ["blindfold register --name openai_api_key", "echo \"$KEY\" | blindfold register --name openai_api_key"],
74
+ notes: "Without --from-env it prompts with no echo (preferred). Piped stdin also works.",
75
+ },
76
+ {
77
+ name: "use", group: "πŸ”‘ Secrets",
78
+ usage: "--name <secret> [--as <ENV>] -- <cmd...> | --name <secret> --url <https>",
79
+ summary: "Release a sealed secret into ONE command as $ENV β€” never back in your environment.",
80
+ flags: [
81
+ ["--name <secret>", "The sealed secret to release (required)."],
82
+ ["--as <ENV>", "Env var to inject it as. Auto-detected for known tools (ghβ†’GH_TOKEN, psqlβ†’PGPASSWORD…)."],
83
+ ["--url <https>", "Instead of running a command, do a quick authenticated GET to this URL."],
84
+ ],
85
+ examples: ["blindfold use --name gh_token -- gh api user", "blindfold use --name openai_api_key --as OPENAI_API_KEY -- node agent.js"],
86
+ },
87
+ {
88
+ name: "export", group: "πŸ”‘ Secrets", usage: "--name <secret> [--as <ENV>]",
89
+ summary: "CI: release a sealed secret into $GITHUB_ENV for later steps (masked in logs).",
90
+ flags: [["--name <secret>", "The sealed secret to export (required)."], ["--as <ENV>", "Env var name to write (default: the secret name upper-cased)."]],
91
+ examples: ["blindfold export --name openai_api_key --as OPENAI_API_KEY"],
92
+ },
93
+ {
94
+ name: "rotate", group: "πŸ”‘ Secrets", usage: "--name <secret> [--from-env <ENV>]",
95
+ summary: "Replace a sealed secret's value; snapshots the old one for rollback (fingerprints only).",
96
+ flags: [["--name <secret>", "The sealed secret to rotate (required)."], ["--from-env <ENV>", "Read the new value from an env var instead of prompting."]],
97
+ examples: ["blindfold rotate --name stripe_secret_key"],
98
+ },
99
+ {
100
+ name: "rollback", group: "πŸ”‘ Secrets", usage: "--name <secret> [--to <fp|iso-ts>]",
101
+ summary: "Restore a previous value snapshotted by rotate (most recent by default).",
102
+ flags: [["--name <secret>", "The sealed secret to roll back (required)."], ["--to <fp|iso-ts>", "Target a specific snapshot by fingerprint or timestamp."]],
103
+ examples: ["blindfold rollback --name stripe_secret_key"],
104
+ },
105
+ { name: "versions", group: "πŸ”‘ Secrets", usage: "[--name <secret>]", summary: "List the snapshots available to roll back to (metadata only).", flags: [["--name <secret>", "Limit to one secret."]], examples: ["blindfold versions --name stripe_secret_key"] },
106
+ {
107
+ name: "migrate", group: "πŸ”‘ Secrets", usage: "[--dry-run] [--keep]",
108
+ summary: "Seal every secret in .env at once, then remove the plaintext lines (backup kept).",
109
+ flags: [["--dry-run", "Preview what would be sealed, change nothing."], ["--keep", "Comment out the .env lines instead of deleting them."]],
110
+ examples: ["blindfold migrate --dry-run", "blindfold migrate"],
111
+ notes: "Skips T3 creds + config (T3N_API_KEY / DID).",
112
+ },
113
+
114
+ // ── Proxy & serve ────────────────────────────────────────────────────────
115
+ {
116
+ name: "proxy", group: "🌐 Proxy & serve", usage: "[--port <n>] [--auth] [--socket [path]]",
117
+ summary: "Run the local sentinel proxy your agent points at. Substitution happens in the enclave.",
118
+ flags: [
119
+ ["--port <n>", "TCP port to listen on (default 8787)."],
120
+ ["--auth", "Mint a per-session token so only the wrapped agent can use the proxy."],
121
+ ["--socket [path]", "Bind a 0600 unix-domain socket (only your OS user can connect)."],
122
+ ],
123
+ examples: ["blindfold proxy", "blindfold proxy --auth", "blindfold proxy --socket"],
124
+ notes: "Point your agent at http://127.0.0.1:8787 and send Authorization: Bearer __BLINDFOLD__.",
125
+ },
126
+ {
127
+ name: "attest", group: "🌐 Proxy & serve", usage: "[--expect-rtmr3 <b64>] [--pin] [--json]",
128
+ summary: "Verify the enclave's TDX attestation (chains to Intel's root CA).",
129
+ flags: [
130
+ ["--expect-rtmr3 <b64>", "Assert the code measurement equals this value."],
131
+ ["--pin", "Record the RTMR3 so seal/proxy auto-verify the enclave first."],
132
+ ["--json", "Machine-readable output."],
133
+ ],
134
+ examples: ["blindfold attest", "blindfold attest --pin"],
135
+ },
136
+ { name: "dashboard", group: "🌐 Proxy & serve", usage: "[--port <n>]", summary: "Live HTML dashboard of proxy usage (default :8799).", flags: [["--port <n>", "Port for the dashboard (default 8799)."]], examples: ["blindfold dashboard"] },
137
+ { name: "stats", group: "🌐 Proxy & serve", usage: "", summary: "CLI summary of proxy usage. `stats:clear` wipes the log.", examples: ["blindfold stats", "blindfold stats:clear"] },
138
+
139
+ // ── Team & sharing ───────────────────────────────────────────────────────
140
+ {
141
+ name: "grant", group: "πŸ‘₯ Team & sharing", usage: "--host <host>[,<host2>...]",
142
+ summary: "Authorize the contract to call these hosts (required before proxy/in-enclave can reach them).",
143
+ flags: [["--host <host,...>", "Comma-separated egress hosts to allow (additive)."]],
144
+ examples: ["blindfold grant --host api.openai.com", "blindfold grant --host api.github.com,api.stripe.com"],
145
+ },
146
+ {
147
+ name: "share", group: "πŸ‘₯ Team & sharing", usage: "--to <agent-did> --host <host>[,...]",
148
+ summary: "Let a teammate's agent USE your sealed keys for a host β€” forward only, never the plaintext.",
149
+ flags: [["--to <agent-did>", "The teammate's agent DID to authorize."], ["--host <host,...>", "Hosts they may reach through the enclave."]],
150
+ examples: ["blindfold share --to did:t3n:… --host api.openai.com"],
151
+ },
152
+ { name: "revoke", group: "πŸ‘₯ Team & sharing", usage: "--to <agent-did>", summary: "Remove a teammate's access β€” immediate and complete.", flags: [["--to <agent-did>", "The teammate's agent DID to revoke."]], examples: ["blindfold revoke --to did:t3n:…"] },
153
+
154
+ // ── Enclave & admin ──────────────────────────────────────────────────────
155
+ { name: "publish", group: "πŸ“¦ Enclave & admin", usage: "[--wasm <path>]", summary: "Publish the Rustβ†’WASM contract to your tenant (one-time).", flags: [["--wasm <path>", "Path to blindfold_proxy.wasm (defaults to the bundled build)."]], examples: ["blindfold publish"] },
156
+ { name: "status", group: "πŸ“¦ Enclave & admin", usage: "", summary: "One-glance overview: mode, tenant health, and the list of sealed secrets.", examples: ["blindfold status"] },
157
+ { name: "sealed", group: "πŸ“¦ Enclave & admin", usage: "", summary: "List sealed keys β€” metadata only (name, byte-length, when, where). Never the value.", examples: ["blindfold sealed"] },
158
+ { name: "audit", group: "πŸ“¦ Enclave & admin", usage: "", summary: "Verify the ledger hash-chain and reconcile it against the enclave (flags drift/tampering).", examples: ["blindfold audit"] },
159
+ { name: "compat", group: "πŸ“¦ Enclave & admin", usage: "[--json]", summary: "Scan this machine for AI agent tools and print the exact env-var swap for each.", flags: [["--json", "Machine-readable output."]], examples: ["blindfold compat"] },
160
+ { name: "update", group: "πŸ“¦ Enclave & admin", usage: "[--from <path>]", aliases: ["upgrade"], summary: "Update the global install (from npm, or a local repo with --from).", flags: [["--from <path>", "Build + install from a local repo checkout instead of npm."]], examples: ["blindfold update"] },
161
+
162
+ // ── Account ──────────────────────────────────────────────────────────────
163
+ {
164
+ name: "login", group: "πŸ‘€ Account", usage: "[--did <did>] [--key <0x…>] [--env <net>] [--file]",
165
+ summary: "Store EXISTING Terminal 3 credentials (tenant key β†’ OS keychain).",
166
+ flags: [
167
+ ["--did <did>", "Tenant DID (prompts if omitted)."],
168
+ ["--key <0x…>", "Tenant key (prompts hidden if omitted)."],
169
+ ["--env <net>", "testnet (default) or production."],
170
+ ["--file", "Force a 0600 config file instead of the OS keychain."],
171
+ ],
172
+ examples: ["blindfold login", "blindfold login --did did:t3n:… --env testnet"],
173
+ },
174
+ { name: "logout", group: "πŸ‘€ Account", usage: "", summary: "Remove stored credentials (keychain entry + config file).", examples: ["blindfold logout"] },
175
+ { name: "whoami", group: "πŸ‘€ Account", usage: "", summary: "Show tenant, env, and key source β€” never the value.", examples: ["blindfold whoami"] },
176
+
177
+ // ── Agent skill ──────────────────────────────────────────────────────────
178
+ {
179
+ name: "skill", group: "πŸ€– Agent skill", usage: "install [--global|--cursor|--opencode|--cline|--all] | uninstall",
180
+ summary: "Install the Blindfold agent skill so your coding agent handles secrets safely.",
181
+ flags: [
182
+ ["--global", "Install for every Claude Code session on this machine."],
183
+ ["--cursor / --opencode / --cline", "Install for that agent instead of Claude Code."],
184
+ ["--all", "Install everywhere at once."],
185
+ ],
186
+ examples: ["blindfold skill install", "blindfold skill install --all"],
187
+ },
188
+ ];
189
+
190
+ /** Look up a command by name or alias. */
191
+ export function findCommand(name: string): CmdDef | undefined {
192
+ return COMMANDS.find((c) => c.name === name || c.aliases?.includes(name));
193
+ }
194
+
195
+ /** The grouped overview: `blindfold help`. */
196
+ export function renderMainHelp(): string {
197
+ const w = termWidth();
198
+ const out: string[] = [
199
+ "",
200
+ bannerBox("πŸ›‘οΈ Blindfold", "Protect your AI agent's API keys with Terminal 3 enclaves. The agent only ever holds a placeholder β€” the real key is substituted inside the TDX enclave."),
201
+ "",
202
+ ];
203
+
204
+ const nameW = Math.min(12, COMMANDS.reduce((m, cmd) => Math.max(m, vlen(cmd.name)), 0));
205
+ const descW = Math.max(16, w - 4 - nameW - 2);
206
+
207
+ for (const group of GROUP_ORDER) {
208
+ const cmds = COMMANDS.filter((cmd) => cmd.group === group);
209
+ const lines: string[] = [];
210
+ for (const cmd of cmds) {
211
+ const dl = wrapText(cmd.summary, descW);
212
+ lines.push(pad(c.cyan(cmd.name), nameW) + " " + (dl[0] ?? ""));
213
+ for (let i = 1; i < dl.length; i++) lines.push(pad("", nameW) + " " + c.gray(dl[i] ?? ""));
214
+ if (cmd.usage) {
215
+ lines.push(pad("", nameW) + " " + c.gray("↳ blindfold " + cmd.name + " " + cmd.usage));
216
+ }
217
+ }
218
+ out.push(boxLines(group, lines));
219
+ out.push("");
220
+ }
221
+
222
+ out.push(
223
+ c.bold("Quick start"),
224
+ ` ${c.cyan("blindfold signup --email you@x.com")} ${c.gray("# create a funded testnet tenant")}`,
225
+ ` ${c.cyan("blindfold register --name openai_api_key")}`,
226
+ ` ${c.cyan("blindfold proxy")} ${c.gray("# point your agent at http://127.0.0.1:8787")}`,
227
+ "",
228
+ `${c.gray("Details for any command:")} ${c.cyan("blindfold <command> --help")} ${c.gray("Β·")} ${c.gray("Docs:")} ${c.cyan("npmjs.com/package/@fiscalmindset/blindfold")}`,
229
+ "",
230
+ );
231
+ return out.join("\n");
232
+ }
233
+
234
+ /** Detailed help for one command: `blindfold <cmd> --help`. */
235
+ export function renderCommandHelp(name: string): string {
236
+ const cmd = findCommand(name);
237
+ if (!cmd) return renderMainHelp();
238
+ const w = termWidth();
239
+ const out: string[] = ["", boxLines(`blindfold ${cmd.name}`, wrapText(cmd.summary, w - 4)), ""];
240
+
241
+ out.push(rule("Usage"));
242
+ out.push(" " + c.cyan(`blindfold ${cmd.name}${cmd.usage ? " " + cmd.usage : ""}`));
243
+ if (cmd.aliases?.length) out.push(" " + c.gray(`alias: ${cmd.aliases.map((a) => "blindfold " + a).join(", ")}`));
244
+ out.push("");
245
+
246
+ if (cmd.flags?.length) {
247
+ out.push(rule("Flags"));
248
+ const flagW = Math.min(24, cmd.flags.reduce((m, [f]) => Math.max(m, vlen(f)), 0));
249
+ const dW = Math.max(16, w - 2 - flagW - 2);
250
+ for (const [flag, desc] of cmd.flags) {
251
+ const dl = wrapText(desc, dW);
252
+ out.push(" " + pad(c.yellow(flag), flagW) + " " + (dl[0] ?? ""));
253
+ for (let i = 1; i < dl.length; i++) out.push(" " + pad("", flagW) + " " + c.gray(dl[i] ?? ""));
254
+ }
255
+ out.push("");
256
+ }
257
+
258
+ if (cmd.examples?.length) {
259
+ out.push(rule("Examples"));
260
+ for (const ex of cmd.examples) out.push(" " + c.cyan(ex));
261
+ out.push("");
262
+ }
263
+
264
+ if (cmd.notes) {
265
+ out.push(rule("Notes"));
266
+ for (const nl of wrapText(cmd.notes, w - 2)) out.push(" " + c.gray(nl));
267
+ out.push("");
268
+ }
269
+
270
+ out.push(c.gray("See all commands: ") + c.cyan("blindfold help"));
271
+ return out.join("\n");
272
+ }
package/src/tui.ts CHANGED
@@ -45,9 +45,11 @@ export function vlen(s: string): number {
45
45
  }
46
46
  return w;
47
47
  }
48
- function padEnd(s: string, w: number): string {
48
+ /** Pad `s` with spaces to display width `w` (ANSI/emoji aware). */
49
+ export function pad(s: string, w: number): string {
49
50
  return s + " ".repeat(Math.max(0, w - vlen(s)));
50
51
  }
52
+ const padEnd = pad;
51
53
 
52
54
  /** Word-wrap `text` to `width` columns (ANSI-aware). */
53
55
  export function wrapText(text: string, width: number): string[] {
@@ -113,6 +115,31 @@ export function commandBox(title: string, rows: Array<[string, string]>): string
113
115
  return out.join("\n");
114
116
  }
115
117
 
118
+ /**
119
+ * Wrap pre-rendered content lines in a titled rounded box, padding each line to
120
+ * the inner width so the right border stays aligned. `title` is embedded in the
121
+ * top border. Content lines may already contain ANSI color.
122
+ */
123
+ export function boxLines(title: string, lines: string[]): string {
124
+ const w = termWidth();
125
+ const inner = w - 4;
126
+ const prefix = B.tl + B.h + " ";
127
+ const fill = Math.max(0, w - vlen(prefix) - vlen(title) - 2);
128
+ const out: string[] = [dim(prefix) + c.bold(title) + dim(" " + B.h.repeat(fill) + B.tr)];
129
+ for (const l of lines) out.push(dim(B.v) + " " + pad(l, inner) + " " + dim(B.v));
130
+ out.push(dim(B.bl + B.h.repeat(w - 2) + B.br));
131
+ return out.join("\n");
132
+ }
133
+
134
+ /** A plain full-width rule with an optional bold label: "── Label ─────". */
135
+ export function rule(label = ""): string {
136
+ const w = termWidth();
137
+ if (!label) return dim(B.h.repeat(w));
138
+ const left = B.h + B.h + " ";
139
+ const fill = Math.max(0, w - vlen(left) - vlen(label) - 1);
140
+ return dim(left) + c.bold(label) + dim(" " + B.h.repeat(fill));
141
+ }
142
+
116
143
  /** Nearest command by edit distance, for "did you mean" suggestions. */
117
144
  export function nearest(input: string, candidates: string[]): string | null {
118
145
  let best: string | null = null;