@lifeaitools/clauth 1.5.38 → 1.5.39

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");
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,29 @@ 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
+
3448
3497
  // GET|POST /shutdown (for daemon stop — programmatic, keeps boot.key)
3449
3498
  // Accept POST as well — older scripts and curl default to POST
3450
3499
  if ((method === "GET" || method === "POST") && reqPath === "/shutdown") {
@@ -3576,11 +3625,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3576
3625
  const statusResult = await api.status(password, machineHash, token, timestamp);
3577
3626
  const existingTypes = [...new Set((statusResult.services || []).map(s => s.key_type).filter(Boolean))];
3578
3627
  // Merge with known types (in case no service of that type exists yet)
3579
- const knownTypes = ["token", "secret", "keypair", "connstring", "oauth"];
3628
+ const knownTypes = ["token", "secret", "keypair", "connstring", "oauth", "fileserver"];
3580
3629
  const allTypes = [...new Set([...knownTypes, ...existingTypes])];
3581
3630
  return ok(res, { key_types: allTypes });
3582
3631
  } catch (err) {
3583
- return ok(res, { key_types: ["token", "secret", "keypair", "connstring", "oauth"] });
3632
+ return ok(res, { key_types: ["token", "secret", "keypair", "connstring", "oauth", "fileserver"] });
3584
3633
  }
3585
3634
  }
3586
3635
 
@@ -4403,7 +4452,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
4403
4452
  res.writeHead(400, { "Content-Type": "application/json", ...CORS });
4404
4453
  return res.end(JSON.stringify({ error: "name is required" }));
4405
4454
  }
4406
- const validTypes = ["token", "keypair", "connstring", "oauth", "secret"];
4455
+ const validTypes = ["token", "keypair", "connstring", "oauth", "secret", "fileserver"];
4407
4456
  const type = (key_type || "token").toLowerCase();
4408
4457
  if (!validTypes.includes(type)) {
4409
4458
  res.writeHead(400, { "Content-Type": "application/json", ...CORS });
@@ -4770,6 +4819,53 @@ async function actionForeground(opts) {
4770
4819
  import { createInterface } from "readline";
4771
4820
  import { execSync, spawn as spawnProc } from "child_process";
4772
4821
 
4822
+ // ── Monkey dispatch — headless Claude CLI worker ─────────────────
4823
+ function findClaudeBinary() {
4824
+ const candidates = [
4825
+ process.env.CLAUDE_BIN,
4826
+ path.join(process.env.APPDATA || '', 'npm', 'claude.cmd'),
4827
+ path.join(process.env.APPDATA || '', 'npm', 'claude'),
4828
+ 'claude', // PATH fallback
4829
+ ].filter(Boolean);
4830
+
4831
+ for (const c of candidates) {
4832
+ try {
4833
+ execSync(`"${c}" --version`, { stdio: 'ignore', timeout: 3000 });
4834
+ return c;
4835
+ } catch {}
4836
+ }
4837
+ return null;
4838
+ }
4839
+
4840
+ let activeCliWorkers = 0;
4841
+ const MAX_CLI_WORKERS = 2;
4842
+
4843
+ function spawnClaudeTask(prompt, jobId) {
4844
+ if (activeCliWorkers >= MAX_CLI_WORKERS) {
4845
+ return { error: 'concurrency_limit', message: `Max ${MAX_CLI_WORKERS} CLI workers active` };
4846
+ }
4847
+ const binary = findClaudeBinary();
4848
+ if (!binary) {
4849
+ return { error: 'binary_not_found', message: 'claude CLI not found in PATH or AppData/npm' };
4850
+ }
4851
+
4852
+ activeCliWorkers++;
4853
+ const proc = spawnProc(binary, ['-p', prompt, '--dangerously-skip-permissions'], {
4854
+ cwd: 'C:/Dev/regen-root',
4855
+ env: process.env,
4856
+ stdio: ['ignore', 'pipe', 'pipe'],
4857
+ shell: true,
4858
+ });
4859
+
4860
+ const startedAt = Date.now();
4861
+ proc.on('close', (code) => {
4862
+ activeCliWorkers--;
4863
+ console.log(`[monkey] job ${jobId} exited code=${code} in ${Date.now() - startedAt}ms`);
4864
+ });
4865
+
4866
+ return { status: 'spawned', pid: proc.pid, jobId, activeWorkers: activeCliWorkers };
4867
+ }
4868
+
4773
4869
  const ENV_MAP = {
4774
4870
  "github": "GITHUB_TOKEN",
4775
4871
  "supabase-anon": "NEXT_PUBLIC_SUPABASE_ANON_KEY",
@@ -4787,14 +4883,48 @@ const ENV_MAP = {
4787
4883
  "gmail": "GMAIL_CREDENTIALS",
4788
4884
  };
4789
4885
 
4790
- // ── Filesystem service config ──
4791
- const FS_MOUNTS = [
4792
- { name: "regen-root", path: "C:/Dev/regen-root", access: "rwd" },
4793
- ];
4886
+ // ── Filesystem service config — loaded from clauth vault ──
4887
+ let _fsMountsCache = null;
4888
+ let _fsMountsCacheTime = 0;
4889
+ const FS_CACHE_TTL = 60000; // 1 minute
4890
+
4891
+ async function getFileserverMounts(vault) {
4892
+ if (!vault.password) return { error: "Vault is locked — unlock first" };
4893
+ const now = Date.now();
4894
+ if (_fsMountsCache && now - _fsMountsCacheTime < FS_CACHE_TTL) return { mounts: _fsMountsCache };
4895
+
4896
+ try {
4897
+ const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
4898
+ const result = await api.status(vault.password, vault.machineHash, token, timestamp);
4899
+ if (result.error) return { error: result.error };
4900
+ const mounts = [];
4901
+ for (const s of (result.services || [])) {
4902
+ if (s.key_type === "fileserver" && s.enabled) {
4903
+ try {
4904
+ const { token: t2, timestamp: ts2 } = deriveToken(vault.password, vault.machineHash);
4905
+ const secret = await api.retrieve(vault.password, vault.machineHash, t2, ts2, s.name);
4906
+ if (secret.value) {
4907
+ const config = JSON.parse(secret.value);
4908
+ mounts.push({ name: s.name, path: config.path, access: config.access || "r" });
4909
+ }
4910
+ } catch {}
4911
+ }
4912
+ }
4913
+ _fsMountsCache = mounts;
4914
+ _fsMountsCacheTime = now;
4915
+ return { mounts };
4916
+ } catch (err) {
4917
+ return { error: `Mount lookup failed: ${err.message}` };
4918
+ }
4919
+ }
4794
4920
 
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" };
4921
+ async function resolveInMount(requestedPath, mountName, vault) {
4922
+ const { mounts, error } = await getFileserverMounts(vault);
4923
+ if (error) return { error };
4924
+ 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\"}" };
4925
+ const mount = mountName ? mounts.find(m => m.name === mountName) : mounts[0];
4926
+ if (!mount) return { error: `Mount '${mountName}' not found. Available: ${mounts.map(m => m.name).join(", ")}` };
4927
+ if (!mount.path) return { error: `Fileserver '${mount.name}' has no path configured` };
4798
4928
  const resolved = path.resolve(mount.path, requestedPath);
4799
4929
  const normalized = path.normalize(resolved);
4800
4930
  if (!normalized.startsWith(path.normalize(mount.path))) {
@@ -4902,6 +5032,19 @@ const MCP_TOOLS = [
4902
5032
  description: "Test whether the clauth MCP connector is reachable via the Cloudflare tunnel. Returns connectivity status and tunnel URL.",
4903
5033
  inputSchema: { type: "object", properties: {}, additionalProperties: false }
4904
5034
  },
5035
+ {
5036
+ name: "monkey_dispatch",
5037
+ 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.",
5038
+ inputSchema: {
5039
+ type: "object",
5040
+ properties: {
5041
+ prompt: { type: "string", description: "Full prompt for the CLI worker to execute" },
5042
+ job_id: { type: "string", description: "monkey_jobs UUID for tracking" },
5043
+ },
5044
+ required: ["prompt"],
5045
+ additionalProperties: false,
5046
+ },
5047
+ },
4905
5048
 
4906
5049
  // ── Google Workspace (gws CLI) ──────────────────────────────────────────
4907
5050
  {
@@ -5083,6 +5226,11 @@ const MCP_TOOLS = [
5083
5226
  additionalProperties: false,
5084
5227
  },
5085
5228
  },
5229
+ {
5230
+ name: "fs_mounts",
5231
+ description: "List configured filesystem mounts (fileserver services from vault).",
5232
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
5233
+ },
5086
5234
  ];
5087
5235
 
5088
5236
  function writeTempSecret(service, value) {
@@ -5111,6 +5259,9 @@ function mcpError(text) {
5111
5259
  return { content: [{ type: "text", text }], isError: true };
5112
5260
  }
5113
5261
 
5262
+ // Windows cmd.exe doesn't support single quotes — use bash for gws JSON args
5263
+ const GWS_EXEC_OPTS = { encoding: "utf8", timeout: 30000, windowsHide: true, shell: os.platform() === "win32" ? "bash" : undefined };
5264
+
5114
5265
  async function handleMcpTool(vault, name, args) {
5115
5266
  switch (name) {
5116
5267
  case "clauth_ping": {
@@ -5384,7 +5535,7 @@ async function handleMcpTool(vault, name, args) {
5384
5535
  if (params) cmdArgs.push("--params", `'${JSON.stringify(params)}'`);
5385
5536
  if (body) cmdArgs.push("--json", `'${JSON.stringify(body)}'`);
5386
5537
  try {
5387
- const raw = execSyncTop(["gws", ...cmdArgs].join(" "), { encoding: "utf8", timeout: 30000, windowsHide: true });
5538
+ const raw = execSyncTop(["gws", ...cmdArgs].join(" "), GWS_EXEC_OPTS);
5388
5539
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5389
5540
  } catch (err) {
5390
5541
  return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`);
@@ -5395,7 +5546,7 @@ async function handleMcpTool(vault, name, args) {
5395
5546
  const p = { userId: "me", maxResults: args.max_results ?? 10 };
5396
5547
  if (args.query) p.q = args.query;
5397
5548
  try {
5398
- const raw = execSyncTop(`gws gmail users messages list --params '${JSON.stringify(p)}'`, { encoding: "utf8", timeout: 30000, windowsHide: true });
5549
+ const raw = execSyncTop(`gws gmail users messages list --params '${JSON.stringify(p)}'`, GWS_EXEC_OPTS);
5399
5550
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5400
5551
  } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5401
5552
  }
@@ -5403,7 +5554,7 @@ async function handleMcpTool(vault, name, args) {
5403
5554
  case "gws_gmail_read": {
5404
5555
  const p = { userId: "me", id: args.message_id, format: "full" };
5405
5556
  try {
5406
- const raw = execSyncTop(`gws gmail users messages get --params '${JSON.stringify(p)}'`, { encoding: "utf8", timeout: 30000, windowsHide: true });
5557
+ const raw = execSyncTop(`gws gmail users messages get --params '${JSON.stringify(p)}'`, GWS_EXEC_OPTS);
5407
5558
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5408
5559
  } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5409
5560
  }
@@ -5415,7 +5566,7 @@ async function handleMcpTool(vault, name, args) {
5415
5566
  const encoded = Buffer.from(lines.join("\r\n")).toString("base64url");
5416
5567
  const bodyObj = { userId: "me", resource: { raw: encoded } };
5417
5568
  try {
5418
- const raw = execSyncTop(`gws gmail users messages send --json '${JSON.stringify(bodyObj)}'`, { encoding: "utf8", timeout: 30000, windowsHide: true });
5569
+ const raw = execSyncTop(`gws gmail users messages send --json '${JSON.stringify(bodyObj)}'`, GWS_EXEC_OPTS);
5419
5570
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5420
5571
  } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5421
5572
  }
@@ -5425,7 +5576,7 @@ async function handleMcpTool(vault, name, args) {
5425
5576
  if (args.time_min) p.timeMin = args.time_min;
5426
5577
  if (args.time_max) p.timeMax = args.time_max;
5427
5578
  try {
5428
- const raw = execSyncTop(`gws calendar events list --params '${JSON.stringify(p)}'`, { encoding: "utf8", timeout: 30000, windowsHide: true });
5579
+ const raw = execSyncTop(`gws calendar events list --params '${JSON.stringify(p)}'`, GWS_EXEC_OPTS);
5429
5580
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5430
5581
  } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5431
5582
  }
@@ -5434,7 +5585,7 @@ async function handleMcpTool(vault, name, args) {
5434
5585
  const p = { pageSize: args.max_results ?? 10, fields: "files(id,name,mimeType,modifiedTime,size,webViewLink)" };
5435
5586
  if (args.query) p.q = args.query;
5436
5587
  try {
5437
- const raw = execSyncTop(`gws drive files list --params '${JSON.stringify(p)}'`, { encoding: "utf8", timeout: 30000, windowsHide: true });
5588
+ const raw = execSyncTop(`gws drive files list --params '${JSON.stringify(p)}'`, GWS_EXEC_OPTS);
5438
5589
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5439
5590
  } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5440
5591
  }
@@ -5442,7 +5593,7 @@ async function handleMcpTool(vault, name, args) {
5442
5593
  // ── Filesystem tools ──────────────────────────────────────
5443
5594
 
5444
5595
  case "fs_read": {
5445
- const r = resolveInMount(args.path, args.mount);
5596
+ const r = await resolveInMount(args.path, args.mount, vault);
5446
5597
  if (r.error) return mcpError(r.error);
5447
5598
  if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
5448
5599
  try {
@@ -5461,7 +5612,7 @@ async function handleMcpTool(vault, name, args) {
5461
5612
  }
5462
5613
 
5463
5614
  case "fs_write": {
5464
- const r = resolveInMount(args.path, args.mount);
5615
+ const r = await resolveInMount(args.path, args.mount, vault);
5465
5616
  if (r.error) return mcpError(r.error);
5466
5617
  if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
5467
5618
  try {
@@ -5475,7 +5626,7 @@ async function handleMcpTool(vault, name, args) {
5475
5626
 
5476
5627
  case "fs_list": {
5477
5628
  const dirPath = args.path || ".";
5478
- const r = resolveInMount(dirPath, args.mount);
5629
+ const r = await resolveInMount(dirPath, args.mount, vault);
5479
5630
  if (r.error) return mcpError(r.error);
5480
5631
  if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
5481
5632
  try {
@@ -5503,7 +5654,7 @@ async function handleMcpTool(vault, name, args) {
5503
5654
 
5504
5655
  case "fs_grep": {
5505
5656
  const searchPath = args.path || ".";
5506
- const r = resolveInMount(searchPath, args.mount);
5657
+ const r = await resolveInMount(searchPath, args.mount, vault);
5507
5658
  if (r.error) return mcpError(r.error);
5508
5659
  if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
5509
5660
 
@@ -5553,7 +5704,7 @@ async function handleMcpTool(vault, name, args) {
5553
5704
 
5554
5705
  case "fs_glob": {
5555
5706
  const basePath = args.path || ".";
5556
- const r = resolveInMount(basePath, args.mount);
5707
+ const r = await resolveInMount(basePath, args.mount, vault);
5557
5708
  if (r.error) return mcpError(r.error);
5558
5709
  if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
5559
5710
  try {
@@ -5571,11 +5722,12 @@ async function handleMcpTool(vault, name, args) {
5571
5722
  }
5572
5723
 
5573
5724
  case "fs_delete": {
5574
- const r = resolveInMount(args.path, args.mount);
5725
+ const r = await resolveInMount(args.path, args.mount, vault);
5575
5726
  if (r.error) return mcpError(r.error);
5576
5727
  if (!checkAccess(r.mount, "d")) return mcpError("Delete access denied on this mount");
5577
5728
  try {
5578
- await rm(r.resolved);
5729
+ const s = await stat(r.resolved);
5730
+ await rm(r.resolved, { recursive: s.isDirectory() });
5579
5731
  return mcpResult(`Deleted: ${args.path}`);
5580
5732
  } catch (err) {
5581
5733
  if (err.code === "ENOENT") return mcpError(`Not found: ${args.path}`);
@@ -5584,7 +5736,7 @@ async function handleMcpTool(vault, name, args) {
5584
5736
  }
5585
5737
 
5586
5738
  case "fs_mkdir": {
5587
- const r = resolveInMount(args.path, args.mount);
5739
+ const r = await resolveInMount(args.path, args.mount, vault);
5588
5740
  if (r.error) return mcpError(r.error);
5589
5741
  if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
5590
5742
  try {
@@ -5595,6 +5747,21 @@ async function handleMcpTool(vault, name, args) {
5595
5747
  }
5596
5748
  }
5597
5749
 
5750
+ case "fs_mounts": {
5751
+ const { mounts, error } = await getFileserverMounts(vault);
5752
+ if (error) return mcpError(error);
5753
+ 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\"}");
5754
+ return mcpResult(JSON.stringify(mounts, null, 2));
5755
+ }
5756
+
5757
+ case "monkey_dispatch": {
5758
+ const { prompt, job_id } = args;
5759
+ if (!prompt) return mcpError("prompt required");
5760
+ const result = spawnClaudeTask(prompt, job_id || "untracked");
5761
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
5762
+ return mcpResult(JSON.stringify(result));
5763
+ }
5764
+
5598
5765
  default:
5599
5766
  return mcpError(`Unknown tool: ${name}`);
5600
5767
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "1.5.38",
3
+ "version": "1.5.39",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {