@lifeaitools/clauth 0.3.12 → 0.4.0

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.
Files changed (3) hide show
  1. package/cli/commands/serve.js +1653 -1213
  2. package/cli/index.js +496 -493
  3. package/package.json +1 -1
package/cli/index.js CHANGED
@@ -1,493 +1,496 @@
1
- #!/usr/bin/env node
2
- // cli/index.js — clauth entry point
3
-
4
- import { Command } from "commander";
5
- import chalk from "chalk";
6
- import ora from "ora";
7
- import inquirer from "inquirer";
8
- import Conf from "conf";
9
- import { getConfOptions } from "./conf-path.js";
10
- import { getMachineHash, deriveToken, deriveSeedHash } from "./fingerprint.js";
11
- import * as api from "./api.js";
12
- import os from "os";
13
-
14
- const config = new Conf(getConfOptions());
15
- const VERSION = "0.3.11";
16
-
17
- // ============================================================
18
- // Password prompt helper
19
- // ============================================================
20
- async function promptPassword(message = "clauth password") {
21
- const { pw } = await inquirer.prompt([{
22
- type: "password",
23
- name: "pw",
24
- message,
25
- mask: "*",
26
- validate: v => v.length >= 8 || "Password must be at least 8 characters"
27
- }]);
28
- return pw;
29
- }
30
-
31
- // ============================================================
32
- // Auth helper — get pw + derive token
33
- // ============================================================
34
- async function getAuth(pw) {
35
- const password = pw || await promptPassword();
36
- const machineHash = getMachineHash();
37
- const { token, timestamp } = deriveToken(password, machineHash);
38
- return { password, machineHash, token, timestamp };
39
- }
40
-
41
- // ============================================================
42
- // Program
43
- // ============================================================
44
- const program = new Command();
45
-
46
- program
47
- .name("clauth")
48
- .version(VERSION)
49
- .description(chalk.cyan("🔐 clauth") + " — Hardware-bound credential vault for LIFEAI infrastructure");
50
-
51
- // ──────────────────────────────────────────────
52
- // clauth install (Supabase provisioning + skill install + test)
53
- // ──────────────────────────────────────────────
54
- import { runInstall } from './commands/install.js';
55
- import { runUninstall } from './commands/uninstall.js';
56
- import { runScrub } from './commands/scrub.js';
57
- import { runServe } from './commands/serve.js';
58
-
59
- program
60
- .command('install')
61
- .description('Provision Supabase, deploy Edge Function, install Claude skill')
62
- .option('--ref <ref>', 'Supabase project ref')
63
- .option('--pat <pat>', 'Supabase Personal Access Token')
64
- .action(async (opts) => {
65
- await runInstall(opts);
66
- });
67
-
68
- program
69
- .command('uninstall')
70
- .description('Full teardown — drop DB objects, Edge Function, secrets, skill, config')
71
- .option('--ref <ref>', 'Supabase project ref')
72
- .option('--pat <pat>', 'Supabase Personal Access Token (required)')
73
- .option('--yes', 'Skip confirmation prompt')
74
- .action(async (opts) => {
75
- if (!opts.yes) {
76
- const inquirerMod = await import('inquirer');
77
- const { confirm } = await inquirerMod.default.prompt([{
78
- type: 'input',
79
- name: 'confirm',
80
- message: chalk.red('Type "CONFIRM UNINSTALL" to proceed:'),
81
- }]);
82
- if (confirm !== 'CONFIRM UNINSTALL') {
83
- console.log(chalk.yellow('\n Uninstall cancelled.\n'));
84
- process.exit(0);
85
- }
86
- }
87
- await runUninstall(opts);
88
- });
89
-
90
- // ──────────────────────────────────────────────
91
- // clauth setup
92
- // ──────────────────────────────────────────────
93
- program
94
- .command("setup")
95
- .description("Register this machine with the vault (run after clauth install)")
96
- .option("--admin-token <token>", "Bootstrap token (from clauth install output)")
97
- .option("--label <label>", "Human label for this machine")
98
- .option("-p, --pw <password>", "Password (skip interactive prompt)")
99
- .action(async (opts) => {
100
- console.log(chalk.cyan("\n🔐 clauth setup\n"));
101
-
102
- // URL + anon key already saved by clauth install — fail fast if missing
103
- const savedUrl = config.get("supabase_url");
104
- const savedAnon = config.get("supabase_anon_key");
105
- if (!savedUrl || !savedAnon) {
106
- console.log(chalk.yellow(" Supabase config not found. Run clauth install first.\n"));
107
- process.exit(1);
108
- }
109
- console.log(chalk.gray(` Project: ${savedUrl}\n`));
110
-
111
- let answers;
112
- if (opts.pw && opts.adminToken) {
113
- // Non-interactive mode — all flags provided
114
- answers = { label: opts.label || os.hostname(), pw: opts.pw, adminTk: opts.adminToken };
115
- } else {
116
- answers = await inquirer.prompt([
117
- { type: "input", name: "label", message: "Machine label:", default: opts.label || os.hostname() },
118
- { type: "password", name: "pw", message: "Set password:", mask: "*", default: opts.pw || "" },
119
- { type: "password", name: "adminTk", message: "Bootstrap token:", mask: "*",
120
- default: opts.adminToken || "" },
121
- ]);
122
- }
123
-
124
- const spinner = ora("Registering machine with vault...").start();
125
- try {
126
- const machineHash = getMachineHash();
127
- const seedHash = deriveSeedHash(machineHash, answers.pw);
128
- const result = await api.registerMachine(machineHash, seedHash, answers.label, answers.adminTk);
129
- if (result.error) throw new Error(result.error);
130
- spinner.succeed(chalk.green(`Machine registered: ${machineHash.slice(0,12)}...`));
131
- console.log(chalk.green("\n✓ clauth is ready.\n"));
132
- console.log(chalk.cyan(" clauth test — verify connection"));
133
- console.log(chalk.cyan(" clauth status — see all services\n"));
134
- } catch (err) {
135
- spinner.fail(chalk.red(`Setup failed: ${err.message}`));
136
- process.exit(1);
137
- }
138
- });
139
-
140
- // ──────────────────────────────────────────────
141
- // clauth status
142
- // ──────────────────────────────────────────────
143
- program
144
- .command("status")
145
- .description("Show all services and their state")
146
- .option("-p, --pw <password>", "Password (or will prompt)")
147
- .action(async (opts) => {
148
- const auth = await getAuth(opts.pw);
149
- const spinner = ora("Fetching service status...").start();
150
- try {
151
- const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp);
152
- spinner.stop();
153
- if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); return; }
154
-
155
- console.log(chalk.cyan("\n🔐 clauth service status\n"));
156
- console.log(
157
- chalk.bold(
158
- " " + "SERVICE".padEnd(20) + "TYPE".padEnd(12) + "STATUS".padEnd(12) +
159
- "KEY STORED".padEnd(12) + "LAST RETRIEVED"
160
- )
161
- );
162
- console.log(" " + "─".repeat(72));
163
-
164
- for (const s of result.services || []) {
165
- const status = s.enabled
166
- ? chalk.green("ACTIVE".padEnd(12))
167
- : s.vault_key
168
- ? chalk.yellow("SUSPENDED".padEnd(12))
169
- : chalk.gray("NO KEY".padEnd(12));
170
- const hasKey = s.vault_key ? chalk.green("✓".padEnd(12)) : chalk.gray("—".padEnd(12));
171
- const lastGet = s.last_retrieved
172
- ? new Date(s.last_retrieved).toLocaleDateString()
173
- : chalk.gray("never");
174
-
175
- console.log(` ${s.name.padEnd(20)}${s.key_type.padEnd(12)}${status}${hasKey}${lastGet}`);
176
- }
177
- console.log();
178
- } catch (err) {
179
- spinner.fail(chalk.red(err.message));
180
- }
181
- });
182
-
183
- // ──────────────────────────────────────────────
184
- // clauth write pw <new_password>
185
- // clauth write params
186
- // clauth write key <service> <value>
187
- // ──────────────────────────────────────────────
188
- const writeCmd = program.command("write").description("Write credentials or update auth parameters");
189
-
190
- writeCmd
191
- .command("pw [newpw]")
192
- .description("Set or update clauth master password")
193
- .action(async (newpw) => {
194
- console.log(chalk.cyan("\n🔐 clauth write pw\n"));
195
- const current = await promptPassword("Current password (to verify)");
196
- const pw = newpw || (await inquirer.prompt([
197
- { type: "password", name: "p", message: "New password:", mask: "*" },
198
- { type: "password", name: "c", message: "Confirm new password:", mask: "*" }
199
- ]).then(a => { if (a.p !== a.c) { console.log(chalk.red("Passwords don't match")); process.exit(1); } return a.p; }));
200
-
201
- // Re-register machine with new seed hash
202
- const machineHash = getMachineHash();
203
- const newSeedHash = deriveSeedHash(machineHash, pw);
204
- const { token, timestamp } = deriveToken(current, machineHash);
205
- const adminToken = await inquirer.prompt([{
206
- type: "password", name: "t", message: "Admin bootstrap token (required for re-registration):", mask: "*"
207
- }]).then(a => a.t);
208
-
209
- const spinner = ora("Updating password and re-registering machine...").start();
210
- try {
211
- const result = await api.registerMachine(machineHash, newSeedHash, null, adminToken);
212
- if (result.error) throw new Error(result.error);
213
- spinner.succeed(chalk.green("Password updated and machine re-registered."));
214
- } catch (err) {
215
- spinner.fail(chalk.red(err.message));
216
- }
217
- });
218
-
219
- writeCmd
220
- .command("params")
221
- .description("Re-read hardware fingerprint (use after hardware change)")
222
- .action(async () => {
223
- const spinner = ora("Reading hardware fingerprint...").start();
224
- try {
225
- const hash = getMachineHash();
226
- spinner.succeed(chalk.green(`Machine hash: ${hash.slice(0,16)}...`));
227
- console.log(chalk.gray("Full hash: " + hash));
228
- } catch (err) {
229
- spinner.fail(chalk.red(err.message));
230
- }
231
- });
232
-
233
- writeCmd
234
- .command("key <service> [value]")
235
- .description("Write a credential into vault for a service")
236
- .option("-p, --pw <password>", "Password")
237
- .action(async (service, value, opts) => {
238
- const auth = await getAuth(opts.pw);
239
- let val = value;
240
- if (!val) {
241
- const { v } = await inquirer.prompt([{ type: "password", name: "v", message: `Value for ${service}:`, mask: "*" }]);
242
- val = v;
243
- }
244
- const spinner = ora(`Writing key for ${service}...`).start();
245
- try {
246
- const result = await api.write(auth.password, auth.machineHash, auth.token, auth.timestamp, service, val);
247
- if (result.error) throw new Error(result.error);
248
- spinner.succeed(chalk.green(`Key stored in vault: auth.${service}`));
249
- } catch (err) {
250
- spinner.fail(chalk.red(err.message));
251
- }
252
- });
253
-
254
- // ──────────────────────────────────────────────
255
- // clauth enable <service|all>
256
- // clauth disable <service|all>
257
- // ──────────────────────────────────────────────
258
- program
259
- .command("enable <service>")
260
- .description("Enable a service (or 'all')")
261
- .option("-p, --pw <password>")
262
- .action(async (service, opts) => {
263
- const auth = await getAuth(opts.pw);
264
- const spinner = ora(`Enabling ${service}...`).start();
265
- try {
266
- const result = await api.enable(auth.password, auth.machineHash, auth.token, auth.timestamp, service, true);
267
- if (result.error) throw new Error(result.error);
268
- spinner.succeed(chalk.green(`Enabled: ${service}`));
269
- } catch (err) { spinner.fail(chalk.red(err.message)); }
270
- });
271
-
272
- program
273
- .command("disable <service>")
274
- .description("Disable a service (or 'all')")
275
- .option("-p, --pw <password>")
276
- .action(async (service, opts) => {
277
- const auth = await getAuth(opts.pw);
278
- const spinner = ora(`Disabling ${service}...`).start();
279
- try {
280
- const result = await api.enable(auth.password, auth.machineHash, auth.token, auth.timestamp, service, false);
281
- if (result.error) throw new Error(result.error);
282
- spinner.succeed(chalk.yellow(`Disabled: ${service}`));
283
- } catch (err) { spinner.fail(chalk.red(err.message)); }
284
- });
285
-
286
- // ──────────────────────────────────────────────
287
- // clauth add service <name>
288
- // clauth remove service <name>
289
- // clauth list services
290
- // ──────────────────────────────────────────────
291
- const addCmd = program.command("add").description("Add resources to the registry");
292
-
293
- addCmd
294
- .command("service <name>")
295
- .description("Register a new service slot")
296
- .option("--type <type>", "Key type: token | keypair | connstring | oauth")
297
- .option("--label <label>", "Human-readable label")
298
- .option("--description <desc>", "Description")
299
- .option("-p, --pw <password>")
300
- .action(async (name, opts) => {
301
- const auth = await getAuth(opts.pw);
302
- let answers;
303
- if (opts.type && opts.label) {
304
- // Non-interactive — all flags provided
305
- answers = { label: opts.label, key_type: opts.type, desc: opts.description || "" };
306
- } else {
307
- answers = await inquirer.prompt([
308
- { type: "input", name: "label", message: "Label:", default: opts.label || name },
309
- { type: "list", name: "key_type", message: "Key type:", choices: ["token","keypair","connstring","oauth"], default: opts.type || "token" },
310
- { type: "input", name: "desc", message: "Description (optional):", default: opts.description || "" }
311
- ]);
312
- }
313
- const spinner = ora(`Adding service: ${name}...`).start();
314
- try {
315
- const result = await api.addService(
316
- auth.password, auth.machineHash, auth.token, auth.timestamp,
317
- name, answers.label, answers.key_type, answers.desc
318
- );
319
- if (result.error) throw new Error(result.error);
320
- spinner.succeed(chalk.green(`Service added: ${name} (${answers.key_type})`));
321
- console.log(chalk.gray(` Next: clauth write key ${name}`));
322
- } catch (err) { spinner.fail(chalk.red(err.message)); }
323
- });
324
-
325
- const removeCmd = program.command("remove").description("Remove resources from the registry");
326
-
327
- removeCmd
328
- .command("service <name>")
329
- .description("Remove a service and its key from vault")
330
- .option("-p, --pw <password>")
331
- .action(async (name, opts) => {
332
- const { confirm } = await inquirer.prompt([{
333
- type: "input", name: "confirm",
334
- message: chalk.red(`Type "CONFIRM REMOVE ${name.toUpperCase()}" to proceed:`)
335
- }]);
336
- const auth = await getAuth(opts.pw);
337
- const spinner = ora(`Removing ${name}...`).start();
338
- try {
339
- const result = await api.removeService(auth.password, auth.machineHash, auth.token, auth.timestamp, name, confirm);
340
- if (result.error) throw new Error(result.error);
341
- spinner.succeed(chalk.yellow(`Removed: ${name}`));
342
- } catch (err) { spinner.fail(chalk.red(err.message)); }
343
- });
344
-
345
- program
346
- .command("list")
347
- .description("List all registered services")
348
- .option("-p, --pw <password>")
349
- .action(async (opts) => {
350
- const auth = await getAuth(opts.pw);
351
- const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp);
352
- if (result.error) { console.log(chalk.red(result.error)); return; }
353
- console.log(chalk.cyan("\n Registered services:\n"));
354
- for (const s of result.services || []) {
355
- console.log(` ${chalk.bold(s.name.padEnd(20))} ${chalk.gray(s.key_type.padEnd(12))} ${chalk.gray(s.description || "")}`);
356
- }
357
- console.log();
358
- });
359
-
360
- // ──────────────────────────────────────────────
361
- // clauth test <service|all>
362
- // ──────────────────────────────────────────────
363
- program
364
- .command("test [service]")
365
- .description("Test HMAC handshake — no key returned")
366
- .option("-p, --pw <password>")
367
- .action(async (service, opts) => {
368
- const auth = await getAuth(opts.pw);
369
- const spinner = ora("Testing auth handshake...").start();
370
- try {
371
- const result = await api.test(auth.password, auth.machineHash, auth.token, auth.timestamp);
372
- if (result.error) throw new Error(`${result.error}: ${result.reason}`);
373
- spinner.succeed(chalk.green("PASS — HMAC validated"));
374
- console.log(chalk.gray(` Machine: ${auth.machineHash.slice(0,16)}...`));
375
- console.log(chalk.gray(` Window: ${new Date(result.timestamp).toISOString()}`));
376
- } catch (err) {
377
- spinner.fail(chalk.red("FAIL — " + err.message));
378
- }
379
- });
380
-
381
- // ──────────────────────────────────────────────
382
- // clauth get <service>
383
- // ──────────────────────────────────────────────
384
- program
385
- .command("get <service>")
386
- .description("Retrieve a key from vault")
387
- .option("-p, --pw <password>")
388
- .option("--json", "Output raw JSON")
389
- .action(async (service, opts) => {
390
- const auth = await getAuth(opts.pw);
391
- const spinner = ora(`Retrieving ${service}...`).start();
392
- try {
393
- const result = await api.retrieve(auth.password, auth.machineHash, auth.token, auth.timestamp, service);
394
- spinner.stop();
395
- if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); return; }
396
- if (opts.json) {
397
- console.log(JSON.stringify(result, null, 2));
398
- } else {
399
- console.log(chalk.cyan(`\n🔑 ${service} (${result.key_type})\n`));
400
- const val = typeof result.value === "string" ? result.value : JSON.stringify(result.value, null, 2);
401
- console.log(val);
402
- console.log();
403
- }
404
- } catch (err) {
405
- spinner.fail(chalk.red(err.message));
406
- }
407
- });
408
-
409
- // ──────────────────────────────────────────────
410
- // clauth revoke <service|all>
411
- // ──────────────────────────────────────────────
412
- program
413
- .command("revoke <service>")
414
- .description("Delete key from vault (destructive)")
415
- .option("-p, --pw <password>")
416
- .action(async (service, opts) => {
417
- const phrase = service === "all" ? "CONFIRM REVOKE ALL" : `CONFIRM REVOKE ${service.toUpperCase()}`;
418
- const { confirm } = await inquirer.prompt([{
419
- type: "input", name: "confirm",
420
- message: chalk.red(`Type "${phrase}" to proceed:`)
421
- }]);
422
- const auth = await getAuth(opts.pw);
423
- const spinner = ora(`Revoking ${service}...`).start();
424
- try {
425
- const result = await api.revoke(auth.password, auth.machineHash, auth.token, auth.timestamp, service, confirm);
426
- if (result.error) throw new Error(result.error);
427
- spinner.succeed(chalk.yellow(`Revoked: ${service}`));
428
- } catch (err) { spinner.fail(chalk.red(err.message)); }
429
- });
430
-
431
- // ──────────────────────────────────────────────
432
- // clauth scrub [target]
433
- // ──────────────────────────────────────────────
434
- program
435
- .command("scrub [target]")
436
- .description("Scrub credentials from Claude Code transcript logs (no auth required)")
437
- .option("--force", "Rescrub files even if already marked clean")
438
- .addHelpText("after", `
439
- Examples:
440
- clauth scrub Scrub the most recent (active) transcript
441
- clauth scrub <file> Scrub a specific .jsonl file
442
- clauth scrub all Scrub every transcript in ~/.claude/projects/
443
- clauth scrub all --force Rescrub all files (ignore markers)
444
- `)
445
- .action(async (target, opts) => {
446
- await runScrub(target, opts);
447
- });
448
-
449
- // ──────────────────────────────────────────────
450
- // clauth --help override banner
451
- // ──────────────────────────────────────────────
452
- program.addHelpText("beforeAll", chalk.cyan(`
453
- ██████╗██╗ █████╗ ██╗ ██╗████████╗██╗ ██╗
454
- ██╔════╝██║ ██╔══██╗██║ ██║╚══██╔══╝██║ ██║
455
- ██║ ██║ ███████║██║ ██║ ██║ ███████║
456
- ██║ ██║ ██╔══██║██║ ██║ ██║ ██╔══██║
457
- ╚██████╗███████╗██║ ██║╚██████╔╝ ██║ ██║ ██║
458
- ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝
459
- v${VERSION} — LIFEAI Credential Vault
460
- `));
461
-
462
- // ──────────────────────────────────────────────
463
- // clauth serve [action]
464
- // ──────────────────────────────────────────────
465
- program
466
- .command("serve [action]")
467
- .description("Manage localhost HTTP vault daemon (start|stop|restart|ping)")
468
- .option("--port <n>", "Port (default: 52437)")
469
- .option("-p, --pw <password>", "clauth password (optional — omit to start locked, unlock in browser)")
470
- .option("--services <list>", "Comma-separated service whitelist (default: all)")
471
- .option("--action <action>", "Internal: action override for daemon child")
472
- .addHelpText("after", `
473
- Actions:
474
- start Start the server as a background daemon
475
- stop Stop the running daemon
476
- restart Stop + start
477
- ping Check if the daemon is running
478
- foreground Run in foreground (Ctrl+C to stop) — default if no action given
479
-
480
- Examples:
481
- clauth serve start Start locked — unlock at http://127.0.0.1:52437
482
- clauth serve start -p mypass Start pre-unlocked (password in memory only)
483
- clauth serve stop Stop the daemon
484
- clauth serve ping Check status
485
- clauth serve restart Restart (stays locked until browser unlock)
486
- clauth serve start --services github,vercel
487
- `)
488
- .action(async (action, opts) => {
489
- const resolvedAction = opts.action || action || "foreground";
490
- await runServe({ ...opts, action: resolvedAction });
491
- });
492
-
493
- program.parse(process.argv);
1
+ #!/usr/bin/env node
2
+ // cli/index.js — clauth entry point
3
+
4
+ import { Command } from "commander";
5
+ import chalk from "chalk";
6
+ import ora from "ora";
7
+ import inquirer from "inquirer";
8
+ import Conf from "conf";
9
+ import { getConfOptions } from "./conf-path.js";
10
+ import { getMachineHash, deriveToken, deriveSeedHash } from "./fingerprint.js";
11
+ import * as api from "./api.js";
12
+ import os from "os";
13
+
14
+ const config = new Conf(getConfOptions());
15
+ const VERSION = "0.4.0";
16
+
17
+ // ============================================================
18
+ // Password prompt helper
19
+ // ============================================================
20
+ async function promptPassword(message = "clauth password") {
21
+ const { pw } = await inquirer.prompt([{
22
+ type: "password",
23
+ name: "pw",
24
+ message,
25
+ mask: "*",
26
+ validate: v => v.length >= 8 || "Password must be at least 8 characters"
27
+ }]);
28
+ return pw;
29
+ }
30
+
31
+ // ============================================================
32
+ // Auth helper — get pw + derive token
33
+ // ============================================================
34
+ async function getAuth(pw) {
35
+ const password = pw || await promptPassword();
36
+ const machineHash = getMachineHash();
37
+ const { token, timestamp } = deriveToken(password, machineHash);
38
+ return { password, machineHash, token, timestamp };
39
+ }
40
+
41
+ // ============================================================
42
+ // Program
43
+ // ============================================================
44
+ const program = new Command();
45
+
46
+ program
47
+ .name("clauth")
48
+ .version(VERSION)
49
+ .description(chalk.cyan("🔐 clauth") + " — Hardware-bound credential vault for LIFEAI infrastructure");
50
+
51
+ // ──────────────────────────────────────────────
52
+ // clauth install (Supabase provisioning + skill install + test)
53
+ // ──────────────────────────────────────────────
54
+ import { runInstall } from './commands/install.js';
55
+ import { runUninstall } from './commands/uninstall.js';
56
+ import { runScrub } from './commands/scrub.js';
57
+ import { runServe } from './commands/serve.js';
58
+
59
+ program
60
+ .command('install')
61
+ .description('Provision Supabase, deploy Edge Function, install Claude skill')
62
+ .option('--ref <ref>', 'Supabase project ref')
63
+ .option('--pat <pat>', 'Supabase Personal Access Token')
64
+ .action(async (opts) => {
65
+ await runInstall(opts);
66
+ });
67
+
68
+ program
69
+ .command('uninstall')
70
+ .description('Full teardown — drop DB objects, Edge Function, secrets, skill, config')
71
+ .option('--ref <ref>', 'Supabase project ref')
72
+ .option('--pat <pat>', 'Supabase Personal Access Token (required)')
73
+ .option('--yes', 'Skip confirmation prompt')
74
+ .action(async (opts) => {
75
+ if (!opts.yes) {
76
+ const inquirerMod = await import('inquirer');
77
+ const { confirm } = await inquirerMod.default.prompt([{
78
+ type: 'input',
79
+ name: 'confirm',
80
+ message: chalk.red('Type "CONFIRM UNINSTALL" to proceed:'),
81
+ }]);
82
+ if (confirm !== 'CONFIRM UNINSTALL') {
83
+ console.log(chalk.yellow('\n Uninstall cancelled.\n'));
84
+ process.exit(0);
85
+ }
86
+ }
87
+ await runUninstall(opts);
88
+ });
89
+
90
+ // ──────────────────────────────────────────────
91
+ // clauth setup
92
+ // ──────────────────────────────────────────────
93
+ program
94
+ .command("setup")
95
+ .description("Register this machine with the vault (run after clauth install)")
96
+ .option("--admin-token <token>", "Bootstrap token (from clauth install output)")
97
+ .option("--label <label>", "Human label for this machine")
98
+ .option("-p, --pw <password>", "Password (skip interactive prompt)")
99
+ .action(async (opts) => {
100
+ console.log(chalk.cyan("\n🔐 clauth setup\n"));
101
+
102
+ // URL + anon key already saved by clauth install — fail fast if missing
103
+ const savedUrl = config.get("supabase_url");
104
+ const savedAnon = config.get("supabase_anon_key");
105
+ if (!savedUrl || !savedAnon) {
106
+ console.log(chalk.yellow(" Supabase config not found. Run clauth install first.\n"));
107
+ process.exit(1);
108
+ }
109
+ console.log(chalk.gray(` Project: ${savedUrl}\n`));
110
+
111
+ let answers;
112
+ if (opts.pw && opts.adminToken) {
113
+ // Non-interactive mode — all flags provided
114
+ answers = { label: opts.label || os.hostname(), pw: opts.pw, adminTk: opts.adminToken };
115
+ } else {
116
+ answers = await inquirer.prompt([
117
+ { type: "input", name: "label", message: "Machine label:", default: opts.label || os.hostname() },
118
+ { type: "password", name: "pw", message: "Set password:", mask: "*", default: opts.pw || "" },
119
+ { type: "password", name: "adminTk", message: "Bootstrap token:", mask: "*",
120
+ default: opts.adminToken || "" },
121
+ ]);
122
+ }
123
+
124
+ const spinner = ora("Registering machine with vault...").start();
125
+ try {
126
+ const machineHash = getMachineHash();
127
+ const seedHash = deriveSeedHash(machineHash, answers.pw);
128
+ const result = await api.registerMachine(machineHash, seedHash, answers.label, answers.adminTk);
129
+ if (result.error) throw new Error(result.error);
130
+ spinner.succeed(chalk.green(`Machine registered: ${machineHash.slice(0,12)}...`));
131
+ console.log(chalk.green("\n✓ clauth is ready.\n"));
132
+ console.log(chalk.cyan(" clauth test — verify connection"));
133
+ console.log(chalk.cyan(" clauth status — see all services\n"));
134
+ } catch (err) {
135
+ spinner.fail(chalk.red(`Setup failed: ${err.message}`));
136
+ process.exit(1);
137
+ }
138
+ });
139
+
140
+ // ──────────────────────────────────────────────
141
+ // clauth status
142
+ // ──────────────────────────────────────────────
143
+ program
144
+ .command("status")
145
+ .description("Show all services and their state")
146
+ .option("-p, --pw <password>", "Password (or will prompt)")
147
+ .action(async (opts) => {
148
+ const auth = await getAuth(opts.pw);
149
+ const spinner = ora("Fetching service status...").start();
150
+ try {
151
+ const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp);
152
+ spinner.stop();
153
+ if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); return; }
154
+
155
+ console.log(chalk.cyan("\n🔐 clauth service status\n"));
156
+ console.log(
157
+ chalk.bold(
158
+ " " + "SERVICE".padEnd(20) + "TYPE".padEnd(12) + "STATUS".padEnd(12) +
159
+ "KEY STORED".padEnd(12) + "LAST RETRIEVED"
160
+ )
161
+ );
162
+ console.log(" " + "─".repeat(72));
163
+
164
+ for (const s of result.services || []) {
165
+ const status = s.enabled
166
+ ? chalk.green("ACTIVE".padEnd(12))
167
+ : s.vault_key
168
+ ? chalk.yellow("SUSPENDED".padEnd(12))
169
+ : chalk.gray("NO KEY".padEnd(12));
170
+ const hasKey = s.vault_key ? chalk.green("✓".padEnd(12)) : chalk.gray("—".padEnd(12));
171
+ const lastGet = s.last_retrieved
172
+ ? new Date(s.last_retrieved).toLocaleDateString()
173
+ : chalk.gray("never");
174
+
175
+ console.log(` ${s.name.padEnd(20)}${s.key_type.padEnd(12)}${status}${hasKey}${lastGet}`);
176
+ }
177
+ console.log();
178
+ } catch (err) {
179
+ spinner.fail(chalk.red(err.message));
180
+ }
181
+ });
182
+
183
+ // ──────────────────────────────────────────────
184
+ // clauth write pw <new_password>
185
+ // clauth write params
186
+ // clauth write key <service> <value>
187
+ // ──────────────────────────────────────────────
188
+ const writeCmd = program.command("write").description("Write credentials or update auth parameters");
189
+
190
+ writeCmd
191
+ .command("pw [newpw]")
192
+ .description("Set or update clauth master password")
193
+ .action(async (newpw) => {
194
+ console.log(chalk.cyan("\n🔐 clauth write pw\n"));
195
+ const current = await promptPassword("Current password (to verify)");
196
+ const pw = newpw || (await inquirer.prompt([
197
+ { type: "password", name: "p", message: "New password:", mask: "*" },
198
+ { type: "password", name: "c", message: "Confirm new password:", mask: "*" }
199
+ ]).then(a => { if (a.p !== a.c) { console.log(chalk.red("Passwords don't match")); process.exit(1); } return a.p; }));
200
+
201
+ // Re-register machine with new seed hash
202
+ const machineHash = getMachineHash();
203
+ const newSeedHash = deriveSeedHash(machineHash, pw);
204
+ const { token, timestamp } = deriveToken(current, machineHash);
205
+ const adminToken = await inquirer.prompt([{
206
+ type: "password", name: "t", message: "Admin bootstrap token (required for re-registration):", mask: "*"
207
+ }]).then(a => a.t);
208
+
209
+ const spinner = ora("Updating password and re-registering machine...").start();
210
+ try {
211
+ const result = await api.registerMachine(machineHash, newSeedHash, null, adminToken);
212
+ if (result.error) throw new Error(result.error);
213
+ spinner.succeed(chalk.green("Password updated and machine re-registered."));
214
+ } catch (err) {
215
+ spinner.fail(chalk.red(err.message));
216
+ }
217
+ });
218
+
219
+ writeCmd
220
+ .command("params")
221
+ .description("Re-read hardware fingerprint (use after hardware change)")
222
+ .action(async () => {
223
+ const spinner = ora("Reading hardware fingerprint...").start();
224
+ try {
225
+ const hash = getMachineHash();
226
+ spinner.succeed(chalk.green(`Machine hash: ${hash.slice(0,16)}...`));
227
+ console.log(chalk.gray("Full hash: " + hash));
228
+ } catch (err) {
229
+ spinner.fail(chalk.red(err.message));
230
+ }
231
+ });
232
+
233
+ writeCmd
234
+ .command("key <service> [value]")
235
+ .description("Write a credential into vault for a service")
236
+ .option("-p, --pw <password>", "Password")
237
+ .action(async (service, value, opts) => {
238
+ const auth = await getAuth(opts.pw);
239
+ let val = value;
240
+ if (!val) {
241
+ const { v } = await inquirer.prompt([{ type: "password", name: "v", message: `Value for ${service}:`, mask: "*" }]);
242
+ val = v;
243
+ }
244
+ const spinner = ora(`Writing key for ${service}...`).start();
245
+ try {
246
+ const result = await api.write(auth.password, auth.machineHash, auth.token, auth.timestamp, service, val);
247
+ if (result.error) throw new Error(result.error);
248
+ spinner.succeed(chalk.green(`Key stored in vault: auth.${service}`));
249
+ } catch (err) {
250
+ spinner.fail(chalk.red(err.message));
251
+ }
252
+ });
253
+
254
+ // ──────────────────────────────────────────────
255
+ // clauth enable <service|all>
256
+ // clauth disable <service|all>
257
+ // ──────────────────────────────────────────────
258
+ program
259
+ .command("enable <service>")
260
+ .description("Enable a service (or 'all')")
261
+ .option("-p, --pw <password>")
262
+ .action(async (service, opts) => {
263
+ const auth = await getAuth(opts.pw);
264
+ const spinner = ora(`Enabling ${service}...`).start();
265
+ try {
266
+ const result = await api.enable(auth.password, auth.machineHash, auth.token, auth.timestamp, service, true);
267
+ if (result.error) throw new Error(result.error);
268
+ spinner.succeed(chalk.green(`Enabled: ${service}`));
269
+ } catch (err) { spinner.fail(chalk.red(err.message)); }
270
+ });
271
+
272
+ program
273
+ .command("disable <service>")
274
+ .description("Disable a service (or 'all')")
275
+ .option("-p, --pw <password>")
276
+ .action(async (service, opts) => {
277
+ const auth = await getAuth(opts.pw);
278
+ const spinner = ora(`Disabling ${service}...`).start();
279
+ try {
280
+ const result = await api.enable(auth.password, auth.machineHash, auth.token, auth.timestamp, service, false);
281
+ if (result.error) throw new Error(result.error);
282
+ spinner.succeed(chalk.yellow(`Disabled: ${service}`));
283
+ } catch (err) { spinner.fail(chalk.red(err.message)); }
284
+ });
285
+
286
+ // ──────────────────────────────────────────────
287
+ // clauth add service <name>
288
+ // clauth remove service <name>
289
+ // clauth list services
290
+ // ──────────────────────────────────────────────
291
+ const addCmd = program.command("add").description("Add resources to the registry");
292
+
293
+ addCmd
294
+ .command("service <name>")
295
+ .description("Register a new service slot")
296
+ .option("--type <type>", "Key type: token | keypair | connstring | oauth")
297
+ .option("--label <label>", "Human-readable label")
298
+ .option("--description <desc>", "Description")
299
+ .option("-p, --pw <password>")
300
+ .action(async (name, opts) => {
301
+ const auth = await getAuth(opts.pw);
302
+ let answers;
303
+ if (opts.type && opts.label) {
304
+ // Non-interactive — all flags provided
305
+ answers = { label: opts.label, key_type: opts.type, desc: opts.description || "" };
306
+ } else {
307
+ answers = await inquirer.prompt([
308
+ { type: "input", name: "label", message: "Label:", default: opts.label || name },
309
+ { type: "list", name: "key_type", message: "Key type:", choices: ["token","keypair","connstring","oauth"], default: opts.type || "token" },
310
+ { type: "input", name: "desc", message: "Description (optional):", default: opts.description || "" }
311
+ ]);
312
+ }
313
+ const spinner = ora(`Adding service: ${name}...`).start();
314
+ try {
315
+ const result = await api.addService(
316
+ auth.password, auth.machineHash, auth.token, auth.timestamp,
317
+ name, answers.label, answers.key_type, answers.desc
318
+ );
319
+ if (result.error) throw new Error(result.error);
320
+ spinner.succeed(chalk.green(`Service added: ${name} (${answers.key_type})`));
321
+ console.log(chalk.gray(` Next: clauth write key ${name}`));
322
+ } catch (err) { spinner.fail(chalk.red(err.message)); }
323
+ });
324
+
325
+ const removeCmd = program.command("remove").description("Remove resources from the registry");
326
+
327
+ removeCmd
328
+ .command("service <name>")
329
+ .description("Remove a service and its key from vault")
330
+ .option("-p, --pw <password>")
331
+ .action(async (name, opts) => {
332
+ const { confirm } = await inquirer.prompt([{
333
+ type: "input", name: "confirm",
334
+ message: chalk.red(`Type "CONFIRM REMOVE ${name.toUpperCase()}" to proceed:`)
335
+ }]);
336
+ const auth = await getAuth(opts.pw);
337
+ const spinner = ora(`Removing ${name}...`).start();
338
+ try {
339
+ const result = await api.removeService(auth.password, auth.machineHash, auth.token, auth.timestamp, name, confirm);
340
+ if (result.error) throw new Error(result.error);
341
+ spinner.succeed(chalk.yellow(`Removed: ${name}`));
342
+ } catch (err) { spinner.fail(chalk.red(err.message)); }
343
+ });
344
+
345
+ program
346
+ .command("list")
347
+ .description("List all registered services")
348
+ .option("-p, --pw <password>")
349
+ .action(async (opts) => {
350
+ const auth = await getAuth(opts.pw);
351
+ const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp);
352
+ if (result.error) { console.log(chalk.red(result.error)); return; }
353
+ console.log(chalk.cyan("\n Registered services:\n"));
354
+ for (const s of result.services || []) {
355
+ console.log(` ${chalk.bold(s.name.padEnd(20))} ${chalk.gray(s.key_type.padEnd(12))} ${chalk.gray(s.description || "")}`);
356
+ }
357
+ console.log();
358
+ });
359
+
360
+ // ──────────────────────────────────────────────
361
+ // clauth test <service|all>
362
+ // ──────────────────────────────────────────────
363
+ program
364
+ .command("test [service]")
365
+ .description("Test HMAC handshake — no key returned")
366
+ .option("-p, --pw <password>")
367
+ .action(async (service, opts) => {
368
+ const auth = await getAuth(opts.pw);
369
+ const spinner = ora("Testing auth handshake...").start();
370
+ try {
371
+ const result = await api.test(auth.password, auth.machineHash, auth.token, auth.timestamp);
372
+ if (result.error) throw new Error(`${result.error}: ${result.reason}`);
373
+ spinner.succeed(chalk.green("PASS — HMAC validated"));
374
+ console.log(chalk.gray(` Machine: ${auth.machineHash.slice(0,16)}...`));
375
+ console.log(chalk.gray(` Window: ${new Date(result.timestamp).toISOString()}`));
376
+ } catch (err) {
377
+ spinner.fail(chalk.red("FAIL — " + err.message));
378
+ }
379
+ });
380
+
381
+ // ──────────────────────────────────────────────
382
+ // clauth get <service>
383
+ // ──────────────────────────────────────────────
384
+ program
385
+ .command("get <service>")
386
+ .description("Retrieve a key from vault")
387
+ .option("-p, --pw <password>")
388
+ .option("--json", "Output raw JSON")
389
+ .action(async (service, opts) => {
390
+ const auth = await getAuth(opts.pw);
391
+ const spinner = ora(`Retrieving ${service}...`).start();
392
+ try {
393
+ const result = await api.retrieve(auth.password, auth.machineHash, auth.token, auth.timestamp, service);
394
+ spinner.stop();
395
+ if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); return; }
396
+ if (opts.json) {
397
+ console.log(JSON.stringify(result, null, 2));
398
+ } else {
399
+ console.log(chalk.cyan(`\n🔑 ${service} (${result.key_type})\n`));
400
+ const val = typeof result.value === "string" ? result.value : JSON.stringify(result.value, null, 2);
401
+ console.log(val);
402
+ console.log();
403
+ }
404
+ } catch (err) {
405
+ spinner.fail(chalk.red(err.message));
406
+ }
407
+ });
408
+
409
+ // ──────────────────────────────────────────────
410
+ // clauth revoke <service|all>
411
+ // ──────────────────────────────────────────────
412
+ program
413
+ .command("revoke <service>")
414
+ .description("Delete key from vault (destructive)")
415
+ .option("-p, --pw <password>")
416
+ .action(async (service, opts) => {
417
+ const phrase = service === "all" ? "CONFIRM REVOKE ALL" : `CONFIRM REVOKE ${service.toUpperCase()}`;
418
+ const { confirm } = await inquirer.prompt([{
419
+ type: "input", name: "confirm",
420
+ message: chalk.red(`Type "${phrase}" to proceed:`)
421
+ }]);
422
+ const auth = await getAuth(opts.pw);
423
+ const spinner = ora(`Revoking ${service}...`).start();
424
+ try {
425
+ const result = await api.revoke(auth.password, auth.machineHash, auth.token, auth.timestamp, service, confirm);
426
+ if (result.error) throw new Error(result.error);
427
+ spinner.succeed(chalk.yellow(`Revoked: ${service}`));
428
+ } catch (err) { spinner.fail(chalk.red(err.message)); }
429
+ });
430
+
431
+ // ──────────────────────────────────────────────
432
+ // clauth scrub [target]
433
+ // ──────────────────────────────────────────────
434
+ program
435
+ .command("scrub [target]")
436
+ .description("Scrub credentials from Claude Code transcript logs (no auth required)")
437
+ .option("--force", "Rescrub files even if already marked clean")
438
+ .addHelpText("after", `
439
+ Examples:
440
+ clauth scrub Scrub the most recent (active) transcript
441
+ clauth scrub <file> Scrub a specific .jsonl file
442
+ clauth scrub all Scrub every transcript in ~/.claude/projects/
443
+ clauth scrub all --force Rescrub all files (ignore markers)
444
+ `)
445
+ .action(async (target, opts) => {
446
+ await runScrub(target, opts);
447
+ });
448
+
449
+ // ──────────────────────────────────────────────
450
+ // clauth --help override banner
451
+ // ──────────────────────────────────────────────
452
+ program.addHelpText("beforeAll", chalk.cyan(`
453
+ ██████╗██╗ █████╗ ██╗ ██╗████████╗██╗ ██╗
454
+ ██╔════╝██║ ██╔══██╗██║ ██║╚══██╔══╝██║ ██║
455
+ ██║ ██║ ███████║██║ ██║ ██║ ███████║
456
+ ██║ ██║ ██╔══██║██║ ██║ ██║ ██╔══██║
457
+ ╚██████╗███████╗██║ ██║╚██████╔╝ ██║ ██║ ██║
458
+ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝
459
+ v${VERSION} — LIFEAI Credential Vault
460
+ `));
461
+
462
+ // ──────────────────────────────────────────────
463
+ // clauth serve [action]
464
+ // ──────────────────────────────────────────────
465
+ program
466
+ .command("serve [action]")
467
+ .description("Manage localhost HTTP vault daemon (start|stop|restart|ping)")
468
+ .option("--port <n>", "Port (default: 52437)")
469
+ .option("-p, --pw <password>", "clauth password (optional — omit to start locked, unlock in browser)")
470
+ .option("--services <list>", "Comma-separated service whitelist (default: all)")
471
+ .option("--action <action>", "Internal: action override for daemon child")
472
+ .addHelpText("after", `
473
+ Actions:
474
+ start Start the server as a background daemon
475
+ stop Stop the running daemon
476
+ restart Stop + start
477
+ ping Check if the daemon is running
478
+ foreground Run in foreground (Ctrl+C to stop) — default if no action given
479
+ mcp Run as MCP stdio server for Claude Code (JSON-RPC over stdin/stdout)
480
+
481
+ Examples:
482
+ clauth serve start Start locked unlock at http://127.0.0.1:52437
483
+ clauth serve start -p mypass Start pre-unlocked (password in memory only)
484
+ clauth serve stop Stop the daemon
485
+ clauth serve ping Check status
486
+ clauth serve restart Restart (stays locked until browser unlock)
487
+ clauth serve start --services github,vercel
488
+ clauth serve mcp Start MCP server for Claude Code
489
+ clauth serve mcp -p mypass Start MCP server pre-unlocked
490
+ `)
491
+ .action(async (action, opts) => {
492
+ const resolvedAction = opts.action || action || "foreground";
493
+ await runServe({ ...opts, action: resolvedAction });
494
+ });
495
+
496
+ program.parse(process.argv);