@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.
- package/.cdoing/permissions.json +8 -0
- package/dist/callbacks.d.ts +17 -0
- package/dist/callbacks.d.ts.map +1 -0
- package/dist/callbacks.js +265 -0
- package/dist/callbacks.js.map +1 -0
- package/dist/chat.d.ts +27 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +57 -0
- package/dist/chat.js.map +1 -0
- package/dist/commands.d.ts +22 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +452 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +84 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +427 -0
- package/dist/config.js.map +1 -0
- package/dist/help.d.ts +9 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +167 -0
- package/dist/help.js.map +1 -0
- package/dist/history.d.ts +51 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +207 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +220 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth.d.ts +13 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +182 -0
- package/dist/oauth.js.map +1 -0
- package/dist/review.d.ts +26 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +198 -0
- package/dist/review.js.map +1 -0
- package/dist/serve.d.ts +23 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +293 -0
- package/dist/serve.js.map +1 -0
- package/dist/tools.d.ts +14 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +57 -0
- package/dist/tools.js.map +1 -0
- package/dist/ui/App.d.ts +24 -0
- package/dist/ui/App.d.ts.map +1 -0
- package/dist/ui/App.js +321 -0
- package/dist/ui/App.js.map +1 -0
- package/dist/ui/MessageList.d.ts +14 -0
- package/dist/ui/MessageList.d.ts.map +1 -0
- package/dist/ui/MessageList.js +147 -0
- package/dist/ui/MessageList.js.map +1 -0
- package/dist/ui/SessionBrowser.d.ts +18 -0
- package/dist/ui/SessionBrowser.d.ts.map +1 -0
- package/dist/ui/SessionBrowser.js +149 -0
- package/dist/ui/SessionBrowser.js.map +1 -0
- package/dist/ui/SetupWizard.d.ts +23 -0
- package/dist/ui/SetupWizard.d.ts.map +1 -0
- package/dist/ui/SetupWizard.js +402 -0
- package/dist/ui/SetupWizard.js.map +1 -0
- package/dist/ui/Spinner.d.ts +15 -0
- package/dist/ui/Spinner.d.ts.map +1 -0
- package/dist/ui/Spinner.js +111 -0
- package/dist/ui/Spinner.js.map +1 -0
- package/dist/ui/StatusBar.d.ts +16 -0
- package/dist/ui/StatusBar.d.ts.map +1 -0
- package/dist/ui/StatusBar.js +56 -0
- package/dist/ui/StatusBar.js.map +1 -0
- package/dist/ui/UserInput.d.ts +13 -0
- package/dist/ui/UserInput.d.ts.map +1 -0
- package/dist/ui/UserInput.js +872 -0
- package/dist/ui/UserInput.js.map +1 -0
- package/dist/ui/hooks/helpers.d.ts +55 -0
- package/dist/ui/hooks/helpers.d.ts.map +1 -0
- package/dist/ui/hooks/helpers.js +304 -0
- package/dist/ui/hooks/helpers.js.map +1 -0
- package/dist/ui/hooks/useAgent.d.ts +60 -0
- package/dist/ui/hooks/useAgent.d.ts.map +1 -0
- package/dist/ui/hooks/useAgent.js +213 -0
- package/dist/ui/hooks/useAgent.js.map +1 -0
- package/dist/ui/hooks/useChat.d.ts +74 -0
- package/dist/ui/hooks/useChat.d.ts.map +1 -0
- package/dist/ui/hooks/useChat.js +819 -0
- package/dist/ui/hooks/useChat.js.map +1 -0
- package/dist/ui/theme.d.ts +73 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +214 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/ui/types.d.ts +37 -0
- package/dist/ui/types.d.ts.map +1 -0
- package/dist/ui/types.js +3 -0
- package/dist/ui/types.js.map +1 -0
- package/package.json +33 -0
- package/src/callbacks.ts +294 -0
- package/src/chat.ts +72 -0
- package/src/commands.ts +425 -0
- package/src/config.ts +462 -0
- package/src/help.ts +182 -0
- package/src/history.ts +205 -0
- package/src/index.ts +248 -0
- package/src/oauth.ts +164 -0
- package/src/review.ts +233 -0
- package/src/serve.ts +290 -0
- package/src/tools.ts +104 -0
- package/src/ui/App.tsx +426 -0
- package/src/ui/MessageList.tsx +222 -0
- package/src/ui/SessionBrowser.tsx +161 -0
- package/src/ui/SetupWizard.tsx +412 -0
- package/src/ui/Spinner.tsx +103 -0
- package/src/ui/StatusBar.tsx +106 -0
- package/src/ui/UserInput.tsx +954 -0
- package/src/ui/hooks/helpers.ts +271 -0
- package/src/ui/hooks/useAgent.ts +270 -0
- package/src/ui/hooks/useChat.ts +943 -0
- package/src/ui/theme.ts +326 -0
- package/src/ui/types.ts +41 -0
- 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
|
+
}
|