@lifeaitools/clauth 0.7.5 → 1.1.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,210 @@
1
+ // cli/commands/tunnel.js
2
+ // clauth tunnel setup — configures Cloudflare named tunnel for claude.ai web integration
3
+
4
+ import { execSync, spawnSync } from "child_process";
5
+ import os from "os";
6
+ import path from "path";
7
+ import fs from "fs";
8
+ import readline from "readline";
9
+
10
+ function ask(question) {
11
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
12
+ return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim()); }));
13
+ }
14
+
15
+ async function actionTunnelSetup() {
16
+ console.log("\n clauth tunnel setup\n");
17
+ console.log(" This wizard configures a Cloudflare named tunnel so claude.ai web");
18
+ console.log(" can connect to your local clauth daemon without exposing any ports.\n");
19
+
20
+ // Step 1: Check cloudflared is installed
21
+ try {
22
+ execSync("cloudflared --version", { stdio: "ignore" });
23
+ console.log(" ✓ cloudflared detected\n");
24
+ } catch {
25
+ console.log(" ✗ cloudflared not found.\n");
26
+ console.log(" Install it first:");
27
+ console.log(" Windows: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/");
28
+ console.log(" Or via winget: winget install Cloudflare.cloudflared\n");
29
+ process.exit(1);
30
+ }
31
+
32
+ // Step 2: Authenticate with Cloudflare
33
+ console.log(" Step 1/4 — Authenticate with Cloudflare");
34
+ console.log(" This will open your browser to log in.\n");
35
+ const doAuth = await ask(" Proceed? (y/n): ");
36
+ if (doAuth.toLowerCase() !== "y") { console.log(" Aborted."); process.exit(0); }
37
+
38
+ try {
39
+ spawnSync("cloudflared", ["tunnel", "login"], { stdio: "inherit" });
40
+ console.log(" ✓ Authenticated\n");
41
+ } catch (e) {
42
+ console.error(" ✗ Login failed:", e.message);
43
+ process.exit(1);
44
+ }
45
+
46
+ // Step 3: Create tunnel
47
+ console.log(" Step 2/4 — Create named tunnel");
48
+ const tunnelName = await ask(" Tunnel name (default: clauth): ") || "clauth";
49
+
50
+ let tunnelId;
51
+ try {
52
+ const result = execSync(`cloudflared tunnel create ${tunnelName}`, { encoding: "utf8" });
53
+ const match = result.match(/Created tunnel (.+) with id ([a-f0-9-]+)/i);
54
+ if (match) tunnelId = match[2];
55
+ console.log(` ✓ Tunnel created: ${tunnelName} (${tunnelId || "see above"})\n`);
56
+ } catch (e) {
57
+ // Tunnel may already exist
58
+ console.log(" (Tunnel may already exist — continuing)\n");
59
+ try {
60
+ const listResult = execSync(`cloudflared tunnel list`, { encoding: "utf8" });
61
+ const lines = listResult.split("\n");
62
+ for (const line of lines) {
63
+ if (line.toLowerCase().includes(tunnelName.toLowerCase())) {
64
+ const parts = line.trim().split(/\s+/);
65
+ if (parts[0] && parts[0].includes("-")) tunnelId = parts[0];
66
+ }
67
+ }
68
+ } catch {
69
+ // ignore list errors
70
+ }
71
+ }
72
+
73
+ // Step 4: Configure hostname
74
+ console.log(" Step 3/4 — Set public hostname");
75
+ const hostname = await ask(" Public hostname (e.g. clauth.yourdomain.com): ");
76
+ if (!hostname) { console.log(" Hostname required."); process.exit(1); }
77
+
78
+ // Write config.yml
79
+ const cfDir = path.join(os.homedir(), ".cloudflared");
80
+ if (!fs.existsSync(cfDir)) fs.mkdirSync(cfDir, { recursive: true });
81
+
82
+ const configPath = path.join(cfDir, "config.yml");
83
+ const credFile = tunnelId ? path.join(cfDir, `${tunnelId}.json`) : `<tunnel-id>.json`;
84
+ const config = `tunnel: ${tunnelId || tunnelName}
85
+ credentials-file: ${credFile}
86
+ ingress:
87
+ - hostname: ${hostname}
88
+ service: http://127.0.0.1:52437
89
+ - service: http_status:404
90
+ `;
91
+ fs.writeFileSync(configPath, config, "utf8");
92
+ console.log(` ✓ Config written: ${configPath}\n`);
93
+
94
+ // Route DNS
95
+ console.log(" Step 4/4 — Configure DNS");
96
+ try {
97
+ execSync(`cloudflared tunnel route dns ${tunnelName} ${hostname}`, { stdio: "inherit" });
98
+ console.log(` ✓ DNS configured: ${hostname}\n`);
99
+ } catch {
100
+ console.log(` ⚠ DNS route failed — add manually in Cloudflare dashboard:`);
101
+ console.log(` CNAME ${hostname} → ${tunnelId}.cfargotunnel.com\n`);
102
+ }
103
+
104
+ // Save hostname to clauth config via api module
105
+ try {
106
+ const apiMod = await import("../api.js").catch(() => null);
107
+ if (apiMod) {
108
+ const sbUrl = (apiMod.getBaseUrl?.() || "").replace("/functions/v1/auth-vault", "");
109
+ const sbKey = apiMod.getAnonKey?.();
110
+ if (sbUrl && sbKey) {
111
+ await fetch(
112
+ `${sbUrl}/rest/v1/clauth_config`,
113
+ {
114
+ method: "POST",
115
+ headers: {
116
+ apikey: sbKey,
117
+ Authorization: `Bearer ${sbKey}`,
118
+ "Content-Type": "application/json",
119
+ Prefer: "resolution=merge-duplicates",
120
+ },
121
+ body: JSON.stringify({ key: "tunnel_hostname", value: hostname }),
122
+ }
123
+ );
124
+ console.log(` ✓ Hostname saved to clauth config: ${hostname}`);
125
+ }
126
+ }
127
+ } catch {
128
+ console.log(` ⚠ Could not save to DB. Hostname is set in config.yml — 'clauth serve' will pick it up.`);
129
+ }
130
+
131
+ console.log("\n Setup complete!");
132
+ console.log(` Restart the daemon: clauth serve restart`);
133
+ console.log(` Or use: npm run worker:restart (from clauth directory)\n`);
134
+ }
135
+
136
+ // clauth tunnel start — tells daemon to start the tunnel
137
+ async function actionTunnelStart() {
138
+ try {
139
+ const r = await fetch("http://127.0.0.1:52437/tunnel/start", {
140
+ method: "POST",
141
+ headers: { "Content-Type": "application/json" },
142
+ signal: AbortSignal.timeout(5000),
143
+ });
144
+ const data = await r.json().catch(() => ({}));
145
+ if (!r.ok) {
146
+ console.error(` ✗ ${data.error || r.statusText}`);
147
+ if (r.status === 401) console.error(" Unlock the daemon first: http://127.0.0.1:52437");
148
+ process.exit(1);
149
+ }
150
+ console.log(` ✓ ${data.message || "Tunnel starting — check status with: clauth tunnel status"}`);
151
+ } catch (e) {
152
+ console.error(" ✗ Daemon not running. Start it with: clauth serve");
153
+ process.exit(1);
154
+ }
155
+ }
156
+
157
+ // clauth tunnel stop — tells daemon to stop the tunnel
158
+ async function actionTunnelStop() {
159
+ try {
160
+ const r = await fetch("http://127.0.0.1:52437/tunnel/stop", {
161
+ method: "POST",
162
+ headers: { "Content-Type": "application/json" },
163
+ signal: AbortSignal.timeout(5000),
164
+ });
165
+ const data = await r.json().catch(() => ({}));
166
+ if (!r.ok) {
167
+ console.error(` ✗ ${data.error || r.statusText}`);
168
+ process.exit(1);
169
+ }
170
+ console.log(" ✓ Tunnel stopped.");
171
+ } catch (e) {
172
+ console.error(" ✗ Daemon not running.");
173
+ process.exit(1);
174
+ }
175
+ }
176
+
177
+ // clauth tunnel status — shows current tunnel status from daemon
178
+ async function actionTunnelStatus() {
179
+ try {
180
+ const r = await fetch("http://127.0.0.1:52437/tunnel", {
181
+ signal: AbortSignal.timeout(5000),
182
+ });
183
+ const data = await r.json().catch(() => ({}));
184
+
185
+ const icons = {
186
+ live: "✓",
187
+ starting: "◌",
188
+ not_configured: "⚠",
189
+ not_started: "○",
190
+ error: "✗",
191
+ missing_cloudflared: "✗",
192
+ };
193
+ const labels = {
194
+ live: `Live — ${data.url || ""}`,
195
+ starting: "Starting...",
196
+ not_configured: "Not configured — run: clauth tunnel setup",
197
+ not_started: "Not started — run: clauth tunnel start",
198
+ error: `Error${data.error ? ": " + data.error : ""} — check cloudflared config`,
199
+ missing_cloudflared: "cloudflared not installed — https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/",
200
+ };
201
+
202
+ const status = data.status || "unknown";
203
+ console.log(`\n ${icons[status] || "?"} Tunnel: ${labels[status] || status}\n`);
204
+ } catch (e) {
205
+ console.error(" ✗ Daemon not running. Start it with: clauth serve");
206
+ process.exit(1);
207
+ }
208
+ }
209
+
210
+ export { actionTunnelSetup, actionTunnelStart, actionTunnelStop, actionTunnelStatus };
package/cli/index.js CHANGED
@@ -10,9 +10,10 @@ import { getConfOptions } from "./conf-path.js";
10
10
  import { getMachineHash, deriveToken, deriveSeedHash } from "./fingerprint.js";
11
11
  import * as api from "./api.js";
12
12
  import os from "os";
13
+ import fs from "fs";
13
14
 
14
15
  const config = new Conf(getConfOptions());
15
- const VERSION = "0.7.0";
16
+ const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
16
17
 
17
18
  // ============================================================
18
19
  // Password prompt helper
@@ -128,6 +129,7 @@ program
128
129
  const result = await api.registerMachine(machineHash, seedHash, answers.label, answers.adminTk);
129
130
  if (result.error) throw new Error(result.error);
130
131
  spinner.succeed(chalk.green(`Machine registered: ${machineHash.slice(0,12)}...`));
132
+
131
133
  console.log(chalk.green("\n✓ clauth is ready.\n"));
132
134
  console.log(chalk.cyan(" clauth test — verify connection"));
133
135
  console.log(chalk.cyan(" clauth status — see all services\n"));
@@ -144,22 +146,24 @@ program
144
146
  .command("status")
145
147
  .description("Show all services and their state")
146
148
  .option("-p, --pw <password>", "Password (or will prompt)")
149
+ .option("--project <name>", "Filter by project scope")
147
150
  .action(async (opts) => {
148
151
  const auth = await getAuth(opts.pw);
149
152
  const spinner = ora("Fetching service status...").start();
150
153
  try {
151
- const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp);
154
+ const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp, opts.project);
152
155
  spinner.stop();
153
156
  if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); return; }
154
157
 
155
- console.log(chalk.cyan("\n🔐 clauth service status\n"));
158
+ const heading = opts.project ? `clauth service status (project: ${opts.project})` : "clauth service status";
159
+ console.log(chalk.cyan(`\n🔐 ${heading}\n`));
156
160
  console.log(
157
161
  chalk.bold(
158
- " " + "SERVICE".padEnd(20) + "TYPE".padEnd(12) + "STATUS".padEnd(12) +
162
+ " " + "SERVICE".padEnd(24) + "TYPE".padEnd(12) + "PROJECT".padEnd(22) + "STATUS".padEnd(12) +
159
163
  "KEY STORED".padEnd(12) + "LAST RETRIEVED"
160
164
  )
161
165
  );
162
- console.log(" " + "─".repeat(72));
166
+ console.log(" " + "─".repeat(90));
163
167
 
164
168
  for (const s of result.services || []) {
165
169
  const status = s.enabled
@@ -171,8 +175,9 @@ program
171
175
  const lastGet = s.last_retrieved
172
176
  ? new Date(s.last_retrieved).toLocaleDateString()
173
177
  : chalk.gray("never");
178
+ const proj = s.project ? chalk.blue(s.project.padEnd(22)) : chalk.gray("—".padEnd(22));
174
179
 
175
- console.log(` ${s.name.padEnd(20)}${s.key_type.padEnd(12)}${status}${hasKey}${lastGet}`);
180
+ console.log(` ${s.name.padEnd(24)}${s.key_type.padEnd(12)}${proj}${status}${hasKey}${lastGet}`);
176
181
  }
177
182
  console.log();
178
183
  } catch (err) {
@@ -296,6 +301,7 @@ addCmd
296
301
  .option("--type <type>", "Key type: token | keypair | connstring | oauth")
297
302
  .option("--label <label>", "Human-readable label")
298
303
  .option("--description <desc>", "Description")
304
+ .option("--project <project>", "Project scope (groups related services)")
299
305
  .option("-p, --pw <password>")
300
306
  .action(async (name, opts) => {
301
307
  const auth = await getAuth(opts.pw);
@@ -310,14 +316,14 @@ addCmd
310
316
  { type: "input", name: "desc", message: "Description (optional):", default: opts.description || "" }
311
317
  ]);
312
318
  }
313
- const spinner = ora(`Adding service: ${name}...`).start();
319
+ const spinner = ora(`Adding service: ${name}${opts.project ? ` (project: ${opts.project})` : ""}...`).start();
314
320
  try {
315
321
  const result = await api.addService(
316
322
  auth.password, auth.machineHash, auth.token, auth.timestamp,
317
- name, answers.label, answers.key_type, answers.desc
323
+ name, answers.label, answers.key_type, answers.desc, opts.project
318
324
  );
319
325
  if (result.error) throw new Error(result.error);
320
- spinner.succeed(chalk.green(`Service added: ${name} (${answers.key_type})`));
326
+ spinner.succeed(chalk.green(`Service added: ${name} (${answers.key_type})${opts.project ? chalk.blue(` [${opts.project}]`) : ""}`));
321
327
  console.log(chalk.gray(` Next: clauth write key ${name}`));
322
328
  } catch (err) { spinner.fail(chalk.red(err.message)); }
323
329
  });
@@ -346,13 +352,22 @@ program
346
352
  .command("list")
347
353
  .description("List all registered services")
348
354
  .option("-p, --pw <password>")
355
+ .option("--project <name>", "Filter by project scope")
349
356
  .action(async (opts) => {
350
357
  const auth = await getAuth(opts.pw);
351
- const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp);
358
+ const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp, opts.project);
352
359
  if (result.error) { console.log(chalk.red(result.error)); return; }
353
- console.log(chalk.cyan("\n Registered services:\n"));
360
+ const heading = opts.project ? `Registered services (project: ${opts.project})` : "Registered services";
361
+ console.log(chalk.cyan(`\n ${heading}:\n`));
362
+ let lastProject = undefined;
354
363
  for (const s of result.services || []) {
355
- console.log(` ${chalk.bold(s.name.padEnd(20))} ${chalk.gray(s.key_type.padEnd(12))} ${chalk.gray(s.description || "")}`);
364
+ const proj = s.project || null;
365
+ if (proj !== lastProject) {
366
+ if (lastProject !== undefined) console.log();
367
+ console.log(chalk.gray(` [${proj || "global"}]`));
368
+ lastProject = proj;
369
+ }
370
+ console.log(` ${chalk.bold(s.name.padEnd(24))} ${chalk.gray(s.key_type.padEnd(12))} ${chalk.gray(s.label || "")}`);
356
371
  }
357
372
  console.log();
358
373
  });
@@ -446,6 +461,113 @@ Examples:
446
461
  await runScrub(target, opts);
447
462
  });
448
463
 
464
+ // ──────────────────────────────────────────────
465
+ // clauth doctor
466
+ // ──────────────────────────────────────────────
467
+ program
468
+ .command("doctor")
469
+ .description("Check all prerequisites and diagnose issues")
470
+ .action(async () => {
471
+ const { runDoctor } = await import("./commands/doctor.js");
472
+ await runDoctor();
473
+ });
474
+
475
+ // ──────────────────────────────────────────────
476
+ // clauth invite generate|list|revoke
477
+ // ──────────────────────────────────────────────
478
+ const invite = program.command("invite").description("Manage vault invites");
479
+
480
+ invite
481
+ .command("generate")
482
+ .description("Generate an invite code for a friend")
483
+ .option("--uses <n>", "Max redemptions", "1")
484
+ .option("--expires <hours>", "Expiry in hours", "168")
485
+ .action(async (opts) => {
486
+ const { runInvite } = await import("./commands/invite.js");
487
+ await runInvite("generate", opts);
488
+ });
489
+
490
+ invite
491
+ .command("list")
492
+ .description("List active invites")
493
+ .action(async () => {
494
+ const { runInvite } = await import("./commands/invite.js");
495
+ await runInvite("list", {});
496
+ });
497
+
498
+ invite
499
+ .command("revoke <code>")
500
+ .description("Revoke an invite code")
501
+ .action(async (code) => {
502
+ const { runInvite } = await import("./commands/invite.js");
503
+ await runInvite("revoke", { code });
504
+ });
505
+
506
+ // ──────────────────────────────────────────────
507
+ // clauth join <invite-code>
508
+ // ──────────────────────────────────────────────
509
+ program
510
+ .command("join <invite-code>")
511
+ .description("Join a vault using an invite code from a friend")
512
+ .action(async (code) => {
513
+ const { runJoin } = await import("./commands/join.js");
514
+ await runJoin(code);
515
+ });
516
+
517
+ // ──────────────────────────────────────────────
518
+ // clauth update
519
+ // ──────────────────────────────────────────────
520
+ program
521
+ .command("update")
522
+ .description("Update clauth to the latest version")
523
+ .action(async () => {
524
+ const { execSync } = await import("child_process");
525
+ console.log(chalk.cyan("\n Updating clauth...\n"));
526
+ try {
527
+ execSync("npm install -g @lifeaitools/clauth@latest", { stdio: "inherit" });
528
+ console.log(chalk.green("\n Updated successfully.\n"));
529
+ } catch (err) {
530
+ console.log(chalk.red(`\n Update failed: ${err.message}\n`));
531
+ }
532
+ });
533
+
534
+ // ──────────────────────────────────────────────
535
+ // clauth tunnel setup
536
+ // ──────────────────────────────────────────────
537
+ const tunnelCmd = program.command("tunnel").description("Manage Cloudflare tunnel for claude.ai web integration");
538
+
539
+ tunnelCmd
540
+ .command("setup")
541
+ .description("Interactive wizard — configure cloudflared named tunnel")
542
+ .action(async () => {
543
+ const { actionTunnelSetup } = await import("./commands/tunnel.js");
544
+ await actionTunnelSetup();
545
+ });
546
+
547
+ tunnelCmd
548
+ .command("start")
549
+ .description("Tell daemon to start the tunnel")
550
+ .action(async () => {
551
+ const { actionTunnelStart } = await import("./commands/tunnel.js");
552
+ await actionTunnelStart();
553
+ });
554
+
555
+ tunnelCmd
556
+ .command("stop")
557
+ .description("Tell daemon to stop the tunnel")
558
+ .action(async () => {
559
+ const { actionTunnelStop } = await import("./commands/tunnel.js");
560
+ await actionTunnelStop();
561
+ });
562
+
563
+ tunnelCmd
564
+ .command("status")
565
+ .description("Show current tunnel status")
566
+ .action(async () => {
567
+ const { actionTunnelStatus } = await import("./commands/tunnel.js");
568
+ await actionTunnelStatus();
569
+ });
570
+
449
571
  // ──────────────────────────────────────────────
450
572
  // clauth --help override banner
451
573
  // ──────────────────────────────────────────────
@@ -478,8 +600,9 @@ Actions:
478
600
  ping Check if the daemon is running
479
601
  foreground Run in foreground (Ctrl+C to stop) — default if no action given
480
602
  mcp Run as MCP stdio server for Claude Code (JSON-RPC over stdin/stdout)
481
- install (Windows) Encrypt password with DPAPI + create Scheduled Task for auto-start on login
482
- uninstall (Windows) Remove Scheduled Task + delete boot.key
603
+ install Store password securely + register auto-start service (cross-platform)
604
+ Windows: DPAPI + Scheduled Task | macOS: Keychain + LaunchAgent | Linux: libsecret/openssl + systemd
605
+ uninstall Remove auto-start service + delete stored password
483
606
 
484
607
  MCP SSE (built into start/foreground):
485
608
  The HTTP daemon also serves MCP SSE transport at GET /sse + POST /message.
@@ -494,8 +617,9 @@ Examples:
494
617
  clauth serve start --services github,vercel
495
618
  clauth serve mcp Start MCP server for Claude Code
496
619
  clauth serve mcp -p mypass Start MCP server pre-unlocked
497
- clauth serve install (Windows) Set up auto-start on login via DPAPI
498
- clauth serve uninstall (Windows) Remove auto-start
620
+ clauth serve install Set up auto-start on login (DPAPI/Keychain/libsecret)
621
+ clauth serve install --tunnel host Auto-start with Cloudflare Tunnel
622
+ clauth serve uninstall Remove auto-start
499
623
  `)
500
624
  .action(async (action, opts) => {
501
625
  const resolvedAction = opts.action || action || "foreground";
package/install.ps1 CHANGED
@@ -23,6 +23,13 @@ try { node --version | Out-Null } catch {
23
23
  exit 1
24
24
  }
25
25
 
26
+ # Check cloudflared (soft warning — not required for install)
27
+ if (-not (Get-Command cloudflared -ErrorAction SilentlyContinue)) {
28
+ Write-Host " Note: cloudflared not found. Install after setup for claude.ai web integration." -ForegroundColor Yellow
29
+ Write-Host " winget install Cloudflare.cloudflared" -ForegroundColor Gray
30
+ Write-Host ""
31
+ }
32
+
26
33
  # Clone or update
27
34
  if (Test-Path "$DIR\.git") {
28
35
  Write-Host " Updating clauth..."
package/install.sh CHANGED
@@ -19,6 +19,17 @@ if ! command -v node &>/dev/null; then
19
19
  exit 1
20
20
  fi
21
21
 
22
+ # Check cloudflared (soft warning)
23
+ if ! command -v cloudflared &>/dev/null; then
24
+ echo " Note: cloudflared not found. Install after setup for claude.ai web integration."
25
+ if [[ "$OSTYPE" == "darwin"* ]]; then
26
+ echo " brew install cloudflare/cloudflare/cloudflared"
27
+ else
28
+ echo " https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"
29
+ fi
30
+ echo ""
31
+ fi
32
+
22
33
  # Clone or update
23
34
  if [ -d "$DIR/.git" ]; then
24
35
  echo " Updating clauth..."; cd "$DIR" && git pull --quiet
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "0.7.5",
3
+ "version": "1.1.0",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "clauth": "./cli/index.js"
8
8
  },
9
9
  "scripts": {
10
- "build": "bash scripts/build.sh"
10
+ "build": "bash scripts/build.sh",
11
+ "postinstall": "node scripts/postinstall.js",
12
+ "worker:start": "node cli/index.js serve",
13
+ "worker:stop": "curl -s http://127.0.0.1:52437/shutdown 2>nul || taskkill /F /IM cloudflared.exe 2>nul & exit 0",
14
+ "worker:restart": "npm run worker:stop && timeout /t 3 /nobreak >nul && npm run worker:start"
11
15
  },
12
16
  "dependencies": {
13
17
  "chalk": "^5.3.0",
@@ -41,6 +45,7 @@
41
45
  "scripts/bin/",
42
46
  "scripts/bootstrap.cjs",
43
47
  "scripts/build.sh",
48
+ "scripts/postinstall.js",
44
49
  "supabase/",
45
50
  ".clauth-skill/",
46
51
  "install.sh",
@@ -38,6 +38,84 @@ if (lnk.status !== 0) {
38
38
  }
39
39
  console.log(' + clauth linked');
40
40
 
41
+ // Check if cloudflared is installed
42
+ var cfFound = false;
43
+ try {
44
+ execSync('cloudflared --version', { stdio: 'ignore' });
45
+ cfFound = true;
46
+ } catch {}
47
+
48
+ if (!cfFound) {
49
+ console.log('\n cloudflared not found — needed for claude.ai web integration (tunnel).');
50
+
51
+ var platform = process.platform;
52
+ var installCmd = null;
53
+ var installLabel = null;
54
+
55
+ if (platform === 'win32') {
56
+ try {
57
+ execSync('winget --version', { stdio: 'ignore' });
58
+ installCmd = 'winget install Cloudflare.cloudflared --silent --accept-package-agreements --accept-source-agreements';
59
+ installLabel = 'winget';
60
+ } catch {}
61
+ } else if (platform === 'darwin') {
62
+ try {
63
+ execSync('brew --version', { stdio: 'ignore' });
64
+ installCmd = 'brew install cloudflare/cloudflare/cloudflared';
65
+ installLabel = 'Homebrew';
66
+ } catch {}
67
+ }
68
+ // Linux: no auto-install — falls through to manual instructions
69
+
70
+ if (installCmd) {
71
+ process.stdout.write(' Install now via ' + installLabel + '? (y/n): ');
72
+ var answer = (function () {
73
+ try {
74
+ var buf = Buffer.alloc(3);
75
+ var fd = require('fs').openSync('/dev/tty', 'r');
76
+ require('fs').readSync(fd, buf, 0, 3);
77
+ require('fs').closeSync(fd);
78
+ return buf.toString().trim().toLowerCase();
79
+ } catch {
80
+ try {
81
+ var result = require('child_process').spawnSync(
82
+ 'powershell',
83
+ ['-Command', "$k = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); $k.Character"],
84
+ { encoding: 'utf8' }
85
+ );
86
+ return (result.stdout || '').trim().toLowerCase();
87
+ } catch { return 'n'; }
88
+ }
89
+ })();
90
+
91
+ if (answer === 'y') {
92
+ console.log(' Installing cloudflared via ' + installLabel + '...');
93
+ try {
94
+ execSync(installCmd, { stdio: 'inherit' });
95
+ console.log(' + cloudflared installed.');
96
+ } catch (e) {
97
+ console.log(' x Auto-install failed. Install manually:');
98
+ console.log(' https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/');
99
+ }
100
+ } else {
101
+ console.log(' Skipped. Install later and run: clauth tunnel setup');
102
+ }
103
+ } else {
104
+ console.log(' Install manually:');
105
+ if (platform === 'win32') {
106
+ console.log(' winget install Cloudflare.cloudflared');
107
+ console.log(' or: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/');
108
+ } else if (platform === 'darwin') {
109
+ console.log(' brew install cloudflare/cloudflare/cloudflared');
110
+ console.log(' or: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/');
111
+ } else {
112
+ console.log(' https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/');
113
+ }
114
+ console.log(' Then run: clauth tunnel setup');
115
+ }
116
+ console.log('');
117
+ }
118
+
41
119
  console.log('\n -> Launching clauth install...\n');
42
120
  var r = run('clauth install');
43
121
  process.exit(r.status || 0);
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ // scripts/postinstall.js
3
+ // Runs after npm install -g @lifeaitools/clauth
4
+ // Detects fresh install vs upgrade and acts accordingly
5
+
6
+ import fs from "fs";
7
+ import path from "path";
8
+ import os from "os";
9
+ import { fileURLToPath } from "url";
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+
13
+ const CONFIG_DIR = os.platform() === "win32"
14
+ ? path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "clauth-nodejs", "Config")
15
+ : path.join(os.homedir(), ".config", "clauth-nodejs");
16
+
17
+ let version = "1.0.0";
18
+ try {
19
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
20
+ version = pkg.version;
21
+ } catch {}
22
+
23
+ async function main() {
24
+ const configPath = path.join(CONFIG_DIR, "config.json");
25
+ const isUpgrade = fs.existsSync(configPath);
26
+
27
+ if (isUpgrade) {
28
+ console.log(`\n \u2713 clauth upgraded to v${version}`);
29
+ console.log(` \u2713 Config preserved at ${CONFIG_DIR}`);
30
+
31
+ // Try to detect running daemon
32
+ try {
33
+ const controller = new AbortController();
34
+ const timeout = setTimeout(() => controller.abort(), 2000);
35
+ const res = await fetch("http://127.0.0.1:52437/ping", { signal: controller.signal });
36
+ clearTimeout(timeout);
37
+ if (res.ok) {
38
+ console.log(" \u21BB Daemon is running \u2014 restart it with: clauth serve restart");
39
+ }
40
+ } catch {
41
+ console.log(" \u25CB Daemon not running");
42
+ }
43
+
44
+ console.log(" Run 'clauth doctor' to verify installation\n");
45
+ } else {
46
+ console.log(`\n Welcome to clauth v${version}!`);
47
+ console.log(" Run 'clauth install' to set up your vault");
48
+ console.log(" Run 'clauth doctor' to check prerequisites\n");
49
+ }
50
+ }
51
+
52
+ main().catch(() => {});