@lifeaitools/clauth 1.5.80 → 1.5.82
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/README.md +13 -11
- package/cli/commands/serve.js +470 -62
- package/package.json +61 -61
package/README.md
CHANGED
|
@@ -123,7 +123,7 @@ Full daemon operations reference: see `regen-root/.claude/rules/clauth.md`.
|
|
|
123
123
|
|
|
124
124
|
---
|
|
125
125
|
|
|
126
|
-
## MCP Server — 3 Namespaces,
|
|
126
|
+
## MCP Server — 3 Namespaces, 32 Tools
|
|
127
127
|
|
|
128
128
|
clauth is the single MCP interface for all local tools. One process, namespaced paths:
|
|
129
129
|
|
|
@@ -131,16 +131,18 @@ clauth is the single MCP interface for all local tools. One process, namespaced
|
|
|
131
131
|
|------|-----------|-------|-------------|
|
|
132
132
|
| `/clauth` | `clauth_*` | 13 | Credential vault operations |
|
|
133
133
|
| `/gws` | `gws_*` | 6 | Google Workspace (Gmail, Calendar, Drive) |
|
|
134
|
-
| `/fs` | `fs_*` |
|
|
135
|
-
| `/mcp` | all |
|
|
136
|
-
|
|
137
|
-
### FS Tools
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
134
|
+
| `/fs` | `fs_*` | 13 | Filesystem (read, write, append, chunked write, URL ingest, Git import, stat, grep, glob, delete, mkdir, mounts) |
|
|
135
|
+
| `/mcp` | all | 32 | All namespaces combined (Claude Code) |
|
|
136
|
+
|
|
137
|
+
### FS Tools
|
|
138
|
+
|
|
139
|
+
13 filesystem tools with path-jail security:
|
|
140
|
+
- `fs_read`, `fs_write`, `fs_stat`, `fs_append`, `fs_write_chunk`, `fs_ingest_url`, `fs_import_git_files`, `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
|
+
- Large writes should use `fs_write_chunk`; cloud-to-local transfer should use `fs_ingest_url`; guarded appends should pass `expected_sha256` from `fs_stat`
|
|
145
|
+
- Durable new files authored by Claude.ai in GitHub should use `fs_import_git_files` so the local dirty monorepo fetches and restores only named paths without `git pull`
|
|
144
146
|
|
|
145
147
|
### GWS Tools
|
|
146
148
|
|
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 { readdir, readFile, writeFile, rm, mkdir, stat, rename } from "node:fs/promises";
|
|
20
|
+
import { appendFile, readdir, readFile, writeFile, rm, mkdir, stat, rename } 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";
|
|
@@ -3687,37 +3687,43 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3687
3687
|
return ok(res, { status: tunnelStatus });
|
|
3688
3688
|
}
|
|
3689
3689
|
|
|
3690
|
-
// POST /launch-ccandme —
|
|
3690
|
+
// POST /launch-ccandme — open a new CCandMe WezTerm window without killing existing sessions
|
|
3691
3691
|
if (method === "POST" && reqPath === "/launch-ccandme") {
|
|
3692
3692
|
if (lockedGuard(res)) return;
|
|
3693
3693
|
try {
|
|
3694
3694
|
const { spawn } = await import("child_process");
|
|
3695
|
-
const
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3695
|
+
const { existsSync, readFileSync, writeFileSync } = await import("fs");
|
|
3696
|
+
|
|
3697
|
+
// Locate WezTerm
|
|
3698
|
+
const wezCandidates = [
|
|
3699
|
+
"C:/Program Files/WezTerm/wezterm.exe",
|
|
3700
|
+
"C:/Program Files (x86)/WezTerm/wezterm.exe",
|
|
3701
|
+
process.env.WEZTERM_EXE,
|
|
3702
|
+
].filter(Boolean);
|
|
3703
|
+
const wezExe = wezCandidates.find(existsSync) || "wezterm";
|
|
3704
|
+
|
|
3705
|
+
// Render the lua config from the CCandMe template (skip the kill-existing step)
|
|
3706
|
+
const CCANDME_DIR = "C:/Dev/CCandMe";
|
|
3707
|
+
const WORK_DIR = "C:/Dev/regen-root";
|
|
3708
|
+
const templatePath = path.join(CCANDME_DIR, "templates", "wezterm.lua");
|
|
3709
|
+
const luaOutPath = path.join(CCANDME_DIR, ".ccandme-wezterm.lua");
|
|
3710
|
+
|
|
3711
|
+
if (existsSync(templatePath)) {
|
|
3712
|
+
const lua = readFileSync(templatePath, "utf8")
|
|
3713
|
+
.replaceAll("__SUPERVISOR_DIR__", CCANDME_DIR.replace(/\//g, "\\\\"))
|
|
3714
|
+
.replaceAll("__WORK_DIR__", WORK_DIR.replace(/\//g, "\\\\"))
|
|
3715
|
+
.replaceAll("__WORKSPACE__", "ccandme")
|
|
3716
|
+
.replaceAll("__CLAUDE_CMD__", "claude")
|
|
3717
|
+
.replaceAll("__CODEX_CMD__", "codex")
|
|
3718
|
+
.replaceAll("__PACKAGE_ROOT__", CCANDME_DIR.replace(/\//g, "\\\\"));
|
|
3719
|
+
writeFileSync(luaOutPath, lua, "utf8");
|
|
3715
3720
|
}
|
|
3716
|
-
|
|
3721
|
+
|
|
3722
|
+
// Launch WezTerm with the CCandMe config — no kill of existing sessions
|
|
3723
|
+
const luaArg = existsSync(luaOutPath) ? luaOutPath : path.join(CCANDME_DIR, ".ccandme-wezterm.lua");
|
|
3724
|
+
const child = spawn(wezExe, ["--config-file", luaArg, "start"], {
|
|
3717
3725
|
detached: true,
|
|
3718
3726
|
stdio: "ignore",
|
|
3719
|
-
windowsHide: false,
|
|
3720
|
-
shell: process.platform === "win32" && cmd === "cmd.exe" ? false : true,
|
|
3721
3727
|
});
|
|
3722
3728
|
child.unref();
|
|
3723
3729
|
return ok(res, { ok: true, message: "CCandMe launched" });
|
|
@@ -5476,8 +5482,8 @@ async function actionForeground(opts) {
|
|
|
5476
5482
|
// Reuses the same auth model as the HTTP daemon.
|
|
5477
5483
|
// Secrets are delivered via temp files — never in the MCP response.
|
|
5478
5484
|
|
|
5479
|
-
import { createInterface } from "readline";
|
|
5480
|
-
import { execSync, spawn as spawnProc } from "child_process";
|
|
5485
|
+
import { createInterface } from "readline";
|
|
5486
|
+
import { execSync, spawn as spawnProc, spawnSync } from "child_process";
|
|
5481
5487
|
|
|
5482
5488
|
// ── Monkey dispatch — headless Claude CLI worker ─────────────────
|
|
5483
5489
|
function findClaudeBinary() {
|
|
@@ -6132,24 +6138,115 @@ async function getFileserverMounts(vault) {
|
|
|
6132
6138
|
}
|
|
6133
6139
|
}
|
|
6134
6140
|
|
|
6135
|
-
async function resolveInMount(requestedPath, mountName, vault) {
|
|
6141
|
+
async function resolveInMount(requestedPath, mountName, vault) {
|
|
6136
6142
|
const { mounts, error } = await getFileserverMounts(vault);
|
|
6137
6143
|
if (error) return { error };
|
|
6138
6144
|
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\"}" };
|
|
6139
6145
|
const mount = mountName ? mounts.find(m => m.name === mountName) : mounts[0];
|
|
6140
6146
|
if (!mount) return { error: `Mount '${mountName}' not found. Available: ${mounts.map(m => m.name).join(", ")}` };
|
|
6141
6147
|
if (!mount.path) return { error: `Fileserver '${mount.name}' has no path configured` };
|
|
6142
|
-
const resolved = path.resolve(mount.path, requestedPath);
|
|
6143
|
-
const normalized = path.normalize(resolved);
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
}
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6148
|
+
const resolved = path.resolve(mount.path, requestedPath);
|
|
6149
|
+
const normalized = path.normalize(resolved);
|
|
6150
|
+
const relative = path.relative(path.normalize(mount.path), normalized);
|
|
6151
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
6152
|
+
return { error: `Path escapes mount: ${requestedPath}` };
|
|
6153
|
+
}
|
|
6154
|
+
return { resolved: normalized, mount };
|
|
6155
|
+
}
|
|
6156
|
+
|
|
6157
|
+
function checkAccess(mount, flag) {
|
|
6158
|
+
return mount.access.includes(flag);
|
|
6159
|
+
}
|
|
6160
|
+
|
|
6161
|
+
function sha256Hex(value) {
|
|
6162
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
6163
|
+
}
|
|
6164
|
+
|
|
6165
|
+
async function atomicWriteText(filePath, content) {
|
|
6166
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
6167
|
+
const tempPath = path.join(
|
|
6168
|
+
path.dirname(filePath),
|
|
6169
|
+
`.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`
|
|
6170
|
+
);
|
|
6171
|
+
await writeFile(tempPath, content, "utf8");
|
|
6172
|
+
await rename(tempPath, filePath);
|
|
6173
|
+
}
|
|
6174
|
+
|
|
6175
|
+
async function fileInfo(filePath, requestedPath) {
|
|
6176
|
+
const s = await stat(filePath);
|
|
6177
|
+
const info = {
|
|
6178
|
+
path: requestedPath,
|
|
6179
|
+
type: s.isDirectory() ? "dir" : "file",
|
|
6180
|
+
size: s.size,
|
|
6181
|
+
modified: s.mtime.toISOString(),
|
|
6182
|
+
};
|
|
6183
|
+
if (s.isFile()) {
|
|
6184
|
+
const content = await readFile(filePath);
|
|
6185
|
+
info.sha256 = sha256Hex(content);
|
|
6186
|
+
}
|
|
6187
|
+
return info;
|
|
6188
|
+
}
|
|
6189
|
+
|
|
6190
|
+
const FS_UPLOAD_SESSIONS = new Map();
|
|
6191
|
+
const FS_UPLOAD_TTL_MS = 30 * 60 * 1000;
|
|
6192
|
+
const FS_MAX_CHUNKS = 500;
|
|
6193
|
+
const FS_MAX_CHUNK_BYTES = 128 * 1024;
|
|
6194
|
+
const FS_MAX_INGEST_BYTES = 25 * 1024 * 1024;
|
|
6195
|
+
const FS_GIT_IMPORT_ALLOWED_PREFIXES = [
|
|
6196
|
+
"docs/",
|
|
6197
|
+
".rdc/plans/",
|
|
6198
|
+
".rdc/guides/",
|
|
6199
|
+
".claude/context/",
|
|
6200
|
+
".claude/rules/",
|
|
6201
|
+
".rdc/relay/from-claude-ai/",
|
|
6202
|
+
];
|
|
6203
|
+
|
|
6204
|
+
function cleanupFsUploadSessions() {
|
|
6205
|
+
const cutoff = Date.now() - FS_UPLOAD_TTL_MS;
|
|
6206
|
+
for (const [id, session] of FS_UPLOAD_SESSIONS) {
|
|
6207
|
+
if (session.updatedAt < cutoff) FS_UPLOAD_SESSIONS.delete(id);
|
|
6208
|
+
}
|
|
6209
|
+
}
|
|
6210
|
+
|
|
6211
|
+
function runGit(cwd, args, opts = {}) {
|
|
6212
|
+
const res = spawnSync("git", args, {
|
|
6213
|
+
cwd,
|
|
6214
|
+
encoding: "utf8",
|
|
6215
|
+
windowsHide: true,
|
|
6216
|
+
maxBuffer: opts.maxBuffer || 10 * 1024 * 1024,
|
|
6217
|
+
});
|
|
6218
|
+
if (res.status !== 0) {
|
|
6219
|
+
const detail = (res.stderr || res.stdout || "").trim();
|
|
6220
|
+
throw new Error(`git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
|
|
6221
|
+
}
|
|
6222
|
+
return (res.stdout || "").trim();
|
|
6223
|
+
}
|
|
6224
|
+
|
|
6225
|
+
function runGitRaw(cwd, args, opts = {}) {
|
|
6226
|
+
const res = spawnSync("git", args, {
|
|
6227
|
+
cwd,
|
|
6228
|
+
encoding: "buffer",
|
|
6229
|
+
windowsHide: true,
|
|
6230
|
+
maxBuffer: opts.maxBuffer || 10 * 1024 * 1024,
|
|
6231
|
+
});
|
|
6232
|
+
if (res.status !== 0) {
|
|
6233
|
+
const detail = Buffer.concat([res.stderr || Buffer.alloc(0), res.stdout || Buffer.alloc(0)]).toString("utf8").trim();
|
|
6234
|
+
throw new Error(`git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
|
|
6235
|
+
}
|
|
6236
|
+
return res.stdout || Buffer.alloc(0);
|
|
6237
|
+
}
|
|
6238
|
+
|
|
6239
|
+
function normalizeRepoPath(p) {
|
|
6240
|
+
if (!p || typeof p !== "string") return null;
|
|
6241
|
+
const normalized = p.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
6242
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
6243
|
+
if (parts.length === 0 || parts.includes("..") || path.isAbsolute(p)) return null;
|
|
6244
|
+
return parts.join("/");
|
|
6245
|
+
}
|
|
6246
|
+
|
|
6247
|
+
function isAllowedGitImportPath(p, allowedPrefixes = FS_GIT_IMPORT_ALLOWED_PREFIXES) {
|
|
6248
|
+
return allowedPrefixes.some((prefix) => p === prefix.replace(/\/$/, "") || p.startsWith(prefix));
|
|
6249
|
+
}
|
|
6153
6250
|
|
|
6154
6251
|
const MCP_TOOLS = [
|
|
6155
6252
|
{
|
|
@@ -6642,9 +6739,9 @@ const MCP_TOOLS = [
|
|
|
6642
6739
|
additionalProperties: false,
|
|
6643
6740
|
},
|
|
6644
6741
|
},
|
|
6645
|
-
{
|
|
6646
|
-
name: "fs_write",
|
|
6647
|
-
description: "Write content to a file. Creates parent directories if needed. Overwrites existing file.",
|
|
6742
|
+
{
|
|
6743
|
+
name: "fs_write",
|
|
6744
|
+
description: "Write content to a file. Creates parent directories if needed. Overwrites existing file.",
|
|
6648
6745
|
inputSchema: {
|
|
6649
6746
|
type: "object",
|
|
6650
6747
|
properties: {
|
|
@@ -6653,12 +6750,93 @@ const MCP_TOOLS = [
|
|
|
6653
6750
|
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
6654
6751
|
},
|
|
6655
6752
|
required: ["path", "content"],
|
|
6656
|
-
additionalProperties: false,
|
|
6657
|
-
},
|
|
6658
|
-
},
|
|
6659
|
-
{
|
|
6660
|
-
name: "
|
|
6661
|
-
description: "
|
|
6753
|
+
additionalProperties: false,
|
|
6754
|
+
},
|
|
6755
|
+
},
|
|
6756
|
+
{
|
|
6757
|
+
name: "fs_stat",
|
|
6758
|
+
description: "Get file or directory metadata. Files include a SHA-256 hash for guarded edits.",
|
|
6759
|
+
inputSchema: {
|
|
6760
|
+
type: "object",
|
|
6761
|
+
properties: {
|
|
6762
|
+
path: { type: "string", description: "Relative path within mount" },
|
|
6763
|
+
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
6764
|
+
},
|
|
6765
|
+
required: ["path"],
|
|
6766
|
+
additionalProperties: false,
|
|
6767
|
+
},
|
|
6768
|
+
},
|
|
6769
|
+
{
|
|
6770
|
+
name: "fs_append",
|
|
6771
|
+
description: "Append text to a file, optionally guarded by the current file SHA-256 hash.",
|
|
6772
|
+
inputSchema: {
|
|
6773
|
+
type: "object",
|
|
6774
|
+
properties: {
|
|
6775
|
+
path: { type: "string", description: "Relative path within mount" },
|
|
6776
|
+
content: { type: "string", description: "Text to append" },
|
|
6777
|
+
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
6778
|
+
expected_sha256: { type: "string", description: "Optional current file SHA-256. If provided and the file exists, append only when it matches." },
|
|
6779
|
+
},
|
|
6780
|
+
required: ["path", "content"],
|
|
6781
|
+
additionalProperties: false,
|
|
6782
|
+
},
|
|
6783
|
+
},
|
|
6784
|
+
{
|
|
6785
|
+
name: "fs_write_chunk",
|
|
6786
|
+
description: "Stage chunked text writes and atomically publish when all chunks arrive. Use when single write arguments are too large.",
|
|
6787
|
+
inputSchema: {
|
|
6788
|
+
type: "object",
|
|
6789
|
+
properties: {
|
|
6790
|
+
upload_id: { type: "string", description: "Caller-chosen idempotency key for this file upload" },
|
|
6791
|
+
path: { type: "string", description: "Relative path within mount" },
|
|
6792
|
+
chunk_index: { type: "number", description: "Zero-based chunk index" },
|
|
6793
|
+
total_chunks: { type: "number", description: "Total chunks expected" },
|
|
6794
|
+
content: { type: "string", description: "Chunk text content" },
|
|
6795
|
+
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
6796
|
+
expected_sha256: { type: "string", description: "Optional SHA-256 of the final assembled file content" },
|
|
6797
|
+
},
|
|
6798
|
+
required: ["upload_id", "path", "chunk_index", "total_chunks", "content"],
|
|
6799
|
+
additionalProperties: false,
|
|
6800
|
+
},
|
|
6801
|
+
},
|
|
6802
|
+
{
|
|
6803
|
+
name: "fs_ingest_url",
|
|
6804
|
+
description: "Download text from an http(s) URL and atomically write it inside the mount. Useful for moving cloud files into the local filesystem.",
|
|
6805
|
+
inputSchema: {
|
|
6806
|
+
type: "object",
|
|
6807
|
+
properties: {
|
|
6808
|
+
url: { type: "string", description: "HTTPS or HTTP URL to fetch" },
|
|
6809
|
+
path: { type: "string", description: "Relative output path within mount" },
|
|
6810
|
+
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
6811
|
+
max_bytes: { type: "number", description: "Maximum download size in bytes (default 5MB, hard max 25MB)" },
|
|
6812
|
+
expected_sha256: { type: "string", description: "Optional SHA-256 of fetched content before writing" },
|
|
6813
|
+
},
|
|
6814
|
+
required: ["url", "path"],
|
|
6815
|
+
additionalProperties: false,
|
|
6816
|
+
},
|
|
6817
|
+
},
|
|
6818
|
+
{
|
|
6819
|
+
name: "fs_import_git_files",
|
|
6820
|
+
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.",
|
|
6821
|
+
inputSchema: {
|
|
6822
|
+
type: "object",
|
|
6823
|
+
properties: {
|
|
6824
|
+
remote: { type: "string", description: "Git remote name (default: origin)" },
|
|
6825
|
+
ref: { type: "string", description: "Branch, tag, or commit to fetch/restore from" },
|
|
6826
|
+
paths: { type: "array", items: { type: "string" }, description: "Repo-relative file paths to import" },
|
|
6827
|
+
mode: { type: "string", enum: ["new_only", "overwrite"], description: "new_only refuses existing local paths. overwrite restores named paths only." },
|
|
6828
|
+
commit: { type: "boolean", description: "When true, stage only imported paths and create a local commit. Never pushes." },
|
|
6829
|
+
message: { type: "string", description: "Commit subject when commit=true" },
|
|
6830
|
+
mount: { type: "string", description: "Mount name (default: first mount)" },
|
|
6831
|
+
allowed_prefixes: { type: "array", items: { type: "string" }, description: "Optional path allowlist prefixes. Defaults to docs and agent corpus paths." },
|
|
6832
|
+
},
|
|
6833
|
+
required: ["ref", "paths"],
|
|
6834
|
+
additionalProperties: false,
|
|
6835
|
+
},
|
|
6836
|
+
},
|
|
6837
|
+
{
|
|
6838
|
+
name: "fs_list",
|
|
6839
|
+
description: "List directory contents with file type, size, and modification time.",
|
|
6662
6840
|
inputSchema: {
|
|
6663
6841
|
type: "object",
|
|
6664
6842
|
properties: {
|
|
@@ -7127,20 +7305,250 @@ async function handleMcpTool(vault, name, args) {
|
|
|
7127
7305
|
}
|
|
7128
7306
|
}
|
|
7129
7307
|
|
|
7130
|
-
case "fs_write": {
|
|
7131
|
-
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7132
|
-
if (r.error) return mcpError(r.error);
|
|
7133
|
-
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7134
|
-
try {
|
|
7135
|
-
await
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7308
|
+
case "fs_write": {
|
|
7309
|
+
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7310
|
+
if (r.error) return mcpError(r.error);
|
|
7311
|
+
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7312
|
+
try {
|
|
7313
|
+
await atomicWriteText(r.resolved, args.content);
|
|
7314
|
+
return mcpResult(`Written: ${args.path} (${Buffer.byteLength(args.content)} bytes)`);
|
|
7315
|
+
} catch (err) {
|
|
7316
|
+
return mcpError(`Write failed: ${err.message}`);
|
|
7317
|
+
}
|
|
7318
|
+
}
|
|
7319
|
+
|
|
7320
|
+
case "fs_stat": {
|
|
7321
|
+
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7322
|
+
if (r.error) return mcpError(r.error);
|
|
7323
|
+
if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
|
|
7324
|
+
try {
|
|
7325
|
+
return mcpResult(JSON.stringify(await fileInfo(r.resolved, args.path), null, 2));
|
|
7326
|
+
} catch (err) {
|
|
7327
|
+
if (err.code === "ENOENT") return mcpError(`Not found: ${args.path}`);
|
|
7328
|
+
return mcpError(`Stat failed: ${err.message}`);
|
|
7329
|
+
}
|
|
7330
|
+
}
|
|
7331
|
+
|
|
7332
|
+
case "fs_append": {
|
|
7333
|
+
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7334
|
+
if (r.error) return mcpError(r.error);
|
|
7335
|
+
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7336
|
+
try {
|
|
7337
|
+
try {
|
|
7338
|
+
const current = await readFile(r.resolved);
|
|
7339
|
+
if (args.expected_sha256 && sha256Hex(current) !== args.expected_sha256) {
|
|
7340
|
+
return mcpError("Append rejected: current file hash does not match expected_sha256");
|
|
7341
|
+
}
|
|
7342
|
+
} catch (err) {
|
|
7343
|
+
if (err.code !== "ENOENT") throw err;
|
|
7344
|
+
if (args.expected_sha256) return mcpError("Append rejected: file does not exist for expected_sha256 guard");
|
|
7345
|
+
}
|
|
7346
|
+
await mkdir(path.dirname(r.resolved), { recursive: true });
|
|
7347
|
+
await appendFile(r.resolved, args.content, "utf8");
|
|
7348
|
+
const info = await fileInfo(r.resolved, args.path);
|
|
7349
|
+
return mcpResult(JSON.stringify({ appended_bytes: Buffer.byteLength(args.content), ...info }, null, 2));
|
|
7350
|
+
} catch (err) {
|
|
7351
|
+
return mcpError(`Append failed: ${err.message}`);
|
|
7352
|
+
}
|
|
7353
|
+
}
|
|
7354
|
+
|
|
7355
|
+
case "fs_write_chunk": {
|
|
7356
|
+
cleanupFsUploadSessions();
|
|
7357
|
+
const { upload_id, chunk_index, total_chunks, content } = args;
|
|
7358
|
+
const index = Number(chunk_index);
|
|
7359
|
+
const total = Number(total_chunks);
|
|
7360
|
+
if (!Number.isInteger(index) || !Number.isInteger(total) || index < 0 || total < 1 || index >= total) {
|
|
7361
|
+
return mcpError("Invalid chunk_index/total_chunks");
|
|
7362
|
+
}
|
|
7363
|
+
if (total > FS_MAX_CHUNKS) return mcpError(`Too many chunks: max ${FS_MAX_CHUNKS}`);
|
|
7364
|
+
if (Buffer.byteLength(content, "utf8") > FS_MAX_CHUNK_BYTES) {
|
|
7365
|
+
return mcpError(`Chunk too large: max ${FS_MAX_CHUNK_BYTES} bytes`);
|
|
7366
|
+
}
|
|
7367
|
+
|
|
7368
|
+
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7369
|
+
if (r.error) return mcpError(r.error);
|
|
7370
|
+
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7371
|
+
|
|
7372
|
+
const key = `${r.mount.name}:${args.path}:${upload_id}`;
|
|
7373
|
+
let session = FS_UPLOAD_SESSIONS.get(key);
|
|
7374
|
+
if (!session) {
|
|
7375
|
+
session = { path: args.path, resolved: r.resolved, total, chunks: new Map(), expectedSha256: args.expected_sha256 || null, updatedAt: Date.now() };
|
|
7376
|
+
FS_UPLOAD_SESSIONS.set(key, session);
|
|
7377
|
+
}
|
|
7378
|
+
if (session.total !== total || session.path !== args.path || session.resolved !== r.resolved) {
|
|
7379
|
+
return mcpError("Upload id collision: path or total_chunks differs from existing session");
|
|
7380
|
+
}
|
|
7381
|
+
if (args.expected_sha256 && session.expectedSha256 && args.expected_sha256 !== session.expectedSha256) {
|
|
7382
|
+
return mcpError("Upload id collision: expected_sha256 differs from existing session");
|
|
7383
|
+
}
|
|
7384
|
+
|
|
7385
|
+
session.chunks.set(index, content);
|
|
7386
|
+
session.updatedAt = Date.now();
|
|
7387
|
+
|
|
7388
|
+
if (session.chunks.size < total) {
|
|
7389
|
+
return mcpResult(JSON.stringify({ upload_id, status: "staged", received_chunks: session.chunks.size, total_chunks: total }, null, 2));
|
|
7390
|
+
}
|
|
7391
|
+
|
|
7392
|
+
const assembled = Array.from({ length: total }, (_, i) => session.chunks.get(i)).join("");
|
|
7393
|
+
const actualSha = sha256Hex(assembled);
|
|
7394
|
+
if (session.expectedSha256 && actualSha !== session.expectedSha256) {
|
|
7395
|
+
FS_UPLOAD_SESSIONS.delete(key);
|
|
7396
|
+
return mcpError(`Final SHA-256 mismatch: expected ${session.expectedSha256}, got ${actualSha}`);
|
|
7397
|
+
}
|
|
7398
|
+
|
|
7399
|
+
try {
|
|
7400
|
+
await atomicWriteText(r.resolved, assembled);
|
|
7401
|
+
FS_UPLOAD_SESSIONS.delete(key);
|
|
7402
|
+
return mcpResult(JSON.stringify({ upload_id, status: "written", path: args.path, bytes: Buffer.byteLength(assembled), sha256: actualSha }, null, 2));
|
|
7403
|
+
} catch (err) {
|
|
7404
|
+
return mcpError(`Chunked write failed: ${err.message}`);
|
|
7405
|
+
}
|
|
7406
|
+
}
|
|
7407
|
+
|
|
7408
|
+
case "fs_ingest_url": {
|
|
7409
|
+
const r = await resolveInMount(args.path, args.mount, vault);
|
|
7410
|
+
if (r.error) return mcpError(r.error);
|
|
7411
|
+
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7412
|
+
|
|
7413
|
+
let url;
|
|
7414
|
+
try {
|
|
7415
|
+
url = new URL(args.url);
|
|
7416
|
+
} catch {
|
|
7417
|
+
return mcpError("Invalid URL");
|
|
7418
|
+
}
|
|
7419
|
+
if (!["http:", "https:"].includes(url.protocol)) return mcpError("Only http(s) URLs are supported");
|
|
7420
|
+
|
|
7421
|
+
const maxBytes = Math.min(Number(args.max_bytes || 5 * 1024 * 1024), FS_MAX_INGEST_BYTES);
|
|
7422
|
+
try {
|
|
7423
|
+
const response = await fetch(url, { redirect: "follow" });
|
|
7424
|
+
if (!response.ok) return mcpError(`Fetch failed: HTTP ${response.status}`);
|
|
7425
|
+
const length = Number(response.headers.get("content-length") || 0);
|
|
7426
|
+
if (length && length > maxBytes) return mcpError(`Fetch rejected: content-length ${length} exceeds max_bytes ${maxBytes}`);
|
|
7427
|
+
|
|
7428
|
+
const reader = response.body?.getReader();
|
|
7429
|
+
if (!reader) return mcpError("Fetch failed: response body is not readable");
|
|
7430
|
+
|
|
7431
|
+
let received = 0;
|
|
7432
|
+
const chunks = [];
|
|
7433
|
+
while (true) {
|
|
7434
|
+
const { done, value } = await reader.read();
|
|
7435
|
+
if (done) break;
|
|
7436
|
+
received += value.byteLength;
|
|
7437
|
+
if (received > maxBytes) return mcpError(`Fetch rejected: response exceeds max_bytes ${maxBytes}`);
|
|
7438
|
+
chunks.push(Buffer.from(value));
|
|
7439
|
+
}
|
|
7440
|
+
|
|
7441
|
+
const content = Buffer.concat(chunks).toString("utf8");
|
|
7442
|
+
const actualSha = sha256Hex(content);
|
|
7443
|
+
if (args.expected_sha256 && actualSha !== args.expected_sha256) {
|
|
7444
|
+
return mcpError(`Fetched SHA-256 mismatch: expected ${args.expected_sha256}, got ${actualSha}`);
|
|
7445
|
+
}
|
|
7446
|
+
await atomicWriteText(r.resolved, content);
|
|
7447
|
+
return mcpResult(JSON.stringify({ status: "written", path: args.path, bytes: Buffer.byteLength(content), sha256: actualSha, source: url.href }, null, 2));
|
|
7448
|
+
} catch (err) {
|
|
7449
|
+
return mcpError(`Ingest failed: ${err.message}`);
|
|
7450
|
+
}
|
|
7451
|
+
}
|
|
7452
|
+
|
|
7453
|
+
case "fs_import_git_files": {
|
|
7454
|
+
if (!Array.isArray(args.paths) || args.paths.length === 0) return mcpError("paths must be a non-empty array");
|
|
7455
|
+
if (args.paths.length > 25) return mcpError("Too many paths: max 25 per import");
|
|
7456
|
+
|
|
7457
|
+
const r = await resolveInMount(".", args.mount, vault);
|
|
7458
|
+
if (r.error) return mcpError(r.error);
|
|
7459
|
+
if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
|
|
7460
|
+
|
|
7461
|
+
const repoRoot = r.resolved;
|
|
7462
|
+
const remote = args.remote || "origin";
|
|
7463
|
+
const mode = args.mode || "new_only";
|
|
7464
|
+
const doCommit = args.commit === true;
|
|
7465
|
+
const allowedPrefixes = Array.isArray(args.allowed_prefixes) && args.allowed_prefixes.length > 0
|
|
7466
|
+
? args.allowed_prefixes.map((p) => normalizeRepoPath(p.endsWith("/") ? p : `${p}/`)).filter(Boolean)
|
|
7467
|
+
: FS_GIT_IMPORT_ALLOWED_PREFIXES;
|
|
7468
|
+
|
|
7469
|
+
try {
|
|
7470
|
+
const topLevel = path.normalize(runGit(repoRoot, ["rev-parse", "--show-toplevel"]));
|
|
7471
|
+
if (topLevel.toLowerCase() !== path.normalize(repoRoot).toLowerCase()) {
|
|
7472
|
+
return mcpError(`Mount root is not the git repo root: ${repoRoot} (repo root: ${topLevel})`);
|
|
7473
|
+
}
|
|
7474
|
+
|
|
7475
|
+
const normalizedPaths = [];
|
|
7476
|
+
for (const rawPath of args.paths) {
|
|
7477
|
+
const normalized = normalizeRepoPath(rawPath);
|
|
7478
|
+
if (!normalized) return mcpError(`Invalid repo path: ${rawPath}`);
|
|
7479
|
+
if (!isAllowedGitImportPath(normalized, allowedPrefixes)) return mcpError(`Path not allowed for git import: ${normalized}`);
|
|
7480
|
+
normalizedPaths.push(normalized);
|
|
7481
|
+
}
|
|
7482
|
+
|
|
7483
|
+
if (mode === "new_only") {
|
|
7484
|
+
for (const rel of normalizedPaths) {
|
|
7485
|
+
const localPath = path.join(repoRoot, rel);
|
|
7486
|
+
try {
|
|
7487
|
+
await stat(localPath);
|
|
7488
|
+
return mcpError(`Import refused: local path already exists in new_only mode: ${rel}`);
|
|
7489
|
+
} catch (err) {
|
|
7490
|
+
if (err.code !== "ENOENT") throw err;
|
|
7491
|
+
}
|
|
7492
|
+
}
|
|
7493
|
+
}
|
|
7494
|
+
|
|
7495
|
+
if (doCommit) {
|
|
7496
|
+
const staged = runGit(repoRoot, ["diff", "--cached", "--name-only"]);
|
|
7497
|
+
if (staged) return mcpError(`Import refused: index already has staged files:\n${staged}`);
|
|
7498
|
+
if (!args.message || !args.message.trim()) return mcpError("message is required when commit=true");
|
|
7499
|
+
}
|
|
7500
|
+
|
|
7501
|
+
runGit(repoRoot, ["fetch", "--no-tags", remote, args.ref]);
|
|
7502
|
+
const sourceCommit = runGit(repoRoot, ["rev-parse", "FETCH_HEAD"]);
|
|
7503
|
+
|
|
7504
|
+
for (const rel of normalizedPaths) {
|
|
7505
|
+
runGit(repoRoot, ["cat-file", "-e", `${sourceCommit}:${rel}`]);
|
|
7506
|
+
}
|
|
7507
|
+
|
|
7508
|
+
runGit(repoRoot, ["restore", `--source=${sourceCommit}`, "--", ...normalizedPaths]);
|
|
7509
|
+
|
|
7510
|
+
const imported = [];
|
|
7511
|
+
for (const rel of normalizedPaths) {
|
|
7512
|
+
const localPath = path.join(repoRoot, rel);
|
|
7513
|
+
const info = await fileInfo(localPath, rel);
|
|
7514
|
+
const sourceBlob = runGit(repoRoot, ["rev-parse", `${sourceCommit}:${rel}`]);
|
|
7515
|
+
const sourceSize = Number(runGitRaw(repoRoot, ["cat-file", "-s", `${sourceCommit}:${rel}`]).toString("utf8").trim());
|
|
7516
|
+
imported.push({ ...info, source_blob: sourceBlob, source_size: sourceSize });
|
|
7517
|
+
}
|
|
7518
|
+
|
|
7519
|
+
let localCommit = null;
|
|
7520
|
+
if (doCommit) {
|
|
7521
|
+
runGit(repoRoot, ["add", "--", ...normalizedPaths]);
|
|
7522
|
+
const body = [
|
|
7523
|
+
args.message.trim(),
|
|
7524
|
+
"",
|
|
7525
|
+
"Imported from Claude.ai GitHub upload.",
|
|
7526
|
+
"",
|
|
7527
|
+
`Source remote: ${remote}`,
|
|
7528
|
+
`Source ref: ${args.ref}`,
|
|
7529
|
+
`Source commit: ${sourceCommit}`,
|
|
7530
|
+
"",
|
|
7531
|
+
"Paths:",
|
|
7532
|
+
...normalizedPaths.map((p) => `- ${p}`),
|
|
7533
|
+
].join("\n");
|
|
7534
|
+
runGit(repoRoot, ["commit", "-m", body]);
|
|
7535
|
+
localCommit = runGit(repoRoot, ["rev-parse", "HEAD"]);
|
|
7536
|
+
}
|
|
7537
|
+
|
|
7538
|
+
return mcpResult(JSON.stringify({
|
|
7539
|
+
status: "ok",
|
|
7540
|
+
mode,
|
|
7541
|
+
committed: doCommit,
|
|
7542
|
+
source_commit: sourceCommit,
|
|
7543
|
+
local_commit: localCommit,
|
|
7544
|
+
imported,
|
|
7545
|
+
}, null, 2));
|
|
7546
|
+
} catch (err) {
|
|
7547
|
+
return mcpError(`Git import failed: ${err.message}`);
|
|
7548
|
+
}
|
|
7549
|
+
}
|
|
7550
|
+
|
|
7551
|
+
case "fs_list": {
|
|
7144
7552
|
const dirPath = args.path || ".";
|
|
7145
7553
|
const r = await resolveInMount(dirPath, args.mount, vault);
|
|
7146
7554
|
if (r.error) return mcpError(r.error);
|
package/package.json
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@lifeaitools/clauth",
|
|
3
|
-
"version": "1.5.
|
|
4
|
-
"description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"clauth": "./cli/index.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "bash scripts/build.sh",
|
|
11
|
-
"postinstall": "node scripts/postinstall.js",
|
|
12
|
-
"worker:start": "node cli/index.js serve",
|
|
13
|
-
"worker:stop": "curl -s http://127.0.0.1:52437/shutdown 2>nul || taskkill /F /IM cloudflared.exe 2>nul & exit 0",
|
|
14
|
-
"worker:restart": "npm run worker:stop && timeout /t 3 /nobreak >nul && npm run worker:start"
|
|
15
|
-
},
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"chalk": "^5.3.0",
|
|
18
|
-
"commander": "^12.1.0",
|
|
19
|
-
"conf": "^13.0.0",
|
|
20
|
-
"inquirer": "^10.1.0",
|
|
21
|
-
"node-fetch": "^3.3.2",
|
|
22
|
-
"ora": "^8.1.0",
|
|
23
|
-
"@vscode/ripgrep": "^1.15.9",
|
|
24
|
-
"fast-glob": "^3.3.2"
|
|
25
|
-
},
|
|
26
|
-
"engines": {
|
|
27
|
-
"node": ">=18.0.0"
|
|
28
|
-
},
|
|
29
|
-
"keywords": [
|
|
30
|
-
"lifeai",
|
|
31
|
-
"credentials",
|
|
32
|
-
"vault",
|
|
33
|
-
"cli",
|
|
34
|
-
"prt"
|
|
35
|
-
],
|
|
36
|
-
"author": "Dave Ladouceur <dave@life.ai>",
|
|
37
|
-
"license": "MIT",
|
|
38
|
-
"repository": {
|
|
39
|
-
"type": "git",
|
|
40
|
-
"url": "https://github.com/LIFEAI/clauth.git"
|
|
41
|
-
},
|
|
42
|
-
"devDependencies": {
|
|
43
|
-
"javascript-obfuscator": "^5.3.0"
|
|
44
|
-
},
|
|
45
|
-
"files": [
|
|
46
|
-
"cli/",
|
|
47
|
-
"scripts/bin/",
|
|
48
|
-
"scripts/bootstrap.cjs",
|
|
49
|
-
"scripts/build.sh",
|
|
50
|
-
"scripts/postinstall.js",
|
|
51
|
-
"supabase/",
|
|
52
|
-
".clauth-skill/",
|
|
53
|
-
"install.sh",
|
|
54
|
-
"install.ps1",
|
|
55
|
-
"README.md"
|
|
56
|
-
],
|
|
57
|
-
"publishConfig": {
|
|
58
|
-
"access": "public"
|
|
59
|
-
},
|
|
60
|
-
"homepage": "https://github.com/LIFEAI/clauth"
|
|
61
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@lifeaitools/clauth",
|
|
3
|
+
"version": "1.5.82",
|
|
4
|
+
"description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"clauth": "./cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "bash scripts/build.sh",
|
|
11
|
+
"postinstall": "node scripts/postinstall.js",
|
|
12
|
+
"worker:start": "node cli/index.js serve",
|
|
13
|
+
"worker:stop": "curl -s http://127.0.0.1:52437/shutdown 2>nul || taskkill /F /IM cloudflared.exe 2>nul & exit 0",
|
|
14
|
+
"worker:restart": "npm run worker:stop && timeout /t 3 /nobreak >nul && npm run worker:start"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"chalk": "^5.3.0",
|
|
18
|
+
"commander": "^12.1.0",
|
|
19
|
+
"conf": "^13.0.0",
|
|
20
|
+
"inquirer": "^10.1.0",
|
|
21
|
+
"node-fetch": "^3.3.2",
|
|
22
|
+
"ora": "^8.1.0",
|
|
23
|
+
"@vscode/ripgrep": "^1.15.9",
|
|
24
|
+
"fast-glob": "^3.3.2"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"lifeai",
|
|
31
|
+
"credentials",
|
|
32
|
+
"vault",
|
|
33
|
+
"cli",
|
|
34
|
+
"prt"
|
|
35
|
+
],
|
|
36
|
+
"author": "Dave Ladouceur <dave@life.ai>",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/LIFEAI/clauth.git"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"javascript-obfuscator": "^5.3.0"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"cli/",
|
|
47
|
+
"scripts/bin/",
|
|
48
|
+
"scripts/bootstrap.cjs",
|
|
49
|
+
"scripts/build.sh",
|
|
50
|
+
"scripts/postinstall.js",
|
|
51
|
+
"supabase/",
|
|
52
|
+
".clauth-skill/",
|
|
53
|
+
"install.sh",
|
|
54
|
+
"install.ps1",
|
|
55
|
+
"README.md"
|
|
56
|
+
],
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"homepage": "https://github.com/LIFEAI/clauth"
|
|
61
|
+
}
|