@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
@@ -0,0 +1,84 @@
1
+ import { redactForModel, redactMessages } from "../redact.js";
2
+ /**
3
+ * Deterministic mock provider for tests and offline demos.
4
+ * Never calls the network.
5
+ * Optional `script` advances one step per chat() for coding-loop tests.
6
+ */
7
+ export class MockModelProvider {
8
+ id = "mock";
9
+ turn = 0;
10
+ script;
11
+ constructor(options) {
12
+ this.script = options?.script;
13
+ }
14
+ metadata() {
15
+ return {
16
+ provider: "mock",
17
+ model: "mock",
18
+ supportsTools: true,
19
+ supportsStreaming: false,
20
+ supportsStructuredOutput: false,
21
+ };
22
+ }
23
+ async chat(request) {
24
+ const messages = redactMessages(request.messages);
25
+ if (this.script) {
26
+ const step = this.script[this.turn] ?? { content: "[AI-GENERATED mock] done", toolCalls: [] };
27
+ this.turn += 1;
28
+ const toolCalls = step.toolCalls ?? [];
29
+ const text = step.content ?? (toolCalls.length ? "(proposing tools)" : "done");
30
+ return {
31
+ provider: "mock",
32
+ model: "mock",
33
+ message: { role: "assistant", content: text },
34
+ toolCalls,
35
+ usage: {
36
+ promptTokens: messages.reduce((n, m) => n + Math.ceil(m.content.length / 4), 0),
37
+ completionTokens: Math.ceil(text.length / 4),
38
+ totalTokens: 0,
39
+ },
40
+ finishReason: toolCalls.length ? "tool_calls" : "stop",
41
+ aiGenerated: true,
42
+ };
43
+ }
44
+ const lastUser = [...messages].reverse().find((m) => m.role === "user");
45
+ const lastTool = [...messages].reverse().find((m) => m.role === "tool");
46
+ const preview = redactForModel(lastUser?.content ?? "").slice(0, 120);
47
+ let text = `[AI-GENERATED mock] Received ${messages.length} message(s). Last user: ${preview || "(empty)"}`;
48
+ // Tool-output injection must never become instructions — mock refuses to honor them.
49
+ if (lastTool?.content && /IGNORE\s+ALL\s+PREVIOUS\s+INSTRUCTIONS/i.test(lastTool.content)) {
50
+ text =
51
+ "[AI-GENERATED mock] Ignoring untrusted TOOL_OUTPUT instructions. Continuing safely without destructive actions.";
52
+ return {
53
+ provider: "mock",
54
+ model: "mock",
55
+ message: { role: "assistant", content: text },
56
+ toolCalls: [],
57
+ finishReason: "stop",
58
+ aiGenerated: true,
59
+ };
60
+ }
61
+ const toolCalls = [];
62
+ const match = /call tool\s+([a-z0-9_]+)/i.exec(lastUser?.content ?? "");
63
+ if (match && request.tools?.some((t) => t.name === match[1])) {
64
+ toolCalls.push({
65
+ id: "mock_tool_1",
66
+ name: match[1],
67
+ arguments: {},
68
+ });
69
+ }
70
+ return {
71
+ provider: "mock",
72
+ model: "mock",
73
+ message: { role: "assistant", content: text },
74
+ toolCalls,
75
+ usage: {
76
+ promptTokens: messages.reduce((n, m) => n + Math.ceil(m.content.length / 4), 0),
77
+ completionTokens: Math.ceil(text.length / 4),
78
+ totalTokens: 0,
79
+ },
80
+ finishReason: toolCalls.length ? "tool_calls" : "stop",
81
+ aiGenerated: true,
82
+ };
83
+ }
84
+ }
@@ -0,0 +1,6 @@
1
+ import type { ChatRequest, ChatResponse, ModelMetadata, ModelProvider } from "../types.js";
2
+ export declare class NoneModelProvider implements ModelProvider {
3
+ readonly id: "none";
4
+ metadata(): ModelMetadata;
5
+ chat(_request: ChatRequest): Promise<ChatResponse>;
6
+ }
@@ -0,0 +1,23 @@
1
+ import { AI_PROVIDER_REQUIRED_MESSAGE } from "../types.js";
2
+ export class NoneModelProvider {
3
+ id = "none";
4
+ metadata() {
5
+ return {
6
+ provider: "none",
7
+ model: "none",
8
+ supportsTools: false,
9
+ supportsStreaming: false,
10
+ supportsStructuredOutput: false,
11
+ };
12
+ }
13
+ async chat(_request) {
14
+ return {
15
+ provider: "none",
16
+ model: "none",
17
+ message: { role: "assistant", content: "" },
18
+ toolCalls: [],
19
+ aiGenerated: true,
20
+ error: AI_PROVIDER_REQUIRED_MESSAGE,
21
+ };
22
+ }
23
+ }
@@ -0,0 +1,21 @@
1
+ import type { ChatRequest, ChatResponse, ModelMetadata, ModelProvider } from "../types.js";
2
+ export interface OpenAiCompatibleOptions {
3
+ apiKey?: string;
4
+ baseUrl?: string;
5
+ model: string;
6
+ timeoutMs?: number;
7
+ }
8
+ /**
9
+ * Minimal OpenAI-compatible Chat Completions client (custom base URL supported).
10
+ * Uses Node fetch. Does not silently fall back to another provider.
11
+ */
12
+ export declare class OpenAiCompatibleProvider implements ModelProvider {
13
+ readonly id: "openai-compatible";
14
+ private readonly apiKey;
15
+ private readonly baseUrl;
16
+ private readonly model;
17
+ private readonly timeoutMs;
18
+ constructor(options: OpenAiCompatibleOptions);
19
+ metadata(): ModelMetadata;
20
+ chat(request: ChatRequest): Promise<ChatResponse>;
21
+ }
@@ -0,0 +1,151 @@
1
+ import { redactMessages } from "../redact.js";
2
+ /**
3
+ * Minimal OpenAI-compatible Chat Completions client (custom base URL supported).
4
+ * Uses Node fetch. Does not silently fall back to another provider.
5
+ */
6
+ export class OpenAiCompatibleProvider {
7
+ id = "openai-compatible";
8
+ apiKey;
9
+ baseUrl;
10
+ model;
11
+ timeoutMs;
12
+ constructor(options) {
13
+ this.apiKey = options.apiKey;
14
+ this.baseUrl = (options.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "");
15
+ this.model = options.model;
16
+ this.timeoutMs = options.timeoutMs ?? 60_000;
17
+ }
18
+ metadata() {
19
+ return {
20
+ provider: "openai-compatible",
21
+ model: this.model,
22
+ supportsTools: true,
23
+ supportsStreaming: false,
24
+ supportsStructuredOutput: false,
25
+ };
26
+ }
27
+ async chat(request) {
28
+ if (!this.apiKey &&
29
+ !this.baseUrl.includes("localhost") &&
30
+ !this.baseUrl.includes("127.0.0.1")) {
31
+ return {
32
+ provider: "openai-compatible",
33
+ model: this.model,
34
+ message: { role: "assistant", content: "" },
35
+ toolCalls: [],
36
+ aiGenerated: true,
37
+ error: "AI provider unavailable: AGENTDOCTOR_AI_API_KEY is not set.",
38
+ };
39
+ }
40
+ const messages = redactMessages(request.messages).map((m) => ({
41
+ role: m.role === "tool" ? "tool" : m.role,
42
+ content: m.content,
43
+ ...(m.toolCallId ? { tool_call_id: m.toolCallId } : {}),
44
+ ...(m.name ? { name: m.name } : {}),
45
+ }));
46
+ const body = {
47
+ model: request.model ?? this.model,
48
+ messages,
49
+ temperature: request.temperature ?? 0.2,
50
+ max_tokens: request.maxTokens ?? 2048,
51
+ };
52
+ if (request.tools && request.tools.length > 0) {
53
+ body.tools = request.tools.map((t) => ({
54
+ type: "function",
55
+ function: {
56
+ name: t.name,
57
+ description: t.description,
58
+ parameters: t.parameters,
59
+ },
60
+ }));
61
+ }
62
+ const controller = new AbortController();
63
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
64
+ const onAbort = () => controller.abort();
65
+ request.signal?.addEventListener("abort", onAbort);
66
+ try {
67
+ const headers = {
68
+ "Content-Type": "application/json",
69
+ };
70
+ if (this.apiKey) {
71
+ headers.Authorization = `Bearer ${this.apiKey}`;
72
+ }
73
+ const res = await fetch(`${this.baseUrl}/chat/completions`, {
74
+ method: "POST",
75
+ headers,
76
+ body: JSON.stringify(body),
77
+ signal: controller.signal,
78
+ });
79
+ if (!res.ok) {
80
+ const errText = await res.text().catch(() => "");
81
+ return {
82
+ provider: "openai-compatible",
83
+ model: this.model,
84
+ message: { role: "assistant", content: "" },
85
+ toolCalls: [],
86
+ aiGenerated: true,
87
+ error: `AI provider unavailable: HTTP ${res.status}${errText ? ` (${errText.slice(0, 200)})` : ""}`,
88
+ };
89
+ }
90
+ const data = (await res.json());
91
+ const choice = data.choices?.[0];
92
+ const rawTools = choice?.message?.tool_calls ?? [];
93
+ const toolCalls = rawTools.map((tc) => {
94
+ let args = {};
95
+ try {
96
+ args = JSON.parse(tc.function?.arguments ?? "{}");
97
+ }
98
+ catch {
99
+ args = {};
100
+ }
101
+ return {
102
+ id: tc.id,
103
+ name: tc.function?.name ?? "unknown",
104
+ arguments: args,
105
+ };
106
+ });
107
+ const message = {
108
+ role: "assistant",
109
+ content: String(choice?.message?.content ?? ""),
110
+ };
111
+ return {
112
+ provider: "openai-compatible",
113
+ model: data.model ?? this.model,
114
+ message,
115
+ toolCalls,
116
+ ...(data.usage
117
+ ? {
118
+ usage: {
119
+ ...(data.usage.prompt_tokens !== undefined
120
+ ? { promptTokens: data.usage.prompt_tokens }
121
+ : {}),
122
+ ...(data.usage.completion_tokens !== undefined
123
+ ? { completionTokens: data.usage.completion_tokens }
124
+ : {}),
125
+ ...(data.usage.total_tokens !== undefined
126
+ ? { totalTokens: data.usage.total_tokens }
127
+ : {}),
128
+ },
129
+ }
130
+ : {}),
131
+ ...(choice?.finish_reason ? { finishReason: choice.finish_reason } : {}),
132
+ aiGenerated: true,
133
+ };
134
+ }
135
+ catch (error) {
136
+ const msg = error instanceof Error ? error.message : String(error);
137
+ return {
138
+ provider: "openai-compatible",
139
+ model: this.model,
140
+ message: { role: "assistant", content: "" },
141
+ toolCalls: [],
142
+ aiGenerated: true,
143
+ error: `AI provider unavailable: ${msg}`,
144
+ };
145
+ }
146
+ finally {
147
+ clearTimeout(timer);
148
+ request.signal?.removeEventListener("abort", onAbort);
149
+ }
150
+ }
151
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Redaction for outbound model payloads.
3
+ * Extends the local-ai redaction patterns; never send raw secrets to providers.
4
+ */
5
+ export declare function redactForModel(text: string): string;
6
+ export declare function redactMessages<T extends {
7
+ content: string;
8
+ }>(messages: T[]): T[];
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Redaction for outbound model payloads.
3
+ * Extends the local-ai redaction patterns; never send raw secrets to providers.
4
+ */
5
+ const SECRET_PATTERNS = [
6
+ /\b(AKIA[0-9A-Z]{16})\b/g,
7
+ /\b(ghp_[A-Za-z0-9]{36})\b/g,
8
+ /\b(sk-[A-Za-z0-9]{20,})\b/g,
9
+ /-----BEGIN [^-]+PRIVATE KEY-----[\s\S]*?-----END [^-]+PRIVATE KEY-----/g,
10
+ /\b((?:export\s+)?(?:AWS_SECRET_ACCESS_KEY|GITHUB_TOKEN|NPM_TOKEN|OPENAI_API_KEY|ANTHROPIC_API_KEY|STRIPE_SECRET_KEY|AGENTDOCTOR_AI_API_KEY)\s*=\s*)(['"]?)[^\s'"]+\2/gi,
11
+ ];
12
+ export function redactForModel(text) {
13
+ let out = text;
14
+ for (const re of SECRET_PATTERNS) {
15
+ out = out.replace(re, "[REDACTED]");
16
+ }
17
+ return out;
18
+ }
19
+ export function redactMessages(messages) {
20
+ return messages.map((m) => ({ ...m, content: redactForModel(m.content) }));
21
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Provider-agnostic model types for AgentDoctor 2.1 Project AI Agent.
3
+ * The model reasons; AgentDoctor owns context, tools, and verification.
4
+ */
5
+ export type AiProviderId = "none" | "mock" | "openai-compatible" | "ollama" | "anthropic" | "gemini";
6
+ export type ChatRole = "system" | "user" | "assistant" | "tool";
7
+ export interface ChatMessage {
8
+ role: ChatRole;
9
+ content: string;
10
+ /** Tool call id when role is tool */
11
+ toolCallId?: string;
12
+ name?: string;
13
+ }
14
+ export interface ToolSpec {
15
+ name: string;
16
+ description: string;
17
+ /** JSON Schema object for arguments */
18
+ parameters: Record<string, unknown>;
19
+ }
20
+ export interface ToolCallRequest {
21
+ id: string;
22
+ name: string;
23
+ arguments: Record<string, unknown>;
24
+ }
25
+ export interface TokenUsage {
26
+ promptTokens?: number;
27
+ completionTokens?: number;
28
+ totalTokens?: number;
29
+ }
30
+ export interface ChatRequest {
31
+ messages: ChatMessage[];
32
+ tools?: ToolSpec[];
33
+ model?: string;
34
+ temperature?: number;
35
+ maxTokens?: number;
36
+ /** AbortSignal for cancellation when supported */
37
+ signal?: AbortSignal;
38
+ }
39
+ export interface ChatResponse {
40
+ provider: AiProviderId;
41
+ model: string;
42
+ message: ChatMessage;
43
+ toolCalls: ToolCallRequest[];
44
+ usage?: TokenUsage;
45
+ finishReason?: string;
46
+ /** Always true for model-generated content */
47
+ aiGenerated: true;
48
+ error?: string;
49
+ }
50
+ export interface ModelMetadata {
51
+ provider: AiProviderId;
52
+ model: string;
53
+ supportsTools: boolean;
54
+ supportsStreaming: boolean;
55
+ supportsStructuredOutput: boolean;
56
+ }
57
+ export interface ModelProvider {
58
+ readonly id: AiProviderId;
59
+ metadata(): ModelMetadata;
60
+ chat(request: ChatRequest): Promise<ChatResponse>;
61
+ /**
62
+ * Optional streaming. Providers that do not support streaming reject.
63
+ */
64
+ streamChat?(request: ChatRequest): AsyncIterable<{
65
+ delta: string;
66
+ done: boolean;
67
+ response?: ChatResponse;
68
+ }>;
69
+ }
70
+ /** User-facing copy when AI is not configured. */
71
+ export declare const AI_PROVIDER_REQUIRED_MESSAGE = "AI features require an AI provider. AgentDoctor's project analysis features continue to work without AI.";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Provider-agnostic model types for AgentDoctor 2.1 Project AI Agent.
3
+ * The model reasons; AgentDoctor owns context, tools, and verification.
4
+ */
5
+ /** User-facing copy when AI is not configured. */
6
+ export const AI_PROVIDER_REQUIRED_MESSAGE = "AI features require an AI provider. AgentDoctor's project analysis features continue to work without AI.";
@@ -0,0 +1,27 @@
1
+ import { type ExitCode } from "../../types/index.js";
2
+ /**
3
+ * agentdoctor plan — produce an implementation plan (no writes unless --apply elsewhere).
4
+ */
5
+ export declare function runPlanCommand(options: {
6
+ goal: string;
7
+ root?: string;
8
+ json?: boolean;
9
+ approve?: boolean;
10
+ }): Promise<ExitCode>;
11
+ /**
12
+ * agentdoctor agent — inspect, plan, and (with --approve) mutate via controlled tools.
13
+ */
14
+ export declare function runAgentCommand(options: {
15
+ root?: string;
16
+ goal?: string;
17
+ tool?: string;
18
+ toolArgs?: Record<string, unknown>;
19
+ listTools?: boolean;
20
+ json?: boolean;
21
+ approve?: boolean;
22
+ apply?: boolean;
23
+ /** JSON array of {name, arguments} for deterministic apply */
24
+ applyOpsJson?: string;
25
+ verify?: boolean;
26
+ runTests?: boolean;
27
+ }): Promise<ExitCode>;
@@ -0,0 +1,153 @@
1
+ import { approvePlan, buildAgentPlan, formatAgentPlan } from "../../agent/plan.js";
2
+ import { evaluateApproval, formatApprovalPrompt } from "../../agent/approvals.js";
3
+ import { runCodingLoop } from "../../agent/loop.js";
4
+ import { executeAgentTool, getToolSpec, listAgentToolSpecs, newToolCall, } from "../../agent/tools/index.js";
5
+ import { EXIT_CODES } from "../../types/index.js";
6
+ import { resolveRepoRoot } from "../../utils/path.js";
7
+ import { colors } from "../../utils/colors.js";
8
+ /**
9
+ * agentdoctor plan — produce an implementation plan (no writes unless --apply elsewhere).
10
+ */
11
+ export async function runPlanCommand(options) {
12
+ const root = resolveRepoRoot(options.root ?? process.cwd());
13
+ const plan = await buildAgentPlan({ root, goal: options.goal });
14
+ if (options.approve) {
15
+ const approved = approvePlan(plan, true);
16
+ if (options.json) {
17
+ process.stdout.write(`${JSON.stringify({ ...approved, note: "Approval recorded; use agent --apply to mutate" }, null, 2)}\n`);
18
+ }
19
+ else {
20
+ process.stdout.write(formatAgentPlan(approved));
21
+ process.stdout.write(`${colors.yellow('\nApproval recorded. Use: agentdoctor agent --goal "..." --approve --apply\n')}\n`);
22
+ }
23
+ return EXIT_CODES.SUCCESS;
24
+ }
25
+ if (options.json) {
26
+ process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
27
+ }
28
+ else {
29
+ process.stdout.write(formatAgentPlan(plan));
30
+ }
31
+ return EXIT_CODES.SUCCESS;
32
+ }
33
+ /**
34
+ * agentdoctor agent — inspect, plan, and (with --approve) mutate via controlled tools.
35
+ */
36
+ export async function runAgentCommand(options) {
37
+ const root = resolveRepoRoot(options.root ?? process.cwd());
38
+ const approvedByHuman = options.approve === true;
39
+ if (options.listTools) {
40
+ const tools = listAgentToolSpecs({ includeWrite: true, includeExecute: true });
41
+ if (options.json) {
42
+ process.stdout.write(`${JSON.stringify(tools, null, 2)}\n`);
43
+ }
44
+ else {
45
+ process.stdout.write(`\n${colors.bold("AgentDoctor tools")}\n\n`);
46
+ for (const t of tools) {
47
+ process.stdout.write(` [${t.risk}] ${t.name} (${t.category}) — ${t.description}\n`);
48
+ }
49
+ process.stdout.write("\n");
50
+ }
51
+ return EXIT_CODES.SUCCESS;
52
+ }
53
+ if (options.tool) {
54
+ const name = options.tool;
55
+ const spec = getToolSpec(name);
56
+ if (!spec) {
57
+ process.stderr.write(`Unknown tool: ${name}\n`);
58
+ return EXIT_CODES.USAGE_ERROR;
59
+ }
60
+ const approval = evaluateApproval({ action: `tool:${name}`, risk: spec.risk, toolName: name }, { approvedByHuman });
61
+ if (approval.needsHumanApproval) {
62
+ process.stderr.write(formatApprovalPrompt({ action: `tool:${name}`, risk: approval.risk, toolName: name }, approval));
63
+ process.stderr.write("Re-run with --approve for write/execute tools.\n");
64
+ return EXIT_CODES.USAGE_ERROR;
65
+ }
66
+ const call = newToolCall("cli-agent", name, options.toolArgs ?? {});
67
+ const result = await executeAgentTool(root, call, {
68
+ allowWrite: approvedByHuman,
69
+ allowExecute: approvedByHuman,
70
+ approvedByHuman,
71
+ });
72
+ if (options.json) {
73
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
74
+ }
75
+ else if (result.ok) {
76
+ process.stdout.write(`${JSON.stringify(result.data, null, 2)}\n`);
77
+ }
78
+ else {
79
+ process.stderr.write(`Tool failed: ${result.error?.code} — ${result.error?.message}\n`);
80
+ return EXIT_CODES.INTERNAL_ERROR;
81
+ }
82
+ return EXIT_CODES.SUCCESS;
83
+ }
84
+ if (options.goal && options.apply) {
85
+ if (!approvedByHuman) {
86
+ process.stderr.write("Refusing --apply without --approve (human approval required).\n");
87
+ return EXIT_CODES.USAGE_ERROR;
88
+ }
89
+ let toolCalls;
90
+ if (options.applyOpsJson) {
91
+ try {
92
+ const parsed = JSON.parse(options.applyOpsJson);
93
+ if (!Array.isArray(parsed))
94
+ throw new Error("expected array");
95
+ toolCalls = parsed.map((item) => {
96
+ const row = item;
97
+ if (!row.name || typeof row.name !== "string")
98
+ throw new Error("ops need name");
99
+ return {
100
+ name: row.name,
101
+ arguments: row.arguments ?? {},
102
+ };
103
+ });
104
+ }
105
+ catch (error) {
106
+ process.stderr.write(`Invalid --apply-ops JSON: ${error instanceof Error ? error.message : String(error)}\n`);
107
+ return EXIT_CODES.USAGE_ERROR;
108
+ }
109
+ }
110
+ const result = await runCodingLoop({
111
+ root,
112
+ goal: options.goal,
113
+ approvedByHuman: true,
114
+ ...(toolCalls ? { toolCalls } : {}),
115
+ verify: options.verify !== false,
116
+ runTests: options.runTests === true,
117
+ });
118
+ if (options.json) {
119
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
120
+ }
121
+ else {
122
+ process.stdout.write(result.responseText);
123
+ }
124
+ if (result.stoppedReason === "awaiting-approval")
125
+ return EXIT_CODES.USAGE_ERROR;
126
+ if (result.stoppedReason === "failed" || result.stoppedReason === "limit") {
127
+ return EXIT_CODES.INTERNAL_ERROR;
128
+ }
129
+ return EXIT_CODES.SUCCESS;
130
+ }
131
+ if (options.goal) {
132
+ return runPlanCommand({
133
+ goal: options.goal,
134
+ root,
135
+ ...(options.json !== undefined ? { json: options.json } : {}),
136
+ ...(approvedByHuman ? { approve: true } : {}),
137
+ });
138
+ }
139
+ process.stderr.write([
140
+ "",
141
+ "AgentDoctor agent (Milestone 4)",
142
+ "",
143
+ "Usage:",
144
+ " agentdoctor agent --list-tools",
145
+ " agentdoctor agent --tool inspect_project [path]",
146
+ ' agentdoctor plan "Add registration" [path]',
147
+ " agentdoctor agent --goal \"Add registration\" --approve --apply --apply-ops '[...]' [path]",
148
+ "",
149
+ "Writes require --approve. Unrestricted shell is never exposed.",
150
+ "",
151
+ ].join("\n"));
152
+ return EXIT_CODES.USAGE_ERROR;
153
+ }
@@ -0,0 +1,14 @@
1
+ import { type ExitCode } from "../../types/index.js";
2
+ export declare function runAskCommand(options: {
3
+ question: string;
4
+ root?: string;
5
+ json?: boolean;
6
+ /** Test-only: force mock provider */
7
+ useMock?: boolean;
8
+ }): Promise<ExitCode>;
9
+ export declare function runChatCommand(options: {
10
+ root?: string;
11
+ /** Non-interactive scripted lines (tests) */
12
+ scriptedInputs?: string[];
13
+ useMock?: boolean;
14
+ }): Promise<ExitCode>;