@lifeaitools/clauth 1.5.38 → 1.5.40

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.
@@ -170,6 +170,38 @@ clauth uninstall --ref R --pat P --yes Skip confirmation
170
170
  `vercel` `namecheap` `neo4j` `anthropic`
171
171
  `r2` `r2-bucket` `cloudflare` `rocketreach`
172
172
 
173
+ Service type `fileserver` — mount configuration for fs tools (UI-only config).
174
+
175
+ ---
176
+
177
+ ## MCP Server (v1.5.38+)
178
+
179
+ clauth runs as an MCP server with 3 namespaces and 27 tools:
180
+
181
+ | Path | Namespace | Tools |
182
+ |------|-----------|-------|
183
+ | `/clauth` | `clauth_*` | 13 credential vault tools |
184
+ | `/gws` | `gws_*` | 6 Google Workspace tools |
185
+ | `/fs` | `fs_*` | 8 filesystem tools (read, write, grep, glob, list, delete, mkdir, mounts) |
186
+ | `/mcp` | all | 27 tools combined |
187
+
188
+ ### claude.ai Connector URLs (noauth mode)
189
+ - `https://clauth.regendevcorp.com/clauth` — credential tools
190
+ - `https://clauth.regendevcorp.com/gws` — Google Workspace
191
+ - `https://fs.regendevcorp.com/fs` — filesystem tools
192
+
193
+ Noauth mode: fresh domains that return 404 on OAuth endpoints. claude.ai connects directly (Anthropic OAuth proxy bug workaround).
194
+
195
+ ### FS Tools
196
+ `fs_read` `fs_write` `fs_list` `fs_grep` `fs_glob` `fs_delete` `fs_mkdir` `fs_mounts`
197
+
198
+ Path-jail security: all paths resolved against mount root. Permission flags (r/w/d) per mount. Uses `@vscode/ripgrep` for grep, `fast-glob` for glob.
199
+
200
+ ### Testing
201
+ ```bash
202
+ node test-tools.mjs # 25 tool execution tests across all 3 namespaces
203
+ ```
204
+
173
205
  ---
174
206
 
175
207
  ## Troubleshooting
package/README.md CHANGED
@@ -108,16 +108,102 @@ Nothing stored locally. Password never persisted. Machine hash is one-way only.
108
108
 
109
109
  ---
110
110
 
111
+ ## Daemon Mode (`clauth serve`)
112
+
113
+ clauth runs as an HTTP daemon on `http://127.0.0.1:52437`. The daemon provides:
114
+
115
+ - **Web UI** — unlock vault, manage services, configure mounts
116
+ - **REST API** — `GET /get/<service>`, `GET /ping`, `POST /restart`, `GET /shutdown`
117
+ - **MCP server** — Model Context Protocol for Claude Code and claude.ai
118
+ - **Cloudflare Tunnel** — exposes MCP endpoints publicly for claude.ai connectors
119
+
120
+ Start: `clauth serve start` (starts locked, auto-opens browser for unlock).
121
+
122
+ Full daemon operations reference: see `regen-root/.claude/rules/clauth.md`.
123
+
124
+ ---
125
+
126
+ ## MCP Server — 3 Namespaces, 27 Tools
127
+
128
+ clauth is the single MCP interface for all local tools. One process, namespaced paths:
129
+
130
+ | Path | Namespace | Tools | Description |
131
+ |------|-----------|-------|-------------|
132
+ | `/clauth` | `clauth_*` | 13 | Credential vault operations |
133
+ | `/gws` | `gws_*` | 6 | Google Workspace (Gmail, Calendar, Drive) |
134
+ | `/fs` | `fs_*` | 8 | Filesystem (read, write, grep, glob, delete, mkdir, mounts) |
135
+ | `/mcp` | all | 27 | All namespaces combined (Claude Code) |
136
+
137
+ ### FS Tools (v1.5.38)
138
+
139
+ 8 filesystem tools with path-jail security:
140
+ - `fs_read`, `fs_write`, `fs_list`, `fs_grep`, `fs_glob`, `fs_delete`, `fs_mkdir`, `fs_mounts`
141
+ - Uses `node:fs/promises` (async), `@vscode/ripgrep` (shipped binary), `fast-glob`
142
+ - Permission flags per mount: `r` (read), `w` (write), `d` (delete)
143
+ - Mount config stored as "fileserver" service type in vault — only configurable through web UI
144
+
145
+ ### GWS Tools
146
+
147
+ 6 Google Workspace tools: `gws_gmail_list`, `gws_gmail_read`, `gws_gmail_send`, `gws_gmail_draft`, `gws_calendar_list`, `gws_calendar_create`
148
+ - Calls `gws` CLI via `execSync` with `shell: 'bash'` (fixes Windows cmd.exe JSON quoting)
149
+
150
+ ---
151
+
152
+ ## claude.ai Integration
153
+
154
+ ### Noauth Mode (v1.5.38)
155
+
156
+ claude.ai's OAuth proxy has a confirmed bug ([anthropics/claude-code#46140](https://github.com/anthropics/claude-code/issues/46140), [anthropics/claude-ai-mcp#136](https://github.com/anthropics/claude-ai-mcp/issues/136)): it completes the token exchange but never sends the authenticated request.
157
+
158
+ **Workaround:** Noauth hosts — fresh domains where OAuth endpoints return 404. claude.ai connects directly (tunnel URL is the shared secret).
159
+
160
+ ### OAuth 2.1 (v1.5.36-37)
161
+
162
+ Full OAuth 2.1 protocol implementation is present for future use when Anthropic fixes the bug:
163
+ - 401 gate with `WWW-Authenticate` header
164
+ - Dynamic client registration (public client, no secret)
165
+ - Mandatory PKCE S256
166
+ - `Cache-Control: no-store`
167
+
168
+ ### Connector URLs
169
+
170
+ | Connector | URL |
171
+ |-----------|-----|
172
+ | clauth | `https://clauth.regendevcorp.com/clauth` |
173
+ | gws | `https://clauth.regendevcorp.com/gws` |
174
+ | fs | `https://fs.regendevcorp.com/fs` |
175
+
176
+ ---
177
+
178
+ ## Dependencies (notable)
179
+
180
+ - `@vscode/ripgrep` — shipped ripgrep binary for `fs_grep`
181
+ - `fast-glob` — pattern matching for `fs_glob`
182
+
183
+ ---
184
+
185
+ ## Testing
186
+
187
+ ```bash
188
+ node test-tools.mjs # 25 tool execution tests across all 3 namespaces
189
+ ```
190
+
191
+ Tests actual MCP tool calls (not just OAuth + listing).
192
+
193
+ ---
194
+
111
195
  ## Releasing a New Version (maintainers)
112
196
 
113
197
  ```bash
114
198
  # 1. Bump version in package.json
115
199
  # 2. Commit and tag
116
- git tag v0.1.1
117
- git push --tags
200
+ git tag v1.5.38
201
+ git push && git push --tags
118
202
  # GitHub Actions publishes automatically via Trusted Publishing
119
203
  ```
120
204
 
205
+ **NEVER** commit a version bump without tagging — the tag triggers npm CI.
206
+
121
207
  ---
122
208
 
123
209
  > Life before Profits. — LIFEAI / PRT
@@ -75,9 +75,17 @@ CREATE TABLE IF NOT EXISTS clauth_rotation_log (
75
75
  );
76
76
  ALTER TABLE clauth_rotation_log ENABLE ROW LEVEL SECURITY;`,
77
77
  },
78
+ {
79
+ version: 5,
80
+ name: "005_fileserver_key_type",
81
+ description: "Add fileserver key_type for filesystem mount configs",
82
+ type: "safe",
83
+ sql: `ALTER TABLE clauth_services DROP CONSTRAINT IF EXISTS clauth_services_key_type_check;
84
+ ALTER TABLE clauth_services ADD CONSTRAINT clauth_services_key_type_check CHECK (key_type IN ('token','keypair','connstring','oauth','secret','fileserver'));`,
85
+ },
78
86
  ];
79
87
 
80
- const CURRENT_SCHEMA_VERSION = 4;
88
+ const CURRENT_SCHEMA_VERSION = 5;
81
89
 
82
90
  // ── Key Rotation Config ─────────────────────────────────────────
83
91
  // Per-service rotation capabilities. "auto" services can be rotated programmatically.
@@ -1754,6 +1762,7 @@ const TYPE_LABELS = {
1754
1762
  keypair: "keypair (user:key pair)",
1755
1763
  connstring: "connstring (connection string)",
1756
1764
  oauth: "oauth (OAuth credentials)",
1765
+ fileserver: "fileserver (mount config)",
1757
1766
  };
1758
1767
 
1759
1768
  async function toggleAddService() {
@@ -1771,12 +1780,12 @@ async function toggleAddService() {
1771
1780
  sel.innerHTML = '<option value="">Loading…</option>';
1772
1781
  try {
1773
1782
  const r = await fetch(BASE + "/meta").then(r => r.json());
1774
- const types = r.key_types || ["token", "secret", "keypair", "connstring", "oauth"];
1783
+ const types = r.key_types || ["token", "secret", "keypair", "connstring", "oauth", "fileserver"];
1775
1784
  sel.innerHTML = types.map(t =>
1776
1785
  \`<option value="\${t}">\${TYPE_LABELS[t] || t}</option>\`
1777
1786
  ).join("");
1778
1787
  } catch {
1779
- sel.innerHTML = ["token","secret","keypair","connstring","oauth"].map(t =>
1788
+ sel.innerHTML = ["token","secret","keypair","connstring","oauth","fileserver"].map(t =>
1780
1789
  \`<option value="\${t}">\${TYPE_LABELS[t] || t}</option>\`
1781
1790
  ).join("");
1782
1791
  }
@@ -2911,7 +2920,18 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2911
2920
  return res.end();
2912
2921
  }
2913
2922
 
2923
+ // ── Hosts that bypass OAuth (fresh domains for claude.ai compatibility) ──
2924
+ const NOAUTH_HOSTS = ["fs.regendevcorp.com", "clauth.regendevcorp.com"];
2925
+ const requestHost = (req.headers.host || "").split(":")[0].toLowerCase();
2926
+ const noAuthHost = NOAUTH_HOSTS.includes(requestHost);
2927
+
2914
2928
  // ── OAuth Discovery (RFC 9728 + RFC 8414) ──────────────
2929
+ // Suppress OAuth discovery on noauth hosts — prevents claude.ai from entering OAuth flow
2930
+ if (noAuthHost && reqPath.startsWith("/.well-known/")) {
2931
+ res.writeHead(404, { "Content-Type": "application/json", ...CORS });
2932
+ return res.end(JSON.stringify({ error: "not_found" }));
2933
+ }
2934
+
2915
2935
  // Restore full well-known + OAuth so custom setup with client_id/secret works.
2916
2936
  if (reqPath.startsWith("/.well-known/oauth-protected-resource")) {
2917
2937
  const base = oauthBase();
@@ -2943,6 +2963,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2943
2963
  }
2944
2964
 
2945
2965
  // ── Dynamic Client Registration (RFC 7591) ──────────────
2966
+ // Block OAuth endpoints on noauth hosts
2967
+ if (noAuthHost && ["/register", "/authorize", "/token"].includes(reqPath)) {
2968
+ res.writeHead(404, { "Content-Type": "application/json", ...CORS });
2969
+ return res.end(JSON.stringify({ error: "not_found" }));
2970
+ }
2946
2971
  if (method === "POST" && reqPath === "/register") {
2947
2972
  let body;
2948
2973
  try { body = await readBody(req); } catch {
@@ -3094,7 +3119,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3094
3119
  const isMcpPath = MCP_PATHS.includes(reqPath);
3095
3120
  function toolsForPath(p) {
3096
3121
  if (p === "/gws") return MCP_TOOLS.filter(t => t.name.startsWith("gws_"));
3097
- if (p === "/clauth") return MCP_TOOLS.filter(t => t.name.startsWith("clauth_"));
3122
+ if (p === "/clauth") return MCP_TOOLS.filter(t => t.name.startsWith("clauth_") || t.name === "monkey_dispatch" || t.name.startsWith("terminal_"));
3098
3123
  if (p === "/fs") return MCP_TOOLS.filter(t => t.name.startsWith("fs_"));
3099
3124
  return MCP_TOOLS; // /mcp — all tools
3100
3125
  }
@@ -3106,11 +3131,12 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3106
3131
  }
3107
3132
 
3108
3133
  // ── MCP endpoint auth — 401 gate (OAuth 2.1 protocol) ──
3134
+ // Skipped for noauth hosts — those domains use tunnel URL as shared secret (like regen-media)
3109
3135
  if (method === "POST" && (reqPath === "/sse" || isMcpPath)) {
3110
3136
  const authHeader = req.headers.authorization;
3111
3137
  const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
3112
3138
 
3113
- if (!token || !oauthTokens.has(token)) {
3139
+ if (!noAuthHost && (!token || !oauthTokens.has(token))) {
3114
3140
  // No valid Bearer token → return 401 with discovery hint
3115
3141
  const base = oauthBase();
3116
3142
  const resourcePath = isMcpPath ? reqPath.slice(1) : "sse"; // "mcp", "gws", "clauth", or "sse"
@@ -3445,6 +3471,106 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3445
3471
  return;
3446
3472
  }
3447
3473
 
3474
+ // POST /dispatch — spawn a headless Claude CLI worker for monkey skill queue
3475
+ if (method === "POST" && reqPath === "/dispatch") {
3476
+ let body = "";
3477
+ req.on("data", d => body += d);
3478
+ req.on("end", () => {
3479
+ try {
3480
+ const { prompt, job_id } = JSON.parse(body);
3481
+ if (!prompt) {
3482
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3483
+ return res.end(JSON.stringify({ error: "prompt required" }));
3484
+ }
3485
+ const result = spawnClaudeTask(prompt, job_id || "untracked");
3486
+ const status = result.error ? 503 : 200;
3487
+ res.writeHead(status, { "Content-Type": "application/json", ...CORS });
3488
+ res.end(JSON.stringify(result));
3489
+ } catch {
3490
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3491
+ res.end(JSON.stringify({ error: "invalid JSON" }));
3492
+ }
3493
+ });
3494
+ return;
3495
+ }
3496
+
3497
+ // POST /terminal/start — start a named terminal session
3498
+ if (method === "POST" && reqPath === "/terminal/start") {
3499
+ let body = "";
3500
+ req.on("data", d => body += d);
3501
+ req.on("end", () => {
3502
+ try {
3503
+ const { name, knowledge_tier, context_md } = JSON.parse(body);
3504
+ if (!name) {
3505
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3506
+ return res.end(JSON.stringify({ error: "name required" }));
3507
+ }
3508
+ const tier = knowledge_tier || 'db_only';
3509
+ const result = startTerminalSession(name, tier, context_md || null);
3510
+ const status = result.error ? 503 : 200;
3511
+ res.writeHead(status, { "Content-Type": "application/json", ...CORS });
3512
+ res.end(JSON.stringify(result));
3513
+ } catch {
3514
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3515
+ res.end(JSON.stringify({ error: "invalid JSON" }));
3516
+ }
3517
+ });
3518
+ return;
3519
+ }
3520
+
3521
+ // POST /terminal/send — send a message to a running terminal session
3522
+ if (method === "POST" && reqPath === "/terminal/send") {
3523
+ let body = "";
3524
+ req.on("data", d => body += d);
3525
+ req.on("end", () => {
3526
+ try {
3527
+ const { session_id, message } = JSON.parse(body);
3528
+ if (!session_id || !message) {
3529
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3530
+ return res.end(JSON.stringify({ error: "session_id and message required" }));
3531
+ }
3532
+ const result = sendTerminalMessage(session_id, message);
3533
+ const status = result.error === 'session_busy' ? 409 : result.error ? 404 : 200;
3534
+ res.writeHead(status, { "Content-Type": "application/json", ...CORS });
3535
+ res.end(JSON.stringify(result));
3536
+ } catch {
3537
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3538
+ res.end(JSON.stringify({ error: "invalid JSON" }));
3539
+ }
3540
+ });
3541
+ return;
3542
+ }
3543
+
3544
+ // GET /terminal/list — list all active terminal sessions
3545
+ if (method === "GET" && reqPath === "/terminal/list") {
3546
+ res.writeHead(200, { "Content-Type": "application/json", ...CORS });
3547
+ res.end(JSON.stringify(listTerminalSessions()));
3548
+ return;
3549
+ }
3550
+
3551
+ // POST /terminal/stop — stop a terminal session
3552
+ if (method === "POST" && reqPath === "/terminal/stop") {
3553
+ let body = "";
3554
+ req.on("data", d => body += d);
3555
+ req.on("end", () => {
3556
+ try {
3557
+ const { session_id } = JSON.parse(body);
3558
+ if (!session_id) {
3559
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3560
+ return res.end(JSON.stringify({ error: "session_id required" }));
3561
+ }
3562
+ const result = stopTerminalSession(session_id);
3563
+ const status = result.error ? 404 : 200;
3564
+ res.writeHead(status, { "Content-Type": "application/json", ...CORS });
3565
+ res.end(JSON.stringify(result));
3566
+ } catch {
3567
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3568
+ res.end(JSON.stringify({ error: "invalid JSON" }));
3569
+ }
3570
+ });
3571
+ return;
3572
+ }
3573
+
3448
3574
  // GET|POST /shutdown (for daemon stop — programmatic, keeps boot.key)
3449
3575
  // Accept POST as well — older scripts and curl default to POST
3450
3576
  if ((method === "GET" || method === "POST") && reqPath === "/shutdown") {
@@ -3576,11 +3702,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3576
3702
  const statusResult = await api.status(password, machineHash, token, timestamp);
3577
3703
  const existingTypes = [...new Set((statusResult.services || []).map(s => s.key_type).filter(Boolean))];
3578
3704
  // Merge with known types (in case no service of that type exists yet)
3579
- const knownTypes = ["token", "secret", "keypair", "connstring", "oauth"];
3705
+ const knownTypes = ["token", "secret", "keypair", "connstring", "oauth", "fileserver"];
3580
3706
  const allTypes = [...new Set([...knownTypes, ...existingTypes])];
3581
3707
  return ok(res, { key_types: allTypes });
3582
3708
  } catch (err) {
3583
- return ok(res, { key_types: ["token", "secret", "keypair", "connstring", "oauth"] });
3709
+ return ok(res, { key_types: ["token", "secret", "keypair", "connstring", "oauth", "fileserver"] });
3584
3710
  }
3585
3711
  }
3586
3712
 
@@ -4403,7 +4529,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
4403
4529
  res.writeHead(400, { "Content-Type": "application/json", ...CORS });
4404
4530
  return res.end(JSON.stringify({ error: "name is required" }));
4405
4531
  }
4406
- const validTypes = ["token", "keypair", "connstring", "oauth", "secret"];
4532
+ const validTypes = ["token", "keypair", "connstring", "oauth", "secret", "fileserver"];
4407
4533
  const type = (key_type || "token").toLowerCase();
4408
4534
  if (!validTypes.includes(type)) {
4409
4535
  res.writeHead(400, { "Content-Type": "application/json", ...CORS });
@@ -4770,6 +4896,153 @@ async function actionForeground(opts) {
4770
4896
  import { createInterface } from "readline";
4771
4897
  import { execSync, spawn as spawnProc } from "child_process";
4772
4898
 
4899
+ // ── Monkey dispatch — headless Claude CLI worker ─────────────────
4900
+ function findClaudeBinary() {
4901
+ const candidates = [
4902
+ process.env.CLAUDE_BIN,
4903
+ path.join(process.env.APPDATA || '', 'npm', 'claude.cmd'),
4904
+ path.join(process.env.APPDATA || '', 'npm', 'claude'),
4905
+ 'claude', // PATH fallback
4906
+ ].filter(Boolean);
4907
+
4908
+ for (const c of candidates) {
4909
+ try {
4910
+ execSync(`"${c}" --version`, { stdio: 'ignore', timeout: 3000 });
4911
+ return c;
4912
+ } catch {}
4913
+ }
4914
+ return null;
4915
+ }
4916
+
4917
+ let activeCliWorkers = 0;
4918
+ const MAX_CLI_WORKERS = 2;
4919
+
4920
+ function spawnClaudeTask(prompt, jobId) {
4921
+ if (activeCliWorkers >= MAX_CLI_WORKERS) {
4922
+ return { error: 'concurrency_limit', message: `Max ${MAX_CLI_WORKERS} CLI workers active` };
4923
+ }
4924
+ const binary = findClaudeBinary();
4925
+ if (!binary) {
4926
+ return { error: 'binary_not_found', message: 'claude CLI not found in PATH or AppData/npm' };
4927
+ }
4928
+
4929
+ activeCliWorkers++;
4930
+ const proc = spawnProc(binary, ['-p', prompt, '--dangerously-skip-permissions'], {
4931
+ cwd: 'C:/Dev/regen-root',
4932
+ env: process.env,
4933
+ stdio: ['ignore', 'pipe', 'pipe'],
4934
+ shell: true,
4935
+ });
4936
+
4937
+ const startedAt = Date.now();
4938
+ proc.on('close', (code) => {
4939
+ activeCliWorkers--;
4940
+ console.log(`[monkey] job ${jobId} exited code=${code} in ${Date.now() - startedAt}ms`);
4941
+ });
4942
+
4943
+ return { status: 'spawned', pid: proc.pid, jobId, activeWorkers: activeCliWorkers };
4944
+ }
4945
+
4946
+ // ── Terminal session manager ─────────────────────────────────────
4947
+ // Rolling-context approach: each session stores a context string.
4948
+ // /terminal/send spawns a fresh claude -p with [context + message],
4949
+ // captures stdout, stores result back in session context.
4950
+ const terminalSessions = new Map(); // session_id → SessionState
4951
+
4952
+ function generateSessionId() {
4953
+ return crypto.randomUUID();
4954
+ }
4955
+
4956
+ function startTerminalSession(name, knowledge_tier, context_md) {
4957
+ const binary = findClaudeBinary();
4958
+ if (!binary) {
4959
+ return { error: 'binary_not_found', message: 'claude CLI not found in PATH or AppData/npm' };
4960
+ }
4961
+ const session_id = generateSessionId();
4962
+ const preamble = context_md
4963
+ ? `${context_md}\n\n---\n`
4964
+ : '';
4965
+ const initialContext = `${preamble}Session: ${name} | Tier: ${knowledge_tier}\nReady. Await instructions.`;
4966
+ const session = {
4967
+ session_id,
4968
+ name,
4969
+ knowledge_tier,
4970
+ status: 'ready',
4971
+ started_at: new Date().toISOString(),
4972
+ context: initialContext,
4973
+ activeProc: null,
4974
+ };
4975
+ terminalSessions.set(session_id, session);
4976
+ console.log(`[terminal] started session ${session_id} name=${name}`);
4977
+ return { session_id, status: 'ready' };
4978
+ }
4979
+
4980
+ function sendTerminalMessage(session_id, message) {
4981
+ const session = terminalSessions.get(session_id);
4982
+ if (!session) return { error: 'not_found', message: `Session ${session_id} not found` };
4983
+ if (session.status === 'stopped') return { error: 'stopped', message: 'Session is stopped' };
4984
+ if (session.status === 'busy') return { error: 'session_busy', message: 'Session is busy — try again shortly' };
4985
+
4986
+ const binary = findClaudeBinary();
4987
+ if (!binary) return { error: 'binary_not_found', message: 'claude CLI not found in PATH or AppData/npm' };
4988
+
4989
+ session.status = 'busy';
4990
+ const fullPrompt = `${session.context}\n\n---\nUser: ${message}`;
4991
+
4992
+ const proc = spawnProc(binary, ['-p', fullPrompt, '--dangerously-skip-permissions'], {
4993
+ cwd: 'C:/Dev/regen-root',
4994
+ env: process.env,
4995
+ stdio: ['ignore', 'pipe', 'pipe'],
4996
+ shell: true,
4997
+ });
4998
+ session.activeProc = proc;
4999
+
5000
+ let stdout = '';
5001
+ let stderr = '';
5002
+ proc.stdout.on('data', d => { stdout += d; });
5003
+ proc.stderr.on('data', d => { stderr += d; });
5004
+ proc.on('close', (code) => {
5005
+ if (terminalSessions.has(session_id)) {
5006
+ const s = terminalSessions.get(session_id);
5007
+ const response = stdout.trim() || stderr.trim() || '(no output)';
5008
+ // Append turn to rolling context (cap at ~8000 chars to avoid overflow)
5009
+ const turn = `\n\nUser: ${message}\nAssistant: ${response}`;
5010
+ const combined = s.context + turn;
5011
+ s.context = combined.length > 8000 ? combined.slice(combined.length - 8000) : combined;
5012
+ s.status = 'ready';
5013
+ s.activeProc = null;
5014
+ console.log(`[terminal] session ${session_id} turn complete code=${code}`);
5015
+ }
5016
+ });
5017
+
5018
+ return { queued: true, session_id };
5019
+ }
5020
+
5021
+ function listTerminalSessions() {
5022
+ const sessions = [];
5023
+ for (const [, s] of terminalSessions) {
5024
+ sessions.push({
5025
+ session_id: s.session_id,
5026
+ name: s.name,
5027
+ status: s.status,
5028
+ knowledge_tier: s.knowledge_tier,
5029
+ started_at: s.started_at,
5030
+ });
5031
+ }
5032
+ return sessions;
5033
+ }
5034
+
5035
+ function stopTerminalSession(session_id) {
5036
+ const session = terminalSessions.get(session_id);
5037
+ if (!session) return { error: 'not_found', message: `Session ${session_id} not found` };
5038
+ if (session.activeProc) {
5039
+ try { session.activeProc.kill(); } catch {}
5040
+ }
5041
+ terminalSessions.delete(session_id);
5042
+ console.log(`[terminal] stopped session ${session_id}`);
5043
+ return { stopped: true, session_id };
5044
+ }
5045
+
4773
5046
  const ENV_MAP = {
4774
5047
  "github": "GITHUB_TOKEN",
4775
5048
  "supabase-anon": "NEXT_PUBLIC_SUPABASE_ANON_KEY",
@@ -4787,14 +5060,48 @@ const ENV_MAP = {
4787
5060
  "gmail": "GMAIL_CREDENTIALS",
4788
5061
  };
4789
5062
 
4790
- // ── Filesystem service config ──
4791
- const FS_MOUNTS = [
4792
- { name: "regen-root", path: "C:/Dev/regen-root", access: "rwd" },
4793
- ];
5063
+ // ── Filesystem service config — loaded from clauth vault ──
5064
+ let _fsMountsCache = null;
5065
+ let _fsMountsCacheTime = 0;
5066
+ const FS_CACHE_TTL = 60000; // 1 minute
5067
+
5068
+ async function getFileserverMounts(vault) {
5069
+ if (!vault.password) return { error: "Vault is locked — unlock first" };
5070
+ const now = Date.now();
5071
+ if (_fsMountsCache && now - _fsMountsCacheTime < FS_CACHE_TTL) return { mounts: _fsMountsCache };
5072
+
5073
+ try {
5074
+ const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
5075
+ const result = await api.status(vault.password, vault.machineHash, token, timestamp);
5076
+ if (result.error) return { error: result.error };
5077
+ const mounts = [];
5078
+ for (const s of (result.services || [])) {
5079
+ if (s.key_type === "fileserver" && s.enabled) {
5080
+ try {
5081
+ const { token: t2, timestamp: ts2 } = deriveToken(vault.password, vault.machineHash);
5082
+ const secret = await api.retrieve(vault.password, vault.machineHash, t2, ts2, s.name);
5083
+ if (secret.value) {
5084
+ const config = JSON.parse(secret.value);
5085
+ mounts.push({ name: s.name, path: config.path, access: config.access || "r" });
5086
+ }
5087
+ } catch {}
5088
+ }
5089
+ }
5090
+ _fsMountsCache = mounts;
5091
+ _fsMountsCacheTime = now;
5092
+ return { mounts };
5093
+ } catch (err) {
5094
+ return { error: `Mount lookup failed: ${err.message}` };
5095
+ }
5096
+ }
4794
5097
 
4795
- function resolveInMount(requestedPath, mountName) {
4796
- const mount = FS_MOUNTS.find(m => m.name === mountName) || FS_MOUNTS[0];
4797
- if (!mount) return { error: "No filesystem mounts configured" };
5098
+ async function resolveInMount(requestedPath, mountName, vault) {
5099
+ const { mounts, error } = await getFileserverMounts(vault);
5100
+ if (error) return { error };
5101
+ if (!mounts || mounts.length === 0) return { error: "No fileserver services configured. Add one with key_type='fileserver' and value: {\"path\": \"C:/Dev/regen-root\", \"access\": \"rwd\"}" };
5102
+ const mount = mountName ? mounts.find(m => m.name === mountName) : mounts[0];
5103
+ if (!mount) return { error: `Mount '${mountName}' not found. Available: ${mounts.map(m => m.name).join(", ")}` };
5104
+ if (!mount.path) return { error: `Fileserver '${mount.name}' has no path configured` };
4798
5105
  const resolved = path.resolve(mount.path, requestedPath);
4799
5106
  const normalized = path.normalize(resolved);
4800
5107
  if (!normalized.startsWith(path.normalize(mount.path))) {
@@ -4902,6 +5209,65 @@ const MCP_TOOLS = [
4902
5209
  description: "Test whether the clauth MCP connector is reachable via the Cloudflare tunnel. Returns connectivity status and tunnel URL.",
4903
5210
  inputSchema: { type: "object", properties: {}, additionalProperties: false }
4904
5211
  },
5212
+ {
5213
+ name: "monkey_dispatch",
5214
+ description: "Dispatch a skill job to a headless Claude Code CLI worker. Spawns claude -p with the given prompt in C:/Dev/regen-root. Max 2 concurrent workers.",
5215
+ inputSchema: {
5216
+ type: "object",
5217
+ properties: {
5218
+ prompt: { type: "string", description: "Full prompt for the CLI worker to execute" },
5219
+ job_id: { type: "string", description: "monkey_jobs UUID for tracking" },
5220
+ },
5221
+ required: ["prompt"],
5222
+ additionalProperties: false,
5223
+ },
5224
+ },
5225
+
5226
+ // ── Terminal session manager ───────────────────────────────────────────
5227
+ {
5228
+ name: "terminal_start",
5229
+ description: "Start a named persistent Claude session. Sessions maintain rolling context across sends. Returns session_id for subsequent sends.",
5230
+ inputSchema: {
5231
+ type: "object",
5232
+ properties: {
5233
+ name: { type: "string", description: "Human-readable session name (e.g. 'knowledgedude-prt')" },
5234
+ knowledge_tier: { type: "string", enum: ["db_only", "corpus", "project"], description: "Knowledge tier for the session. 'corpus' injects context_md as preamble." },
5235
+ context_md: { type: "string", description: "Optional markdown preamble injected as session context (used for 'corpus' tier)" },
5236
+ },
5237
+ required: ["name"],
5238
+ additionalProperties: false,
5239
+ },
5240
+ },
5241
+ {
5242
+ name: "terminal_send",
5243
+ description: "Send a message to a running terminal session. Spawns a Claude worker with rolling context + message. Returns immediately — session processes async.",
5244
+ inputSchema: {
5245
+ type: "object",
5246
+ properties: {
5247
+ session_id: { type: "string", description: "Session ID returned by terminal_start" },
5248
+ message: { type: "string", description: "Message to send to the session" },
5249
+ },
5250
+ required: ["session_id", "message"],
5251
+ additionalProperties: false,
5252
+ },
5253
+ },
5254
+ {
5255
+ name: "terminal_list",
5256
+ description: "List all active terminal sessions with their status (ready/busy/stopped), knowledge tier, and start time.",
5257
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
5258
+ },
5259
+ {
5260
+ name: "terminal_stop",
5261
+ description: "Stop a terminal session and remove it from memory. Kills any active process.",
5262
+ inputSchema: {
5263
+ type: "object",
5264
+ properties: {
5265
+ session_id: { type: "string", description: "Session ID to stop" },
5266
+ },
5267
+ required: ["session_id"],
5268
+ additionalProperties: false,
5269
+ },
5270
+ },
4905
5271
 
4906
5272
  // ── Google Workspace (gws CLI) ──────────────────────────────────────────
4907
5273
  {
@@ -5083,6 +5449,11 @@ const MCP_TOOLS = [
5083
5449
  additionalProperties: false,
5084
5450
  },
5085
5451
  },
5452
+ {
5453
+ name: "fs_mounts",
5454
+ description: "List configured filesystem mounts (fileserver services from vault).",
5455
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
5456
+ },
5086
5457
  ];
5087
5458
 
5088
5459
  function writeTempSecret(service, value) {
@@ -5111,6 +5482,9 @@ function mcpError(text) {
5111
5482
  return { content: [{ type: "text", text }], isError: true };
5112
5483
  }
5113
5484
 
5485
+ // Windows cmd.exe doesn't support single quotes — use bash for gws JSON args
5486
+ const GWS_EXEC_OPTS = { encoding: "utf8", timeout: 30000, windowsHide: true, shell: os.platform() === "win32" ? "bash" : undefined };
5487
+
5114
5488
  async function handleMcpTool(vault, name, args) {
5115
5489
  switch (name) {
5116
5490
  case "clauth_ping": {
@@ -5384,7 +5758,7 @@ async function handleMcpTool(vault, name, args) {
5384
5758
  if (params) cmdArgs.push("--params", `'${JSON.stringify(params)}'`);
5385
5759
  if (body) cmdArgs.push("--json", `'${JSON.stringify(body)}'`);
5386
5760
  try {
5387
- const raw = execSyncTop(["gws", ...cmdArgs].join(" "), { encoding: "utf8", timeout: 30000, windowsHide: true });
5761
+ const raw = execSyncTop(["gws", ...cmdArgs].join(" "), GWS_EXEC_OPTS);
5388
5762
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5389
5763
  } catch (err) {
5390
5764
  return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`);
@@ -5395,7 +5769,7 @@ async function handleMcpTool(vault, name, args) {
5395
5769
  const p = { userId: "me", maxResults: args.max_results ?? 10 };
5396
5770
  if (args.query) p.q = args.query;
5397
5771
  try {
5398
- const raw = execSyncTop(`gws gmail users messages list --params '${JSON.stringify(p)}'`, { encoding: "utf8", timeout: 30000, windowsHide: true });
5772
+ const raw = execSyncTop(`gws gmail users messages list --params '${JSON.stringify(p)}'`, GWS_EXEC_OPTS);
5399
5773
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5400
5774
  } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5401
5775
  }
@@ -5403,7 +5777,7 @@ async function handleMcpTool(vault, name, args) {
5403
5777
  case "gws_gmail_read": {
5404
5778
  const p = { userId: "me", id: args.message_id, format: "full" };
5405
5779
  try {
5406
- const raw = execSyncTop(`gws gmail users messages get --params '${JSON.stringify(p)}'`, { encoding: "utf8", timeout: 30000, windowsHide: true });
5780
+ const raw = execSyncTop(`gws gmail users messages get --params '${JSON.stringify(p)}'`, GWS_EXEC_OPTS);
5407
5781
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5408
5782
  } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5409
5783
  }
@@ -5415,7 +5789,7 @@ async function handleMcpTool(vault, name, args) {
5415
5789
  const encoded = Buffer.from(lines.join("\r\n")).toString("base64url");
5416
5790
  const bodyObj = { userId: "me", resource: { raw: encoded } };
5417
5791
  try {
5418
- const raw = execSyncTop(`gws gmail users messages send --json '${JSON.stringify(bodyObj)}'`, { encoding: "utf8", timeout: 30000, windowsHide: true });
5792
+ const raw = execSyncTop(`gws gmail users messages send --json '${JSON.stringify(bodyObj)}'`, GWS_EXEC_OPTS);
5419
5793
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5420
5794
  } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5421
5795
  }
@@ -5425,7 +5799,7 @@ async function handleMcpTool(vault, name, args) {
5425
5799
  if (args.time_min) p.timeMin = args.time_min;
5426
5800
  if (args.time_max) p.timeMax = args.time_max;
5427
5801
  try {
5428
- const raw = execSyncTop(`gws calendar events list --params '${JSON.stringify(p)}'`, { encoding: "utf8", timeout: 30000, windowsHide: true });
5802
+ const raw = execSyncTop(`gws calendar events list --params '${JSON.stringify(p)}'`, GWS_EXEC_OPTS);
5429
5803
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5430
5804
  } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5431
5805
  }
@@ -5434,7 +5808,7 @@ async function handleMcpTool(vault, name, args) {
5434
5808
  const p = { pageSize: args.max_results ?? 10, fields: "files(id,name,mimeType,modifiedTime,size,webViewLink)" };
5435
5809
  if (args.query) p.q = args.query;
5436
5810
  try {
5437
- const raw = execSyncTop(`gws drive files list --params '${JSON.stringify(p)}'`, { encoding: "utf8", timeout: 30000, windowsHide: true });
5811
+ const raw = execSyncTop(`gws drive files list --params '${JSON.stringify(p)}'`, GWS_EXEC_OPTS);
5438
5812
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5439
5813
  } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5440
5814
  }
@@ -5442,7 +5816,7 @@ async function handleMcpTool(vault, name, args) {
5442
5816
  // ── Filesystem tools ──────────────────────────────────────
5443
5817
 
5444
5818
  case "fs_read": {
5445
- const r = resolveInMount(args.path, args.mount);
5819
+ const r = await resolveInMount(args.path, args.mount, vault);
5446
5820
  if (r.error) return mcpError(r.error);
5447
5821
  if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
5448
5822
  try {
@@ -5461,7 +5835,7 @@ async function handleMcpTool(vault, name, args) {
5461
5835
  }
5462
5836
 
5463
5837
  case "fs_write": {
5464
- const r = resolveInMount(args.path, args.mount);
5838
+ const r = await resolveInMount(args.path, args.mount, vault);
5465
5839
  if (r.error) return mcpError(r.error);
5466
5840
  if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
5467
5841
  try {
@@ -5475,7 +5849,7 @@ async function handleMcpTool(vault, name, args) {
5475
5849
 
5476
5850
  case "fs_list": {
5477
5851
  const dirPath = args.path || ".";
5478
- const r = resolveInMount(dirPath, args.mount);
5852
+ const r = await resolveInMount(dirPath, args.mount, vault);
5479
5853
  if (r.error) return mcpError(r.error);
5480
5854
  if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
5481
5855
  try {
@@ -5503,7 +5877,7 @@ async function handleMcpTool(vault, name, args) {
5503
5877
 
5504
5878
  case "fs_grep": {
5505
5879
  const searchPath = args.path || ".";
5506
- const r = resolveInMount(searchPath, args.mount);
5880
+ const r = await resolveInMount(searchPath, args.mount, vault);
5507
5881
  if (r.error) return mcpError(r.error);
5508
5882
  if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
5509
5883
 
@@ -5553,7 +5927,7 @@ async function handleMcpTool(vault, name, args) {
5553
5927
 
5554
5928
  case "fs_glob": {
5555
5929
  const basePath = args.path || ".";
5556
- const r = resolveInMount(basePath, args.mount);
5930
+ const r = await resolveInMount(basePath, args.mount, vault);
5557
5931
  if (r.error) return mcpError(r.error);
5558
5932
  if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
5559
5933
  try {
@@ -5571,11 +5945,12 @@ async function handleMcpTool(vault, name, args) {
5571
5945
  }
5572
5946
 
5573
5947
  case "fs_delete": {
5574
- const r = resolveInMount(args.path, args.mount);
5948
+ const r = await resolveInMount(args.path, args.mount, vault);
5575
5949
  if (r.error) return mcpError(r.error);
5576
5950
  if (!checkAccess(r.mount, "d")) return mcpError("Delete access denied on this mount");
5577
5951
  try {
5578
- await rm(r.resolved);
5952
+ const s = await stat(r.resolved);
5953
+ await rm(r.resolved, { recursive: s.isDirectory() });
5579
5954
  return mcpResult(`Deleted: ${args.path}`);
5580
5955
  } catch (err) {
5581
5956
  if (err.code === "ENOENT") return mcpError(`Not found: ${args.path}`);
@@ -5584,7 +5959,7 @@ async function handleMcpTool(vault, name, args) {
5584
5959
  }
5585
5960
 
5586
5961
  case "fs_mkdir": {
5587
- const r = resolveInMount(args.path, args.mount);
5962
+ const r = await resolveInMount(args.path, args.mount, vault);
5588
5963
  if (r.error) return mcpError(r.error);
5589
5964
  if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
5590
5965
  try {
@@ -5595,6 +5970,49 @@ async function handleMcpTool(vault, name, args) {
5595
5970
  }
5596
5971
  }
5597
5972
 
5973
+ case "fs_mounts": {
5974
+ const { mounts, error } = await getFileserverMounts(vault);
5975
+ if (error) return mcpError(error);
5976
+ if (!mounts || mounts.length === 0) return mcpResult("No fileserver mounts configured. Create one:\n1. Use clauth dashboard or clauth_enable to add a service with key_type='fileserver'\n2. Set the secret value to JSON: {\"path\": \"C:/Dev/regen-root\", \"access\": \"rwd\"}");
5977
+ return mcpResult(JSON.stringify(mounts, null, 2));
5978
+ }
5979
+
5980
+ case "monkey_dispatch": {
5981
+ const { prompt, job_id } = args;
5982
+ if (!prompt) return mcpError("prompt required");
5983
+ const result = spawnClaudeTask(prompt, job_id || "untracked");
5984
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
5985
+ return mcpResult(JSON.stringify(result));
5986
+ }
5987
+
5988
+ case "terminal_start": {
5989
+ const { name, knowledge_tier, context_md } = args;
5990
+ if (!name) return mcpError("name required");
5991
+ const result = startTerminalSession(name, knowledge_tier || 'db_only', context_md || null);
5992
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
5993
+ return mcpResult(JSON.stringify(result));
5994
+ }
5995
+
5996
+ case "terminal_send": {
5997
+ const { session_id, message } = args;
5998
+ if (!session_id || !message) return mcpError("session_id and message required");
5999
+ const result = sendTerminalMessage(session_id, message);
6000
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
6001
+ return mcpResult(JSON.stringify(result));
6002
+ }
6003
+
6004
+ case "terminal_list": {
6005
+ return mcpResult(JSON.stringify(listTerminalSessions()));
6006
+ }
6007
+
6008
+ case "terminal_stop": {
6009
+ const { session_id } = args;
6010
+ if (!session_id) return mcpError("session_id required");
6011
+ const result = stopTerminalSession(session_id);
6012
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
6013
+ return mcpResult(JSON.stringify(result));
6014
+ }
6015
+
5598
6016
  default:
5599
6017
  return mcpError(`Unknown tool: ${name}`);
5600
6018
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "1.5.38",
3
+ "version": "1.5.40",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {