@lifeaitools/clauth 1.5.38 → 1.5.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.clauth-skill/SKILL.md +32 -0
- package/README.md +88 -2
- package/cli/commands/serve.js +447 -29
- package/package.json +1 -1
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
|
@@ -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 =
|
|
88
|
+
const CURRENT_SCHEMA_VERSION = 5;
|
|
81
89
|
|
|
82
90
|
// ── Key Rotation Config ─────────────────────────────────────────
|
|
83
91
|
// Per-service rotation capabilities. "auto" services can be rotated programmatically.
|
|
@@ -1754,6 +1762,7 @@ const TYPE_LABELS = {
|
|
|
1754
1762
|
keypair: "keypair (user:key pair)",
|
|
1755
1763
|
connstring: "connstring (connection string)",
|
|
1756
1764
|
oauth: "oauth (OAuth credentials)",
|
|
1765
|
+
fileserver: "fileserver (mount config)",
|
|
1757
1766
|
};
|
|
1758
1767
|
|
|
1759
1768
|
async function toggleAddService() {
|
|
@@ -1771,12 +1780,12 @@ async function toggleAddService() {
|
|
|
1771
1780
|
sel.innerHTML = '<option value="">Loading…</option>';
|
|
1772
1781
|
try {
|
|
1773
1782
|
const r = await fetch(BASE + "/meta").then(r => r.json());
|
|
1774
|
-
const types = r.key_types || ["token", "secret", "keypair", "connstring", "oauth"];
|
|
1783
|
+
const types = r.key_types || ["token", "secret", "keypair", "connstring", "oauth", "fileserver"];
|
|
1775
1784
|
sel.innerHTML = types.map(t =>
|
|
1776
1785
|
\`<option value="\${t}">\${TYPE_LABELS[t] || t}</option>\`
|
|
1777
1786
|
).join("");
|
|
1778
1787
|
} catch {
|
|
1779
|
-
sel.innerHTML = ["token","secret","keypair","connstring","oauth"].map(t =>
|
|
1788
|
+
sel.innerHTML = ["token","secret","keypair","connstring","oauth","fileserver"].map(t =>
|
|
1780
1789
|
\`<option value="\${t}">\${TYPE_LABELS[t] || t}</option>\`
|
|
1781
1790
|
).join("");
|
|
1782
1791
|
}
|
|
@@ -2911,7 +2920,18 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2911
2920
|
return res.end();
|
|
2912
2921
|
}
|
|
2913
2922
|
|
|
2923
|
+
// ── Hosts that bypass OAuth (fresh domains for claude.ai compatibility) ──
|
|
2924
|
+
const NOAUTH_HOSTS = ["fs.regendevcorp.com", "clauth.regendevcorp.com"];
|
|
2925
|
+
const requestHost = (req.headers.host || "").split(":")[0].toLowerCase();
|
|
2926
|
+
const noAuthHost = NOAUTH_HOSTS.includes(requestHost);
|
|
2927
|
+
|
|
2914
2928
|
// ── OAuth Discovery (RFC 9728 + RFC 8414) ──────────────
|
|
2929
|
+
// Suppress OAuth discovery on noauth hosts — prevents claude.ai from entering OAuth flow
|
|
2930
|
+
if (noAuthHost && reqPath.startsWith("/.well-known/")) {
|
|
2931
|
+
res.writeHead(404, { "Content-Type": "application/json", ...CORS });
|
|
2932
|
+
return res.end(JSON.stringify({ error: "not_found" }));
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2915
2935
|
// Restore full well-known + OAuth so custom setup with client_id/secret works.
|
|
2916
2936
|
if (reqPath.startsWith("/.well-known/oauth-protected-resource")) {
|
|
2917
2937
|
const base = oauthBase();
|
|
@@ -2943,6 +2963,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2943
2963
|
}
|
|
2944
2964
|
|
|
2945
2965
|
// ── Dynamic Client Registration (RFC 7591) ──────────────
|
|
2966
|
+
// Block OAuth endpoints on noauth hosts
|
|
2967
|
+
if (noAuthHost && ["/register", "/authorize", "/token"].includes(reqPath)) {
|
|
2968
|
+
res.writeHead(404, { "Content-Type": "application/json", ...CORS });
|
|
2969
|
+
return res.end(JSON.stringify({ error: "not_found" }));
|
|
2970
|
+
}
|
|
2946
2971
|
if (method === "POST" && reqPath === "/register") {
|
|
2947
2972
|
let body;
|
|
2948
2973
|
try { body = await readBody(req); } catch {
|
|
@@ -3094,7 +3119,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3094
3119
|
const isMcpPath = MCP_PATHS.includes(reqPath);
|
|
3095
3120
|
function toolsForPath(p) {
|
|
3096
3121
|
if (p === "/gws") return MCP_TOOLS.filter(t => t.name.startsWith("gws_"));
|
|
3097
|
-
if (p === "/clauth") return MCP_TOOLS.filter(t => t.name.startsWith("clauth_"));
|
|
3122
|
+
if (p === "/clauth") return MCP_TOOLS.filter(t => t.name.startsWith("clauth_") || t.name === "monkey_dispatch" || t.name.startsWith("terminal_"));
|
|
3098
3123
|
if (p === "/fs") return MCP_TOOLS.filter(t => t.name.startsWith("fs_"));
|
|
3099
3124
|
return MCP_TOOLS; // /mcp — all tools
|
|
3100
3125
|
}
|
|
@@ -3106,11 +3131,12 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3106
3131
|
}
|
|
3107
3132
|
|
|
3108
3133
|
// ── MCP endpoint auth — 401 gate (OAuth 2.1 protocol) ──
|
|
3134
|
+
// Skipped for noauth hosts — those domains use tunnel URL as shared secret (like regen-media)
|
|
3109
3135
|
if (method === "POST" && (reqPath === "/sse" || isMcpPath)) {
|
|
3110
3136
|
const authHeader = req.headers.authorization;
|
|
3111
3137
|
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
3112
3138
|
|
|
3113
|
-
if (!token || !oauthTokens.has(token)) {
|
|
3139
|
+
if (!noAuthHost && (!token || !oauthTokens.has(token))) {
|
|
3114
3140
|
// No valid Bearer token → return 401 with discovery hint
|
|
3115
3141
|
const base = oauthBase();
|
|
3116
3142
|
const resourcePath = isMcpPath ? reqPath.slice(1) : "sse"; // "mcp", "gws", "clauth", or "sse"
|
|
@@ -3445,6 +3471,106 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3445
3471
|
return;
|
|
3446
3472
|
}
|
|
3447
3473
|
|
|
3474
|
+
// POST /dispatch — spawn a headless Claude CLI worker for monkey skill queue
|
|
3475
|
+
if (method === "POST" && reqPath === "/dispatch") {
|
|
3476
|
+
let body = "";
|
|
3477
|
+
req.on("data", d => body += d);
|
|
3478
|
+
req.on("end", () => {
|
|
3479
|
+
try {
|
|
3480
|
+
const { prompt, job_id } = JSON.parse(body);
|
|
3481
|
+
if (!prompt) {
|
|
3482
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3483
|
+
return res.end(JSON.stringify({ error: "prompt required" }));
|
|
3484
|
+
}
|
|
3485
|
+
const result = spawnClaudeTask(prompt, job_id || "untracked");
|
|
3486
|
+
const status = result.error ? 503 : 200;
|
|
3487
|
+
res.writeHead(status, { "Content-Type": "application/json", ...CORS });
|
|
3488
|
+
res.end(JSON.stringify(result));
|
|
3489
|
+
} catch {
|
|
3490
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3491
|
+
res.end(JSON.stringify({ error: "invalid JSON" }));
|
|
3492
|
+
}
|
|
3493
|
+
});
|
|
3494
|
+
return;
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
// POST /terminal/start — start a named terminal session
|
|
3498
|
+
if (method === "POST" && reqPath === "/terminal/start") {
|
|
3499
|
+
let body = "";
|
|
3500
|
+
req.on("data", d => body += d);
|
|
3501
|
+
req.on("end", () => {
|
|
3502
|
+
try {
|
|
3503
|
+
const { name, knowledge_tier, context_md } = JSON.parse(body);
|
|
3504
|
+
if (!name) {
|
|
3505
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3506
|
+
return res.end(JSON.stringify({ error: "name required" }));
|
|
3507
|
+
}
|
|
3508
|
+
const tier = knowledge_tier || 'db_only';
|
|
3509
|
+
const result = startTerminalSession(name, tier, context_md || null);
|
|
3510
|
+
const status = result.error ? 503 : 200;
|
|
3511
|
+
res.writeHead(status, { "Content-Type": "application/json", ...CORS });
|
|
3512
|
+
res.end(JSON.stringify(result));
|
|
3513
|
+
} catch {
|
|
3514
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3515
|
+
res.end(JSON.stringify({ error: "invalid JSON" }));
|
|
3516
|
+
}
|
|
3517
|
+
});
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
// POST /terminal/send — send a message to a running terminal session
|
|
3522
|
+
if (method === "POST" && reqPath === "/terminal/send") {
|
|
3523
|
+
let body = "";
|
|
3524
|
+
req.on("data", d => body += d);
|
|
3525
|
+
req.on("end", () => {
|
|
3526
|
+
try {
|
|
3527
|
+
const { session_id, message } = JSON.parse(body);
|
|
3528
|
+
if (!session_id || !message) {
|
|
3529
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3530
|
+
return res.end(JSON.stringify({ error: "session_id and message required" }));
|
|
3531
|
+
}
|
|
3532
|
+
const result = sendTerminalMessage(session_id, message);
|
|
3533
|
+
const status = result.error === 'session_busy' ? 409 : result.error ? 404 : 200;
|
|
3534
|
+
res.writeHead(status, { "Content-Type": "application/json", ...CORS });
|
|
3535
|
+
res.end(JSON.stringify(result));
|
|
3536
|
+
} catch {
|
|
3537
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3538
|
+
res.end(JSON.stringify({ error: "invalid JSON" }));
|
|
3539
|
+
}
|
|
3540
|
+
});
|
|
3541
|
+
return;
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
// GET /terminal/list — list all active terminal sessions
|
|
3545
|
+
if (method === "GET" && reqPath === "/terminal/list") {
|
|
3546
|
+
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
3547
|
+
res.end(JSON.stringify(listTerminalSessions()));
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
// POST /terminal/stop — stop a terminal session
|
|
3552
|
+
if (method === "POST" && reqPath === "/terminal/stop") {
|
|
3553
|
+
let body = "";
|
|
3554
|
+
req.on("data", d => body += d);
|
|
3555
|
+
req.on("end", () => {
|
|
3556
|
+
try {
|
|
3557
|
+
const { session_id } = JSON.parse(body);
|
|
3558
|
+
if (!session_id) {
|
|
3559
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3560
|
+
return res.end(JSON.stringify({ error: "session_id required" }));
|
|
3561
|
+
}
|
|
3562
|
+
const result = stopTerminalSession(session_id);
|
|
3563
|
+
const status = result.error ? 404 : 200;
|
|
3564
|
+
res.writeHead(status, { "Content-Type": "application/json", ...CORS });
|
|
3565
|
+
res.end(JSON.stringify(result));
|
|
3566
|
+
} catch {
|
|
3567
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3568
|
+
res.end(JSON.stringify({ error: "invalid JSON" }));
|
|
3569
|
+
}
|
|
3570
|
+
});
|
|
3571
|
+
return;
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3448
3574
|
// GET|POST /shutdown (for daemon stop — programmatic, keeps boot.key)
|
|
3449
3575
|
// Accept POST as well — older scripts and curl default to POST
|
|
3450
3576
|
if ((method === "GET" || method === "POST") && reqPath === "/shutdown") {
|
|
@@ -3576,11 +3702,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3576
3702
|
const statusResult = await api.status(password, machineHash, token, timestamp);
|
|
3577
3703
|
const existingTypes = [...new Set((statusResult.services || []).map(s => s.key_type).filter(Boolean))];
|
|
3578
3704
|
// Merge with known types (in case no service of that type exists yet)
|
|
3579
|
-
const knownTypes = ["token", "secret", "keypair", "connstring", "oauth"];
|
|
3705
|
+
const knownTypes = ["token", "secret", "keypair", "connstring", "oauth", "fileserver"];
|
|
3580
3706
|
const allTypes = [...new Set([...knownTypes, ...existingTypes])];
|
|
3581
3707
|
return ok(res, { key_types: allTypes });
|
|
3582
3708
|
} catch (err) {
|
|
3583
|
-
return ok(res, { key_types: ["token", "secret", "keypair", "connstring", "oauth"] });
|
|
3709
|
+
return ok(res, { key_types: ["token", "secret", "keypair", "connstring", "oauth", "fileserver"] });
|
|
3584
3710
|
}
|
|
3585
3711
|
}
|
|
3586
3712
|
|
|
@@ -4403,7 +4529,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4403
4529
|
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
4404
4530
|
return res.end(JSON.stringify({ error: "name is required" }));
|
|
4405
4531
|
}
|
|
4406
|
-
const validTypes = ["token", "keypair", "connstring", "oauth", "secret"];
|
|
4532
|
+
const validTypes = ["token", "keypair", "connstring", "oauth", "secret", "fileserver"];
|
|
4407
4533
|
const type = (key_type || "token").toLowerCase();
|
|
4408
4534
|
if (!validTypes.includes(type)) {
|
|
4409
4535
|
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
@@ -4770,6 +4896,153 @@ async function actionForeground(opts) {
|
|
|
4770
4896
|
import { createInterface } from "readline";
|
|
4771
4897
|
import { execSync, spawn as spawnProc } from "child_process";
|
|
4772
4898
|
|
|
4899
|
+
// ── Monkey dispatch — headless Claude CLI worker ─────────────────
|
|
4900
|
+
function findClaudeBinary() {
|
|
4901
|
+
const candidates = [
|
|
4902
|
+
process.env.CLAUDE_BIN,
|
|
4903
|
+
path.join(process.env.APPDATA || '', 'npm', 'claude.cmd'),
|
|
4904
|
+
path.join(process.env.APPDATA || '', 'npm', 'claude'),
|
|
4905
|
+
'claude', // PATH fallback
|
|
4906
|
+
].filter(Boolean);
|
|
4907
|
+
|
|
4908
|
+
for (const c of candidates) {
|
|
4909
|
+
try {
|
|
4910
|
+
execSync(`"${c}" --version`, { stdio: 'ignore', timeout: 3000 });
|
|
4911
|
+
return c;
|
|
4912
|
+
} catch {}
|
|
4913
|
+
}
|
|
4914
|
+
return null;
|
|
4915
|
+
}
|
|
4916
|
+
|
|
4917
|
+
let activeCliWorkers = 0;
|
|
4918
|
+
const MAX_CLI_WORKERS = 2;
|
|
4919
|
+
|
|
4920
|
+
function spawnClaudeTask(prompt, jobId) {
|
|
4921
|
+
if (activeCliWorkers >= MAX_CLI_WORKERS) {
|
|
4922
|
+
return { error: 'concurrency_limit', message: `Max ${MAX_CLI_WORKERS} CLI workers active` };
|
|
4923
|
+
}
|
|
4924
|
+
const binary = findClaudeBinary();
|
|
4925
|
+
if (!binary) {
|
|
4926
|
+
return { error: 'binary_not_found', message: 'claude CLI not found in PATH or AppData/npm' };
|
|
4927
|
+
}
|
|
4928
|
+
|
|
4929
|
+
activeCliWorkers++;
|
|
4930
|
+
const proc = spawnProc(binary, ['-p', prompt, '--dangerously-skip-permissions'], {
|
|
4931
|
+
cwd: 'C:/Dev/regen-root',
|
|
4932
|
+
env: process.env,
|
|
4933
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4934
|
+
shell: true,
|
|
4935
|
+
});
|
|
4936
|
+
|
|
4937
|
+
const startedAt = Date.now();
|
|
4938
|
+
proc.on('close', (code) => {
|
|
4939
|
+
activeCliWorkers--;
|
|
4940
|
+
console.log(`[monkey] job ${jobId} exited code=${code} in ${Date.now() - startedAt}ms`);
|
|
4941
|
+
});
|
|
4942
|
+
|
|
4943
|
+
return { status: 'spawned', pid: proc.pid, jobId, activeWorkers: activeCliWorkers };
|
|
4944
|
+
}
|
|
4945
|
+
|
|
4946
|
+
// ── Terminal session manager ─────────────────────────────────────
|
|
4947
|
+
// Rolling-context approach: each session stores a context string.
|
|
4948
|
+
// /terminal/send spawns a fresh claude -p with [context + message],
|
|
4949
|
+
// captures stdout, stores result back in session context.
|
|
4950
|
+
const terminalSessions = new Map(); // session_id → SessionState
|
|
4951
|
+
|
|
4952
|
+
function generateSessionId() {
|
|
4953
|
+
return crypto.randomUUID();
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4956
|
+
function startTerminalSession(name, knowledge_tier, context_md) {
|
|
4957
|
+
const binary = findClaudeBinary();
|
|
4958
|
+
if (!binary) {
|
|
4959
|
+
return { error: 'binary_not_found', message: 'claude CLI not found in PATH or AppData/npm' };
|
|
4960
|
+
}
|
|
4961
|
+
const session_id = generateSessionId();
|
|
4962
|
+
const preamble = context_md
|
|
4963
|
+
? `${context_md}\n\n---\n`
|
|
4964
|
+
: '';
|
|
4965
|
+
const initialContext = `${preamble}Session: ${name} | Tier: ${knowledge_tier}\nReady. Await instructions.`;
|
|
4966
|
+
const session = {
|
|
4967
|
+
session_id,
|
|
4968
|
+
name,
|
|
4969
|
+
knowledge_tier,
|
|
4970
|
+
status: 'ready',
|
|
4971
|
+
started_at: new Date().toISOString(),
|
|
4972
|
+
context: initialContext,
|
|
4973
|
+
activeProc: null,
|
|
4974
|
+
};
|
|
4975
|
+
terminalSessions.set(session_id, session);
|
|
4976
|
+
console.log(`[terminal] started session ${session_id} name=${name}`);
|
|
4977
|
+
return { session_id, status: 'ready' };
|
|
4978
|
+
}
|
|
4979
|
+
|
|
4980
|
+
function sendTerminalMessage(session_id, message) {
|
|
4981
|
+
const session = terminalSessions.get(session_id);
|
|
4982
|
+
if (!session) return { error: 'not_found', message: `Session ${session_id} not found` };
|
|
4983
|
+
if (session.status === 'stopped') return { error: 'stopped', message: 'Session is stopped' };
|
|
4984
|
+
if (session.status === 'busy') return { error: 'session_busy', message: 'Session is busy — try again shortly' };
|
|
4985
|
+
|
|
4986
|
+
const binary = findClaudeBinary();
|
|
4987
|
+
if (!binary) return { error: 'binary_not_found', message: 'claude CLI not found in PATH or AppData/npm' };
|
|
4988
|
+
|
|
4989
|
+
session.status = 'busy';
|
|
4990
|
+
const fullPrompt = `${session.context}\n\n---\nUser: ${message}`;
|
|
4991
|
+
|
|
4992
|
+
const proc = spawnProc(binary, ['-p', fullPrompt, '--dangerously-skip-permissions'], {
|
|
4993
|
+
cwd: 'C:/Dev/regen-root',
|
|
4994
|
+
env: process.env,
|
|
4995
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4996
|
+
shell: true,
|
|
4997
|
+
});
|
|
4998
|
+
session.activeProc = proc;
|
|
4999
|
+
|
|
5000
|
+
let stdout = '';
|
|
5001
|
+
let stderr = '';
|
|
5002
|
+
proc.stdout.on('data', d => { stdout += d; });
|
|
5003
|
+
proc.stderr.on('data', d => { stderr += d; });
|
|
5004
|
+
proc.on('close', (code) => {
|
|
5005
|
+
if (terminalSessions.has(session_id)) {
|
|
5006
|
+
const s = terminalSessions.get(session_id);
|
|
5007
|
+
const response = stdout.trim() || stderr.trim() || '(no output)';
|
|
5008
|
+
// Append turn to rolling context (cap at ~8000 chars to avoid overflow)
|
|
5009
|
+
const turn = `\n\nUser: ${message}\nAssistant: ${response}`;
|
|
5010
|
+
const combined = s.context + turn;
|
|
5011
|
+
s.context = combined.length > 8000 ? combined.slice(combined.length - 8000) : combined;
|
|
5012
|
+
s.status = 'ready';
|
|
5013
|
+
s.activeProc = null;
|
|
5014
|
+
console.log(`[terminal] session ${session_id} turn complete code=${code}`);
|
|
5015
|
+
}
|
|
5016
|
+
});
|
|
5017
|
+
|
|
5018
|
+
return { queued: true, session_id };
|
|
5019
|
+
}
|
|
5020
|
+
|
|
5021
|
+
function listTerminalSessions() {
|
|
5022
|
+
const sessions = [];
|
|
5023
|
+
for (const [, s] of terminalSessions) {
|
|
5024
|
+
sessions.push({
|
|
5025
|
+
session_id: s.session_id,
|
|
5026
|
+
name: s.name,
|
|
5027
|
+
status: s.status,
|
|
5028
|
+
knowledge_tier: s.knowledge_tier,
|
|
5029
|
+
started_at: s.started_at,
|
|
5030
|
+
});
|
|
5031
|
+
}
|
|
5032
|
+
return sessions;
|
|
5033
|
+
}
|
|
5034
|
+
|
|
5035
|
+
function stopTerminalSession(session_id) {
|
|
5036
|
+
const session = terminalSessions.get(session_id);
|
|
5037
|
+
if (!session) return { error: 'not_found', message: `Session ${session_id} not found` };
|
|
5038
|
+
if (session.activeProc) {
|
|
5039
|
+
try { session.activeProc.kill(); } catch {}
|
|
5040
|
+
}
|
|
5041
|
+
terminalSessions.delete(session_id);
|
|
5042
|
+
console.log(`[terminal] stopped session ${session_id}`);
|
|
5043
|
+
return { stopped: true, session_id };
|
|
5044
|
+
}
|
|
5045
|
+
|
|
4773
5046
|
const ENV_MAP = {
|
|
4774
5047
|
"github": "GITHUB_TOKEN",
|
|
4775
5048
|
"supabase-anon": "NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
|
@@ -4787,14 +5060,48 @@ const ENV_MAP = {
|
|
|
4787
5060
|
"gmail": "GMAIL_CREDENTIALS",
|
|
4788
5061
|
};
|
|
4789
5062
|
|
|
4790
|
-
// ── Filesystem service config ──
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
5063
|
+
// ── Filesystem service config — loaded from clauth vault ──
|
|
5064
|
+
let _fsMountsCache = null;
|
|
5065
|
+
let _fsMountsCacheTime = 0;
|
|
5066
|
+
const FS_CACHE_TTL = 60000; // 1 minute
|
|
5067
|
+
|
|
5068
|
+
async function getFileserverMounts(vault) {
|
|
5069
|
+
if (!vault.password) return { error: "Vault is locked — unlock first" };
|
|
5070
|
+
const now = Date.now();
|
|
5071
|
+
if (_fsMountsCache && now - _fsMountsCacheTime < FS_CACHE_TTL) return { mounts: _fsMountsCache };
|
|
5072
|
+
|
|
5073
|
+
try {
|
|
5074
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
5075
|
+
const result = await api.status(vault.password, vault.machineHash, token, timestamp);
|
|
5076
|
+
if (result.error) return { error: result.error };
|
|
5077
|
+
const mounts = [];
|
|
5078
|
+
for (const s of (result.services || [])) {
|
|
5079
|
+
if (s.key_type === "fileserver" && s.enabled) {
|
|
5080
|
+
try {
|
|
5081
|
+
const { token: t2, timestamp: ts2 } = deriveToken(vault.password, vault.machineHash);
|
|
5082
|
+
const secret = await api.retrieve(vault.password, vault.machineHash, t2, ts2, s.name);
|
|
5083
|
+
if (secret.value) {
|
|
5084
|
+
const config = JSON.parse(secret.value);
|
|
5085
|
+
mounts.push({ name: s.name, path: config.path, access: config.access || "r" });
|
|
5086
|
+
}
|
|
5087
|
+
} catch {}
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
_fsMountsCache = mounts;
|
|
5091
|
+
_fsMountsCacheTime = now;
|
|
5092
|
+
return { mounts };
|
|
5093
|
+
} catch (err) {
|
|
5094
|
+
return { error: `Mount lookup failed: ${err.message}` };
|
|
5095
|
+
}
|
|
5096
|
+
}
|
|
4794
5097
|
|
|
4795
|
-
function resolveInMount(requestedPath, mountName) {
|
|
4796
|
-
const
|
|
4797
|
-
if (
|
|
5098
|
+
async function resolveInMount(requestedPath, mountName, vault) {
|
|
5099
|
+
const { mounts, error } = await getFileserverMounts(vault);
|
|
5100
|
+
if (error) return { error };
|
|
5101
|
+
if (!mounts || mounts.length === 0) return { error: "No fileserver services configured. Add one with key_type='fileserver' and value: {\"path\": \"C:/Dev/regen-root\", \"access\": \"rwd\"}" };
|
|
5102
|
+
const mount = mountName ? mounts.find(m => m.name === mountName) : mounts[0];
|
|
5103
|
+
if (!mount) return { error: `Mount '${mountName}' not found. Available: ${mounts.map(m => m.name).join(", ")}` };
|
|
5104
|
+
if (!mount.path) return { error: `Fileserver '${mount.name}' has no path configured` };
|
|
4798
5105
|
const resolved = path.resolve(mount.path, requestedPath);
|
|
4799
5106
|
const normalized = path.normalize(resolved);
|
|
4800
5107
|
if (!normalized.startsWith(path.normalize(mount.path))) {
|
|
@@ -4902,6 +5209,65 @@ const MCP_TOOLS = [
|
|
|
4902
5209
|
description: "Test whether the clauth MCP connector is reachable via the Cloudflare tunnel. Returns connectivity status and tunnel URL.",
|
|
4903
5210
|
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
4904
5211
|
},
|
|
5212
|
+
{
|
|
5213
|
+
name: "monkey_dispatch",
|
|
5214
|
+
description: "Dispatch a skill job to a headless Claude Code CLI worker. Spawns claude -p with the given prompt in C:/Dev/regen-root. Max 2 concurrent workers.",
|
|
5215
|
+
inputSchema: {
|
|
5216
|
+
type: "object",
|
|
5217
|
+
properties: {
|
|
5218
|
+
prompt: { type: "string", description: "Full prompt for the CLI worker to execute" },
|
|
5219
|
+
job_id: { type: "string", description: "monkey_jobs UUID for tracking" },
|
|
5220
|
+
},
|
|
5221
|
+
required: ["prompt"],
|
|
5222
|
+
additionalProperties: false,
|
|
5223
|
+
},
|
|
5224
|
+
},
|
|
5225
|
+
|
|
5226
|
+
// ── Terminal session manager ───────────────────────────────────────────
|
|
5227
|
+
{
|
|
5228
|
+
name: "terminal_start",
|
|
5229
|
+
description: "Start a named persistent Claude session. Sessions maintain rolling context across sends. Returns session_id for subsequent sends.",
|
|
5230
|
+
inputSchema: {
|
|
5231
|
+
type: "object",
|
|
5232
|
+
properties: {
|
|
5233
|
+
name: { type: "string", description: "Human-readable session name (e.g. 'knowledgedude-prt')" },
|
|
5234
|
+
knowledge_tier: { type: "string", enum: ["db_only", "corpus", "project"], description: "Knowledge tier for the session. 'corpus' injects context_md as preamble." },
|
|
5235
|
+
context_md: { type: "string", description: "Optional markdown preamble injected as session context (used for 'corpus' tier)" },
|
|
5236
|
+
},
|
|
5237
|
+
required: ["name"],
|
|
5238
|
+
additionalProperties: false,
|
|
5239
|
+
},
|
|
5240
|
+
},
|
|
5241
|
+
{
|
|
5242
|
+
name: "terminal_send",
|
|
5243
|
+
description: "Send a message to a running terminal session. Spawns a Claude worker with rolling context + message. Returns immediately — session processes async.",
|
|
5244
|
+
inputSchema: {
|
|
5245
|
+
type: "object",
|
|
5246
|
+
properties: {
|
|
5247
|
+
session_id: { type: "string", description: "Session ID returned by terminal_start" },
|
|
5248
|
+
message: { type: "string", description: "Message to send to the session" },
|
|
5249
|
+
},
|
|
5250
|
+
required: ["session_id", "message"],
|
|
5251
|
+
additionalProperties: false,
|
|
5252
|
+
},
|
|
5253
|
+
},
|
|
5254
|
+
{
|
|
5255
|
+
name: "terminal_list",
|
|
5256
|
+
description: "List all active terminal sessions with their status (ready/busy/stopped), knowledge tier, and start time.",
|
|
5257
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
5258
|
+
},
|
|
5259
|
+
{
|
|
5260
|
+
name: "terminal_stop",
|
|
5261
|
+
description: "Stop a terminal session and remove it from memory. Kills any active process.",
|
|
5262
|
+
inputSchema: {
|
|
5263
|
+
type: "object",
|
|
5264
|
+
properties: {
|
|
5265
|
+
session_id: { type: "string", description: "Session ID to stop" },
|
|
5266
|
+
},
|
|
5267
|
+
required: ["session_id"],
|
|
5268
|
+
additionalProperties: false,
|
|
5269
|
+
},
|
|
5270
|
+
},
|
|
4905
5271
|
|
|
4906
5272
|
// ── Google Workspace (gws CLI) ──────────────────────────────────────────
|
|
4907
5273
|
{
|
|
@@ -5083,6 +5449,11 @@ const MCP_TOOLS = [
|
|
|
5083
5449
|
additionalProperties: false,
|
|
5084
5450
|
},
|
|
5085
5451
|
},
|
|
5452
|
+
{
|
|
5453
|
+
name: "fs_mounts",
|
|
5454
|
+
description: "List configured filesystem mounts (fileserver services from vault).",
|
|
5455
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
5456
|
+
},
|
|
5086
5457
|
];
|
|
5087
5458
|
|
|
5088
5459
|
function writeTempSecret(service, value) {
|
|
@@ -5111,6 +5482,9 @@ function mcpError(text) {
|
|
|
5111
5482
|
return { content: [{ type: "text", text }], isError: true };
|
|
5112
5483
|
}
|
|
5113
5484
|
|
|
5485
|
+
// Windows cmd.exe doesn't support single quotes — use bash for gws JSON args
|
|
5486
|
+
const GWS_EXEC_OPTS = { encoding: "utf8", timeout: 30000, windowsHide: true, shell: os.platform() === "win32" ? "bash" : undefined };
|
|
5487
|
+
|
|
5114
5488
|
async function handleMcpTool(vault, name, args) {
|
|
5115
5489
|
switch (name) {
|
|
5116
5490
|
case "clauth_ping": {
|
|
@@ -5384,7 +5758,7 @@ async function handleMcpTool(vault, name, args) {
|
|
|
5384
5758
|
if (params) cmdArgs.push("--params", `'${JSON.stringify(params)}'`);
|
|
5385
5759
|
if (body) cmdArgs.push("--json", `'${JSON.stringify(body)}'`);
|
|
5386
5760
|
try {
|
|
5387
|
-
const raw = execSyncTop(["gws", ...cmdArgs].join(" "),
|
|
5761
|
+
const raw = execSyncTop(["gws", ...cmdArgs].join(" "), GWS_EXEC_OPTS);
|
|
5388
5762
|
try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
|
|
5389
5763
|
} catch (err) {
|
|
5390
5764
|
return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`);
|
|
@@ -5395,7 +5769,7 @@ async function handleMcpTool(vault, name, args) {
|
|
|
5395
5769
|
const p = { userId: "me", maxResults: args.max_results ?? 10 };
|
|
5396
5770
|
if (args.query) p.q = args.query;
|
|
5397
5771
|
try {
|
|
5398
|
-
const raw = execSyncTop(`gws gmail users messages list --params '${JSON.stringify(p)}'`,
|
|
5772
|
+
const raw = execSyncTop(`gws gmail users messages list --params '${JSON.stringify(p)}'`, GWS_EXEC_OPTS);
|
|
5399
5773
|
try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
|
|
5400
5774
|
} catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
|
|
5401
5775
|
}
|
|
@@ -5403,7 +5777,7 @@ async function handleMcpTool(vault, name, args) {
|
|
|
5403
5777
|
case "gws_gmail_read": {
|
|
5404
5778
|
const p = { userId: "me", id: args.message_id, format: "full" };
|
|
5405
5779
|
try {
|
|
5406
|
-
const raw = execSyncTop(`gws gmail users messages get --params '${JSON.stringify(p)}'`,
|
|
5780
|
+
const raw = execSyncTop(`gws gmail users messages get --params '${JSON.stringify(p)}'`, GWS_EXEC_OPTS);
|
|
5407
5781
|
try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
|
|
5408
5782
|
} catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
|
|
5409
5783
|
}
|
|
@@ -5415,7 +5789,7 @@ async function handleMcpTool(vault, name, args) {
|
|
|
5415
5789
|
const encoded = Buffer.from(lines.join("\r\n")).toString("base64url");
|
|
5416
5790
|
const bodyObj = { userId: "me", resource: { raw: encoded } };
|
|
5417
5791
|
try {
|
|
5418
|
-
const raw = execSyncTop(`gws gmail users messages send --json '${JSON.stringify(bodyObj)}'`,
|
|
5792
|
+
const raw = execSyncTop(`gws gmail users messages send --json '${JSON.stringify(bodyObj)}'`, GWS_EXEC_OPTS);
|
|
5419
5793
|
try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
|
|
5420
5794
|
} catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
|
|
5421
5795
|
}
|
|
@@ -5425,7 +5799,7 @@ async function handleMcpTool(vault, name, args) {
|
|
|
5425
5799
|
if (args.time_min) p.timeMin = args.time_min;
|
|
5426
5800
|
if (args.time_max) p.timeMax = args.time_max;
|
|
5427
5801
|
try {
|
|
5428
|
-
const raw = execSyncTop(`gws calendar events list --params '${JSON.stringify(p)}'`,
|
|
5802
|
+
const raw = execSyncTop(`gws calendar events list --params '${JSON.stringify(p)}'`, GWS_EXEC_OPTS);
|
|
5429
5803
|
try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
|
|
5430
5804
|
} catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
|
|
5431
5805
|
}
|
|
@@ -5434,7 +5808,7 @@ async function handleMcpTool(vault, name, args) {
|
|
|
5434
5808
|
const p = { pageSize: args.max_results ?? 10, fields: "files(id,name,mimeType,modifiedTime,size,webViewLink)" };
|
|
5435
5809
|
if (args.query) p.q = args.query;
|
|
5436
5810
|
try {
|
|
5437
|
-
const raw = execSyncTop(`gws drive files list --params '${JSON.stringify(p)}'`,
|
|
5811
|
+
const raw = execSyncTop(`gws drive files list --params '${JSON.stringify(p)}'`, GWS_EXEC_OPTS);
|
|
5438
5812
|
try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
|
|
5439
5813
|
} catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
|
|
5440
5814
|
}
|
|
@@ -5442,7 +5816,7 @@ async function handleMcpTool(vault, name, args) {
|
|
|
5442
5816
|
// ── Filesystem tools ──────────────────────────────────────
|
|
5443
5817
|
|
|
5444
5818
|
case "fs_read": {
|
|
5445
|
-
const r = resolveInMount(args.path, args.mount);
|
|
5819
|
+
const r = await resolveInMount(args.path, args.mount, vault);
|
|
5446
5820
|
if (r.error) return mcpError(r.error);
|
|
5447
5821
|
if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
|
|
5448
5822
|
try {
|
|
@@ -5461,7 +5835,7 @@ async function handleMcpTool(vault, name, args) {
|
|
|
5461
5835
|
}
|
|
5462
5836
|
|
|
5463
5837
|
case "fs_write": {
|
|
5464
|
-
const r = resolveInMount(args.path, args.mount);
|
|
5838
|
+
const r = await resolveInMount(args.path, args.mount, vault);
|
|
5465
5839
|
if (r.error) return mcpError(r.error);
|
|
5466
5840
|
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
5467
5841
|
try {
|
|
@@ -5475,7 +5849,7 @@ async function handleMcpTool(vault, name, args) {
|
|
|
5475
5849
|
|
|
5476
5850
|
case "fs_list": {
|
|
5477
5851
|
const dirPath = args.path || ".";
|
|
5478
|
-
const r = resolveInMount(dirPath, args.mount);
|
|
5852
|
+
const r = await resolveInMount(dirPath, args.mount, vault);
|
|
5479
5853
|
if (r.error) return mcpError(r.error);
|
|
5480
5854
|
if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
|
|
5481
5855
|
try {
|
|
@@ -5503,7 +5877,7 @@ async function handleMcpTool(vault, name, args) {
|
|
|
5503
5877
|
|
|
5504
5878
|
case "fs_grep": {
|
|
5505
5879
|
const searchPath = args.path || ".";
|
|
5506
|
-
const r = resolveInMount(searchPath, args.mount);
|
|
5880
|
+
const r = await resolveInMount(searchPath, args.mount, vault);
|
|
5507
5881
|
if (r.error) return mcpError(r.error);
|
|
5508
5882
|
if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
|
|
5509
5883
|
|
|
@@ -5553,7 +5927,7 @@ async function handleMcpTool(vault, name, args) {
|
|
|
5553
5927
|
|
|
5554
5928
|
case "fs_glob": {
|
|
5555
5929
|
const basePath = args.path || ".";
|
|
5556
|
-
const r = resolveInMount(basePath, args.mount);
|
|
5930
|
+
const r = await resolveInMount(basePath, args.mount, vault);
|
|
5557
5931
|
if (r.error) return mcpError(r.error);
|
|
5558
5932
|
if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
|
|
5559
5933
|
try {
|
|
@@ -5571,11 +5945,12 @@ async function handleMcpTool(vault, name, args) {
|
|
|
5571
5945
|
}
|
|
5572
5946
|
|
|
5573
5947
|
case "fs_delete": {
|
|
5574
|
-
const r = resolveInMount(args.path, args.mount);
|
|
5948
|
+
const r = await resolveInMount(args.path, args.mount, vault);
|
|
5575
5949
|
if (r.error) return mcpError(r.error);
|
|
5576
5950
|
if (!checkAccess(r.mount, "d")) return mcpError("Delete access denied on this mount");
|
|
5577
5951
|
try {
|
|
5578
|
-
await
|
|
5952
|
+
const s = await stat(r.resolved);
|
|
5953
|
+
await rm(r.resolved, { recursive: s.isDirectory() });
|
|
5579
5954
|
return mcpResult(`Deleted: ${args.path}`);
|
|
5580
5955
|
} catch (err) {
|
|
5581
5956
|
if (err.code === "ENOENT") return mcpError(`Not found: ${args.path}`);
|
|
@@ -5584,7 +5959,7 @@ async function handleMcpTool(vault, name, args) {
|
|
|
5584
5959
|
}
|
|
5585
5960
|
|
|
5586
5961
|
case "fs_mkdir": {
|
|
5587
|
-
const r = resolveInMount(args.path, args.mount);
|
|
5962
|
+
const r = await resolveInMount(args.path, args.mount, vault);
|
|
5588
5963
|
if (r.error) return mcpError(r.error);
|
|
5589
5964
|
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
5590
5965
|
try {
|
|
@@ -5595,6 +5970,49 @@ async function handleMcpTool(vault, name, args) {
|
|
|
5595
5970
|
}
|
|
5596
5971
|
}
|
|
5597
5972
|
|
|
5973
|
+
case "fs_mounts": {
|
|
5974
|
+
const { mounts, error } = await getFileserverMounts(vault);
|
|
5975
|
+
if (error) return mcpError(error);
|
|
5976
|
+
if (!mounts || mounts.length === 0) return mcpResult("No fileserver mounts configured. Create one:\n1. Use clauth dashboard or clauth_enable to add a service with key_type='fileserver'\n2. Set the secret value to JSON: {\"path\": \"C:/Dev/regen-root\", \"access\": \"rwd\"}");
|
|
5977
|
+
return mcpResult(JSON.stringify(mounts, null, 2));
|
|
5978
|
+
}
|
|
5979
|
+
|
|
5980
|
+
case "monkey_dispatch": {
|
|
5981
|
+
const { prompt, job_id } = args;
|
|
5982
|
+
if (!prompt) return mcpError("prompt required");
|
|
5983
|
+
const result = spawnClaudeTask(prompt, job_id || "untracked");
|
|
5984
|
+
if (result.error) return mcpError(`${result.error}: ${result.message}`);
|
|
5985
|
+
return mcpResult(JSON.stringify(result));
|
|
5986
|
+
}
|
|
5987
|
+
|
|
5988
|
+
case "terminal_start": {
|
|
5989
|
+
const { name, knowledge_tier, context_md } = args;
|
|
5990
|
+
if (!name) return mcpError("name required");
|
|
5991
|
+
const result = startTerminalSession(name, knowledge_tier || 'db_only', context_md || null);
|
|
5992
|
+
if (result.error) return mcpError(`${result.error}: ${result.message}`);
|
|
5993
|
+
return mcpResult(JSON.stringify(result));
|
|
5994
|
+
}
|
|
5995
|
+
|
|
5996
|
+
case "terminal_send": {
|
|
5997
|
+
const { session_id, message } = args;
|
|
5998
|
+
if (!session_id || !message) return mcpError("session_id and message required");
|
|
5999
|
+
const result = sendTerminalMessage(session_id, message);
|
|
6000
|
+
if (result.error) return mcpError(`${result.error}: ${result.message}`);
|
|
6001
|
+
return mcpResult(JSON.stringify(result));
|
|
6002
|
+
}
|
|
6003
|
+
|
|
6004
|
+
case "terminal_list": {
|
|
6005
|
+
return mcpResult(JSON.stringify(listTerminalSessions()));
|
|
6006
|
+
}
|
|
6007
|
+
|
|
6008
|
+
case "terminal_stop": {
|
|
6009
|
+
const { session_id } = args;
|
|
6010
|
+
if (!session_id) return mcpError("session_id required");
|
|
6011
|
+
const result = stopTerminalSession(session_id);
|
|
6012
|
+
if (result.error) return mcpError(`${result.error}: ${result.message}`);
|
|
6013
|
+
return mcpResult(JSON.stringify(result));
|
|
6014
|
+
}
|
|
6015
|
+
|
|
5598
6016
|
default:
|
|
5599
6017
|
return mcpError(`Unknown tool: ${name}`);
|
|
5600
6018
|
}
|