@lifeaitools/clauth 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,378 @@
1
+ // cli/commands/serve.js
2
+ // Localhost-only credential daemon with daemon lifecycle management
3
+ // Binds 127.0.0.1 ONLY — unreachable from outside the machine
4
+ // 3 failed requests of any kind → process exits, requires manual restart
5
+ // Supports: start (background daemon), stop, restart, ping, foreground
6
+
7
+ import http from "http";
8
+ import fs from "fs";
9
+ import os from "os";
10
+ import path from "path";
11
+ import { getMachineHash, deriveToken } from "../fingerprint.js";
12
+ import * as api from "../api.js";
13
+ import chalk from "chalk";
14
+
15
+ const PID_FILE = path.join(os.tmpdir(), "clauth-serve.pid");
16
+ const LOG_FILE = path.join(os.tmpdir(), "clauth-serve.log");
17
+
18
+ // ── PID helpers ──────────────────────────────────────────────
19
+ function readPid() {
20
+ try {
21
+ const raw = fs.readFileSync(PID_FILE, "utf8").trim();
22
+ const [pid, port] = raw.split(":");
23
+ return { pid: parseInt(pid, 10), port: parseInt(port, 10) };
24
+ } catch { return null; }
25
+ }
26
+
27
+ function writePid(pid, port) {
28
+ fs.writeFileSync(PID_FILE, `${pid}:${port}`, "utf8");
29
+ }
30
+
31
+ function removePid() {
32
+ try { fs.unlinkSync(PID_FILE); } catch {}
33
+ }
34
+
35
+ function isProcessAlive(pid) {
36
+ try { process.kill(pid, 0); return true; } catch { return false; }
37
+ }
38
+
39
+ // ── Server logic (shared by foreground + daemon) ─────────────
40
+ function createServer(password, whitelist, port) {
41
+ const MAX_FAILS = 3;
42
+ let failCount = 0;
43
+ const machineHash = getMachineHash();
44
+
45
+ function strike(res, code, message) {
46
+ failCount++;
47
+ const remaining = MAX_FAILS - failCount;
48
+ const logLine = `[${new Date().toISOString()}] [FAIL ${failCount}/${MAX_FAILS}] ${message}\n`;
49
+ try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
50
+
51
+ const body = JSON.stringify({
52
+ error: message,
53
+ failures: failCount,
54
+ failures_remaining: remaining,
55
+ ...(failCount >= MAX_FAILS ? { shutdown: true } : {})
56
+ });
57
+
58
+ res.writeHead(code, { "Content-Type": "application/json" });
59
+ res.end(body);
60
+
61
+ if (failCount >= MAX_FAILS) {
62
+ const msg = `[${new Date().toISOString()}] Failure limit reached — shutting down\n`;
63
+ try { fs.appendFileSync(LOG_FILE, msg); } catch {}
64
+ removePid();
65
+ setTimeout(() => process.exit(1), 100);
66
+ }
67
+ }
68
+
69
+ function ok(res, data) {
70
+ res.writeHead(200, { "Content-Type": "application/json" });
71
+ res.end(JSON.stringify(data));
72
+ }
73
+
74
+ const server = http.createServer(async (req, res) => {
75
+ // Hard reject anything not from loopback
76
+ const remote = req.socket.remoteAddress;
77
+ const isLocal = remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1";
78
+ if (!isLocal) {
79
+ return strike(res, 403, `Rejected non-local address: ${remote}`);
80
+ }
81
+
82
+ const url = new URL(req.url, `http://127.0.0.1:${port}`);
83
+ const reqPath = url.pathname;
84
+ const method = req.method;
85
+
86
+ // GET /ping
87
+ if (method === "GET" && reqPath === "/ping") {
88
+ return ok(res, {
89
+ status: "ok",
90
+ pid: process.pid,
91
+ failures: failCount,
92
+ failures_remaining: MAX_FAILS - failCount,
93
+ services: whitelist || "all",
94
+ port
95
+ });
96
+ }
97
+
98
+ // GET /shutdown (for daemon stop)
99
+ if (method === "GET" && reqPath === "/shutdown") {
100
+ ok(res, { ok: true, message: "shutting down" });
101
+ removePid();
102
+ setTimeout(() => process.exit(0), 100);
103
+ return;
104
+ }
105
+
106
+ // GET /status
107
+ if (method === "GET" && reqPath === "/status") {
108
+ try {
109
+ const { token, timestamp } = deriveToken(password, machineHash);
110
+ const result = await api.status(password, machineHash, token, timestamp);
111
+ if (result.error) return strike(res, 502, result.error);
112
+ if (whitelist) {
113
+ result.services = (result.services || []).filter(
114
+ s => whitelist.includes(s.name.toLowerCase())
115
+ );
116
+ }
117
+ return ok(res, result);
118
+ } catch (err) {
119
+ return strike(res, 502, err.message);
120
+ }
121
+ }
122
+
123
+ // GET /get/:service
124
+ const getMatch = reqPath.match(/^\/get\/([a-zA-Z0-9_-]+)$/);
125
+ if (method === "GET" && getMatch) {
126
+ const service = getMatch[1].toLowerCase();
127
+
128
+ if (whitelist && !whitelist.includes(service)) {
129
+ return strike(res, 403, `Service '${service}' not in whitelist`);
130
+ }
131
+
132
+ try {
133
+ const { token, timestamp } = deriveToken(password, machineHash);
134
+ const result = await api.retrieve(password, machineHash, token, timestamp, service);
135
+ if (result.error) return strike(res, 502, result.error);
136
+ return ok(res, { service, value: result.value, key_type: result.key_type });
137
+ } catch (err) {
138
+ return strike(res, 502, err.message);
139
+ }
140
+ }
141
+
142
+ // Unknown route
143
+ return strike(res, 404, `Unknown endpoint: ${reqPath}`);
144
+ });
145
+
146
+ return server;
147
+ }
148
+
149
+ // ── Actions ──────────────────────────────────────────────────
150
+
151
+ async function verifyAuth(password) {
152
+ const machineHash = getMachineHash();
153
+ const { token, timestamp } = deriveToken(password, machineHash);
154
+ const result = await api.test(password, machineHash, token, timestamp);
155
+ if (result.error) throw new Error(result.error);
156
+ }
157
+
158
+ async function actionStart(opts) {
159
+ const port = parseInt(opts.port || "52437", 10);
160
+ const password = opts.pw;
161
+ const whitelist = opts.services
162
+ ? opts.services.split(",").map(s => s.trim().toLowerCase())
163
+ : null;
164
+
165
+ // Check for existing instance
166
+ const existing = readPid();
167
+ if (existing && isProcessAlive(existing.pid)) {
168
+ console.log(chalk.yellow(`\n clauth serve already running (PID ${existing.pid}, port ${existing.port})`));
169
+ console.log(chalk.gray(` Stop it first: clauth serve stop\n`));
170
+ process.exit(1);
171
+ }
172
+
173
+ // If we're the daemon child, run the server directly
174
+ if (process.env.__CLAUTH_DAEMON === "1") {
175
+ try {
176
+ await verifyAuth(password);
177
+ } catch (err) {
178
+ const msg = `[${new Date().toISOString()}] Auth failed: ${err.message}\n`;
179
+ fs.appendFileSync(LOG_FILE, msg);
180
+ process.exit(1);
181
+ }
182
+
183
+ const server = createServer(password, whitelist, port);
184
+ server.listen(port, "127.0.0.1", () => {
185
+ writePid(process.pid, port);
186
+ const msg = `[${new Date().toISOString()}] clauth serve started — PID ${process.pid}, port ${port}, services: ${whitelist ? whitelist.join(",") : "all"}\n`;
187
+ fs.appendFileSync(LOG_FILE, msg);
188
+ });
189
+
190
+ server.on("error", err => {
191
+ const msg = `[${new Date().toISOString()}] Server error: ${err.message}\n`;
192
+ fs.appendFileSync(LOG_FILE, msg);
193
+ process.exit(1);
194
+ });
195
+
196
+ const shutdown = () => { removePid(); process.exit(0); };
197
+ process.on("SIGTERM", shutdown);
198
+ process.on("SIGINT", shutdown);
199
+ return;
200
+ }
201
+
202
+ // Parent: verify auth first (show errors to user)
203
+ console.log(chalk.gray("\n Verifying vault credentials..."));
204
+ try {
205
+ await verifyAuth(password);
206
+ } catch (err) {
207
+ console.log(chalk.red(`\n Auth failed: ${err.message}\n`));
208
+ process.exit(1);
209
+ }
210
+ console.log(chalk.green(" ✓ Vault auth verified"));
211
+
212
+ // Spawn detached daemon child
213
+ const { spawn } = await import("child_process");
214
+ const { fileURLToPath } = await import("url");
215
+ const __filename = fileURLToPath(import.meta.url);
216
+
217
+ // Build args: node serve.js --action start --port N --pw PW [--services S]
218
+ const childArgs = [__filename, "--action", "start", "--port", String(port), "--pw", password];
219
+ if (opts.services) childArgs.push("--services", opts.services);
220
+
221
+ const out = fs.openSync(LOG_FILE, "a");
222
+ const child = spawn(process.execPath, childArgs, {
223
+ detached: true,
224
+ stdio: ["ignore", out, out],
225
+ env: { ...process.env, __CLAUTH_DAEMON: "1" },
226
+ });
227
+ child.unref();
228
+
229
+ // Give it a moment then verify
230
+ await new Promise(r => setTimeout(r, 800));
231
+
232
+ const info = readPid();
233
+ if (info && isProcessAlive(info.pid)) {
234
+ console.log(chalk.green(`\n 🔐 clauth serve started`));
235
+ console.log(chalk.gray(` PID: ${info.pid}`));
236
+ console.log(chalk.gray(` Port: 127.0.0.1:${info.port}`));
237
+ console.log(chalk.gray(` Services: ${whitelist ? whitelist.join(", ") : "all"}`));
238
+ console.log(chalk.gray(` Log: ${LOG_FILE}`));
239
+ console.log(chalk.gray(` Stop: clauth serve stop\n`));
240
+ } else {
241
+ console.log(chalk.red(`\n ❌ Failed to start daemon — check ${LOG_FILE}\n`));
242
+ process.exit(1);
243
+ }
244
+ }
245
+
246
+ async function actionStop() {
247
+ const info = readPid();
248
+ if (!info) {
249
+ console.log(chalk.yellow("\n No clauth serve PID file found — not running.\n"));
250
+ return;
251
+ }
252
+
253
+ if (!isProcessAlive(info.pid)) {
254
+ console.log(chalk.yellow(`\n PID ${info.pid} is not running (stale PID file). Cleaning up.\n`));
255
+ removePid();
256
+ return;
257
+ }
258
+
259
+ // Try HTTP shutdown first (clean)
260
+ try {
261
+ const resp = await fetch(`http://127.0.0.1:${info.port}/shutdown`);
262
+ if (resp.ok) {
263
+ await new Promise(r => setTimeout(r, 300));
264
+ console.log(chalk.green(`\n 🛑 clauth serve stopped (was PID ${info.pid}, port ${info.port})\n`));
265
+ removePid();
266
+ return;
267
+ }
268
+ } catch {}
269
+
270
+ // Fallback: kill the process
271
+ try {
272
+ process.kill(info.pid, "SIGTERM");
273
+ await new Promise(r => setTimeout(r, 300));
274
+ console.log(chalk.green(`\n 🛑 clauth serve stopped via SIGTERM (PID ${info.pid})\n`));
275
+ } catch (err) {
276
+ console.log(chalk.yellow(`\n Could not kill PID ${info.pid}: ${err.message}\n`));
277
+ }
278
+ removePid();
279
+ }
280
+
281
+ async function actionPing() {
282
+ const info = readPid();
283
+ if (!info) {
284
+ console.log(chalk.red("\n clauth serve is not running (no PID file)\n"));
285
+ process.exit(1);
286
+ }
287
+
288
+ if (!isProcessAlive(info.pid)) {
289
+ console.log(chalk.red(`\n PID ${info.pid} is not alive (stale PID file)\n`));
290
+ removePid();
291
+ process.exit(1);
292
+ }
293
+
294
+ try {
295
+ const resp = await fetch(`http://127.0.0.1:${info.port}/ping`);
296
+ const data = await resp.json();
297
+ if (data.status === "ok") {
298
+ console.log(chalk.green(`\n ✅ clauth serve running`));
299
+ console.log(chalk.gray(` PID: ${info.pid}`));
300
+ console.log(chalk.gray(` Port: ${info.port}`));
301
+ console.log(chalk.gray(` Fails: ${data.failures}/${data.failures + data.failures_remaining}`));
302
+ console.log(chalk.gray(` Services: ${Array.isArray(data.services) ? data.services.join(", ") : data.services}\n`));
303
+ } else {
304
+ console.log(chalk.yellow(`\n PID alive but /ping returned unexpected response\n`));
305
+ }
306
+ } catch (err) {
307
+ console.log(chalk.yellow(`\n PID ${info.pid} alive but HTTP failed: ${err.message}\n`));
308
+ }
309
+ }
310
+
311
+ async function actionRestart(opts) {
312
+ const info = readPid();
313
+ if (info && isProcessAlive(info.pid)) {
314
+ await actionStop();
315
+ await new Promise(r => setTimeout(r, 500));
316
+ }
317
+ await actionStart(opts);
318
+ }
319
+
320
+ async function actionForeground(opts) {
321
+ const port = parseInt(opts.port || "52437", 10);
322
+ const password = opts.pw;
323
+ const whitelist = opts.services
324
+ ? opts.services.split(",").map(s => s.trim().toLowerCase())
325
+ : null;
326
+
327
+ console.log(chalk.gray("\n Verifying vault credentials..."));
328
+ try {
329
+ await verifyAuth(password);
330
+ } catch (err) {
331
+ console.log(chalk.red(`\n Auth failed: ${err.message}\n`));
332
+ process.exit(1);
333
+ }
334
+
335
+ console.log(chalk.green(" ✓ Vault auth verified"));
336
+ console.log(chalk.gray(` Port: 127.0.0.1:${port}`));
337
+ console.log(chalk.gray(` Services: ${whitelist ? whitelist.join(", ") : "all"}`));
338
+ console.log(chalk.gray(` Lockout: 3 failures → exit\n`));
339
+
340
+ const server = createServer(password, whitelist, port);
341
+ server.listen(port, "127.0.0.1", () => {
342
+ writePid(process.pid, port);
343
+ console.log(chalk.green(` clauth serve → http://127.0.0.1:${port}`));
344
+ console.log(chalk.gray(" Ctrl+C to stop\n"));
345
+ });
346
+
347
+ server.on("error", err => {
348
+ if (err.code === "EADDRINUSE") {
349
+ console.log(chalk.red(`\n Port ${port} already in use. Use --port to choose another.\n`));
350
+ } else {
351
+ console.log(chalk.red(`\n Server error: ${err.message}\n`));
352
+ }
353
+ process.exit(1);
354
+ });
355
+
356
+ process.on("SIGINT", () => {
357
+ console.log(chalk.yellow("\n Stopping clauth serve...\n"));
358
+ removePid();
359
+ server.close(() => process.exit(0));
360
+ });
361
+ }
362
+
363
+ // ── Export ────────────────────────────────────────────────────
364
+ export async function runServe(opts) {
365
+ const action = opts.action || "foreground";
366
+
367
+ switch (action) {
368
+ case "start": return actionStart(opts);
369
+ case "stop": return actionStop();
370
+ case "restart": return actionRestart(opts);
371
+ case "ping": return actionPing();
372
+ case "foreground": return actionForeground(opts);
373
+ default:
374
+ console.log(chalk.red(`\n Unknown serve action: ${action}`));
375
+ console.log(chalk.gray(" Actions: start | stop | restart | ping | foreground\n"));
376
+ process.exit(1);
377
+ }
378
+ }
@@ -0,0 +1,164 @@
1
+ // cli/commands/uninstall.js
2
+ // clauth uninstall — full teardown: DB objects, Edge Function, secrets, skill, local config
3
+ //
4
+ // Reverses everything `clauth install` does:
5
+ // 1. Drops clauth tables, policies, triggers, functions from Supabase
6
+ // 2. Deletes auth-vault Edge Function
7
+ // 3. Removes CLAUTH_* secrets
8
+ // 4. Removes Claude skill directory
9
+ // 5. Clears local config (Conf store)
10
+
11
+ import { existsSync, rmSync } from 'fs';
12
+ import { join } from 'path';
13
+ import Conf from 'conf';
14
+ import chalk from 'chalk';
15
+ import ora from 'ora';
16
+
17
+ const MGMT = 'https://api.supabase.com/v1';
18
+ const SKILLS_DIR = process.env.CLAUTH_SKILLS_DIR ||
19
+ (process.platform === 'win32'
20
+ ? join(process.env.USERPROFILE || '', '.claude', 'skills')
21
+ : join(process.env.HOME || '', '.claude', 'skills'));
22
+
23
+ // ─────────────────────────────────────────────
24
+ // Supabase Management API helper
25
+ // ─────────────────────────────────────────────
26
+ async function mgmt(pat, method, path, body) {
27
+ const res = await fetch(`${MGMT}${path}`, {
28
+ method,
29
+ headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
30
+ body: body ? JSON.stringify(body) : undefined,
31
+ });
32
+ if (!res.ok) {
33
+ const text = await res.text().catch(() => res.statusText);
34
+ throw new Error(`${method} ${path} → HTTP ${res.status}: ${text}`);
35
+ }
36
+ if (res.status === 204) return {};
37
+ const text = await res.text();
38
+ if (!text) return {};
39
+ return JSON.parse(text);
40
+ }
41
+
42
+ // ─────────────────────────────────────────────
43
+ // Main uninstall command
44
+ // ─────────────────────────────────────────────
45
+ export async function runUninstall(opts = {}) {
46
+ console.log(chalk.red('\n🗑️ clauth uninstall\n'));
47
+
48
+ const config = new Conf({ projectName: 'clauth' });
49
+
50
+ // ── Collect credentials ────────────────────
51
+ const ref = opts.ref || config.get('supabase_url')?.match(/https:\/\/(.+)\.supabase\.co/)?.[1];
52
+ const pat = opts.pat;
53
+
54
+ if (!ref) {
55
+ console.log(chalk.red(' Cannot determine Supabase project ref.'));
56
+ console.log(chalk.gray(' Use: clauth uninstall --ref <project-ref> --pat <personal-access-token>'));
57
+ process.exit(1);
58
+ }
59
+ if (!pat) {
60
+ console.log(chalk.red(' Supabase PAT required for teardown.'));
61
+ console.log(chalk.gray(' Use: clauth uninstall --ref <project-ref> --pat <personal-access-token>'));
62
+ process.exit(1);
63
+ }
64
+
65
+ console.log(chalk.gray(` Project: ${ref}\n`));
66
+
67
+ // ── Step 1: Drop database objects ──────────
68
+ const s1 = ora('Dropping clauth database objects...').start();
69
+ const teardownSQL = `
70
+ -- Drop triggers
71
+ DROP TRIGGER IF EXISTS clauth_services_updated ON public.clauth_services;
72
+
73
+ -- Drop tables (CASCADE drops policies automatically)
74
+ DROP TABLE IF EXISTS public.clauth_audit CASCADE;
75
+ DROP TABLE IF EXISTS public.clauth_machines CASCADE;
76
+ DROP TABLE IF EXISTS public.clauth_services CASCADE;
77
+
78
+ -- Drop functions
79
+ DROP FUNCTION IF EXISTS public.clauth_touch_updated() CASCADE;
80
+ DROP FUNCTION IF EXISTS public.clauth_upsert_vault_secret(text, text) CASCADE;
81
+ DROP FUNCTION IF EXISTS public.clauth_get_vault_secret(text) CASCADE;
82
+ DROP FUNCTION IF EXISTS public.clauth_delete_vault_secret(text) CASCADE;
83
+ `;
84
+
85
+ try {
86
+ await mgmt(pat, 'POST', `/projects/${ref}/database/query`, { query: teardownSQL });
87
+ s1.succeed('Database objects dropped (tables, triggers, functions, policies)');
88
+ } catch (e) {
89
+ s1.fail(`Database teardown failed: ${e.message}`);
90
+ console.log(chalk.yellow(' You may need to drop objects manually via SQL editor.'));
91
+ }
92
+
93
+ // ── Step 2: Delete Edge Function ───────────
94
+ const s2 = ora('Deleting auth-vault Edge Function...').start();
95
+ try {
96
+ const res = await fetch(`${MGMT}/projects/${ref}/functions/auth-vault`, {
97
+ method: 'DELETE',
98
+ headers: { 'Authorization': `Bearer ${pat}` },
99
+ });
100
+ if (res.ok || res.status === 404) {
101
+ s2.succeed(res.status === 404
102
+ ? 'Edge Function not found (already deleted)'
103
+ : 'Edge Function deleted');
104
+ } else {
105
+ const text = await res.text().catch(() => res.statusText);
106
+ throw new Error(`HTTP ${res.status}: ${text}`);
107
+ }
108
+ } catch (e) {
109
+ s2.fail(`Edge Function delete failed: ${e.message}`);
110
+ console.log(chalk.yellow(' Delete manually: Supabase Dashboard → Edge Functions → auth-vault → Delete'));
111
+ }
112
+
113
+ // ── Step 3: Remove secrets ─────────────────
114
+ const s3 = ora('Removing clauth secrets...').start();
115
+ try {
116
+ // Supabase Management API: DELETE /projects/{ref}/secrets with body listing secret names
117
+ const res = await fetch(`${MGMT}/projects/${ref}/secrets`, {
118
+ method: 'DELETE',
119
+ headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
120
+ body: JSON.stringify(['CLAUTH_HMAC_SALT', 'CLAUTH_ADMIN_BOOTSTRAP_TOKEN']),
121
+ });
122
+ if (res.ok) {
123
+ s3.succeed('Secrets removed (CLAUTH_HMAC_SALT, CLAUTH_ADMIN_BOOTSTRAP_TOKEN)');
124
+ } else {
125
+ const text = await res.text().catch(() => res.statusText);
126
+ throw new Error(`HTTP ${res.status}: ${text}`);
127
+ }
128
+ } catch (e) {
129
+ s3.warn(`Secret removal failed: ${e.message}`);
130
+ console.log(chalk.yellow(' Remove manually: Supabase → Settings → Edge Functions → Secrets'));
131
+ }
132
+
133
+ // ── Step 4: Remove Claude skill ────────────
134
+ const s4 = ora('Removing Claude skill...').start();
135
+ const skillDir = join(SKILLS_DIR, 'clauth');
136
+ if (existsSync(skillDir)) {
137
+ try {
138
+ rmSync(skillDir, { recursive: true, force: true });
139
+ s4.succeed(`Skill removed: ${skillDir}`);
140
+ } catch (e) {
141
+ s4.warn(`Could not remove skill: ${e.message}`);
142
+ }
143
+ } else {
144
+ s4.succeed('Skill directory not found (already removed)');
145
+ }
146
+
147
+ // ── Step 5: Clear local config ─────────────
148
+ const s5 = ora('Clearing local config...').start();
149
+ try {
150
+ config.clear();
151
+ s5.succeed('Local config cleared');
152
+ } catch (e) {
153
+ s5.warn(`Could not clear config: ${e.message}`);
154
+ }
155
+
156
+ // ── Done ───────────────────────────────────
157
+ console.log('');
158
+ console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
159
+ console.log(chalk.yellow(' ✓ clauth fully uninstalled'));
160
+ console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
161
+ console.log('');
162
+ console.log(chalk.gray(' To reinstall: npx @lifeaitools/clauth install'));
163
+ console.log('');
164
+ }
package/cli/index.js CHANGED
@@ -11,7 +11,7 @@ import * as api from "./api.js";
11
11
  import os from "os";
12
12
 
13
13
  const config = new Conf({ projectName: "clauth" });
14
- const VERSION = "0.2.0";
14
+ const VERSION = "0.3.0";
15
15
 
16
16
  // ============================================================
17
17
  // Password prompt helper
@@ -51,6 +51,9 @@ program
51
51
  // clauth install (Supabase provisioning + skill install + test)
52
52
  // ──────────────────────────────────────────────
53
53
  import { runInstall } from './commands/install.js';
54
+ import { runUninstall } from './commands/uninstall.js';
55
+ import { runScrub } from './commands/scrub.js';
56
+ import { runServe } from './commands/serve.js';
54
57
 
55
58
  program
56
59
  .command('install')
@@ -61,6 +64,28 @@ program
61
64
  await runInstall(opts);
62
65
  });
63
66
 
67
+ program
68
+ .command('uninstall')
69
+ .description('Full teardown — drop DB objects, Edge Function, secrets, skill, config')
70
+ .option('--ref <ref>', 'Supabase project ref')
71
+ .option('--pat <pat>', 'Supabase Personal Access Token (required)')
72
+ .option('--yes', 'Skip confirmation prompt')
73
+ .action(async (opts) => {
74
+ if (!opts.yes) {
75
+ const inquirerMod = await import('inquirer');
76
+ const { confirm } = await inquirerMod.default.prompt([{
77
+ type: 'input',
78
+ name: 'confirm',
79
+ message: chalk.red('Type "CONFIRM UNINSTALL" to proceed:'),
80
+ }]);
81
+ if (confirm !== 'CONFIRM UNINSTALL') {
82
+ console.log(chalk.yellow('\n Uninstall cancelled.\n'));
83
+ process.exit(0);
84
+ }
85
+ }
86
+ await runUninstall(opts);
87
+ });
88
+
64
89
  // ──────────────────────────────────────────────
65
90
  // clauth setup
66
91
  // ──────────────────────────────────────────────
@@ -273,11 +298,17 @@ addCmd
273
298
  .option("-p, --pw <password>")
274
299
  .action(async (name, opts) => {
275
300
  const auth = await getAuth(opts.pw);
276
- const answers = await inquirer.prompt([
277
- { type: "input", name: "label", message: "Label:", default: opts.label || name },
278
- { type: "list", name: "key_type", message: "Key type:", choices: ["token","keypair","connstring","oauth"], default: opts.type || "token" },
279
- { type: "input", name: "desc", message: "Description (optional):", default: opts.description || "" }
280
- ]);
301
+ let answers;
302
+ if (opts.type && opts.label) {
303
+ // Non-interactive all flags provided
304
+ answers = { label: opts.label, key_type: opts.type, desc: opts.description || "" };
305
+ } else {
306
+ answers = await inquirer.prompt([
307
+ { type: "input", name: "label", message: "Label:", default: opts.label || name },
308
+ { type: "list", name: "key_type", message: "Key type:", choices: ["token","keypair","connstring","oauth"], default: opts.type || "token" },
309
+ { type: "input", name: "desc", message: "Description (optional):", default: opts.description || "" }
310
+ ]);
311
+ }
281
312
  const spinner = ora(`Adding service: ${name}...`).start();
282
313
  try {
283
314
  const result = await api.addService(
@@ -396,6 +427,24 @@ program
396
427
  } catch (err) { spinner.fail(chalk.red(err.message)); }
397
428
  });
398
429
 
430
+ // ──────────────────────────────────────────────
431
+ // clauth scrub [target]
432
+ // ──────────────────────────────────────────────
433
+ program
434
+ .command("scrub [target]")
435
+ .description("Scrub credentials from Claude Code transcript logs (no auth required)")
436
+ .option("--force", "Rescrub files even if already marked clean")
437
+ .addHelpText("after", `
438
+ Examples:
439
+ clauth scrub Scrub the most recent (active) transcript
440
+ clauth scrub <file> Scrub a specific .jsonl file
441
+ clauth scrub all Scrub every transcript in ~/.claude/projects/
442
+ clauth scrub all --force Rescrub all files (ignore markers)
443
+ `)
444
+ .action(async (target, opts) => {
445
+ await runScrub(target, opts);
446
+ });
447
+
399
448
  // ──────────────────────────────────────────────
400
449
  // clauth --help override banner
401
450
  // ──────────────────────────────────────────────
@@ -409,4 +458,43 @@ program.addHelpText("beforeAll", chalk.cyan(`
409
458
  v${VERSION} — LIFEAI Credential Vault
410
459
  `));
411
460
 
461
+ // ──────────────────────────────────────────────
462
+ // clauth serve [action]
463
+ // ──────────────────────────────────────────────
464
+ program
465
+ .command("serve [action]")
466
+ .description("Manage localhost HTTP vault daemon (start|stop|restart|ping)")
467
+ .option("--port <n>", "Port (default: 52437)")
468
+ .option("-p, --pw <password>", "clauth password (required for start/restart)")
469
+ .option("--services <list>", "Comma-separated service whitelist (default: all)")
470
+ .option("--action <action>", "Internal: action override for daemon child")
471
+ .addHelpText("after", `
472
+ Actions:
473
+ start Start the server as a background daemon
474
+ stop Stop the running daemon
475
+ restart Stop + start
476
+ ping Check if the daemon is running
477
+ foreground Run in foreground (Ctrl+C to stop) — default if no action given
478
+
479
+ Examples:
480
+ clauth serve -p mypass Run in foreground (original behavior)
481
+ clauth serve start -p mypass Start as background daemon
482
+ clauth serve stop Stop the daemon
483
+ clauth serve ping Check status
484
+ clauth serve restart -p mypass Restart the daemon
485
+ clauth serve start --services github,vercel -p mypass
486
+ `)
487
+ .action(async (action, opts) => {
488
+ const resolvedAction = opts.action || action || "foreground";
489
+
490
+ // stop and ping don't need a password
491
+ if (!["stop", "ping"].includes(resolvedAction) && !opts.pw) {
492
+ console.log(chalk.red("\n --pw is required for serve mode\n"));
493
+ console.log(chalk.gray(" Example: clauth serve start --pw yourpassword\n"));
494
+ process.exit(1);
495
+ }
496
+
497
+ await runServe({ ...opts, action: resolvedAction });
498
+ });
499
+
412
500
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {