@praneeth_54/agentdoctor 2.0.1 → 2.1.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +69 -1
  2. package/README.md +65 -14
  3. package/dist/agent/approvals.d.ts +24 -0
  4. package/dist/agent/approvals.js +64 -0
  5. package/dist/agent/chat/index.d.ts +7 -0
  6. package/dist/agent/chat/index.js +5 -0
  7. package/dist/agent/chat/memory.d.ts +44 -0
  8. package/dist/agent/chat/memory.js +103 -0
  9. package/dist/agent/chat/project-summary.d.ts +16 -0
  10. package/dist/agent/chat/project-summary.js +70 -0
  11. package/dist/agent/chat/prompts.d.ts +6 -0
  12. package/dist/agent/chat/prompts.js +39 -0
  13. package/dist/agent/chat/response.d.ts +19 -0
  14. package/dist/agent/chat/response.js +109 -0
  15. package/dist/agent/chat/service.d.ts +42 -0
  16. package/dist/agent/chat/service.js +252 -0
  17. package/dist/agent/chat/types.d.ts +48 -0
  18. package/dist/agent/chat/types.js +1 -0
  19. package/dist/agent/context/retrieve.d.ts +21 -0
  20. package/dist/agent/context/retrieve.js +117 -0
  21. package/dist/agent/context/truth.d.ts +6 -0
  22. package/dist/agent/context/truth.js +19 -0
  23. package/dist/agent/context/types.d.ts +23 -0
  24. package/dist/agent/context/types.js +4 -0
  25. package/dist/agent/index.d.ts +26 -0
  26. package/dist/agent/index.js +14 -0
  27. package/dist/agent/loop.d.ts +51 -0
  28. package/dist/agent/loop.js +222 -0
  29. package/dist/agent/modes.d.ts +18 -0
  30. package/dist/agent/modes.js +102 -0
  31. package/dist/agent/plan.d.ts +30 -0
  32. package/dist/agent/plan.js +121 -0
  33. package/dist/agent/runtime.d.ts +67 -0
  34. package/dist/agent/runtime.js +180 -0
  35. package/dist/agent/state.d.ts +30 -0
  36. package/dist/agent/state.js +95 -0
  37. package/dist/agent/student.d.ts +56 -0
  38. package/dist/agent/student.js +230 -0
  39. package/dist/agent/tools/execute.d.ts +18 -0
  40. package/dist/agent/tools/execute.js +355 -0
  41. package/dist/agent/tools/index.d.ts +6 -0
  42. package/dist/agent/tools/index.js +5 -0
  43. package/dist/agent/tools/registry.d.ts +7 -0
  44. package/dist/agent/tools/registry.js +212 -0
  45. package/dist/agent/tools/run.d.ts +24 -0
  46. package/dist/agent/tools/run.js +44 -0
  47. package/dist/agent/tools/types.d.ts +32 -0
  48. package/dist/agent/tools/types.js +10 -0
  49. package/dist/agent/tools/write.d.ts +24 -0
  50. package/dist/agent/tools/write.js +121 -0
  51. package/dist/agent/verify.d.ts +29 -0
  52. package/dist/agent/verify.js +210 -0
  53. package/dist/ai/config.d.ts +22 -0
  54. package/dist/ai/config.js +68 -0
  55. package/dist/ai/index.d.ts +17 -0
  56. package/dist/ai/index.js +52 -0
  57. package/dist/ai/providers/mock.d.ts +20 -0
  58. package/dist/ai/providers/mock.js +84 -0
  59. package/dist/ai/providers/none.d.ts +6 -0
  60. package/dist/ai/providers/none.js +23 -0
  61. package/dist/ai/providers/openai-compatible.d.ts +21 -0
  62. package/dist/ai/providers/openai-compatible.js +151 -0
  63. package/dist/ai/redact.d.ts +8 -0
  64. package/dist/ai/redact.js +21 -0
  65. package/dist/ai/types.d.ts +71 -0
  66. package/dist/ai/types.js +6 -0
  67. package/dist/cli/commands/agent.d.ts +27 -0
  68. package/dist/cli/commands/agent.js +153 -0
  69. package/dist/cli/commands/chat.d.ts +14 -0
  70. package/dist/cli/commands/chat.js +155 -0
  71. package/dist/cli/commands/learn.d.ts +12 -0
  72. package/dist/cli/commands/learn.js +107 -0
  73. package/dist/cli/program.js +92 -0
  74. package/dist/constants.d.ts +1 -1
  75. package/dist/constants.js +1 -1
  76. package/dist/dashboard/server.d.ts +6 -0
  77. package/dist/dashboard/server.js +89 -1
  78. package/dist/enforcement/runner.d.ts +5 -0
  79. package/dist/enforcement/runner.js +51 -4
  80. package/dist/index.d.ts +13 -0
  81. package/dist/index.js +8 -0
  82. package/dist/languages/go.js +7 -2
  83. package/dist/languages/php.d.ts +1 -0
  84. package/dist/languages/php.js +11 -2
  85. package/dist/languages/python.d.ts +6 -2
  86. package/dist/languages/python.js +39 -9
  87. package/dist/mcp/agent/registry.d.ts +13 -0
  88. package/dist/mcp/agent/registry.js +234 -0
  89. package/dist/mcp/agentdoctor/server.js +8 -1
  90. package/dist/security/paths.js +43 -15
  91. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -36,3 +36,11 @@ export { buildFixPlan } from "./core/fix/plan.js";
36
36
  export { applyFixPlan } from "./core/fix/apply.js";
37
37
  export { runFix } from "./core/fix/run.js";
38
38
  export { evaluatePolicy, evaluateScanPolicy, evaluateVerifyPolicy, parseFailOnRules, parseSeverityGate, } from "./core/policy/evaluate.js";
39
+ /** AgentDoctor 2.1 AI provider layer (optional — core works without AI). */
40
+ export { createModelProvider, loadAiConfig, publicAiConfig, redactForModel, AI_PROVIDER_REQUIRED_MESSAGE, NoneModelProvider, MockModelProvider, OpenAiCompatibleProvider, } from "./ai/index.js";
41
+ /** AgentDoctor 2.1 Project AI Agent core (M1 scaffolding). */
42
+ export { AgentState, AgentStateMachine, AgentRuntime, DEFAULT_AGENT_LIMITS, retrieveProjectContext, truthLabelHelp, minTruth, } from "./agent/index.js";
43
+ export { ChatService, createChatService, ChatMemory, CHAT_PROVIDER_NONE_MESSAGE, buildChatTurnResponse, formatChatResponseForCli, summarizeProjectForChat, } from "./agent/chat/index.js";
44
+ /** AgentDoctor 2.1 agent tools + plan (M3). */
45
+ export { listAgentToolSpecs, getToolSpec, riskForTool, executeAgentTool, isReadTool, newToolCall, evaluateApproval, formatApprovalPrompt, buildAgentPlan, formatAgentPlan, approvePlan, runCodingLoop, verifyAgentWork, formatVerificationReport, } from "./agent/index.js";
46
+ export { getModeProfile, defaultStudentMode, parseAgentMode, modeAllowsMutation, modeBlocksToolCategory, StudentService, } from "./agent/index.js";
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ const GO_PROBE_TIMEOUT_MS = 5_000;
2
3
  /**
3
4
  * Go toolchain bridge placeholder.
4
5
  * Prefer real `go/ast` via `go run` when the Go toolchain is present; until an
@@ -6,8 +7,12 @@ import { spawnSync } from "node:child_process";
6
7
  * Today: always unsupported (no bundled extractor; avoids fake AST claims).
7
8
  */
8
9
  export function goAvailable() {
9
- const r = spawnSync("go", ["version"], { encoding: "utf8" });
10
- return r.status === 0;
10
+ const r = spawnSync("go", ["version"], {
11
+ encoding: "utf8",
12
+ timeout: GO_PROBE_TIMEOUT_MS,
13
+ windowsHide: true,
14
+ });
15
+ return r.status === 0 && !r.error;
11
16
  }
12
17
  export const goAdapter = {
13
18
  id: "go",
@@ -2,6 +2,7 @@ import type { LanguageAdapter } from "./types.js";
2
2
  /**
3
3
  * Real PHP token extractor via `php -r` + token_get_all (requires `php` on PATH).
4
4
  * Not regex. If php is missing, capabilities are unsupported.
5
+ * Probe timeout skips hanging Windows Store / stub `php` binaries.
5
6
  */
6
7
  export declare function phpAvailable(): boolean;
7
8
  export declare const phpAdapter: LanguageAdapter;
@@ -3,13 +3,20 @@ import { createHash } from "node:crypto";
3
3
  function sid(kind, key) {
4
4
  return `${kind}:${createHash("sha256").update(key).digest("hex").slice(0, 12)}`;
5
5
  }
6
+ const PHP_PROBE_TIMEOUT_MS = 5_000;
7
+ const PHP_EXTRACT_TIMEOUT_MS = 15_000;
6
8
  /**
7
9
  * Real PHP token extractor via `php -r` + token_get_all (requires `php` on PATH).
8
10
  * Not regex. If php is missing, capabilities are unsupported.
11
+ * Probe timeout skips hanging Windows Store / stub `php` binaries.
9
12
  */
10
13
  export function phpAvailable() {
11
- const r = spawnSync("php", ["-r", "echo PHP_VERSION;"], { encoding: "utf8" });
12
- return r.status === 0 && Boolean(r.stdout?.trim());
14
+ const r = spawnSync("php", ["-r", "echo PHP_VERSION;"], {
15
+ encoding: "utf8",
16
+ timeout: PHP_PROBE_TIMEOUT_MS,
17
+ windowsHide: true,
18
+ });
19
+ return r.status === 0 && !r.error && Boolean(r.stdout?.trim());
13
20
  }
14
21
  const PHP_EXTRACTOR = `
15
22
  $source = stream_get_contents(STDIN);
@@ -117,6 +124,8 @@ export const phpAdapter = {
117
124
  input: source,
118
125
  encoding: "utf8",
119
126
  maxBuffer: 8 * 1024 * 1024,
127
+ timeout: PHP_EXTRACT_TIMEOUT_MS,
128
+ windowsHide: true,
120
129
  });
121
130
  if (r.status !== 0 || !r.stdout) {
122
131
  return {
@@ -1,7 +1,11 @@
1
1
  import type { LanguageAdapter } from "./types.js";
2
2
  /**
3
- * Real Python AST via the CPython `ast` module (requires `python3` on PATH).
4
- * Not regex. If python3 is missing, capabilities are unsupported.
3
+ * Resolve a working CPython binary, or null if none responds in time.
4
+ */
5
+ export declare function resolvePythonBin(): string | null;
6
+ /**
7
+ * Real Python AST via the CPython `ast` module (requires python on PATH).
8
+ * Not regex. If no interpreter is available, capabilities are unsupported.
5
9
  */
6
10
  export declare function pythonAvailable(): boolean;
7
11
  export declare const pythonAdapter: LanguageAdapter;
@@ -3,13 +3,40 @@ import { createHash } from "node:crypto";
3
3
  function sid(kind, key) {
4
4
  return `${kind}:${createHash("sha256").update(key).digest("hex").slice(0, 12)}`;
5
5
  }
6
+ const PYTHON_PROBE_TIMEOUT_MS = 5_000;
6
7
  /**
7
- * Real Python AST via the CPython `ast` module (requires `python3` on PATH).
8
- * Not regex. If python3 is missing, capabilities are unsupported.
8
+ * Prefer real interpreters; skip hanging Windows Store `python3` stubs via timeout.
9
+ */
10
+ function pythonCandidates() {
11
+ return process.platform === "win32" ? ["python", "py", "python3"] : ["python3", "python"];
12
+ }
13
+ let cachedPythonBin;
14
+ /**
15
+ * Resolve a working CPython binary, or null if none responds in time.
16
+ */
17
+ export function resolvePythonBin() {
18
+ if (cachedPythonBin !== undefined)
19
+ return cachedPythonBin;
20
+ for (const bin of pythonCandidates()) {
21
+ const r = spawnSync(bin, ["-c", "import ast, json"], {
22
+ encoding: "utf8",
23
+ timeout: PYTHON_PROBE_TIMEOUT_MS,
24
+ windowsHide: true,
25
+ });
26
+ if (r.status === 0 && !r.error) {
27
+ cachedPythonBin = bin;
28
+ return bin;
29
+ }
30
+ }
31
+ cachedPythonBin = null;
32
+ return null;
33
+ }
34
+ /**
35
+ * Real Python AST via the CPython `ast` module (requires python on PATH).
36
+ * Not regex. If no interpreter is available, capabilities are unsupported.
9
37
  */
10
38
  export function pythonAvailable() {
11
- const r = spawnSync("python3", ["-c", "import ast, json"], { encoding: "utf8" });
12
- return r.status === 0;
39
+ return resolvePythonBin() !== null;
13
40
  }
14
41
  const PY_EXTRACTOR = `
15
42
  import ast, json, sys
@@ -59,7 +86,8 @@ export const pythonAdapter = {
59
86
  };
60
87
  },
61
88
  async parse(filePath, source) {
62
- if (!pythonAvailable()) {
89
+ const pythonBin = resolvePythonBin();
90
+ if (!pythonBin) {
63
91
  return {
64
92
  language: "python",
65
93
  file: filePath,
@@ -68,14 +96,16 @@ export const pythonAdapter = {
68
96
  symbols: [],
69
97
  imports: [],
70
98
  calls: [],
71
- diagnostics: ["python3 not available on PATH"],
72
- limitations: ["Python AST adapter requires python3 with stdlib ast module"],
99
+ diagnostics: ["python not available on PATH"],
100
+ limitations: ["Python AST adapter requires python3/python with stdlib ast module"],
73
101
  };
74
102
  }
75
- const r = spawnSync("python3", ["-c", PY_EXTRACTOR, filePath], {
103
+ const r = spawnSync(pythonBin, ["-c", PY_EXTRACTOR, filePath], {
76
104
  input: source,
77
105
  encoding: "utf8",
78
106
  maxBuffer: 8 * 1024 * 1024,
107
+ timeout: 15_000,
108
+ windowsHide: true,
79
109
  });
80
110
  if (r.status !== 0 || !r.stdout) {
81
111
  return {
@@ -86,7 +116,7 @@ export const pythonAdapter = {
86
116
  symbols: [],
87
117
  imports: [],
88
118
  calls: [],
89
- diagnostics: [r.stderr || "python3 extractor failed"],
119
+ diagnostics: [r.stderr || r.error?.message || "python extractor failed"],
90
120
  limitations: [],
91
121
  };
92
122
  }
@@ -0,0 +1,13 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import type { ModelProvider } from "../../ai/types.js";
3
+ export declare const AGENT_MCP_TOOL_NAMES: readonly ["project_context", "project_ask", "code_search", "file_read", "file_create", "file_edit", "agent_plan", "change_verify"];
4
+ export type AgentMcpToolName = (typeof AGENT_MCP_TOOL_NAMES)[number];
5
+ export declare function listAgentMcpTools(): Tool[];
6
+ export declare function invokeAgentMcpTool(root: string, name: string, args: Record<string, unknown>, options?: {
7
+ provider?: ModelProvider;
8
+ }): Promise<{
9
+ structured: unknown;
10
+ isError: boolean;
11
+ }>;
12
+ /** Convenience for tests */
13
+ export declare function projectFingerprint(root: string): Promise<unknown>;
@@ -0,0 +1,234 @@
1
+ import { createModelProvider, loadAiConfig } from "../../ai/index.js";
2
+ import { ChatService } from "../../agent/chat/service.js";
3
+ import { buildAgentPlan } from "../../agent/plan.js";
4
+ import { executeAgentTool, newToolCall } from "../../agent/tools/index.js";
5
+ import { retrieveProjectContext } from "../../agent/context/retrieve.js";
6
+ import { summarizeProjectForChat } from "../../agent/chat/project-summary.js";
7
+ export const AGENT_MCP_TOOL_NAMES = [
8
+ "project_context",
9
+ "project_ask",
10
+ "code_search",
11
+ "file_read",
12
+ "file_create",
13
+ "file_edit",
14
+ "agent_plan",
15
+ "change_verify",
16
+ ];
17
+ export function listAgentMcpTools() {
18
+ return [
19
+ {
20
+ name: "project_context",
21
+ description: "READ: Budgeted project context pack for a query (path-safe).",
22
+ inputSchema: {
23
+ type: "object",
24
+ properties: { query: { type: "string" } },
25
+ required: ["query"],
26
+ additionalProperties: false,
27
+ },
28
+ },
29
+ {
30
+ name: "project_ask",
31
+ description: "READ: One-shot Project Chat ask (requires AI provider; no file writes).",
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: { question: { type: "string" } },
35
+ required: ["question"],
36
+ additionalProperties: false,
37
+ },
38
+ },
39
+ {
40
+ name: "code_search",
41
+ description: "READ: Search code via AgentDoctor graph search.",
42
+ inputSchema: {
43
+ type: "object",
44
+ properties: { query: { type: "string" } },
45
+ required: ["query"],
46
+ additionalProperties: false,
47
+ },
48
+ },
49
+ {
50
+ name: "file_read",
51
+ description: "READ: Path-safe file read inside workspace.",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: { path: { type: "string" } },
55
+ required: ["path"],
56
+ additionalProperties: false,
57
+ },
58
+ },
59
+ {
60
+ name: "file_create",
61
+ description: "WRITE: Create file (requires approved=true). Path-safe; never shell. Model cannot self-approve.",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: {
65
+ path: { type: "string" },
66
+ content: { type: "string" },
67
+ approved: { type: "boolean" },
68
+ },
69
+ required: ["path", "content", "approved"],
70
+ additionalProperties: false,
71
+ },
72
+ },
73
+ {
74
+ name: "file_edit",
75
+ description: "WRITE: Edit file (requires approved=true). Path-safe.",
76
+ inputSchema: {
77
+ type: "object",
78
+ properties: {
79
+ path: { type: "string" },
80
+ content: { type: "string" },
81
+ oldContent: { type: "string" },
82
+ approved: { type: "boolean" },
83
+ },
84
+ required: ["path", "approved"],
85
+ additionalProperties: false,
86
+ },
87
+ },
88
+ {
89
+ name: "agent_plan",
90
+ description: "READ: Build a change plan without modifying files.",
91
+ inputSchema: {
92
+ type: "object",
93
+ properties: { goal: { type: "string" } },
94
+ required: ["goal"],
95
+ additionalProperties: false,
96
+ },
97
+ },
98
+ {
99
+ name: "change_verify",
100
+ description: "READ: Produce evidence/proof for current changes (may write evidence artifacts).",
101
+ inputSchema: {
102
+ type: "object",
103
+ properties: { changeId: { type: "string" } },
104
+ additionalProperties: false,
105
+ },
106
+ },
107
+ ];
108
+ }
109
+ export async function invokeAgentMcpTool(root, name, args, options) {
110
+ try {
111
+ switch (name) {
112
+ case "project_context": {
113
+ const query = typeof args.query === "string" ? args.query : "";
114
+ const bundle = await retrieveProjectContext({ root, query });
115
+ return { structured: { ok: true, bundle }, isError: false };
116
+ }
117
+ case "project_ask": {
118
+ const question = typeof args.question === "string" ? args.question : "";
119
+ if (!question) {
120
+ return {
121
+ structured: {
122
+ ok: false,
123
+ error: { code: "invalid_argument", message: "question required" },
124
+ },
125
+ isError: true,
126
+ };
127
+ }
128
+ const provider = options?.provider ?? createModelProvider(loadAiConfig());
129
+ if (provider.id === "none") {
130
+ return {
131
+ structured: {
132
+ ok: false,
133
+ error: {
134
+ code: "provider_none",
135
+ message: "AI chat is not configured. Set AGENTDOCTOR_AI_PROVIDER=mock (or openai-compatible). Silent mock fallback is disabled.",
136
+ },
137
+ },
138
+ isError: true,
139
+ };
140
+ }
141
+ const chat = new ChatService({
142
+ root,
143
+ provider,
144
+ persistAudit: false,
145
+ });
146
+ try {
147
+ const response = await chat.ask(question);
148
+ return {
149
+ structured: { ok: response.status === "ok", response },
150
+ isError: response.status !== "ok",
151
+ };
152
+ }
153
+ finally {
154
+ await chat.end();
155
+ }
156
+ }
157
+ case "code_search": {
158
+ const result = await executeAgentTool(root, newToolCall("mcp", "search_code", { query: args.query }));
159
+ return { structured: result, isError: !result.ok };
160
+ }
161
+ case "file_read": {
162
+ const result = await executeAgentTool(root, newToolCall("mcp", "read_file", { path: args.path }));
163
+ return { structured: result, isError: !result.ok };
164
+ }
165
+ case "file_create": {
166
+ if (args.approved !== true) {
167
+ return {
168
+ structured: {
169
+ ok: false,
170
+ error: { code: "approval_required", message: "approved=true required" },
171
+ },
172
+ isError: true,
173
+ };
174
+ }
175
+ const result = await executeAgentTool(root, newToolCall("mcp", "create_file", { path: args.path, content: args.content }), { allowWrite: true, approvedByHuman: true });
176
+ return { structured: result, isError: !result.ok };
177
+ }
178
+ case "file_edit": {
179
+ if (args.approved !== true) {
180
+ return {
181
+ structured: {
182
+ ok: false,
183
+ error: { code: "approval_required", message: "approved=true required" },
184
+ },
185
+ isError: true,
186
+ };
187
+ }
188
+ const result = await executeAgentTool(root, newToolCall("mcp", "edit_file", {
189
+ path: args.path,
190
+ content: args.content,
191
+ oldContent: args.oldContent,
192
+ }), { allowWrite: true, approvedByHuman: true });
193
+ return { structured: result, isError: !result.ok };
194
+ }
195
+ case "agent_plan": {
196
+ const goal = typeof args.goal === "string" ? args.goal : "";
197
+ const plan = await buildAgentPlan({ root, goal });
198
+ return { structured: { ok: true, plan }, isError: false };
199
+ }
200
+ case "change_verify": {
201
+ const { verifyChange } = await import("../../assurance/change.js");
202
+ const result = await verifyChange({
203
+ root,
204
+ ...(typeof args.changeId === "string" ? { changeId: args.changeId } : {}),
205
+ });
206
+ return { structured: { ok: true, ...result }, isError: false };
207
+ }
208
+ default:
209
+ return {
210
+ structured: {
211
+ ok: false,
212
+ error: { code: "invalid_argument", message: `unknown agent tool: ${name}` },
213
+ },
214
+ isError: true,
215
+ };
216
+ }
217
+ }
218
+ catch (error) {
219
+ return {
220
+ structured: {
221
+ ok: false,
222
+ error: {
223
+ code: "internal",
224
+ message: error instanceof Error ? error.message : String(error),
225
+ },
226
+ },
227
+ isError: true,
228
+ };
229
+ }
230
+ }
231
+ /** Convenience for tests */
232
+ export async function projectFingerprint(root) {
233
+ return summarizeProjectForChat(root);
234
+ }
@@ -6,6 +6,7 @@ import { resolveRepoRoot } from "../../utils/path.js";
6
6
  import { BrainMcpSession } from "../brain/session.js";
7
7
  import { BRAIN_MCP_TOOL_NAMES, invokeBrainMcpTool, listBrainMcpTools, } from "../brain/tools/registry.js";
8
8
  import { INTELLIGENCE_MCP_TOOL_NAMES, invokeIntelligenceMcpTool, listIntelligenceMcpTools, } from "../intelligence/registry.js";
9
+ import { AGENT_MCP_TOOL_NAMES, invokeAgentMcpTool, listAgentMcpTools } from "../agent/registry.js";
9
10
  /**
10
11
  * Combined AgentDoctor MCP server: preserves all Brain tool names and adds
11
12
  * intelligence tools. No arbitrary shell execution; policy_evaluate is evaluate-only.
@@ -19,6 +20,7 @@ export async function startAgentDoctorMcpServer(options) {
19
20
  await session.initialize();
20
21
  const brainNames = new Set(BRAIN_MCP_TOOL_NAMES);
21
22
  const intelNames = new Set(INTELLIGENCE_MCP_TOOL_NAMES);
23
+ const agentNames = new Set(AGENT_MCP_TOOL_NAMES);
22
24
  const server = new Server({
23
25
  name: "agentdoctor",
24
26
  version: PACKAGE_VERSION,
@@ -28,7 +30,7 @@ export async function startAgentDoctorMcpServer(options) {
28
30
  },
29
31
  });
30
32
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
31
- tools: [...listBrainMcpTools(), ...listIntelligenceMcpTools()],
33
+ tools: [...listBrainMcpTools(), ...listIntelligenceMcpTools(), ...listAgentMcpTools()],
32
34
  }));
33
35
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
34
36
  const name = request.params.name;
@@ -45,6 +47,11 @@ export async function startAgentDoctorMcpServer(options) {
45
47
  structured = result.structured;
46
48
  isError = result.isError;
47
49
  }
50
+ else if (agentNames.has(name)) {
51
+ const result = await invokeAgentMcpTool(root, name, args);
52
+ structured = result.structured;
53
+ isError = result.isError;
54
+ }
48
55
  else {
49
56
  structured = {
50
57
  ok: false,
@@ -49,17 +49,6 @@ export function rejectHostilePathInput(candidate) {
49
49
  }
50
50
  }
51
51
  }
52
- function realpathOrSelf(target) {
53
- try {
54
- if (fs.existsSync(target)) {
55
- return fs.realpathSync(target);
56
- }
57
- }
58
- catch {
59
- // fall through
60
- }
61
- return target;
62
- }
63
52
  function realpathRoot(root) {
64
53
  try {
65
54
  return fs.realpathSync(root);
@@ -85,12 +74,51 @@ export function resolveSafeRepoPath(rootInput, candidate) {
85
74
  throw new PathEscapeError("path escapes repository root");
86
75
  }
87
76
  const realRoot = realpathRoot(root);
88
- const realTarget = realpathOrSelf(absolute);
89
- // Symlink / realpath escape check (both sides realpath'd when possible).
90
- if (!isPathInsideRoot(realRoot, realTarget) && realTarget !== realRoot) {
77
+ // Existing path: realpath and verify (catches symlink escape).
78
+ if (fs.existsSync(absolute)) {
79
+ let realTarget;
80
+ try {
81
+ realTarget = fs.realpathSync(absolute);
82
+ }
83
+ catch {
84
+ realTarget = absolute;
85
+ }
86
+ if (!isPathInsideRoot(realRoot, realTarget) && realTarget !== realRoot) {
87
+ throw new PathEscapeError("path escapes repository root");
88
+ }
89
+ return realTarget;
90
+ }
91
+ // Non-existent path (create): reject if any existing ancestor realpath escapes
92
+ // (symlink-dir escape), then map under realRoot for /var vs /private/var.
93
+ let ancestor = path.dirname(absolute);
94
+ for (;;) {
95
+ if (fs.existsSync(ancestor)) {
96
+ let realAncestor;
97
+ try {
98
+ realAncestor = fs.realpathSync(ancestor);
99
+ }
100
+ catch {
101
+ realAncestor = ancestor;
102
+ }
103
+ if (!isPathInsideRoot(realRoot, realAncestor) && realAncestor !== realRoot) {
104
+ throw new PathEscapeError("path escapes repository root");
105
+ }
106
+ break;
107
+ }
108
+ const parent = path.dirname(ancestor);
109
+ if (parent === ancestor)
110
+ break;
111
+ ancestor = parent;
112
+ }
113
+ const rel = path.relative(root, absolute);
114
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
115
+ throw new PathEscapeError("path escapes repository root");
116
+ }
117
+ const mapped = path.resolve(realRoot, rel);
118
+ if (!isPathInsideRoot(realRoot, mapped) && mapped !== realRoot) {
91
119
  throw new PathEscapeError("path escapes repository root");
92
120
  }
93
- return realTarget;
121
+ return mapped;
94
122
  }
95
123
  /** Assert `candidate` resolves inside `root`; throws PathEscapeError otherwise. */
96
124
  export function assertInsideRepo(rootInput, candidate) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praneeth_54/agentdoctor",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Engineering assurance for AI coding agents — repository intelligence, change evidence, safety controls, and MCP tools.",
5
5
  "type": "module",
6
6
  "bin": {