@lifeaitools/clauth 1.5.36 → 1.5.38

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.
@@ -17,6 +17,9 @@ import ora from "ora";
17
17
  import { execSync as execSyncTop } from "child_process";
18
18
  import Conf from "conf";
19
19
  import { getConfOptions } from "../conf-path.js";
20
+ import { readdir, readFile, writeFile, rm, mkdir, stat, rename } from "node:fs/promises";
21
+ import fg from "fast-glob";
22
+ import { rgPath } from "@vscode/ripgrep";
20
23
 
21
24
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
25
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"), "utf8"));
@@ -2913,8 +2916,8 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2913
2916
  if (reqPath.startsWith("/.well-known/oauth-protected-resource")) {
2914
2917
  const base = oauthBase();
2915
2918
  const suffix = reqPath.replace("/.well-known/oauth-protected-resource", "").replace(/^\//, "");
2916
- const resourcePath = suffix && ["/gws", "/clauth", "/mcp", "/sse"].includes("/" + suffix) ? "/" + suffix : "/sse";
2917
- res.writeHead(200, { "Content-Type": "application/json", ...CORS });
2919
+ const resourcePath = suffix && ["/gws", "/clauth", "/mcp", "/fs", "/sse"].includes("/" + suffix) ? "/" + suffix : "/sse";
2920
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store", ...CORS });
2918
2921
  return res.end(JSON.stringify({
2919
2922
  resource: `${base}${resourcePath}`,
2920
2923
  authorization_servers: [base],
@@ -2925,7 +2928,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2925
2928
 
2926
2929
  if (reqPath === "/.well-known/oauth-authorization-server") {
2927
2930
  const base = oauthBase();
2928
- res.writeHead(200, { "Content-Type": "application/json", ...CORS });
2931
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store", ...CORS });
2929
2932
  return res.end(JSON.stringify({
2930
2933
  issuer: base,
2931
2934
  authorization_endpoint: `${base}/authorize`,
@@ -2959,7 +2962,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2959
2962
  oauthClients.set(clientId, client);
2960
2963
  const logMsg = `[${new Date().toISOString()}] OAuth: registered public client ${clientId} (${client.client_name})\n`;
2961
2964
  try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
2962
- res.writeHead(201, { "Content-Type": "application/json", ...CORS });
2965
+ res.writeHead(201, { "Content-Type": "application/json", "Cache-Control": "no-store", ...CORS });
2963
2966
  return res.end(JSON.stringify(client));
2964
2967
  }
2965
2968
 
@@ -3011,7 +3014,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3011
3014
 
3012
3015
  const logMsg = `[${new Date().toISOString()}] OAuth: authorize → code for ${clientId}, redirect to ${redirect.origin}\n`;
3013
3016
  try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
3014
- res.writeHead(302, { Location: redirect.toString(), ...CORS });
3017
+ res.writeHead(302, { Location: redirect.toString(), "Cache-Control": "no-store", ...CORS });
3015
3018
  return res.end();
3016
3019
  }
3017
3020
 
@@ -3082,21 +3085,23 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3082
3085
 
3083
3086
  const logMsg = `[${new Date().toISOString()}] OAuth: token issued for ${stored.client_id} (token=${accessToken.slice(0,8)}…)\n`;
3084
3087
  try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
3085
- res.writeHead(200, { "Content-Type": "application/json", ...CORS });
3088
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store", ...CORS });
3086
3089
  return res.end(JSON.stringify({ access_token: accessToken, token_type: "Bearer", scope: "mcp:tools", expires_in: 86400 }));
3087
3090
  }
3088
3091
 
3089
3092
  // ── MCP path helpers ──
3090
- const MCP_PATHS = ["/mcp", "/gws", "/clauth"];
3093
+ const MCP_PATHS = ["/mcp", "/gws", "/clauth", "/fs"];
3091
3094
  const isMcpPath = MCP_PATHS.includes(reqPath);
3092
3095
  function toolsForPath(p) {
3093
3096
  if (p === "/gws") return MCP_TOOLS.filter(t => t.name.startsWith("gws_"));
3094
3097
  if (p === "/clauth") return MCP_TOOLS.filter(t => t.name.startsWith("clauth_"));
3098
+ if (p === "/fs") return MCP_TOOLS.filter(t => t.name.startsWith("fs_"));
3095
3099
  return MCP_TOOLS; // /mcp — all tools
3096
3100
  }
3097
3101
  function serverNameForPath(p) {
3098
3102
  if (p === "/gws") return "gws";
3099
3103
  if (p === "/clauth") return "clauth";
3104
+ if (p === "/fs") return "fs";
3100
3105
  return "clauth";
3101
3106
  }
3102
3107
 
@@ -3112,7 +3117,8 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3112
3117
  const resourceMeta = `${base}/.well-known/oauth-protected-resource/${resourcePath}`;
3113
3118
  res.writeHead(401, {
3114
3119
  "Content-Type": "application/json",
3115
- "WWW-Authenticate": `Bearer resource_metadata="${resourceMeta}"`,
3120
+ "WWW-Authenticate": `Bearer realm="MCP", resource_metadata="${resourceMeta}"`,
3121
+ "Cache-Control": "no-store",
3116
3122
  ...CORS,
3117
3123
  });
3118
3124
  return res.end(JSON.stringify({
@@ -4781,6 +4787,26 @@ const ENV_MAP = {
4781
4787
  "gmail": "GMAIL_CREDENTIALS",
4782
4788
  };
4783
4789
 
4790
+ // ── Filesystem service config ──
4791
+ const FS_MOUNTS = [
4792
+ { name: "regen-root", path: "C:/Dev/regen-root", access: "rwd" },
4793
+ ];
4794
+
4795
+ function resolveInMount(requestedPath, mountName) {
4796
+ const mount = FS_MOUNTS.find(m => m.name === mountName) || FS_MOUNTS[0];
4797
+ if (!mount) return { error: "No filesystem mounts configured" };
4798
+ const resolved = path.resolve(mount.path, requestedPath);
4799
+ const normalized = path.normalize(resolved);
4800
+ if (!normalized.startsWith(path.normalize(mount.path))) {
4801
+ return { error: `Path escapes mount: ${requestedPath}` };
4802
+ }
4803
+ return { resolved: normalized, mount };
4804
+ }
4805
+
4806
+ function checkAccess(mount, flag) {
4807
+ return mount.access.includes(flag);
4808
+ }
4809
+
4784
4810
  const MCP_TOOLS = [
4785
4811
  {
4786
4812
  name: "clauth_ping",
@@ -4958,6 +4984,105 @@ const MCP_TOOLS = [
4958
4984
  additionalProperties: false
4959
4985
  }
4960
4986
  },
4987
+ // ── Filesystem tools ──
4988
+ {
4989
+ name: "fs_read",
4990
+ description: "Read a file. Returns UTF-8 text with line numbers. Supports offset/limit for large files.",
4991
+ inputSchema: {
4992
+ type: "object",
4993
+ properties: {
4994
+ path: { type: "string", description: "Relative path within mount (e.g. 'packages/ui/src/index.ts')" },
4995
+ mount: { type: "string", description: "Mount name (default: first mount)" },
4996
+ offset: { type: "number", description: "Start line (0-based, default 0)" },
4997
+ limit: { type: "number", description: "Max lines to return (default 500)" },
4998
+ },
4999
+ required: ["path"],
5000
+ additionalProperties: false,
5001
+ },
5002
+ },
5003
+ {
5004
+ name: "fs_write",
5005
+ description: "Write content to a file. Creates parent directories if needed. Overwrites existing file.",
5006
+ inputSchema: {
5007
+ type: "object",
5008
+ properties: {
5009
+ path: { type: "string", description: "Relative path within mount" },
5010
+ content: { type: "string", description: "File content to write" },
5011
+ mount: { type: "string", description: "Mount name (default: first mount)" },
5012
+ },
5013
+ required: ["path", "content"],
5014
+ additionalProperties: false,
5015
+ },
5016
+ },
5017
+ {
5018
+ name: "fs_list",
5019
+ description: "List directory contents with file type, size, and modification time.",
5020
+ inputSchema: {
5021
+ type: "object",
5022
+ properties: {
5023
+ path: { type: "string", description: "Relative directory path (default: mount root)" },
5024
+ mount: { type: "string", description: "Mount name (default: first mount)" },
5025
+ },
5026
+ additionalProperties: false,
5027
+ },
5028
+ },
5029
+ {
5030
+ name: "fs_grep",
5031
+ description: "Search file contents using ripgrep. Returns matching lines with context. Spawns rg process.",
5032
+ inputSchema: {
5033
+ type: "object",
5034
+ properties: {
5035
+ pattern: { type: "string", description: "Regex pattern to search for" },
5036
+ path: { type: "string", description: "Relative path to search in (default: mount root)" },
5037
+ glob: { type: "string", description: "File glob filter (e.g. '*.ts', '*.{js,tsx}')" },
5038
+ context: { type: "number", description: "Lines of context around matches (default 0)" },
5039
+ max_results: { type: "number", description: "Max matches (default 50)" },
5040
+ mount: { type: "string", description: "Mount name (default: first mount)" },
5041
+ },
5042
+ required: ["pattern"],
5043
+ additionalProperties: false,
5044
+ },
5045
+ },
5046
+ {
5047
+ name: "fs_glob",
5048
+ description: "Find files by glob pattern. Returns matching file paths relative to mount.",
5049
+ inputSchema: {
5050
+ type: "object",
5051
+ properties: {
5052
+ pattern: { type: "string", description: "Glob pattern (e.g. '**/*.tsx', 'apps/*/package.json')" },
5053
+ path: { type: "string", description: "Relative base path (default: mount root)" },
5054
+ mount: { type: "string", description: "Mount name (default: first mount)" },
5055
+ },
5056
+ required: ["pattern"],
5057
+ additionalProperties: false,
5058
+ },
5059
+ },
5060
+ {
5061
+ name: "fs_delete",
5062
+ description: "Delete a file or empty directory.",
5063
+ inputSchema: {
5064
+ type: "object",
5065
+ properties: {
5066
+ path: { type: "string", description: "Relative path to delete" },
5067
+ mount: { type: "string", description: "Mount name (default: first mount)" },
5068
+ },
5069
+ required: ["path"],
5070
+ additionalProperties: false,
5071
+ },
5072
+ },
5073
+ {
5074
+ name: "fs_mkdir",
5075
+ description: "Create a directory (recursive — creates parents if needed).",
5076
+ inputSchema: {
5077
+ type: "object",
5078
+ properties: {
5079
+ path: { type: "string", description: "Relative directory path to create" },
5080
+ mount: { type: "string", description: "Mount name (default: first mount)" },
5081
+ },
5082
+ required: ["path"],
5083
+ additionalProperties: false,
5084
+ },
5085
+ },
4961
5086
  ];
4962
5087
 
4963
5088
  function writeTempSecret(service, value) {
@@ -5314,6 +5439,162 @@ async function handleMcpTool(vault, name, args) {
5314
5439
  } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5315
5440
  }
5316
5441
 
5442
+ // ── Filesystem tools ──────────────────────────────────────
5443
+
5444
+ case "fs_read": {
5445
+ const r = resolveInMount(args.path, args.mount);
5446
+ if (r.error) return mcpError(r.error);
5447
+ if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
5448
+ try {
5449
+ const content = await readFile(r.resolved, "utf8");
5450
+ const lines = content.split("\n");
5451
+ const offset = args.offset || 0;
5452
+ const limit = args.limit || 500;
5453
+ const slice = lines.slice(offset, offset + limit);
5454
+ const numbered = slice.map((line, i) => `${offset + i + 1}\t${line}`).join("\n");
5455
+ const header = `${r.resolved} (${lines.length} lines${offset > 0 ? `, showing ${offset + 1}-${Math.min(offset + limit, lines.length)}` : ""})`;
5456
+ return mcpResult(`${header}\n${numbered}`);
5457
+ } catch (err) {
5458
+ if (err.code === "ENOENT") return mcpError(`File not found: ${args.path}`);
5459
+ return mcpError(`Read failed: ${err.message}`);
5460
+ }
5461
+ }
5462
+
5463
+ case "fs_write": {
5464
+ const r = resolveInMount(args.path, args.mount);
5465
+ if (r.error) return mcpError(r.error);
5466
+ if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
5467
+ try {
5468
+ await mkdir(path.dirname(r.resolved), { recursive: true });
5469
+ await writeFile(r.resolved, args.content, "utf8");
5470
+ return mcpResult(`Written: ${args.path} (${Buffer.byteLength(args.content)} bytes)`);
5471
+ } catch (err) {
5472
+ return mcpError(`Write failed: ${err.message}`);
5473
+ }
5474
+ }
5475
+
5476
+ case "fs_list": {
5477
+ const dirPath = args.path || ".";
5478
+ const r = resolveInMount(dirPath, args.mount);
5479
+ if (r.error) return mcpError(r.error);
5480
+ if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
5481
+ try {
5482
+ const entries = await readdir(r.resolved, { withFileTypes: true });
5483
+ const results = [];
5484
+ for (const entry of entries) {
5485
+ try {
5486
+ const s = await stat(path.join(r.resolved, entry.name));
5487
+ results.push({
5488
+ name: entry.name,
5489
+ type: entry.isDirectory() ? "dir" : "file",
5490
+ size: s.size,
5491
+ modified: s.mtime.toISOString(),
5492
+ });
5493
+ } catch {
5494
+ results.push({ name: entry.name, type: entry.isDirectory() ? "dir" : "file" });
5495
+ }
5496
+ }
5497
+ return mcpResult(JSON.stringify(results, null, 2));
5498
+ } catch (err) {
5499
+ if (err.code === "ENOENT") return mcpError(`Directory not found: ${dirPath}`);
5500
+ return mcpError(`List failed: ${err.message}`);
5501
+ }
5502
+ }
5503
+
5504
+ case "fs_grep": {
5505
+ const searchPath = args.path || ".";
5506
+ const r = resolveInMount(searchPath, args.mount);
5507
+ if (r.error) return mcpError(r.error);
5508
+ if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
5509
+
5510
+ const maxResults = args.max_results || 50;
5511
+ const rgArgs = [
5512
+ "--no-heading", "--line-number", "--color", "never",
5513
+ "--max-count", String(maxResults),
5514
+ ];
5515
+ if (args.context) rgArgs.push("-C", String(args.context));
5516
+ if (args.glob) rgArgs.push("--glob", args.glob);
5517
+ rgArgs.push(args.pattern, r.resolved);
5518
+
5519
+ return new Promise((resolve) => {
5520
+ let output = "";
5521
+ let killed = false;
5522
+ const proc = spawnProc(rgPath, rgArgs, { timeout: 15000, windowsHide: true });
5523
+
5524
+ proc.stdout.on("data", (chunk) => {
5525
+ output += chunk.toString();
5526
+ if (output.length > 65536) { // 64KB cap
5527
+ killed = true;
5528
+ proc.kill();
5529
+ }
5530
+ });
5531
+ proc.stderr.on("data", () => {}); // ignore stderr
5532
+
5533
+ proc.on("close", (code) => {
5534
+ if (killed) {
5535
+ resolve(mcpResult(output.slice(0, 65536) + "\n... (output truncated at 64KB)"));
5536
+ } else if (code === 1) {
5537
+ resolve(mcpResult("No matches found"));
5538
+ } else if (output) {
5539
+ // Make paths relative to mount
5540
+ const mountNorm = r.mount.path.replace(/\\/g, "/");
5541
+ const cleaned = output.replace(new RegExp(mountNorm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + "/?", "g"), "");
5542
+ resolve(mcpResult(cleaned));
5543
+ } else {
5544
+ resolve(mcpResult("No matches found"));
5545
+ }
5546
+ });
5547
+
5548
+ proc.on("error", (err) => {
5549
+ resolve(mcpError(`Grep failed: ${err.message}`));
5550
+ });
5551
+ });
5552
+ }
5553
+
5554
+ case "fs_glob": {
5555
+ const basePath = args.path || ".";
5556
+ const r = resolveInMount(basePath, args.mount);
5557
+ if (r.error) return mcpError(r.error);
5558
+ if (!checkAccess(r.mount, "r")) return mcpError("Read access denied on this mount");
5559
+ try {
5560
+ const matches = await fg(args.pattern, {
5561
+ cwd: r.resolved,
5562
+ dot: false,
5563
+ onlyFiles: true,
5564
+ ignore: ["**/node_modules/**", "**/.git/**"],
5565
+ });
5566
+ if (matches.length === 0) return mcpResult("No files matched");
5567
+ return mcpResult(matches.sort().join("\n"));
5568
+ } catch (err) {
5569
+ return mcpError(`Glob failed: ${err.message}`);
5570
+ }
5571
+ }
5572
+
5573
+ case "fs_delete": {
5574
+ const r = resolveInMount(args.path, args.mount);
5575
+ if (r.error) return mcpError(r.error);
5576
+ if (!checkAccess(r.mount, "d")) return mcpError("Delete access denied on this mount");
5577
+ try {
5578
+ await rm(r.resolved);
5579
+ return mcpResult(`Deleted: ${args.path}`);
5580
+ } catch (err) {
5581
+ if (err.code === "ENOENT") return mcpError(`Not found: ${args.path}`);
5582
+ return mcpError(`Delete failed: ${err.message}`);
5583
+ }
5584
+ }
5585
+
5586
+ case "fs_mkdir": {
5587
+ const r = resolveInMount(args.path, args.mount);
5588
+ if (r.error) return mcpError(r.error);
5589
+ if (!checkAccess(r.mount, "w")) return mcpError("Write access denied on this mount");
5590
+ try {
5591
+ await mkdir(r.resolved, { recursive: true });
5592
+ return mcpResult(`Created: ${args.path}`);
5593
+ } catch (err) {
5594
+ return mcpError(`Mkdir failed: ${err.message}`);
5595
+ }
5596
+ }
5597
+
5317
5598
  default:
5318
5599
  return mcpError(`Unknown tool: ${name}`);
5319
5600
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "1.5.36",
3
+ "version": "1.5.38",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,9 @@
19
19
  "conf": "^13.0.0",
20
20
  "inquirer": "^10.1.0",
21
21
  "node-fetch": "^3.3.2",
22
- "ora": "^8.1.0"
22
+ "ora": "^8.1.0",
23
+ "@vscode/ripgrep": "^1.15.9",
24
+ "fast-glob": "^3.3.2"
23
25
  },
24
26
  "engines": {
25
27
  "node": ">=18.0.0"