@lifeaitools/clauth 1.5.84 → 1.6.0
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/cli/commands/serve.js +873 -699
- package/package.json +1 -1
package/cli/commands/serve.js
CHANGED
|
@@ -17,7 +17,7 @@ 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 { appendFile, readdir, readFile, writeFile, rm, mkdir, stat, rename } from "node:fs/promises";
|
|
20
|
+
import { appendFile, readdir, readFile, writeFile, rm, mkdir, stat, rename, cp } from "node:fs/promises";
|
|
21
21
|
import fg from "fast-glob";
|
|
22
22
|
import { rgPath } from "@vscode/ripgrep";
|
|
23
23
|
import { createStudioDebugRuntime } from "../studio-debug.js";
|
|
@@ -607,14 +607,14 @@ function dashboardHtml(port, whitelist, isStaged = false) {
|
|
|
607
607
|
.project-tab{background:none;border:none;border-bottom:2px solid transparent;color:#64748b;padding:8px 16px;font-size:.82rem;font-weight:500;cursor:pointer;white-space:nowrap;transition:all .15s}
|
|
608
608
|
.project-tab:hover{color:#94a3b8;background:rgba(59,130,246,.05)}
|
|
609
609
|
.project-tab.active{color:#60a5fa;border-bottom-color:#3b82f6;background:rgba(59,130,246,.08)}
|
|
610
|
-
.project-tab .tab-count{font-size:.7rem;color:#475569;margin-left:4px;font-weight:400}
|
|
611
|
-
.project-tab.active .tab-count{color:#3b82f6}
|
|
612
|
-
.service-search{display:flex;align-items:center;gap:10px;margin:-.35rem 0 1rem;background:#0f172a;border:1px solid #1e293b;border-radius:8px;padding:9px 12px}
|
|
613
|
-
.service-search-label{font-size:.76rem;color:#64748b;font-weight:600;letter-spacing:.02em;text-transform:uppercase;white-space:nowrap}
|
|
614
|
-
.service-search-input{flex:1;min-width:180px;background:#0a0f1a;border:1px solid #334155;border-radius:6px;color:#e2e8f0;font-family:'Courier New',monospace;font-size:.88rem;padding:8px 11px;outline:none;transition:border-color .15s}
|
|
615
|
-
.service-search-input:focus{border-color:#3b82f6}
|
|
616
|
-
.service-search-count{font-size:.78rem;color:#64748b;white-space:nowrap}
|
|
617
|
-
.project-edit{display:none;margin-top:8px;padding:8px 10px;background:#0f172a;border:1px solid #334155;border-radius:6px}
|
|
610
|
+
.project-tab .tab-count{font-size:.7rem;color:#475569;margin-left:4px;font-weight:400}
|
|
611
|
+
.project-tab.active .tab-count{color:#3b82f6}
|
|
612
|
+
.service-search{display:flex;align-items:center;gap:10px;margin:-.35rem 0 1rem;background:#0f172a;border:1px solid #1e293b;border-radius:8px;padding:9px 12px}
|
|
613
|
+
.service-search-label{font-size:.76rem;color:#64748b;font-weight:600;letter-spacing:.02em;text-transform:uppercase;white-space:nowrap}
|
|
614
|
+
.service-search-input{flex:1;min-width:180px;background:#0a0f1a;border:1px solid #334155;border-radius:6px;color:#e2e8f0;font-family:'Courier New',monospace;font-size:.88rem;padding:8px 11px;outline:none;transition:border-color .15s}
|
|
615
|
+
.service-search-input:focus{border-color:#3b82f6}
|
|
616
|
+
.service-search-count{font-size:.78rem;color:#64748b;white-space:nowrap}
|
|
617
|
+
.project-edit{display:none;margin-top:8px;padding:8px 10px;background:#0f172a;border:1px solid #334155;border-radius:6px}
|
|
618
618
|
.project-edit.open{display:flex;gap:6px;align-items:center}
|
|
619
619
|
.project-edit input{background:#1e293b;border:1px solid #334155;border-radius:4px;color:#e2e8f0;font-size:.78rem;padding:4px 8px;outline:none;flex:1;font-family:'Courier New',monospace;transition:border-color .15s}
|
|
620
620
|
.project-edit input:focus{border-color:#3b82f6}
|
|
@@ -868,13 +868,13 @@ function dashboardHtml(port, whitelist, isStaged = false) {
|
|
|
868
868
|
<div class="wizard-foot" id="wizard-foot"></div>
|
|
869
869
|
</div>
|
|
870
870
|
|
|
871
|
-
<div id="project-tabs" class="project-tabs" style="display:none"></div>
|
|
872
|
-
<div id="service-search" class="service-search">
|
|
873
|
-
<span class="service-search-label">Search</span>
|
|
874
|
-
<input id="service-search-input" class="service-search-input" type="search" placeholder="service name or display name" autocomplete="off" spellcheck="false" oninput="setServiceSearch(this.value)">
|
|
875
|
-
<span id="service-search-count" class="service-search-count"></span>
|
|
876
|
-
</div>
|
|
877
|
-
<div id="grid" class="grid"><p class="loading">Loading services…</p></div>
|
|
871
|
+
<div id="project-tabs" class="project-tabs" style="display:none"></div>
|
|
872
|
+
<div id="service-search" class="service-search">
|
|
873
|
+
<span class="service-search-label">Search</span>
|
|
874
|
+
<input id="service-search-input" class="service-search-input" type="search" placeholder="service name or display name" autocomplete="off" spellcheck="false" oninput="setServiceSearch(this.value)">
|
|
875
|
+
<span id="service-search-count" class="service-search-count"></span>
|
|
876
|
+
</div>
|
|
877
|
+
<div id="grid" class="grid"><p class="loading">Loading services…</p></div>
|
|
878
878
|
<div class="footer">localhost:${port} · 127.0.0.1 only · 10-strike lockout</div>
|
|
879
879
|
</div>
|
|
880
880
|
|
|
@@ -1168,25 +1168,25 @@ async function stopDaemon() {
|
|
|
1168
1168
|
}
|
|
1169
1169
|
|
|
1170
1170
|
// ── Load services ───────────────────────────
|
|
1171
|
-
let allServices = [];
|
|
1172
|
-
let activeProjectTab = "all";
|
|
1173
|
-
let serviceSearchQuery = "";
|
|
1174
|
-
|
|
1175
|
-
function serviceSort(a, b) {
|
|
1176
|
-
return String(a.name || "").localeCompare(String(b.name || ""), undefined, { sensitivity: "base", numeric: true });
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
function matchesServiceSearch(s, query) {
|
|
1180
|
-
if (!query) return true;
|
|
1181
|
-
const q = query.toLowerCase();
|
|
1182
|
-
return String(s.name || "").toLowerCase().includes(q) ||
|
|
1183
|
-
String(s.label || "").toLowerCase().includes(q);
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
function setServiceSearch(value) {
|
|
1187
|
-
serviceSearchQuery = value || "";
|
|
1188
|
-
renderServiceGrid(allServices);
|
|
1189
|
-
}
|
|
1171
|
+
let allServices = [];
|
|
1172
|
+
let activeProjectTab = "all";
|
|
1173
|
+
let serviceSearchQuery = "";
|
|
1174
|
+
|
|
1175
|
+
function serviceSort(a, b) {
|
|
1176
|
+
return String(a.name || "").localeCompare(String(b.name || ""), undefined, { sensitivity: "base", numeric: true });
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function matchesServiceSearch(s, query) {
|
|
1180
|
+
if (!query) return true;
|
|
1181
|
+
const q = query.toLowerCase();
|
|
1182
|
+
return String(s.name || "").toLowerCase().includes(q) ||
|
|
1183
|
+
String(s.label || "").toLowerCase().includes(q);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function setServiceSearch(value) {
|
|
1187
|
+
serviceSearchQuery = value || "";
|
|
1188
|
+
renderServiceGrid(allServices);
|
|
1189
|
+
}
|
|
1190
1190
|
|
|
1191
1191
|
function renderProjectTabs(services) {
|
|
1192
1192
|
const tabsEl = document.getElementById("project-tabs");
|
|
@@ -1212,35 +1212,35 @@ function renderProjectTabs(services) {
|
|
|
1212
1212
|
).join("");
|
|
1213
1213
|
}
|
|
1214
1214
|
|
|
1215
|
-
function switchProjectTab(key) {
|
|
1216
|
-
activeProjectTab = key;
|
|
1217
|
-
renderProjectTabs(allServices);
|
|
1218
|
-
renderServiceGrid(allServices);
|
|
1219
|
-
}
|
|
1215
|
+
function switchProjectTab(key) {
|
|
1216
|
+
activeProjectTab = key;
|
|
1217
|
+
renderProjectTabs(allServices);
|
|
1218
|
+
renderServiceGrid(allServices);
|
|
1219
|
+
}
|
|
1220
1220
|
|
|
1221
1221
|
function renderServiceGrid(services) {
|
|
1222
1222
|
const grid = document.getElementById("grid");
|
|
1223
1223
|
let filtered = services;
|
|
1224
1224
|
if (activeProjectTab === "unassigned") {
|
|
1225
1225
|
filtered = services.filter(s => !s.project);
|
|
1226
|
-
} else if (activeProjectTab !== "all") {
|
|
1227
|
-
filtered = services.filter(s => s.project === activeProjectTab);
|
|
1228
|
-
}
|
|
1229
|
-
filtered = filtered
|
|
1230
|
-
.filter(s => matchesServiceSearch(s, serviceSearchQuery))
|
|
1231
|
-
.slice()
|
|
1232
|
-
.sort(serviceSort);
|
|
1233
|
-
const searchCount = document.getElementById("service-search-count");
|
|
1234
|
-
if (searchCount) {
|
|
1235
|
-
const trimmed = serviceSearchQuery.trim();
|
|
1236
|
-
searchCount.textContent = trimmed ? filtered.length + " match" + (filtered.length === 1 ? "" : "es") : filtered.length + " shown";
|
|
1237
|
-
}
|
|
1238
|
-
if (!filtered.length) {
|
|
1239
|
-
grid.innerHTML = serviceSearchQuery.trim()
|
|
1240
|
-
? '<p class="loading">No services match that search.</p>'
|
|
1241
|
-
: '<p class="loading">No services in this group.</p>';
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1226
|
+
} else if (activeProjectTab !== "all") {
|
|
1227
|
+
filtered = services.filter(s => s.project === activeProjectTab);
|
|
1228
|
+
}
|
|
1229
|
+
filtered = filtered
|
|
1230
|
+
.filter(s => matchesServiceSearch(s, serviceSearchQuery))
|
|
1231
|
+
.slice()
|
|
1232
|
+
.sort(serviceSort);
|
|
1233
|
+
const searchCount = document.getElementById("service-search-count");
|
|
1234
|
+
if (searchCount) {
|
|
1235
|
+
const trimmed = serviceSearchQuery.trim();
|
|
1236
|
+
searchCount.textContent = trimmed ? filtered.length + " match" + (filtered.length === 1 ? "" : "es") : filtered.length + " shown";
|
|
1237
|
+
}
|
|
1238
|
+
if (!filtered.length) {
|
|
1239
|
+
grid.innerHTML = serviceSearchQuery.trim()
|
|
1240
|
+
? '<p class="loading">No services match that search.</p>'
|
|
1241
|
+
: '<p class="loading">No services in this group.</p>';
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
1244
|
grid.innerHTML = filtered.map(s => \`
|
|
1245
1245
|
<div class="card">
|
|
1246
1246
|
<div style="display:flex;align-items:flex-start;justify-content:space-between">
|
|
@@ -5117,8 +5117,8 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5117
5117
|
}
|
|
5118
5118
|
|
|
5119
5119
|
// POST /update-service — update service metadata (project, label, description)
|
|
5120
|
-
if (method === "POST" && reqPath === "/update-service") {
|
|
5121
|
-
if (lockedGuard(res)) return;
|
|
5120
|
+
if (method === "POST" && reqPath === "/update-service") {
|
|
5121
|
+
if (lockedGuard(res)) return;
|
|
5122
5122
|
|
|
5123
5123
|
let body;
|
|
5124
5124
|
try { body = await readBody(req); } catch {
|
|
@@ -5149,39 +5149,39 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5149
5149
|
return ok(res, { ok: true, service: service.toLowerCase(), ...updates });
|
|
5150
5150
|
} catch (err) {
|
|
5151
5151
|
return strike(res, 502, err.message);
|
|
5152
|
-
}
|
|
5153
|
-
}
|
|
5154
|
-
|
|
5155
|
-
if (method === "GET" && reqPath === "/search") {
|
|
5156
|
-
if (lockedGuard(res)) return;
|
|
5157
|
-
const query = (url.searchParams.get("q") || url.searchParams.get("query") || "").trim();
|
|
5158
|
-
const project = (url.searchParams.get("project") || "").trim() || undefined;
|
|
5159
|
-
const includeAddresses = url.searchParams.get("addresses") !== "false";
|
|
5160
|
-
if (!query) {
|
|
5161
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
5162
|
-
return res.end(JSON.stringify({ error: "q query parameter is required" }));
|
|
5163
|
-
}
|
|
5164
|
-
|
|
5165
|
-
try {
|
|
5166
|
-
const { token, timestamp } = deriveToken(password, machineHash);
|
|
5167
|
-
const result = await searchServices({
|
|
5168
|
-
password,
|
|
5169
|
-
machineHash,
|
|
5170
|
-
token,
|
|
5171
|
-
timestamp,
|
|
5172
|
-
project,
|
|
5173
|
-
query,
|
|
5174
|
-
includeAddresses,
|
|
5175
|
-
whitelist
|
|
5176
|
-
});
|
|
5177
|
-
if (result.error) return strike(res, 502, result.error);
|
|
5178
|
-
return ok(res, result);
|
|
5179
|
-
} catch (err) {
|
|
5180
|
-
return strike(res, 502, err.message);
|
|
5181
|
-
}
|
|
5182
|
-
}
|
|
5183
|
-
|
|
5184
|
-
// Unknown route — a wrong URL is not an auth failure. Log it, return 404,
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
5154
|
+
|
|
5155
|
+
if (method === "GET" && reqPath === "/search") {
|
|
5156
|
+
if (lockedGuard(res)) return;
|
|
5157
|
+
const query = (url.searchParams.get("q") || url.searchParams.get("query") || "").trim();
|
|
5158
|
+
const project = (url.searchParams.get("project") || "").trim() || undefined;
|
|
5159
|
+
const includeAddresses = url.searchParams.get("addresses") !== "false";
|
|
5160
|
+
if (!query) {
|
|
5161
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
5162
|
+
return res.end(JSON.stringify({ error: "q query parameter is required" }));
|
|
5163
|
+
}
|
|
5164
|
+
|
|
5165
|
+
try {
|
|
5166
|
+
const { token, timestamp } = deriveToken(password, machineHash);
|
|
5167
|
+
const result = await searchServices({
|
|
5168
|
+
password,
|
|
5169
|
+
machineHash,
|
|
5170
|
+
token,
|
|
5171
|
+
timestamp,
|
|
5172
|
+
project,
|
|
5173
|
+
query,
|
|
5174
|
+
includeAddresses,
|
|
5175
|
+
whitelist
|
|
5176
|
+
});
|
|
5177
|
+
if (result.error) return strike(res, 502, result.error);
|
|
5178
|
+
return ok(res, result);
|
|
5179
|
+
} catch (err) {
|
|
5180
|
+
return strike(res, 502, err.message);
|
|
5181
|
+
}
|
|
5182
|
+
}
|
|
5183
|
+
|
|
5184
|
+
// Unknown route — a wrong URL is not an auth failure. Log it, return 404,
|
|
5185
5185
|
// but do NOT increment failCount (which locks the vault at MAX_FAILS).
|
|
5186
5186
|
// Auth failures (wrong password, wrong token) still strike via /auth and /get/:service.
|
|
5187
5187
|
try {
|
|
@@ -5552,8 +5552,8 @@ async function actionForeground(opts) {
|
|
|
5552
5552
|
// Reuses the same auth model as the HTTP daemon.
|
|
5553
5553
|
// Secrets are delivered via temp files — never in the MCP response.
|
|
5554
5554
|
|
|
5555
|
-
import { createInterface } from "readline";
|
|
5556
|
-
import { execSync, spawn as spawnProc, spawnSync } from "child_process";
|
|
5555
|
+
import { createInterface } from "readline";
|
|
5556
|
+
import { execSync, spawn as spawnProc, spawnSync } from "child_process";
|
|
5557
5557
|
|
|
5558
5558
|
// ── Monkey dispatch — headless Claude CLI worker ─────────────────
|
|
5559
5559
|
function findClaudeBinary() {
|
|
@@ -6156,9 +6156,9 @@ function stopCodevelopSession(session_id) {
|
|
|
6156
6156
|
return { stopped: true, session_id };
|
|
6157
6157
|
}
|
|
6158
6158
|
|
|
6159
|
-
const ENV_MAP = {
|
|
6160
|
-
"github": "GITHUB_TOKEN",
|
|
6161
|
-
"supabase-anon": "NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
|
6159
|
+
const ENV_MAP = {
|
|
6160
|
+
"github": "GITHUB_TOKEN",
|
|
6161
|
+
"supabase-anon": "NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
|
6162
6162
|
"supabase-service": "SUPABASE_SERVICE_ROLE_KEY",
|
|
6163
6163
|
"supabase-db": "SUPABASE_DB_URL",
|
|
6164
6164
|
"vercel": "VERCEL_TOKEN",
|
|
@@ -6170,116 +6170,116 @@ const ENV_MAP = {
|
|
|
6170
6170
|
"rocketreach": "ROCKETREACH_API_KEY",
|
|
6171
6171
|
"npm": "NPM_TOKEN",
|
|
6172
6172
|
"namecheap": "NAMECHEAP_API_KEY",
|
|
6173
|
-
"gmail": "GMAIL_CREDENTIALS",
|
|
6174
|
-
};
|
|
6175
|
-
|
|
6176
|
-
const ADDRESS_KEY_TYPES = new Set(["connstring", "fileserver", "oauth"]);
|
|
6177
|
-
const ADDRESS_FIELDS = new Set(["url", "uri", "host", "hostname", "server", "address", "base_url", "endpoint", "path", "root"]);
|
|
6178
|
-
|
|
6179
|
-
function normalizeSearchText(value) {
|
|
6180
|
-
return String(value || "").toLowerCase();
|
|
6181
|
-
}
|
|
6182
|
-
|
|
6183
|
-
function redactUrlish(value) {
|
|
6184
|
-
const text = String(value || "").trim();
|
|
6185
|
-
if (!text) return "";
|
|
6186
|
-
try {
|
|
6187
|
-
const url = new URL(text);
|
|
6188
|
-
if (url.username) url.username = "***";
|
|
6189
|
-
if (url.password) url.password = "***";
|
|
6190
|
-
return url.toString();
|
|
6191
|
-
} catch {
|
|
6192
|
-
return text.replace(/:\/\/([^:@/\s]+):([^@/\s]+)@/g, "://***:***@");
|
|
6193
|
-
}
|
|
6194
|
-
}
|
|
6195
|
-
|
|
6196
|
-
function collectAddressHints(value, keyType) {
|
|
6197
|
-
if (!ADDRESS_KEY_TYPES.has(String(keyType || "").toLowerCase())) return [];
|
|
6198
|
-
const hints = new Set();
|
|
6199
|
-
|
|
6200
|
-
function add(candidate) {
|
|
6201
|
-
if (candidate === undefined || candidate === null) return;
|
|
6202
|
-
const text = redactUrlish(candidate);
|
|
6203
|
-
if (text) hints.add(text);
|
|
6204
|
-
}
|
|
6205
|
-
|
|
6206
|
-
function walk(node, fieldName = "") {
|
|
6207
|
-
if (node === undefined || node === null) return;
|
|
6208
|
-
if (typeof node === "string") {
|
|
6209
|
-
if (fieldName && ADDRESS_FIELDS.has(fieldName.toLowerCase())) add(node);
|
|
6210
|
-
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(node) || /^[A-Za-z]:[\\/]/.test(node) || node.startsWith("\\\\")) add(node);
|
|
6211
|
-
return;
|
|
6212
|
-
}
|
|
6213
|
-
if (Array.isArray(node)) {
|
|
6214
|
-
for (const item of node) walk(item, fieldName);
|
|
6215
|
-
return;
|
|
6216
|
-
}
|
|
6217
|
-
if (typeof node === "object") {
|
|
6218
|
-
for (const [key, child] of Object.entries(node)) walk(child, key);
|
|
6219
|
-
}
|
|
6220
|
-
}
|
|
6221
|
-
|
|
6222
|
-
try {
|
|
6223
|
-
walk(JSON.parse(value));
|
|
6224
|
-
} catch {
|
|
6225
|
-
walk(value);
|
|
6226
|
-
}
|
|
6227
|
-
|
|
6228
|
-
return [...hints];
|
|
6229
|
-
}
|
|
6230
|
-
|
|
6231
|
-
async function searchServices({ password, machineHash, token, timestamp, query, project, includeAddresses = true, whitelist }) {
|
|
6232
|
-
const q = normalizeSearchText(query);
|
|
6233
|
-
if (!q) return { error: "query is required" };
|
|
6234
|
-
|
|
6235
|
-
const result = await api.status(password, machineHash, token, timestamp, project);
|
|
6236
|
-
if (result.error) return { error: result.error };
|
|
6237
|
-
|
|
6238
|
-
let services = result.services || [];
|
|
6239
|
-
if (whitelist) services = services.filter(s => whitelist.includes(String(s.name || "").toLowerCase()));
|
|
6240
|
-
|
|
6241
|
-
const matches = [];
|
|
6242
|
-
for (const s of services) {
|
|
6243
|
-
const fields = {
|
|
6244
|
-
name: s.name,
|
|
6245
|
-
label: s.label,
|
|
6246
|
-
project: s.project,
|
|
6247
|
-
type: s.key_type,
|
|
6248
|
-
description: s.description
|
|
6249
|
-
};
|
|
6250
|
-
const matched = Object.entries(fields)
|
|
6251
|
-
.filter(([, value]) => normalizeSearchText(value).includes(q))
|
|
6252
|
-
.map(([field]) => field);
|
|
6253
|
-
|
|
6254
|
-
let addressHints = [];
|
|
6255
|
-
if (includeAddresses && ADDRESS_KEY_TYPES.has(String(s.key_type || "").toLowerCase()) && s.vault_key) {
|
|
6256
|
-
const secret = await api.retrieve(password, machineHash, token, timestamp, s.name);
|
|
6257
|
-
if (!secret.error) {
|
|
6258
|
-
addressHints = collectAddressHints(secret.value, s.key_type);
|
|
6259
|
-
if (addressHints.some(h => normalizeSearchText(h).includes(q))) matched.push("address");
|
|
6260
|
-
}
|
|
6261
|
-
}
|
|
6262
|
-
|
|
6263
|
-
if (matched.length) {
|
|
6264
|
-
matches.push({
|
|
6265
|
-
name: s.name,
|
|
6266
|
-
label: s.label || null,
|
|
6267
|
-
project: s.project || null,
|
|
6268
|
-
key_type: s.key_type,
|
|
6269
|
-
enabled: !!s.enabled,
|
|
6270
|
-
has_key: !!s.vault_key,
|
|
6271
|
-
description: s.description || null,
|
|
6272
|
-
matched: [...new Set(matched)],
|
|
6273
|
-
address_hints: addressHints
|
|
6274
|
-
});
|
|
6275
|
-
}
|
|
6276
|
-
}
|
|
6277
|
-
|
|
6278
|
-
return { query, count: matches.length, matches };
|
|
6279
|
-
}
|
|
6280
|
-
|
|
6281
|
-
// ── Filesystem service config — loaded from clauth vault ──
|
|
6282
|
-
let _fsMountsCache = null;
|
|
6173
|
+
"gmail": "GMAIL_CREDENTIALS",
|
|
6174
|
+
};
|
|
6175
|
+
|
|
6176
|
+
const ADDRESS_KEY_TYPES = new Set(["connstring", "fileserver", "oauth"]);
|
|
6177
|
+
const ADDRESS_FIELDS = new Set(["url", "uri", "host", "hostname", "server", "address", "base_url", "endpoint", "path", "root"]);
|
|
6178
|
+
|
|
6179
|
+
function normalizeSearchText(value) {
|
|
6180
|
+
return String(value || "").toLowerCase();
|
|
6181
|
+
}
|
|
6182
|
+
|
|
6183
|
+
function redactUrlish(value) {
|
|
6184
|
+
const text = String(value || "").trim();
|
|
6185
|
+
if (!text) return "";
|
|
6186
|
+
try {
|
|
6187
|
+
const url = new URL(text);
|
|
6188
|
+
if (url.username) url.username = "***";
|
|
6189
|
+
if (url.password) url.password = "***";
|
|
6190
|
+
return url.toString();
|
|
6191
|
+
} catch {
|
|
6192
|
+
return text.replace(/:\/\/([^:@/\s]+):([^@/\s]+)@/g, "://***:***@");
|
|
6193
|
+
}
|
|
6194
|
+
}
|
|
6195
|
+
|
|
6196
|
+
function collectAddressHints(value, keyType) {
|
|
6197
|
+
if (!ADDRESS_KEY_TYPES.has(String(keyType || "").toLowerCase())) return [];
|
|
6198
|
+
const hints = new Set();
|
|
6199
|
+
|
|
6200
|
+
function add(candidate) {
|
|
6201
|
+
if (candidate === undefined || candidate === null) return;
|
|
6202
|
+
const text = redactUrlish(candidate);
|
|
6203
|
+
if (text) hints.add(text);
|
|
6204
|
+
}
|
|
6205
|
+
|
|
6206
|
+
function walk(node, fieldName = "") {
|
|
6207
|
+
if (node === undefined || node === null) return;
|
|
6208
|
+
if (typeof node === "string") {
|
|
6209
|
+
if (fieldName && ADDRESS_FIELDS.has(fieldName.toLowerCase())) add(node);
|
|
6210
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(node) || /^[A-Za-z]:[\\/]/.test(node) || node.startsWith("\\\\")) add(node);
|
|
6211
|
+
return;
|
|
6212
|
+
}
|
|
6213
|
+
if (Array.isArray(node)) {
|
|
6214
|
+
for (const item of node) walk(item, fieldName);
|
|
6215
|
+
return;
|
|
6216
|
+
}
|
|
6217
|
+
if (typeof node === "object") {
|
|
6218
|
+
for (const [key, child] of Object.entries(node)) walk(child, key);
|
|
6219
|
+
}
|
|
6220
|
+
}
|
|
6221
|
+
|
|
6222
|
+
try {
|
|
6223
|
+
walk(JSON.parse(value));
|
|
6224
|
+
} catch {
|
|
6225
|
+
walk(value);
|
|
6226
|
+
}
|
|
6227
|
+
|
|
6228
|
+
return [...hints];
|
|
6229
|
+
}
|
|
6230
|
+
|
|
6231
|
+
async function searchServices({ password, machineHash, token, timestamp, query, project, includeAddresses = true, whitelist }) {
|
|
6232
|
+
const q = normalizeSearchText(query);
|
|
6233
|
+
if (!q) return { error: "query is required" };
|
|
6234
|
+
|
|
6235
|
+
const result = await api.status(password, machineHash, token, timestamp, project);
|
|
6236
|
+
if (result.error) return { error: result.error };
|
|
6237
|
+
|
|
6238
|
+
let services = result.services || [];
|
|
6239
|
+
if (whitelist) services = services.filter(s => whitelist.includes(String(s.name || "").toLowerCase()));
|
|
6240
|
+
|
|
6241
|
+
const matches = [];
|
|
6242
|
+
for (const s of services) {
|
|
6243
|
+
const fields = {
|
|
6244
|
+
name: s.name,
|
|
6245
|
+
label: s.label,
|
|
6246
|
+
project: s.project,
|
|
6247
|
+
type: s.key_type,
|
|
6248
|
+
description: s.description
|
|
6249
|
+
};
|
|
6250
|
+
const matched = Object.entries(fields)
|
|
6251
|
+
.filter(([, value]) => normalizeSearchText(value).includes(q))
|
|
6252
|
+
.map(([field]) => field);
|
|
6253
|
+
|
|
6254
|
+
let addressHints = [];
|
|
6255
|
+
if (includeAddresses && ADDRESS_KEY_TYPES.has(String(s.key_type || "").toLowerCase()) && s.vault_key) {
|
|
6256
|
+
const secret = await api.retrieve(password, machineHash, token, timestamp, s.name);
|
|
6257
|
+
if (!secret.error) {
|
|
6258
|
+
addressHints = collectAddressHints(secret.value, s.key_type);
|
|
6259
|
+
if (addressHints.some(h => normalizeSearchText(h).includes(q))) matched.push("address");
|
|
6260
|
+
}
|
|
6261
|
+
}
|
|
6262
|
+
|
|
6263
|
+
if (matched.length) {
|
|
6264
|
+
matches.push({
|
|
6265
|
+
name: s.name,
|
|
6266
|
+
label: s.label || null,
|
|
6267
|
+
project: s.project || null,
|
|
6268
|
+
key_type: s.key_type,
|
|
6269
|
+
enabled: !!s.enabled,
|
|
6270
|
+
has_key: !!s.vault_key,
|
|
6271
|
+
description: s.description || null,
|
|
6272
|
+
matched: [...new Set(matched)],
|
|
6273
|
+
address_hints: addressHints
|
|
6274
|
+
});
|
|
6275
|
+
}
|
|
6276
|
+
}
|
|
6277
|
+
|
|
6278
|
+
return { query, count: matches.length, matches };
|
|
6279
|
+
}
|
|
6280
|
+
|
|
6281
|
+
// ── Filesystem service config — loaded from clauth vault ──
|
|
6282
|
+
let _fsMountsCache = null;
|
|
6283
6283
|
let _fsMountsCacheTime = 0;
|
|
6284
6284
|
const FS_CACHE_TTL = 60000; // 1 minute
|
|
6285
6285
|
|
|
@@ -6313,115 +6313,115 @@ async function getFileserverMounts(vault) {
|
|
|
6313
6313
|
}
|
|
6314
6314
|
}
|
|
6315
6315
|
|
|
6316
|
-
async function resolveInMount(requestedPath, mountName, vault) {
|
|
6316
|
+
async function resolveInMount(requestedPath, mountName, vault) {
|
|
6317
6317
|
const { mounts, error } = await getFileserverMounts(vault);
|
|
6318
6318
|
if (error) return { error };
|
|
6319
6319
|
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\"}" };
|
|
6320
6320
|
const mount = mountName ? mounts.find(m => m.name === mountName) : mounts[0];
|
|
6321
6321
|
if (!mount) return { error: `Mount '${mountName}' not found. Available: ${mounts.map(m => m.name).join(", ")}` };
|
|
6322
6322
|
if (!mount.path) return { error: `Fileserver '${mount.name}' has no path configured` };
|
|
6323
|
-
const resolved = path.resolve(mount.path, requestedPath);
|
|
6324
|
-
const normalized = path.normalize(resolved);
|
|
6325
|
-
const relative = path.relative(path.normalize(mount.path), normalized);
|
|
6326
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
6327
|
-
return { error: `Path escapes mount: ${requestedPath}` };
|
|
6328
|
-
}
|
|
6329
|
-
return { resolved: normalized, mount };
|
|
6330
|
-
}
|
|
6331
|
-
|
|
6332
|
-
function checkAccess(mount, flag) {
|
|
6333
|
-
return mount.access.includes(flag);
|
|
6334
|
-
}
|
|
6335
|
-
|
|
6336
|
-
function sha256Hex(value) {
|
|
6337
|
-
return crypto.createHash("sha256").update(value).digest("hex");
|
|
6338
|
-
}
|
|
6339
|
-
|
|
6340
|
-
async function atomicWriteText(filePath, content) {
|
|
6341
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
6342
|
-
const tempPath = path.join(
|
|
6343
|
-
path.dirname(filePath),
|
|
6344
|
-
`.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`
|
|
6345
|
-
);
|
|
6346
|
-
await writeFile(tempPath, content, "utf8");
|
|
6347
|
-
await rename(tempPath, filePath);
|
|
6348
|
-
}
|
|
6349
|
-
|
|
6350
|
-
async function fileInfo(filePath, requestedPath) {
|
|
6351
|
-
const s = await stat(filePath);
|
|
6352
|
-
const info = {
|
|
6353
|
-
path: requestedPath,
|
|
6354
|
-
type: s.isDirectory() ? "dir" : "file",
|
|
6355
|
-
size: s.size,
|
|
6356
|
-
modified: s.mtime.toISOString(),
|
|
6357
|
-
};
|
|
6358
|
-
if (s.isFile()) {
|
|
6359
|
-
const content = await readFile(filePath);
|
|
6360
|
-
info.sha256 = sha256Hex(content);
|
|
6361
|
-
}
|
|
6362
|
-
return info;
|
|
6363
|
-
}
|
|
6364
|
-
|
|
6365
|
-
const FS_UPLOAD_SESSIONS = new Map();
|
|
6366
|
-
const FS_UPLOAD_TTL_MS = 30 * 60 * 1000;
|
|
6367
|
-
const FS_MAX_CHUNKS = 500;
|
|
6368
|
-
const FS_MAX_CHUNK_BYTES = 128 * 1024;
|
|
6369
|
-
const FS_MAX_INGEST_BYTES = 25 * 1024 * 1024;
|
|
6370
|
-
const FS_GIT_IMPORT_ALLOWED_PREFIXES = [
|
|
6371
|
-
"docs/",
|
|
6372
|
-
".rdc/plans/",
|
|
6373
|
-
".rdc/guides/",
|
|
6374
|
-
".claude/context/",
|
|
6375
|
-
".claude/rules/",
|
|
6376
|
-
".rdc/relay/from-claude-ai/",
|
|
6377
|
-
];
|
|
6378
|
-
|
|
6379
|
-
function cleanupFsUploadSessions() {
|
|
6380
|
-
const cutoff = Date.now() - FS_UPLOAD_TTL_MS;
|
|
6381
|
-
for (const [id, session] of FS_UPLOAD_SESSIONS) {
|
|
6382
|
-
if (session.updatedAt < cutoff) FS_UPLOAD_SESSIONS.delete(id);
|
|
6383
|
-
}
|
|
6384
|
-
}
|
|
6385
|
-
|
|
6386
|
-
function runGit(cwd, args, opts = {}) {
|
|
6387
|
-
const res = spawnSync("git", args, {
|
|
6388
|
-
cwd,
|
|
6389
|
-
encoding: "utf8",
|
|
6390
|
-
windowsHide: true,
|
|
6391
|
-
maxBuffer: opts.maxBuffer || 10 * 1024 * 1024,
|
|
6392
|
-
});
|
|
6393
|
-
if (res.status !== 0) {
|
|
6394
|
-
const detail = (res.stderr || res.stdout || "").trim();
|
|
6395
|
-
throw new Error(`git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
|
|
6396
|
-
}
|
|
6397
|
-
return (res.stdout || "").trim();
|
|
6398
|
-
}
|
|
6399
|
-
|
|
6400
|
-
function runGitRaw(cwd, args, opts = {}) {
|
|
6401
|
-
const res = spawnSync("git", args, {
|
|
6402
|
-
cwd,
|
|
6403
|
-
encoding: "buffer",
|
|
6404
|
-
windowsHide: true,
|
|
6405
|
-
maxBuffer: opts.maxBuffer || 10 * 1024 * 1024,
|
|
6406
|
-
});
|
|
6407
|
-
if (res.status !== 0) {
|
|
6408
|
-
const detail = Buffer.concat([res.stderr || Buffer.alloc(0), res.stdout || Buffer.alloc(0)]).toString("utf8").trim();
|
|
6409
|
-
throw new Error(`git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
|
|
6410
|
-
}
|
|
6411
|
-
return res.stdout || Buffer.alloc(0);
|
|
6412
|
-
}
|
|
6413
|
-
|
|
6414
|
-
function normalizeRepoPath(p) {
|
|
6415
|
-
if (!p || typeof p !== "string") return null;
|
|
6416
|
-
const normalized = p.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
6417
|
-
const parts = normalized.split("/").filter(Boolean);
|
|
6418
|
-
if (parts.length === 0 || parts.includes("..") || path.isAbsolute(p)) return null;
|
|
6419
|
-
return parts.join("/");
|
|
6420
|
-
}
|
|
6421
|
-
|
|
6422
|
-
function isAllowedGitImportPath(p, allowedPrefixes = FS_GIT_IMPORT_ALLOWED_PREFIXES) {
|
|
6423
|
-
return allowedPrefixes.some((prefix) => p === prefix.replace(/\/$/, "") || p.startsWith(prefix));
|
|
6424
|
-
}
|
|
6323
|
+
const resolved = path.resolve(mount.path, requestedPath);
|
|
6324
|
+
const normalized = path.normalize(resolved);
|
|
6325
|
+
const relative = path.relative(path.normalize(mount.path), normalized);
|
|
6326
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
6327
|
+
return { error: `Path escapes mount: ${requestedPath}` };
|
|
6328
|
+
}
|
|
6329
|
+
return { resolved: normalized, mount };
|
|
6330
|
+
}
|
|
6331
|
+
|
|
6332
|
+
function checkAccess(mount, flag) {
|
|
6333
|
+
return mount.access.includes(flag);
|
|
6334
|
+
}
|
|
6335
|
+
|
|
6336
|
+
function sha256Hex(value) {
|
|
6337
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
6338
|
+
}
|
|
6339
|
+
|
|
6340
|
+
async function atomicWriteText(filePath, content) {
|
|
6341
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
6342
|
+
const tempPath = path.join(
|
|
6343
|
+
path.dirname(filePath),
|
|
6344
|
+
`.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`
|
|
6345
|
+
);
|
|
6346
|
+
await writeFile(tempPath, content, "utf8");
|
|
6347
|
+
await rename(tempPath, filePath);
|
|
6348
|
+
}
|
|
6349
|
+
|
|
6350
|
+
async function fileInfo(filePath, requestedPath) {
|
|
6351
|
+
const s = await stat(filePath);
|
|
6352
|
+
const info = {
|
|
6353
|
+
path: requestedPath,
|
|
6354
|
+
type: s.isDirectory() ? "dir" : "file",
|
|
6355
|
+
size: s.size,
|
|
6356
|
+
modified: s.mtime.toISOString(),
|
|
6357
|
+
};
|
|
6358
|
+
if (s.isFile()) {
|
|
6359
|
+
const content = await readFile(filePath);
|
|
6360
|
+
info.sha256 = sha256Hex(content);
|
|
6361
|
+
}
|
|
6362
|
+
return info;
|
|
6363
|
+
}
|
|
6364
|
+
|
|
6365
|
+
const FS_UPLOAD_SESSIONS = new Map();
|
|
6366
|
+
const FS_UPLOAD_TTL_MS = 30 * 60 * 1000;
|
|
6367
|
+
const FS_MAX_CHUNKS = 500;
|
|
6368
|
+
const FS_MAX_CHUNK_BYTES = 128 * 1024;
|
|
6369
|
+
const FS_MAX_INGEST_BYTES = 25 * 1024 * 1024;
|
|
6370
|
+
const FS_GIT_IMPORT_ALLOWED_PREFIXES = [
|
|
6371
|
+
"docs/",
|
|
6372
|
+
".rdc/plans/",
|
|
6373
|
+
".rdc/guides/",
|
|
6374
|
+
".claude/context/",
|
|
6375
|
+
".claude/rules/",
|
|
6376
|
+
".rdc/relay/from-claude-ai/",
|
|
6377
|
+
];
|
|
6378
|
+
|
|
6379
|
+
function cleanupFsUploadSessions() {
|
|
6380
|
+
const cutoff = Date.now() - FS_UPLOAD_TTL_MS;
|
|
6381
|
+
for (const [id, session] of FS_UPLOAD_SESSIONS) {
|
|
6382
|
+
if (session.updatedAt < cutoff) FS_UPLOAD_SESSIONS.delete(id);
|
|
6383
|
+
}
|
|
6384
|
+
}
|
|
6385
|
+
|
|
6386
|
+
function runGit(cwd, args, opts = {}) {
|
|
6387
|
+
const res = spawnSync("git", args, {
|
|
6388
|
+
cwd,
|
|
6389
|
+
encoding: "utf8",
|
|
6390
|
+
windowsHide: true,
|
|
6391
|
+
maxBuffer: opts.maxBuffer || 10 * 1024 * 1024,
|
|
6392
|
+
});
|
|
6393
|
+
if (res.status !== 0) {
|
|
6394
|
+
const detail = (res.stderr || res.stdout || "").trim();
|
|
6395
|
+
throw new Error(`git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
|
|
6396
|
+
}
|
|
6397
|
+
return (res.stdout || "").trim();
|
|
6398
|
+
}
|
|
6399
|
+
|
|
6400
|
+
function runGitRaw(cwd, args, opts = {}) {
|
|
6401
|
+
const res = spawnSync("git", args, {
|
|
6402
|
+
cwd,
|
|
6403
|
+
encoding: "buffer",
|
|
6404
|
+
windowsHide: true,
|
|
6405
|
+
maxBuffer: opts.maxBuffer || 10 * 1024 * 1024,
|
|
6406
|
+
});
|
|
6407
|
+
if (res.status !== 0) {
|
|
6408
|
+
const detail = Buffer.concat([res.stderr || Buffer.alloc(0), res.stdout || Buffer.alloc(0)]).toString("utf8").trim();
|
|
6409
|
+
throw new Error(`git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
|
|
6410
|
+
}
|
|
6411
|
+
return res.stdout || Buffer.alloc(0);
|
|
6412
|
+
}
|
|
6413
|
+
|
|
6414
|
+
function normalizeRepoPath(p) {
|
|
6415
|
+
if (!p || typeof p !== "string") return null;
|
|
6416
|
+
const normalized = p.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
6417
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
6418
|
+
if (parts.length === 0 || parts.includes("..") || path.isAbsolute(p)) return null;
|
|
6419
|
+
return parts.join("/");
|
|
6420
|
+
}
|
|
6421
|
+
|
|
6422
|
+
function isAllowedGitImportPath(p, allowedPrefixes = FS_GIT_IMPORT_ALLOWED_PREFIXES) {
|
|
6423
|
+
return allowedPrefixes.some((prefix) => p === prefix.replace(/\/$/, "") || p.startsWith(prefix));
|
|
6424
|
+
}
|
|
6425
6425
|
|
|
6426
6426
|
const MCP_TOOLS = [
|
|
6427
6427
|
{
|
|
@@ -6444,28 +6444,28 @@ const MCP_TOOLS = [
|
|
|
6444
6444
|
description: "List all services with type, enabled state, key presence, and last retrieval time",
|
|
6445
6445
|
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
6446
6446
|
},
|
|
6447
|
-
{
|
|
6448
|
-
name: "clauth_list",
|
|
6449
|
-
description: "List registered service names",
|
|
6450
|
-
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
6451
|
-
},
|
|
6452
|
-
{
|
|
6453
|
-
name: "clauth_search",
|
|
6454
|
-
description: "Search registered services by name, label, project, description, type, or redacted address hints from address-bearing secrets",
|
|
6455
|
-
inputSchema: {
|
|
6456
|
-
type: "object",
|
|
6457
|
-
properties: {
|
|
6458
|
-
query: { type: "string", description: "Search text, such as part of a service name, project, host, URL, or filesystem path" },
|
|
6459
|
-
project: { type: "string", description: "Optional project scope" },
|
|
6460
|
-
addresses: { type: "boolean", default: true, description: "Include redacted address hints from connstring/fileserver/oauth secrets" }
|
|
6461
|
-
},
|
|
6462
|
-
required: ["query"],
|
|
6463
|
-
additionalProperties: false
|
|
6464
|
-
}
|
|
6465
|
-
},
|
|
6466
|
-
{
|
|
6467
|
-
name: "clauth_get",
|
|
6468
|
-
description: "Retrieve a secret and deliver to a temp file (default), clipboard, or stdout. Temp files auto-delete after 30 seconds.",
|
|
6447
|
+
{
|
|
6448
|
+
name: "clauth_list",
|
|
6449
|
+
description: "List registered service names",
|
|
6450
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
6451
|
+
},
|
|
6452
|
+
{
|
|
6453
|
+
name: "clauth_search",
|
|
6454
|
+
description: "Search registered services by name, label, project, description, type, or redacted address hints from address-bearing secrets",
|
|
6455
|
+
inputSchema: {
|
|
6456
|
+
type: "object",
|
|
6457
|
+
properties: {
|
|
6458
|
+
query: { type: "string", description: "Search text, such as part of a service name, project, host, URL, or filesystem path" },
|
|
6459
|
+
project: { type: "string", description: "Optional project scope" },
|
|
6460
|
+
addresses: { type: "boolean", default: true, description: "Include redacted address hints from connstring/fileserver/oauth secrets" }
|
|
6461
|
+
},
|
|
6462
|
+
required: ["query"],
|
|
6463
|
+
additionalProperties: false
|
|
6464
|
+
}
|
|
6465
|
+
},
|
|
6466
|
+
{
|
|
6467
|
+
name: "clauth_get",
|
|
6468
|
+
description: "Retrieve a secret and deliver to a temp file (default), clipboard, or stdout. Temp files auto-delete after 30 seconds.",
|
|
6469
6469
|
inputSchema: {
|
|
6470
6470
|
type: "object",
|
|
6471
6471
|
properties: {
|
|
@@ -6928,9 +6928,9 @@ const MCP_TOOLS = [
|
|
|
6928
6928
|
additionalProperties: false,
|
|
6929
6929
|
},
|
|
6930
6930
|
},
|
|
6931
|
-
{
|
|
6932
|
-
name: "fs_write",
|
|
6933
|
-
description: "Write content to a file. Creates parent directories if needed. Overwrites existing file.",
|
|
6931
|
+
{
|
|
6932
|
+
name: "fs_write",
|
|
6933
|
+
description: "Write content to a file. Creates parent directories if needed. Overwrites existing file.",
|
|
6934
6934
|
inputSchema: {
|
|
6935
6935
|
type: "object",
|
|
6936
6936
|
properties: {
|
|
@@ -6939,93 +6939,93 @@ const MCP_TOOLS = [
|
|
|
6939
6939
|
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
6940
6940
|
},
|
|
6941
6941
|
required: ["path", "content"],
|
|
6942
|
-
additionalProperties: false,
|
|
6943
|
-
},
|
|
6944
|
-
},
|
|
6945
|
-
{
|
|
6946
|
-
name: "fs_stat",
|
|
6947
|
-
description: "Get file or directory metadata. Files include a SHA-256 hash for guarded edits.",
|
|
6948
|
-
inputSchema: {
|
|
6949
|
-
type: "object",
|
|
6950
|
-
properties: {
|
|
6951
|
-
path: { type: "string", description: "Relative path within mount" },
|
|
6952
|
-
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
6953
|
-
},
|
|
6954
|
-
required: ["path"],
|
|
6955
|
-
additionalProperties: false,
|
|
6956
|
-
},
|
|
6957
|
-
},
|
|
6958
|
-
{
|
|
6959
|
-
name: "fs_append",
|
|
6960
|
-
description: "Append text to a file, optionally guarded by the current file SHA-256 hash.",
|
|
6961
|
-
inputSchema: {
|
|
6962
|
-
type: "object",
|
|
6963
|
-
properties: {
|
|
6964
|
-
path: { type: "string", description: "Relative path within mount" },
|
|
6965
|
-
content: { type: "string", description: "Text to append" },
|
|
6966
|
-
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
6967
|
-
expected_sha256: { type: "string", description: "Optional current file SHA-256. If provided and the file exists, append only when it matches." },
|
|
6968
|
-
},
|
|
6969
|
-
required: ["path", "content"],
|
|
6970
|
-
additionalProperties: false,
|
|
6971
|
-
},
|
|
6972
|
-
},
|
|
6973
|
-
{
|
|
6974
|
-
name: "fs_write_chunk",
|
|
6975
|
-
description: "Stage chunked text writes and atomically publish when all chunks arrive. Use when single write arguments are too large.",
|
|
6976
|
-
inputSchema: {
|
|
6977
|
-
type: "object",
|
|
6978
|
-
properties: {
|
|
6979
|
-
upload_id: { type: "string", description: "Caller-chosen idempotency key for this file upload" },
|
|
6980
|
-
path: { type: "string", description: "Relative path within mount" },
|
|
6981
|
-
chunk_index: { type: "number", description: "Zero-based chunk index" },
|
|
6982
|
-
total_chunks: { type: "number", description: "Total chunks expected" },
|
|
6983
|
-
content: { type: "string", description: "Chunk text content" },
|
|
6984
|
-
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
6985
|
-
expected_sha256: { type: "string", description: "Optional SHA-256 of the final assembled file content" },
|
|
6986
|
-
},
|
|
6987
|
-
required: ["upload_id", "path", "chunk_index", "total_chunks", "content"],
|
|
6988
|
-
additionalProperties: false,
|
|
6989
|
-
},
|
|
6990
|
-
},
|
|
6991
|
-
{
|
|
6992
|
-
name: "fs_ingest_url",
|
|
6993
|
-
description: "Download text from an http(s) URL and atomically write it inside the mount. Useful for moving cloud files into the local filesystem.",
|
|
6994
|
-
inputSchema: {
|
|
6995
|
-
type: "object",
|
|
6996
|
-
properties: {
|
|
6997
|
-
url: { type: "string", description: "HTTPS or HTTP URL to fetch" },
|
|
6998
|
-
path: { type: "string", description: "Relative output path within mount" },
|
|
6999
|
-
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
7000
|
-
max_bytes: { type: "number", description: "Maximum download size in bytes (default 5MB, hard max 25MB)" },
|
|
7001
|
-
expected_sha256: { type: "string", description: "Optional SHA-256 of fetched content before writing" },
|
|
7002
|
-
},
|
|
7003
|
-
required: ["url", "path"],
|
|
7004
|
-
additionalProperties: false,
|
|
7005
|
-
},
|
|
7006
|
-
},
|
|
7007
|
-
{
|
|
7008
|
-
name: "fs_import_git_files",
|
|
7009
|
-
description: "Fetch a Git ref and restore only named files into the mounted repo, optionally committing just those files. Designed for Claude.ai GitHub uploads into dirty local monorepos.",
|
|
7010
|
-
inputSchema: {
|
|
7011
|
-
type: "object",
|
|
7012
|
-
properties: {
|
|
7013
|
-
remote: { type: "string", description: "Git remote name (default: origin)" },
|
|
7014
|
-
ref: { type: "string", description: "Branch, tag, or commit to fetch/restore from" },
|
|
7015
|
-
paths: { type: "array", items: { type: "string" }, description: "Repo-relative file paths to import" },
|
|
7016
|
-
mode: { type: "string", enum: ["new_only", "overwrite"], description: "new_only refuses existing local paths. overwrite restores named paths only." },
|
|
7017
|
-
commit: { type: "boolean", description: "When true, stage only imported paths and create a local commit. Never pushes." },
|
|
7018
|
-
message: { type: "string", description: "Commit subject when commit=true" },
|
|
7019
|
-
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
7020
|
-
allowed_prefixes: { type: "array", items: { type: "string" }, description: "Optional path allowlist prefixes. Defaults to docs and agent corpus paths." },
|
|
7021
|
-
},
|
|
7022
|
-
required: ["ref", "paths"],
|
|
7023
|
-
additionalProperties: false,
|
|
7024
|
-
},
|
|
7025
|
-
},
|
|
7026
|
-
{
|
|
7027
|
-
name: "fs_list",
|
|
7028
|
-
description: "List directory contents with file type, size, and modification time.",
|
|
6942
|
+
additionalProperties: false,
|
|
6943
|
+
},
|
|
6944
|
+
},
|
|
6945
|
+
{
|
|
6946
|
+
name: "fs_stat",
|
|
6947
|
+
description: "Get file or directory metadata. Files include a SHA-256 hash for guarded edits.",
|
|
6948
|
+
inputSchema: {
|
|
6949
|
+
type: "object",
|
|
6950
|
+
properties: {
|
|
6951
|
+
path: { type: "string", description: "Relative path within mount" },
|
|
6952
|
+
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
6953
|
+
},
|
|
6954
|
+
required: ["path"],
|
|
6955
|
+
additionalProperties: false,
|
|
6956
|
+
},
|
|
6957
|
+
},
|
|
6958
|
+
{
|
|
6959
|
+
name: "fs_append",
|
|
6960
|
+
description: "Append text to a file, optionally guarded by the current file SHA-256 hash.",
|
|
6961
|
+
inputSchema: {
|
|
6962
|
+
type: "object",
|
|
6963
|
+
properties: {
|
|
6964
|
+
path: { type: "string", description: "Relative path within mount" },
|
|
6965
|
+
content: { type: "string", description: "Text to append" },
|
|
6966
|
+
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
6967
|
+
expected_sha256: { type: "string", description: "Optional current file SHA-256. If provided and the file exists, append only when it matches." },
|
|
6968
|
+
},
|
|
6969
|
+
required: ["path", "content"],
|
|
6970
|
+
additionalProperties: false,
|
|
6971
|
+
},
|
|
6972
|
+
},
|
|
6973
|
+
{
|
|
6974
|
+
name: "fs_write_chunk",
|
|
6975
|
+
description: "Stage chunked text writes and atomically publish when all chunks arrive. Use when single write arguments are too large.",
|
|
6976
|
+
inputSchema: {
|
|
6977
|
+
type: "object",
|
|
6978
|
+
properties: {
|
|
6979
|
+
upload_id: { type: "string", description: "Caller-chosen idempotency key for this file upload" },
|
|
6980
|
+
path: { type: "string", description: "Relative path within mount" },
|
|
6981
|
+
chunk_index: { type: "number", description: "Zero-based chunk index" },
|
|
6982
|
+
total_chunks: { type: "number", description: "Total chunks expected" },
|
|
6983
|
+
content: { type: "string", description: "Chunk text content" },
|
|
6984
|
+
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
6985
|
+
expected_sha256: { type: "string", description: "Optional SHA-256 of the final assembled file content" },
|
|
6986
|
+
},
|
|
6987
|
+
required: ["upload_id", "path", "chunk_index", "total_chunks", "content"],
|
|
6988
|
+
additionalProperties: false,
|
|
6989
|
+
},
|
|
6990
|
+
},
|
|
6991
|
+
{
|
|
6992
|
+
name: "fs_ingest_url",
|
|
6993
|
+
description: "Download text from an http(s) URL and atomically write it inside the mount. Useful for moving cloud files into the local filesystem.",
|
|
6994
|
+
inputSchema: {
|
|
6995
|
+
type: "object",
|
|
6996
|
+
properties: {
|
|
6997
|
+
url: { type: "string", description: "HTTPS or HTTP URL to fetch" },
|
|
6998
|
+
path: { type: "string", description: "Relative output path within mount" },
|
|
6999
|
+
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
7000
|
+
max_bytes: { type: "number", description: "Maximum download size in bytes (default 5MB, hard max 25MB)" },
|
|
7001
|
+
expected_sha256: { type: "string", description: "Optional SHA-256 of fetched content before writing" },
|
|
7002
|
+
},
|
|
7003
|
+
required: ["url", "path"],
|
|
7004
|
+
additionalProperties: false,
|
|
7005
|
+
},
|
|
7006
|
+
},
|
|
7007
|
+
{
|
|
7008
|
+
name: "fs_import_git_files",
|
|
7009
|
+
description: "Fetch a Git ref and restore only named files into the mounted repo, optionally committing just those files. Designed for Claude.ai GitHub uploads into dirty local monorepos.",
|
|
7010
|
+
inputSchema: {
|
|
7011
|
+
type: "object",
|
|
7012
|
+
properties: {
|
|
7013
|
+
remote: { type: "string", description: "Git remote name (default: origin)" },
|
|
7014
|
+
ref: { type: "string", description: "Branch, tag, or commit to fetch/restore from" },
|
|
7015
|
+
paths: { type: "array", items: { type: "string" }, description: "Repo-relative file paths to import" },
|
|
7016
|
+
mode: { type: "string", enum: ["new_only", "overwrite"], description: "new_only refuses existing local paths. overwrite restores named paths only." },
|
|
7017
|
+
commit: { type: "boolean", description: "When true, stage only imported paths and create a local commit. Never pushes." },
|
|
7018
|
+
message: { type: "string", description: "Commit subject when commit=true" },
|
|
7019
|
+
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
7020
|
+
allowed_prefixes: { type: "array", items: { type: "string" }, description: "Optional path allowlist prefixes. Defaults to docs and agent corpus paths." },
|
|
7021
|
+
},
|
|
7022
|
+
required: ["ref", "paths"],
|
|
7023
|
+
additionalProperties: false,
|
|
7024
|
+
},
|
|
7025
|
+
},
|
|
7026
|
+
{
|
|
7027
|
+
name: "fs_list",
|
|
7028
|
+
description: "List directory contents with file type, size, and modification time.",
|
|
7029
7029
|
inputSchema: {
|
|
7030
7030
|
type: "object",
|
|
7031
7031
|
properties: {
|
|
@@ -7092,6 +7092,53 @@ const MCP_TOOLS = [
|
|
|
7092
7092
|
additionalProperties: false,
|
|
7093
7093
|
},
|
|
7094
7094
|
},
|
|
7095
|
+
{
|
|
7096
|
+
name: "fs_edit",
|
|
7097
|
+
description: "Replace a unique string in a file. Like Edit in Claude Code: fails if old_string is not found or matches multiple times unless replace_all=true. Atomic write. Returns new file info including sha256.",
|
|
7098
|
+
inputSchema: {
|
|
7099
|
+
type: "object",
|
|
7100
|
+
properties: {
|
|
7101
|
+
path: { type: "string", description: "Relative path within mount" },
|
|
7102
|
+
old_string: { type: "string", description: "Exact text to find (must be unique unless replace_all=true)" },
|
|
7103
|
+
new_string: { type: "string", description: "Replacement text (must differ from old_string)" },
|
|
7104
|
+
replace_all: { type: "boolean", description: "Replace every occurrence (default false)" },
|
|
7105
|
+
expected_sha256: { type: "string", description: "Optional current file SHA-256 guard. If provided and mismatched, edit is rejected." },
|
|
7106
|
+
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
7107
|
+
},
|
|
7108
|
+
required: ["path", "old_string", "new_string"],
|
|
7109
|
+
additionalProperties: false,
|
|
7110
|
+
},
|
|
7111
|
+
},
|
|
7112
|
+
{
|
|
7113
|
+
name: "fs_move",
|
|
7114
|
+
description: "Move or rename a file or directory within a mount. Single syscall when possible; falls back to copy+delete across devices. Refuses to overwrite existing destination unless overwrite=true.",
|
|
7115
|
+
inputSchema: {
|
|
7116
|
+
type: "object",
|
|
7117
|
+
properties: {
|
|
7118
|
+
from: { type: "string", description: "Source relative path within mount" },
|
|
7119
|
+
to: { type: "string", description: "Destination relative path within mount" },
|
|
7120
|
+
overwrite: { type: "boolean", description: "Overwrite destination if it exists (default false)" },
|
|
7121
|
+
mount: { type: "string", description: "Mount name for both from and to (default: first mount)" },
|
|
7122
|
+
},
|
|
7123
|
+
required: ["from", "to"],
|
|
7124
|
+
additionalProperties: false,
|
|
7125
|
+
},
|
|
7126
|
+
},
|
|
7127
|
+
{
|
|
7128
|
+
name: "fs_copy",
|
|
7129
|
+
description: "Copy a file or directory tree within a mount. Recursive by default. Refuses to overwrite existing destination unless overwrite=true.",
|
|
7130
|
+
inputSchema: {
|
|
7131
|
+
type: "object",
|
|
7132
|
+
properties: {
|
|
7133
|
+
from: { type: "string", description: "Source relative path within mount" },
|
|
7134
|
+
to: { type: "string", description: "Destination relative path within mount" },
|
|
7135
|
+
overwrite: { type: "boolean", description: "Overwrite destination if it exists (default false)" },
|
|
7136
|
+
mount: { type: "string", description: "Mount name for both from and to (default: first mount)" },
|
|
7137
|
+
},
|
|
7138
|
+
required: ["from", "to"],
|
|
7139
|
+
additionalProperties: false,
|
|
7140
|
+
},
|
|
7141
|
+
},
|
|
7095
7142
|
{
|
|
7096
7143
|
name: "fs_mounts",
|
|
7097
7144
|
description: "List configured filesystem mounts (fileserver services from vault).",
|
|
@@ -7182,10 +7229,10 @@ async function handleMcpTool(vault, name, args) {
|
|
|
7182
7229
|
}
|
|
7183
7230
|
}
|
|
7184
7231
|
|
|
7185
|
-
case "clauth_list": {
|
|
7186
|
-
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
7187
|
-
try {
|
|
7188
|
-
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
7232
|
+
case "clauth_list": {
|
|
7233
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
7234
|
+
try {
|
|
7235
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
7189
7236
|
const result = await api.status(vault.password, vault.machineHash, token, timestamp);
|
|
7190
7237
|
if (result.error) return mcpError(result.error);
|
|
7191
7238
|
let services = result.services || [];
|
|
@@ -7194,34 +7241,34 @@ async function handleMcpTool(vault, name, args) {
|
|
|
7194
7241
|
}
|
|
7195
7242
|
return mcpResult(services.map(s => s.name).join(", "));
|
|
7196
7243
|
} catch (err) {
|
|
7197
|
-
return mcpError(err.message);
|
|
7198
|
-
}
|
|
7199
|
-
}
|
|
7200
|
-
|
|
7201
|
-
case "clauth_search": {
|
|
7202
|
-
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
7203
|
-
try {
|
|
7204
|
-
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
7205
|
-
const result = await searchServices({
|
|
7206
|
-
password: vault.password,
|
|
7207
|
-
machineHash: vault.machineHash,
|
|
7208
|
-
token,
|
|
7209
|
-
timestamp,
|
|
7210
|
-
query: args.query,
|
|
7211
|
-
project: args.project,
|
|
7212
|
-
includeAddresses: args.addresses !== false,
|
|
7213
|
-
whitelist: vault.whitelist
|
|
7214
|
-
});
|
|
7215
|
-
if (result.error) return mcpError(result.error);
|
|
7216
|
-
return mcpResult(JSON.stringify(result, null, 2));
|
|
7217
|
-
} catch (err) {
|
|
7218
|
-
return mcpError(err.message);
|
|
7219
|
-
}
|
|
7220
|
-
}
|
|
7221
|
-
|
|
7222
|
-
case "clauth_get": {
|
|
7223
|
-
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
7224
|
-
const service = (args.service || "").toLowerCase();
|
|
7244
|
+
return mcpError(err.message);
|
|
7245
|
+
}
|
|
7246
|
+
}
|
|
7247
|
+
|
|
7248
|
+
case "clauth_search": {
|
|
7249
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
7250
|
+
try {
|
|
7251
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
7252
|
+
const result = await searchServices({
|
|
7253
|
+
password: vault.password,
|
|
7254
|
+
machineHash: vault.machineHash,
|
|
7255
|
+
token,
|
|
7256
|
+
timestamp,
|
|
7257
|
+
query: args.query,
|
|
7258
|
+
project: args.project,
|
|
7259
|
+
includeAddresses: args.addresses !== false,
|
|
7260
|
+
whitelist: vault.whitelist
|
|
7261
|
+
});
|
|
7262
|
+
if (result.error) return mcpError(result.error);
|
|
7263
|
+
return mcpResult(JSON.stringify(result, null, 2));
|
|
7264
|
+
} catch (err) {
|
|
7265
|
+
return mcpError(err.message);
|
|
7266
|
+
}
|
|
7267
|
+
}
|
|
7268
|
+
|
|
7269
|
+
case "clauth_get": {
|
|
7270
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
7271
|
+
const service = (args.service || "").toLowerCase();
|
|
7225
7272
|
const target = args.target || "file";
|
|
7226
7273
|
if (!service) return mcpError("service is required");
|
|
7227
7274
|
if (vault.whitelist && !vault.whitelist.includes(service)) {
|
|
@@ -7515,250 +7562,250 @@ async function handleMcpTool(vault, name, args) {
|
|
|
7515
7562
|
}
|
|
7516
7563
|
}
|
|
7517
7564
|
|
|
7518
|
-
case "fs_write": {
|
|
7519
|
-
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7520
|
-
if (r.error) return mcpError(r.error);
|
|
7521
|
-
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7522
|
-
try {
|
|
7523
|
-
await atomicWriteText(r.resolved, args.content);
|
|
7524
|
-
return mcpResult(`Written: ${args.path} (${Buffer.byteLength(args.content)} bytes)`);
|
|
7525
|
-
} catch (err) {
|
|
7526
|
-
return mcpError(`Write failed: ${err.message}`);
|
|
7527
|
-
}
|
|
7528
|
-
}
|
|
7529
|
-
|
|
7530
|
-
case "fs_stat": {
|
|
7531
|
-
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7532
|
-
if (r.error) return mcpError(r.error);
|
|
7533
|
-
if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
|
|
7534
|
-
try {
|
|
7535
|
-
return mcpResult(JSON.stringify(await fileInfo(r.resolved, args.path), null, 2));
|
|
7536
|
-
} catch (err) {
|
|
7537
|
-
if (err.code === "ENOENT") return mcpError(`Not found: ${args.path}`);
|
|
7538
|
-
return mcpError(`Stat failed: ${err.message}`);
|
|
7539
|
-
}
|
|
7540
|
-
}
|
|
7541
|
-
|
|
7542
|
-
case "fs_append": {
|
|
7543
|
-
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7544
|
-
if (r.error) return mcpError(r.error);
|
|
7545
|
-
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7546
|
-
try {
|
|
7547
|
-
try {
|
|
7548
|
-
const current = await readFile(r.resolved);
|
|
7549
|
-
if (args.expected_sha256 && sha256Hex(current) !== args.expected_sha256) {
|
|
7550
|
-
return mcpError("Append rejected: current file hash does not match expected_sha256");
|
|
7551
|
-
}
|
|
7552
|
-
} catch (err) {
|
|
7553
|
-
if (err.code !== "ENOENT") throw err;
|
|
7554
|
-
if (args.expected_sha256) return mcpError("Append rejected: file does not exist for expected_sha256 guard");
|
|
7555
|
-
}
|
|
7556
|
-
await mkdir(path.dirname(r.resolved), { recursive: true });
|
|
7557
|
-
await appendFile(r.resolved, args.content, "utf8");
|
|
7558
|
-
const info = await fileInfo(r.resolved, args.path);
|
|
7559
|
-
return mcpResult(JSON.stringify({ appended_bytes: Buffer.byteLength(args.content), ...info }, null, 2));
|
|
7560
|
-
} catch (err) {
|
|
7561
|
-
return mcpError(`Append failed: ${err.message}`);
|
|
7562
|
-
}
|
|
7563
|
-
}
|
|
7564
|
-
|
|
7565
|
-
case "fs_write_chunk": {
|
|
7566
|
-
cleanupFsUploadSessions();
|
|
7567
|
-
const { upload_id, chunk_index, total_chunks, content } = args;
|
|
7568
|
-
const index = Number(chunk_index);
|
|
7569
|
-
const total = Number(total_chunks);
|
|
7570
|
-
if (!Number.isInteger(index) || !Number.isInteger(total) || index < 0 || total < 1 || index >= total) {
|
|
7571
|
-
return mcpError("Invalid chunk_index/total_chunks");
|
|
7572
|
-
}
|
|
7573
|
-
if (total > FS_MAX_CHUNKS) return mcpError(`Too many chunks: max ${FS_MAX_CHUNKS}`);
|
|
7574
|
-
if (Buffer.byteLength(content, "utf8") > FS_MAX_CHUNK_BYTES) {
|
|
7575
|
-
return mcpError(`Chunk too large: max ${FS_MAX_CHUNK_BYTES} bytes`);
|
|
7576
|
-
}
|
|
7577
|
-
|
|
7578
|
-
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7579
|
-
if (r.error) return mcpError(r.error);
|
|
7580
|
-
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7581
|
-
|
|
7582
|
-
const key = `${r.mount.name}:${args.path}:${upload_id}`;
|
|
7583
|
-
let session = FS_UPLOAD_SESSIONS.get(key);
|
|
7584
|
-
if (!session) {
|
|
7585
|
-
session = { path: args.path, resolved: r.resolved, total, chunks: new Map(), expectedSha256: args.expected_sha256 || null, updatedAt: Date.now() };
|
|
7586
|
-
FS_UPLOAD_SESSIONS.set(key, session);
|
|
7587
|
-
}
|
|
7588
|
-
if (session.total !== total || session.path !== args.path || session.resolved !== r.resolved) {
|
|
7589
|
-
return mcpError("Upload id collision: path or total_chunks differs from existing session");
|
|
7590
|
-
}
|
|
7591
|
-
if (args.expected_sha256 && session.expectedSha256 && args.expected_sha256 !== session.expectedSha256) {
|
|
7592
|
-
return mcpError("Upload id collision: expected_sha256 differs from existing session");
|
|
7593
|
-
}
|
|
7594
|
-
|
|
7595
|
-
session.chunks.set(index, content);
|
|
7596
|
-
session.updatedAt = Date.now();
|
|
7597
|
-
|
|
7598
|
-
if (session.chunks.size < total) {
|
|
7599
|
-
return mcpResult(JSON.stringify({ upload_id, status: "staged", received_chunks: session.chunks.size, total_chunks: total }, null, 2));
|
|
7600
|
-
}
|
|
7601
|
-
|
|
7602
|
-
const assembled = Array.from({ length: total }, (_, i) => session.chunks.get(i)).join("");
|
|
7603
|
-
const actualSha = sha256Hex(assembled);
|
|
7604
|
-
if (session.expectedSha256 && actualSha !== session.expectedSha256) {
|
|
7605
|
-
FS_UPLOAD_SESSIONS.delete(key);
|
|
7606
|
-
return mcpError(`Final SHA-256 mismatch: expected ${session.expectedSha256}, got ${actualSha}`);
|
|
7607
|
-
}
|
|
7608
|
-
|
|
7609
|
-
try {
|
|
7610
|
-
await atomicWriteText(r.resolved, assembled);
|
|
7611
|
-
FS_UPLOAD_SESSIONS.delete(key);
|
|
7612
|
-
return mcpResult(JSON.stringify({ upload_id, status: "written", path: args.path, bytes: Buffer.byteLength(assembled), sha256: actualSha }, null, 2));
|
|
7613
|
-
} catch (err) {
|
|
7614
|
-
return mcpError(`Chunked write failed: ${err.message}`);
|
|
7615
|
-
}
|
|
7616
|
-
}
|
|
7617
|
-
|
|
7618
|
-
case "fs_ingest_url": {
|
|
7619
|
-
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7620
|
-
if (r.error) return mcpError(r.error);
|
|
7621
|
-
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7622
|
-
|
|
7623
|
-
let url;
|
|
7624
|
-
try {
|
|
7625
|
-
url = new URL(args.url);
|
|
7626
|
-
} catch {
|
|
7627
|
-
return mcpError("Invalid URL");
|
|
7628
|
-
}
|
|
7629
|
-
if (!["http:", "https:"].includes(url.protocol)) return mcpError("Only http(s) URLs are supported");
|
|
7630
|
-
|
|
7631
|
-
const maxBytes = Math.min(Number(args.max_bytes || 5 * 1024 * 1024), FS_MAX_INGEST_BYTES);
|
|
7632
|
-
try {
|
|
7633
|
-
const response = await fetch(url, { redirect: "follow" });
|
|
7634
|
-
if (!response.ok) return mcpError(`Fetch failed: HTTP ${response.status}`);
|
|
7635
|
-
const length = Number(response.headers.get("content-length") || 0);
|
|
7636
|
-
if (length && length > maxBytes) return mcpError(`Fetch rejected: content-length ${length} exceeds max_bytes ${maxBytes}`);
|
|
7637
|
-
|
|
7638
|
-
const reader = response.body?.getReader();
|
|
7639
|
-
if (!reader) return mcpError("Fetch failed: response body is not readable");
|
|
7640
|
-
|
|
7641
|
-
let received = 0;
|
|
7642
|
-
const chunks = [];
|
|
7643
|
-
while (true) {
|
|
7644
|
-
const { done, value } = await reader.read();
|
|
7645
|
-
if (done) break;
|
|
7646
|
-
received += value.byteLength;
|
|
7647
|
-
if (received > maxBytes) return mcpError(`Fetch rejected: response exceeds max_bytes ${maxBytes}`);
|
|
7648
|
-
chunks.push(Buffer.from(value));
|
|
7649
|
-
}
|
|
7650
|
-
|
|
7651
|
-
const content = Buffer.concat(chunks).toString("utf8");
|
|
7652
|
-
const actualSha = sha256Hex(content);
|
|
7653
|
-
if (args.expected_sha256 && actualSha !== args.expected_sha256) {
|
|
7654
|
-
return mcpError(`Fetched SHA-256 mismatch: expected ${args.expected_sha256}, got ${actualSha}`);
|
|
7655
|
-
}
|
|
7656
|
-
await atomicWriteText(r.resolved, content);
|
|
7657
|
-
return mcpResult(JSON.stringify({ status: "written", path: args.path, bytes: Buffer.byteLength(content), sha256: actualSha, source: url.href }, null, 2));
|
|
7658
|
-
} catch (err) {
|
|
7659
|
-
return mcpError(`Ingest failed: ${err.message}`);
|
|
7660
|
-
}
|
|
7661
|
-
}
|
|
7662
|
-
|
|
7663
|
-
case "fs_import_git_files": {
|
|
7664
|
-
if (!Array.isArray(args.paths) || args.paths.length === 0) return mcpError("paths must be a non-empty array");
|
|
7665
|
-
if (args.paths.length > 25) return mcpError("Too many paths: max 25 per import");
|
|
7666
|
-
|
|
7667
|
-
const r = await resolveInMount(".", args.mount, vault);
|
|
7668
|
-
if (r.error) return mcpError(r.error);
|
|
7669
|
-
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7670
|
-
|
|
7671
|
-
const repoRoot = r.resolved;
|
|
7672
|
-
const remote = args.remote || "origin";
|
|
7673
|
-
const mode = args.mode || "new_only";
|
|
7674
|
-
const doCommit = args.commit === true;
|
|
7675
|
-
const allowedPrefixes = Array.isArray(args.allowed_prefixes) && args.allowed_prefixes.length > 0
|
|
7676
|
-
? args.allowed_prefixes.map((p) => normalizeRepoPath(p.endsWith("/") ? p : `${p}/`)).filter(Boolean)
|
|
7677
|
-
: FS_GIT_IMPORT_ALLOWED_PREFIXES;
|
|
7678
|
-
|
|
7679
|
-
try {
|
|
7680
|
-
const topLevel = path.normalize(runGit(repoRoot, ["rev-parse", "--show-toplevel"]));
|
|
7681
|
-
if (topLevel.toLowerCase() !== path.normalize(repoRoot).toLowerCase()) {
|
|
7682
|
-
return mcpError(`Mount root is not the git repo root: ${repoRoot} (repo root: ${topLevel})`);
|
|
7683
|
-
}
|
|
7684
|
-
|
|
7685
|
-
const normalizedPaths = [];
|
|
7686
|
-
for (const rawPath of args.paths) {
|
|
7687
|
-
const normalized = normalizeRepoPath(rawPath);
|
|
7688
|
-
if (!normalized) return mcpError(`Invalid repo path: ${rawPath}`);
|
|
7689
|
-
if (!isAllowedGitImportPath(normalized, allowedPrefixes)) return mcpError(`Path not allowed for git import: ${normalized}`);
|
|
7690
|
-
normalizedPaths.push(normalized);
|
|
7691
|
-
}
|
|
7692
|
-
|
|
7693
|
-
if (mode === "new_only") {
|
|
7694
|
-
for (const rel of normalizedPaths) {
|
|
7695
|
-
const localPath = path.join(repoRoot, rel);
|
|
7696
|
-
try {
|
|
7697
|
-
await stat(localPath);
|
|
7698
|
-
return mcpError(`Import refused: local path already exists in new_only mode: ${rel}`);
|
|
7699
|
-
} catch (err) {
|
|
7700
|
-
if (err.code !== "ENOENT") throw err;
|
|
7701
|
-
}
|
|
7702
|
-
}
|
|
7703
|
-
}
|
|
7704
|
-
|
|
7705
|
-
if (doCommit) {
|
|
7706
|
-
const staged = runGit(repoRoot, ["diff", "--cached", "--name-only"]);
|
|
7707
|
-
if (staged) return mcpError(`Import refused: index already has staged files:\n${staged}`);
|
|
7708
|
-
if (!args.message || !args.message.trim()) return mcpError("message is required when commit=true");
|
|
7709
|
-
}
|
|
7710
|
-
|
|
7711
|
-
runGit(repoRoot, ["fetch", "--no-tags", remote, args.ref]);
|
|
7712
|
-
const sourceCommit = runGit(repoRoot, ["rev-parse", "FETCH_HEAD"]);
|
|
7713
|
-
|
|
7714
|
-
for (const rel of normalizedPaths) {
|
|
7715
|
-
runGit(repoRoot, ["cat-file", "-e", `${sourceCommit}:${rel}`]);
|
|
7716
|
-
}
|
|
7717
|
-
|
|
7718
|
-
runGit(repoRoot, ["restore", `--source=${sourceCommit}`, "--", ...normalizedPaths]);
|
|
7719
|
-
|
|
7720
|
-
const imported = [];
|
|
7721
|
-
for (const rel of normalizedPaths) {
|
|
7722
|
-
const localPath = path.join(repoRoot, rel);
|
|
7723
|
-
const info = await fileInfo(localPath, rel);
|
|
7724
|
-
const sourceBlob = runGit(repoRoot, ["rev-parse", `${sourceCommit}:${rel}`]);
|
|
7725
|
-
const sourceSize = Number(runGitRaw(repoRoot, ["cat-file", "-s", `${sourceCommit}:${rel}`]).toString("utf8").trim());
|
|
7726
|
-
imported.push({ ...info, source_blob: sourceBlob, source_size: sourceSize });
|
|
7727
|
-
}
|
|
7728
|
-
|
|
7729
|
-
let localCommit = null;
|
|
7730
|
-
if (doCommit) {
|
|
7731
|
-
runGit(repoRoot, ["add", "--", ...normalizedPaths]);
|
|
7732
|
-
const body = [
|
|
7733
|
-
args.message.trim(),
|
|
7734
|
-
"",
|
|
7735
|
-
"Imported from Claude.ai GitHub upload.",
|
|
7736
|
-
"",
|
|
7737
|
-
`Source remote: ${remote}`,
|
|
7738
|
-
`Source ref: ${args.ref}`,
|
|
7739
|
-
`Source commit: ${sourceCommit}`,
|
|
7740
|
-
"",
|
|
7741
|
-
"Paths:",
|
|
7742
|
-
...normalizedPaths.map((p) => `- ${p}`),
|
|
7743
|
-
].join("\n");
|
|
7744
|
-
runGit(repoRoot, ["commit", "-m", body]);
|
|
7745
|
-
localCommit = runGit(repoRoot, ["rev-parse", "HEAD"]);
|
|
7746
|
-
}
|
|
7747
|
-
|
|
7748
|
-
return mcpResult(JSON.stringify({
|
|
7749
|
-
status: "ok",
|
|
7750
|
-
mode,
|
|
7751
|
-
committed: doCommit,
|
|
7752
|
-
source_commit: sourceCommit,
|
|
7753
|
-
local_commit: localCommit,
|
|
7754
|
-
imported,
|
|
7755
|
-
}, null, 2));
|
|
7756
|
-
} catch (err) {
|
|
7757
|
-
return mcpError(`Git import failed: ${err.message}`);
|
|
7758
|
-
}
|
|
7759
|
-
}
|
|
7760
|
-
|
|
7761
|
-
case "fs_list": {
|
|
7565
|
+
case "fs_write": {
|
|
7566
|
+
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7567
|
+
if (r.error) return mcpError(r.error);
|
|
7568
|
+
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7569
|
+
try {
|
|
7570
|
+
await atomicWriteText(r.resolved, args.content);
|
|
7571
|
+
return mcpResult(`Written: ${args.path} (${Buffer.byteLength(args.content)} bytes)`);
|
|
7572
|
+
} catch (err) {
|
|
7573
|
+
return mcpError(`Write failed: ${err.message}`);
|
|
7574
|
+
}
|
|
7575
|
+
}
|
|
7576
|
+
|
|
7577
|
+
case "fs_stat": {
|
|
7578
|
+
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7579
|
+
if (r.error) return mcpError(r.error);
|
|
7580
|
+
if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
|
|
7581
|
+
try {
|
|
7582
|
+
return mcpResult(JSON.stringify(await fileInfo(r.resolved, args.path), null, 2));
|
|
7583
|
+
} catch (err) {
|
|
7584
|
+
if (err.code === "ENOENT") return mcpError(`Not found: ${args.path}`);
|
|
7585
|
+
return mcpError(`Stat failed: ${err.message}`);
|
|
7586
|
+
}
|
|
7587
|
+
}
|
|
7588
|
+
|
|
7589
|
+
case "fs_append": {
|
|
7590
|
+
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7591
|
+
if (r.error) return mcpError(r.error);
|
|
7592
|
+
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7593
|
+
try {
|
|
7594
|
+
try {
|
|
7595
|
+
const current = await readFile(r.resolved);
|
|
7596
|
+
if (args.expected_sha256 && sha256Hex(current) !== args.expected_sha256) {
|
|
7597
|
+
return mcpError("Append rejected: current file hash does not match expected_sha256");
|
|
7598
|
+
}
|
|
7599
|
+
} catch (err) {
|
|
7600
|
+
if (err.code !== "ENOENT") throw err;
|
|
7601
|
+
if (args.expected_sha256) return mcpError("Append rejected: file does not exist for expected_sha256 guard");
|
|
7602
|
+
}
|
|
7603
|
+
await mkdir(path.dirname(r.resolved), { recursive: true });
|
|
7604
|
+
await appendFile(r.resolved, args.content, "utf8");
|
|
7605
|
+
const info = await fileInfo(r.resolved, args.path);
|
|
7606
|
+
return mcpResult(JSON.stringify({ appended_bytes: Buffer.byteLength(args.content), ...info }, null, 2));
|
|
7607
|
+
} catch (err) {
|
|
7608
|
+
return mcpError(`Append failed: ${err.message}`);
|
|
7609
|
+
}
|
|
7610
|
+
}
|
|
7611
|
+
|
|
7612
|
+
case "fs_write_chunk": {
|
|
7613
|
+
cleanupFsUploadSessions();
|
|
7614
|
+
const { upload_id, chunk_index, total_chunks, content } = args;
|
|
7615
|
+
const index = Number(chunk_index);
|
|
7616
|
+
const total = Number(total_chunks);
|
|
7617
|
+
if (!Number.isInteger(index) || !Number.isInteger(total) || index < 0 || total < 1 || index >= total) {
|
|
7618
|
+
return mcpError("Invalid chunk_index/total_chunks");
|
|
7619
|
+
}
|
|
7620
|
+
if (total > FS_MAX_CHUNKS) return mcpError(`Too many chunks: max ${FS_MAX_CHUNKS}`);
|
|
7621
|
+
if (Buffer.byteLength(content, "utf8") > FS_MAX_CHUNK_BYTES) {
|
|
7622
|
+
return mcpError(`Chunk too large: max ${FS_MAX_CHUNK_BYTES} bytes`);
|
|
7623
|
+
}
|
|
7624
|
+
|
|
7625
|
+
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7626
|
+
if (r.error) return mcpError(r.error);
|
|
7627
|
+
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7628
|
+
|
|
7629
|
+
const key = `${r.mount.name}:${args.path}:${upload_id}`;
|
|
7630
|
+
let session = FS_UPLOAD_SESSIONS.get(key);
|
|
7631
|
+
if (!session) {
|
|
7632
|
+
session = { path: args.path, resolved: r.resolved, total, chunks: new Map(), expectedSha256: args.expected_sha256 || null, updatedAt: Date.now() };
|
|
7633
|
+
FS_UPLOAD_SESSIONS.set(key, session);
|
|
7634
|
+
}
|
|
7635
|
+
if (session.total !== total || session.path !== args.path || session.resolved !== r.resolved) {
|
|
7636
|
+
return mcpError("Upload id collision: path or total_chunks differs from existing session");
|
|
7637
|
+
}
|
|
7638
|
+
if (args.expected_sha256 && session.expectedSha256 && args.expected_sha256 !== session.expectedSha256) {
|
|
7639
|
+
return mcpError("Upload id collision: expected_sha256 differs from existing session");
|
|
7640
|
+
}
|
|
7641
|
+
|
|
7642
|
+
session.chunks.set(index, content);
|
|
7643
|
+
session.updatedAt = Date.now();
|
|
7644
|
+
|
|
7645
|
+
if (session.chunks.size < total) {
|
|
7646
|
+
return mcpResult(JSON.stringify({ upload_id, status: "staged", received_chunks: session.chunks.size, total_chunks: total }, null, 2));
|
|
7647
|
+
}
|
|
7648
|
+
|
|
7649
|
+
const assembled = Array.from({ length: total }, (_, i) => session.chunks.get(i)).join("");
|
|
7650
|
+
const actualSha = sha256Hex(assembled);
|
|
7651
|
+
if (session.expectedSha256 && actualSha !== session.expectedSha256) {
|
|
7652
|
+
FS_UPLOAD_SESSIONS.delete(key);
|
|
7653
|
+
return mcpError(`Final SHA-256 mismatch: expected ${session.expectedSha256}, got ${actualSha}`);
|
|
7654
|
+
}
|
|
7655
|
+
|
|
7656
|
+
try {
|
|
7657
|
+
await atomicWriteText(r.resolved, assembled);
|
|
7658
|
+
FS_UPLOAD_SESSIONS.delete(key);
|
|
7659
|
+
return mcpResult(JSON.stringify({ upload_id, status: "written", path: args.path, bytes: Buffer.byteLength(assembled), sha256: actualSha }, null, 2));
|
|
7660
|
+
} catch (err) {
|
|
7661
|
+
return mcpError(`Chunked write failed: ${err.message}`);
|
|
7662
|
+
}
|
|
7663
|
+
}
|
|
7664
|
+
|
|
7665
|
+
case "fs_ingest_url": {
|
|
7666
|
+
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7667
|
+
if (r.error) return mcpError(r.error);
|
|
7668
|
+
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7669
|
+
|
|
7670
|
+
let url;
|
|
7671
|
+
try {
|
|
7672
|
+
url = new URL(args.url);
|
|
7673
|
+
} catch {
|
|
7674
|
+
return mcpError("Invalid URL");
|
|
7675
|
+
}
|
|
7676
|
+
if (!["http:", "https:"].includes(url.protocol)) return mcpError("Only http(s) URLs are supported");
|
|
7677
|
+
|
|
7678
|
+
const maxBytes = Math.min(Number(args.max_bytes || 5 * 1024 * 1024), FS_MAX_INGEST_BYTES);
|
|
7679
|
+
try {
|
|
7680
|
+
const response = await fetch(url, { redirect: "follow" });
|
|
7681
|
+
if (!response.ok) return mcpError(`Fetch failed: HTTP ${response.status}`);
|
|
7682
|
+
const length = Number(response.headers.get("content-length") || 0);
|
|
7683
|
+
if (length && length > maxBytes) return mcpError(`Fetch rejected: content-length ${length} exceeds max_bytes ${maxBytes}`);
|
|
7684
|
+
|
|
7685
|
+
const reader = response.body?.getReader();
|
|
7686
|
+
if (!reader) return mcpError("Fetch failed: response body is not readable");
|
|
7687
|
+
|
|
7688
|
+
let received = 0;
|
|
7689
|
+
const chunks = [];
|
|
7690
|
+
while (true) {
|
|
7691
|
+
const { done, value } = await reader.read();
|
|
7692
|
+
if (done) break;
|
|
7693
|
+
received += value.byteLength;
|
|
7694
|
+
if (received > maxBytes) return mcpError(`Fetch rejected: response exceeds max_bytes ${maxBytes}`);
|
|
7695
|
+
chunks.push(Buffer.from(value));
|
|
7696
|
+
}
|
|
7697
|
+
|
|
7698
|
+
const content = Buffer.concat(chunks).toString("utf8");
|
|
7699
|
+
const actualSha = sha256Hex(content);
|
|
7700
|
+
if (args.expected_sha256 && actualSha !== args.expected_sha256) {
|
|
7701
|
+
return mcpError(`Fetched SHA-256 mismatch: expected ${args.expected_sha256}, got ${actualSha}`);
|
|
7702
|
+
}
|
|
7703
|
+
await atomicWriteText(r.resolved, content);
|
|
7704
|
+
return mcpResult(JSON.stringify({ status: "written", path: args.path, bytes: Buffer.byteLength(content), sha256: actualSha, source: url.href }, null, 2));
|
|
7705
|
+
} catch (err) {
|
|
7706
|
+
return mcpError(`Ingest failed: ${err.message}`);
|
|
7707
|
+
}
|
|
7708
|
+
}
|
|
7709
|
+
|
|
7710
|
+
case "fs_import_git_files": {
|
|
7711
|
+
if (!Array.isArray(args.paths) || args.paths.length === 0) return mcpError("paths must be a non-empty array");
|
|
7712
|
+
if (args.paths.length > 25) return mcpError("Too many paths: max 25 per import");
|
|
7713
|
+
|
|
7714
|
+
const r = await resolveInMount(".", args.mount, vault);
|
|
7715
|
+
if (r.error) return mcpError(r.error);
|
|
7716
|
+
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7717
|
+
|
|
7718
|
+
const repoRoot = r.resolved;
|
|
7719
|
+
const remote = args.remote || "origin";
|
|
7720
|
+
const mode = args.mode || "new_only";
|
|
7721
|
+
const doCommit = args.commit === true;
|
|
7722
|
+
const allowedPrefixes = Array.isArray(args.allowed_prefixes) && args.allowed_prefixes.length > 0
|
|
7723
|
+
? args.allowed_prefixes.map((p) => normalizeRepoPath(p.endsWith("/") ? p : `${p}/`)).filter(Boolean)
|
|
7724
|
+
: FS_GIT_IMPORT_ALLOWED_PREFIXES;
|
|
7725
|
+
|
|
7726
|
+
try {
|
|
7727
|
+
const topLevel = path.normalize(runGit(repoRoot, ["rev-parse", "--show-toplevel"]));
|
|
7728
|
+
if (topLevel.toLowerCase() !== path.normalize(repoRoot).toLowerCase()) {
|
|
7729
|
+
return mcpError(`Mount root is not the git repo root: ${repoRoot} (repo root: ${topLevel})`);
|
|
7730
|
+
}
|
|
7731
|
+
|
|
7732
|
+
const normalizedPaths = [];
|
|
7733
|
+
for (const rawPath of args.paths) {
|
|
7734
|
+
const normalized = normalizeRepoPath(rawPath);
|
|
7735
|
+
if (!normalized) return mcpError(`Invalid repo path: ${rawPath}`);
|
|
7736
|
+
if (!isAllowedGitImportPath(normalized, allowedPrefixes)) return mcpError(`Path not allowed for git import: ${normalized}`);
|
|
7737
|
+
normalizedPaths.push(normalized);
|
|
7738
|
+
}
|
|
7739
|
+
|
|
7740
|
+
if (mode === "new_only") {
|
|
7741
|
+
for (const rel of normalizedPaths) {
|
|
7742
|
+
const localPath = path.join(repoRoot, rel);
|
|
7743
|
+
try {
|
|
7744
|
+
await stat(localPath);
|
|
7745
|
+
return mcpError(`Import refused: local path already exists in new_only mode: ${rel}`);
|
|
7746
|
+
} catch (err) {
|
|
7747
|
+
if (err.code !== "ENOENT") throw err;
|
|
7748
|
+
}
|
|
7749
|
+
}
|
|
7750
|
+
}
|
|
7751
|
+
|
|
7752
|
+
if (doCommit) {
|
|
7753
|
+
const staged = runGit(repoRoot, ["diff", "--cached", "--name-only"]);
|
|
7754
|
+
if (staged) return mcpError(`Import refused: index already has staged files:\n${staged}`);
|
|
7755
|
+
if (!args.message || !args.message.trim()) return mcpError("message is required when commit=true");
|
|
7756
|
+
}
|
|
7757
|
+
|
|
7758
|
+
runGit(repoRoot, ["fetch", "--no-tags", remote, args.ref]);
|
|
7759
|
+
const sourceCommit = runGit(repoRoot, ["rev-parse", "FETCH_HEAD"]);
|
|
7760
|
+
|
|
7761
|
+
for (const rel of normalizedPaths) {
|
|
7762
|
+
runGit(repoRoot, ["cat-file", "-e", `${sourceCommit}:${rel}`]);
|
|
7763
|
+
}
|
|
7764
|
+
|
|
7765
|
+
runGit(repoRoot, ["restore", `--source=${sourceCommit}`, "--", ...normalizedPaths]);
|
|
7766
|
+
|
|
7767
|
+
const imported = [];
|
|
7768
|
+
for (const rel of normalizedPaths) {
|
|
7769
|
+
const localPath = path.join(repoRoot, rel);
|
|
7770
|
+
const info = await fileInfo(localPath, rel);
|
|
7771
|
+
const sourceBlob = runGit(repoRoot, ["rev-parse", `${sourceCommit}:${rel}`]);
|
|
7772
|
+
const sourceSize = Number(runGitRaw(repoRoot, ["cat-file", "-s", `${sourceCommit}:${rel}`]).toString("utf8").trim());
|
|
7773
|
+
imported.push({ ...info, source_blob: sourceBlob, source_size: sourceSize });
|
|
7774
|
+
}
|
|
7775
|
+
|
|
7776
|
+
let localCommit = null;
|
|
7777
|
+
if (doCommit) {
|
|
7778
|
+
runGit(repoRoot, ["add", "--", ...normalizedPaths]);
|
|
7779
|
+
const body = [
|
|
7780
|
+
args.message.trim(),
|
|
7781
|
+
"",
|
|
7782
|
+
"Imported from Claude.ai GitHub upload.",
|
|
7783
|
+
"",
|
|
7784
|
+
`Source remote: ${remote}`,
|
|
7785
|
+
`Source ref: ${args.ref}`,
|
|
7786
|
+
`Source commit: ${sourceCommit}`,
|
|
7787
|
+
"",
|
|
7788
|
+
"Paths:",
|
|
7789
|
+
...normalizedPaths.map((p) => `- ${p}`),
|
|
7790
|
+
].join("\n");
|
|
7791
|
+
runGit(repoRoot, ["commit", "-m", body]);
|
|
7792
|
+
localCommit = runGit(repoRoot, ["rev-parse", "HEAD"]);
|
|
7793
|
+
}
|
|
7794
|
+
|
|
7795
|
+
return mcpResult(JSON.stringify({
|
|
7796
|
+
status: "ok",
|
|
7797
|
+
mode,
|
|
7798
|
+
committed: doCommit,
|
|
7799
|
+
source_commit: sourceCommit,
|
|
7800
|
+
local_commit: localCommit,
|
|
7801
|
+
imported,
|
|
7802
|
+
}, null, 2));
|
|
7803
|
+
} catch (err) {
|
|
7804
|
+
return mcpError(`Git import failed: ${err.message}`);
|
|
7805
|
+
}
|
|
7806
|
+
}
|
|
7807
|
+
|
|
7808
|
+
case "fs_list": {
|
|
7762
7809
|
const dirPath = args.path || ".";
|
|
7763
7810
|
const r = await resolveInMount(dirPath, args.mount, vault);
|
|
7764
7811
|
if (r.error) return mcpError(r.error);
|
|
@@ -7881,6 +7928,133 @@ async function handleMcpTool(vault, name, args) {
|
|
|
7881
7928
|
}
|
|
7882
7929
|
}
|
|
7883
7930
|
|
|
7931
|
+
case "fs_edit": {
|
|
7932
|
+
if (typeof args.old_string !== "string" || typeof args.new_string !== "string") {
|
|
7933
|
+
return mcpError("old_string and new_string must be strings");
|
|
7934
|
+
}
|
|
7935
|
+
if (args.old_string === args.new_string) {
|
|
7936
|
+
return mcpError("old_string and new_string must differ");
|
|
7937
|
+
}
|
|
7938
|
+
if (args.old_string.length === 0) {
|
|
7939
|
+
return mcpError("old_string must not be empty");
|
|
7940
|
+
}
|
|
7941
|
+
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7942
|
+
if (r.error) return mcpError(r.error);
|
|
7943
|
+
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7944
|
+
try {
|
|
7945
|
+
const current = await readFile(r.resolved, "utf8");
|
|
7946
|
+
if (args.expected_sha256 && sha256Hex(current) !== args.expected_sha256) {
|
|
7947
|
+
return mcpError("Edit rejected: current file hash does not match expected_sha256");
|
|
7948
|
+
}
|
|
7949
|
+
const parts = current.split(args.old_string);
|
|
7950
|
+
const occurrences = parts.length - 1;
|
|
7951
|
+
if (occurrences === 0) {
|
|
7952
|
+
return mcpError(`Edit failed: old_string not found in ${args.path}`);
|
|
7953
|
+
}
|
|
7954
|
+
if (occurrences > 1 && !args.replace_all) {
|
|
7955
|
+
return mcpError(`Edit failed: old_string is not unique (${occurrences} matches in ${args.path}). Set replace_all=true to replace all, or provide more surrounding context.`);
|
|
7956
|
+
}
|
|
7957
|
+
const updated = args.replace_all
|
|
7958
|
+
? parts.join(args.new_string)
|
|
7959
|
+
: current.replace(args.old_string, args.new_string);
|
|
7960
|
+
await atomicWriteText(r.resolved, updated);
|
|
7961
|
+
const info = await fileInfo(r.resolved, args.path);
|
|
7962
|
+
return mcpResult(JSON.stringify({ replacements: args.replace_all ? occurrences : 1, ...info }, null, 2));
|
|
7963
|
+
} catch (err) {
|
|
7964
|
+
if (err.code === "ENOENT") return mcpError(`File not found: ${args.path}`);
|
|
7965
|
+
return mcpError(`Edit failed: ${err.message}`);
|
|
7966
|
+
}
|
|
7967
|
+
}
|
|
7968
|
+
|
|
7969
|
+
case "fs_move": {
|
|
7970
|
+
const src = await resolveInMount(args.from, args.mount, vault);
|
|
7971
|
+
if (src.error) return mcpError(src.error);
|
|
7972
|
+
const dst = await resolveInMount(args.to, args.mount, vault);
|
|
7973
|
+
if (dst.error) return mcpError(dst.error);
|
|
7974
|
+
if (!checkAccess(src.mount, "w") || !checkAccess(src.mount, "d")) {
|
|
7975
|
+
return mcpError("Move requires write+delete access on this mount");
|
|
7976
|
+
}
|
|
7977
|
+
try {
|
|
7978
|
+
await stat(src.resolved);
|
|
7979
|
+
} catch (err) {
|
|
7980
|
+
if (err.code === "ENOENT") return mcpError(`Source not found: ${args.from}`);
|
|
7981
|
+
return mcpError(`Stat failed: ${err.message}`);
|
|
7982
|
+
}
|
|
7983
|
+
let dstExists = false;
|
|
7984
|
+
try {
|
|
7985
|
+
await stat(dst.resolved);
|
|
7986
|
+
dstExists = true;
|
|
7987
|
+
} catch (err) {
|
|
7988
|
+
if (err.code !== "ENOENT") return mcpError(`Stat failed: ${err.message}`);
|
|
7989
|
+
}
|
|
7990
|
+
if (dstExists && !args.overwrite) {
|
|
7991
|
+
return mcpError(`Move refused: destination already exists: ${args.to} (use overwrite=true)`);
|
|
7992
|
+
}
|
|
7993
|
+
try {
|
|
7994
|
+
await mkdir(path.dirname(dst.resolved), { recursive: true });
|
|
7995
|
+
if (dstExists && args.overwrite) {
|
|
7996
|
+
const dstStat = await stat(dst.resolved);
|
|
7997
|
+
await rm(dst.resolved, { recursive: dstStat.isDirectory(), force: true });
|
|
7998
|
+
}
|
|
7999
|
+
try {
|
|
8000
|
+
await rename(src.resolved, dst.resolved);
|
|
8001
|
+
} catch (err) {
|
|
8002
|
+
if (err.code === "EXDEV") {
|
|
8003
|
+
await cp(src.resolved, dst.resolved, { recursive: true, errorOnExist: false, force: true });
|
|
8004
|
+
const srcStat = await stat(src.resolved);
|
|
8005
|
+
await rm(src.resolved, { recursive: srcStat.isDirectory(), force: true });
|
|
8006
|
+
} else {
|
|
8007
|
+
throw err;
|
|
8008
|
+
}
|
|
8009
|
+
}
|
|
8010
|
+
return mcpResult(JSON.stringify({ status: "moved", from: args.from, to: args.to }, null, 2));
|
|
8011
|
+
} catch (err) {
|
|
8012
|
+
return mcpError(`Move failed: ${err.message}`);
|
|
8013
|
+
}
|
|
8014
|
+
}
|
|
8015
|
+
|
|
8016
|
+
case "fs_copy": {
|
|
8017
|
+
const src = await resolveInMount(args.from, args.mount, vault);
|
|
8018
|
+
if (src.error) return mcpError(src.error);
|
|
8019
|
+
const dst = await resolveInMount(args.to, args.mount, vault);
|
|
8020
|
+
if (dst.error) return mcpError(dst.error);
|
|
8021
|
+
if (!checkAccess(src.mount, "r")) return mcpError("Read access denied on this mount");
|
|
8022
|
+
if (!checkAccess(src.mount, "w")) return mcpError("Write access denied on this mount");
|
|
8023
|
+
let srcStat;
|
|
8024
|
+
try {
|
|
8025
|
+
srcStat = await stat(src.resolved);
|
|
8026
|
+
} catch (err) {
|
|
8027
|
+
if (err.code === "ENOENT") return mcpError(`Source not found: ${args.from}`);
|
|
8028
|
+
return mcpError(`Stat failed: ${err.message}`);
|
|
8029
|
+
}
|
|
8030
|
+
let dstExists = false;
|
|
8031
|
+
try {
|
|
8032
|
+
await stat(dst.resolved);
|
|
8033
|
+
dstExists = true;
|
|
8034
|
+
} catch (err) {
|
|
8035
|
+
if (err.code !== "ENOENT") return mcpError(`Stat failed: ${err.message}`);
|
|
8036
|
+
}
|
|
8037
|
+
if (dstExists && !args.overwrite) {
|
|
8038
|
+
return mcpError(`Copy refused: destination already exists: ${args.to} (use overwrite=true)`);
|
|
8039
|
+
}
|
|
8040
|
+
try {
|
|
8041
|
+
await mkdir(path.dirname(dst.resolved), { recursive: true });
|
|
8042
|
+
await cp(src.resolved, dst.resolved, {
|
|
8043
|
+
recursive: true,
|
|
8044
|
+
errorOnExist: false,
|
|
8045
|
+
force: !!args.overwrite,
|
|
8046
|
+
});
|
|
8047
|
+
return mcpResult(JSON.stringify({
|
|
8048
|
+
status: "copied",
|
|
8049
|
+
from: args.from,
|
|
8050
|
+
to: args.to,
|
|
8051
|
+
type: srcStat.isDirectory() ? "dir" : "file",
|
|
8052
|
+
}, null, 2));
|
|
8053
|
+
} catch (err) {
|
|
8054
|
+
return mcpError(`Copy failed: ${err.message}`);
|
|
8055
|
+
}
|
|
8056
|
+
}
|
|
8057
|
+
|
|
7884
8058
|
case "fs_mounts": {
|
|
7885
8059
|
const { mounts, error } = await getFileserverMounts(vault);
|
|
7886
8060
|
if (error) return mcpError(error);
|