@lifeaitools/clauth 1.30.2 → 1.30.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/cli/index.js CHANGED
@@ -1,1089 +1,1089 @@
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 { writeCredentialWithRecovery } from "./recovery.js";
13
- import os from "os";
14
- import fs from "fs";
15
- import path from "path";
16
-
17
- const config = new Conf(getConfOptions());
18
- const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
19
-
20
- function shellSingleQuote(value) {
21
- return `'${String(value ?? "").replace(/'/g, "''")}'`;
22
- }
23
-
24
- function enrollmentScriptName(label) {
25
- const slug = String(label || "new-computer")
26
- .toLowerCase()
27
- .replace(/[^a-z0-9]+/g, "-")
28
- .replace(/^-+|-+$/g, "")
29
- .slice(0, 40) || "new-computer";
30
- return `clauth-enroll-${slug}.ps1`;
31
- }
32
-
33
- function writeEnrollmentScript({ supabaseUrl, anonKey, enrollmentCode, label }) {
34
- const appDir = process.platform === "win32"
35
- ? path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "clauth")
36
- : path.join(os.homedir(), ".config", "clauth");
37
- fs.mkdirSync(appDir, { recursive: true });
38
- const scriptPath = path.join(appDir, enrollmentScriptName(label));
39
- const script = [
40
- "$ErrorActionPreference = 'Stop'",
41
- "$label = $env:COMPUTERNAME",
42
- "if (-not $label) { $label = [System.Net.Dns]::GetHostName() }",
43
- "Write-Host 'Installing clauth...'",
44
- "npm install -g @lifeaitools/clauth@latest",
45
- "Write-Host 'Enrolling this computer with clauth...'",
46
- [
47
- "clauth setup",
48
- `--supabase-url ${shellSingleQuote(supabaseUrl)}`,
49
- `--anon-key ${shellSingleQuote(anonKey)}`,
50
- `--enrollment-code ${shellSingleQuote(enrollmentCode)}`,
51
- "--label \"$label\"",
52
- ].join(" "),
53
- "Write-Host 'Installing clauth startup service...'",
54
- "clauth serve install",
55
- "Write-Host 'clauth enrollment complete.'",
56
- "$self = $PSCommandPath",
57
- "Start-Process powershell.exe -WindowStyle Hidden -ArgumentList @('-NoProfile','-Command',\"Start-Sleep -Seconds 2; Remove-Item -LiteralPath '$self' -Force -ErrorAction SilentlyContinue\")",
58
- ].join("\r\n");
59
- fs.writeFileSync(scriptPath, `${script}\r\n`, "utf8");
60
- return scriptPath;
61
- }
62
-
63
- // ============================================================
64
- // Password prompt helper
65
- // ============================================================
66
- async function promptPassword(message = "clauth password") {
67
- const { pw } = await inquirer.prompt([{
68
- type: "password",
69
- name: "pw",
70
- message,
71
- mask: "*",
72
- validate: v => v.length >= 8 || "Password must be at least 8 characters"
73
- }]);
74
- return pw;
75
- }
76
-
77
- // ============================================================
78
- // Auth helper — get pw + derive token
79
- // ============================================================
80
- async function getAuth(pw) {
81
- const password = pw || await promptPassword();
82
- const machineHash = getMachineHash();
83
- const { token, timestamp } = deriveToken(password, machineHash);
84
- return { password, machineHash, token, timestamp };
85
- }
86
-
87
- const ADDRESS_KEY_TYPES = new Set(["connstring", "fileserver", "oauth"]);
88
- const ADDRESS_FIELDS = new Set(["url", "uri", "host", "hostname", "server", "address", "base_url", "endpoint", "path", "root"]);
89
-
90
- function normalizeSearchText(value) {
91
- return String(value || "").toLowerCase();
92
- }
93
-
94
- function redactUrlish(value) {
95
- const text = String(value || "").trim();
96
- if (!text) return "";
97
- try {
98
- const url = new URL(text);
99
- if (url.username) url.username = "***";
100
- if (url.password) url.password = "***";
101
- return url.toString();
102
- } catch {
103
- return text.replace(/:\/\/([^:@/\s]+):([^@/\s]+)@/g, "://***:***@");
104
- }
105
- }
106
-
107
- function collectAddressHints(value, keyType) {
108
- if (!ADDRESS_KEY_TYPES.has(String(keyType || "").toLowerCase())) return [];
109
- const hints = new Set();
110
-
111
- function add(candidate) {
112
- if (candidate === undefined || candidate === null) return;
113
- const text = redactUrlish(candidate);
114
- if (text) hints.add(text);
115
- }
116
-
117
- function walk(node, fieldName = "") {
118
- if (node === undefined || node === null) return;
119
- if (typeof node === "string") {
120
- if (fieldName && ADDRESS_FIELDS.has(fieldName.toLowerCase())) add(node);
121
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(node) || /^[A-Za-z]:[\\/]/.test(node) || node.startsWith("\\\\")) add(node);
122
- return;
123
- }
124
- if (Array.isArray(node)) {
125
- for (const item of node) walk(item, fieldName);
126
- return;
127
- }
128
- if (typeof node === "object") {
129
- for (const [key, child] of Object.entries(node)) walk(child, key);
130
- }
131
- }
132
-
133
- try {
134
- walk(JSON.parse(value));
135
- } catch {
136
- walk(value);
137
- }
138
-
139
- return [...hints];
140
- }
141
-
142
- async function searchServices(auth, query, opts = {}) {
143
- const q = normalizeSearchText(query);
144
- if (!q) throw new Error("Search query is required");
145
- const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp, opts.project);
146
- if (result.error) throw new Error(result.error);
147
-
148
- const services = result.services || [];
149
- const rows = [];
150
-
151
- for (const s of services) {
152
- const fields = {
153
- name: s.name,
154
- label: s.label,
155
- project: s.project,
156
- type: s.key_type,
157
- description: s.description
158
- };
159
- const matched = Object.entries(fields)
160
- .filter(([, value]) => normalizeSearchText(value).includes(q))
161
- .map(([field]) => field);
162
-
163
- let addressHints = [];
164
- if (opts.addresses === true && ADDRESS_KEY_TYPES.has(String(s.key_type || "").toLowerCase()) && s.vault_key) {
165
- const secret = await api.retrieve(auth.password, auth.machineHash, auth.token, auth.timestamp, s.name);
166
- if (!secret.error) {
167
- addressHints = collectAddressHints(secret.value, s.key_type);
168
- if (addressHints.some(h => normalizeSearchText(h).includes(q))) matched.push("address");
169
- }
170
- }
171
-
172
- if (matched.length) rows.push({ ...s, matched: [...new Set(matched)], addressHints });
173
- }
174
-
175
- return rows;
176
- }
177
-
178
- // ============================================================
179
- // Program
180
- // ============================================================
181
- const program = new Command();
182
-
183
- program
184
- .name("clauth")
185
- .version(VERSION)
186
- .description(chalk.cyan("🔐 clauth") + " — Hardware-bound credential vault for LIFEAI infrastructure");
187
-
188
- // ──────────────────────────────────────────────
189
- // clauth install (Supabase provisioning + skill install + test)
190
- // ──────────────────────────────────────────────
191
- import { runInstall } from './commands/install.js';
192
- import { runUninstall } from './commands/uninstall.js';
193
- import { runScrub } from './commands/scrub.js';
194
- import { runServe } from './commands/serve.js';
195
- import { runCodevelop } from './commands/codevelop.js';
196
- import { runNpm, runPublish } from './commands/npm.js';
197
-
198
- program
199
- .command('install')
200
- .description('Provision Supabase, deploy Edge Function, install Claude skill')
201
- .option('--ref <ref>', 'Supabase project ref')
202
- .option('--pat <pat>', 'Supabase Personal Access Token')
203
- .action(async (opts) => {
204
- await runInstall(opts);
205
- });
206
-
207
- program
208
- .command('uninstall')
209
- .description('Full teardown — drop DB objects, Edge Function, secrets, skill, config')
210
- .option('--ref <ref>', 'Supabase project ref')
211
- .option('--pat <pat>', 'Supabase Personal Access Token (required)')
212
- .option('--yes', 'Skip confirmation prompt')
213
- .action(async (opts) => {
214
- if (!opts.yes) {
215
- const inquirerMod = await import('inquirer');
216
- const { confirm } = await inquirerMod.default.prompt([{
217
- type: 'input',
218
- name: 'confirm',
219
- message: chalk.red('Type "CONFIRM UNINSTALL" to proceed:'),
220
- }]);
221
- if (confirm !== 'CONFIRM UNINSTALL') {
222
- console.log(chalk.yellow('\n Uninstall cancelled.\n'));
223
- process.exit(0);
224
- }
225
- }
226
- await runUninstall(opts);
227
- });
228
-
229
- program
230
- .command("codevelop")
231
- .description("Install and launch Claude/Codex co-development terminal sessions")
232
- .argument("[action]", "install-terminal | start | join | say | read | watch | who | ask | reply | inbox | listen | request-partner | launch-peer | check-partner | sync | help", "help")
233
- .option("--repo <path>", "Repo root", "C:\\Dev\\regen-root")
234
- .option("--port <port>", "Isolated clauth port", "53137")
235
- .option("--base-url <url>", "Override clauth base URL")
236
- .option("--name <name>", "Session name")
237
- .option("--session <idOrManifestPath>", "Co-develop session id or manifest path")
238
- .option("--task <task>", "Initial partner request task")
239
- .option("--context <path>", "Context, plan, or architecture file the partner should read first")
240
- .option("--channel <name>", "Ad hoc channel name for join/say/read/watch")
241
- .option("--message <text>", "Ad hoc message for say")
242
- .option("--to <peer>", "Target peer for ask/reply, or optional direct ad hoc recipient for say")
243
- .option("--from <peer>", "Sender peer for ask/reply")
244
- .option("--turn <turn_id>", "Turn ID for reply")
245
- .option("--role <role>", "Turn role, e.g. reviewer or builder")
246
- .option("--skill <skill>", "Requested skill name, e.g. rdc:review")
247
- .option("--verdict <verdict>", "Reply verdict: pass, fail, or blocked")
248
- .option("--summary <summary>", "Reply summary")
249
- .option("--evidence <items>", "Reply evidence; separate multiple items with semicolons")
250
- .option("--files-changed <items>", "Reply changed files; separate multiple items with semicolons")
251
- .option("--commits <items>", "Reply commits; separate multiple items with semicolons")
252
- .option("--blockers <items>", "Reply blockers; separate multiple items with semicolons")
253
- .option("--next <items>", "Reply next actions; separate multiple items with semicolons")
254
- .option("--wait", "For ask: wait for a matching reply")
255
- .option("--once", "For listen: exit after the first message event")
256
- .option("--json", "For read: print raw JSON")
257
- .option("--timeout-ms <ms>", "Wait timeout in milliseconds", "300000")
258
- .option("--interval-ms <ms>", "Wait polling interval in milliseconds", "2000")
259
- .option("--start-isolated-clauth", "Start isolated clauth if the selected port is not running")
260
- .option("--peer <peer>", "Peer name for launch-peer/check-partner/sync, or ad hoc name such as codex-1")
261
- .option("--dry-run", "Print and write session manifest without opening Windows Terminal")
262
- .option("--no-open", "Create session/config but do not open Windows Terminal")
263
- .option("--print-only", "For launch-peer: resolve command without starting the CLI")
264
- .action(async (action, opts) => {
265
- await runCodevelop({ ...opts, action });
266
- });
267
-
268
- program
269
- .command("npm")
270
- .description("Operate npm auth safely through the clauth npm service")
271
- .argument("[action]", "whoami | tokens | set-local | sync-github-secret | rerun | help", "help")
272
- .argument("[args...]", "Action arguments")
273
- .option("--repo <repo>", "GitHub repo, e.g. LIFEAI/rdc-skills")
274
- .action(async (action, args, opts) => {
275
- await runNpm(action, { ...opts, args });
276
- });
277
-
278
- // ──────────────────────────────────────────────
279
- // clauth publish [target]
280
- // Guarded npm publish for ANY package — refuses to ship code that isn't
281
- // committed AND pushed to GitHub (prevents npm/repo divergence from dev builds).
282
- // ──────────────────────────────────────────────
283
- program
284
- .command("publish [target]")
285
- .description("Safely publish an npm package (default: cwd). Refuses unless committed + pushed to GitHub.")
286
- .option("--dry-run", "Run all guards and pack, but do not publish")
287
- .option("--access <access>", "npm access: public | restricted")
288
- .option("--allow-dirty", "Override the uncommitted-changes guard (NOT recommended)")
289
- .option("--allow-unpushed", "Override the not-pushed-to-remote guard (NOT recommended)")
290
- .action(async (target, opts) => {
291
- try {
292
- await runPublish(target, opts);
293
- } catch (err) {
294
- console.error(chalk.red(err.message));
295
- process.exitCode = 1;
296
- }
297
- });
298
-
299
- // ──────────────────────────────────────────────
300
- // clauth setup
301
- // ──────────────────────────────────────────────
302
- program
303
- .command("setup")
304
- .description("Register this machine with the vault (run after clauth install)")
305
- .option("--admin-token <token>", "Bootstrap token (from clauth install output)")
306
- .option("--enrollment-code <code>", "One-time enrollment code from clauth enroll")
307
- .option("--supabase-url <url>", "Vault Supabase URL, for enrolling a new computer without running clauth install")
308
- .option("--anon-key <key>", "Vault Supabase anon key, for enrolling a new computer without running clauth install")
309
- .option("--install-id <id>", "Logical install/owner group for admin-token setup", "default")
310
- .option("--label <label>", "Human label for this machine")
311
- .option("-p, --pw <password>", "Password (skip interactive prompt)")
312
- .action(async (opts) => {
313
- console.log(chalk.cyan("\n🔐 clauth setup\n"));
314
-
315
- if (opts.supabaseUrl) config.set("supabase_url", opts.supabaseUrl);
316
- if (opts.anonKey) config.set("supabase_anon_key", opts.anonKey);
317
-
318
- // URL + anon key may already be saved by clauth install, or provided by
319
- // an old-machine enrollment command.
320
- let savedUrl = config.get("supabase_url");
321
- let savedAnon = config.get("supabase_anon_key");
322
- if (!savedUrl || !savedAnon) {
323
- const configAnswers = await inquirer.prompt([
324
- { type: "input", name: "supabaseUrl", message: "Vault Supabase URL:", default: savedUrl || opts.supabaseUrl || "" },
325
- { type: "password", name: "anonKey", message: "Vault anon key:", mask: "*", default: savedAnon || opts.anonKey || "" },
326
- ]);
327
- if (!configAnswers.supabaseUrl || !configAnswers.anonKey) {
328
- console.log(chalk.yellow(" Supabase config not found. Run clauth install first, or provide --supabase-url and --anon-key.\n"));
329
- process.exit(1);
330
- }
331
- config.set("supabase_url", configAnswers.supabaseUrl);
332
- config.set("supabase_anon_key", configAnswers.anonKey);
333
- savedUrl = configAnswers.supabaseUrl;
334
- savedAnon = configAnswers.anonKey;
335
- }
336
- console.log(chalk.gray(` Project: ${savedUrl}\n`));
337
-
338
- let answers;
339
- if (opts.pw && (opts.adminToken || opts.enrollmentCode)) {
340
- // Non-interactive mode — all flags provided
341
- answers = {
342
- label: opts.label || os.hostname(),
343
- pw: opts.pw,
344
- adminTk: opts.adminToken,
345
- enrollmentCode: opts.enrollmentCode,
346
- };
347
- } else if (opts.enrollmentCode) {
348
- const pw = opts.pw || await promptPassword("Set clauth password for this computer");
349
- answers = {
350
- label: opts.label || os.hostname(),
351
- pw,
352
- adminTk: opts.adminToken,
353
- enrollmentCode: opts.enrollmentCode,
354
- };
355
- } else {
356
- answers = await inquirer.prompt([
357
- { type: "input", name: "label", message: "Machine label:", default: opts.label || os.hostname() },
358
- { type: "password", name: "pw", message: "Set password:", mask: "*", default: opts.pw || "" },
359
- { type: "password", name: "enrollmentCode", message: "Enrollment code (preferred for new computer; leave blank if using bootstrap token):", mask: "*",
360
- default: opts.enrollmentCode || "" },
361
- { type: "password", name: "adminTk", message: "Bootstrap token (admin fallback):", mask: "*",
362
- default: opts.adminToken || "" },
363
- ]);
364
- }
365
-
366
- const spinner = ora("Registering machine with vault...").start();
367
- try {
368
- const machineHash = getMachineHash();
369
- const seedHash = deriveSeedHash(machineHash, answers.pw);
370
- const result = answers.enrollmentCode
371
- ? await api.redeemEnrollment(machineHash, seedHash, answers.label, answers.enrollmentCode)
372
- : await api.registerMachine(machineHash, seedHash, answers.label, answers.adminTk, { install_id: opts.installId || "default" });
373
- if (result.error) throw new Error(result.error);
374
- spinner.succeed(chalk.green(`Machine registered: ${machineHash.slice(0,12)}... install_id=${result.install_id || opts.installId || "default"}`));
375
-
376
- console.log(chalk.green("\n✓ clauth is ready.\n"));
377
- console.log(chalk.cyan(" clauth test — verify connection"));
378
- console.log(chalk.cyan(" clauth status — see all services\n"));
379
- } catch (err) {
380
- spinner.fail(chalk.red(`Setup failed: ${err.message}`));
381
- process.exit(1);
382
- }
383
- });
384
-
385
- // ──────────────────────────────────────────────
386
- // clauth enroll
387
- // ──────────────────────────────────────────────
388
- program
389
- .command("enroll")
390
- .description("Create a one-time enrollment code for adding another computer")
391
- .option("--label <label>", "Suggested label for the new computer")
392
- .option("--ttl-minutes <minutes>", "Enrollment lifetime, 5 to 1440 minutes", "60")
393
- .option("--install-id <id>", "Override install id; default is current machine's install id")
394
- .option("-p, --pw <password>", "Password (or will prompt)")
395
- .action(async (opts) => {
396
- console.log(chalk.cyan("\n🔐 clauth enroll\n"));
397
- const auth = await getAuth(opts.pw);
398
- const spinner = ora("Creating one-time machine enrollment...").start();
399
- try {
400
- const result = await api.createEnrollment(
401
- auth.password,
402
- auth.machineHash,
403
- auth.token,
404
- auth.timestamp,
405
- opts.label,
406
- Number(opts.ttlMinutes || 60),
407
- opts.installId
408
- );
409
- if (result.error) throw new Error(result.error);
410
- const supabaseUrl = config.get("supabase_url");
411
- const anonKey = config.get("supabase_anon_key");
412
- const scriptPath = writeEnrollmentScript({
413
- supabaseUrl,
414
- anonKey,
415
- enrollmentCode: result.enrollment_code,
416
- label: opts.label,
417
- });
418
- spinner.succeed(chalk.green(`Enrollment created for install_id=${result.install_id}`));
419
- console.log("");
420
- console.log(chalk.bold(" Enrollment code:"));
421
- console.log(chalk.white(` ${result.enrollment_code}`));
422
- console.log("");
423
- console.log(chalk.bold(" On the new computer:"));
424
- console.log(chalk.gray(` Run this one-time script: ${scriptPath}`));
425
- console.log(chalk.gray(" It installs clauth, enrolls with this code, installs startup, then deletes itself."));
426
- console.log("");
427
- console.log(chalk.gray(` Expires: ${result.expires_at}`));
428
- } catch (err) {
429
- spinner.fail(chalk.red(`Enroll failed: ${err.message}`));
430
- process.exitCode = 1;
431
- }
432
- });
433
-
434
- // ──────────────────────────────────────────────
435
- // clauth status
436
- // ──────────────────────────────────────────────
437
- program
438
- .command("status")
439
- .description("Show all services and their state")
440
- .option("-p, --pw <password>", "Password (or will prompt)")
441
- .option("--project <name>", "Filter by project scope")
442
- .action(async (opts) => {
443
- const auth = await getAuth(opts.pw);
444
- const spinner = ora("Fetching service status...").start();
445
- try {
446
- const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp, opts.project);
447
- spinner.stop();
448
- if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); return; }
449
-
450
- const heading = opts.project ? `clauth service status (project: ${opts.project})` : "clauth service status";
451
- console.log(chalk.cyan(`\n🔐 ${heading}\n`));
452
- console.log(
453
- chalk.bold(
454
- " " + "SERVICE".padEnd(24) + "TYPE".padEnd(12) + "PROJECT".padEnd(22) + "STATUS".padEnd(12) +
455
- "KEY STORED".padEnd(12) + "LAST RETRIEVED"
456
- )
457
- );
458
- console.log(" " + "─".repeat(90));
459
-
460
- for (const s of result.services || []) {
461
- const status = s.enabled
462
- ? chalk.green("ACTIVE".padEnd(12))
463
- : s.vault_key
464
- ? chalk.yellow("SUSPENDED".padEnd(12))
465
- : chalk.gray("NO KEY".padEnd(12));
466
- const hasKey = s.vault_key ? chalk.green("✓".padEnd(12)) : chalk.gray("—".padEnd(12));
467
- const lastGet = s.last_retrieved
468
- ? new Date(s.last_retrieved).toLocaleDateString()
469
- : chalk.gray("never");
470
- const proj = s.project ? chalk.blue(s.project.padEnd(22)) : chalk.gray("—".padEnd(22));
471
-
472
- console.log(` ${s.name.padEnd(24)}${s.key_type.padEnd(12)}${proj}${status}${hasKey}${lastGet}`);
473
- }
474
- console.log();
475
- } catch (err) {
476
- spinner.fail(chalk.red(err.message));
477
- }
478
- });
479
-
480
- // ──────────────────────────────────────────────
481
- // clauth write pw <new_password>
482
- // clauth write params
483
- // clauth write key <service> <value>
484
- // ──────────────────────────────────────────────
485
- const writeCmd = program.command("write").description("Write credentials or update auth parameters");
486
-
487
- writeCmd
488
- .command("pw [newpw]")
489
- .description("Set or update clauth master password")
490
- .action(async (newpw) => {
491
- console.log(chalk.cyan("\n🔐 clauth write pw\n"));
492
- const current = await promptPassword("Current password (to verify)");
493
- const pw = newpw || (await inquirer.prompt([
494
- { type: "password", name: "p", message: "New password:", mask: "*" },
495
- { type: "password", name: "c", message: "Confirm new password:", mask: "*" }
496
- ]).then(a => { if (a.p !== a.c) { console.log(chalk.red("Passwords don't match")); process.exit(1); } return a.p; }));
497
-
498
- // Re-register machine with new seed hash
499
- const machineHash = getMachineHash();
500
- const newSeedHash = deriveSeedHash(machineHash, pw);
501
- const { token, timestamp } = deriveToken(current, machineHash);
502
- const adminToken = await inquirer.prompt([{
503
- type: "password", name: "t", message: "Admin bootstrap token (required for re-registration):", mask: "*"
504
- }]).then(a => a.t);
505
-
506
- const spinner = ora("Updating password and re-registering machine...").start();
507
- try {
508
- const result = await api.registerMachine(machineHash, newSeedHash, null, adminToken);
509
- if (result.error) throw new Error(result.error);
510
- spinner.succeed(chalk.green("Password updated and machine re-registered."));
511
- } catch (err) {
512
- spinner.fail(chalk.red(err.message));
513
- }
514
- });
515
-
516
- writeCmd
517
- .command("params")
518
- .description("Re-read hardware fingerprint (use after hardware change)")
519
- .action(async () => {
520
- const spinner = ora("Reading hardware fingerprint...").start();
521
- try {
522
- const hash = getMachineHash();
523
- spinner.succeed(chalk.green(`Machine hash: ${hash.slice(0,16)}...`));
524
- console.log(chalk.gray("Full hash: " + hash));
525
- } catch (err) {
526
- spinner.fail(chalk.red(err.message));
527
- }
528
- });
529
-
530
- writeCmd
531
- .command("key <service> [value]")
532
- .description("Write a credential into vault for a service")
533
- .option("-p, --pw <password>", "Password")
534
- .action(async (service, value, opts) => {
535
- const auth = await getAuth(opts.pw);
536
- let val = value;
537
- if (!val) {
538
- const { v } = await inquirer.prompt([{ type: "password", name: "v", message: `Value for ${service}:`, mask: "*" }]);
539
- val = v;
540
- }
541
- const spinner = ora(`Writing key for ${service}...`).start();
542
- try {
543
- const { result, snapshot, normalized } = await writeCredentialWithRecovery({
544
- password: auth.password,
545
- machineHash: auth.machineHash,
546
- service,
547
- value: val,
548
- });
549
- if (result.error) throw new Error(result.error);
550
- const details = [
551
- snapshot?.ok ? "recovery snapshot written" : null,
552
- normalized ? "value normalized" : null,
553
- ].filter(Boolean);
554
- spinner.succeed(chalk.green(`Key stored in vault: auth.${service}${details.length ? ` (${details.join(", ")})` : ""}`));
555
- } catch (err) {
556
- spinner.fail(chalk.red(err.message));
557
- }
558
- });
559
-
560
- // ──────────────────────────────────────────────
561
- // clauth enable <service|all>
562
- // clauth disable <service|all>
563
- // ──────────────────────────────────────────────
564
- program
565
- .command("enable <service>")
566
- .description("Enable a service (or 'all')")
567
- .option("-p, --pw <password>")
568
- .action(async (service, opts) => {
569
- const auth = await getAuth(opts.pw);
570
- const spinner = ora(`Enabling ${service}...`).start();
571
- try {
572
- const result = await api.enable(auth.password, auth.machineHash, auth.token, auth.timestamp, service, true);
573
- if (result.error) throw new Error(result.error);
574
- spinner.succeed(chalk.green(`Enabled: ${service}`));
575
- } catch (err) { spinner.fail(chalk.red(err.message)); }
576
- });
577
-
578
- program
579
- .command("disable <service>")
580
- .description("Disable a service (or 'all')")
581
- .option("-p, --pw <password>")
582
- .action(async (service, opts) => {
583
- const auth = await getAuth(opts.pw);
584
- const spinner = ora(`Disabling ${service}...`).start();
585
- try {
586
- const result = await api.enable(auth.password, auth.machineHash, auth.token, auth.timestamp, service, false);
587
- if (result.error) throw new Error(result.error);
588
- spinner.succeed(chalk.yellow(`Disabled: ${service}`));
589
- } catch (err) { spinner.fail(chalk.red(err.message)); }
590
- });
591
-
592
- // ──────────────────────────────────────────────
593
- // clauth add service <name>
594
- // clauth remove service <name>
595
- // clauth list services
596
- // ──────────────────────────────────────────────
597
- const addCmd = program.command("add").description("Add resources to the registry");
598
-
599
- addCmd
600
- .command("service <name>")
601
- .description("Register a new service slot")
602
- .option("--type <type>", "Key type: token | keypair | connstring | oauth")
603
- .option("--label <label>", "Human-readable label")
604
- .option("--description <desc>", "Description")
605
- .option("--project <project>", "Project scope (groups related services)")
606
- .option("-p, --pw <password>")
607
- .action(async (name, opts) => {
608
- const auth = await getAuth(opts.pw);
609
- let answers;
610
- if (opts.type && opts.label) {
611
- // Non-interactive — all flags provided
612
- answers = { label: opts.label, key_type: opts.type, desc: opts.description || "" };
613
- } else {
614
- answers = await inquirer.prompt([
615
- { type: "input", name: "label", message: "Label:", default: opts.label || name },
616
- { type: "list", name: "key_type", message: "Key type:", choices: ["token","keypair","connstring","oauth"], default: opts.type || "token" },
617
- { type: "input", name: "desc", message: "Description (optional):", default: opts.description || "" }
618
- ]);
619
- }
620
- const spinner = ora(`Adding service: ${name}${opts.project ? ` (project: ${opts.project})` : ""}...`).start();
621
- try {
622
- const result = await api.addService(
623
- auth.password, auth.machineHash, auth.token, auth.timestamp,
624
- name, answers.label, answers.key_type, answers.desc, opts.project
625
- );
626
- if (result.error) throw new Error(result.error);
627
- spinner.succeed(chalk.green(`Service added: ${name} (${answers.key_type})${opts.project ? chalk.blue(` [${opts.project}]`) : ""}`));
628
- console.log(chalk.gray(` Next: clauth write key ${name}`));
629
- } catch (err) { spinner.fail(chalk.red(err.message)); }
630
- });
631
-
632
- const removeCmd = program.command("remove").description("Remove resources from the registry");
633
-
634
- removeCmd
635
- .command("service <name>")
636
- .description("Remove a service and its key from vault")
637
- .option("-p, --pw <password>")
638
- .action(async (name, opts) => {
639
- const { confirm } = await inquirer.prompt([{
640
- type: "input", name: "confirm",
641
- message: chalk.red(`Type "CONFIRM REMOVE ${name.toUpperCase()}" to proceed:`)
642
- }]);
643
- const auth = await getAuth(opts.pw);
644
- const spinner = ora(`Removing ${name}...`).start();
645
- try {
646
- const result = await api.removeService(auth.password, auth.machineHash, auth.token, auth.timestamp, name, confirm);
647
- if (result.error) throw new Error(result.error);
648
- spinner.succeed(chalk.yellow(`Removed: ${name}`));
649
- } catch (err) { spinner.fail(chalk.red(err.message)); }
650
- });
651
-
652
- program
653
- .command("list")
654
- .description("List all registered services")
655
- .option("-p, --pw <password>")
656
- .option("--project <name>", "Filter by project scope")
657
- .action(async (opts) => {
658
- const auth = await getAuth(opts.pw);
659
- const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp, opts.project);
660
- if (result.error) { console.log(chalk.red(result.error)); return; }
661
- const heading = opts.project ? `Registered services (project: ${opts.project})` : "Registered services";
662
- console.log(chalk.cyan(`\n ${heading}:\n`));
663
- let lastProject = undefined;
664
- for (const s of result.services || []) {
665
- const proj = s.project || null;
666
- if (proj !== lastProject) {
667
- if (lastProject !== undefined) console.log();
668
- console.log(chalk.gray(` [${proj || "global"}]`));
669
- lastProject = proj;
670
- }
671
- console.log(` ${chalk.bold(s.name.padEnd(24))} ${chalk.gray(s.key_type.padEnd(12))} ${chalk.gray(s.label || "")}`);
672
- }
673
- console.log();
674
- });
675
-
676
- program
677
- .command("search <query>")
678
- .description("Search services by name, label, project, description, or type")
679
- .option("-p, --pw <password>")
680
- .option("--project <name>", "Filter by project scope")
681
- .option("--addresses", "Also search redacted address hints from address-bearing secrets (may retrieve multiple secrets)")
682
- .action(async (query, opts) => {
683
- const auth = await getAuth(opts.pw);
684
- const spinner = ora("Searching services...").start();
685
- try {
686
- const rows = await searchServices(auth, query, { project: opts.project, addresses: opts.addresses === true });
687
- spinner.stop();
688
- console.log(chalk.cyan(`\n Search results for "${query}":\n`));
689
- if (!rows.length) {
690
- console.log(chalk.gray(" No matching services found.\n"));
691
- return;
692
- }
693
- console.log(chalk.bold(" " + "SERVICE".padEnd(24) + "TYPE".padEnd(12) + "PROJECT".padEnd(20) + "MATCHED"));
694
- console.log(" " + "─".repeat(78));
695
- for (const s of rows) {
696
- const project = s.project || "global";
697
- console.log(` ${chalk.bold(s.name.padEnd(24))}${String(s.key_type || "").padEnd(12)}${project.padEnd(20)}${s.matched.join(", ")}`);
698
- if (s.label) console.log(chalk.gray(` label: ${s.label}`));
699
- if (s.description) console.log(chalk.gray(` description: ${s.description}`));
700
- for (const hint of s.addressHints || []) console.log(chalk.gray(` address: ${hint}`));
701
- }
702
- console.log();
703
- } catch (err) {
704
- spinner.fail(chalk.red(err.message));
705
- }
706
- });
707
-
708
- // ──────────────────────────────────────────────
709
- // clauth test <service|all>
710
- // ──────────────────────────────────────────────
711
- program
712
- .command("test [service]")
713
- .description("Test HMAC handshake — no key returned")
714
- .option("-p, --pw <password>")
715
- .action(async (service, opts) => {
716
- const auth = await getAuth(opts.pw);
717
- const spinner = ora("Testing auth handshake...").start();
718
- try {
719
- const result = await api.test(auth.password, auth.machineHash, auth.token, auth.timestamp);
720
- if (result.error) throw new Error(`${result.error}: ${result.reason}`);
721
- spinner.succeed(chalk.green("PASS — HMAC validated"));
722
- console.log(chalk.gray(` Machine: ${auth.machineHash.slice(0,16)}...`));
723
- console.log(chalk.gray(` Window: ${new Date(result.timestamp).toISOString()}`));
724
- } catch (err) {
725
- spinner.fail(chalk.red("FAIL — " + err.message));
726
- }
727
- });
728
-
729
- // ──────────────────────────────────────────────
730
- // clauth get <service>
731
- // ──────────────────────────────────────────────
732
- program
733
- .command("get <service>")
734
- .description("Retrieve a key from vault")
735
- .option("-p, --pw <password>")
736
- .option("--json", "Output raw JSON")
737
- .action(async (service, opts) => {
738
- const auth = await getAuth(opts.pw);
739
- const spinner = ora(`Retrieving ${service}...`).start();
740
- try {
741
- const result = await api.retrieve(auth.password, auth.machineHash, auth.token, auth.timestamp, service);
742
- spinner.stop();
743
- if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); return; }
744
- if (opts.json) {
745
- console.log(JSON.stringify(result, null, 2));
746
- } else {
747
- console.log(chalk.cyan(`\n🔑 ${service} (${result.key_type})\n`));
748
- const val = typeof result.value === "string" ? result.value : JSON.stringify(result.value, null, 2);
749
- console.log(val);
750
- console.log();
751
- }
752
- } catch (err) {
753
- spinner.fail(chalk.red(err.message));
754
- }
755
- });
756
-
757
- // ──────────────────────────────────────────────
758
- // clauth revoke <service|all>
759
- // ──────────────────────────────────────────────
760
- program
761
- .command("revoke <service>")
762
- .description("Delete key from vault (destructive)")
763
- .option("-p, --pw <password>")
764
- .action(async (service, opts) => {
765
- const phrase = service === "all" ? "CONFIRM REVOKE ALL" : `CONFIRM REVOKE ${service.toUpperCase()}`;
766
- const { confirm } = await inquirer.prompt([{
767
- type: "input", name: "confirm",
768
- message: chalk.red(`Type "${phrase}" to proceed:`)
769
- }]);
770
- const auth = await getAuth(opts.pw);
771
- const spinner = ora(`Revoking ${service}...`).start();
772
- try {
773
- const result = await api.revoke(auth.password, auth.machineHash, auth.token, auth.timestamp, service, confirm);
774
- if (result.error) throw new Error(result.error);
775
- spinner.succeed(chalk.yellow(`Revoked: ${service}`));
776
- } catch (err) { spinner.fail(chalk.red(err.message)); }
777
- });
778
-
779
- // ──────────────────────────────────────────────
780
- // clauth scrub [target]
781
- // ──────────────────────────────────────────────
782
- program
783
- .command("scrub [target]")
784
- .description("Scrub credentials from Claude Code transcript logs (no auth required)")
785
- .option("--force", "Rescrub files even if already marked clean")
786
- .addHelpText("after", `
787
- Examples:
788
- clauth scrub Scrub the most recent (active) transcript
789
- clauth scrub <file> Scrub a specific file
790
- clauth scrub all Scrub every transcript + tool-result sidecar (.jsonl + .txt)
791
- clauth scrub all --force Rescrub all files (ignore markers)
792
- clauth scrub session Scrub ONLY the ending session (transcript + sidecars); reads SessionEnd hook JSON on stdin
793
-
794
- Redacts: built-in token patterns, your ~/.clauth/scrub-patterns.json,
795
- and this machine's live vault values (best-effort via the daemon).
796
- `)
797
- .action(async (target, opts) => {
798
- await runScrub(target, opts);
799
- });
800
-
801
- // ──────────────────────────────────────────────
802
- // clauth watchdog
803
- // ──────────────────────────────────────────────
804
- program
805
- .command("watchdog [action] [args...]")
806
- .description("Manage auto-restart watchdog (install|uninstall|status|start|register|list|events|restart)")
807
- .option("--manifest <path>", "Watchdog service manifest for register")
808
- .option("--service <id>", "Watchdog service id for restart")
809
- .option("--limit <n>", "Event count for events", "100")
810
- .action(async (action, args, opts) => {
811
- const { runWatchdog } = await import("./commands/watchdog.js");
812
- await runWatchdog(action, { ...opts, args });
813
- });
814
-
815
- // clauth doctor
816
- // ──────────────────────────────────────────────
817
- program
818
- .command("doctor")
819
- .description("Check all prerequisites and diagnose issues")
820
- .action(async () => {
821
- const { runDoctor } = await import("./commands/doctor.js");
822
- await runDoctor();
823
- });
824
-
825
- // ──────────────────────────────────────────────
826
- // clauth invite generate|list|revoke
827
- // ──────────────────────────────────────────────
828
- const invite = program.command("invite").description("Manage vault invites");
829
-
830
- invite
831
- .command("generate")
832
- .description("Generate an invite code for a friend")
833
- .option("--uses <n>", "Max redemptions", "1")
834
- .option("--expires <hours>", "Expiry in hours", "168")
835
- .action(async (opts) => {
836
- const { runInvite } = await import("./commands/invite.js");
837
- await runInvite("generate", opts);
838
- });
839
-
840
- invite
841
- .command("list")
842
- .description("List active invites")
843
- .action(async () => {
844
- const { runInvite } = await import("./commands/invite.js");
845
- await runInvite("list", {});
846
- });
847
-
848
- invite
849
- .command("revoke <code>")
850
- .description("Revoke an invite code")
851
- .action(async (code) => {
852
- const { runInvite } = await import("./commands/invite.js");
853
- await runInvite("revoke", { code });
854
- });
855
-
856
- // ──────────────────────────────────────────────
857
- // clauth join <invite-code>
858
- // ──────────────────────────────────────────────
859
- program
860
- .command("join <invite-code>")
861
- .description("Join a vault using an invite code from a friend")
862
- .action(async (code) => {
863
- const { runJoin } = await import("./commands/join.js");
864
- await runJoin(code);
865
- });
866
-
867
- // ──────────────────────────────────────────────
868
- // clauth update
869
- // ──────────────────────────────────────────────
870
- program
871
- .command("update")
872
- .description("Update clauth to the latest version")
873
- .action(async () => {
874
- const { execSync } = await import("child_process");
875
- console.log(chalk.cyan("\n Updating clauth...\n"));
876
- try {
877
- execSync("npm install -g @lifeaitools/clauth@latest", { stdio: "inherit" });
878
- console.log(chalk.green("\n Updated successfully.\n"));
879
- } catch (err) {
880
- console.log(chalk.red(`\n Update failed: ${err.message}\n`));
881
- }
882
- });
883
-
884
- // ──────────────────────────────────────────────
885
- // clauth tunnel start|stop|status
886
- // (setup moved to in-browser wizard at http://127.0.0.1:52437)
887
- // ──────────────────────────────────────────────
888
- const tunnelCmd = program.command("tunnel").description("Manage Cloudflare tunnel for claude.ai web integration");
889
-
890
- tunnelCmd
891
- .command("setup")
892
- .description("Open the tunnel setup wizard in your browser")
893
- .action(async () => {
894
- console.log(chalk.cyan("\n Tunnel setup is now handled in the browser.\n"));
895
- console.log(chalk.white(" 1. Start the daemon: clauth serve start"));
896
- console.log(chalk.white(" 2. Open: http://127.0.0.1:52437"));
897
- console.log(chalk.white(" 3. Unlock the vault and click \"Setup Tunnel\"\n"));
898
- });
899
-
900
- tunnelCmd
901
- .command("start")
902
- .description("Tell daemon to start the tunnel")
903
- .action(async () => {
904
- try {
905
- const r = await fetch("http://127.0.0.1:52437/tunnel/start", {
906
- method: "POST",
907
- headers: { "Content-Type": "application/json" },
908
- signal: AbortSignal.timeout(5000),
909
- });
910
- const data = await r.json().catch(() => ({}));
911
- if (!r.ok) {
912
- console.error(` ✗ ${data.error || r.statusText}`);
913
- if (r.status === 401) console.error(" Unlock the daemon first: http://127.0.0.1:52437");
914
- process.exit(1);
915
- }
916
- console.log(` ✓ ${data.message || "Tunnel starting — check status with: clauth tunnel status"}`);
917
- } catch (e) {
918
- console.error(" ✗ Daemon not running. Start it with: clauth serve");
919
- process.exit(1);
920
- }
921
- });
922
-
923
- tunnelCmd
924
- .command("stop")
925
- .description("Tell daemon to stop the tunnel")
926
- .action(async () => {
927
- try {
928
- const r = await fetch("http://127.0.0.1:52437/tunnel/stop", {
929
- method: "POST",
930
- headers: { "Content-Type": "application/json" },
931
- signal: AbortSignal.timeout(5000),
932
- });
933
- const data = await r.json().catch(() => ({}));
934
- if (!r.ok) {
935
- console.error(` ✗ ${data.error || r.statusText}`);
936
- process.exit(1);
937
- }
938
- console.log(" ✓ Tunnel stopped.");
939
- } catch (e) {
940
- console.error(" ✗ Daemon not running.");
941
- process.exit(1);
942
- }
943
- });
944
-
945
- tunnelCmd
946
- .command("status")
947
- .description("Show current tunnel status")
948
- .action(async () => {
949
- try {
950
- const r = await fetch("http://127.0.0.1:52437/tunnel", {
951
- signal: AbortSignal.timeout(5000),
952
- });
953
- const data = await r.json().catch(() => ({}));
954
- const icons = {
955
- live: "✓", starting: "◌", not_configured: "⚠",
956
- not_started: "○", error: "✗", missing_cloudflared: "✗",
957
- };
958
- const labels = {
959
- live: `Live — ${data.url || ""}`,
960
- starting: "Starting...",
961
- not_configured: "Not configured — open http://127.0.0.1:52437 and click Setup Tunnel",
962
- not_started: "Not started — run: clauth tunnel start",
963
- error: `Error${data.error ? ": " + data.error : ""} — check cloudflared config`,
964
- missing_cloudflared: "cloudflared not installed — https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/",
965
- };
966
- const status = data.status || "unknown";
967
- console.log(`\n ${icons[status] || "?"} Tunnel: ${labels[status] || status}\n`);
968
- } catch (e) {
969
- console.error(" ✗ Daemon not running. Start it with: clauth serve");
970
- process.exit(1);
971
- }
972
- });
973
-
974
- // ──────────────────────────────────────────────
975
- // clauth chitchat --session <id>
976
- // ──────────────────────────────────────────────
977
- program
978
- .command("chitchat")
979
- .description("Join a chitchat collab session — auto-starts /rdc:collab via claude -p")
980
- .requiredOption("--session <id>", "Session ID from claude.ai")
981
- .action(async (opts) => {
982
- const id = opts.session;
983
- // Verify session exists in the daemon
984
- try {
985
- const r = await fetch(`http://127.0.0.1:52437/chitchat/${id}`, { signal: AbortSignal.timeout(3000) });
986
- if (!r.ok) {
987
- console.error(`\n ✗ Session ${id} not found (daemon returned ${r.status})\n`);
988
- process.exit(1);
989
- }
990
- } catch (e) {
991
- console.error(`\n ✗ Daemon not reachable at http://127.0.0.1:52437 — is clauth running?\n`);
992
- process.exit(1);
993
- }
994
- console.log(chalk.cyan(`\n [collab] session ${id} — starting /rdc:collab...\n`));
995
-
996
- // Find claude binary (same candidates as daemon)
997
- const { execSync: es, spawn } = await import("child_process");
998
- let claudeBin = null;
999
- for (const c of [
1000
- process.env.CLAUDE_BIN,
1001
- path.join(process.env.APPDATA || '', 'npm', 'claude.cmd'),
1002
- path.join(process.env.APPDATA || '', 'npm', 'claude'),
1003
- 'claude',
1004
- ].filter(Boolean)) {
1005
- try { es(`"${c}" --version`, { stdio: 'ignore', timeout: 3000 }); claudeBin = c; break; } catch {}
1006
- }
1007
- if (!claudeBin) {
1008
- console.error(' ✗ claude CLI not found — is @anthropic-ai/claude-code installed globally?');
1009
- process.exit(1);
1010
- }
1011
-
1012
- // Auto-invoke /rdc:collab skill with streaming output to this terminal
1013
- const proc = spawn(claudeBin, [
1014
- '-p', `/rdc:collab --session ${id}`,
1015
- '--dangerously-skip-permissions',
1016
- ], {
1017
- stdio: 'inherit',
1018
- cwd: 'C:/Dev/regen-root',
1019
- shell: true,
1020
- });
1021
- proc.on('error', e => { console.error(` ✗ spawn error: ${e.message}`); process.exit(1); });
1022
- proc.on('exit', code => process.exit(code ?? 0));
1023
- });
1024
-
1025
- // ──────────────────────────────────────────────
1026
- // clauth --help override banner
1027
- // ──────────────────────────────────────────────
1028
- program.addHelpText("beforeAll", chalk.cyan(`
1029
- ██████╗██╗ █████╗ ██╗ ██╗████████╗██╗ ██╗
1030
- ██╔════╝██║ ██╔══██╗██║ ██║╚══██╔══╝██║ ██║
1031
- ██║ ██║ ███████║██║ ██║ ██║ ███████║
1032
- ██║ ██║ ██╔══██║██║ ██║ ██║ ██╔══██║
1033
- ╚██████╗███████╗██║ ██║╚██████╔╝ ██║ ██║ ██║
1034
- ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝
1035
- v${VERSION} — LIFEAI Credential Vault
1036
- `));
1037
-
1038
- // ──────────────────────────────────────────────
1039
- // clauth serve [action]
1040
- // ──────────────────────────────────────────────
1041
- program
1042
- .command("serve [action]")
1043
- .description("Manage localhost HTTP vault daemon (start|stop|restart|ping|install|uninstall)")
1044
- .option("--port <n>", "Port (default: 52437)")
1045
- .option("-p, --pw <password>", "clauth password (optional — omit to start locked, unlock in browser)")
1046
- .option("--services <list>", "Comma-separated service whitelist (default: all)")
1047
- .option("--tunnel <hostname>", "Fixed tunnel hostname (e.g. clauth.prtrust.fund) — uses named Cloudflare Tunnel instead of random URL")
1048
- .option("--staged", "Start on staging port (52438) for blue-green verification before make-live")
1049
- .option("--isolated", "Run on a non-live port without touching live PID files, browser, or boot-key credentials")
1050
- .option("--from-boot-key", "Internal: password came from boot.key auto-unlock (degrade gracefully on verify failure)")
1051
- .option("--action <action>", "Internal: action override for daemon child")
1052
- .addHelpText("after", `
1053
- Actions:
1054
- start Start the server as a background daemon
1055
- stop Stop the running daemon
1056
- restart Stop + start
1057
- ping Check if the daemon is running
1058
- foreground Run in foreground (Ctrl+C to stop) — default if no action given
1059
- mcp Run as MCP stdio server for Claude Code (JSON-RPC over stdin/stdout)
1060
- install Store password securely + register auto-start service (cross-platform)
1061
- Windows: DPAPI + HKCU\\Run | macOS: Keychain + LaunchAgent | Linux: libsecret/openssl + systemd
1062
- uninstall Remove auto-start service + delete stored password
1063
- upgrade Blue-green upgrade: start new version on staging port, verify, then make live
1064
-
1065
- MCP SSE (built into start/foreground):
1066
- The HTTP daemon also serves MCP SSE transport at GET /sse + POST /message.
1067
- Connect claude.ai via Cloudflare Tunnel pointing to http://127.0.0.1:52437/sse
1068
-
1069
- Examples:
1070
- clauth serve start Start locked — unlock at http://127.0.0.1:52437
1071
- clauth serve start -p mypass Start pre-unlocked (password in memory only)
1072
- clauth serve stop Stop the daemon
1073
- clauth serve ping Check status
1074
- clauth serve restart Restart (stays locked until browser unlock)
1075
- clauth serve start --services github,vercel
1076
- clauth serve mcp Start MCP server for Claude Code
1077
- clauth serve mcp -p mypass Start MCP server pre-unlocked
1078
- clauth serve foreground --port 53137 --isolated
1079
- Start isolated passwordless server for route tests
1080
- clauth serve install Set up auto-start on login (DPAPI/Keychain/libsecret)
1081
- clauth serve install --tunnel host Auto-start with Cloudflare Tunnel
1082
- clauth serve uninstall Remove auto-start
1083
- `)
1084
- .action(async (action, opts) => {
1085
- const resolvedAction = opts.action || action || "foreground";
1086
- await runServe({ ...opts, action: resolvedAction });
1087
- });
1088
-
1089
- 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 { writeCredentialWithRecovery } from "./recovery.js";
13
+ import os from "os";
14
+ import fs from "fs";
15
+ import path from "path";
16
+
17
+ const config = new Conf(getConfOptions());
18
+ const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
19
+
20
+ function shellSingleQuote(value) {
21
+ return `'${String(value ?? "").replace(/'/g, "''")}'`;
22
+ }
23
+
24
+ function enrollmentScriptName(label) {
25
+ const slug = String(label || "new-computer")
26
+ .toLowerCase()
27
+ .replace(/[^a-z0-9]+/g, "-")
28
+ .replace(/^-+|-+$/g, "")
29
+ .slice(0, 40) || "new-computer";
30
+ return `clauth-enroll-${slug}.ps1`;
31
+ }
32
+
33
+ function writeEnrollmentScript({ supabaseUrl, anonKey, enrollmentCode, label }) {
34
+ const appDir = process.platform === "win32"
35
+ ? path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "clauth")
36
+ : path.join(os.homedir(), ".config", "clauth");
37
+ fs.mkdirSync(appDir, { recursive: true });
38
+ const scriptPath = path.join(appDir, enrollmentScriptName(label));
39
+ const script = [
40
+ "$ErrorActionPreference = 'Stop'",
41
+ "$label = $env:COMPUTERNAME",
42
+ "if (-not $label) { $label = [System.Net.Dns]::GetHostName() }",
43
+ "Write-Host 'Installing clauth...'",
44
+ "npm install -g @lifeaitools/clauth@latest",
45
+ "Write-Host 'Enrolling this computer with clauth...'",
46
+ [
47
+ "clauth setup",
48
+ `--supabase-url ${shellSingleQuote(supabaseUrl)}`,
49
+ `--anon-key ${shellSingleQuote(anonKey)}`,
50
+ `--enrollment-code ${shellSingleQuote(enrollmentCode)}`,
51
+ "--label \"$label\"",
52
+ ].join(" "),
53
+ "Write-Host 'Installing clauth startup service...'",
54
+ "clauth serve install",
55
+ "Write-Host 'clauth enrollment complete.'",
56
+ "$self = $PSCommandPath",
57
+ "Start-Process powershell.exe -WindowStyle Hidden -ArgumentList @('-NoProfile','-Command',\"Start-Sleep -Seconds 2; Remove-Item -LiteralPath '$self' -Force -ErrorAction SilentlyContinue\")",
58
+ ].join("\r\n");
59
+ fs.writeFileSync(scriptPath, `${script}\r\n`, "utf8");
60
+ return scriptPath;
61
+ }
62
+
63
+ // ============================================================
64
+ // Password prompt helper
65
+ // ============================================================
66
+ async function promptPassword(message = "clauth password") {
67
+ const { pw } = await inquirer.prompt([{
68
+ type: "password",
69
+ name: "pw",
70
+ message,
71
+ mask: "*",
72
+ validate: v => v.length >= 8 || "Password must be at least 8 characters"
73
+ }]);
74
+ return pw;
75
+ }
76
+
77
+ // ============================================================
78
+ // Auth helper — get pw + derive token
79
+ // ============================================================
80
+ async function getAuth(pw) {
81
+ const password = pw || await promptPassword();
82
+ const machineHash = getMachineHash();
83
+ const { token, timestamp } = deriveToken(password, machineHash);
84
+ return { password, machineHash, token, timestamp };
85
+ }
86
+
87
+ const ADDRESS_KEY_TYPES = new Set(["connstring", "fileserver", "oauth"]);
88
+ const ADDRESS_FIELDS = new Set(["url", "uri", "host", "hostname", "server", "address", "base_url", "endpoint", "path", "root"]);
89
+
90
+ function normalizeSearchText(value) {
91
+ return String(value || "").toLowerCase();
92
+ }
93
+
94
+ function redactUrlish(value) {
95
+ const text = String(value || "").trim();
96
+ if (!text) return "";
97
+ try {
98
+ const url = new URL(text);
99
+ if (url.username) url.username = "***";
100
+ if (url.password) url.password = "***";
101
+ return url.toString();
102
+ } catch {
103
+ return text.replace(/:\/\/([^:@/\s]+):([^@/\s]+)@/g, "://***:***@");
104
+ }
105
+ }
106
+
107
+ function collectAddressHints(value, keyType) {
108
+ if (!ADDRESS_KEY_TYPES.has(String(keyType || "").toLowerCase())) return [];
109
+ const hints = new Set();
110
+
111
+ function add(candidate) {
112
+ if (candidate === undefined || candidate === null) return;
113
+ const text = redactUrlish(candidate);
114
+ if (text) hints.add(text);
115
+ }
116
+
117
+ function walk(node, fieldName = "") {
118
+ if (node === undefined || node === null) return;
119
+ if (typeof node === "string") {
120
+ if (fieldName && ADDRESS_FIELDS.has(fieldName.toLowerCase())) add(node);
121
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(node) || /^[A-Za-z]:[\\/]/.test(node) || node.startsWith("\\\\")) add(node);
122
+ return;
123
+ }
124
+ if (Array.isArray(node)) {
125
+ for (const item of node) walk(item, fieldName);
126
+ return;
127
+ }
128
+ if (typeof node === "object") {
129
+ for (const [key, child] of Object.entries(node)) walk(child, key);
130
+ }
131
+ }
132
+
133
+ try {
134
+ walk(JSON.parse(value));
135
+ } catch {
136
+ walk(value);
137
+ }
138
+
139
+ return [...hints];
140
+ }
141
+
142
+ async function searchServices(auth, query, opts = {}) {
143
+ const q = normalizeSearchText(query);
144
+ if (!q) throw new Error("Search query is required");
145
+ const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp, opts.project);
146
+ if (result.error) throw new Error(result.error);
147
+
148
+ const services = result.services || [];
149
+ const rows = [];
150
+
151
+ for (const s of services) {
152
+ const fields = {
153
+ name: s.name,
154
+ label: s.label,
155
+ project: s.project,
156
+ type: s.key_type,
157
+ description: s.description
158
+ };
159
+ const matched = Object.entries(fields)
160
+ .filter(([, value]) => normalizeSearchText(value).includes(q))
161
+ .map(([field]) => field);
162
+
163
+ let addressHints = [];
164
+ if (opts.addresses === true && ADDRESS_KEY_TYPES.has(String(s.key_type || "").toLowerCase()) && s.vault_key) {
165
+ const secret = await api.retrieve(auth.password, auth.machineHash, auth.token, auth.timestamp, s.name);
166
+ if (!secret.error) {
167
+ addressHints = collectAddressHints(secret.value, s.key_type);
168
+ if (addressHints.some(h => normalizeSearchText(h).includes(q))) matched.push("address");
169
+ }
170
+ }
171
+
172
+ if (matched.length) rows.push({ ...s, matched: [...new Set(matched)], addressHints });
173
+ }
174
+
175
+ return rows;
176
+ }
177
+
178
+ // ============================================================
179
+ // Program
180
+ // ============================================================
181
+ const program = new Command();
182
+
183
+ program
184
+ .name("clauth")
185
+ .version(VERSION)
186
+ .description(chalk.cyan("🔐 clauth") + " — Hardware-bound credential vault for LIFEAI infrastructure");
187
+
188
+ // ──────────────────────────────────────────────
189
+ // clauth install (Supabase provisioning + skill install + test)
190
+ // ──────────────────────────────────────────────
191
+ import { runInstall } from './commands/install.js';
192
+ import { runUninstall } from './commands/uninstall.js';
193
+ import { runScrub } from './commands/scrub.js';
194
+ import { runServe } from './commands/serve.js';
195
+ import { runCodevelop } from './commands/codevelop.js';
196
+ import { runNpm, runPublish } from './commands/npm.js';
197
+
198
+ program
199
+ .command('install')
200
+ .description('Provision Supabase, deploy Edge Function, install Claude skill')
201
+ .option('--ref <ref>', 'Supabase project ref')
202
+ .option('--pat <pat>', 'Supabase Personal Access Token')
203
+ .action(async (opts) => {
204
+ await runInstall(opts);
205
+ });
206
+
207
+ program
208
+ .command('uninstall')
209
+ .description('Full teardown — drop DB objects, Edge Function, secrets, skill, config')
210
+ .option('--ref <ref>', 'Supabase project ref')
211
+ .option('--pat <pat>', 'Supabase Personal Access Token (required)')
212
+ .option('--yes', 'Skip confirmation prompt')
213
+ .action(async (opts) => {
214
+ if (!opts.yes) {
215
+ const inquirerMod = await import('inquirer');
216
+ const { confirm } = await inquirerMod.default.prompt([{
217
+ type: 'input',
218
+ name: 'confirm',
219
+ message: chalk.red('Type "CONFIRM UNINSTALL" to proceed:'),
220
+ }]);
221
+ if (confirm !== 'CONFIRM UNINSTALL') {
222
+ console.log(chalk.yellow('\n Uninstall cancelled.\n'));
223
+ process.exit(0);
224
+ }
225
+ }
226
+ await runUninstall(opts);
227
+ });
228
+
229
+ program
230
+ .command("codevelop")
231
+ .description("Install and launch Claude/Codex co-development terminal sessions")
232
+ .argument("[action]", "install-terminal | start | join | say | read | watch | who | ask | reply | inbox | listen | request-partner | launch-peer | check-partner | sync | help", "help")
233
+ .option("--repo <path>", "Repo root", "C:\\Dev\\regen-root")
234
+ .option("--port <port>", "Isolated clauth port", "53137")
235
+ .option("--base-url <url>", "Override clauth base URL")
236
+ .option("--name <name>", "Session name")
237
+ .option("--session <idOrManifestPath>", "Co-develop session id or manifest path")
238
+ .option("--task <task>", "Initial partner request task")
239
+ .option("--context <path>", "Context, plan, or architecture file the partner should read first")
240
+ .option("--channel <name>", "Ad hoc channel name for join/say/read/watch")
241
+ .option("--message <text>", "Ad hoc message for say")
242
+ .option("--to <peer>", "Target peer for ask/reply, or optional direct ad hoc recipient for say")
243
+ .option("--from <peer>", "Sender peer for ask/reply")
244
+ .option("--turn <turn_id>", "Turn ID for reply")
245
+ .option("--role <role>", "Turn role, e.g. reviewer or builder")
246
+ .option("--skill <skill>", "Requested skill name, e.g. rdc:review")
247
+ .option("--verdict <verdict>", "Reply verdict: pass, fail, or blocked")
248
+ .option("--summary <summary>", "Reply summary")
249
+ .option("--evidence <items>", "Reply evidence; separate multiple items with semicolons")
250
+ .option("--files-changed <items>", "Reply changed files; separate multiple items with semicolons")
251
+ .option("--commits <items>", "Reply commits; separate multiple items with semicolons")
252
+ .option("--blockers <items>", "Reply blockers; separate multiple items with semicolons")
253
+ .option("--next <items>", "Reply next actions; separate multiple items with semicolons")
254
+ .option("--wait", "For ask: wait for a matching reply")
255
+ .option("--once", "For listen: exit after the first message event")
256
+ .option("--json", "For read: print raw JSON")
257
+ .option("--timeout-ms <ms>", "Wait timeout in milliseconds", "300000")
258
+ .option("--interval-ms <ms>", "Wait polling interval in milliseconds", "2000")
259
+ .option("--start-isolated-clauth", "Start isolated clauth if the selected port is not running")
260
+ .option("--peer <peer>", "Peer name for launch-peer/check-partner/sync, or ad hoc name such as codex-1")
261
+ .option("--dry-run", "Print and write session manifest without opening Windows Terminal")
262
+ .option("--no-open", "Create session/config but do not open Windows Terminal")
263
+ .option("--print-only", "For launch-peer: resolve command without starting the CLI")
264
+ .action(async (action, opts) => {
265
+ await runCodevelop({ ...opts, action });
266
+ });
267
+
268
+ program
269
+ .command("npm")
270
+ .description("Operate npm auth safely through the clauth npm service")
271
+ .argument("[action]", "whoami | tokens | set-local | sync-github-secret | rerun | help", "help")
272
+ .argument("[args...]", "Action arguments")
273
+ .option("--repo <repo>", "GitHub repo, e.g. LIFEAI/rdc-skills")
274
+ .action(async (action, args, opts) => {
275
+ await runNpm(action, { ...opts, args });
276
+ });
277
+
278
+ // ──────────────────────────────────────────────
279
+ // clauth publish [target]
280
+ // Guarded npm publish for ANY package — refuses to ship code that isn't
281
+ // committed AND pushed to GitHub (prevents npm/repo divergence from dev builds).
282
+ // ──────────────────────────────────────────────
283
+ program
284
+ .command("publish [target]")
285
+ .description("Safely publish an npm package (default: cwd). Refuses unless committed + pushed to GitHub.")
286
+ .option("--dry-run", "Run all guards and pack, but do not publish")
287
+ .option("--access <access>", "npm access: public | restricted")
288
+ .option("--allow-dirty", "Override the uncommitted-changes guard (NOT recommended)")
289
+ .option("--allow-unpushed", "Override the not-pushed-to-remote guard (NOT recommended)")
290
+ .action(async (target, opts) => {
291
+ try {
292
+ await runPublish(target, opts);
293
+ } catch (err) {
294
+ console.error(chalk.red(err.message));
295
+ process.exitCode = 1;
296
+ }
297
+ });
298
+
299
+ // ──────────────────────────────────────────────
300
+ // clauth setup
301
+ // ──────────────────────────────────────────────
302
+ program
303
+ .command("setup")
304
+ .description("Register this machine with the vault (run after clauth install)")
305
+ .option("--admin-token <token>", "Bootstrap token (from clauth install output)")
306
+ .option("--enrollment-code <code>", "One-time enrollment code from clauth enroll")
307
+ .option("--supabase-url <url>", "Vault Supabase URL, for enrolling a new computer without running clauth install")
308
+ .option("--anon-key <key>", "Vault Supabase anon key, for enrolling a new computer without running clauth install")
309
+ .option("--install-id <id>", "Logical install/owner group for admin-token setup", "default")
310
+ .option("--label <label>", "Human label for this machine")
311
+ .option("-p, --pw <password>", "Password (skip interactive prompt)")
312
+ .action(async (opts) => {
313
+ console.log(chalk.cyan("\n🔐 clauth setup\n"));
314
+
315
+ if (opts.supabaseUrl) config.set("supabase_url", opts.supabaseUrl);
316
+ if (opts.anonKey) config.set("supabase_anon_key", opts.anonKey);
317
+
318
+ // URL + anon key may already be saved by clauth install, or provided by
319
+ // an old-machine enrollment command.
320
+ let savedUrl = config.get("supabase_url");
321
+ let savedAnon = config.get("supabase_anon_key");
322
+ if (!savedUrl || !savedAnon) {
323
+ const configAnswers = await inquirer.prompt([
324
+ { type: "input", name: "supabaseUrl", message: "Vault Supabase URL:", default: savedUrl || opts.supabaseUrl || "" },
325
+ { type: "password", name: "anonKey", message: "Vault anon key:", mask: "*", default: savedAnon || opts.anonKey || "" },
326
+ ]);
327
+ if (!configAnswers.supabaseUrl || !configAnswers.anonKey) {
328
+ console.log(chalk.yellow(" Supabase config not found. Run clauth install first, or provide --supabase-url and --anon-key.\n"));
329
+ process.exit(1);
330
+ }
331
+ config.set("supabase_url", configAnswers.supabaseUrl);
332
+ config.set("supabase_anon_key", configAnswers.anonKey);
333
+ savedUrl = configAnswers.supabaseUrl;
334
+ savedAnon = configAnswers.anonKey;
335
+ }
336
+ console.log(chalk.gray(` Project: ${savedUrl}\n`));
337
+
338
+ let answers;
339
+ if (opts.pw && (opts.adminToken || opts.enrollmentCode)) {
340
+ // Non-interactive mode — all flags provided
341
+ answers = {
342
+ label: opts.label || os.hostname(),
343
+ pw: opts.pw,
344
+ adminTk: opts.adminToken,
345
+ enrollmentCode: opts.enrollmentCode,
346
+ };
347
+ } else if (opts.enrollmentCode) {
348
+ const pw = opts.pw || await promptPassword("Set clauth password for this computer");
349
+ answers = {
350
+ label: opts.label || os.hostname(),
351
+ pw,
352
+ adminTk: opts.adminToken,
353
+ enrollmentCode: opts.enrollmentCode,
354
+ };
355
+ } else {
356
+ answers = await inquirer.prompt([
357
+ { type: "input", name: "label", message: "Machine label:", default: opts.label || os.hostname() },
358
+ { type: "password", name: "pw", message: "Set password:", mask: "*", default: opts.pw || "" },
359
+ { type: "password", name: "enrollmentCode", message: "Enrollment code (preferred for new computer; leave blank if using bootstrap token):", mask: "*",
360
+ default: opts.enrollmentCode || "" },
361
+ { type: "password", name: "adminTk", message: "Bootstrap token (admin fallback):", mask: "*",
362
+ default: opts.adminToken || "" },
363
+ ]);
364
+ }
365
+
366
+ const spinner = ora("Registering machine with vault...").start();
367
+ try {
368
+ const machineHash = getMachineHash();
369
+ const seedHash = deriveSeedHash(machineHash, answers.pw);
370
+ const result = answers.enrollmentCode
371
+ ? await api.redeemEnrollment(machineHash, seedHash, answers.label, answers.enrollmentCode)
372
+ : await api.registerMachine(machineHash, seedHash, answers.label, answers.adminTk, { install_id: opts.installId || "default" });
373
+ if (result.error) throw new Error(result.error);
374
+ spinner.succeed(chalk.green(`Machine registered: ${machineHash.slice(0,12)}... install_id=${result.install_id || opts.installId || "default"}`));
375
+
376
+ console.log(chalk.green("\n✓ clauth is ready.\n"));
377
+ console.log(chalk.cyan(" clauth test — verify connection"));
378
+ console.log(chalk.cyan(" clauth status — see all services\n"));
379
+ } catch (err) {
380
+ spinner.fail(chalk.red(`Setup failed: ${err.message}`));
381
+ process.exit(1);
382
+ }
383
+ });
384
+
385
+ // ──────────────────────────────────────────────
386
+ // clauth enroll
387
+ // ──────────────────────────────────────────────
388
+ program
389
+ .command("enroll")
390
+ .description("Create a one-time enrollment code for adding another computer")
391
+ .option("--label <label>", "Suggested label for the new computer")
392
+ .option("--ttl-minutes <minutes>", "Enrollment lifetime, 5 to 1440 minutes", "60")
393
+ .option("--install-id <id>", "Override install id; default is current machine's install id")
394
+ .option("-p, --pw <password>", "Password (or will prompt)")
395
+ .action(async (opts) => {
396
+ console.log(chalk.cyan("\n🔐 clauth enroll\n"));
397
+ const auth = await getAuth(opts.pw);
398
+ const spinner = ora("Creating one-time machine enrollment...").start();
399
+ try {
400
+ const result = await api.createEnrollment(
401
+ auth.password,
402
+ auth.machineHash,
403
+ auth.token,
404
+ auth.timestamp,
405
+ opts.label,
406
+ Number(opts.ttlMinutes || 60),
407
+ opts.installId
408
+ );
409
+ if (result.error) throw new Error(result.error);
410
+ const supabaseUrl = config.get("supabase_url");
411
+ const anonKey = config.get("supabase_anon_key");
412
+ const scriptPath = writeEnrollmentScript({
413
+ supabaseUrl,
414
+ anonKey,
415
+ enrollmentCode: result.enrollment_code,
416
+ label: opts.label,
417
+ });
418
+ spinner.succeed(chalk.green(`Enrollment created for install_id=${result.install_id}`));
419
+ console.log("");
420
+ console.log(chalk.bold(" Enrollment code:"));
421
+ console.log(chalk.white(` ${result.enrollment_code}`));
422
+ console.log("");
423
+ console.log(chalk.bold(" On the new computer:"));
424
+ console.log(chalk.gray(` Run this one-time script: ${scriptPath}`));
425
+ console.log(chalk.gray(" It installs clauth, enrolls with this code, installs startup, then deletes itself."));
426
+ console.log("");
427
+ console.log(chalk.gray(` Expires: ${result.expires_at}`));
428
+ } catch (err) {
429
+ spinner.fail(chalk.red(`Enroll failed: ${err.message}`));
430
+ process.exitCode = 1;
431
+ }
432
+ });
433
+
434
+ // ──────────────────────────────────────────────
435
+ // clauth status
436
+ // ──────────────────────────────────────────────
437
+ program
438
+ .command("status")
439
+ .description("Show all services and their state")
440
+ .option("-p, --pw <password>", "Password (or will prompt)")
441
+ .option("--project <name>", "Filter by project scope")
442
+ .action(async (opts) => {
443
+ const auth = await getAuth(opts.pw);
444
+ const spinner = ora("Fetching service status...").start();
445
+ try {
446
+ const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp, opts.project);
447
+ spinner.stop();
448
+ if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); return; }
449
+
450
+ const heading = opts.project ? `clauth service status (project: ${opts.project})` : "clauth service status";
451
+ console.log(chalk.cyan(`\n🔐 ${heading}\n`));
452
+ console.log(
453
+ chalk.bold(
454
+ " " + "SERVICE".padEnd(24) + "TYPE".padEnd(12) + "PROJECT".padEnd(22) + "STATUS".padEnd(12) +
455
+ "KEY STORED".padEnd(12) + "LAST RETRIEVED"
456
+ )
457
+ );
458
+ console.log(" " + "─".repeat(90));
459
+
460
+ for (const s of result.services || []) {
461
+ const status = s.enabled
462
+ ? chalk.green("ACTIVE".padEnd(12))
463
+ : s.vault_key
464
+ ? chalk.yellow("SUSPENDED".padEnd(12))
465
+ : chalk.gray("NO KEY".padEnd(12));
466
+ const hasKey = s.vault_key ? chalk.green("✓".padEnd(12)) : chalk.gray("—".padEnd(12));
467
+ const lastGet = s.last_retrieved
468
+ ? new Date(s.last_retrieved).toLocaleDateString()
469
+ : chalk.gray("never");
470
+ const proj = s.project ? chalk.blue(s.project.padEnd(22)) : chalk.gray("—".padEnd(22));
471
+
472
+ console.log(` ${s.name.padEnd(24)}${s.key_type.padEnd(12)}${proj}${status}${hasKey}${lastGet}`);
473
+ }
474
+ console.log();
475
+ } catch (err) {
476
+ spinner.fail(chalk.red(err.message));
477
+ }
478
+ });
479
+
480
+ // ──────────────────────────────────────────────
481
+ // clauth write pw <new_password>
482
+ // clauth write params
483
+ // clauth write key <service> <value>
484
+ // ──────────────────────────────────────────────
485
+ const writeCmd = program.command("write").description("Write credentials or update auth parameters");
486
+
487
+ writeCmd
488
+ .command("pw [newpw]")
489
+ .description("Set or update clauth master password")
490
+ .action(async (newpw) => {
491
+ console.log(chalk.cyan("\n🔐 clauth write pw\n"));
492
+ const current = await promptPassword("Current password (to verify)");
493
+ const pw = newpw || (await inquirer.prompt([
494
+ { type: "password", name: "p", message: "New password:", mask: "*" },
495
+ { type: "password", name: "c", message: "Confirm new password:", mask: "*" }
496
+ ]).then(a => { if (a.p !== a.c) { console.log(chalk.red("Passwords don't match")); process.exit(1); } return a.p; }));
497
+
498
+ // Re-register machine with new seed hash
499
+ const machineHash = getMachineHash();
500
+ const newSeedHash = deriveSeedHash(machineHash, pw);
501
+ const { token, timestamp } = deriveToken(current, machineHash);
502
+ const adminToken = await inquirer.prompt([{
503
+ type: "password", name: "t", message: "Admin bootstrap token (required for re-registration):", mask: "*"
504
+ }]).then(a => a.t);
505
+
506
+ const spinner = ora("Updating password and re-registering machine...").start();
507
+ try {
508
+ const result = await api.registerMachine(machineHash, newSeedHash, null, adminToken);
509
+ if (result.error) throw new Error(result.error);
510
+ spinner.succeed(chalk.green("Password updated and machine re-registered."));
511
+ } catch (err) {
512
+ spinner.fail(chalk.red(err.message));
513
+ }
514
+ });
515
+
516
+ writeCmd
517
+ .command("params")
518
+ .description("Re-read hardware fingerprint (use after hardware change)")
519
+ .action(async () => {
520
+ const spinner = ora("Reading hardware fingerprint...").start();
521
+ try {
522
+ const hash = getMachineHash();
523
+ spinner.succeed(chalk.green(`Machine hash: ${hash.slice(0,16)}...`));
524
+ console.log(chalk.gray("Full hash: " + hash));
525
+ } catch (err) {
526
+ spinner.fail(chalk.red(err.message));
527
+ }
528
+ });
529
+
530
+ writeCmd
531
+ .command("key <service> [value]")
532
+ .description("Write a credential into vault for a service")
533
+ .option("-p, --pw <password>", "Password")
534
+ .action(async (service, value, opts) => {
535
+ const auth = await getAuth(opts.pw);
536
+ let val = value;
537
+ if (!val) {
538
+ const { v } = await inquirer.prompt([{ type: "password", name: "v", message: `Value for ${service}:`, mask: "*" }]);
539
+ val = v;
540
+ }
541
+ const spinner = ora(`Writing key for ${service}...`).start();
542
+ try {
543
+ const { result, snapshot, normalized } = await writeCredentialWithRecovery({
544
+ password: auth.password,
545
+ machineHash: auth.machineHash,
546
+ service,
547
+ value: val,
548
+ });
549
+ if (result.error) throw new Error(result.error);
550
+ const details = [
551
+ snapshot?.ok ? "recovery snapshot written" : null,
552
+ normalized ? "value normalized" : null,
553
+ ].filter(Boolean);
554
+ spinner.succeed(chalk.green(`Key stored in vault: auth.${service}${details.length ? ` (${details.join(", ")})` : ""}`));
555
+ } catch (err) {
556
+ spinner.fail(chalk.red(err.message));
557
+ }
558
+ });
559
+
560
+ // ──────────────────────────────────────────────
561
+ // clauth enable <service|all>
562
+ // clauth disable <service|all>
563
+ // ──────────────────────────────────────────────
564
+ program
565
+ .command("enable <service>")
566
+ .description("Enable a service (or 'all')")
567
+ .option("-p, --pw <password>")
568
+ .action(async (service, opts) => {
569
+ const auth = await getAuth(opts.pw);
570
+ const spinner = ora(`Enabling ${service}...`).start();
571
+ try {
572
+ const result = await api.enable(auth.password, auth.machineHash, auth.token, auth.timestamp, service, true);
573
+ if (result.error) throw new Error(result.error);
574
+ spinner.succeed(chalk.green(`Enabled: ${service}`));
575
+ } catch (err) { spinner.fail(chalk.red(err.message)); }
576
+ });
577
+
578
+ program
579
+ .command("disable <service>")
580
+ .description("Disable a service (or 'all')")
581
+ .option("-p, --pw <password>")
582
+ .action(async (service, opts) => {
583
+ const auth = await getAuth(opts.pw);
584
+ const spinner = ora(`Disabling ${service}...`).start();
585
+ try {
586
+ const result = await api.enable(auth.password, auth.machineHash, auth.token, auth.timestamp, service, false);
587
+ if (result.error) throw new Error(result.error);
588
+ spinner.succeed(chalk.yellow(`Disabled: ${service}`));
589
+ } catch (err) { spinner.fail(chalk.red(err.message)); }
590
+ });
591
+
592
+ // ──────────────────────────────────────────────
593
+ // clauth add service <name>
594
+ // clauth remove service <name>
595
+ // clauth list services
596
+ // ──────────────────────────────────────────────
597
+ const addCmd = program.command("add").description("Add resources to the registry");
598
+
599
+ addCmd
600
+ .command("service <name>")
601
+ .description("Register a new service slot")
602
+ .option("--type <type>", "Key type: token | keypair | connstring | oauth")
603
+ .option("--label <label>", "Human-readable label")
604
+ .option("--description <desc>", "Description")
605
+ .option("--project <project>", "Project scope (groups related services)")
606
+ .option("-p, --pw <password>")
607
+ .action(async (name, opts) => {
608
+ const auth = await getAuth(opts.pw);
609
+ let answers;
610
+ if (opts.type && opts.label) {
611
+ // Non-interactive — all flags provided
612
+ answers = { label: opts.label, key_type: opts.type, desc: opts.description || "" };
613
+ } else {
614
+ answers = await inquirer.prompt([
615
+ { type: "input", name: "label", message: "Label:", default: opts.label || name },
616
+ { type: "list", name: "key_type", message: "Key type:", choices: ["token","keypair","connstring","oauth"], default: opts.type || "token" },
617
+ { type: "input", name: "desc", message: "Description (optional):", default: opts.description || "" }
618
+ ]);
619
+ }
620
+ const spinner = ora(`Adding service: ${name}${opts.project ? ` (project: ${opts.project})` : ""}...`).start();
621
+ try {
622
+ const result = await api.addService(
623
+ auth.password, auth.machineHash, auth.token, auth.timestamp,
624
+ name, answers.label, answers.key_type, answers.desc, opts.project
625
+ );
626
+ if (result.error) throw new Error(result.error);
627
+ spinner.succeed(chalk.green(`Service added: ${name} (${answers.key_type})${opts.project ? chalk.blue(` [${opts.project}]`) : ""}`));
628
+ console.log(chalk.gray(` Next: clauth write key ${name}`));
629
+ } catch (err) { spinner.fail(chalk.red(err.message)); }
630
+ });
631
+
632
+ const removeCmd = program.command("remove").description("Remove resources from the registry");
633
+
634
+ removeCmd
635
+ .command("service <name>")
636
+ .description("Remove a service and its key from vault")
637
+ .option("-p, --pw <password>")
638
+ .action(async (name, opts) => {
639
+ const { confirm } = await inquirer.prompt([{
640
+ type: "input", name: "confirm",
641
+ message: chalk.red(`Type "CONFIRM REMOVE ${name.toUpperCase()}" to proceed:`)
642
+ }]);
643
+ const auth = await getAuth(opts.pw);
644
+ const spinner = ora(`Removing ${name}...`).start();
645
+ try {
646
+ const result = await api.removeService(auth.password, auth.machineHash, auth.token, auth.timestamp, name, confirm);
647
+ if (result.error) throw new Error(result.error);
648
+ spinner.succeed(chalk.yellow(`Removed: ${name}`));
649
+ } catch (err) { spinner.fail(chalk.red(err.message)); }
650
+ });
651
+
652
+ program
653
+ .command("list")
654
+ .description("List all registered services")
655
+ .option("-p, --pw <password>")
656
+ .option("--project <name>", "Filter by project scope")
657
+ .action(async (opts) => {
658
+ const auth = await getAuth(opts.pw);
659
+ const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp, opts.project);
660
+ if (result.error) { console.log(chalk.red(result.error)); return; }
661
+ const heading = opts.project ? `Registered services (project: ${opts.project})` : "Registered services";
662
+ console.log(chalk.cyan(`\n ${heading}:\n`));
663
+ let lastProject = undefined;
664
+ for (const s of result.services || []) {
665
+ const proj = s.project || null;
666
+ if (proj !== lastProject) {
667
+ if (lastProject !== undefined) console.log();
668
+ console.log(chalk.gray(` [${proj || "global"}]`));
669
+ lastProject = proj;
670
+ }
671
+ console.log(` ${chalk.bold(s.name.padEnd(24))} ${chalk.gray(s.key_type.padEnd(12))} ${chalk.gray(s.label || "")}`);
672
+ }
673
+ console.log();
674
+ });
675
+
676
+ program
677
+ .command("search <query>")
678
+ .description("Search services by name, label, project, description, or type")
679
+ .option("-p, --pw <password>")
680
+ .option("--project <name>", "Filter by project scope")
681
+ .option("--addresses", "Also search redacted address hints from address-bearing secrets (may retrieve multiple secrets)")
682
+ .action(async (query, opts) => {
683
+ const auth = await getAuth(opts.pw);
684
+ const spinner = ora("Searching services...").start();
685
+ try {
686
+ const rows = await searchServices(auth, query, { project: opts.project, addresses: opts.addresses === true });
687
+ spinner.stop();
688
+ console.log(chalk.cyan(`\n Search results for "${query}":\n`));
689
+ if (!rows.length) {
690
+ console.log(chalk.gray(" No matching services found.\n"));
691
+ return;
692
+ }
693
+ console.log(chalk.bold(" " + "SERVICE".padEnd(24) + "TYPE".padEnd(12) + "PROJECT".padEnd(20) + "MATCHED"));
694
+ console.log(" " + "─".repeat(78));
695
+ for (const s of rows) {
696
+ const project = s.project || "global";
697
+ console.log(` ${chalk.bold(s.name.padEnd(24))}${String(s.key_type || "").padEnd(12)}${project.padEnd(20)}${s.matched.join(", ")}`);
698
+ if (s.label) console.log(chalk.gray(` label: ${s.label}`));
699
+ if (s.description) console.log(chalk.gray(` description: ${s.description}`));
700
+ for (const hint of s.addressHints || []) console.log(chalk.gray(` address: ${hint}`));
701
+ }
702
+ console.log();
703
+ } catch (err) {
704
+ spinner.fail(chalk.red(err.message));
705
+ }
706
+ });
707
+
708
+ // ──────────────────────────────────────────────
709
+ // clauth test <service|all>
710
+ // ──────────────────────────────────────────────
711
+ program
712
+ .command("test [service]")
713
+ .description("Test HMAC handshake — no key returned")
714
+ .option("-p, --pw <password>")
715
+ .action(async (service, opts) => {
716
+ const auth = await getAuth(opts.pw);
717
+ const spinner = ora("Testing auth handshake...").start();
718
+ try {
719
+ const result = await api.test(auth.password, auth.machineHash, auth.token, auth.timestamp);
720
+ if (result.error) throw new Error(`${result.error}: ${result.reason}`);
721
+ spinner.succeed(chalk.green("PASS — HMAC validated"));
722
+ console.log(chalk.gray(` Machine: ${auth.machineHash.slice(0,16)}...`));
723
+ console.log(chalk.gray(` Window: ${new Date(result.timestamp).toISOString()}`));
724
+ } catch (err) {
725
+ spinner.fail(chalk.red("FAIL — " + err.message));
726
+ }
727
+ });
728
+
729
+ // ──────────────────────────────────────────────
730
+ // clauth get <service>
731
+ // ──────────────────────────────────────────────
732
+ program
733
+ .command("get <service>")
734
+ .description("Retrieve a key from vault")
735
+ .option("-p, --pw <password>")
736
+ .option("--json", "Output raw JSON")
737
+ .action(async (service, opts) => {
738
+ const auth = await getAuth(opts.pw);
739
+ const spinner = ora(`Retrieving ${service}...`).start();
740
+ try {
741
+ const result = await api.retrieve(auth.password, auth.machineHash, auth.token, auth.timestamp, service);
742
+ spinner.stop();
743
+ if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); return; }
744
+ if (opts.json) {
745
+ console.log(JSON.stringify(result, null, 2));
746
+ } else {
747
+ console.log(chalk.cyan(`\n🔑 ${service} (${result.key_type})\n`));
748
+ const val = typeof result.value === "string" ? result.value : JSON.stringify(result.value, null, 2);
749
+ console.log(val);
750
+ console.log();
751
+ }
752
+ } catch (err) {
753
+ spinner.fail(chalk.red(err.message));
754
+ }
755
+ });
756
+
757
+ // ──────────────────────────────────────────────
758
+ // clauth revoke <service|all>
759
+ // ──────────────────────────────────────────────
760
+ program
761
+ .command("revoke <service>")
762
+ .description("Delete key from vault (destructive)")
763
+ .option("-p, --pw <password>")
764
+ .action(async (service, opts) => {
765
+ const phrase = service === "all" ? "CONFIRM REVOKE ALL" : `CONFIRM REVOKE ${service.toUpperCase()}`;
766
+ const { confirm } = await inquirer.prompt([{
767
+ type: "input", name: "confirm",
768
+ message: chalk.red(`Type "${phrase}" to proceed:`)
769
+ }]);
770
+ const auth = await getAuth(opts.pw);
771
+ const spinner = ora(`Revoking ${service}...`).start();
772
+ try {
773
+ const result = await api.revoke(auth.password, auth.machineHash, auth.token, auth.timestamp, service, confirm);
774
+ if (result.error) throw new Error(result.error);
775
+ spinner.succeed(chalk.yellow(`Revoked: ${service}`));
776
+ } catch (err) { spinner.fail(chalk.red(err.message)); }
777
+ });
778
+
779
+ // ──────────────────────────────────────────────
780
+ // clauth scrub [target]
781
+ // ──────────────────────────────────────────────
782
+ program
783
+ .command("scrub [target]")
784
+ .description("Scrub credentials from Claude Code transcript logs (no auth required)")
785
+ .option("--force", "Rescrub files even if already marked clean")
786
+ .addHelpText("after", `
787
+ Examples:
788
+ clauth scrub Scrub the most recent (active) transcript
789
+ clauth scrub <file> Scrub a specific file
790
+ clauth scrub all Scrub every transcript + tool-result sidecar (.jsonl + .txt)
791
+ clauth scrub all --force Rescrub all files (ignore markers)
792
+ clauth scrub session Scrub ONLY the ending session (transcript + sidecars); reads SessionEnd hook JSON on stdin
793
+
794
+ Redacts: built-in token patterns, your ~/.clauth/scrub-patterns.json,
795
+ and this machine's live vault values (best-effort via the daemon).
796
+ `)
797
+ .action(async (target, opts) => {
798
+ await runScrub(target, opts);
799
+ });
800
+
801
+ // ──────────────────────────────────────────────
802
+ // clauth watchdog
803
+ // ──────────────────────────────────────────────
804
+ program
805
+ .command("watchdog [action] [args...]")
806
+ .description("Manage auto-restart watchdog (install|uninstall|status|start|register|list|events|restart)")
807
+ .option("--manifest <path>", "Watchdog service manifest for register")
808
+ .option("--service <id>", "Watchdog service id for restart")
809
+ .option("--limit <n>", "Event count for events", "100")
810
+ .action(async (action, args, opts) => {
811
+ const { runWatchdog } = await import("./commands/watchdog.js");
812
+ await runWatchdog(action, { ...opts, args });
813
+ });
814
+
815
+ // clauth doctor
816
+ // ──────────────────────────────────────────────
817
+ program
818
+ .command("doctor")
819
+ .description("Check all prerequisites and diagnose issues")
820
+ .action(async () => {
821
+ const { runDoctor } = await import("./commands/doctor.js");
822
+ await runDoctor();
823
+ });
824
+
825
+ // ──────────────────────────────────────────────
826
+ // clauth invite generate|list|revoke
827
+ // ──────────────────────────────────────────────
828
+ const invite = program.command("invite").description("Manage vault invites");
829
+
830
+ invite
831
+ .command("generate")
832
+ .description("Generate an invite code for a friend")
833
+ .option("--uses <n>", "Max redemptions", "1")
834
+ .option("--expires <hours>", "Expiry in hours", "168")
835
+ .action(async (opts) => {
836
+ const { runInvite } = await import("./commands/invite.js");
837
+ await runInvite("generate", opts);
838
+ });
839
+
840
+ invite
841
+ .command("list")
842
+ .description("List active invites")
843
+ .action(async () => {
844
+ const { runInvite } = await import("./commands/invite.js");
845
+ await runInvite("list", {});
846
+ });
847
+
848
+ invite
849
+ .command("revoke <code>")
850
+ .description("Revoke an invite code")
851
+ .action(async (code) => {
852
+ const { runInvite } = await import("./commands/invite.js");
853
+ await runInvite("revoke", { code });
854
+ });
855
+
856
+ // ──────────────────────────────────────────────
857
+ // clauth join <invite-code>
858
+ // ──────────────────────────────────────────────
859
+ program
860
+ .command("join <invite-code>")
861
+ .description("Join a vault using an invite code from a friend")
862
+ .action(async (code) => {
863
+ const { runJoin } = await import("./commands/join.js");
864
+ await runJoin(code);
865
+ });
866
+
867
+ // ──────────────────────────────────────────────
868
+ // clauth update
869
+ // ──────────────────────────────────────────────
870
+ program
871
+ .command("update")
872
+ .description("Update clauth to the latest version")
873
+ .action(async () => {
874
+ const { execSync } = await import("child_process");
875
+ console.log(chalk.cyan("\n Updating clauth...\n"));
876
+ try {
877
+ execSync("npm install -g @lifeaitools/clauth@latest", { stdio: "inherit" });
878
+ console.log(chalk.green("\n Updated successfully.\n"));
879
+ } catch (err) {
880
+ console.log(chalk.red(`\n Update failed: ${err.message}\n`));
881
+ }
882
+ });
883
+
884
+ // ──────────────────────────────────────────────
885
+ // clauth tunnel start|stop|status
886
+ // (setup moved to in-browser wizard at http://127.0.0.1:52437)
887
+ // ──────────────────────────────────────────────
888
+ const tunnelCmd = program.command("tunnel").description("Manage Cloudflare tunnel for claude.ai web integration");
889
+
890
+ tunnelCmd
891
+ .command("setup")
892
+ .description("Open the tunnel setup wizard in your browser")
893
+ .action(async () => {
894
+ console.log(chalk.cyan("\n Tunnel setup is now handled in the browser.\n"));
895
+ console.log(chalk.white(" 1. Start the daemon: clauth serve start"));
896
+ console.log(chalk.white(" 2. Open: http://127.0.0.1:52437"));
897
+ console.log(chalk.white(" 3. Unlock the vault and click \"Setup Tunnel\"\n"));
898
+ });
899
+
900
+ tunnelCmd
901
+ .command("start")
902
+ .description("Tell daemon to start the tunnel")
903
+ .action(async () => {
904
+ try {
905
+ const r = await fetch("http://127.0.0.1:52437/tunnel/start", {
906
+ method: "POST",
907
+ headers: { "Content-Type": "application/json" },
908
+ signal: AbortSignal.timeout(5000),
909
+ });
910
+ const data = await r.json().catch(() => ({}));
911
+ if (!r.ok) {
912
+ console.error(` ✗ ${data.error || r.statusText}`);
913
+ if (r.status === 401) console.error(" Unlock the daemon first: http://127.0.0.1:52437");
914
+ process.exit(1);
915
+ }
916
+ console.log(` ✓ ${data.message || "Tunnel starting — check status with: clauth tunnel status"}`);
917
+ } catch (e) {
918
+ console.error(" ✗ Daemon not running. Start it with: clauth serve");
919
+ process.exit(1);
920
+ }
921
+ });
922
+
923
+ tunnelCmd
924
+ .command("stop")
925
+ .description("Tell daemon to stop the tunnel")
926
+ .action(async () => {
927
+ try {
928
+ const r = await fetch("http://127.0.0.1:52437/tunnel/stop", {
929
+ method: "POST",
930
+ headers: { "Content-Type": "application/json" },
931
+ signal: AbortSignal.timeout(5000),
932
+ });
933
+ const data = await r.json().catch(() => ({}));
934
+ if (!r.ok) {
935
+ console.error(` ✗ ${data.error || r.statusText}`);
936
+ process.exit(1);
937
+ }
938
+ console.log(" ✓ Tunnel stopped.");
939
+ } catch (e) {
940
+ console.error(" ✗ Daemon not running.");
941
+ process.exit(1);
942
+ }
943
+ });
944
+
945
+ tunnelCmd
946
+ .command("status")
947
+ .description("Show current tunnel status")
948
+ .action(async () => {
949
+ try {
950
+ const r = await fetch("http://127.0.0.1:52437/tunnel", {
951
+ signal: AbortSignal.timeout(5000),
952
+ });
953
+ const data = await r.json().catch(() => ({}));
954
+ const icons = {
955
+ live: "✓", starting: "◌", not_configured: "⚠",
956
+ not_started: "○", error: "✗", missing_cloudflared: "✗",
957
+ };
958
+ const labels = {
959
+ live: `Live — ${data.url || ""}`,
960
+ starting: "Starting...",
961
+ not_configured: "Not configured — open http://127.0.0.1:52437 and click Setup Tunnel",
962
+ not_started: "Not started — run: clauth tunnel start",
963
+ error: `Error${data.error ? ": " + data.error : ""} — check cloudflared config`,
964
+ missing_cloudflared: "cloudflared not installed — https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/",
965
+ };
966
+ const status = data.status || "unknown";
967
+ console.log(`\n ${icons[status] || "?"} Tunnel: ${labels[status] || status}\n`);
968
+ } catch (e) {
969
+ console.error(" ✗ Daemon not running. Start it with: clauth serve");
970
+ process.exit(1);
971
+ }
972
+ });
973
+
974
+ // ──────────────────────────────────────────────
975
+ // clauth chitchat --session <id>
976
+ // ──────────────────────────────────────────────
977
+ program
978
+ .command("chitchat")
979
+ .description("Join a chitchat collab session — auto-starts /rdc:collab via claude -p")
980
+ .requiredOption("--session <id>", "Session ID from claude.ai")
981
+ .action(async (opts) => {
982
+ const id = opts.session;
983
+ // Verify session exists in the daemon
984
+ try {
985
+ const r = await fetch(`http://127.0.0.1:52437/chitchat/${id}`, { signal: AbortSignal.timeout(3000) });
986
+ if (!r.ok) {
987
+ console.error(`\n ✗ Session ${id} not found (daemon returned ${r.status})\n`);
988
+ process.exit(1);
989
+ }
990
+ } catch (e) {
991
+ console.error(`\n ✗ Daemon not reachable at http://127.0.0.1:52437 — is clauth running?\n`);
992
+ process.exit(1);
993
+ }
994
+ console.log(chalk.cyan(`\n [collab] session ${id} — starting /rdc:collab...\n`));
995
+
996
+ // Find claude binary (same candidates as daemon)
997
+ const { execSync: es, spawn } = await import("child_process");
998
+ let claudeBin = null;
999
+ for (const c of [
1000
+ process.env.CLAUDE_BIN,
1001
+ path.join(process.env.APPDATA || '', 'npm', 'claude.cmd'),
1002
+ path.join(process.env.APPDATA || '', 'npm', 'claude'),
1003
+ 'claude',
1004
+ ].filter(Boolean)) {
1005
+ try { es(`"${c}" --version`, { stdio: 'ignore', timeout: 3000 }); claudeBin = c; break; } catch {}
1006
+ }
1007
+ if (!claudeBin) {
1008
+ console.error(' ✗ claude CLI not found — is @anthropic-ai/claude-code installed globally?');
1009
+ process.exit(1);
1010
+ }
1011
+
1012
+ // Auto-invoke /rdc:collab skill with streaming output to this terminal
1013
+ const proc = spawn(claudeBin, [
1014
+ '-p', `/rdc:collab --session ${id}`,
1015
+ '--dangerously-skip-permissions',
1016
+ ], {
1017
+ stdio: 'inherit',
1018
+ cwd: 'C:/Dev/regen-root',
1019
+ shell: true,
1020
+ });
1021
+ proc.on('error', e => { console.error(` ✗ spawn error: ${e.message}`); process.exit(1); });
1022
+ proc.on('exit', code => process.exit(code ?? 0));
1023
+ });
1024
+
1025
+ // ──────────────────────────────────────────────
1026
+ // clauth --help override banner
1027
+ // ──────────────────────────────────────────────
1028
+ program.addHelpText("beforeAll", chalk.cyan(`
1029
+ ██████╗██╗ █████╗ ██╗ ██╗████████╗██╗ ██╗
1030
+ ██╔════╝██║ ██╔══██╗██║ ██║╚══██╔══╝██║ ██║
1031
+ ██║ ██║ ███████║██║ ██║ ██║ ███████║
1032
+ ██║ ██║ ██╔══██║██║ ██║ ██║ ██╔══██║
1033
+ ╚██████╗███████╗██║ ██║╚██████╔╝ ██║ ██║ ██║
1034
+ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝
1035
+ v${VERSION} — LIFEAI Credential Vault
1036
+ `));
1037
+
1038
+ // ──────────────────────────────────────────────
1039
+ // clauth serve [action]
1040
+ // ──────────────────────────────────────────────
1041
+ program
1042
+ .command("serve [action]")
1043
+ .description("Manage localhost HTTP vault daemon (start|stop|restart|ping|install|uninstall)")
1044
+ .option("--port <n>", "Port (default: 52437)")
1045
+ .option("-p, --pw <password>", "clauth password (optional — omit to start locked, unlock in browser)")
1046
+ .option("--services <list>", "Comma-separated service whitelist (default: all)")
1047
+ .option("--tunnel <hostname>", "Fixed tunnel hostname (e.g. clauth.prtrust.fund) — uses named Cloudflare Tunnel instead of random URL")
1048
+ .option("--staged", "Start on staging port (52438) for blue-green verification before make-live")
1049
+ .option("--isolated", "Run on a non-live port without touching live PID files, browser, or boot-key credentials")
1050
+ .option("--from-boot-key", "Internal: password came from boot.key auto-unlock (degrade gracefully on verify failure)")
1051
+ .option("--action <action>", "Internal: action override for daemon child")
1052
+ .addHelpText("after", `
1053
+ Actions:
1054
+ start Start the server as a background daemon
1055
+ stop Stop the running daemon
1056
+ restart Stop + start
1057
+ ping Check if the daemon is running
1058
+ foreground Run in foreground (Ctrl+C to stop) — default if no action given
1059
+ mcp Run as MCP stdio server for Claude Code (JSON-RPC over stdin/stdout)
1060
+ install Store password securely + register auto-start service (cross-platform)
1061
+ Windows: DPAPI + HKCU\\Run | macOS: Keychain + LaunchAgent | Linux: libsecret/openssl + systemd
1062
+ uninstall Remove auto-start service + delete stored password
1063
+ upgrade Blue-green upgrade: start new version on staging port, verify, then make live
1064
+
1065
+ MCP SSE (built into start/foreground):
1066
+ The HTTP daemon also serves MCP SSE transport at GET /sse + POST /message.
1067
+ Connect claude.ai via Cloudflare Tunnel pointing to http://127.0.0.1:52437/sse
1068
+
1069
+ Examples:
1070
+ clauth serve start Start locked — unlock at http://127.0.0.1:52437
1071
+ clauth serve start -p mypass Start pre-unlocked (password in memory only)
1072
+ clauth serve stop Stop the daemon
1073
+ clauth serve ping Check status
1074
+ clauth serve restart Restart (stays locked until browser unlock)
1075
+ clauth serve start --services github,vercel
1076
+ clauth serve mcp Start MCP server for Claude Code
1077
+ clauth serve mcp -p mypass Start MCP server pre-unlocked
1078
+ clauth serve foreground --port 53137 --isolated
1079
+ Start isolated passwordless server for route tests
1080
+ clauth serve install Set up auto-start on login (DPAPI/Keychain/libsecret)
1081
+ clauth serve install --tunnel host Auto-start with Cloudflare Tunnel
1082
+ clauth serve uninstall Remove auto-start
1083
+ `)
1084
+ .action(async (action, opts) => {
1085
+ const resolvedAction = opts.action || action || "foreground";
1086
+ await runServe({ ...opts, action: resolvedAction });
1087
+ });
1088
+
1089
+ program.parse(process.argv);