atabey 0.0.14 → 0.0.15

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.
Files changed (71) hide show
  1. package/PRIVACY.md +8 -14
  2. package/README.md +163 -14
  3. package/bin/cli.js +72 -5
  4. package/dist/framework-mcp/src/index.js +76 -3
  5. package/dist/framework-mcp/src/index.js.map +1 -1
  6. package/dist/framework-mcp/src/tools/messaging/approve_operation.d.ts +8 -8
  7. package/dist/framework-mcp/src/tools/messaging/approve_operation.js +11 -11
  8. package/dist/framework-mcp/src/tools/messaging/approve_operation.js.map +1 -1
  9. package/dist/framework-mcp/src/tools/messaging/ask_human.d.ts +8 -8
  10. package/dist/framework-mcp/src/tools/messaging/ask_human.js +8 -8
  11. package/dist/framework-mcp/src/tools/observability/check_ports.d.ts +1 -0
  12. package/dist/framework-mcp/src/tools/observability/check_ports.js +21 -7
  13. package/dist/framework-mcp/src/tools/observability/check_ports.js.map +1 -1
  14. package/dist/framework-mcp/src/tools/shell/run_command.js +138 -92
  15. package/dist/framework-mcp/src/tools/shell/run_command.js.map +1 -1
  16. package/dist/framework-mcp/src/utils/auth.js +4 -4
  17. package/dist/framework-mcp/src/utils/auth.js.map +1 -1
  18. package/dist/framework-mcp/src/utils/discipline.js +1 -1
  19. package/dist/framework-mcp/src/utils/discipline.js.map +1 -1
  20. package/dist/framework-mcp/src/utils/human-in-loop.js +1 -1
  21. package/dist/framework-mcp/src/utils/human-in-loop.js.map +1 -1
  22. package/dist/framework-mcp/src/utils/loop-detector.js +3 -3
  23. package/dist/framework-mcp/src/utils/loop-detector.js.map +1 -1
  24. package/dist/framework-mcp/src/utils/quality.js +4 -3
  25. package/dist/framework-mcp/src/utils/quality.js.map +1 -1
  26. package/dist/framework-mcp/src/utils/silent-router.d.ts +4 -2
  27. package/dist/framework-mcp/src/utils/silent-router.js +35 -9
  28. package/dist/framework-mcp/src/utils/silent-router.js.map +1 -1
  29. package/dist/framework-mcp/tests/tools/observability/check_ports.test.js +26 -12
  30. package/dist/framework-mcp/tests/tools/observability/check_ports.test.js.map +1 -1
  31. package/dist/framework-mcp/tests/tools/shell/run_command.test.js +38 -15
  32. package/dist/framework-mcp/tests/tools/shell/run_command.test.js.map +1 -1
  33. package/dist/src/cli/commands/init.js +14 -7
  34. package/dist/src/cli/commands/init.js.map +1 -1
  35. package/dist/src/cli/commands/mcp.js +3 -1
  36. package/dist/src/cli/commands/mcp.js.map +1 -1
  37. package/dist/src/cli/commands/script.js +8 -1
  38. package/dist/src/cli/commands/script.js.map +1 -1
  39. package/dist/src/modules/engines/agent-executor.js +11 -1
  40. package/dist/src/modules/engines/agent-executor.js.map +1 -1
  41. package/dist/src/modules/engines/agent-loop.d.ts +5 -5
  42. package/dist/src/modules/engines/agent-loop.js +5 -5
  43. package/dist/src/modules/engines/evaluation-engine.d.ts +12 -2
  44. package/dist/src/modules/engines/evaluation-engine.js +76 -9
  45. package/dist/src/modules/engines/evaluation-engine.js.map +1 -1
  46. package/dist/src/shared/pii.js +1 -1
  47. package/dist/src/shared/pii.js.map +1 -1
  48. package/dist/src/shared/storage.d.ts +43 -0
  49. package/dist/src/shared/storage.js +1 -1
  50. package/dist/src/shared/storage.js.map +1 -1
  51. package/dist/tests/modules/engines/agent-executor.test.js +1 -1
  52. package/dist/tests/modules/engines/agent-executor.test.js.map +1 -1
  53. package/framework-mcp/dist/dashboard/assets/{index-B2mYld0c.js → index-B_rK57vi.js} +17 -17
  54. package/framework-mcp/dist/dashboard/index.html +1 -1
  55. package/framework-mcp/dist/framework-mcp/src/index.js +76 -3
  56. package/framework-mcp/dist/framework-mcp/src/tools/messaging/approve_operation.js +11 -11
  57. package/framework-mcp/dist/framework-mcp/src/tools/messaging/ask_human.js +8 -8
  58. package/framework-mcp/dist/framework-mcp/src/tools/observability/check_ports.js +21 -7
  59. package/framework-mcp/dist/framework-mcp/src/tools/shell/run_command.js +138 -92
  60. package/framework-mcp/dist/framework-mcp/src/utils/auth.js +4 -4
  61. package/framework-mcp/dist/framework-mcp/src/utils/discipline.js +1 -1
  62. package/framework-mcp/dist/framework-mcp/src/utils/human-in-loop.js +1 -1
  63. package/framework-mcp/dist/framework-mcp/src/utils/loop-detector.js +3 -3
  64. package/framework-mcp/dist/framework-mcp/src/utils/quality.js +4 -3
  65. package/framework-mcp/dist/framework-mcp/src/utils/silent-router.js +35 -9
  66. package/framework-mcp/dist/src/modules/engines/evaluation-engine.js +169 -0
  67. package/framework-mcp/dist/src/shared/pii.js +1 -1
  68. package/framework-mcp/dist/src/shared/storage.js +1 -1
  69. package/framework-mcp/package.json +1 -1
  70. package/mcp.json +3 -2
  71. package/package.json +3 -4
@@ -7,7 +7,7 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
9
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
10
- <script type="module" crossorigin src="/assets/index-B2mYld0c.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-B_rK57vi.js"></script>
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
@@ -16,6 +16,28 @@ import { RESOURCES, handleReadResource } from "./resources/index.js";
16
16
  import { TOOLS, toolHandlers, toolSchemas } from "./tools/index.js";
17
17
  // ─── Paths ────────────────────────────────────────────────────────
18
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ // ─── Friendly Startup Validation ──────────────────────────────────
20
+ function validateEnvironment() {
21
+ // Node.js version check
22
+ const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
23
+ if (nodeMajor < 18) {
24
+ process.stderr.write(`
25
+ ╔══════════════════════════════════════════════════════════╗
26
+ ║ ⚠️ Unsupported Node.js version ║
27
+ ╠══════════════════════════════════════════════════════════╣
28
+ ║ Atabey MCP requires Node.js >= 18.0.0 ║
29
+ ║ ║
30
+ ║ Current version: ${process.versions.node} ║
31
+ ║ ║
32
+ ║ 📋 To fix this: ║
33
+ ║ nvm install 18 && nvm use 18 (if using nvm) ║
34
+ ║ brew install node (if using Homebrew) ║
35
+ ║ https://nodejs.org (official installer) ║
36
+ ╚══════════════════════════════════════════════════════════╝
37
+ `);
38
+ process.exit(1);
39
+ }
40
+ }
19
41
  function findPackageJson(startDir) {
20
42
  let currentDir = startDir;
21
43
  while (currentDir !== path.parse(currentDir).root) {
@@ -24,16 +46,68 @@ function findPackageJson(startDir) {
24
46
  return pkgPath;
25
47
  currentDir = path.dirname(currentDir);
26
48
  }
27
- throw new Error("Could not find package.json for atabey-mcp");
49
+ process.stderr.write(`
50
+ ╔══════════════════════════════════════════════════════════╗
51
+ ║ 🚧 Package not found ║
52
+ ╠══════════════════════════════════════════════════════════╣
53
+ ║ Atabey MCP package.json could not be located. ║
54
+ ║ ║
55
+ ║ This usually means: ║
56
+ ║ 1. The package was not installed correctly ║
57
+ ║ 2. Node modules are missing ║
58
+ ║ ║
59
+ ║ 📋 To fix this: ║
60
+ ║ npm install -g atabey (global install) ║
61
+ ║ npm install (local install) ║
62
+ ║ npm run build (if developing) ║
63
+ ╚══════════════════════════════════════════════════════════╝
64
+ `);
65
+ process.exit(1);
28
66
  }
67
+ validateEnvironment();
29
68
  const pkgPath = findPackageJson(__dirname);
30
69
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
31
70
  const serverVersion = pkg.version;
71
+ // Validate environment variables with friendly messages
32
72
  const PROJECT_ROOT = process.env.ATABEY_PROJECT_ROOT || process.cwd();
73
+ if (!process.env.ATABEY_PROJECT_ROOT) {
74
+ const transportMode = process.env.MCP_TRANSPORT || "unified";
75
+ if (transportMode !== "stdio") {
76
+ process.stderr.write(`
77
+ ╔══════════════════════════════════════════════════════════╗
78
+ ║ 💡 Tip: Set ATABEY_PROJECT_ROOT ║
79
+ ╠══════════════════════════════════════════════════════════╣
80
+ ║ In HTTP/SSE mode, it's recommended to explicitly set ║
81
+ ║ the project root to avoid confusion. ║
82
+ ║ ║
83
+ ║ Example: ║
84
+ ║ export ATABEY_PROJECT_ROOT=/path/to/your/project ║
85
+ ║ ║
86
+ ║ Using current directory: ${process.cwd()} ║
87
+ ╚══════════════════════════════════════════════════════════╝
88
+ `);
89
+ }
90
+ }
33
91
  const FRAMEWORK_DIR = process.env.ATABEY_FRAMEWORK_DIR || path.join(PROJECT_ROOT, ".atabey");
34
- const UI_DIST_PATH = path.join(__dirname, "../../dist/dashboard");
92
+ const UI_DIST_PATH = path.join(__dirname, "../../dashboard/dist");
35
93
  // ─── Ports ────────────────────────────────────────────────────────
36
94
  const PORT = parseInt(process.env.MCP_PORT || "5858", 10);
95
+ if (isNaN(PORT) || PORT < 1 || PORT > 65535) {
96
+ process.stderr.write(`
97
+ ╔══════════════════════════════════════════════════════════╗
98
+ ║ ❌ Invalid MCP_PORT ║
99
+ ╠══════════════════════════════════════════════════════════╣
100
+ ║ The port must be a number between 1 and 65535. ║
101
+ ║ ║
102
+ ║ You set: MCP_PORT=${process.env.MCP_PORT || "(empty)"} ║
103
+ ║ ║
104
+ ║ 📋 To fix this: ║
105
+ ║ export MCP_PORT=5858 (default) ║
106
+ ║ export MCP_PORT=8080 (alternative) ║
107
+ ╚══════════════════════════════════════════════════════════╝
108
+ `);
109
+ process.exit(1);
110
+ }
37
111
  const HOST = process.env.MCP_HOST || "0.0.0.0";
38
112
  // ─── MCP Server ───────────────────────────────────────────────────
39
113
  const server = new Server({ name: "atabey-mcp", version: serverVersion }, { capabilities: { tools: {}, resources: {} } });
@@ -242,7 +316,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
242
316
  // [AUTO-ROLLBACK] Check write results for governance violations
243
317
  if (name === "write_file" || name === "replace_text" || name === "patch_file") {
244
318
  try {
245
- const responseText = result.content?.filter(b => b.type === "text").map(b => b.text).join(" ") || "";
246
319
  const filePath = maskedArgs.path || "";
247
320
  const content = maskedArgs.content || "";
248
321
  if (filePath && content) {
@@ -1,21 +1,21 @@
1
- import { approveOperation, rejectOperation, getPendingApprovals } from "../../utils/human-in-loop.js";
1
+ import { approveOperation, getPendingApprovals, rejectOperation } from "../../utils/human-in-loop.js";
2
2
  /**
3
- * ─── APPROVE OPERATION — In-Chat Risk Gate ──────────────────────────
3
+ * MCP Tool: approve_operation
4
4
  *
5
- * AI CLI kullanımında "atabey approve <traceId>" CLI komutu çalıştırmak mümkün
6
- * değildir — chat'e yazılan şey CLI komutu olarak değil mesaj olarak işlenir.
5
+ * In AI CLI (Claude Code, Gemini CLI), running "atabey approve <traceId>"
6
+ * is not possible anything typed in chat is treated as a message, not a CLI command.
7
7
  *
8
- * Bu tool, risk gate approval flow'unu doğrudan MCP tool çağrısına taşır:
8
+ * This tool moves the risk gate approval flow directly into an MCP tool call:
9
9
  *
10
- * 1. Atabey yüksek riskli bir operasyonu bloklar
11
- * 2. Geliştiriciye: "approve_operation tool'unu çağır" der
12
- * 3. AI bu tool'u çağırır operasyon onaylanır/reddedilir
10
+ * 1. Atabey blocks a high-risk operation
11
+ * 2. Tells the developer: "call the approve_operation tool"
12
+ * 3. AI calls this tool → operation is approved/rejected
13
13
  *
14
- * Geliştirici terminale geçmek zorunda kalmaz.
14
+ * Developer never has to switch to terminal.
15
15
  */
16
16
  export async function handleApproveOperation(_root, args) {
17
17
  const { action, traceId, reason } = args;
18
- // LIST: Bekleyen approval'ları göster
18
+ // LIST: Show pending approvals
19
19
  if (action === "list") {
20
20
  const pending = getPendingApprovals();
21
21
  if (pending.length === 0) {
@@ -42,7 +42,7 @@ export async function handleApproveOperation(_root, args) {
42
42
  content: [{ type: "text", text: lines.join("\n") }],
43
43
  };
44
44
  }
45
- // APPROVE or REJECT: traceId zorunlu
45
+ // APPROVE or REJECT: traceId is required
46
46
  if (!traceId) {
47
47
  return {
48
48
  isError: true,
@@ -3,16 +3,16 @@ import path from "path";
3
3
  /**
4
4
  * ─── ASK HUMAN — File Bridge ─────────────────────────────────────────
5
5
  *
6
- * AI CLI'lar (Claude Code, Gemini CLI) MCP server stdio subprocess olarak
7
- * başlatır. Bu subprocess'te stdin isTTY=false olur (MCP JSON-RPC için kullanılır).
6
+ * AI CLIs (Claude Code, Gemini CLI) start the MCP server as a stdio subprocess.
7
+ * In this subprocess, stdin is isTTY=false (used for MCP JSON-RPC).
8
8
  *
9
- * Çözüm: File-based Q&A bridge.
10
- * 1. Soru → .atabey/hitl/question.txt dosyasına yazılır
11
- * 2. Geliştirici `atabey hitl` komutuyla cevap girer
12
- * 3. Cevap → .atabey/hitl/answer.txt dosyasına yazılır
13
- * 4. Bu handler cevabı okur ve AI'ya döner
9
+ * Solution: File-based Q&A bridge.
10
+ * 1. Questionwritten to .atabey/hitl/question.txt
11
+ * 2. Developer answers via `atabey hitl` command
12
+ * 3. Answerwritten to .atabey/hitl/answer.txt
13
+ * 4. This handler reads the answer and returns it to the AI
14
14
  *
15
- * Dashboard: /api/approvals endpoint'i üzerinden web dashboard'dan da cevaplanabilir.
15
+ * Dashboard: Can also be answered via /api/approvals endpoint on the web dashboard.
16
16
  */
17
17
  export async function handleAskHuman(root, args) {
18
18
  const timeoutSeconds = args.timeoutSeconds ?? 120;
@@ -1,18 +1,32 @@
1
- import { execSync } from "child_process";
1
+ import { spawnSync } from "child_process";
2
2
  /**
3
3
  * Checks for active network ports and their status.
4
+ * Uses spawnSync instead of execSync to prevent shell injection.
4
5
  */
5
6
  export function handleCheckPorts(projectRoot, args) {
6
7
  const rawFilter = args.filter || "";
7
8
  // Sanitize filter to prevent command injection (allow only alphanumeric, dots, colons, dashes, underscores)
8
9
  const filter = rawFilter.replace(/[^a-zA-Z0-9.:_-]/g, "");
9
10
  try {
10
- // Using 'lsof -i -P -n' to list open files and network connections
11
- // Note: may require permissions or behave differently on non-Unix systems
12
- const command = process.platform === "win32"
13
- ? `netstat -ano | findstr LISTENING ${filter ? `| findstr ${filter}` : ""}`
14
- : `lsof -i -P -n | grep LISTEN ${filter ? `| grep ${filter}` : ""}`;
15
- const output = execSync(command, { encoding: "utf8" });
11
+ let output;
12
+ if (process.platform === "win32") {
13
+ const result = spawnSync("netstat", ["-ano"], { encoding: "utf8" });
14
+ if (result.error)
15
+ throw result.error;
16
+ const lines = (result.stdout || "").split("\n").filter(l => l.includes("LISTENING"));
17
+ output = filter
18
+ ? lines.filter(l => l.includes(filter)).join("\n")
19
+ : lines.join("\n");
20
+ }
21
+ else {
22
+ const result = spawnSync("lsof", ["-i", "-P", "-n"], { encoding: "utf8" });
23
+ if (result.error)
24
+ throw result.error;
25
+ const lines = (result.stdout || "").split("\n").filter(l => l.includes("LISTEN"));
26
+ output = filter
27
+ ? lines.filter(l => l.includes(filter)).join("\n")
28
+ : lines.join("\n");
29
+ }
16
30
  return {
17
31
  content: [{
18
32
  type: "text",
@@ -1,102 +1,129 @@
1
- import { exec } from "child_process";
1
+ import { spawn } from "child_process";
2
2
  import { Metrics } from "../../utils/metrics.js";
3
+ // Each entry defines the executable and its allowed args prefix
3
4
  const COMMAND_ALLOW_LIST = [
4
5
  // Package managers
5
- "npm test",
6
- "npm run",
7
- "npm install",
8
- "npm ci",
9
- "npm audit",
10
- "npx vitest",
11
- "npx tsc",
12
- "npx eslint",
13
- "yarn test",
14
- "yarn run",
15
- "yarn install",
16
- "pnpm test",
17
- "pnpm run",
18
- "pnpm install",
6
+ { cmd: "npm", args: ["test"] },
7
+ { cmd: "npm", args: ["run"] },
8
+ { cmd: "npm", args: ["install"] },
9
+ { cmd: "npm", args: ["ci"] },
10
+ { cmd: "npm", args: ["audit"] },
11
+ { cmd: "npx", args: ["vitest"] },
12
+ { cmd: "npx", args: ["tsc"] },
13
+ { cmd: "npx", args: ["eslint"] },
14
+ { cmd: "yarn", args: ["test"] },
15
+ { cmd: "yarn", args: ["run"] },
16
+ { cmd: "yarn", args: ["install"] },
17
+ { cmd: "pnpm", args: ["test"] },
18
+ { cmd: "pnpm", args: ["run"] },
19
+ { cmd: "pnpm", args: ["install"] },
19
20
  // Git
20
- "git status",
21
- "git diff",
22
- "git log",
23
- "git branch",
24
- "git stash",
25
- "git add",
26
- "git commit",
27
- "git push",
28
- "git pull",
29
- "git fetch",
30
- "git merge",
31
- "git rebase",
32
- "git checkout",
33
- "git reset",
21
+ { cmd: "git", args: ["status"] },
22
+ { cmd: "git", args: ["diff"] },
23
+ { cmd: "git", args: ["log"] },
24
+ { cmd: "git", args: ["branch"] },
25
+ { cmd: "git", args: ["stash"] },
26
+ { cmd: "git", args: ["add"] },
27
+ { cmd: "git", args: ["commit"] },
28
+ { cmd: "git", args: ["push"] },
29
+ { cmd: "git", args: ["pull"] },
30
+ { cmd: "git", args: ["fetch"] },
31
+ { cmd: "git", args: ["merge"] },
32
+ { cmd: "git", args: ["rebase"] },
33
+ { cmd: "git", args: ["checkout"] },
34
+ { cmd: "git", args: ["reset"] },
34
35
  // Build tools
35
- "npm run build",
36
- "npx tsc",
37
- "npx vite build",
38
- "npx webpack",
39
- "npx rollup",
40
- "npx esbuild",
41
- "make",
42
- "cmake",
43
- "dotnet test",
44
- "dotnet format",
45
- "dotnet build",
46
- "dotnet run",
47
- "dotnet restore",
48
- "go test",
49
- "go fmt",
50
- "go build",
51
- "go run",
52
- "go vet",
53
- "go mod",
54
- "pytest",
55
- "ruff check",
56
- "ruff format",
57
- "mvn",
58
- "./gradlew",
59
- "cargo test",
60
- "cargo build",
61
- "cargo check",
62
- "cargo fmt",
63
- // File operations (safe) — NOTE: "rm -rf" and "rm -r" intentionally excluded.
64
- // Only "rmdir" is allowed to prevent dangerous commands like "rm -rf /".
65
- "mkdir",
66
- "cp",
67
- "mv",
68
- "rmdir",
69
- "cat",
70
- "ls",
71
- "touch",
72
- "head",
73
- "tail",
74
- "wc",
75
- "grep",
76
- "find",
77
- "sort",
78
- "uniq",
79
- "echo",
80
- "pwd",
81
- "which",
82
- "file",
83
- "du",
84
- "df",
36
+ { cmd: "make" },
37
+ { cmd: "cmake" },
38
+ { cmd: "dotnet", args: ["test"] },
39
+ { cmd: "dotnet", args: ["format"] },
40
+ { cmd: "dotnet", args: ["build"] },
41
+ { cmd: "dotnet", args: ["run"] },
42
+ { cmd: "dotnet", args: ["restore"] },
43
+ { cmd: "go", args: ["test"] },
44
+ { cmd: "go", args: ["fmt"] },
45
+ { cmd: "go", args: ["build"] },
46
+ { cmd: "go", args: ["run"] },
47
+ { cmd: "go", args: ["vet"] },
48
+ { cmd: "go", args: ["mod"] },
49
+ { cmd: "pytest" },
50
+ { cmd: "ruff", args: ["check"] },
51
+ { cmd: "ruff", args: ["format"] },
52
+ { cmd: "mvn" },
53
+ { cmd: "./gradlew" },
54
+ { cmd: "cargo", args: ["test"] },
55
+ { cmd: "cargo", args: ["build"] },
56
+ { cmd: "cargo", args: ["check"] },
57
+ { cmd: "cargo", args: ["fmt"] },
58
+ // File operations (safe) — spawn prevents shell injection
59
+ { cmd: "mkdir" },
60
+ { cmd: "cp" },
61
+ { cmd: "mv" },
62
+ { cmd: "rmdir" },
63
+ { cmd: "cat" },
64
+ { cmd: "ls" },
65
+ { cmd: "touch" },
66
+ { cmd: "head" },
67
+ { cmd: "tail" },
68
+ { cmd: "wc" },
69
+ { cmd: "grep" },
70
+ { cmd: "find" },
71
+ { cmd: "sort" },
72
+ { cmd: "uniq" },
73
+ { cmd: "echo" },
74
+ { cmd: "pwd" },
75
+ { cmd: "which" },
76
+ { cmd: "file" },
77
+ { cmd: "du" },
78
+ { cmd: "df" },
85
79
  ];
86
80
  const TIMEOUT = 30000; // 30 seconds
81
+ /**
82
+ * Parse a command string into executable + args array.
83
+ * Uses spawn for safety (no shell injection).
84
+ */
85
+ function parseCommand(cmdString) {
86
+ const parts = cmdString.trim().split(/\s+/);
87
+ if (parts.length === 0)
88
+ return null;
89
+ return { cmd: parts[0], args: parts.slice(1) };
90
+ }
91
+ /**
92
+ * Check if a parsed command matches the allow list.
93
+ */
94
+ function isAllowed(parsed) {
95
+ return COMMAND_ALLOW_LIST.some(entry => {
96
+ if (entry.cmd !== parsed.cmd)
97
+ return false;
98
+ if (!entry.args)
99
+ return true; // Any args allowed for this cmd
100
+ if (!parsed.args || parsed.args.length < entry.args.length)
101
+ return false;
102
+ // Check that args start with the allowed prefix
103
+ return entry.args.every((arg, i) => parsed.args[i] === arg);
104
+ });
105
+ }
87
106
  export function handleRunCommand(projectRoot, args) {
88
107
  const command = args.command;
89
- const isAllowed = COMMAND_ALLOW_LIST.some(allowedCmd => command.startsWith(allowedCmd));
90
- if (!isAllowed) {
91
- const errorMsg = `Command not allowed: "${command}". Only commands starting with the following are allowed: ${COMMAND_ALLOW_LIST.join(", ")}`;
108
+ // Parse command into executable + args
109
+ const parsed = parseCommand(command);
110
+ if (!parsed) {
111
+ return Promise.resolve({
112
+ content: [{ type: "text", text: "ERROR: Empty command." }],
113
+ isError: true,
114
+ });
115
+ }
116
+ if (!isAllowed(parsed)) {
117
+ const allowedList = COMMAND_ALLOW_LIST.map(e => e.args ? `${e.cmd} ${e.args.join(" ")}` : e.cmd);
118
+ const errorMsg = `Command not allowed: "${command}". Only the following are allowed: ${allowedList.join(", ")}`;
92
119
  Metrics.logError(projectRoot, "@mcp", `run_shell_command: ${command} (denied)`, errorMsg);
93
120
  return Promise.resolve({
94
121
  content: [{ type: "text", text: `ERROR: ${errorMsg}` }],
95
122
  isError: true,
96
123
  });
97
124
  }
98
- // Harden to prevent shell command injection / chaining
99
- const hasShellMetacharacters = /[;&|><$`\n\r]/.test(command);
125
+ // Reject any args containing shell metacharacters
126
+ const hasShellMetacharacters = parsed.args.some(a => /[;&|><$`\n\r]/.test(a));
100
127
  if (hasShellMetacharacters) {
101
128
  const errorMsg = "Command rejected: Shell metacharacters are forbidden to prevent command injection.";
102
129
  Metrics.logError(projectRoot, "@mcp", `run_shell_command: ${command} (denied: metacharacters)`, errorMsg);
@@ -106,12 +133,30 @@ export function handleRunCommand(projectRoot, args) {
106
133
  });
107
134
  }
108
135
  return new Promise((resolve) => {
109
- exec(command, { cwd: projectRoot, timeout: TIMEOUT }, (error, stdout, stderr) => {
136
+ const child = spawn(parsed.cmd, parsed.args, {
137
+ cwd: projectRoot,
138
+ timeout: TIMEOUT,
139
+ shell: false,
140
+ stdio: ["ignore", "pipe", "pipe"],
141
+ });
142
+ let stdout = "";
143
+ let stderr = "";
144
+ child.stdout.on("data", (data) => { stdout += data.toString(); });
145
+ child.stderr.on("data", (data) => { stderr += data.toString(); });
146
+ child.on("error", (err) => {
147
+ const errorMsg = `Failed to start command: ${err.message}`;
148
+ Metrics.logError(projectRoot, "@mcp", `run_shell_command: ${command}`, errorMsg);
149
+ resolve({
150
+ content: [{ type: "text", text: `ERROR: ${errorMsg}` }],
151
+ isError: true,
152
+ });
153
+ });
154
+ child.on("close", (code) => {
110
155
  const output = stdout + stderr;
111
156
  const tokens = Metrics.estimateTokens(output);
112
157
  Metrics.logUsage(projectRoot, "@mcp", `run_shell_command: ${command}`, tokens);
113
- if (error) {
114
- const errorMsg = `Command failed with exit code ${error.code}: ${error.message}.`;
158
+ if (code !== 0) {
159
+ const errorMsg = `Command failed with exit code ${code}.`;
115
160
  Metrics.logError(projectRoot, "@mcp", `run_shell_command: ${command}`, errorMsg);
116
161
  resolve({
117
162
  content: [{ type: "text", text: `ERROR: ${errorMsg}. Output: ${output}` }],
@@ -121,11 +166,12 @@ export function handleRunCommand(projectRoot, args) {
121
166
  }
122
167
  // Truncate long outputs
123
168
  const MAX_OUTPUT_LENGTH = 5000;
124
- let truncatedOutput = output;
125
- if (output.length > MAX_OUTPUT_LENGTH) {
126
- truncatedOutput = output.substring(0, MAX_OUTPUT_LENGTH) + "... [TRUNCATED] ..."; // Simplified
127
- }
128
- resolve({ content: [{ type: "text", text: truncatedOutput }] });
169
+ const truncatedOutput = output.length > MAX_OUTPUT_LENGTH
170
+ ? output.substring(0, MAX_OUTPUT_LENGTH) + "... [TRUNCATED] ..."
171
+ : output;
172
+ resolve({
173
+ content: [{ type: "text", text: truncatedOutput }],
174
+ });
129
175
  });
130
176
  });
131
177
  }
@@ -27,14 +27,14 @@ export function initAuth() {
27
27
  }).filter(u => u.token);
28
28
  }
29
29
  if (masterToken) {
30
- console.error(`[AUTH] Master token configured (${masterToken.length} chars)`);
30
+ process.stderr.write(`[AUTH] Master token configured (${masterToken.length} chars)\n`);
31
31
  }
32
32
  if (authUsers.length > 0) {
33
- console.error(`[AUTH] ${authUsers.length} user(s) configured: ${authUsers.map(u => u.name).join(", ")}`);
33
+ process.stderr.write(`[AUTH] ${authUsers.length} user(s) configured: ${authUsers.map(u => u.name).join(", ")}\n`);
34
34
  }
35
35
  if (!masterToken && authUsers.length === 0) {
36
- console.error("[AUTH] No authentication configured - OPEN ACCESS");
37
- console.error("[AUTH] Set MCP_AUTH_TOKEN=<key> or MCP_AUTH_USERS=user1:key1,user2:key2 to enable");
36
+ process.stderr.write("[AUTH] No authentication configured - OPEN ACCESS\n");
37
+ process.stderr.write("[AUTH] Set MCP_AUTH_TOKEN=<key> or MCP_AUTH_USERS=user1:key1,user2:key2 to enable\n");
38
38
  }
39
39
  }
40
40
  /**
@@ -202,7 +202,7 @@ export function getDisciplineStats(agent) {
202
202
  */
203
203
  export function getAllDisciplineStats() {
204
204
  const stats = {};
205
- for (const [agent, _discipline] of agentDiscipline) {
205
+ for (const [agent] of agentDiscipline) {
206
206
  stats[agent] = getDisciplineStats(agent);
207
207
  }
208
208
  return stats;
@@ -196,7 +196,7 @@ export function getPendingApprovals() {
196
196
  const now = Date.now();
197
197
  const pending = [];
198
198
  // Clean expired
199
- for (const [traceId, request] of activeApprovals) {
199
+ for (const [, request] of activeApprovals) {
200
200
  if (request.status === "PENDING" && now > request.expiresAt) {
201
201
  request.status = "EXPIRED";
202
202
  }
@@ -274,7 +274,7 @@ export function recordAndCheck(agent, toolName, args) {
274
274
  /**
275
275
  * Apply cooldown to an agent.
276
276
  */
277
- function applyCooldown(history, alert, agent) {
277
+ function applyCooldown(history, alert, _agent) {
278
278
  history.inCooldown = true;
279
279
  history.cooldownUntil = alert.cooldownUntil || Date.now() + CONFIG.COOLDOWN_MS;
280
280
  history.cooldownCount++;
@@ -347,8 +347,8 @@ export function getLoopStats(agent) {
347
347
  */
348
348
  export function getAllLoopStats() {
349
349
  const stats = {};
350
- for (const [agent] of agentHistories) {
351
- stats[agent] = getLoopStats(agent);
350
+ for (const [agentName] of agentHistories) {
351
+ stats[agentName] = getLoopStats(agentName);
352
352
  }
353
353
  return stats;
354
354
  }
@@ -93,9 +93,10 @@ export function analyzePathQuality(projectRoot, targetPath) {
93
93
  }
94
94
  // Check nesting depth
95
95
  lines.forEach((line, idx) => {
96
- const indentMatch = line.match(/^(?:\s{2}){5,}/);
97
- if (indentMatch && !line.trim().startsWith("//") && !line.trim().startsWith("*")) {
98
- const depth = Math.floor(indentMatch[0].length / 2);
96
+ const leadingSpaces = line.match(/^(\s*)/)?.[0].length || 0;
97
+ if (leadingSpaces >= 10 && !line.trim().startsWith("//") && !line.trim().startsWith("*")) {
98
+ const indentSize = (leadingSpaces % 4 === 0) ? 4 : 2;
99
+ const depth = Math.floor(leadingSpaces / indentSize);
99
100
  if (depth > 4) {
100
101
  deepNesting++;
101
102
  issues.push({