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