@fiscalmindset/blindfold 0.4.1 β 0.4.3
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 +15 -70
- package/dist/cli.mjs +285 -86
- package/package.json +1 -1
- package/src/help.ts +277 -0
- package/src/tui.ts +28 -1
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 {
|
|
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
|
|
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
|
|
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
|
|
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,270 @@ 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: "Allow the enclave to reach an API's server. Do this once per API before the proxy can call it \u2014 e.g. allow api.openai.com so a sealed OpenAI key can be used.",
|
|
1615
|
+
flags: [["--host <host,...>", "The API hostname(s) to allow, comma-separated. Each grant adds to the list."]],
|
|
1616
|
+
examples: ["blindfold grant --host api.openai.com", "blindfold grant --host api.github.com,api.stripe.com"],
|
|
1617
|
+
notes: "Why it's needed: the enclave refuses to call any host you haven't allowed (deny-by-default). Grant the host of each API whose key you've sealed."
|
|
1618
|
+
},
|
|
1619
|
+
{
|
|
1620
|
+
name: "share",
|
|
1621
|
+
group: "\u{1F465} Team & sharing",
|
|
1622
|
+
usage: "--to <agent-did> --host <host>[,...]",
|
|
1623
|
+
summary: "Let a teammate use your sealed key for an API \u2014 they can make calls but never see the key itself.",
|
|
1624
|
+
flags: [["--to <agent-did>", "The teammate's agent DID (their Terminal 3 identity)."], ["--host <host,...>", "Which API hosts they're allowed to reach through your enclave."]],
|
|
1625
|
+
examples: ["blindfold share --to did:t3n:abc\u2026 --host api.openai.com"]
|
|
1626
|
+
},
|
|
1627
|
+
{ name: "revoke", group: "\u{1F465} Team & sharing", usage: "--to <agent-did>", summary: "Take back a teammate's access you granted with `share` \u2014 immediate and complete.", flags: [["--to <agent-did>", "The teammate's agent DID to cut off."]], examples: ["blindfold revoke --to did:t3n:abc\u2026"] },
|
|
1628
|
+
// ββ Enclave & admin ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1629
|
+
{ 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"] },
|
|
1630
|
+
{ name: "status", group: "\u{1F4E6} Enclave & admin", usage: "", summary: "One-glance overview: mode, tenant health, and the list of sealed secrets.", examples: ["blindfold status"] },
|
|
1631
|
+
{ 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"] },
|
|
1632
|
+
{ 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"] },
|
|
1633
|
+
{ 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"] },
|
|
1634
|
+
{ 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"] },
|
|
1635
|
+
// ββ Account ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1636
|
+
{
|
|
1637
|
+
name: "login",
|
|
1638
|
+
group: "\u{1F464} Account",
|
|
1639
|
+
usage: "[--did <did>] [--key <0x\u2026>] [--env <net>] [--file]",
|
|
1640
|
+
summary: "Store EXISTING Terminal 3 credentials (tenant key \u2192 OS keychain).",
|
|
1641
|
+
flags: [
|
|
1642
|
+
["--did <did>", "Tenant DID (prompts if omitted)."],
|
|
1643
|
+
["--key <0x\u2026>", "Tenant key (prompts hidden if omitted)."],
|
|
1644
|
+
["--env <net>", "testnet (default) or production."],
|
|
1645
|
+
["--file", "Force a 0600 config file instead of the OS keychain."]
|
|
1646
|
+
],
|
|
1647
|
+
examples: ["blindfold login", "blindfold login --did did:t3n:\u2026 --env testnet"]
|
|
1648
|
+
},
|
|
1649
|
+
{ name: "logout", group: "\u{1F464} Account", usage: "", summary: "Remove stored credentials (keychain entry + config file).", examples: ["blindfold logout"] },
|
|
1650
|
+
{ name: "whoami", group: "\u{1F464} Account", usage: "", summary: "Show tenant, env, and key source \u2014 never the value.", examples: ["blindfold whoami"] },
|
|
1651
|
+
// ββ Agent skill ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1652
|
+
{
|
|
1653
|
+
name: "skill",
|
|
1654
|
+
group: "\u{1F916} Agent skill",
|
|
1655
|
+
usage: "install [--global|--cursor|--opencode|--cline|--all] | uninstall",
|
|
1656
|
+
summary: "Install the Blindfold agent skill so your coding agent handles secrets safely.",
|
|
1657
|
+
flags: [
|
|
1658
|
+
["--global", "Install for every Claude Code session on this machine."],
|
|
1659
|
+
["--cursor / --opencode / --cline", "Install for that agent instead of Claude Code."],
|
|
1660
|
+
["--all", "Install everywhere at once."]
|
|
1661
|
+
],
|
|
1662
|
+
examples: ["blindfold skill install", "blindfold skill install --all"]
|
|
1663
|
+
}
|
|
1664
|
+
];
|
|
1665
|
+
function findCommand(name) {
|
|
1666
|
+
return COMMANDS.find((c2) => c2.name === name || c2.aliases?.includes(name));
|
|
1667
|
+
}
|
|
1668
|
+
function renderMainHelp() {
|
|
1669
|
+
const w = termWidth();
|
|
1670
|
+
const out = [
|
|
1671
|
+
"",
|
|
1672
|
+
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."),
|
|
1673
|
+
""
|
|
1674
|
+
];
|
|
1675
|
+
const nameW = Math.min(12, COMMANDS.reduce((m, cmd) => Math.max(m, vlen(cmd.name)), 0));
|
|
1676
|
+
const descW = Math.max(16, w - 4 - nameW - 2);
|
|
1677
|
+
const fit = (s, max) => s.length > max ? s.slice(0, Math.max(1, max - 1)) + "\u2026" : s;
|
|
1678
|
+
for (const group of GROUP_ORDER) {
|
|
1679
|
+
const cmds = COMMANDS.filter((cmd) => cmd.group === group);
|
|
1680
|
+
const lines = [];
|
|
1681
|
+
for (const [idx, cmd] of cmds.entries()) {
|
|
1682
|
+
if (idx > 0) lines.push("");
|
|
1683
|
+
const dl = wrapText(cmd.summary, descW);
|
|
1684
|
+
lines.push(pad(c.bold(c.cyan(cmd.name)), nameW) + " " + (dl[0] ?? ""));
|
|
1685
|
+
for (let i = 1; i < dl.length; i++) lines.push(pad("", nameW) + " " + (dl[i] ?? ""));
|
|
1686
|
+
const ex = fit(cmd.examples?.[0] ?? `blindfold ${cmd.name}`, descW - 5);
|
|
1687
|
+
lines.push(pad("", nameW) + " " + c.gray("e.g. ") + c.green(ex));
|
|
1688
|
+
}
|
|
1689
|
+
out.push(boxLines(group, lines));
|
|
1690
|
+
out.push("");
|
|
1691
|
+
}
|
|
1692
|
+
out.push(
|
|
1693
|
+
c.bold(c.magenta("\u25B6 Quick start")),
|
|
1694
|
+
` ${c.green("blindfold signup --email you@x.com")} ${c.gray("# create a funded testnet tenant")}`,
|
|
1695
|
+
` ${c.green("blindfold register --name openai_api_key")} ${c.gray("# seal a key (hidden prompt)")}`,
|
|
1696
|
+
` ${c.green("blindfold proxy")} ${c.gray("# point your agent at http://127.0.0.1:8787")}`,
|
|
1697
|
+
"",
|
|
1698
|
+
`${c.gray("Full flags + more examples for any command:")} ${c.cyan("blindfold <command> --help")}`,
|
|
1699
|
+
`${c.gray("Docs:")} ${c.cyan("npmjs.com/package/@fiscalmindset/blindfold")}`,
|
|
1700
|
+
""
|
|
1701
|
+
);
|
|
1702
|
+
return out.join("\n");
|
|
1703
|
+
}
|
|
1704
|
+
function renderCommandHelp(name) {
|
|
1705
|
+
const cmd = findCommand(name);
|
|
1706
|
+
if (!cmd) return renderMainHelp();
|
|
1707
|
+
const w = termWidth();
|
|
1708
|
+
const out = ["", boxLines(`blindfold ${cmd.name}`, wrapText(cmd.summary, w - 4)), ""];
|
|
1709
|
+
out.push(rule("Usage"));
|
|
1710
|
+
out.push(" " + c.cyan(`blindfold ${cmd.name}${cmd.usage ? " " + cmd.usage : ""}`));
|
|
1711
|
+
if (cmd.aliases?.length) out.push(" " + c.gray(`alias: ${cmd.aliases.map((a) => "blindfold " + a).join(", ")}`));
|
|
1712
|
+
out.push("");
|
|
1713
|
+
if (cmd.flags?.length) {
|
|
1714
|
+
out.push(rule("Flags"));
|
|
1715
|
+
const flagW = Math.min(24, cmd.flags.reduce((m, [f]) => Math.max(m, vlen(f)), 0));
|
|
1716
|
+
const dW = Math.max(16, w - 2 - flagW - 2);
|
|
1717
|
+
for (const [flag, desc] of cmd.flags) {
|
|
1718
|
+
const dl = wrapText(desc, dW);
|
|
1719
|
+
out.push(" " + pad(c.yellow(flag), flagW) + " " + (dl[0] ?? ""));
|
|
1720
|
+
for (let i = 1; i < dl.length; i++) out.push(" " + pad("", flagW) + " " + c.gray(dl[i] ?? ""));
|
|
1721
|
+
}
|
|
1722
|
+
out.push("");
|
|
1723
|
+
}
|
|
1724
|
+
if (cmd.examples?.length) {
|
|
1725
|
+
out.push(rule("Examples"));
|
|
1726
|
+
for (const ex of cmd.examples) out.push(" " + c.cyan(ex));
|
|
1727
|
+
out.push("");
|
|
1728
|
+
}
|
|
1729
|
+
if (cmd.notes) {
|
|
1730
|
+
out.push(rule("Notes"));
|
|
1731
|
+
for (const nl of wrapText(cmd.notes, w - 2)) out.push(" " + c.gray(nl));
|
|
1732
|
+
out.push("");
|
|
1733
|
+
}
|
|
1734
|
+
out.push(c.gray("See all commands: ") + c.cyan("blindfold help"));
|
|
1735
|
+
return out.join("\n");
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1481
1738
|
// bin/cmd-auth.ts
|
|
1482
1739
|
init_env();
|
|
1483
1740
|
init_keychain();
|
|
@@ -4367,9 +4624,17 @@ async function main() {
|
|
|
4367
4624
|
const cmdArgs = ddIdx >= 0 ? raw.slice(ddIdx + 1) : [];
|
|
4368
4625
|
const argv = parseArgv(ddIdx >= 0 ? raw.slice(0, ddIdx) : raw);
|
|
4369
4626
|
const cmd = argv._[0] ?? "help";
|
|
4627
|
+
if (cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
4628
|
+
const sub = String(argv._[1] ?? "");
|
|
4629
|
+
console.log(sub && findCommand(sub) ? renderCommandHelp(sub) : renderMainHelp());
|
|
4630
|
+
return;
|
|
4631
|
+
}
|
|
4632
|
+
if ((argv.flags.help || argv.flags.h) && findCommand(cmd)) {
|
|
4633
|
+
console.log(renderCommandHelp(cmd));
|
|
4634
|
+
return;
|
|
4635
|
+
}
|
|
4370
4636
|
const handler = ROUTES[cmd];
|
|
4371
4637
|
if (handler) await handler(cmd, argv, cmdArgs);
|
|
4372
|
-
else if (cmd === "help" || cmd === "--help" || cmd === "-h") printHelp();
|
|
4373
4638
|
else printUnknown(cmd);
|
|
4374
4639
|
}
|
|
4375
4640
|
function printUnknown(cmd) {
|
|
@@ -4381,72 +4646,6 @@ function printUnknown(cmd) {
|
|
|
4381
4646
|
console.error(c.gray(" Run ") + c.cyan("blindfold help") + c.gray(" to see all commands."));
|
|
4382
4647
|
process.exit(1);
|
|
4383
4648
|
}
|
|
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
4649
|
main().catch((e) => {
|
|
4451
4650
|
console.error("\u2716", e.message);
|
|
4452
4651
|
process.exit(1);
|
package/package.json
CHANGED
package/src/help.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
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: "Allow the enclave to reach an API's server. Do this once per API before the proxy can call it β e.g. allow api.openai.com so a sealed OpenAI key can be used.",
|
|
143
|
+
flags: [["--host <host,...>", "The API hostname(s) to allow, comma-separated. Each grant adds to the list."]],
|
|
144
|
+
examples: ["blindfold grant --host api.openai.com", "blindfold grant --host api.github.com,api.stripe.com"],
|
|
145
|
+
notes: "Why it's needed: the enclave refuses to call any host you haven't allowed (deny-by-default). Grant the host of each API whose key you've sealed.",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "share", group: "π₯ Team & sharing", usage: "--to <agent-did> --host <host>[,...]",
|
|
149
|
+
summary: "Let a teammate use your sealed key for an API β they can make calls but never see the key itself.",
|
|
150
|
+
flags: [["--to <agent-did>", "The teammate's agent DID (their Terminal 3 identity)."], ["--host <host,...>", "Which API hosts they're allowed to reach through your enclave."]],
|
|
151
|
+
examples: ["blindfold share --to did:t3n:abc⦠--host api.openai.com"],
|
|
152
|
+
},
|
|
153
|
+
{ name: "revoke", group: "π₯ Team & sharing", usage: "--to <agent-did>", summary: "Take back a teammate's access you granted with `share` β immediate and complete.", flags: [["--to <agent-did>", "The teammate's agent DID to cut off."]], examples: ["blindfold revoke --to did:t3n:abcβ¦"] },
|
|
154
|
+
|
|
155
|
+
// ββ Enclave & admin ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
156
|
+
{ 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"] },
|
|
157
|
+
{ name: "status", group: "π¦ Enclave & admin", usage: "", summary: "One-glance overview: mode, tenant health, and the list of sealed secrets.", examples: ["blindfold status"] },
|
|
158
|
+
{ name: "sealed", group: "π¦ Enclave & admin", usage: "", summary: "List sealed keys β metadata only (name, byte-length, when, where). Never the value.", examples: ["blindfold sealed"] },
|
|
159
|
+
{ name: "audit", group: "π¦ Enclave & admin", usage: "", summary: "Verify the ledger hash-chain and reconcile it against the enclave (flags drift/tampering).", examples: ["blindfold audit"] },
|
|
160
|
+
{ 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"] },
|
|
161
|
+
{ 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"] },
|
|
162
|
+
|
|
163
|
+
// ββ Account ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
164
|
+
{
|
|
165
|
+
name: "login", group: "π€ Account", usage: "[--did <did>] [--key <0xβ¦>] [--env <net>] [--file]",
|
|
166
|
+
summary: "Store EXISTING Terminal 3 credentials (tenant key β OS keychain).",
|
|
167
|
+
flags: [
|
|
168
|
+
["--did <did>", "Tenant DID (prompts if omitted)."],
|
|
169
|
+
["--key <0xβ¦>", "Tenant key (prompts hidden if omitted)."],
|
|
170
|
+
["--env <net>", "testnet (default) or production."],
|
|
171
|
+
["--file", "Force a 0600 config file instead of the OS keychain."],
|
|
172
|
+
],
|
|
173
|
+
examples: ["blindfold login", "blindfold login --did did:t3n:β¦ --env testnet"],
|
|
174
|
+
},
|
|
175
|
+
{ name: "logout", group: "π€ Account", usage: "", summary: "Remove stored credentials (keychain entry + config file).", examples: ["blindfold logout"] },
|
|
176
|
+
{ name: "whoami", group: "π€ Account", usage: "", summary: "Show tenant, env, and key source β never the value.", examples: ["blindfold whoami"] },
|
|
177
|
+
|
|
178
|
+
// ββ Agent skill ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
179
|
+
{
|
|
180
|
+
name: "skill", group: "π€ Agent skill", usage: "install [--global|--cursor|--opencode|--cline|--all] | uninstall",
|
|
181
|
+
summary: "Install the Blindfold agent skill so your coding agent handles secrets safely.",
|
|
182
|
+
flags: [
|
|
183
|
+
["--global", "Install for every Claude Code session on this machine."],
|
|
184
|
+
["--cursor / --opencode / --cline", "Install for that agent instead of Claude Code."],
|
|
185
|
+
["--all", "Install everywhere at once."],
|
|
186
|
+
],
|
|
187
|
+
examples: ["blindfold skill install", "blindfold skill install --all"],
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
/** Look up a command by name or alias. */
|
|
192
|
+
export function findCommand(name: string): CmdDef | undefined {
|
|
193
|
+
return COMMANDS.find((c) => c.name === name || c.aliases?.includes(name));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The grouped overview: `blindfold help`. */
|
|
197
|
+
export function renderMainHelp(): string {
|
|
198
|
+
const w = termWidth();
|
|
199
|
+
const out: string[] = [
|
|
200
|
+
"",
|
|
201
|
+
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."),
|
|
202
|
+
"",
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
const nameW = Math.min(12, COMMANDS.reduce((m, cmd) => Math.max(m, vlen(cmd.name)), 0));
|
|
206
|
+
const descW = Math.max(16, w - 4 - nameW - 2);
|
|
207
|
+
const fit = (s: string, max: number): string => (s.length > max ? s.slice(0, Math.max(1, max - 1)) + "β¦" : s);
|
|
208
|
+
|
|
209
|
+
for (const group of GROUP_ORDER) {
|
|
210
|
+
const cmds = COMMANDS.filter((cmd) => cmd.group === group);
|
|
211
|
+
const lines: string[] = [];
|
|
212
|
+
for (const [idx, cmd] of cmds.entries()) {
|
|
213
|
+
if (idx > 0) lines.push(""); // breathing room between commands
|
|
214
|
+
const dl = wrapText(cmd.summary, descW);
|
|
215
|
+
// command name (bright) + summary
|
|
216
|
+
lines.push(pad(c.bold(c.cyan(cmd.name)), nameW) + " " + (dl[0] ?? ""));
|
|
217
|
+
for (let i = 1; i < dl.length; i++) lines.push(pad("", nameW) + " " + (dl[i] ?? ""));
|
|
218
|
+
// one concrete example β the fastest way to "get" the command
|
|
219
|
+
const ex = fit(cmd.examples?.[0] ?? `blindfold ${cmd.name}`, descW - 5);
|
|
220
|
+
lines.push(pad("", nameW) + " " + c.gray("e.g. ") + c.green(ex));
|
|
221
|
+
}
|
|
222
|
+
out.push(boxLines(group, lines));
|
|
223
|
+
out.push("");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
out.push(
|
|
227
|
+
c.bold(c.magenta("βΆ Quick start")),
|
|
228
|
+
` ${c.green("blindfold signup --email you@x.com")} ${c.gray("# create a funded testnet tenant")}`,
|
|
229
|
+
` ${c.green("blindfold register --name openai_api_key")} ${c.gray("# seal a key (hidden prompt)")}`,
|
|
230
|
+
` ${c.green("blindfold proxy")} ${c.gray("# point your agent at http://127.0.0.1:8787")}`,
|
|
231
|
+
"",
|
|
232
|
+
`${c.gray("Full flags + more examples for any command:")} ${c.cyan("blindfold <command> --help")}`,
|
|
233
|
+
`${c.gray("Docs:")} ${c.cyan("npmjs.com/package/@fiscalmindset/blindfold")}`,
|
|
234
|
+
"",
|
|
235
|
+
);
|
|
236
|
+
return out.join("\n");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Detailed help for one command: `blindfold <cmd> --help`. */
|
|
240
|
+
export function renderCommandHelp(name: string): string {
|
|
241
|
+
const cmd = findCommand(name);
|
|
242
|
+
if (!cmd) return renderMainHelp();
|
|
243
|
+
const w = termWidth();
|
|
244
|
+
const out: string[] = ["", boxLines(`blindfold ${cmd.name}`, wrapText(cmd.summary, w - 4)), ""];
|
|
245
|
+
|
|
246
|
+
out.push(rule("Usage"));
|
|
247
|
+
out.push(" " + c.cyan(`blindfold ${cmd.name}${cmd.usage ? " " + cmd.usage : ""}`));
|
|
248
|
+
if (cmd.aliases?.length) out.push(" " + c.gray(`alias: ${cmd.aliases.map((a) => "blindfold " + a).join(", ")}`));
|
|
249
|
+
out.push("");
|
|
250
|
+
|
|
251
|
+
if (cmd.flags?.length) {
|
|
252
|
+
out.push(rule("Flags"));
|
|
253
|
+
const flagW = Math.min(24, cmd.flags.reduce((m, [f]) => Math.max(m, vlen(f)), 0));
|
|
254
|
+
const dW = Math.max(16, w - 2 - flagW - 2);
|
|
255
|
+
for (const [flag, desc] of cmd.flags) {
|
|
256
|
+
const dl = wrapText(desc, dW);
|
|
257
|
+
out.push(" " + pad(c.yellow(flag), flagW) + " " + (dl[0] ?? ""));
|
|
258
|
+
for (let i = 1; i < dl.length; i++) out.push(" " + pad("", flagW) + " " + c.gray(dl[i] ?? ""));
|
|
259
|
+
}
|
|
260
|
+
out.push("");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (cmd.examples?.length) {
|
|
264
|
+
out.push(rule("Examples"));
|
|
265
|
+
for (const ex of cmd.examples) out.push(" " + c.cyan(ex));
|
|
266
|
+
out.push("");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (cmd.notes) {
|
|
270
|
+
out.push(rule("Notes"));
|
|
271
|
+
for (const nl of wrapText(cmd.notes, w - 2)) out.push(" " + c.gray(nl));
|
|
272
|
+
out.push("");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
out.push(c.gray("See all commands: ") + c.cyan("blindfold help"));
|
|
276
|
+
return out.join("\n");
|
|
277
|
+
}
|
package/src/tui.ts
CHANGED
|
@@ -45,9 +45,11 @@ export function vlen(s: string): number {
|
|
|
45
45
|
}
|
|
46
46
|
return w;
|
|
47
47
|
}
|
|
48
|
-
|
|
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;
|