@fiscalmindset/blindfold 0.4.0 → 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/assets/SKILL.md CHANGED
@@ -14,11 +14,12 @@ You're working in (or with) Blindfold — a Terminal 3 TDX-enclave wrapper that
14
14
  - User asks about `.env`, sealing, vaulting, or "where do I put my key".
15
15
  - You're about to write code that would read `process.env.SOME_PROVIDER_KEY` for an outbound call.
16
16
  - User says "use Blindfold" or "seal it" or "make this safe".
17
+ - User has no Terminal 3 tenant yet ("how do I start", "I don't have an account/DID", `doctor` shows creds MISSING) → point them at self-serve `blindfold signup` (see below).
17
18
 
18
19
  ## The four rules (mirror of `usage_by_claude.md` §3)
19
20
 
20
- 1. **R1 — no-paste-into-chat.** If a new secret needs sealing, *propose* the command (`npm run blindfold -- register --name <KV_KEY>`) for the user to run in their own terminal. Do NOT ask them to paste the value into chat. Only use `printf 'VALUE' | ...` from chat as a fallback, and only after the user explicitly says "go ahead from here".
21
- 2. **R2 — verify by fingerprint, never by value.** To check what's sealed: `npm run blindfold -- sealed`. To check what's in `.env`: `npm run env:fingerprint`. To check a specific sealed key matches an expected value: `npx tsx scripts/test-v5-release.ts <secret_name>` (prints `first3…last2 (N bytes)`, never plaintext). Ask the user to paste the *output* of those commands — that's safe.
21
+ 1. **R1 — no-paste-into-chat.** If a new secret needs sealing, *propose* the command (`blindfold register --name <KV_KEY>`) for the user to run in their own terminal. Do NOT ask them to paste the value into chat. Only use `printf 'VALUE' | ...` from chat as a fallback, and only after the user explicitly says "go ahead from here".
22
+ 2. **R2 — verify by fingerprint, never by value.** To check what's sealed: `blindfold sealed`. To check what's in `.env`: `npm run env:fingerprint`. To check a specific sealed key matches an expected value: `npx tsx scripts/test-v5-release.ts <secret_name>` (prints `first3…last2 (N bytes)`, never plaintext). Ask the user to paste the *output* of those commands — that's safe.
22
23
  3. **R3 — code uses release-broker pattern.** Any code you write that needs a provider key must fetch it from T3 just-in-time via `tenant.contracts.execute("blindfold-proxy", { version: CONTRACT_VERSION, functionName: "release-to-tenant", input: { secret_key: "<name>" } })`, use it inside a `try { … } finally { /* dropped */ }`, and never reference `process.env.<provider>_API_KEY`. Reference templates: `examples/grok-via-blindfold.ts` (HTTPS) and `scripts/smtp-with-blindfold.ts` (non-HTTP).
23
24
  4. **R4 — propose `.env` cleanup after every successful seal.** The sealed copy is canonical; the `.env` copy is leak surface. *Exception:* `T3N_API_KEY` itself stays in `.env` — it's the root credential, can't be sealed (chicken-and-egg).
24
25
 
@@ -26,19 +27,22 @@ You're working in (or with) Blindfold — a Terminal 3 TDX-enclave wrapper that
26
27
 
27
28
  | Command | Purpose | Safe to paste output? |
28
29
  |---|---|---|
29
- | `npm run blindfold -- doctor` | mode + cred presence (yes/no) | |
30
- | `npm run blindfold -- verify` | T3 round-trip status | ✅ |
31
- | `npm run blindfold -- login` | store tenant creds in `~/.blindfold` (OS keychain for the key) — works from any dir | tell user to run it (prompts for key) |
32
- | `npm run blindfold -- whoami` | show config path, tenant, env, key source (never the value) | ✅ |
33
- | `npm run blindfold -- logout` | remove stored creds (keychain + `~/.blindfold/config.json`) | ✅ |
34
- | `npm run blindfold -- sealed` | sealed-keys ledger (metadata only, LOCAL) | |
35
- | `npm run blindfold -- audit` | reconcile ledger against the ENCLAVE what's actually usable now | ✅ |
36
- | `npm run blindfold -- status` | one-glance: mode, tenant health, sealed list | ✅ |
30
+ | `blindfold signup --email <addr>` | self-serve: mint a new testnet tenant (key local, email-verified) | tell user to run it (prompts for the emailed code) |
31
+ | `blindfold doctor` | mode + cred presence (yes/no) | ✅ |
32
+ | `blindfold verify` | T3 round-trip status | |
33
+ | `blindfold credit` | tenant token/credit balance | ✅ |
34
+ | `blindfold attest [--pin]` | verify enclave TDX attestation; `--pin` gates seal/proxy on the code measurement | ✅ |
35
+ | `blindfold login` | store tenant creds in `~/.blindfold` (OS keychain for the key) — works from any dir | tell user to run it (prompts for key) |
36
+ | `blindfold whoami` | show config path, tenant, env, key source (never the value) | ✅ |
37
+ | `blindfold logout` | remove stored creds (keychain + `~/.blindfold/config.json`) | ✅ |
38
+ | `blindfold sealed` | sealed-keys ledger (metadata only, LOCAL) | ✅ |
39
+ | `blindfold audit` | reconcile ledger against the ENCLAVE — what's actually usable now | ✅ |
40
+ | `blindfold status` | one-glance: mode, tenant health, sealed list | ✅ |
37
41
  | `npm run env:fingerprint` | `.env` lines as `KEY = first3…last2 (N bytes)` | ✅ |
38
42
  | `npx tsx scripts/test-v5-release.ts <name>` | fingerprint of the released value | ✅ |
39
43
  | `npm run dashboard` | live HTML dashboard at `http://127.0.0.1:8799` | n/a (UI) |
40
- | `npm run blindfold -- register --name <K>` | interactive seal (no echo) | ⚠ tell user to run in their terminal |
41
- | `printf 'V' \| npm run blindfold -- register --name <K>` | piped seal (value briefly in this process) | ⚠ only if user already pasted value |
44
+ | `blindfold register --name <K>` | interactive seal (no echo) | ⚠ tell user to run in their terminal |
45
+ | `printf 'V' \| blindfold register --name <K>` | piped seal (value briefly in this process) | ⚠ only if user already pasted value |
42
46
 
43
47
  ## Seal once, use forever (no re-seal per session)
44
48
 
@@ -51,16 +55,21 @@ To **use** already-sealed keys, a session needs three persistent things (all
51
55
  survive across sessions — none require re-sealing):
52
56
 
53
57
  1. **Tenant creds** — `T3N_API_KEY` + `DID`. These authenticate to the enclave
54
- and are what release/proxy require. Two ways to provide them:
58
+ and are what release/proxy require. Ways to provide them:
59
+ - **`blindfold signup --email you@x.com`** (self-serve, the fastest start):
60
+ mints a brand-new Terminal 3 **testnet** tenant with no manual token claim —
61
+ generates the tenant key locally (→ OS keychain, never printed), verifies the
62
+ email by an emailed code, self-admits, and mints welcome credits. One email
63
+ binds to one tenant. Use this when the user has no tenant yet.
55
64
  - **Repo `.env`** (dev). Exception to R4: they stay in `.env`; see R4.
56
- - **`blindfold login`** (v0.2+, product path): stores DID + settings in
65
+ - **`blindfold login`** (v0.2+, product path, for an EXISTING tenant): stores DID + settings in
57
66
  `~/.blindfold/config.json` and the tenant key in the **OS keychain** (v0.3;
58
67
  macOS Keychain / Linux secret-tool) — so the CLI works from any directory
59
68
  and the key isn't a readable file. Precedence: `process.env` > repo `.env` >
60
69
  `~/.blindfold`. State (ledger/usage/egress) also lives in `~/.blindfold`.
61
70
  2. **Egress already granted** for the host you'll call (`blindfold grant --host
62
71
  <host>`). The grant is per-tenant and persistent.
63
- 3. **The proxy running** if using the HTTP path: `npm run blindfold -- proxy`
72
+ 3. **The proxy running** if using the HTTP path: `blindfold proxy`
64
73
  (listens on `127.0.0.1:8787`). Point the tool at it with base URL
65
74
  `http://127.0.0.1:8787/v1` and key `__BLINDFOLD__`. (Code paths use the
66
75
  release-broker instead — see R3.) On a shared machine, run `proxy --auth`:
@@ -76,6 +85,24 @@ re-sealing.
76
85
 
77
86
  ## What to do, by scenario
78
87
 
88
+ ### "I'm new / I don't have a Terminal 3 tenant yet" (onboarding)
89
+
90
+ Blindfold is published on npm and onboarding is self-serve — no manual token claim.
91
+ Propose (the user runs it in their own terminal):
92
+
93
+ ```bash
94
+ npm i -g @fiscalmindset/blindfold
95
+ blindfold signup --email you@example.com
96
+ ```
97
+
98
+ `signup` generates the tenant key locally (→ OS keychain, never printed),
99
+ verifies the email by an emailed code, self-admits a funded **testnet** tenant,
100
+ and mints welcome credits. Then confirm with `blindfold doctor` (should show the
101
+ tenant `active`) and `blindfold credit` (token balance). After that they can
102
+ `register` and `use` keys. Notes: testnet-only; one email = one tenant (Gmail
103
+ `+aliases` give fresh identities); if the email is already registered, use
104
+ `blindfold login` with that tenant's key instead.
105
+
79
106
  ### "I want to seal my Stripe key" (or any new credential)
80
107
 
81
108
  **Don't ask for the value.** Respond with:
@@ -83,10 +110,10 @@ re-sealing.
83
110
  > In your own terminal, run:
84
111
  >
85
112
  > ```bash
86
- > npm run blindfold -- register --name stripe_api_key
113
+ > blindfold register --name stripe_api_key
87
114
  > ```
88
115
  >
89
- > It'll prompt for the value with input hidden (no echo, no shell history). Paste your `sk_live_…` there, press Enter. Once done, paste me the output of `npm run blindfold -- sealed` so I can verify it landed in the right place.
116
+ > It'll prompt for the value with input hidden (no echo, no shell history). Paste your `sk_live_…` there, press Enter. Once done, paste me the output of `blindfold sealed` so I can verify it landed in the right place.
90
117
 
91
118
  ### "Seal / use a webhook URL (Discord, Slack, …)"
92
119
 
@@ -126,7 +153,7 @@ end**; only the final `register` (plaintext) is handed back to the user.
126
153
  ```
127
154
  2. Rebuild (`npm run build` in `packages/blindfold`) so `dist/` picks up the entry.
128
155
  3. Hand back the two one-time human steps: seal the key —
129
- `npm run blindfold -- register --name notion_api_key` (in their terminal, R1) —
156
+ `blindfold register --name notion_api_key` (in their terminal, R1) —
130
157
  and grant egress: `blindfold grant --host api.notion.com`.
131
158
  4. The agent then calls `http://127.0.0.1:8787/notion/...` with
132
159
  `Authorization: Bearer __BLINDFOLD__`; the enclave swaps in the sealed key.
@@ -139,7 +166,7 @@ auth scheme none of the four cover — otherwise this stays a TypeScript-only ch
139
166
  It's in our chat context now (can't undo). Reduce future surface:
140
167
 
141
168
  ```bash
142
- printf 'sk-...' | npx tsx packages/blindfold/bin/blindfold.ts register --name openai_api_key
169
+ printf 'sk-...' | blindfold register --name openai_api_key
143
170
  ```
144
171
 
145
172
  Then propose deleting any `.env` copy. Note in your response that the chat-context exposure already happened and you can't retroactively fix it — only forward-protect.
@@ -174,9 +201,9 @@ Ask the user to paste that output. You get key *names* + lengths + first/last fe
174
201
  Run these in order, asking the user to paste each output:
175
202
 
176
203
  ```bash
177
- npm run blindfold -- doctor # config sanity
178
- npm run blindfold -- verify # T3 reachability
179
- npm run blindfold -- sealed # is the key there?
204
+ blindfold doctor # config sanity
205
+ blindfold verify # T3 reachability
206
+ blindfold sealed # is the key there?
180
207
  ```
181
208
 
182
209
  Cross-reference any errors with `vicky.md` Q6 (keyword-indexed error table).
package/bin/blindfold.ts CHANGED
@@ -4,7 +4,9 @@
4
4
  * helpers in ./cli-shared.ts. Every action prints what it did, never a secret.
5
5
  */
6
6
  import { type Argv, parseArgv } from "./cli-shared.ts";
7
- import { c, head } from "../src/color.ts";
7
+ import { c, bad } from "../src/color.ts";
8
+ import { nearest } from "../src/tui.ts";
9
+ import { renderMainHelp, renderCommandHelp, findCommand } from "../src/help.ts";
8
10
  import { handleAuth } from "./cmd-auth.ts";
9
11
  import { handleSecrets } from "./cmd-secrets.ts";
10
12
  import { handleLifecycle } from "./cmd-lifecycle.ts";
@@ -34,54 +36,35 @@ async function main(): Promise<void> {
34
36
  const cmdArgs = ddIdx >= 0 ? raw.slice(ddIdx + 1) : [];
35
37
  const argv = parseArgv(ddIdx >= 0 ? raw.slice(0, ddIdx) : raw);
36
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
+
37
52
  const handler = ROUTES[cmd];
38
53
  if (handler) await handler(cmd, argv, cmdArgs);
39
- else printHelp();
54
+ else printUnknown(cmd);
40
55
  }
41
56
 
42
- function printHelp(): void {
43
- console.log(`${head("🛡️ Blindfold")} ${c.gray("— protect your AI agent's API keys with Terminal 3 enclaves.")}
44
-
45
- ${c.bold("Commands:")}
46
- signup [--email <you@x.com>] Self-serve: create a Terminal 3 testnet tenant from scratch. Generates a key locally, verifies your email by code, and mints welcome credits — no manual provisioning. Then run doctor + register.
47
- init [--seed KV:ENV]... [--start] One-command zero-knowledge setup. Walks through .env, build, auth, publish, seed; can auto-launch the proxy.
48
- verify Handshake + auth against T3 (smoke test).
49
- compat [--json] Scan this machine for AI agent tools/SDKs and print the exact env-var swap for each.
50
- register --name <KV_KEY> [--from-env <ENV_VAR>] Seal a secret into the enclave (one-time). With --from-env: reads process.env. Without: prompts the terminal with no echo (preferred — never touches disk/history). Also accepts piped stdin.
51
- use --name <secret> [--as <ENV>] -- <cmd> USE a sealed secret: release it and run <cmd> with it injected as $ENV for that command only — never back in your env. --as is auto-detected for known tools (gh→GH_TOKEN, psql→PGPASSWORD, …). Or --url <https> for a quick auth check.
52
- rotate --name <secret> [--from-env <ENV_VAR>] Replace a sealed secret's value (snapshots the old value for rollback; shows before/after fingerprints, never the value).
53
- export --name <secret> [--as <ENV_VAR>] CI-only: release a sealed secret into $GITHUB_ENV for later steps (masked in logs). Used by the Blindfold GitHub Action.
54
- rollback --name <secret> [--to <fp|iso-ts>] Restore a previous value snapshotted by rotate (most recent by default).
55
- versions [--name <secret>] List the snapshots available to roll back to (metadata only).
56
- migrate [--dry-run] [--keep] Seal EVERY secret in your .env in one shot, then remove the plaintext lines (backup kept). --dry-run previews; --keep comments lines instead of deleting. Skips T3 creds + config.
57
- status One-glance overview: mode, tenant health, and the list of sealed secrets.
58
- sealed List sealed keys — metadata only (name, byte-length, when, where). Never the value.
59
- audit Verify the ledger's tamper-evident hash-chain AND reconcile it against the enclave (the source of truth) — flags drift/missing/tampering.
60
- proxy [--port 8787] [--auth] [--socket [path]] Run the local OpenAI-shaped proxy. --auth mints a per-session token; --socket binds a 0600 unix socket (only your OS user can connect).
61
- attest [--expect-rtmr3 <b64>] [--pin] [--json] Verify the enclave's TDX attestation (chains to Intel's root CA). --pin records the RTMR3 so seal/proxy auto-verify it first.
62
- publish [--wasm path/to/blindfold_proxy.wasm] Publish the Rust→WASM contract (one-time).
63
- grant --host <host>[,<host2>...] Authorize the contract to call these hosts (required before the proxy / in-enclave path can reach them). E.g. --host api.openai.com
64
- share --to <agent-did> --host <host>[,...] Let a teammate's agent USE your sealed keys for those hosts via the enclave — they never receive the plaintext (forward only, least privilege).
65
- revoke --to <agent-did> Remove a teammate's access. Immediate and complete — nobody holds a raw key copy.
66
-
67
- skill install [--global|--cursor|--opencode|--cline|--all] Install the Blindfold agent skill so your coding agent handles secrets safely. Default: this project.
68
- skill uninstall Remove all installed skill files.
69
-
70
- dashboard [--port 8799] Live HTML dashboard of proxy usage.
71
- stats CLI summary of proxy usage.
72
- stats:clear Wipe the usage log.
73
- doctor Show current mode + config.
74
- credit [--json] Show the tenant's Terminal 3 token/credit balance (no credit cost).
75
- update [--from <path>] Update the global blindfold (from npm, or a local repo with --from).
76
-
77
- The friendliest path is just: blindfold init
78
-
79
- Quick start:
80
- 1) ./scripts/build-contract.sh # build the Rust contract (REAL mode only)
81
- 2) blindfold publish # register the contract on T3
82
- 3) blindfold register --name openai_api_key --from-env OPENAI_API_KEY
83
- 4) blindfold proxy # then point your agent at it
84
- `);
57
+ /** Unknown command: a concise error with a "did you mean" suggestion — not the
58
+ * full help dump (which used to appear on any typo). */
59
+ function printUnknown(cmd: string): void {
60
+ const all = [...Object.keys(ROUTES), "help"];
61
+ const guess = nearest(cmd, all);
62
+ console.error(
63
+ bad(`✖ Unknown command: ${cmd}`) +
64
+ (guess ? ` ${c.gray("— did you mean")} ${c.cyan(guess)}${c.gray("?")}` : ""),
65
+ );
66
+ console.error(c.gray(" Run ") + c.cyan("blindfold help") + c.gray(" to see all commands."));
67
+ process.exit(1);
85
68
  }
86
69
 
87
70
  main().catch((e) => {
package/dist/cli.mjs CHANGED
@@ -1374,6 +1374,364 @@ function die(msg) {
1374
1374
  process.exit(2);
1375
1375
  }
1376
1376
 
1377
+ // src/tui.ts
1378
+ function termWidth() {
1379
+ const cols = process.stdout.columns;
1380
+ if (typeof cols === "number" && cols >= 40) return Math.min(cols, 100);
1381
+ const env = Number(process.env.COLUMNS);
1382
+ if (Number.isFinite(env) && env >= 40) return Math.min(env, 100);
1383
+ return 80;
1384
+ }
1385
+ var ANSI = /\x1b\[[0-9;]*m/g;
1386
+ function isWide(cp) {
1387
+ return cp >= 4352 && cp <= 4447 || cp >= 9728 && cp <= 10175 || cp >= 11008 && cp <= 11263 || cp >= 11904 && cp <= 42191 || cp >= 44032 && cp <= 55203 || cp >= 63744 && cp <= 64255 || cp >= 65072 && cp <= 65103 || cp >= 65280 && cp <= 65376 || cp >= 65504 && cp <= 65510 || cp >= 126976 && cp <= 129791;
1388
+ }
1389
+ function vlen(s) {
1390
+ let w = 0;
1391
+ for (const ch of s.replace(ANSI, "")) {
1392
+ const cp = ch.codePointAt(0) ?? 0;
1393
+ if (cp === 8205 || cp >= 65024 && cp <= 65039 || cp >= 768 && cp <= 879) continue;
1394
+ w += isWide(cp) ? 2 : 1;
1395
+ }
1396
+ return w;
1397
+ }
1398
+ function pad(s, w) {
1399
+ return s + " ".repeat(Math.max(0, w - vlen(s)));
1400
+ }
1401
+ var padEnd = pad;
1402
+ function wrapText(text, width) {
1403
+ if (width <= 4) return [text];
1404
+ const out = [];
1405
+ for (const para of text.split("\n")) {
1406
+ const words = para.split(/\s+/).filter(Boolean);
1407
+ let cur = "";
1408
+ for (const w of words) {
1409
+ if (cur === "") cur = w;
1410
+ else if (vlen(cur) + 1 + vlen(w) <= width) cur += " " + w;
1411
+ else {
1412
+ out.push(cur);
1413
+ cur = w;
1414
+ }
1415
+ }
1416
+ out.push(cur);
1417
+ }
1418
+ return out.length ? out : [""];
1419
+ }
1420
+ var B = { tl: "\u256D", tr: "\u256E", bl: "\u2570", br: "\u256F", h: "\u2500", v: "\u2502" };
1421
+ var dim = (s) => c.gray(s);
1422
+ function bannerBox(title, subtitle) {
1423
+ const w = termWidth();
1424
+ const inner = w - 4;
1425
+ const top = dim(B.tl + B.h.repeat(w - 2) + B.tr);
1426
+ const bot = dim(B.bl + B.h.repeat(w - 2) + B.br);
1427
+ const row = (s) => dim(B.v) + " " + padEnd(s, inner) + " " + dim(B.v);
1428
+ const lines = [top, row(c.bold(title))];
1429
+ for (const sl of wrapText(subtitle, inner)) lines.push(row(dim(sl)));
1430
+ lines.push(bot);
1431
+ return lines.join("\n");
1432
+ }
1433
+ function boxLines(title, lines) {
1434
+ const w = termWidth();
1435
+ const inner = w - 4;
1436
+ const prefix = B.tl + B.h + " ";
1437
+ const fill = Math.max(0, w - vlen(prefix) - vlen(title) - 2);
1438
+ const out = [dim(prefix) + c.bold(title) + dim(" " + B.h.repeat(fill) + B.tr)];
1439
+ for (const l of lines) out.push(dim(B.v) + " " + pad(l, inner) + " " + dim(B.v));
1440
+ out.push(dim(B.bl + B.h.repeat(w - 2) + B.br));
1441
+ return out.join("\n");
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
+ }
1450
+ function nearest(input, candidates) {
1451
+ let best = null;
1452
+ let bestD = Infinity;
1453
+ for (const cand of candidates) {
1454
+ const d = editDistance(input, cand);
1455
+ if (d < bestD) {
1456
+ bestD = d;
1457
+ best = cand;
1458
+ }
1459
+ }
1460
+ return bestD <= Math.max(2, Math.floor(input.length / 3)) ? best : null;
1461
+ }
1462
+ function editDistance(a, b) {
1463
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
1464
+ for (let j = 0; j <= b.length; j++) dp[0][j] = j;
1465
+ for (let i = 1; i <= a.length; i++) {
1466
+ for (let j = 1; j <= b.length; j++) {
1467
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1468
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
1469
+ }
1470
+ }
1471
+ return dp[a.length][b.length];
1472
+ }
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
+
1377
1735
  // bin/cmd-auth.ts
1378
1736
  init_env();
1379
1737
  init_keychain();
@@ -3368,11 +3726,11 @@ var bold = (s) => colour("1", s);
3368
3726
  var green = (s) => colour("32", s);
3369
3727
  var yellow = (s) => colour("33", s);
3370
3728
  var red = (s) => colour("31", s);
3371
- var dim = (s) => colour("2", s);
3729
+ var dim2 = (s) => colour("2", s);
3372
3730
  var cyan = (s) => colour("36", s);
3373
3731
  function header(n, total, title) {
3374
3732
  process.stdout.write(`
3375
- ${dim(`[${n}/${total}]`)} ${bold(title)}
3733
+ ${dim2(`[${n}/${total}]`)} ${bold(title)}
3376
3734
  `);
3377
3735
  }
3378
3736
  function ok2(line) {
@@ -3380,7 +3738,7 @@ function ok2(line) {
3380
3738
  `);
3381
3739
  }
3382
3740
  function info(line) {
3383
- process.stdout.write(` ${dim("\xB7")} ${line}
3741
+ process.stdout.write(` ${dim2("\xB7")} ${line}
3384
3742
  `);
3385
3743
  }
3386
3744
  function warn2(line) {
@@ -3391,7 +3749,7 @@ function fail(line, fixHint) {
3391
3749
  process.stdout.write(` ${red("\u2716")} ${line}
3392
3750
  `);
3393
3751
  if (fixHint) {
3394
- for (const ln of fixHint.split("\n")) process.stdout.write(` ${dim("\u2192")} ${cyan(ln)}
3752
+ for (const ln of fixHint.split("\n")) process.stdout.write(` ${dim2("\u2192")} ${cyan(ln)}
3395
3753
  `);
3396
3754
  }
3397
3755
  }
@@ -3422,7 +3780,7 @@ async function runInit(opts = {}) {
3422
3780
  const total = 5;
3423
3781
  process.stdout.write(`
3424
3782
  ${bold("\u{1F6E1}\uFE0F Blindfold \u2014 first-time setup")}
3425
- ${dim("This wizard sets up REAL T3 mode end-to-end. Secrets are read from your .env, never typed onto the command line.")}
3783
+ ${dim2("This wizard sets up REAL T3 mode end-to-end. Secrets are read from your .env, never typed onto the command line.")}
3426
3784
  `);
3427
3785
  header(1, total, "Preflight");
3428
3786
  await ensureEnvOrPrompt(opts);
@@ -3432,7 +3790,7 @@ ${dim("This wizard sets up REAL T3 mode end-to-end. Secrets are read from your .
3432
3790
  fail("Still missing T3N_API_KEY and/or DID after .env walkthrough.", "Open .env, paste the two values, and re-run `npm run setup`.");
3433
3791
  throw new Error("preflight failed");
3434
3792
  }
3435
- ok2(`T3 ${env.t3Env} \xB7 tenant ${dim(env.did)}`);
3793
+ ok2(`T3 ${env.t3Env} \xB7 tenant ${dim2(env.did)}`);
3436
3794
  const haveSdk = await isSdkInstalled2();
3437
3795
  if (!haveSdk) {
3438
3796
  fail("@terminal3/t3n-sdk is not installed.", "Run: npm install @terminal3/t3n-sdk");
@@ -3544,7 +3902,7 @@ Make sure ${fromEnv} is set in .env (you can delete it after sealing).`);
3544
3902
  ${green(bold("\u2713 All done."))}
3545
3903
  `);
3546
3904
  if (opts.start) {
3547
- process.stdout.write(`${dim("Starting the proxy now (Ctrl+C to stop) \u2026")}
3905
+ process.stdout.write(`${dim2("Starting the proxy now (Ctrl+C to stop) \u2026")}
3548
3906
  `);
3549
3907
  const proxy = spawn2(process.execPath, [process.argv[1] ?? "", "proxy"], {
3550
3908
  cwd: REPO_ROOT2,
@@ -3553,15 +3911,15 @@ ${green(bold("\u2713 All done."))}
3553
3911
  proxy.on("exit", (code) => process.exit(code ?? 0));
3554
3912
  return;
3555
3913
  }
3556
- process.stdout.write(`${dim("Next:")}
3914
+ process.stdout.write(`${dim2("Next:")}
3557
3915
  `);
3558
- process.stdout.write(` ${cyan("npm run blindfold -- proxy")} ${dim("# leave this running")}
3916
+ process.stdout.write(` ${cyan("npm run blindfold -- proxy")} ${dim2("# leave this running")}
3559
3917
  `);
3560
- process.stdout.write(` ${cyan("npm run dashboard")} ${dim("# open http://127.0.0.1:8799")}
3918
+ process.stdout.write(` ${cyan("npm run dashboard")} ${dim2("# open http://127.0.0.1:8799")}
3561
3919
  `);
3562
- process.stdout.write(` ${dim("Then point your agent at:")} ${cyan("OPENAI_BASE_URL=http://127.0.0.1:8787/v1 OPENAI_API_KEY=__BLINDFOLD__")}
3920
+ process.stdout.write(` ${dim2("Then point your agent at:")} ${cyan("OPENAI_BASE_URL=http://127.0.0.1:8787/v1 OPENAI_API_KEY=__BLINDFOLD__")}
3563
3921
  `);
3564
- process.stdout.write(`${dim("Or re-run with")} ${cyan("--start")} ${dim("to launch the proxy automatically.")}
3922
+ process.stdout.write(`${dim2("Or re-run with")} ${cyan("--start")} ${dim2("to launch the proxy automatically.")}
3565
3923
  `);
3566
3924
  }
3567
3925
  async function ensureEnvOrPrompt(opts) {
@@ -3673,7 +4031,7 @@ ${bold("\u{1F6E1}\uFE0F Blindfold \u2014 verify")}
3673
4031
  warn2("MOCK mode \u2014 there's nothing to verify on the T3 side. Set T3N_API_KEY + DID.");
3674
4032
  return;
3675
4033
  }
3676
- process.stdout.write(` ${dim("\xB7")} attempting handshake + authenticate against T3 \u2026
4034
+ process.stdout.write(` ${dim2("\xB7")} attempting handshake + authenticate against T3 \u2026
3677
4035
  `);
3678
4036
  try {
3679
4037
  const t3 = await openT3Client(env);
@@ -3701,7 +4059,7 @@ var HERE4 = path11.dirname(fileURLToPath5(import.meta.url));
3701
4059
  var REPO_ROOT3 = path11.resolve(HERE4, "..", "..", "..");
3702
4060
  var colour2 = process.stdout.isTTY ? (c2, s) => `\x1B[${c2}m${s}\x1B[0m` : (_, s) => s;
3703
4061
  var bold2 = (s) => colour2("1", s);
3704
- var dim2 = (s) => colour2("2", s);
4062
+ var dim3 = (s) => colour2("2", s);
3705
4063
  var green2 = (s) => colour2("32", s);
3706
4064
  var yellow2 = (s) => colour2("33", s);
3707
4065
  var red2 = (s) => colour2("31", s);
@@ -3830,14 +4188,14 @@ async function runCompat(opts = {}) {
3830
4188
  }
3831
4189
  process.stdout.write(`
3832
4190
  ${bold2("\u{1F6E1}\uFE0F Blindfold \u2014 compatibility scan")}
3833
- ${dim2("Probing local box for agent tools and SDKs Blindfold can protect.")}
4191
+ ${dim3("Probing local box for agent tools and SDKs Blindfold can protect.")}
3834
4192
 
3835
4193
  `);
3836
4194
  const detected = results.filter((r) => r.detection.detected);
3837
4195
  const notFound = results.filter((r) => !r.detection.detected);
3838
4196
  process.stdout.write(`${bold2(`Detected (${detected.length}):`)}
3839
4197
  `);
3840
- if (detected.length === 0) process.stdout.write(` ${dim2("(none \u2014 install one of the tools below, or just use the OpenAI/Anthropic SDK in your own code)")}
4198
+ if (detected.length === 0) process.stdout.write(` ${dim3("(none \u2014 install one of the tools below, or just use the OpenAI/Anthropic SDK in your own code)")}
3841
4199
  `);
3842
4200
  for (const r of detected) {
3843
4201
  renderTool(r.tool, r.detection, true);
@@ -3849,24 +4207,24 @@ ${bold2(`Not found on this machine (${notFound.length}):`)}
3849
4207
  renderTool(r.tool, r.detection, false);
3850
4208
  }
3851
4209
  process.stdout.write(`
3852
- ${dim2("For a longer-form compatibility writeup see docs/05-compatibility.md.")}
4210
+ ${dim3("For a longer-form compatibility writeup see docs/05-compatibility.md.")}
3853
4211
  `);
3854
4212
  }
3855
4213
  function renderTool(tool, detection, detected) {
3856
- const mark = !detected ? dim2("\xB7") : tool.applies === "applies" ? green2("\u2713") : tool.applies === "depends" ? yellow2("?") : tool.applies === "needs-base-url" ? yellow2("!") : red2("\u2716");
3857
- const status = !detected ? dim2("(not installed)") : tool.applies === "applies" ? green2("Blindfold protects this") : tool.applies === "depends" ? yellow2("Depends on how you authenticate") : tool.applies === "needs-base-url" ? yellow2("No base-URL hook \u2014 needs upstream support") : red2("Doesn't apply (no user-supplied key)");
3858
- process.stdout.write(` ${mark} ${bold2(tool.name)} ${dim2("\xB7")} ${status}
4214
+ const mark = !detected ? dim3("\xB7") : tool.applies === "applies" ? green2("\u2713") : tool.applies === "depends" ? yellow2("?") : tool.applies === "needs-base-url" ? yellow2("!") : red2("\u2716");
4215
+ const status = !detected ? dim3("(not installed)") : tool.applies === "applies" ? green2("Blindfold protects this") : tool.applies === "depends" ? yellow2("Depends on how you authenticate") : tool.applies === "needs-base-url" ? yellow2("No base-URL hook \u2014 needs upstream support") : red2("Doesn't apply (no user-supplied key)");
4216
+ process.stdout.write(` ${mark} ${bold2(tool.name)} ${dim3("\xB7")} ${status}
3859
4217
  `);
3860
- if (detected && detection.detail) process.stdout.write(` ${dim2("at " + detection.detail)}
4218
+ if (detected && detection.detail) process.stdout.write(` ${dim3("at " + detection.detail)}
3861
4219
  `);
3862
4220
  if (detected && tool.recipe.env) {
3863
4221
  const envLine = Object.entries(tool.recipe.env).map(([k, v]) => `${k}=${v}`).join(" ");
3864
4222
  process.stdout.write(` ${cyan2(envLine)}
3865
4223
  `);
3866
4224
  }
3867
- if (detected && tool.recipe.note) process.stdout.write(` ${dim2(tool.recipe.note)}
4225
+ if (detected && tool.recipe.note) process.stdout.write(` ${dim3(tool.recipe.note)}
3868
4226
  `);
3869
- if (detected && tool.explanation) process.stdout.write(` ${dim2(tool.explanation)}
4227
+ if (detected && tool.explanation) process.stdout.write(` ${dim3(tool.explanation)}
3870
4228
  `);
3871
4229
  }
3872
4230
 
@@ -4263,53 +4621,27 @@ async function main() {
4263
4621
  const cmdArgs = ddIdx >= 0 ? raw.slice(ddIdx + 1) : [];
4264
4622
  const argv = parseArgv(ddIdx >= 0 ? raw.slice(0, ddIdx) : raw);
4265
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
+ }
4266
4633
  const handler = ROUTES[cmd];
4267
4634
  if (handler) await handler(cmd, argv, cmdArgs);
4268
- else printHelp();
4635
+ else printUnknown(cmd);
4269
4636
  }
4270
- function printHelp() {
4271
- console.log(`${head("\u{1F6E1}\uFE0F Blindfold")} ${c.gray("\u2014 protect your AI agent's API keys with Terminal 3 enclaves.")}
4272
-
4273
- ${c.bold("Commands:")}
4274
- signup [--email <you@x.com>] Self-serve: create a Terminal 3 testnet tenant from scratch. Generates a key locally, verifies your email by code, and mints welcome credits \u2014 no manual provisioning. Then run doctor + register.
4275
- init [--seed KV:ENV]... [--start] One-command zero-knowledge setup. Walks through .env, build, auth, publish, seed; can auto-launch the proxy.
4276
- verify Handshake + auth against T3 (smoke test).
4277
- compat [--json] Scan this machine for AI agent tools/SDKs and print the exact env-var swap for each.
4278
- register --name <KV_KEY> [--from-env <ENV_VAR>] Seal a secret into the enclave (one-time). With --from-env: reads process.env. Without: prompts the terminal with no echo (preferred \u2014 never touches disk/history). Also accepts piped stdin.
4279
- use --name <secret> [--as <ENV>] -- <cmd> USE a sealed secret: release it and run <cmd> with it injected as $ENV for that command only \u2014 never back in your env. --as is auto-detected for known tools (gh\u2192GH_TOKEN, psql\u2192PGPASSWORD, \u2026). Or --url <https> for a quick auth check.
4280
- rotate --name <secret> [--from-env <ENV_VAR>] Replace a sealed secret's value (snapshots the old value for rollback; shows before/after fingerprints, never the value).
4281
- export --name <secret> [--as <ENV_VAR>] CI-only: release a sealed secret into $GITHUB_ENV for later steps (masked in logs). Used by the Blindfold GitHub Action.
4282
- rollback --name <secret> [--to <fp|iso-ts>] Restore a previous value snapshotted by rotate (most recent by default).
4283
- versions [--name <secret>] List the snapshots available to roll back to (metadata only).
4284
- migrate [--dry-run] [--keep] Seal EVERY secret in your .env in one shot, then remove the plaintext lines (backup kept). --dry-run previews; --keep comments lines instead of deleting. Skips T3 creds + config.
4285
- status One-glance overview: mode, tenant health, and the list of sealed secrets.
4286
- sealed List sealed keys \u2014 metadata only (name, byte-length, when, where). Never the value.
4287
- audit Verify the ledger's tamper-evident hash-chain AND reconcile it against the enclave (the source of truth) \u2014 flags drift/missing/tampering.
4288
- proxy [--port 8787] [--auth] [--socket [path]] Run the local OpenAI-shaped proxy. --auth mints a per-session token; --socket binds a 0600 unix socket (only your OS user can connect).
4289
- attest [--expect-rtmr3 <b64>] [--pin] [--json] Verify the enclave's TDX attestation (chains to Intel's root CA). --pin records the RTMR3 so seal/proxy auto-verify it first.
4290
- publish [--wasm path/to/blindfold_proxy.wasm] Publish the Rust\u2192WASM contract (one-time).
4291
- grant --host <host>[,<host2>...] Authorize the contract to call these hosts (required before the proxy / in-enclave path can reach them). E.g. --host api.openai.com
4292
- share --to <agent-did> --host <host>[,...] Let a teammate's agent USE your sealed keys for those hosts via the enclave \u2014 they never receive the plaintext (forward only, least privilege).
4293
- revoke --to <agent-did> Remove a teammate's access. Immediate and complete \u2014 nobody holds a raw key copy.
4294
-
4295
- skill install [--global|--cursor|--opencode|--cline|--all] Install the Blindfold agent skill so your coding agent handles secrets safely. Default: this project.
4296
- skill uninstall Remove all installed skill files.
4297
-
4298
- dashboard [--port 8799] Live HTML dashboard of proxy usage.
4299
- stats CLI summary of proxy usage.
4300
- stats:clear Wipe the usage log.
4301
- doctor Show current mode + config.
4302
- credit [--json] Show the tenant's Terminal 3 token/credit balance (no credit cost).
4303
- update [--from <path>] Update the global blindfold (from npm, or a local repo with --from).
4304
-
4305
- The friendliest path is just: blindfold init
4306
-
4307
- Quick start:
4308
- 1) ./scripts/build-contract.sh # build the Rust contract (REAL mode only)
4309
- 2) blindfold publish # register the contract on T3
4310
- 3) blindfold register --name openai_api_key --from-env OPENAI_API_KEY
4311
- 4) blindfold proxy # then point your agent at it
4312
- `);
4637
+ function printUnknown(cmd) {
4638
+ const all = [...Object.keys(ROUTES), "help"];
4639
+ const guess = nearest(cmd, all);
4640
+ console.error(
4641
+ bad(`\u2716 Unknown command: ${cmd}`) + (guess ? ` ${c.gray("\u2014 did you mean")} ${c.cyan(guess)}${c.gray("?")}` : "")
4642
+ );
4643
+ console.error(c.gray(" Run ") + c.cyan("blindfold help") + c.gray(" to see all commands."));
4644
+ process.exit(1);
4313
4645
  }
4314
4646
  main().catch((e) => {
4315
4647
  console.error("\u2716", e.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiscalmindset/blindfold",
3
- "version": "0.4.0",
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 ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Tiny, dependency-free terminal UI primitives: responsive width, ANSI-aware
3
+ * wrapping, rounded boxes, and a two-column command table. Used to render a
4
+ * structured `blindfold help` that reflows to the terminal.
5
+ */
6
+ import { c } from "./color.ts";
7
+
8
+ /** Terminal width, clamped to a readable range. */
9
+ export function termWidth(): number {
10
+ const cols = process.stdout.columns;
11
+ if (typeof cols === "number" && cols >= 40) return Math.min(cols, 100);
12
+ const env = Number(process.env.COLUMNS);
13
+ if (Number.isFinite(env) && env >= 40) return Math.min(env, 100);
14
+ return 80;
15
+ }
16
+
17
+ const ANSI = /\x1b\[[0-9;]*m/g;
18
+
19
+ /** True for code points most terminals render two columns wide (CJK, emoji). */
20
+ function isWide(cp: number): boolean {
21
+ return (
22
+ (cp >= 0x1100 && cp <= 0x115f) ||
23
+ (cp >= 0x2600 && cp <= 0x27bf) ||
24
+ (cp >= 0x2b00 && cp <= 0x2bff) ||
25
+ (cp >= 0x2e80 && cp <= 0xa4cf) ||
26
+ (cp >= 0xac00 && cp <= 0xd7a3) ||
27
+ (cp >= 0xf900 && cp <= 0xfaff) ||
28
+ (cp >= 0xfe30 && cp <= 0xfe4f) ||
29
+ (cp >= 0xff00 && cp <= 0xff60) ||
30
+ (cp >= 0xffe0 && cp <= 0xffe6) ||
31
+ (cp >= 0x1f000 && cp <= 0x1faff)
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Display width of a string (ANSI-stripped), accounting for double-width CJK/
37
+ * emoji and zero-width joiners/variation-selectors — so box borders line up.
38
+ */
39
+ export function vlen(s: string): number {
40
+ let w = 0;
41
+ for (const ch of s.replace(ANSI, "")) {
42
+ const cp = ch.codePointAt(0) ?? 0;
43
+ if (cp === 0x200d || (cp >= 0xfe00 && cp <= 0xfe0f) || (cp >= 0x300 && cp <= 0x36f)) continue; // zero-width
44
+ w += isWide(cp) ? 2 : 1;
45
+ }
46
+ return w;
47
+ }
48
+ /** Pad `s` with spaces to display width `w` (ANSI/emoji aware). */
49
+ export function pad(s: string, w: number): string {
50
+ return s + " ".repeat(Math.max(0, w - vlen(s)));
51
+ }
52
+ const padEnd = pad;
53
+
54
+ /** Word-wrap `text` to `width` columns (ANSI-aware). */
55
+ export function wrapText(text: string, width: number): string[] {
56
+ if (width <= 4) return [text];
57
+ const out: string[] = [];
58
+ for (const para of text.split("\n")) {
59
+ const words = para.split(/\s+/).filter(Boolean);
60
+ let cur = "";
61
+ for (const w of words) {
62
+ if (cur === "") cur = w;
63
+ else if (vlen(cur) + 1 + vlen(w) <= width) cur += " " + w;
64
+ else { out.push(cur); cur = w; }
65
+ }
66
+ out.push(cur);
67
+ }
68
+ return out.length ? out : [""];
69
+ }
70
+
71
+ // Rounded box-drawing set.
72
+ const B = { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" };
73
+ const dim = (s: string): string => c.gray(s);
74
+
75
+ /** A banner box with a bold title and a dim subtitle, sized to the terminal. */
76
+ export function bannerBox(title: string, subtitle: string): string {
77
+ const w = termWidth();
78
+ const inner = w - 4;
79
+ const top = dim(B.tl + B.h.repeat(w - 2) + B.tr);
80
+ const bot = dim(B.bl + B.h.repeat(w - 2) + B.br);
81
+ const row = (s: string): string => dim(B.v) + " " + padEnd(s, inner) + " " + dim(B.v);
82
+ const lines = [top, row(c.bold(title))];
83
+ for (const sl of wrapText(subtitle, inner)) lines.push(row(dim(sl)));
84
+ lines.push(bot);
85
+ return lines.join("\n");
86
+ }
87
+
88
+ /**
89
+ * A titled box holding a two-column command table: the command in a fixed left
90
+ * column (cyan), the description wrapped in the remaining width. Right border
91
+ * stays aligned regardless of wrapping.
92
+ */
93
+ export function commandBox(title: string, rows: Array<[string, string]>): string {
94
+ const w = termWidth();
95
+ const inner = w - 4; // inside "│ " … " │"
96
+ const gap = 2;
97
+ const longest = rows.reduce((m, [cmd]) => Math.max(m, vlen(cmd)), 0);
98
+ const cmdW = Math.min(longest, Math.max(10, Math.floor(inner * 0.34)));
99
+ const descW = Math.max(12, inner - cmdW - gap);
100
+
101
+ // Title embedded in the top border: ╭─ Title ─────────╮ (border dim, title bold)
102
+ const prefix = B.tl + B.h + " ";
103
+ const fill = Math.max(0, w - vlen(prefix) - vlen(title) - 1 /* space */ - 1 /* tr */);
104
+ const out: string[] = [dim(prefix) + c.bold(title) + dim(" " + B.h.repeat(fill) + B.tr)];
105
+
106
+ for (const [cmd, desc] of rows) {
107
+ const dLines = wrapText(desc, descW);
108
+ const cmdCell = padEnd(c.cyan(cmd), cmdW);
109
+ out.push(dim(B.v) + " " + cmdCell + " ".repeat(gap) + padEnd(dLines[0] ?? "", descW) + " " + dim(B.v));
110
+ for (let i = 1; i < dLines.length; i++) {
111
+ out.push(dim(B.v) + " " + " ".repeat(cmdW + gap) + padEnd(dim(dLines[i] ?? ""), descW) + " " + dim(B.v));
112
+ }
113
+ }
114
+ out.push(dim(B.bl + B.h.repeat(w - 2) + B.br));
115
+ return out.join("\n");
116
+ }
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
+
143
+ /** Nearest command by edit distance, for "did you mean" suggestions. */
144
+ export function nearest(input: string, candidates: string[]): string | null {
145
+ let best: string | null = null;
146
+ let bestD = Infinity;
147
+ for (const cand of candidates) {
148
+ const d = editDistance(input, cand);
149
+ if (d < bestD) { bestD = d; best = cand; }
150
+ }
151
+ return bestD <= Math.max(2, Math.floor(input.length / 3)) ? best : null;
152
+ }
153
+
154
+ function editDistance(a: string, b: string): number {
155
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
156
+ for (let j = 0; j <= b.length; j++) dp[0]![j] = j;
157
+ for (let i = 1; i <= a.length; i++) {
158
+ for (let j = 1; j <= b.length; j++) {
159
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
160
+ dp[i]![j] = Math.min(dp[i - 1]![j]! + 1, dp[i]![j - 1]! + 1, dp[i - 1]![j - 1]! + cost);
161
+ }
162
+ }
163
+ return dp[a.length]![b.length]!;
164
+ }