@lifeaitools/clauth 1.30.23 → 1.30.25

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