@cdoing/cli 0.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 (118) hide show
  1. package/.cdoing/permissions.json +8 -0
  2. package/dist/callbacks.d.ts +17 -0
  3. package/dist/callbacks.d.ts.map +1 -0
  4. package/dist/callbacks.js +265 -0
  5. package/dist/callbacks.js.map +1 -0
  6. package/dist/chat.d.ts +27 -0
  7. package/dist/chat.d.ts.map +1 -0
  8. package/dist/chat.js +57 -0
  9. package/dist/chat.js.map +1 -0
  10. package/dist/commands.d.ts +22 -0
  11. package/dist/commands.d.ts.map +1 -0
  12. package/dist/commands.js +452 -0
  13. package/dist/commands.js.map +1 -0
  14. package/dist/config.d.ts +84 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +427 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/help.d.ts +9 -0
  19. package/dist/help.d.ts.map +1 -0
  20. package/dist/help.js +167 -0
  21. package/dist/help.js.map +1 -0
  22. package/dist/history.d.ts +51 -0
  23. package/dist/history.d.ts.map +1 -0
  24. package/dist/history.js +207 -0
  25. package/dist/history.js.map +1 -0
  26. package/dist/index.d.ts +7 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +220 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/oauth.d.ts +13 -0
  31. package/dist/oauth.d.ts.map +1 -0
  32. package/dist/oauth.js +182 -0
  33. package/dist/oauth.js.map +1 -0
  34. package/dist/review.d.ts +26 -0
  35. package/dist/review.d.ts.map +1 -0
  36. package/dist/review.js +198 -0
  37. package/dist/review.js.map +1 -0
  38. package/dist/serve.d.ts +23 -0
  39. package/dist/serve.d.ts.map +1 -0
  40. package/dist/serve.js +293 -0
  41. package/dist/serve.js.map +1 -0
  42. package/dist/tools.d.ts +14 -0
  43. package/dist/tools.d.ts.map +1 -0
  44. package/dist/tools.js +57 -0
  45. package/dist/tools.js.map +1 -0
  46. package/dist/ui/App.d.ts +24 -0
  47. package/dist/ui/App.d.ts.map +1 -0
  48. package/dist/ui/App.js +321 -0
  49. package/dist/ui/App.js.map +1 -0
  50. package/dist/ui/MessageList.d.ts +14 -0
  51. package/dist/ui/MessageList.d.ts.map +1 -0
  52. package/dist/ui/MessageList.js +147 -0
  53. package/dist/ui/MessageList.js.map +1 -0
  54. package/dist/ui/SessionBrowser.d.ts +18 -0
  55. package/dist/ui/SessionBrowser.d.ts.map +1 -0
  56. package/dist/ui/SessionBrowser.js +149 -0
  57. package/dist/ui/SessionBrowser.js.map +1 -0
  58. package/dist/ui/SetupWizard.d.ts +23 -0
  59. package/dist/ui/SetupWizard.d.ts.map +1 -0
  60. package/dist/ui/SetupWizard.js +402 -0
  61. package/dist/ui/SetupWizard.js.map +1 -0
  62. package/dist/ui/Spinner.d.ts +15 -0
  63. package/dist/ui/Spinner.d.ts.map +1 -0
  64. package/dist/ui/Spinner.js +111 -0
  65. package/dist/ui/Spinner.js.map +1 -0
  66. package/dist/ui/StatusBar.d.ts +16 -0
  67. package/dist/ui/StatusBar.d.ts.map +1 -0
  68. package/dist/ui/StatusBar.js +56 -0
  69. package/dist/ui/StatusBar.js.map +1 -0
  70. package/dist/ui/UserInput.d.ts +13 -0
  71. package/dist/ui/UserInput.d.ts.map +1 -0
  72. package/dist/ui/UserInput.js +872 -0
  73. package/dist/ui/UserInput.js.map +1 -0
  74. package/dist/ui/hooks/helpers.d.ts +55 -0
  75. package/dist/ui/hooks/helpers.d.ts.map +1 -0
  76. package/dist/ui/hooks/helpers.js +304 -0
  77. package/dist/ui/hooks/helpers.js.map +1 -0
  78. package/dist/ui/hooks/useAgent.d.ts +60 -0
  79. package/dist/ui/hooks/useAgent.d.ts.map +1 -0
  80. package/dist/ui/hooks/useAgent.js +213 -0
  81. package/dist/ui/hooks/useAgent.js.map +1 -0
  82. package/dist/ui/hooks/useChat.d.ts +74 -0
  83. package/dist/ui/hooks/useChat.d.ts.map +1 -0
  84. package/dist/ui/hooks/useChat.js +819 -0
  85. package/dist/ui/hooks/useChat.js.map +1 -0
  86. package/dist/ui/theme.d.ts +73 -0
  87. package/dist/ui/theme.d.ts.map +1 -0
  88. package/dist/ui/theme.js +214 -0
  89. package/dist/ui/theme.js.map +1 -0
  90. package/dist/ui/types.d.ts +37 -0
  91. package/dist/ui/types.d.ts.map +1 -0
  92. package/dist/ui/types.js +3 -0
  93. package/dist/ui/types.js.map +1 -0
  94. package/package.json +33 -0
  95. package/src/callbacks.ts +294 -0
  96. package/src/chat.ts +72 -0
  97. package/src/commands.ts +425 -0
  98. package/src/config.ts +462 -0
  99. package/src/help.ts +182 -0
  100. package/src/history.ts +205 -0
  101. package/src/index.ts +248 -0
  102. package/src/oauth.ts +164 -0
  103. package/src/review.ts +233 -0
  104. package/src/serve.ts +290 -0
  105. package/src/tools.ts +104 -0
  106. package/src/ui/App.tsx +426 -0
  107. package/src/ui/MessageList.tsx +222 -0
  108. package/src/ui/SessionBrowser.tsx +161 -0
  109. package/src/ui/SetupWizard.tsx +412 -0
  110. package/src/ui/Spinner.tsx +103 -0
  111. package/src/ui/StatusBar.tsx +106 -0
  112. package/src/ui/UserInput.tsx +954 -0
  113. package/src/ui/hooks/helpers.ts +271 -0
  114. package/src/ui/hooks/useAgent.ts +270 -0
  115. package/src/ui/hooks/useChat.ts +943 -0
  116. package/src/ui/theme.ts +326 -0
  117. package/src/ui/types.ts +41 -0
  118. package/tsconfig.json +18 -0
package/src/review.ts ADDED
@@ -0,0 +1,233 @@
1
+ /**
2
+ * cdoing review — AI Code Review
3
+ *
4
+ * Gets the git diff and sends it to the AI for a structured review with
5
+ * concrete improvement suggestions and patches.
6
+ *
7
+ * Usage:
8
+ * cdoing review — review staged + unstaged changes
9
+ * cdoing review HEAD~1 — review last commit
10
+ * cdoing review --staged — review staged changes only
11
+ * cdoing review --base main — review diff from main branch
12
+ * cdoing review --output json — JSON output
13
+ */
14
+
15
+ import { execSync } from "child_process";
16
+ import chalk from "chalk";
17
+ import { AgentRunner } from "@cdoing/ai";
18
+ import { HookManager, MemoryStore, loadProjectConfig } from "@cdoing/core";
19
+ import {
20
+ buildModelConfig,
21
+ createPermissionManager,
22
+ resolveApiKey,
23
+ type CLIOptions,
24
+ } from "./config";
25
+ import { createToolRegistry } from "./tools";
26
+
27
+ export interface ReviewOptions {
28
+ base?: string;
29
+ staged?: boolean;
30
+ dir: string;
31
+ model?: string;
32
+ provider?: string;
33
+ apiKey?: string;
34
+ mode: string;
35
+ output?: "text" | "json";
36
+ verbose?: boolean;
37
+ }
38
+
39
+ function getDiff(opts: ReviewOptions): { diff: string; source: string } {
40
+ const cwd = opts.dir;
41
+
42
+ try {
43
+ if (opts.staged) {
44
+ const diff = execSync("git diff --cached", { cwd, encoding: "utf-8" });
45
+ return { diff, source: "staged changes" };
46
+ }
47
+
48
+ if (opts.base) {
49
+ const diff = execSync(`git diff ${opts.base}`, { cwd, encoding: "utf-8" });
50
+ return { diff, source: `diff from ${opts.base}` };
51
+ }
52
+
53
+ // Default: staged + unstaged; fall back to last commit if clean
54
+ const staged = execSync("git diff --cached", { cwd, encoding: "utf-8" });
55
+ const unstaged = execSync("git diff", { cwd, encoding: "utf-8" });
56
+ const combined = (staged + unstaged).trim();
57
+
58
+ if (combined) {
59
+ return { diff: combined, source: "current changes" };
60
+ }
61
+
62
+ // Nothing staged/unstaged — review last commit
63
+ const lastCommit = execSync("git diff HEAD~1 HEAD", { cwd, encoding: "utf-8" });
64
+ return { diff: lastCommit, source: "last commit" };
65
+ } catch (e) {
66
+ return { diff: "", source: "unknown" };
67
+ }
68
+ }
69
+
70
+ function getRecentCommits(dir: string): string {
71
+ try {
72
+ return execSync("git log --oneline -5", { cwd: dir, encoding: "utf-8" }).trim();
73
+ } catch {
74
+ return "";
75
+ }
76
+ }
77
+
78
+ function getFileSummary(diff: string): string {
79
+ const files = new Set<string>();
80
+ for (const line of diff.split("\n")) {
81
+ if (line.startsWith("+++ b/") || line.startsWith("--- a/")) {
82
+ const f = line.slice(6);
83
+ if (f !== "/dev/null") files.add(f);
84
+ }
85
+ }
86
+ return [...files].join(", ") || "(unknown files)";
87
+ }
88
+
89
+ const REVIEW_SYSTEM_PROMPT = `You are a senior staff engineer conducting a thorough code review. Analyze the provided git diff and give detailed, actionable feedback.
90
+
91
+ Structure your review as follows:
92
+
93
+ ## Summary
94
+ Brief overview of what changed and the overall quality.
95
+
96
+ ## Issues Found
97
+ For each issue, specify:
98
+ - **Severity**: Critical / Major / Minor / Nit
99
+ - **Location**: file:line
100
+ - **Problem**: what's wrong
101
+ - **Fix**: concrete code suggestion
102
+
103
+ ## Security Concerns
104
+ Any security vulnerabilities, auth issues, data exposure, injection risks.
105
+
106
+ ## Performance
107
+ Inefficient algorithms, unnecessary re-renders, N+1 queries, memory leaks.
108
+
109
+ ## Missing Tests
110
+ What should be tested but isn't.
111
+
112
+ ## Suggested Improvements
113
+ Additional improvements beyond bug fixes — readability, naming, patterns.
114
+
115
+ ## Verdict
116
+ Overall: ✅ Approve / ⚠️ Approve with suggestions / ❌ Request changes
117
+
118
+ Be specific, cite line numbers, include before/after code snippets. Use markdown.`;
119
+
120
+ export async function runReview(opts: ReviewOptions): Promise<void> {
121
+ console.log();
122
+ console.log(chalk.bold.cyan(" 🔍 AI Code Review"));
123
+ console.log(chalk.gray(" ─────────────────────────────────────────"));
124
+
125
+ const { diff, source } = getDiff(opts);
126
+
127
+ if (!diff.trim()) {
128
+ console.log(chalk.yellow(" No changes to review.\n"));
129
+ console.log(chalk.dim(" Tips:"));
130
+ console.log(chalk.dim(" cdoing review HEAD~1 — review last commit"));
131
+ console.log(chalk.dim(" cdoing review --staged — review staged changes"));
132
+ console.log(chalk.dim(" cdoing review --base main — review diff from main"));
133
+ console.log();
134
+ return;
135
+ }
136
+
137
+ const lines = diff.split("\n").length;
138
+ const files = getFileSummary(diff);
139
+ const commits = getRecentCommits(opts.dir);
140
+
141
+ console.log(chalk.white(` Source: `) + chalk.cyan(source));
142
+ console.log(chalk.white(` Files: `) + chalk.gray(files));
143
+ console.log(chalk.white(` Lines: `) + chalk.gray(String(lines)));
144
+ console.log();
145
+ console.log(chalk.dim(" Sending to AI for review..."));
146
+ console.log();
147
+
148
+ const cliOpts = {
149
+ model: opts.model,
150
+ provider: opts.provider || "anthropic",
151
+ apiKey: opts.apiKey,
152
+ dir: opts.dir,
153
+ mode: opts.mode || "auto",
154
+ } as CLIOptions;
155
+
156
+ await resolveApiKey(cliOpts);
157
+
158
+ const modelConfig = buildModelConfig(cliOpts);
159
+ const permissionManager = createPermissionManager(cliOpts);
160
+ const hookManager = new HookManager(opts.dir);
161
+ const memoryStore = new MemoryStore();
162
+ const projectConfig = loadProjectConfig(opts.dir);
163
+
164
+ const toolRegistry = createToolRegistry(opts.dir);
165
+ const agent = new AgentRunner(
166
+ modelConfig,
167
+ toolRegistry,
168
+ permissionManager,
169
+ hookManager,
170
+ {
171
+ systemPrompt: REVIEW_SYSTEM_PROMPT,
172
+ projectConfig: projectConfig || undefined,
173
+ memory: memoryStore.formatForPrompt() || undefined,
174
+ },
175
+ );
176
+
177
+ // Truncate very large diffs (keep first 60k chars)
178
+ const truncatedDiff = diff.length > 60000
179
+ ? diff.substring(0, 60000) + "\n\n... (diff truncated at 60k chars)"
180
+ : diff;
181
+
182
+ const prompt = [
183
+ commits ? `Recent commits:\n${commits}\n` : "",
184
+ `Please review the following git diff (${source}):\n`,
185
+ "```diff",
186
+ truncatedDiff,
187
+ "```",
188
+ ].filter(Boolean).join("\n");
189
+
190
+ if (opts.output === "json") {
191
+ let response = "";
192
+ await agent.run(prompt, {
193
+ onToken: (t) => { response += t; },
194
+ onToolCall: () => {},
195
+ onToolResult: () => {},
196
+ onComplete: () => {
197
+ console.log(JSON.stringify({ review: response, source, files, lines }, null, 2));
198
+ },
199
+ onError: (e) => {
200
+ console.error(JSON.stringify({ error: e.message }));
201
+ process.exit(1);
202
+ },
203
+ });
204
+ return;
205
+ }
206
+
207
+ // Streamed text output
208
+ let buffer = "";
209
+ await agent.run(prompt, {
210
+ onToken: (token) => {
211
+ buffer += token;
212
+ const parts = buffer.split("\n");
213
+ for (let i = 0; i < parts.length - 1; i++) {
214
+ process.stdout.write(parts[i] + "\n");
215
+ }
216
+ buffer = parts[parts.length - 1];
217
+ },
218
+ onToolCall: (name) => {
219
+ if (opts.verbose) {
220
+ process.stdout.write(chalk.dim(`\n [${name}]\n`));
221
+ }
222
+ },
223
+ onToolResult: () => {},
224
+ onComplete: () => {
225
+ if (buffer) process.stdout.write(buffer + "\n");
226
+ console.log();
227
+ },
228
+ onError: (e) => {
229
+ console.error(chalk.red(`\n ❌ Error: ${e.message}\n`));
230
+ process.exit(1);
231
+ },
232
+ });
233
+ }
package/src/serve.ts ADDED
@@ -0,0 +1,290 @@
1
+ /**
2
+ * cdoing serve — HTTP API server
3
+ *
4
+ * Exposes the AI agent as a REST API:
5
+ * GET /health
6
+ * GET /sessions
7
+ * POST /sessions — create new session
8
+ * GET /sessions/:id
9
+ * POST /sessions/:id/messages — continue a conversation
10
+ * POST /chat — one-shot prompt
11
+ * POST /chat/stream — streaming via SSE
12
+ */
13
+
14
+ import * as http from "http";
15
+ import * as url from "url";
16
+ import chalk from "chalk";
17
+ import { AgentRunner } from "@cdoing/ai";
18
+ import { HookManager, MemoryStore, loadProjectConfig } from "@cdoing/core";
19
+ import {
20
+ buildModelConfig,
21
+ createPermissionManager,
22
+ resolveApiKey,
23
+ type CLIOptions,
24
+ } from "./config";
25
+ import { createToolRegistry } from "./tools";
26
+ import {
27
+ createConversation,
28
+ addMessage,
29
+ loadConversation,
30
+ listConversations,
31
+ saveConversation,
32
+ } from "./history";
33
+
34
+ export interface ServeOptions {
35
+ port: number;
36
+ host: string;
37
+ model?: string;
38
+ provider?: string;
39
+ apiKey?: string;
40
+ dir: string;
41
+ mode: string;
42
+ }
43
+
44
+ function jsonResponse(res: http.ServerResponse, status: number, data: unknown): void {
45
+ const body = JSON.stringify(data, null, 2);
46
+ res.writeHead(status, {
47
+ "Content-Type": "application/json",
48
+ "Access-Control-Allow-Origin": "*",
49
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
50
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
51
+ });
52
+ res.end(body);
53
+ }
54
+
55
+ function readBody(req: http.IncomingMessage): Promise<Record<string, unknown>> {
56
+ return new Promise((resolve, reject) => {
57
+ let body = "";
58
+ req.on("data", (chunk) => { body += chunk; });
59
+ req.on("end", () => {
60
+ try { resolve(JSON.parse(body || "{}")); }
61
+ catch { reject(new Error("Invalid JSON body")); }
62
+ });
63
+ req.on("error", reject);
64
+ });
65
+ }
66
+
67
+ export async function startServer(opts: ServeOptions): Promise<void> {
68
+ const cliOpts = {
69
+ model: opts.model,
70
+ provider: opts.provider || "anthropic",
71
+ apiKey: opts.apiKey,
72
+ dir: opts.dir,
73
+ mode: opts.mode || "auto",
74
+ } as CLIOptions;
75
+
76
+ await resolveApiKey(cliOpts);
77
+
78
+ const modelConfig = buildModelConfig(cliOpts);
79
+ const permissionManager = createPermissionManager(cliOpts);
80
+ const hookManager = new HookManager(opts.dir);
81
+ const memoryStore = new MemoryStore();
82
+ const projectConfig = loadProjectConfig(opts.dir);
83
+
84
+ const agentOptions = {
85
+ projectConfig: projectConfig || undefined,
86
+ memory: memoryStore.formatForPrompt() || undefined,
87
+ };
88
+
89
+ const server = http.createServer(async (req, res) => {
90
+ // CORS preflight
91
+ if (req.method === "OPTIONS") {
92
+ res.writeHead(204, {
93
+ "Access-Control-Allow-Origin": "*",
94
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
95
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
96
+ });
97
+ res.end();
98
+ return;
99
+ }
100
+
101
+ const parsed = url.parse(req.url || "/", true);
102
+ const pathname = parsed.pathname || "/";
103
+
104
+ try {
105
+ // GET /health
106
+ if (req.method === "GET" && pathname === "/health") {
107
+ jsonResponse(res, 200, {
108
+ status: "ok",
109
+ provider: modelConfig.provider,
110
+ model: modelConfig.model || "(default)",
111
+ dir: opts.dir,
112
+ mode: opts.mode,
113
+ timestamp: new Date().toISOString(),
114
+ });
115
+ return;
116
+ }
117
+
118
+ // GET /sessions
119
+ if (req.method === "GET" && pathname === "/sessions") {
120
+ const sessions = listConversations().slice(0, 100).map((c) => ({
121
+ id: c.id,
122
+ title: c.title,
123
+ createdAt: c.createdAt,
124
+ updatedAt: c.updatedAt,
125
+ provider: c.provider,
126
+ model: c.model,
127
+ messageCount: c.messages.length,
128
+ }));
129
+ jsonResponse(res, 200, { sessions, total: sessions.length });
130
+ return;
131
+ }
132
+
133
+ // POST /sessions — create new session
134
+ if (req.method === "POST" && pathname === "/sessions") {
135
+ const conv = createConversation(
136
+ String(modelConfig.provider || "anthropic"),
137
+ String(modelConfig.model || "default"),
138
+ );
139
+ saveConversation(conv);
140
+ jsonResponse(res, 201, { id: conv.id, title: conv.title, createdAt: conv.createdAt });
141
+ return;
142
+ }
143
+
144
+ // GET /sessions/:id
145
+ const sessionGetMatch = pathname.match(/^\/sessions\/([a-z0-9-]+)$/);
146
+ if (req.method === "GET" && sessionGetMatch) {
147
+ const conv = loadConversation(sessionGetMatch[1]);
148
+ if (!conv) { jsonResponse(res, 404, { error: "Session not found" }); return; }
149
+ jsonResponse(res, 200, conv);
150
+ return;
151
+ }
152
+
153
+ // POST /sessions/:id/messages — continue conversation
154
+ const sessionMsgMatch = pathname.match(/^\/sessions\/([a-z0-9-]+)\/messages$/);
155
+ if (req.method === "POST" && sessionMsgMatch) {
156
+ const conv = loadConversation(sessionMsgMatch[1]);
157
+ if (!conv) { jsonResponse(res, 404, { error: "Session not found" }); return; }
158
+
159
+ const body = await readBody(req);
160
+ const prompt = String(body.prompt || "");
161
+ if (!prompt) { jsonResponse(res, 400, { error: "prompt is required" }); return; }
162
+
163
+ const toolRegistry = createToolRegistry(opts.dir);
164
+ const agent = new AgentRunner(modelConfig, toolRegistry, permissionManager, hookManager, agentOptions);
165
+
166
+ // Restore history
167
+ for (const m of conv.messages) {
168
+ if (m.role === "user") agent.addToHistory("user", m.content);
169
+ else if (m.role === "assistant") agent.addToHistory("assistant", m.content);
170
+ }
171
+
172
+ let response = "";
173
+ const tools: Array<{ name: string; input: Record<string, unknown> }> = [];
174
+
175
+ await agent.run(prompt, {
176
+ onToken: (t) => { response += t; },
177
+ onToolCall: (name, input) => { tools.push({ name, input }); },
178
+ onToolResult: () => {},
179
+ onComplete: () => {},
180
+ onError: (e) => { response = `Error: ${e.message}`; },
181
+ });
182
+
183
+ addMessage(conv, "user", prompt);
184
+ addMessage(conv, "assistant", response);
185
+
186
+ jsonResponse(res, 200, { response, tools, sessionId: conv.id });
187
+ return;
188
+ }
189
+
190
+ // POST /chat — one-shot prompt
191
+ if (req.method === "POST" && pathname === "/chat") {
192
+ const body = await readBody(req);
193
+ const prompt = String(body.prompt || "");
194
+ if (!prompt) { jsonResponse(res, 400, { error: "prompt is required" }); return; }
195
+
196
+ const mc = {
197
+ ...modelConfig,
198
+ ...(body.model ? { model: String(body.model) } : {}),
199
+ ...(body.provider ? { provider: String(body.provider) } : {}),
200
+ };
201
+
202
+ const toolRegistry = createToolRegistry(opts.dir);
203
+ const agent = new AgentRunner(mc, toolRegistry, permissionManager, hookManager, agentOptions);
204
+
205
+ let response = "";
206
+ const tools: Array<{ name: string; input: Record<string, unknown> }> = [];
207
+ let usage: unknown = null;
208
+
209
+ await agent.run(prompt, {
210
+ onToken: (t) => { response += t; },
211
+ onToolCall: (name, input) => { tools.push({ name, input }); },
212
+ onToolResult: () => {},
213
+ onComplete: () => {},
214
+ onError: (e) => { response = `Error: ${e.message}`; },
215
+ onUsage: (u) => { usage = u; },
216
+ });
217
+
218
+ jsonResponse(res, 200, { response, tools, usage });
219
+ return;
220
+ }
221
+
222
+ // POST /chat/stream — streaming via SSE
223
+ if (req.method === "POST" && pathname === "/chat/stream") {
224
+ const body = await readBody(req);
225
+ const prompt = String(body.prompt || "");
226
+ if (!prompt) { jsonResponse(res, 400, { error: "prompt is required" }); return; }
227
+
228
+ res.writeHead(200, {
229
+ "Content-Type": "text/event-stream",
230
+ "Cache-Control": "no-cache",
231
+ "Connection": "keep-alive",
232
+ "Access-Control-Allow-Origin": "*",
233
+ });
234
+
235
+ const sse = (event: string, data: unknown) => {
236
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
237
+ };
238
+
239
+ const toolRegistry = createToolRegistry(opts.dir);
240
+ const agent = new AgentRunner(modelConfig, toolRegistry, permissionManager, hookManager, agentOptions);
241
+
242
+ await agent.run(prompt, {
243
+ onToken: (t) => sse("token", { token: t }),
244
+ onToolCall: (name, input) => sse("tool_call", { name, input }),
245
+ onToolResult: (name, result, isError) => sse("tool_result", { name, result, isError }),
246
+ onComplete: () => { sse("complete", {}); res.end(); },
247
+ onError: (e) => { sse("error", { message: e.message }); res.end(); },
248
+ onUsage: (usage) => sse("usage", usage),
249
+ });
250
+ return;
251
+ }
252
+
253
+ jsonResponse(res, 404, { error: `Not found: ${pathname}` });
254
+ } catch (err) {
255
+ jsonResponse(res, 500, { error: String(err) });
256
+ }
257
+ });
258
+
259
+ server.listen(opts.port, opts.host, () => {
260
+ console.log();
261
+ console.log(chalk.bold.cyan(" 🚀 Cdoing API Server"));
262
+ console.log(chalk.gray(" ─────────────────────────────────────────"));
263
+ console.log(chalk.white(` URL: `) + chalk.cyan(`http://${opts.host}:${opts.port}`));
264
+ console.log(chalk.white(` Provider: `) + chalk.yellow(String(modelConfig.provider || "anthropic")));
265
+ console.log(chalk.white(` Model: `) + chalk.yellow(String(modelConfig.model || "(default)")));
266
+ console.log(chalk.white(` Dir: `) + chalk.gray(opts.dir));
267
+ console.log(chalk.white(` Mode: `) + chalk.green(opts.mode || "auto"));
268
+ console.log();
269
+ console.log(chalk.gray(" Endpoints:"));
270
+ console.log(chalk.dim(" GET /health"));
271
+ console.log(chalk.dim(" GET /sessions — list conversations"));
272
+ console.log(chalk.dim(" POST /sessions — create session"));
273
+ console.log(chalk.dim(" GET /sessions/:id — get session"));
274
+ console.log(chalk.dim(" POST /sessions/:id/messages"));
275
+ console.log(chalk.dim(" POST /chat — one-shot prompt"));
276
+ console.log(chalk.dim(" POST /chat/stream — streaming SSE"));
277
+ console.log();
278
+ console.log(chalk.gray(" Press Ctrl+C to stop."));
279
+ console.log();
280
+ });
281
+
282
+ // Keep server alive until killed
283
+ await new Promise<void>((_resolve, reject) => {
284
+ server.on("error", reject);
285
+ process.on("SIGINT", () => {
286
+ console.log(chalk.yellow("\n\n Shutting down server..."));
287
+ server.close(() => process.exit(0));
288
+ });
289
+ });
290
+ }
package/src/tools.ts ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Tool Registration — creates a ToolRegistry with all core tools.
3
+ */
4
+
5
+ import {
6
+ ToolRegistry,
7
+ FileReadTool,
8
+ FileWriteTool,
9
+ FileEditTool,
10
+ GlobSearchTool,
11
+ GrepSearchTool,
12
+ ShellExecTool,
13
+ FileRunTool,
14
+ CodeVerifyTool,
15
+ WebFetchTool,
16
+ WebSearchTool,
17
+ SubAgentTool,
18
+ SubAgentManager,
19
+ SubAgentStatusTool,
20
+ SubAgentTerminateTool,
21
+ TodoTool,
22
+ TodoStore,
23
+ SandboxManager,
24
+ SystemInfoTool,
25
+ MultiEditTool,
26
+
27
+ ListDirTool,
28
+ ViewDiffTool,
29
+ ViewRepoMapTool,
30
+ CodebaseSearchTool,
31
+ ASTEditTool,
32
+ NotebookEditTool,
33
+ PermissionManager,
34
+ } from "@cdoing/core";
35
+ import type { SubAgentRunnerFactory } from "@cdoing/core";
36
+
37
+ export interface ToolRegistryOptions {
38
+ subAgentFactory?: SubAgentRunnerFactory;
39
+ subAgentManager?: SubAgentManager;
40
+ todoStore?: TodoStore;
41
+ sandboxManager?: SandboxManager;
42
+ permissionManager?: PermissionManager;
43
+ }
44
+
45
+ export function createToolRegistry(
46
+ workingDir: string,
47
+ optionsOrSubAgentFactory?: ToolRegistryOptions | SubAgentRunnerFactory,
48
+ ): ToolRegistry {
49
+ // Support both old signature (subAgentFactory) and new signature (options)
50
+ let options: ToolRegistryOptions = {};
51
+ if (typeof optionsOrSubAgentFactory === "function") {
52
+ options = { subAgentFactory: optionsOrSubAgentFactory };
53
+ } else if (optionsOrSubAgentFactory) {
54
+ options = optionsOrSubAgentFactory;
55
+ }
56
+
57
+ const sm = options.sandboxManager;
58
+ const registry = new ToolRegistry();
59
+
60
+ // File tools
61
+ registry.register(new FileReadTool(workingDir, sm));
62
+ registry.register(new FileWriteTool(workingDir, sm));
63
+ registry.register(new FileEditTool(workingDir, sm));
64
+ registry.register(new MultiEditTool(workingDir, sm));
65
+ registry.register(new ASTEditTool(workingDir, sm));
66
+ registry.register(new NotebookEditTool(workingDir, sm));
67
+
68
+ // Search & discovery tools
69
+ registry.register(new GlobSearchTool(workingDir));
70
+ registry.register(new GrepSearchTool(workingDir));
71
+ registry.register(new ListDirTool(workingDir, sm));
72
+ registry.register(new ViewDiffTool(workingDir));
73
+ registry.register(new ViewRepoMapTool(workingDir));
74
+ registry.register(new CodebaseSearchTool(workingDir));
75
+
76
+ // Execution tools
77
+ registry.register(new ShellExecTool(workingDir, sm, options.permissionManager));
78
+ registry.register(new FileRunTool(workingDir, sm));
79
+ registry.register(new CodeVerifyTool(workingDir));
80
+
81
+ // Web tools
82
+ registry.register(new WebFetchTool(sm));
83
+ registry.register(new WebSearchTool());
84
+
85
+ // Sub-agent (only if factory provided — prevents infinite recursion)
86
+ if (options.subAgentFactory) {
87
+ const manager = options.subAgentManager || new SubAgentManager();
88
+ registry.register(new SubAgentTool(options.subAgentFactory, manager));
89
+ registry.register(new SubAgentStatusTool(manager));
90
+ registry.register(new SubAgentTerminateTool(manager));
91
+ }
92
+
93
+ // Todo tool (for task tracking)
94
+ if (options.todoStore) {
95
+ registry.register(new TodoTool(options.todoStore));
96
+ }
97
+
98
+ // System info tool — gives the LLM live access to its permission/sandbox state
99
+ if (options.permissionManager) {
100
+ registry.register(new SystemInfoTool(options.permissionManager, registry, sm));
101
+ }
102
+
103
+ return registry;
104
+ }