@lifeaitools/clauth 1.5.37 → 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
@@ -17,6 +17,9 @@ import ora from "ora";
17
17
  import { execSync as execSyncTop } from "child_process";
18
18
  import Conf from "conf";
19
19
  import { getConfOptions } from "../conf-path.js";
20
+ import { readdir, readFile, writeFile, rm, mkdir, stat, rename } from "node:fs/promises";
21
+ import fg from "fast-glob";
22
+ import { rgPath } from "@vscode/ripgrep";
20
23
 
21
24
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
25
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"), "utf8"));
@@ -72,9 +75,17 @@ CREATE TABLE IF NOT EXISTS clauth_rotation_log (
72
75
  );
73
76
  ALTER TABLE clauth_rotation_log ENABLE ROW LEVEL SECURITY;`,
74
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
+ },
75
86
  ];
76
87
 
77
- const CURRENT_SCHEMA_VERSION = 4;
88
+ const CURRENT_SCHEMA_VERSION = 5;
78
89
 
79
90
  // ── Key Rotation Config ─────────────────────────────────────────
80
91
  // Per-service rotation capabilities. "auto" services can be rotated programmatically.
@@ -1751,6 +1762,7 @@ const TYPE_LABELS = {
1751
1762
  keypair: "keypair (user:key pair)",
1752
1763
  connstring: "connstring (connection string)",
1753
1764
  oauth: "oauth (OAuth credentials)",
1765
+ fileserver: "fileserver (mount config)",
1754
1766
  };
1755
1767
 
1756
1768
  async function toggleAddService() {
@@ -1768,12 +1780,12 @@ async function toggleAddService() {
1768
1780
  sel.innerHTML = '<option value="">Loading…</option>';
1769
1781
  try {
1770
1782
  const r = await fetch(BASE + "/meta").then(r => r.json());
1771
- const types = r.key_types || ["token", "secret", "keypair", "connstring", "oauth"];
1783
+ const types = r.key_types || ["token", "secret", "keypair", "connstring", "oauth", "fileserver"];
1772
1784
  sel.innerHTML = types.map(t =>
1773
1785
  \`<option value="\${t}">\${TYPE_LABELS[t] || t}</option>\`
1774
1786
  ).join("");
1775
1787
  } catch {
1776
- sel.innerHTML = ["token","secret","keypair","connstring","oauth"].map(t =>
1788
+ sel.innerHTML = ["token","secret","keypair","connstring","oauth","fileserver"].map(t =>
1777
1789
  \`<option value="\${t}">\${TYPE_LABELS[t] || t}</option>\`
1778
1790
  ).join("");
1779
1791
  }
@@ -2908,12 +2920,23 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2908
2920
  return res.end();
2909
2921
  }
2910
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
+
2911
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
+
2912
2935
  // Restore full well-known + OAuth so custom setup with client_id/secret works.
2913
2936
  if (reqPath.startsWith("/.well-known/oauth-protected-resource")) {
2914
2937
  const base = oauthBase();
2915
2938
  const suffix = reqPath.replace("/.well-known/oauth-protected-resource", "").replace(/^\//, "");
2916
- const resourcePath = suffix && ["/gws", "/clauth", "/mcp", "/sse"].includes("/" + suffix) ? "/" + suffix : "/sse";
2939
+ const resourcePath = suffix && ["/gws", "/clauth", "/mcp", "/fs", "/sse"].includes("/" + suffix) ? "/" + suffix : "/sse";
2917
2940
  res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store", ...CORS });
2918
2941
  return res.end(JSON.stringify({
2919
2942
  resource: `${base}${resourcePath}`,
@@ -2940,6 +2963,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2940
2963
  }
2941
2964
 
2942
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
+ }
2943
2971
  if (method === "POST" && reqPath === "/register") {
2944
2972
  let body;
2945
2973
  try { body = await readBody(req); } catch {
@@ -3087,25 +3115,28 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3087
3115
  }
3088
3116
 
3089
3117
  // ── MCP path helpers ──
3090
- const MCP_PATHS = ["/mcp", "/gws", "/clauth"];
3118
+ const MCP_PATHS = ["/mcp", "/gws", "/clauth", "/fs"];
3091
3119
  const isMcpPath = MCP_PATHS.includes(reqPath);
3092
3120
  function toolsForPath(p) {
3093
3121
  if (p === "/gws") return MCP_TOOLS.filter(t => t.name.startsWith("gws_"));
3094
- 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");
3123
+ if (p === "/fs") return MCP_TOOLS.filter(t => t.name.startsWith("fs_"));
3095
3124
  return MCP_TOOLS; // /mcp — all tools
3096
3125
  }
3097
3126
  function serverNameForPath(p) {
3098
3127
  if (p === "/gws") return "gws";
3099
3128
  if (p === "/clauth") return "clauth";
3129
+ if (p === "/fs") return "fs";
3100
3130
  return "clauth";
3101
3131
  }
3102
3132
 
3103
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)
3104
3135
  if (method === "POST" && (reqPath === "/sse" || isMcpPath)) {
3105
3136
  const authHeader = req.headers.authorization;
3106
3137
  const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
3107
3138
 
3108
- if (!token || !oauthTokens.has(token)) {
3139
+ if (!noAuthHost && (!token || !oauthTokens.has(token))) {
3109
3140
  // No valid Bearer token → return 401 with discovery hint
3110
3141
  const base = oauthBase();
3111
3142
  const resourcePath = isMcpPath ? reqPath.slice(1) : "sse"; // "mcp", "gws", "clauth", or "sse"
@@ -3440,6 +3471,29 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3440
3471
  return;
3441
3472
  }
3442
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
+
3443
3497
  // GET|POST /shutdown (for daemon stop — programmatic, keeps boot.key)
3444
3498
  // Accept POST as well — older scripts and curl default to POST
3445
3499
  if ((method === "GET" || method === "POST") && reqPath === "/shutdown") {
@@ -3571,11 +3625,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3571
3625
  const statusResult = await api.status(password, machineHash, token, timestamp);
3572
3626
  const existingTypes = [...new Set((statusResult.services || []).map(s => s.key_type).filter(Boolean))];
3573
3627
  // Merge with known types (in case no service of that type exists yet)
3574
- const knownTypes = ["token", "secret", "keypair", "connstring", "oauth"];
3628
+ const knownTypes = ["token", "secret", "keypair", "connstring", "oauth", "fileserver"];
3575
3629
  const allTypes = [...new Set([...knownTypes, ...existingTypes])];
3576
3630
  return ok(res, { key_types: allTypes });
3577
3631
  } catch (err) {
3578
- return ok(res, { key_types: ["token", "secret", "keypair", "connstring", "oauth"] });
3632
+ return ok(res, { key_types: ["token", "secret", "keypair", "connstring", "oauth", "fileserver"] });
3579
3633
  }
3580
3634
  }
3581
3635
 
@@ -4398,7 +4452,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
4398
4452
  res.writeHead(400, { "Content-Type": "application/json", ...CORS });
4399
4453
  return res.end(JSON.stringify({ error: "name is required" }));
4400
4454
  }
4401
- const validTypes = ["token", "keypair", "connstring", "oauth", "secret"];
4455
+ const validTypes = ["token", "keypair", "connstring", "oauth", "secret", "fileserver"];
4402
4456
  const type = (key_type || "token").toLowerCase();
4403
4457
  if (!validTypes.includes(type)) {
4404
4458
  res.writeHead(400, { "Content-Type": "application/json", ...CORS });
@@ -4765,6 +4819,53 @@ async function actionForeground(opts) {
4765
4819
  import { createInterface } from "readline";
4766
4820
  import { execSync, spawn as spawnProc } from "child_process";
4767
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
+
4768
4869
  const ENV_MAP = {
4769
4870
  "github": "GITHUB_TOKEN",
4770
4871
  "supabase-anon": "NEXT_PUBLIC_SUPABASE_ANON_KEY",
@@ -4782,6 +4883,60 @@ const ENV_MAP = {
4782
4883
  "gmail": "GMAIL_CREDENTIALS",
4783
4884
  };
4784
4885
 
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
+ }
4920
+
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` };
4928
+ const resolved = path.resolve(mount.path, requestedPath);
4929
+ const normalized = path.normalize(resolved);
4930
+ if (!normalized.startsWith(path.normalize(mount.path))) {
4931
+ return { error: `Path escapes mount: ${requestedPath}` };
4932
+ }
4933
+ return { resolved: normalized, mount };
4934
+ }
4935
+
4936
+ function checkAccess(mount, flag) {
4937
+ return mount.access.includes(flag);
4938
+ }
4939
+
4785
4940
  const MCP_TOOLS = [
4786
4941
  {
4787
4942
  name: "clauth_ping",
@@ -4877,6 +5032,19 @@ const MCP_TOOLS = [
4877
5032
  description: "Test whether the clauth MCP connector is reachable via the Cloudflare tunnel. Returns connectivity status and tunnel URL.",
4878
5033
  inputSchema: { type: "object", properties: {}, additionalProperties: false }
4879
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
+ },
4880
5048
 
4881
5049
  // ── Google Workspace (gws CLI) ──────────────────────────────────────────
4882
5050
  {
@@ -4959,6 +5127,110 @@ const MCP_TOOLS = [
4959
5127
  additionalProperties: false
4960
5128
  }
4961
5129
  },
5130
+ // ── Filesystem tools ──
5131
+ {
5132
+ name: "fs_read",
5133
+ description: "Read a file. Returns UTF-8 text with line numbers. Supports offset/limit for large files.",
5134
+ inputSchema: {
5135
+ type: "object",
5136
+ properties: {
5137
+ path: { type: "string", description: "Relative path within mount (e.g. 'packages/ui/src/index.ts')" },
5138
+ mount: { type: "string", description: "Mount name (default: first mount)" },
5139
+ offset: { type: "number", description: "Start line (0-based, default 0)" },
5140
+ limit: { type: "number", description: "Max lines to return (default 500)" },
5141
+ },
5142
+ required: ["path"],
5143
+ additionalProperties: false,
5144
+ },
5145
+ },
5146
+ {
5147
+ name: "fs_write",
5148
+ description: "Write content to a file. Creates parent directories if needed. Overwrites existing file.",
5149
+ inputSchema: {
5150
+ type: "object",
5151
+ properties: {
5152
+ path: { type: "string", description: "Relative path within mount" },
5153
+ content: { type: "string", description: "File content to write" },
5154
+ mount: { type: "string", description: "Mount name (default: first mount)" },
5155
+ },
5156
+ required: ["path", "content"],
5157
+ additionalProperties: false,
5158
+ },
5159
+ },
5160
+ {
5161
+ name: "fs_list",
5162
+ description: "List directory contents with file type, size, and modification time.",
5163
+ inputSchema: {
5164
+ type: "object",
5165
+ properties: {
5166
+ path: { type: "string", description: "Relative directory path (default: mount root)" },
5167
+ mount: { type: "string", description: "Mount name (default: first mount)" },
5168
+ },
5169
+ additionalProperties: false,
5170
+ },
5171
+ },
5172
+ {
5173
+ name: "fs_grep",
5174
+ description: "Search file contents using ripgrep. Returns matching lines with context. Spawns rg process.",
5175
+ inputSchema: {
5176
+ type: "object",
5177
+ properties: {
5178
+ pattern: { type: "string", description: "Regex pattern to search for" },
5179
+ path: { type: "string", description: "Relative path to search in (default: mount root)" },
5180
+ glob: { type: "string", description: "File glob filter (e.g. '*.ts', '*.{js,tsx}')" },
5181
+ context: { type: "number", description: "Lines of context around matches (default 0)" },
5182
+ max_results: { type: "number", description: "Max matches (default 50)" },
5183
+ mount: { type: "string", description: "Mount name (default: first mount)" },
5184
+ },
5185
+ required: ["pattern"],
5186
+ additionalProperties: false,
5187
+ },
5188
+ },
5189
+ {
5190
+ name: "fs_glob",
5191
+ description: "Find files by glob pattern. Returns matching file paths relative to mount.",
5192
+ inputSchema: {
5193
+ type: "object",
5194
+ properties: {
5195
+ pattern: { type: "string", description: "Glob pattern (e.g. '**/*.tsx', 'apps/*/package.json')" },
5196
+ path: { type: "string", description: "Relative base path (default: mount root)" },
5197
+ mount: { type: "string", description: "Mount name (default: first mount)" },
5198
+ },
5199
+ required: ["pattern"],
5200
+ additionalProperties: false,
5201
+ },
5202
+ },
5203
+ {
5204
+ name: "fs_delete",
5205
+ description: "Delete a file or empty directory.",
5206
+ inputSchema: {
5207
+ type: "object",
5208
+ properties: {
5209
+ path: { type: "string", description: "Relative path to delete" },
5210
+ mount: { type: "string", description: "Mount name (default: first mount)" },
5211
+ },
5212
+ required: ["path"],
5213
+ additionalProperties: false,
5214
+ },
5215
+ },
5216
+ {
5217
+ name: "fs_mkdir",
5218
+ description: "Create a directory (recursive — creates parents if needed).",
5219
+ inputSchema: {
5220
+ type: "object",
5221
+ properties: {
5222
+ path: { type: "string", description: "Relative directory path to create" },
5223
+ mount: { type: "string", description: "Mount name (default: first mount)" },
5224
+ },
5225
+ required: ["path"],
5226
+ additionalProperties: false,
5227
+ },
5228
+ },
5229
+ {
5230
+ name: "fs_mounts",
5231
+ description: "List configured filesystem mounts (fileserver services from vault).",
5232
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
5233
+ },
4962
5234
  ];
4963
5235
 
4964
5236
  function writeTempSecret(service, value) {
@@ -4987,6 +5259,9 @@ function mcpError(text) {
4987
5259
  return { content: [{ type: "text", text }], isError: true };
4988
5260
  }
4989
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
+
4990
5265
  async function handleMcpTool(vault, name, args) {
4991
5266
  switch (name) {
4992
5267
  case "clauth_ping": {
@@ -5260,7 +5535,7 @@ async function handleMcpTool(vault, name, args) {
5260
5535
  if (params) cmdArgs.push("--params", `'${JSON.stringify(params)}'`);
5261
5536
  if (body) cmdArgs.push("--json", `'${JSON.stringify(body)}'`);
5262
5537
  try {
5263
- const raw = execSyncTop(["gws", ...cmdArgs].join(" "), { encoding: "utf8", timeout: 30000, windowsHide: true });
5538
+ const raw = execSyncTop(["gws", ...cmdArgs].join(" "), GWS_EXEC_OPTS);
5264
5539
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5265
5540
  } catch (err) {
5266
5541
  return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`);
@@ -5271,7 +5546,7 @@ async function handleMcpTool(vault, name, args) {
5271
5546
  const p = { userId: "me", maxResults: args.max_results ?? 10 };
5272
5547
  if (args.query) p.q = args.query;
5273
5548
  try {
5274
- 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);
5275
5550
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5276
5551
  } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5277
5552
  }
@@ -5279,7 +5554,7 @@ async function handleMcpTool(vault, name, args) {
5279
5554
  case "gws_gmail_read": {
5280
5555
  const p = { userId: "me", id: args.message_id, format: "full" };
5281
5556
  try {
5282
- 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);
5283
5558
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5284
5559
  } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5285
5560
  }
@@ -5291,7 +5566,7 @@ async function handleMcpTool(vault, name, args) {
5291
5566
  const encoded = Buffer.from(lines.join("\r\n")).toString("base64url");
5292
5567
  const bodyObj = { userId: "me", resource: { raw: encoded } };
5293
5568
  try {
5294
- 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);
5295
5570
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5296
5571
  } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5297
5572
  }
@@ -5301,7 +5576,7 @@ async function handleMcpTool(vault, name, args) {
5301
5576
  if (args.time_min) p.timeMin = args.time_min;
5302
5577
  if (args.time_max) p.timeMax = args.time_max;
5303
5578
  try {
5304
- 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);
5305
5580
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5306
5581
  } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5307
5582
  }
@@ -5310,11 +5585,183 @@ async function handleMcpTool(vault, name, args) {
5310
5585
  const p = { pageSize: args.max_results ?? 10, fields: "files(id,name,mimeType,modifiedTime,size,webViewLink)" };
5311
5586
  if (args.query) p.q = args.query;
5312
5587
  try {
5313
- 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);
5314
5589
  try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5315
5590
  } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5316
5591
  }
5317
5592
 
5593
+ // ── Filesystem tools ──────────────────────────────────────
5594
+
5595
+ case "fs_read": {
5596
+ const r = await resolveInMount(args.path, args.mount, vault);
5597
+ if (r.error) return mcpError(r.error);
5598
+ if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
5599
+ try {
5600
+ const content = await readFile(r.resolved, "utf8");
5601
+ const lines = content.split("\n");
5602
+ const offset = args.offset || 0;
5603
+ const limit = args.limit || 500;
5604
+ const slice = lines.slice(offset, offset + limit);
5605
+ const numbered = slice.map((line, i) => `${offset + i + 1}\t${line}`).join("\n");
5606
+ const header = `${r.resolved} (${lines.length} lines${offset > 0 ? `, showing ${offset + 1}-${Math.min(offset + limit, lines.length)}` : ""})`;
5607
+ return mcpResult(`${header}\n${numbered}`);
5608
+ } catch (err) {
5609
+ if (err.code === "ENOENT") return mcpError(`File not found: ${args.path}`);
5610
+ return mcpError(`Read failed: ${err.message}`);
5611
+ }
5612
+ }
5613
+
5614
+ case "fs_write": {
5615
+ const r = await resolveInMount(args.path, args.mount, vault);
5616
+ if (r.error) return mcpError(r.error);
5617
+ if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
5618
+ try {
5619
+ await mkdir(path.dirname(r.resolved), { recursive: true });
5620
+ await writeFile(r.resolved, args.content, "utf8");
5621
+ return mcpResult(`Written: ${args.path} (${Buffer.byteLength(args.content)} bytes)`);
5622
+ } catch (err) {
5623
+ return mcpError(`Write failed: ${err.message}`);
5624
+ }
5625
+ }
5626
+
5627
+ case "fs_list": {
5628
+ const dirPath = args.path || ".";
5629
+ const r = await resolveInMount(dirPath, args.mount, vault);
5630
+ if (r.error) return mcpError(r.error);
5631
+ if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
5632
+ try {
5633
+ const entries = await readdir(r.resolved, { withFileTypes: true });
5634
+ const results = [];
5635
+ for (const entry of entries) {
5636
+ try {
5637
+ const s = await stat(path.join(r.resolved, entry.name));
5638
+ results.push({
5639
+ name: entry.name,
5640
+ type: entry.isDirectory() ? "dir" : "file",
5641
+ size: s.size,
5642
+ modified: s.mtime.toISOString(),
5643
+ });
5644
+ } catch {
5645
+ results.push({ name: entry.name, type: entry.isDirectory() ? "dir" : "file" });
5646
+ }
5647
+ }
5648
+ return mcpResult(JSON.stringify(results, null, 2));
5649
+ } catch (err) {
5650
+ if (err.code === "ENOENT") return mcpError(`Directory not found: ${dirPath}`);
5651
+ return mcpError(`List failed: ${err.message}`);
5652
+ }
5653
+ }
5654
+
5655
+ case "fs_grep": {
5656
+ const searchPath = args.path || ".";
5657
+ const r = await resolveInMount(searchPath, args.mount, vault);
5658
+ if (r.error) return mcpError(r.error);
5659
+ if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
5660
+
5661
+ const maxResults = args.max_results || 50;
5662
+ const rgArgs = [
5663
+ "--no-heading", "--line-number", "--color", "never",
5664
+ "--max-count", String(maxResults),
5665
+ ];
5666
+ if (args.context) rgArgs.push("-C", String(args.context));
5667
+ if (args.glob) rgArgs.push("--glob", args.glob);
5668
+ rgArgs.push(args.pattern, r.resolved);
5669
+
5670
+ return new Promise((resolve) => {
5671
+ let output = "";
5672
+ let killed = false;
5673
+ const proc = spawnProc(rgPath, rgArgs, { timeout: 15000, windowsHide: true });
5674
+
5675
+ proc.stdout.on("data", (chunk) => {
5676
+ output += chunk.toString();
5677
+ if (output.length > 65536) { // 64KB cap
5678
+ killed = true;
5679
+ proc.kill();
5680
+ }
5681
+ });
5682
+ proc.stderr.on("data", () => {}); // ignore stderr
5683
+
5684
+ proc.on("close", (code) => {
5685
+ if (killed) {
5686
+ resolve(mcpResult(output.slice(0, 65536) + "\n... (output truncated at 64KB)"));
5687
+ } else if (code === 1) {
5688
+ resolve(mcpResult("No matches found"));
5689
+ } else if (output) {
5690
+ // Make paths relative to mount
5691
+ const mountNorm = r.mount.path.replace(/\\/g, "/");
5692
+ const cleaned = output.replace(new RegExp(mountNorm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + "/?", "g"), "");
5693
+ resolve(mcpResult(cleaned));
5694
+ } else {
5695
+ resolve(mcpResult("No matches found"));
5696
+ }
5697
+ });
5698
+
5699
+ proc.on("error", (err) => {
5700
+ resolve(mcpError(`Grep failed: ${err.message}`));
5701
+ });
5702
+ });
5703
+ }
5704
+
5705
+ case "fs_glob": {
5706
+ const basePath = args.path || ".";
5707
+ const r = await resolveInMount(basePath, args.mount, vault);
5708
+ if (r.error) return mcpError(r.error);
5709
+ if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
5710
+ try {
5711
+ const matches = await fg(args.pattern, {
5712
+ cwd: r.resolved,
5713
+ dot: false,
5714
+ onlyFiles: true,
5715
+ ignore: ["**/node_modules/**", "**/.git/**"],
5716
+ });
5717
+ if (matches.length === 0) return mcpResult("No files matched");
5718
+ return mcpResult(matches.sort().join("\n"));
5719
+ } catch (err) {
5720
+ return mcpError(`Glob failed: ${err.message}`);
5721
+ }
5722
+ }
5723
+
5724
+ case "fs_delete": {
5725
+ const r = await resolveInMount(args.path, args.mount, vault);
5726
+ if (r.error) return mcpError(r.error);
5727
+ if (!checkAccess(r.mount, "d")) return mcpError("Delete access denied on this mount");
5728
+ try {
5729
+ const s = await stat(r.resolved);
5730
+ await rm(r.resolved, { recursive: s.isDirectory() });
5731
+ return mcpResult(`Deleted: ${args.path}`);
5732
+ } catch (err) {
5733
+ if (err.code === "ENOENT") return mcpError(`Not found: ${args.path}`);
5734
+ return mcpError(`Delete failed: ${err.message}`);
5735
+ }
5736
+ }
5737
+
5738
+ case "fs_mkdir": {
5739
+ const r = await resolveInMount(args.path, args.mount, vault);
5740
+ if (r.error) return mcpError(r.error);
5741
+ if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
5742
+ try {
5743
+ await mkdir(r.resolved, { recursive: true });
5744
+ return mcpResult(`Created: ${args.path}`);
5745
+ } catch (err) {
5746
+ return mcpError(`Mkdir failed: ${err.message}`);
5747
+ }
5748
+ }
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
+
5318
5765
  default:
5319
5766
  return mcpError(`Unknown tool: ${name}`);
5320
5767
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "1.5.37",
3
+ "version": "1.5.39",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,9 @@
19
19
  "conf": "^13.0.0",
20
20
  "inquirer": "^10.1.0",
21
21
  "node-fetch": "^3.3.2",
22
- "ora": "^8.1.0"
22
+ "ora": "^8.1.0",
23
+ "@vscode/ripgrep": "^1.15.9",
24
+ "fast-glob": "^3.3.2"
23
25
  },
24
26
  "engines": {
25
27
  "node": ">=18.0.0"