@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.
- package/.clauth-skill/SKILL.md +32 -0
- package/README.md +88 -2
- package/cli/commands/serve.js +463 -16
- package/package.json +4 -2
package/.clauth-skill/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
package/cli/commands/serve.js
CHANGED
|
@@ -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 =
|
|
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(" "),
|
|
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)}'`,
|
|
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)}'`,
|
|
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)}'`,
|
|
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)}'`,
|
|
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)}'`,
|
|
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.
|
|
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"
|