@lifeaitools/clauth 0.3.1 → 0.3.6

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