@lifeaitools/clauth 0.2.1 → 0.2.2

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.
@@ -9,93 +9,127 @@ Hardware-bound credential vault for the LIFEAI stack. Your machine is the second
9
9
 
10
10
  ---
11
11
 
12
- ## When someone says "install clauth"
12
+ ## How Claude Interfaces with clauth
13
13
 
14
- ### Step 1 Check GitHub MCP is connected
14
+ > **CRITICAL:** Never pipe input to clauth or use interactive prompts. The CLI uses `inquirer` which produces ANSI garbage when stdin is piped. Always use one of the two methods below.
15
15
 
16
- Look at your available tools. You need a GitHub/git tool that can clone a repository.
16
+ ### Method 1 CLI with `--pw` flag (simplest)
17
17
 
18
- If GitHub MCP is NOT connected, stop and tell the user:
18
+ All clauth commands accept `-p` / `--pw <password>` to skip the interactive password prompt:
19
19
 
20
- > "To install clauth I need your GitHub connection. In Claude, go to Settings → Integrations and connect GitHub, then come back and say 'install clauth' again."
20
+ ```bash
21
+ clauth status -p "YourPassword"
22
+ clauth get github -p "YourPassword"
23
+ clauth test -p "YourPassword"
24
+ clauth enable github -p "YourPassword"
25
+ clauth write key github -p "YourPassword" # still prompts for the key value
26
+ ```
21
27
 
22
- Do not proceed without it.
28
+ ### Method 2 Direct API calls (full control, no CLI needed)
23
29
 
24
- ---
30
+ Call the auth-vault Edge Function directly. This is the most reliable method for Claude.
25
31
 
26
- ### Step 2 — Clone the repo
32
+ **Base URL:** `https://<project-ref>.supabase.co/functions/v1/auth-vault`
33
+ **Auth header:** `Authorization: Bearer <supabase-anon-key>`
34
+ **Method:** POST (all routes)
35
+ **Content-Type:** `application/json`
27
36
 
28
- Use bash to clone into `~/.clauth`:
37
+ #### HMAC Token Derivation (must match server)
29
38
 
30
- ```bash
31
- git clone https://github.com/LIFEAI/clauth.git ~/.clauth
32
- ```
39
+ ```js
40
+ import { createHmac, createHash, execSync } from "crypto";
33
41
 
34
- If `~/.clauth` already exists:
35
- ```bash
36
- cd ~/.clauth && git pull
42
+ // 1. Get machine hash (same as fingerprint.js)
43
+ // Windows:
44
+ const uuid = execSync("wmic csproduct get uuid /format:value", { encoding: "utf8" })
45
+ .match(/UUID=([A-F0-9-]+)/i)?.[1]?.trim();
46
+ const machineGuid = execSync(
47
+ "reg query HKLM\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid",
48
+ { encoding: "utf8" }
49
+ ).match(/MachineGuid\s+REG_SZ\s+([a-f0-9-]+)/i)?.[1]?.trim();
50
+ const machineHash = createHash("sha256").update(`${uuid}:${machineGuid}`).digest("hex");
51
+
52
+ // 2. Derive HMAC token
53
+ const windowMs = 5 * 60 * 1000;
54
+ const window = Math.floor(Date.now() / windowMs);
55
+ const message = `${machineHash}:${window}`;
56
+ const token = createHmac("sha256", password).update(message).digest("hex");
57
+ const timestamp = window * windowMs;
37
58
  ```
38
59
 
60
+ #### API Routes
61
+
62
+ | Route | Body fields | Returns |
63
+ |-------|-------------|---------|
64
+ | `POST /status` | `machine_hash, token, timestamp, password` | `{ services: [...] }` |
65
+ | `POST /test` | `machine_hash, token, timestamp, password` | `{ ok: true }` |
66
+ | `POST /retrieve` | `+ service` | `{ value: "..." }` |
67
+ | `POST /write` | `+ service, value` | `{ ok: true }` |
68
+ | `POST /enable` | `+ service, enabled: bool` | `{ ok: true }` |
69
+ | `POST /add` | `+ name, label, key_type, description` | `{ ok: true }` |
70
+ | `POST /remove` | `+ service, confirm: true` | `{ ok: true }` |
71
+ | `POST /revoke` | `+ service, confirm: true` | `{ ok: true }` |
72
+ | `POST /register-machine` | `machine_hash, hmac_seed_hash, label, admin_token` | `{ ok: true }` |
73
+
74
+ All auth routes require: `machine_hash`, `token`, `timestamp`, `password`.
75
+
76
+ ### Password Handling
77
+
78
+ - Ask the user for their clauth password **once per session**
79
+ - Store it in working memory for the duration of the conversation
80
+ - Never log or echo the password
81
+ - If the user says "use clauth" or "get my github key", ask for the password if you don't have it yet
82
+
39
83
  ---
40
84
 
41
- ### Step 3 Run the bootstrap binary
85
+ ## When someone says "install clauth"
42
86
 
43
- **Windows:**
44
- ```powershell
45
- & "$env:USERPROFILE\.clauth\scripts\bin\bootstrap-win.exe"
46
- ```
87
+ ### Step 1 — Install from npm
47
88
 
48
- **Mac/Linux:**
49
89
  ```bash
50
- ~/.clauth/scripts/bin/bootstrap-linux
51
- # Mac:
52
- ~/.clauth/scripts/bin/bootstrap-macos
90
+ npm install -g @lifeaitools/clauth
53
91
  ```
54
92
 
55
- This binary:
56
- - Runs `npm install`
57
- - Links `clauth` globally
58
- - Chains to `clauth install` automatically
93
+ If already installed, update:
94
+ ```bash
95
+ npm update -g @lifeaitools/clauth
96
+ ```
59
97
 
60
- ---
98
+ ### Step 2 — Run the installer
61
99
 
62
- ### Step 4 clauth install (automatic)
100
+ Use CLI flags to avoid interactive prompts:
63
101
 
64
- `clauth install` runs automatically. It asks for two Supabase items:
102
+ ```bash
103
+ clauth install --ref <supabase-project-ref> --pat <personal-access-token>
104
+ ```
65
105
 
66
- **Project ref** — last part of your Supabase project URL:
106
+ **Project ref** — last part of the Supabase project URL:
67
107
  `https://supabase.com/dashboard/project/` **`uvojezuorjgqzmhhgluu`**
68
108
 
69
109
  **Personal Access Token (PAT):**
70
110
  `https://supabase.com/dashboard/account/tokens` → Generate new token
71
111
  *(NOT the anon key or service_role — this is your account-level token)*
72
112
 
73
- Then it provisions everything, tests it, installs this skill, and prints a **bootstrap token** — save it.
74
-
75
- ---
113
+ The installer provisions everything and prints a **bootstrap token** — save it.
76
114
 
77
- ### Step 5clauth setup
115
+ ### Step 3Setup this machine
78
116
 
79
- ```
80
- clauth setup
117
+ ```bash
118
+ clauth setup --admin-token <bootstrap-token> -p <password>
81
119
  ```
82
120
 
83
- Asks: machine label, password, bootstrap token (from step 4).
84
-
85
121
  Then verify:
122
+ ```bash
123
+ clauth test -p <password>
124
+ clauth status -p <password>
86
125
  ```
87
- clauth test → PASS
88
- clauth status → 12 services ready
89
- ```
90
-
91
- ---
92
126
 
93
- ### Step 6 — Write your first key
127
+ ### Step 4 — Write your first key
94
128
 
95
129
  ```bash
96
- clauth write key github # prompts for value
97
- clauth enable github
98
- clauth get github
130
+ clauth write key github -p <password> # prompts for value
131
+ clauth enable github -p <password>
132
+ clauth get github -p <password>
99
133
  ```
100
134
 
101
135
  See `references/keys-guide.md` for where to find every credential.
@@ -105,21 +139,29 @@ See `references/keys-guide.md` for where to find every credential.
105
139
  ## Command reference
106
140
 
107
141
  ```
108
- clauth install First-time: provision Supabase + install skill
109
- clauth setup Register this machine (after install)
110
- clauth status All services + state
111
- clauth test Verify HMAC connection
112
- clauth list Service names
113
-
114
- clauth write key <service> Store a credential
115
- clauth write pw Change password
116
- clauth enable <svc|all> Activate service
117
- clauth disable <svc|all> Suspend service
118
- clauth get <service> Retrieve a key
119
-
120
- clauth add service <n> Register new service
121
- clauth remove service <n> Remove service
122
- clauth revoke <svc|all> Delete key (destructive)
142
+ clauth install [--ref R] [--pat P] First-time: provision Supabase + install skill
143
+ clauth setup [--admin-token T] [-p P] Register this machine
144
+ clauth status [-p P] All services + state
145
+ clauth test [-p P] Verify HMAC connection
146
+ clauth list [-p P] Service names
147
+
148
+ clauth write key <service> [-p P] Store a credential
149
+ clauth write pw [-p P] Change password
150
+ clauth enable <svc|all> [-p P] Activate service
151
+ clauth disable <svc|all> [-p P] Suspend service
152
+ clauth get <service> [-p P] Retrieve a key
153
+
154
+ clauth add service <n> [-p P] Register new service
155
+ clauth remove service <n> [-p P] Remove service
156
+ clauth revoke <svc|all> [-p P] Delete key (destructive)
157
+
158
+ clauth scrub Scrub active transcript (most recent .jsonl)
159
+ clauth scrub <file> Scrub a specific file
160
+ clauth scrub all Scrub all transcripts in ~/.claude/projects/
161
+ clauth scrub all --force Rescrub everything (ignore markers)
162
+
163
+ clauth uninstall --ref R --pat P Full teardown (DB, Edge Fn, secrets, skill, config)
164
+ clauth uninstall --ref R --pat P --yes Skip confirmation
123
165
  ```
124
166
 
125
167
  ## Services
@@ -137,5 +179,6 @@ clauth revoke <svc|all> Delete key (destructive)
137
179
  | `machine_not_found` | Run `clauth setup` |
138
180
  | `timestamp_expired` | Sync system clock |
139
181
  | `invalid_token` | Wrong password |
140
- | `service_disabled` | `clauth enable <service>` |
141
- | `no_key_stored` | `clauth write key <service>` |
182
+ | `service_disabled` | `clauth enable <service> -p <password>` |
183
+ | `no_key_stored` | `clauth write key <service> -p <password>` |
184
+ | ANSI garbage output | You piped stdin — use `-p` flag instead |
@@ -0,0 +1,231 @@
1
+ // cli/commands/scrub.js — Scrub credentials from Claude Code transcript logs
2
+ //
3
+ // clauth scrub → scrub active transcript (most recent .jsonl)
4
+ // clauth scrub <file> → scrub a specific file
5
+ // clauth scrub all → scrub every transcript .jsonl
6
+ // clauth scrub --force → rescrub even if already marked
7
+
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import os from "os";
11
+ import chalk from "chalk";
12
+ import ora from "ora";
13
+
14
+ const SCRUB_MARKER = "[CLAUTH-SCRUBBED]";
15
+ const SCRUB_VERSION = "1.1";
16
+
17
+ // ──────────────────────────────────────────────
18
+ // Credential patterns — regex + replacement
19
+ // ──────────────────────────────────────────────
20
+ const PATTERNS = [
21
+ // Supabase JWTs (anon, service_role)
22
+ [/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
23
+ "[SUPABASE_JWT_REDACTED]"],
24
+
25
+ // Vercel tokens
26
+ [/vcp_[A-Za-z0-9]{20,80}/g,
27
+ "[VERCEL_TOKEN_REDACTED]"],
28
+
29
+ // R2 / S3 secret access keys (64-char hex)
30
+ [/"secret_access_key"\s*:\s*"[a-f0-9]{64}"/g,
31
+ '"secret_access_key": "[R2_SECRET_REDACTED]"'],
32
+
33
+ // R2 / S3 access key IDs (32-char hex)
34
+ [/"access_key_id"\s*:\s*"[a-f0-9]{32}"/g,
35
+ '"access_key_id": "[R2_KEY_REDACTED]"'],
36
+
37
+ // Cloudflare admin tokens
38
+ [/"admin_token"\s*:\s*"[A-Za-z0-9_-]{20,60}"/g,
39
+ '"admin_token": "[CF_TOKEN_REDACTED]"'],
40
+
41
+ // Cloudflare account IDs (32-char hex)
42
+ [/"account_id"\s*:\s*"[a-f0-9]{32}"/g,
43
+ '"account_id": "[CF_ACCOUNT_REDACTED]"'],
44
+
45
+ // GitHub tokens
46
+ [/ghp_[A-Za-z0-9]{36}/g,
47
+ "[GITHUB_TOKEN_REDACTED]"],
48
+ [/github_pat_[A-Za-z0-9_]{40,100}/g,
49
+ "[GITHUB_PAT_REDACTED]"],
50
+
51
+ // Neo4j connection strings with passwords
52
+ [/neo4j\+s?:\/\/[^"\\]+/g,
53
+ "[NEO4J_CONNSTRING_REDACTED]"],
54
+
55
+ // Generic Bearer tokens in Authorization headers
56
+ [/Bearer [A-Za-z0-9_-]{20,}/g,
57
+ "Bearer [TOKEN_REDACTED]"],
58
+ ];
59
+
60
+ // ──────────────────────────────────────────────
61
+ // Marker check — read last 512 bytes
62
+ // ──────────────────────────────────────────────
63
+ function isAlreadyScrubbed(filePath) {
64
+ try {
65
+ const stat = fs.statSync(filePath);
66
+ const fd = fs.openSync(filePath, "r");
67
+ const bufSize = Math.min(512, stat.size);
68
+ const buf = Buffer.alloc(bufSize);
69
+ fs.readSync(fd, buf, 0, bufSize, Math.max(0, stat.size - bufSize));
70
+ fs.closeSync(fd);
71
+
72
+ const tail = buf.toString("utf-8");
73
+ const lastLine = tail.trim().split("\n").pop();
74
+ if (lastLine && lastLine.includes(SCRUB_MARKER)) {
75
+ try {
76
+ const marker = JSON.parse(lastLine);
77
+ return marker.version === SCRUB_VERSION;
78
+ } catch { return false; }
79
+ }
80
+ return false;
81
+ } catch { return false; }
82
+ }
83
+
84
+ // ──────────────────────────────────────────────
85
+ // Stamp file as scrubbed
86
+ // ──────────────────────────────────────────────
87
+ function stampScrubbed(filePath) {
88
+ const marker = JSON.stringify({
89
+ type: SCRUB_MARKER,
90
+ version: SCRUB_VERSION,
91
+ scrubbed_at: new Date().toISOString(),
92
+ patterns: PATTERNS.length,
93
+ });
94
+ fs.appendFileSync(filePath, "\n" + marker + "\n", "utf-8");
95
+ }
96
+
97
+ // ──────────────────────────────────────────────
98
+ // Scrub a single file
99
+ // ──────────────────────────────────────────────
100
+ function scrubFile(filePath, force = false) {
101
+ if (!force && isAlreadyScrubbed(filePath)) {
102
+ return "skipped";
103
+ }
104
+
105
+ let content = fs.readFileSync(filePath, "utf-8");
106
+ let total = 0;
107
+
108
+ for (const [pattern, replacement] of PATTERNS) {
109
+ // Reset lastIndex for global regexes
110
+ pattern.lastIndex = 0;
111
+ const matches = content.match(pattern);
112
+ if (matches) {
113
+ total += matches.length;
114
+ content = content.replace(pattern, replacement);
115
+ }
116
+ }
117
+
118
+ if (total > 0) {
119
+ fs.writeFileSync(filePath, content, "utf-8");
120
+ }
121
+
122
+ // Stamp as clean (whether creds found or not)
123
+ stampScrubbed(filePath);
124
+ return total;
125
+ }
126
+
127
+ // ──────────────────────────────────────────────
128
+ // Find all transcript .jsonl files
129
+ // ──────────────────────────────────────────────
130
+ function findTranscripts() {
131
+ const claudeDir = path.join(os.homedir(), ".claude", "projects");
132
+ if (!fs.existsSync(claudeDir)) return [];
133
+
134
+ const results = [];
135
+ function walk(dir) {
136
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
137
+ const full = path.join(dir, entry.name);
138
+ if (entry.isDirectory()) walk(full);
139
+ else if (entry.name.endsWith(".jsonl")) results.push(full);
140
+ }
141
+ }
142
+ walk(claudeDir);
143
+ return results;
144
+ }
145
+
146
+ // ──────────────────────────────────────────────
147
+ // Find the most recent transcript (active session)
148
+ // ──────────────────────────────────────────────
149
+ function findMostRecent() {
150
+ const files = findTranscripts();
151
+ if (files.length === 0) return null;
152
+
153
+ let newest = files[0];
154
+ let newestMtime = fs.statSync(files[0]).mtimeMs;
155
+ for (const f of files.slice(1)) {
156
+ const mt = fs.statSync(f).mtimeMs;
157
+ if (mt > newestMtime) { newest = f; newestMtime = mt; }
158
+ }
159
+ return newest;
160
+ }
161
+
162
+ // ──────────────────────────────────────────────
163
+ // Exported runner
164
+ // ──────────────────────────────────────────────
165
+ export async function runScrub(target, opts = {}) {
166
+ const force = opts.force || false;
167
+
168
+ // Determine which files to scrub
169
+ let files;
170
+ if (target === "all") {
171
+ files = findTranscripts();
172
+ if (files.length === 0) {
173
+ console.log(chalk.yellow("\n No transcript files found.\n"));
174
+ return;
175
+ }
176
+ console.log(chalk.cyan(`\n Scrubbing ${files.length} transcript file(s)...\n`));
177
+ } else if (target && target !== "all") {
178
+ // Specific file
179
+ const resolved = path.resolve(target);
180
+ if (!fs.existsSync(resolved)) {
181
+ console.log(chalk.red(`\n File not found: ${resolved}\n`));
182
+ process.exit(1);
183
+ }
184
+ files = [resolved];
185
+ } else {
186
+ // No target — find most recent
187
+ const recent = findMostRecent();
188
+ if (!recent) {
189
+ console.log(chalk.yellow("\n No transcript files found.\n"));
190
+ return;
191
+ }
192
+ files = [recent];
193
+ console.log(chalk.gray(`\n Active transcript: ${path.basename(recent)}`));
194
+ }
195
+
196
+ // Scrub
197
+ const spinner = ora("Scrubbing credentials...").start();
198
+ let grandTotal = 0;
199
+ let skipped = 0;
200
+ let scanned = 0;
201
+
202
+ for (const f of files) {
203
+ const result = scrubFile(f, force);
204
+ if (result === "skipped") {
205
+ skipped++;
206
+ } else {
207
+ scanned++;
208
+ if (result > 0) {
209
+ spinner.stop();
210
+ console.log(chalk.yellow(` ${result} redaction(s) in ${path.basename(f)}`));
211
+ spinner.start("Scrubbing credentials...");
212
+ grandTotal += result;
213
+ }
214
+ }
215
+ }
216
+
217
+ spinner.stop();
218
+
219
+ // Summary
220
+ const parts = [];
221
+ if (scanned) parts.push(`scanned ${scanned}`);
222
+ if (skipped) parts.push(chalk.gray(`skipped ${skipped} (already clean)`));
223
+ if (grandTotal) parts.push(chalk.green(`${grandTotal} credential(s) scrubbed`));
224
+ else if (scanned) parts.push(chalk.green("no credentials found"));
225
+
226
+ console.log(`\n ${files.length} file(s): ${parts.join(", ")}.`);
227
+ if (skipped && !force) {
228
+ console.log(chalk.gray(" (use --force to rescan all files)"));
229
+ }
230
+ console.log();
231
+ }
@@ -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.2.2";
15
15
 
16
16
  // ============================================================
17
17
  // Password prompt helper
@@ -51,6 +51,8 @@ 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';
54
56
 
55
57
  program
56
58
  .command('install')
@@ -61,6 +63,28 @@ program
61
63
  await runInstall(opts);
62
64
  });
63
65
 
66
+ program
67
+ .command('uninstall')
68
+ .description('Full teardown — drop DB objects, Edge Function, secrets, skill, config')
69
+ .option('--ref <ref>', 'Supabase project ref')
70
+ .option('--pat <pat>', 'Supabase Personal Access Token (required)')
71
+ .option('--yes', 'Skip confirmation prompt')
72
+ .action(async (opts) => {
73
+ if (!opts.yes) {
74
+ const inquirerMod = await import('inquirer');
75
+ const { confirm } = await inquirerMod.default.prompt([{
76
+ type: 'input',
77
+ name: 'confirm',
78
+ message: chalk.red('Type "CONFIRM UNINSTALL" to proceed:'),
79
+ }]);
80
+ if (confirm !== 'CONFIRM UNINSTALL') {
81
+ console.log(chalk.yellow('\n Uninstall cancelled.\n'));
82
+ process.exit(0);
83
+ }
84
+ }
85
+ await runUninstall(opts);
86
+ });
87
+
64
88
  // ──────────────────────────────────────────────
65
89
  // clauth setup
66
90
  // ──────────────────────────────────────────────
@@ -273,11 +297,17 @@ addCmd
273
297
  .option("-p, --pw <password>")
274
298
  .action(async (name, opts) => {
275
299
  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
- ]);
300
+ let answers;
301
+ if (opts.type && opts.label) {
302
+ // Non-interactive all flags provided
303
+ answers = { label: opts.label, key_type: opts.type, desc: opts.description || "" };
304
+ } else {
305
+ answers = await inquirer.prompt([
306
+ { type: "input", name: "label", message: "Label:", default: opts.label || name },
307
+ { type: "list", name: "key_type", message: "Key type:", choices: ["token","keypair","connstring","oauth"], default: opts.type || "token" },
308
+ { type: "input", name: "desc", message: "Description (optional):", default: opts.description || "" }
309
+ ]);
310
+ }
281
311
  const spinner = ora(`Adding service: ${name}...`).start();
282
312
  try {
283
313
  const result = await api.addService(
@@ -396,6 +426,24 @@ program
396
426
  } catch (err) { spinner.fail(chalk.red(err.message)); }
397
427
  });
398
428
 
429
+ // ──────────────────────────────────────────────
430
+ // clauth scrub [target]
431
+ // ──────────────────────────────────────────────
432
+ program
433
+ .command("scrub [target]")
434
+ .description("Scrub credentials from Claude Code transcript logs (no auth required)")
435
+ .option("--force", "Rescrub files even if already marked clean")
436
+ .addHelpText("after", `
437
+ Examples:
438
+ clauth scrub Scrub the most recent (active) transcript
439
+ clauth scrub <file> Scrub a specific .jsonl file
440
+ clauth scrub all Scrub every transcript in ~/.claude/projects/
441
+ clauth scrub all --force Rescrub all files (ignore markers)
442
+ `)
443
+ .action(async (target, opts) => {
444
+ await runScrub(target, opts);
445
+ });
446
+
399
447
  // ──────────────────────────────────────────────
400
448
  // clauth --help override banner
401
449
  // ──────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {