@alanwchat/coder 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/README.md +289 -0
- package/dist/index.cjs +2987 -0
- package/dist/index.cjs.map +7 -0
- package/package.json +29 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2987 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Coder CLI — AI-powered coding assistant
|
|
4
|
+
* Bundled with esbuild
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
var __defProp = Object.defineProperty;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __esm = (fn, res) => function __init() {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
};
|
|
13
|
+
var __export = (target, all) => {
|
|
14
|
+
for (var name in all)
|
|
15
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/config/index.ts
|
|
19
|
+
var config_exports = {};
|
|
20
|
+
__export(config_exports, {
|
|
21
|
+
getConfigDirPath: () => getConfigDirPath,
|
|
22
|
+
getConfigFilePathExplicit: () => getConfigFilePathExplicit,
|
|
23
|
+
loadConfig: () => loadConfig,
|
|
24
|
+
resolveApiKey: () => resolveApiKey,
|
|
25
|
+
resolveProviderConfig: () => resolveProviderConfig,
|
|
26
|
+
saveConfig: () => saveConfig
|
|
27
|
+
});
|
|
28
|
+
function getConfigDir() {
|
|
29
|
+
const home = (0, import_node_os.homedir)();
|
|
30
|
+
const p = (0, import_node_os.platform)();
|
|
31
|
+
if (p === "win32") {
|
|
32
|
+
return process.env.APPDATA ? (0, import_node_path10.join)(process.env.APPDATA, "Coder", "cli") : (0, import_node_path10.join)(home, ".config", "coder", "cli");
|
|
33
|
+
}
|
|
34
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
35
|
+
return (0, import_node_path10.join)(process.env.XDG_CONFIG_HOME, "coder", "cli");
|
|
36
|
+
}
|
|
37
|
+
return (0, import_node_path10.join)(home, ".config", "coder", "cli");
|
|
38
|
+
}
|
|
39
|
+
function createDefaultProviderSettings(provider) {
|
|
40
|
+
const isCustom = provider === "custom";
|
|
41
|
+
return {
|
|
42
|
+
apiKeySource: "env",
|
|
43
|
+
apiKey: "",
|
|
44
|
+
apiKeyEnvVar: isCustom ? "CUSTOM_API_KEY" : PRESET_PROVIDERS[provider]?.defaultApiKeyEnvVar ?? "API_KEY",
|
|
45
|
+
customBaseUrl: isCustom ? "https://api.example.com/v1" : "",
|
|
46
|
+
showUsage: false
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function createDefaultConfig() {
|
|
50
|
+
const providers = {};
|
|
51
|
+
for (const id of PROVIDER_IDS) {
|
|
52
|
+
providers[id] = createDefaultProviderSettings(id);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
activeProvider: "deepseek",
|
|
56
|
+
providers,
|
|
57
|
+
lastModel: "deepseek-v4-flash",
|
|
58
|
+
showUsage: false
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function getConfigFilePath() {
|
|
62
|
+
return (0, import_node_path10.join)(getConfigDir(), CONFIG_FILE);
|
|
63
|
+
}
|
|
64
|
+
function ensureConfigDir() {
|
|
65
|
+
const dir = getConfigDir();
|
|
66
|
+
if (!(0, import_node_fs9.existsSync)(dir)) {
|
|
67
|
+
(0, import_node_fs9.mkdirSync)(dir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function loadConfig() {
|
|
71
|
+
const configPath = getConfigFilePath();
|
|
72
|
+
if (!(0, import_node_fs9.existsSync)(configPath)) {
|
|
73
|
+
const defaults = createDefaultConfig();
|
|
74
|
+
saveConfig(defaults);
|
|
75
|
+
return defaults;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const raw = (0, import_node_fs9.readFileSync)(configPath, "utf-8");
|
|
79
|
+
const parsed = JSON.parse(raw);
|
|
80
|
+
return mergeWithDefaults(parsed);
|
|
81
|
+
} catch {
|
|
82
|
+
const defaults = createDefaultConfig();
|
|
83
|
+
saveConfig(defaults);
|
|
84
|
+
return defaults;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function mergeWithDefaults(raw) {
|
|
88
|
+
const defaults = createDefaultConfig();
|
|
89
|
+
return {
|
|
90
|
+
activeProvider: PROVIDER_IDS.includes(String(raw.activeProvider ?? "")) ? raw.activeProvider : defaults.activeProvider,
|
|
91
|
+
providers: mergeProviders(raw.providers, defaults.providers),
|
|
92
|
+
lastModel: typeof raw.lastModel === "string" ? raw.lastModel : defaults.lastModel,
|
|
93
|
+
showUsage: typeof raw.showUsage === "boolean" ? raw.showUsage : defaults.showUsage
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function mergeProviders(raw, defaults) {
|
|
97
|
+
const result = { ...defaults };
|
|
98
|
+
if (!raw || typeof raw !== "object") {
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
for (const id of PROVIDER_IDS) {
|
|
102
|
+
const p = raw[id];
|
|
103
|
+
if (p && typeof p === "object") {
|
|
104
|
+
result[id] = {
|
|
105
|
+
apiKeySource: p.apiKeySource === "manual" ? "manual" : "env",
|
|
106
|
+
apiKey: typeof p.apiKey === "string" ? p.apiKey : result[id].apiKey,
|
|
107
|
+
apiKeyEnvVar: typeof p.apiKeyEnvVar === "string" ? p.apiKeyEnvVar : result[id].apiKeyEnvVar,
|
|
108
|
+
customBaseUrl: typeof p.customBaseUrl === "string" ? p.customBaseUrl : result[id].customBaseUrl,
|
|
109
|
+
showUsage: typeof p.showUsage === "boolean" ? p.showUsage : result[id].showUsage
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
function saveConfig(config) {
|
|
116
|
+
ensureConfigDir();
|
|
117
|
+
const configPath = getConfigFilePath();
|
|
118
|
+
(0, import_node_fs9.writeFileSync)(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
119
|
+
}
|
|
120
|
+
function resolveProviderConfig(config, providerId) {
|
|
121
|
+
const provider = providerId ?? config.activeProvider;
|
|
122
|
+
const settings = config.providers[provider];
|
|
123
|
+
if (provider === "custom") {
|
|
124
|
+
if (!settings.customBaseUrl.trim()) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"Custom provider selected but no base URL configured. Run `coder config` or manually edit the config file."
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
provider,
|
|
131
|
+
baseUrl: settings.customBaseUrl.trim(),
|
|
132
|
+
apiKeySource: settings.apiKeySource,
|
|
133
|
+
apiKey: settings.apiKey,
|
|
134
|
+
apiKeyEnvVar: settings.apiKeyEnvVar.trim(),
|
|
135
|
+
models: []
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const preset = PRESET_PROVIDERS[provider];
|
|
139
|
+
if (!preset) {
|
|
140
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
provider,
|
|
144
|
+
baseUrl: settings.customBaseUrl.trim() || preset.baseUrl,
|
|
145
|
+
apiKeySource: settings.apiKeySource,
|
|
146
|
+
apiKey: settings.apiKey,
|
|
147
|
+
apiKeyEnvVar: settings.apiKeyEnvVar.trim() || preset.defaultApiKeyEnvVar,
|
|
148
|
+
models: settings.customBaseUrl.trim() ? [] : PRESET_MODELS[provider] ?? []
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function resolveApiKey(resolved) {
|
|
152
|
+
if (resolved.apiKeySource === "manual") {
|
|
153
|
+
if (!resolved.apiKey.trim()) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`API key for ${resolved.provider} is not configured. Set it with: coder config providers.<provider>.apiKey <key>`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return resolved.apiKey.trim();
|
|
159
|
+
}
|
|
160
|
+
const envVar = resolved.apiKeyEnvVar;
|
|
161
|
+
const envValue = process.env[envVar]?.trim();
|
|
162
|
+
if (!envValue) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Environment variable ${envVar} is not set. Set it with: export ${envVar}=<your-api-key>`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return envValue;
|
|
168
|
+
}
|
|
169
|
+
function getConfigDirPath() {
|
|
170
|
+
return getConfigDir();
|
|
171
|
+
}
|
|
172
|
+
function getConfigFilePathExplicit() {
|
|
173
|
+
return getConfigFilePath();
|
|
174
|
+
}
|
|
175
|
+
var import_node_os, import_node_path10, import_node_fs9, PRESET_PROVIDERS, PRESET_MODELS, PROVIDER_IDS, CONFIG_FILE;
|
|
176
|
+
var init_config = __esm({
|
|
177
|
+
"src/config/index.ts"() {
|
|
178
|
+
"use strict";
|
|
179
|
+
import_node_os = require("node:os");
|
|
180
|
+
import_node_path10 = require("node:path");
|
|
181
|
+
import_node_fs9 = require("node:fs");
|
|
182
|
+
PRESET_PROVIDERS = {
|
|
183
|
+
deepseek: { baseUrl: "https://api.deepseek.com", defaultApiKeyEnvVar: "DEEPSEEK_API_KEY" },
|
|
184
|
+
glm: { baseUrl: "https://open.bigmodel.cn/api/paas/v4", defaultApiKeyEnvVar: "GLM_API_KEY" },
|
|
185
|
+
agnes: { baseUrl: "https://api.agnesai.com", defaultApiKeyEnvVar: "AGNES_API_KEY" },
|
|
186
|
+
nvidia: { baseUrl: "https://integrate.api.nvidia.com/v1", defaultApiKeyEnvVar: "NVIDIA_API_KEY" },
|
|
187
|
+
minimax: { baseUrl: "https://api.minimax.chat/v1", defaultApiKeyEnvVar: "MINIMAX_API_KEY" }
|
|
188
|
+
};
|
|
189
|
+
PRESET_MODELS = {
|
|
190
|
+
deepseek: [
|
|
191
|
+
{ id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", contextWindow: 1e6, supportsThinking: true, supportsMultimodal: false },
|
|
192
|
+
{ id: "deepseek-chat", label: "DeepSeek Chat", contextWindow: 1e6, supportsThinking: false, supportsMultimodal: true }
|
|
193
|
+
],
|
|
194
|
+
glm: [
|
|
195
|
+
{ id: "glm-4", label: "GLM-4", contextWindow: 128e3, supportsThinking: false, supportsMultimodal: true },
|
|
196
|
+
{ id: "glm-4-plus", label: "GLM-4 Plus", contextWindow: 128e3, supportsThinking: true, supportsMultimodal: true }
|
|
197
|
+
],
|
|
198
|
+
agnes: [
|
|
199
|
+
{ id: "agnes-v3", label: "Agnes V3", contextWindow: 128e3, supportsThinking: false, supportsMultimodal: true }
|
|
200
|
+
],
|
|
201
|
+
nvidia: [],
|
|
202
|
+
minimax: [
|
|
203
|
+
{ id: "minimax-m1", label: "MiniMax M1", contextWindow: 2e5, supportsThinking: true, supportsMultimodal: false }
|
|
204
|
+
]
|
|
205
|
+
};
|
|
206
|
+
PROVIDER_IDS = ["deepseek", "glm", "agnes", "nvidia", "minimax", "custom"];
|
|
207
|
+
CONFIG_FILE = "config.json";
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// src/handlers/list-dir.ts
|
|
212
|
+
var import_node_fs = require("node:fs");
|
|
213
|
+
var import_node_path = require("node:path");
|
|
214
|
+
|
|
215
|
+
// src/handlers/result.ts
|
|
216
|
+
function toolSuccess(tool, data) {
|
|
217
|
+
return { ok: true, tool, data };
|
|
218
|
+
}
|
|
219
|
+
function toolFailure(tool, code, message) {
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
tool,
|
|
223
|
+
error: { code, message }
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/handlers/list-dir.ts
|
|
228
|
+
var listDirHandler = async (rawArgs, context) => {
|
|
229
|
+
const args = rawArgs;
|
|
230
|
+
if (!context.workspaceDir) {
|
|
231
|
+
return toolFailure("list_dir", "workspace_required", "No workspace directory set");
|
|
232
|
+
}
|
|
233
|
+
const dirPath = (0, import_node_path.resolve)(context.workspaceDir, args.path);
|
|
234
|
+
const maxDepth = args.recursive ? args.max_depth ?? 1 : 1;
|
|
235
|
+
try {
|
|
236
|
+
const entries = walkDirectory(dirPath, 0, maxDepth, args.show_hidden ?? false);
|
|
237
|
+
return toolSuccess("list_dir", {
|
|
238
|
+
path: (0, import_node_path.relative)(context.workspaceDir, dirPath),
|
|
239
|
+
entries
|
|
240
|
+
});
|
|
241
|
+
} catch (error2) {
|
|
242
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
243
|
+
return toolFailure("list_dir", "read_error", message);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
function walkDirectory(dirPath, currentDepth, maxDepth, showHidden) {
|
|
247
|
+
const results = [];
|
|
248
|
+
let entries;
|
|
249
|
+
try {
|
|
250
|
+
entries = (0, import_node_fs.readdirSync)(dirPath);
|
|
251
|
+
} catch {
|
|
252
|
+
return results;
|
|
253
|
+
}
|
|
254
|
+
for (const name of entries) {
|
|
255
|
+
if (!showHidden && name.startsWith(".")) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const fullPath = (0, import_node_path.resolve)(dirPath, name);
|
|
259
|
+
let stats;
|
|
260
|
+
try {
|
|
261
|
+
stats = (0, import_node_fs.statSync)(fullPath);
|
|
262
|
+
} catch {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
results.push({
|
|
266
|
+
name,
|
|
267
|
+
path: fullPath,
|
|
268
|
+
isDir: stats.isDirectory(),
|
|
269
|
+
size: stats.isFile() ? stats.size : void 0
|
|
270
|
+
});
|
|
271
|
+
if (stats.isDirectory() && currentDepth < maxDepth) {
|
|
272
|
+
results.push(...walkDirectory(fullPath, currentDepth + 1, maxDepth, showHidden));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return results;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/handlers/read-file.ts
|
|
279
|
+
var import_node_fs2 = require("node:fs");
|
|
280
|
+
var import_node_path2 = require("node:path");
|
|
281
|
+
var readFileHandler = async (rawArgs, context) => {
|
|
282
|
+
const args = rawArgs;
|
|
283
|
+
if (!context.workspaceDir) {
|
|
284
|
+
return toolFailure("read_file", "workspace_required", "No workspace directory set");
|
|
285
|
+
}
|
|
286
|
+
const filePath = (0, import_node_path2.resolve)(context.workspaceDir, args.path);
|
|
287
|
+
const startLine = args.start_line ?? 1;
|
|
288
|
+
const maxLines = args.max_lines ?? 500;
|
|
289
|
+
try {
|
|
290
|
+
(0, import_node_fs2.statSync)(filePath);
|
|
291
|
+
const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
|
|
292
|
+
const lines = content.split("\n");
|
|
293
|
+
const totalLines = lines.length;
|
|
294
|
+
const endLine = Math.min(startLine + maxLines - 1, totalLines);
|
|
295
|
+
const paginatedContent = lines.slice(startLine - 1, endLine).join("\n");
|
|
296
|
+
return toolSuccess("read_file", {
|
|
297
|
+
path: filePath,
|
|
298
|
+
encoding: "utf-8",
|
|
299
|
+
mimeType: "text/plain",
|
|
300
|
+
content: paginatedContent,
|
|
301
|
+
totalLines,
|
|
302
|
+
startLine,
|
|
303
|
+
endLine,
|
|
304
|
+
truncated: endLine < totalLines
|
|
305
|
+
});
|
|
306
|
+
} catch (error2) {
|
|
307
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
308
|
+
if (message.includes("ENOENT")) {
|
|
309
|
+
return toolFailure("read_file", "not_found", `File not found: ${filePath}`);
|
|
310
|
+
}
|
|
311
|
+
return toolFailure("read_file", "read_error", message);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// src/handlers/write-file.ts
|
|
316
|
+
var import_node_fs3 = require("node:fs");
|
|
317
|
+
var import_node_path3 = require("node:path");
|
|
318
|
+
var writeFileHandler = async (rawArgs, context) => {
|
|
319
|
+
const args = rawArgs;
|
|
320
|
+
if (!context.workspaceDir) {
|
|
321
|
+
return toolFailure("write_file", "workspace_required", "No workspace directory set");
|
|
322
|
+
}
|
|
323
|
+
const filePath = (0, import_node_path3.resolve)(context.workspaceDir, args.path);
|
|
324
|
+
if (!filePath.startsWith(context.workspaceDir.replace(/\\/g, "/").replace(/\\/g, "/"))) {
|
|
325
|
+
return toolFailure("write_file", "path_escape", "Path escapes workspace directory");
|
|
326
|
+
}
|
|
327
|
+
if ((0, import_node_fs3.existsSync)(filePath)) {
|
|
328
|
+
return toolFailure("write_file", "file_exists", `File already exists: ${args.path}. Use replace_file to overwrite.`);
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
if (args.create_parent_dirs !== false) {
|
|
332
|
+
const parentDir = (0, import_node_path3.dirname)(filePath);
|
|
333
|
+
if (!(0, import_node_fs3.existsSync)(parentDir)) {
|
|
334
|
+
(0, import_node_fs3.mkdirSync)(parentDir, { recursive: true });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const lines = args.content.split("\n");
|
|
338
|
+
(0, import_node_fs3.writeFileSync)(filePath, args.content, "utf-8");
|
|
339
|
+
return toolSuccess("write_file", {
|
|
340
|
+
path: args.path,
|
|
341
|
+
action: "created",
|
|
342
|
+
bytesWritten: Buffer.byteLength(args.content, "utf-8"),
|
|
343
|
+
linesAdded: lines.length,
|
|
344
|
+
linesRemoved: 0
|
|
345
|
+
});
|
|
346
|
+
} catch (error2) {
|
|
347
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
348
|
+
return toolFailure("write_file", "write_error", message);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// src/handlers/replace-file.ts
|
|
353
|
+
var import_node_fs4 = require("node:fs");
|
|
354
|
+
var import_node_path4 = require("node:path");
|
|
355
|
+
var import_node_crypto = require("node:crypto");
|
|
356
|
+
var replaceFileHandler = async (rawArgs, context) => {
|
|
357
|
+
const args = rawArgs;
|
|
358
|
+
if (!context.workspaceDir) {
|
|
359
|
+
return toolFailure("replace_file", "workspace_required", "No workspace directory set");
|
|
360
|
+
}
|
|
361
|
+
const filePath = (0, import_node_path4.resolve)(context.workspaceDir, args.path);
|
|
362
|
+
if (!(0, import_node_fs4.existsSync)(filePath)) {
|
|
363
|
+
return toolFailure("replace_file", "not_found", `File not found: ${args.path}`);
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
const oldContent = (0, import_node_fs4.readFileSync)(filePath, "utf-8");
|
|
367
|
+
const oldLines = oldContent.split("\n");
|
|
368
|
+
const newLines = args.content.split("\n");
|
|
369
|
+
if (args.expected_sha256) {
|
|
370
|
+
const actualHash = (0, import_node_crypto.createHash)("sha256").update(oldContent).digest("hex");
|
|
371
|
+
if (actualHash !== args.expected_sha256) {
|
|
372
|
+
return toolFailure(
|
|
373
|
+
"replace_file",
|
|
374
|
+
"content_changed",
|
|
375
|
+
`File content changed since last read. Expected SHA256: ${args.expected_sha256}, got: ${actualHash}. Read the file again to get the latest content.`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
(0, import_node_fs4.writeFileSync)(filePath, args.content, "utf-8");
|
|
380
|
+
return toolSuccess("replace_file", {
|
|
381
|
+
path: args.path,
|
|
382
|
+
action: "replaced",
|
|
383
|
+
bytesWritten: Buffer.byteLength(args.content, "utf-8"),
|
|
384
|
+
linesAdded: newLines.length,
|
|
385
|
+
linesRemoved: oldLines.length
|
|
386
|
+
});
|
|
387
|
+
} catch (error2) {
|
|
388
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
389
|
+
return toolFailure("replace_file", "write_error", message);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// src/handlers/edit-file.ts
|
|
394
|
+
var import_node_fs5 = require("node:fs");
|
|
395
|
+
var import_node_path5 = require("node:path");
|
|
396
|
+
var import_node_crypto2 = require("node:crypto");
|
|
397
|
+
var editFileHandler = async (rawArgs, context) => {
|
|
398
|
+
const args = rawArgs;
|
|
399
|
+
if (!context.workspaceDir) {
|
|
400
|
+
return toolFailure("edit_file", "workspace_required", "No workspace directory set");
|
|
401
|
+
}
|
|
402
|
+
const filePath = (0, import_node_path5.resolve)(context.workspaceDir, args.path);
|
|
403
|
+
if (!(0, import_node_fs5.existsSync)(filePath)) {
|
|
404
|
+
return toolFailure("edit_file", "not_found", `File not found: ${args.path}`);
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const oldContent = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
|
|
408
|
+
const oldLines = oldContent.split("\n");
|
|
409
|
+
if (args.expected_sha256) {
|
|
410
|
+
const actualHash = (0, import_node_crypto2.createHash)("sha256").update(oldContent).digest("hex");
|
|
411
|
+
if (actualHash !== args.expected_sha256) {
|
|
412
|
+
return toolFailure(
|
|
413
|
+
"edit_file",
|
|
414
|
+
"content_changed",
|
|
415
|
+
`File content changed since last read. Expected SHA256: ${args.expected_sha256}, got: ${actualHash}.`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
let newContent;
|
|
420
|
+
if (args.replace_all) {
|
|
421
|
+
newContent = oldContent.split(args.old_string).join(args.new_string);
|
|
422
|
+
} else {
|
|
423
|
+
const index = oldContent.indexOf(args.old_string);
|
|
424
|
+
if (index === -1) {
|
|
425
|
+
return toolFailure(
|
|
426
|
+
"edit_file",
|
|
427
|
+
"string_not_found",
|
|
428
|
+
`Could not find the exact text to replace in ${args.path}. The text must match exactly.`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
newContent = oldContent.slice(0, index) + args.new_string + oldContent.slice(index + args.old_string.length);
|
|
432
|
+
}
|
|
433
|
+
const newLines = newContent.split("\n");
|
|
434
|
+
(0, import_node_fs5.writeFileSync)(filePath, newContent, "utf-8");
|
|
435
|
+
const linesAdded = newLines.length - oldLines.length;
|
|
436
|
+
const linesRemoved = oldLines.length - newLines.length;
|
|
437
|
+
return toolSuccess("edit_file", {
|
|
438
|
+
path: args.path,
|
|
439
|
+
action: "modified",
|
|
440
|
+
bytesWritten: Buffer.byteLength(newContent, "utf-8"),
|
|
441
|
+
linesAdded: linesAdded > 0 ? linesAdded : 0,
|
|
442
|
+
linesRemoved: linesRemoved > 0 ? linesRemoved : 0
|
|
443
|
+
});
|
|
444
|
+
} catch (error2) {
|
|
445
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
446
|
+
return toolFailure("edit_file", "write_error", message);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// src/handlers/replace-lines.ts
|
|
451
|
+
var import_node_fs6 = require("node:fs");
|
|
452
|
+
var import_node_path6 = require("node:path");
|
|
453
|
+
var import_node_crypto3 = require("node:crypto");
|
|
454
|
+
var replaceLinesHandler = async (rawArgs, context) => {
|
|
455
|
+
const args = rawArgs;
|
|
456
|
+
if (!context.workspaceDir) {
|
|
457
|
+
return toolFailure("replace_lines", "workspace_required", "No workspace directory set");
|
|
458
|
+
}
|
|
459
|
+
const filePath = (0, import_node_path6.resolve)(context.workspaceDir, args.path);
|
|
460
|
+
if (!(0, import_node_fs6.existsSync)(filePath)) {
|
|
461
|
+
return toolFailure("replace_lines", "not_found", `File not found: ${args.path}`);
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
const oldContent = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
|
|
465
|
+
const oldLines = oldContent.split("\n");
|
|
466
|
+
if (args.expected_sha256) {
|
|
467
|
+
const actualHash = (0, import_node_crypto3.createHash)("sha256").update(oldContent).digest("hex");
|
|
468
|
+
if (actualHash !== args.expected_sha256) {
|
|
469
|
+
return toolFailure(
|
|
470
|
+
"replace_lines",
|
|
471
|
+
"content_changed",
|
|
472
|
+
`File content changed since last read. Expected SHA256: ${args.expected_sha256}, got: ${actualHash}.`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (args.start_line < 1 || args.end_line < args.start_line || args.start_line > oldLines.length) {
|
|
477
|
+
return toolFailure(
|
|
478
|
+
"replace_lines",
|
|
479
|
+
"invalid_range",
|
|
480
|
+
`Invalid line range: ${args.start_line}-${args.end_line}. File has ${oldLines.length} lines.`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
const endLine = Math.min(args.end_line, oldLines.length);
|
|
484
|
+
const replacementLines = args.content === "" ? [] : args.content.split("\n");
|
|
485
|
+
const before = oldLines.slice(0, args.start_line - 1);
|
|
486
|
+
const after = oldLines.slice(endLine);
|
|
487
|
+
const newContent = [...before, ...replacementLines, ...after].join("\n");
|
|
488
|
+
(0, import_node_fs6.writeFileSync)(filePath, newContent, "utf-8");
|
|
489
|
+
return toolSuccess("replace_lines", {
|
|
490
|
+
path: args.path,
|
|
491
|
+
action: "modified",
|
|
492
|
+
bytesWritten: Buffer.byteLength(newContent, "utf-8"),
|
|
493
|
+
linesAdded: replacementLines.length,
|
|
494
|
+
linesRemoved: endLine - args.start_line + 1
|
|
495
|
+
});
|
|
496
|
+
} catch (error2) {
|
|
497
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
498
|
+
return toolFailure("replace_lines", "write_error", message);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// src/handlers/glob.ts
|
|
503
|
+
var import_node_fs7 = require("node:fs");
|
|
504
|
+
var import_node_path7 = require("node:path");
|
|
505
|
+
var globHandler = async (rawArgs, context) => {
|
|
506
|
+
const args = rawArgs;
|
|
507
|
+
if (!context.workspaceDir) {
|
|
508
|
+
return toolFailure("glob", "workspace_required", "No workspace directory set");
|
|
509
|
+
}
|
|
510
|
+
const searchDir = args.target_directory ? (0, import_node_path7.resolve)(context.workspaceDir, args.target_directory) : context.workspaceDir;
|
|
511
|
+
const headLimit = args.head_limit ?? 100;
|
|
512
|
+
const pattern = args.glob_pattern;
|
|
513
|
+
try {
|
|
514
|
+
const matches = simpleGlob(searchDir, pattern, context.workspaceDir, headLimit);
|
|
515
|
+
return toolSuccess("glob", {
|
|
516
|
+
pattern,
|
|
517
|
+
targetDirectory: (0, import_node_path7.relative)(context.workspaceDir, searchDir),
|
|
518
|
+
matches: matches.slice(0, headLimit),
|
|
519
|
+
totalMatches: matches.length,
|
|
520
|
+
truncated: matches.length > headLimit
|
|
521
|
+
});
|
|
522
|
+
} catch (error2) {
|
|
523
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
524
|
+
return toolFailure("glob", "search_error", message);
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
function simpleGlob(rootDir, pattern, workspaceDir, limit) {
|
|
528
|
+
const results = [];
|
|
529
|
+
function walk(dir) {
|
|
530
|
+
if (results.length >= limit) return;
|
|
531
|
+
let entries;
|
|
532
|
+
try {
|
|
533
|
+
entries = (0, import_node_fs7.readdirSync)(dir);
|
|
534
|
+
} catch {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
for (const name of entries) {
|
|
538
|
+
if (results.length >= limit) return;
|
|
539
|
+
if (name.startsWith(".")) continue;
|
|
540
|
+
const fullPath = (0, import_node_path7.resolve)(dir, name);
|
|
541
|
+
const relPath = (0, import_node_path7.relative)(workspaceDir, fullPath).replace(/\\/g, "/");
|
|
542
|
+
let stats;
|
|
543
|
+
try {
|
|
544
|
+
stats = (0, import_node_fs7.statSync)(fullPath);
|
|
545
|
+
} catch {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
const regex = globToRegex(pattern);
|
|
549
|
+
if (regex.test(relPath)) {
|
|
550
|
+
results.push(relPath);
|
|
551
|
+
}
|
|
552
|
+
if (stats.isDirectory()) {
|
|
553
|
+
walk(fullPath);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
walk(rootDir);
|
|
558
|
+
results.sort();
|
|
559
|
+
return results;
|
|
560
|
+
}
|
|
561
|
+
function globToRegex(pattern) {
|
|
562
|
+
let regexStr = "^";
|
|
563
|
+
let i = 0;
|
|
564
|
+
while (i < pattern.length) {
|
|
565
|
+
const ch = pattern[i];
|
|
566
|
+
if (ch === "*") {
|
|
567
|
+
if (i + 1 < pattern.length && pattern[i + 1] === "*") {
|
|
568
|
+
regexStr += ".*";
|
|
569
|
+
i += 2;
|
|
570
|
+
if (i < pattern.length && pattern[i] === "/") {
|
|
571
|
+
regexStr += "/?";
|
|
572
|
+
i++;
|
|
573
|
+
}
|
|
574
|
+
} else {
|
|
575
|
+
regexStr += "[^/]*";
|
|
576
|
+
i++;
|
|
577
|
+
}
|
|
578
|
+
} else if (ch === "?") {
|
|
579
|
+
regexStr += "[^/]";
|
|
580
|
+
i++;
|
|
581
|
+
} else if (ch === "." || ch === "+" || ch === "(" || ch === ")" || ch === "[" || ch === "]" || ch === "{" || ch === "}" || ch === "\\" || ch === "^" || ch === "$" || ch === "|") {
|
|
582
|
+
regexStr += "\\" + ch;
|
|
583
|
+
i++;
|
|
584
|
+
} else {
|
|
585
|
+
regexStr += ch;
|
|
586
|
+
i++;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
regexStr += "$";
|
|
590
|
+
return new RegExp(regexStr, "i");
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/handlers/grep.ts
|
|
594
|
+
var import_node_fs8 = require("node:fs");
|
|
595
|
+
var import_node_path8 = require("node:path");
|
|
596
|
+
var grepHandler = async (rawArgs, context) => {
|
|
597
|
+
const args = rawArgs;
|
|
598
|
+
if (!context.workspaceDir) {
|
|
599
|
+
return toolFailure("grep", "workspace_required", "No workspace directory set");
|
|
600
|
+
}
|
|
601
|
+
const searchPath = args.path ? (0, import_node_path8.resolve)(context.workspaceDir, args.path) : context.workspaceDir;
|
|
602
|
+
const headLimit = args.head_limit ?? 200;
|
|
603
|
+
const offset = args.offset ?? 0;
|
|
604
|
+
const ctxBefore = args.context_before ?? args.context ?? 0;
|
|
605
|
+
const ctxAfter = args.context_after ?? args.context ?? 0;
|
|
606
|
+
const flags = args.case_insensitive ? "gi" : "g";
|
|
607
|
+
const outputMode = args.output_mode ?? "content";
|
|
608
|
+
const multiline = args.multiline ?? false;
|
|
609
|
+
try {
|
|
610
|
+
let searchRegex;
|
|
611
|
+
try {
|
|
612
|
+
const regexFlags = multiline ? `${flags}s` : flags;
|
|
613
|
+
searchRegex = new RegExp(args.pattern, regexFlags);
|
|
614
|
+
} catch {
|
|
615
|
+
return toolFailure("grep", "invalid_pattern", `Invalid regex pattern: ${args.pattern}`);
|
|
616
|
+
}
|
|
617
|
+
const files = collectFiles(searchPath);
|
|
618
|
+
const matches = [];
|
|
619
|
+
const fileMatchSet = /* @__PURE__ */ new Set();
|
|
620
|
+
const fileCountMap = {};
|
|
621
|
+
let totalMatches = 0;
|
|
622
|
+
let skippedFiles = 0;
|
|
623
|
+
for (const file of files) {
|
|
624
|
+
if (totalMatches >= headLimit + offset) break;
|
|
625
|
+
let content;
|
|
626
|
+
try {
|
|
627
|
+
content = (0, import_node_fs8.readFileSync)(file, "utf-8");
|
|
628
|
+
} catch {
|
|
629
|
+
skippedFiles++;
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
const relativePath = (0, import_node_path8.relative)(context.workspaceDir, file);
|
|
633
|
+
let fileHasMatch = false;
|
|
634
|
+
let fileMatchCount = 0;
|
|
635
|
+
if (multiline) {
|
|
636
|
+
const lines = content.split("\n");
|
|
637
|
+
searchRegex.lastIndex = 0;
|
|
638
|
+
const fullMatch = searchRegex.exec(content);
|
|
639
|
+
if (fullMatch) {
|
|
640
|
+
fileHasMatch = true;
|
|
641
|
+
do {
|
|
642
|
+
totalMatches++;
|
|
643
|
+
fileMatchCount++;
|
|
644
|
+
searchRegex.lastIndex = fullMatch.index + 1;
|
|
645
|
+
} while (searchRegex.exec(content));
|
|
646
|
+
searchRegex.lastIndex = 0;
|
|
647
|
+
if (outputMode === "content") {
|
|
648
|
+
matches.push({
|
|
649
|
+
path: relativePath,
|
|
650
|
+
lineNumber: 1,
|
|
651
|
+
line: fullMatch[0].slice(0, 200)
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
} else {
|
|
656
|
+
const lines = content.split("\n");
|
|
657
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
658
|
+
searchRegex.lastIndex = 0;
|
|
659
|
+
if (searchRegex.test(lines[lineNum])) {
|
|
660
|
+
totalMatches++;
|
|
661
|
+
fileMatchCount++;
|
|
662
|
+
if (outputMode === "files_with_matches") {
|
|
663
|
+
fileMatchSet.add(relativePath);
|
|
664
|
+
fileHasMatch = true;
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
if (outputMode === "count") {
|
|
668
|
+
fileHasMatch = true;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
if (totalMatches > offset && totalMatches <= offset + headLimit) {
|
|
672
|
+
const contextBeforeLines = ctxBefore > 0 ? lines.slice(Math.max(0, lineNum - ctxBefore), lineNum) : void 0;
|
|
673
|
+
const contextAfterLines = ctxAfter > 0 ? lines.slice(lineNum + 1, lineNum + 1 + ctxAfter) : void 0;
|
|
674
|
+
matches.push({
|
|
675
|
+
path: relativePath,
|
|
676
|
+
lineNumber: lineNum + 1,
|
|
677
|
+
line: lines[lineNum],
|
|
678
|
+
contextBefore: contextBeforeLines?.length ? contextBeforeLines : void 0,
|
|
679
|
+
contextAfter: contextAfterLines?.length ? contextAfterLines : void 0
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (fileHasMatch || fileMatchCount > 0) {
|
|
686
|
+
fileMatchSet.add(relativePath);
|
|
687
|
+
fileCountMap[relativePath] = fileMatchCount;
|
|
688
|
+
}
|
|
689
|
+
if (outputMode === "files_with_matches" && fileMatchSet.size >= headLimit) {
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const result = {
|
|
694
|
+
pattern: args.pattern,
|
|
695
|
+
path: (0, import_node_path8.relative)(context.workspaceDir, searchPath),
|
|
696
|
+
outputMode,
|
|
697
|
+
totalMatches,
|
|
698
|
+
truncated: totalMatches > headLimit
|
|
699
|
+
};
|
|
700
|
+
if (outputMode === "content") {
|
|
701
|
+
result.matches = matches;
|
|
702
|
+
} else if (outputMode === "files_with_matches") {
|
|
703
|
+
result.files = [...fileMatchSet];
|
|
704
|
+
} else if (outputMode === "count") {
|
|
705
|
+
result.files = [...fileMatchSet];
|
|
706
|
+
result.fileCounts = fileCountMap;
|
|
707
|
+
}
|
|
708
|
+
if (skippedFiles > 0) {
|
|
709
|
+
result.skippedFiles = skippedFiles;
|
|
710
|
+
}
|
|
711
|
+
return toolSuccess("grep", result);
|
|
712
|
+
} catch (error2) {
|
|
713
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
714
|
+
return toolFailure("grep", "search_error", message);
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
function collectFiles(dirPath) {
|
|
718
|
+
try {
|
|
719
|
+
const stats = (0, import_node_fs8.statSync)(dirPath);
|
|
720
|
+
if (stats.isFile()) {
|
|
721
|
+
return [dirPath];
|
|
722
|
+
}
|
|
723
|
+
} catch {
|
|
724
|
+
return [];
|
|
725
|
+
}
|
|
726
|
+
const results = [];
|
|
727
|
+
function walk(dir) {
|
|
728
|
+
let entries;
|
|
729
|
+
try {
|
|
730
|
+
entries = (0, import_node_fs8.readdirSync)(dir);
|
|
731
|
+
} catch {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
for (const name of entries) {
|
|
735
|
+
if (name === ".git" || name === "node_modules") continue;
|
|
736
|
+
const fullPath = (0, import_node_path8.resolve)(dir, name);
|
|
737
|
+
let stats;
|
|
738
|
+
try {
|
|
739
|
+
stats = (0, import_node_fs8.statSync)(fullPath);
|
|
740
|
+
} catch {
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (stats.isDirectory()) {
|
|
744
|
+
walk(fullPath);
|
|
745
|
+
} else if (stats.isFile() && stats.size > 0) {
|
|
746
|
+
results.push(fullPath);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
walk(dirPath);
|
|
751
|
+
return results;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/handlers/shell-manager.ts
|
|
755
|
+
var import_node_child_process = require("node:child_process");
|
|
756
|
+
var import_node_path9 = require("node:path");
|
|
757
|
+
var shells = /* @__PURE__ */ new Map();
|
|
758
|
+
var shellCounter = 0;
|
|
759
|
+
function generateShellId() {
|
|
760
|
+
shellCounter++;
|
|
761
|
+
return `shell-${Date.now()}-${shellCounter}`;
|
|
762
|
+
}
|
|
763
|
+
async function executeShell(command, options) {
|
|
764
|
+
const shellId = generateShellId();
|
|
765
|
+
const cwd = options.workspaceDir ? options.workingDirectory ? (0, import_node_path9.resolve)(options.workspaceDir, options.workingDirectory) : options.workspaceDir : process.cwd();
|
|
766
|
+
const startTime = Date.now();
|
|
767
|
+
const blockMs = options.blockUntilMs ?? 3e4;
|
|
768
|
+
const isWindows = process.platform === "win32";
|
|
769
|
+
const shellCmd = isWindows ? "cmd.exe" : "/bin/sh";
|
|
770
|
+
const shellArgs = isWindows ? ["/c", command] : ["-c", command];
|
|
771
|
+
return new Promise((resolvePromise) => {
|
|
772
|
+
const child = (0, import_node_child_process.spawn)(shellCmd, shellArgs, {
|
|
773
|
+
cwd,
|
|
774
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
775
|
+
shell: false,
|
|
776
|
+
env: { ...process.env }
|
|
777
|
+
});
|
|
778
|
+
let stdout = "";
|
|
779
|
+
let stderr = "";
|
|
780
|
+
let settled = false;
|
|
781
|
+
const shell = {
|
|
782
|
+
shellId,
|
|
783
|
+
command,
|
|
784
|
+
description: options.description,
|
|
785
|
+
process: child,
|
|
786
|
+
status: "running",
|
|
787
|
+
stdout,
|
|
788
|
+
stderr,
|
|
789
|
+
workingDirectory: cwd,
|
|
790
|
+
startedAtMs: startTime,
|
|
791
|
+
taskId: options.taskId
|
|
792
|
+
};
|
|
793
|
+
child.stdout?.on("data", (data) => {
|
|
794
|
+
stdout += data.toString();
|
|
795
|
+
shell.stdout = stdout;
|
|
796
|
+
});
|
|
797
|
+
child.stderr?.on("data", (data) => {
|
|
798
|
+
stderr += data.toString();
|
|
799
|
+
shell.stderr = stderr;
|
|
800
|
+
});
|
|
801
|
+
const finish = (status, code) => {
|
|
802
|
+
if (settled) return;
|
|
803
|
+
settled = true;
|
|
804
|
+
shell.status = status;
|
|
805
|
+
shell.exitCode = code;
|
|
806
|
+
const duration = Date.now() - startTime;
|
|
807
|
+
const stdoutBytes = Buffer.byteLength(stdout);
|
|
808
|
+
const stderrBytes = Buffer.byteLength(stderr);
|
|
809
|
+
if (blockMs <= 0) {
|
|
810
|
+
shells.set(shellId, shell);
|
|
811
|
+
} else {
|
|
812
|
+
shells.delete(shellId);
|
|
813
|
+
}
|
|
814
|
+
resolvePromise({
|
|
815
|
+
command,
|
|
816
|
+
description: options.description,
|
|
817
|
+
workingDirectory: cwd,
|
|
818
|
+
stdout,
|
|
819
|
+
stderr,
|
|
820
|
+
stdoutTruncated: stdoutBytes > 65536,
|
|
821
|
+
stderrTruncated: stderrBytes > 65536,
|
|
822
|
+
stdoutTotalBytes: stdoutBytes,
|
|
823
|
+
stderrTotalBytes: stderrBytes,
|
|
824
|
+
exitCode: code,
|
|
825
|
+
durationMs: duration,
|
|
826
|
+
status,
|
|
827
|
+
shellId: blockMs <= 0 ? shellId : void 0
|
|
828
|
+
});
|
|
829
|
+
};
|
|
830
|
+
child.on("error", (err) => {
|
|
831
|
+
stderr += err.message;
|
|
832
|
+
finish("failed", 1);
|
|
833
|
+
});
|
|
834
|
+
child.on("close", (code) => {
|
|
835
|
+
finish(code === 0 ? "completed" : "failed", code ?? void 0);
|
|
836
|
+
});
|
|
837
|
+
if (blockMs > 0) {
|
|
838
|
+
const timeout = setTimeout(() => {
|
|
839
|
+
if (!settled) {
|
|
840
|
+
child.kill();
|
|
841
|
+
finish("timeout", void 0);
|
|
842
|
+
}
|
|
843
|
+
}, blockMs);
|
|
844
|
+
child.on("close", () => clearTimeout(timeout));
|
|
845
|
+
}
|
|
846
|
+
if (blockMs <= 0) {
|
|
847
|
+
shells.set(shellId, shell);
|
|
848
|
+
const stdoutBytes = Buffer.byteLength(stdout);
|
|
849
|
+
const stderrBytes = Buffer.byteLength(stderr);
|
|
850
|
+
resolvePromise({
|
|
851
|
+
command,
|
|
852
|
+
description: options.description,
|
|
853
|
+
workingDirectory: cwd,
|
|
854
|
+
stdout: "",
|
|
855
|
+
stderr: "",
|
|
856
|
+
stdoutTruncated: false,
|
|
857
|
+
stderrTruncated: false,
|
|
858
|
+
stdoutTotalBytes: 0,
|
|
859
|
+
stderrTotalBytes: 0,
|
|
860
|
+
exitCode: void 0,
|
|
861
|
+
durationMs: 0,
|
|
862
|
+
status: "running",
|
|
863
|
+
shellId
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
async function awaitShell(shellId, blockUntilMs = 3e4) {
|
|
869
|
+
const shell = shells.get(shellId);
|
|
870
|
+
if (!shell) {
|
|
871
|
+
return {
|
|
872
|
+
command: "",
|
|
873
|
+
workingDirectory: "",
|
|
874
|
+
stdout: "",
|
|
875
|
+
stderr: "",
|
|
876
|
+
stdoutTruncated: false,
|
|
877
|
+
stderrTruncated: false,
|
|
878
|
+
stdoutTotalBytes: 0,
|
|
879
|
+
stderrTotalBytes: 0,
|
|
880
|
+
durationMs: 0,
|
|
881
|
+
status: "completed",
|
|
882
|
+
shellId
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
return new Promise((resolvePromise) => {
|
|
886
|
+
const startTime = Date.now();
|
|
887
|
+
const checkProcess = () => {
|
|
888
|
+
if (shell.exitCode !== void 0 || shell.status !== "running") {
|
|
889
|
+
const duration = Date.now() - startTime;
|
|
890
|
+
const stdoutBytes = Buffer.byteLength(shell.stdout);
|
|
891
|
+
const stderrBytes = Buffer.byteLength(shell.stderr);
|
|
892
|
+
resolvePromise({
|
|
893
|
+
command: shell.command,
|
|
894
|
+
description: shell.description,
|
|
895
|
+
workingDirectory: shell.workingDirectory,
|
|
896
|
+
stdout: shell.stdout,
|
|
897
|
+
stderr: shell.stderr,
|
|
898
|
+
stdoutTruncated: stdoutBytes > 65536,
|
|
899
|
+
stderrTruncated: stderrBytes > 65536,
|
|
900
|
+
stdoutTotalBytes: stdoutBytes,
|
|
901
|
+
stderrTotalBytes: stderrBytes,
|
|
902
|
+
exitCode: shell.exitCode,
|
|
903
|
+
durationMs: duration,
|
|
904
|
+
status: shell.status,
|
|
905
|
+
shellId
|
|
906
|
+
});
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (Date.now() - startTime > blockUntilMs) {
|
|
910
|
+
const duration = Date.now() - startTime;
|
|
911
|
+
const stdoutBytes = Buffer.byteLength(shell.stdout);
|
|
912
|
+
const stderrBytes = Buffer.byteLength(shell.stderr);
|
|
913
|
+
resolvePromise({
|
|
914
|
+
command: shell.command,
|
|
915
|
+
description: shell.description,
|
|
916
|
+
workingDirectory: shell.workingDirectory,
|
|
917
|
+
stdout: shell.stdout,
|
|
918
|
+
stderr: shell.stderr,
|
|
919
|
+
stdoutTruncated: stdoutBytes > 65536,
|
|
920
|
+
stderrTruncated: stderrBytes > 65536,
|
|
921
|
+
stdoutTotalBytes: stdoutBytes,
|
|
922
|
+
stderrTotalBytes: stderrBytes,
|
|
923
|
+
exitCode: shell.exitCode,
|
|
924
|
+
durationMs: duration,
|
|
925
|
+
status: shell.status,
|
|
926
|
+
shellId
|
|
927
|
+
});
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
setTimeout(checkProcess, 50);
|
|
931
|
+
};
|
|
932
|
+
shell.process.on("close", () => {
|
|
933
|
+
checkProcess();
|
|
934
|
+
});
|
|
935
|
+
checkProcess();
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
function listShells(statusFilter) {
|
|
939
|
+
const result = [];
|
|
940
|
+
for (const shell of shells.values()) {
|
|
941
|
+
if (statusFilter && shell.status !== statusFilter && statusFilter !== "all") {
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
result.push({
|
|
945
|
+
shellId: shell.shellId,
|
|
946
|
+
command: shell.command,
|
|
947
|
+
description: shell.description,
|
|
948
|
+
workingDirectory: shell.workingDirectory,
|
|
949
|
+
status: shell.status,
|
|
950
|
+
exitCode: shell.exitCode,
|
|
951
|
+
startedAtMs: shell.startedAtMs,
|
|
952
|
+
taskId: shell.taskId,
|
|
953
|
+
stdout: shell.stdout,
|
|
954
|
+
stderr: shell.stderr,
|
|
955
|
+
stdoutTruncated: Buffer.byteLength(shell.stdout) > 65536,
|
|
956
|
+
stderrTruncated: Buffer.byteLength(shell.stderr) > 65536
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
return result;
|
|
960
|
+
}
|
|
961
|
+
function killShell(shellId) {
|
|
962
|
+
const shell = shells.get(shellId);
|
|
963
|
+
if (!shell) return false;
|
|
964
|
+
try {
|
|
965
|
+
shell.process.kill("SIGTERM");
|
|
966
|
+
shell.status = "cancelled";
|
|
967
|
+
setTimeout(() => {
|
|
968
|
+
try {
|
|
969
|
+
shell.process.kill("SIGKILL");
|
|
970
|
+
} catch {
|
|
971
|
+
}
|
|
972
|
+
}, 2e3);
|
|
973
|
+
return true;
|
|
974
|
+
} catch {
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function readShellLogs(shellId, stream, offset = 0, limit = 4096) {
|
|
979
|
+
const shell = shells.get(shellId);
|
|
980
|
+
if (!shell) {
|
|
981
|
+
return { data: "", offset: 0, totalBytes: 0, truncated: false };
|
|
982
|
+
}
|
|
983
|
+
const content = stream === "stdout" ? shell.stdout : shell.stderr;
|
|
984
|
+
const totalBytes = Buffer.byteLength(content);
|
|
985
|
+
const sliced = content.slice(offset, offset + limit);
|
|
986
|
+
return {
|
|
987
|
+
data: sliced,
|
|
988
|
+
offset: offset + sliced.length,
|
|
989
|
+
totalBytes,
|
|
990
|
+
truncated: offset + limit < totalBytes
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// src/handlers/shell.ts
|
|
995
|
+
var shellHandler = async (rawArgs, context) => {
|
|
996
|
+
const args = rawArgs;
|
|
997
|
+
if (!args.command?.trim()) {
|
|
998
|
+
return toolFailure("shell", "invalid_arguments", "Command is required");
|
|
999
|
+
}
|
|
1000
|
+
try {
|
|
1001
|
+
const result = await executeShell(args.command.trim(), {
|
|
1002
|
+
workingDirectory: args.working_directory,
|
|
1003
|
+
description: args.description,
|
|
1004
|
+
blockUntilMs: args.block_until_ms ?? 3e4,
|
|
1005
|
+
taskId: context.taskId,
|
|
1006
|
+
workspaceDir: context.workspaceDir
|
|
1007
|
+
});
|
|
1008
|
+
return toolSuccess("shell", result);
|
|
1009
|
+
} catch (error2) {
|
|
1010
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1011
|
+
return toolFailure("shell", "execution_error", message);
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
// src/handlers/await-shell.ts
|
|
1016
|
+
var awaitShellHandler = async (rawArgs, _context) => {
|
|
1017
|
+
const args = rawArgs;
|
|
1018
|
+
if (!args.shell_id?.trim()) {
|
|
1019
|
+
return toolFailure("await", "invalid_arguments", "shell_id is required");
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
const result = await awaitShell(args.shell_id.trim(), args.block_until_ms ?? 3e4);
|
|
1023
|
+
return toolSuccess("await", result);
|
|
1024
|
+
} catch (error2) {
|
|
1025
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1026
|
+
return toolFailure("await", "error", message);
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
// src/handlers/list-shells.ts
|
|
1031
|
+
var listShellsHandler = async (rawArgs, _context) => {
|
|
1032
|
+
const args = rawArgs;
|
|
1033
|
+
const shells2 = listShells(args.status_filter);
|
|
1034
|
+
return toolSuccess("list_shells", {
|
|
1035
|
+
shells: shells2,
|
|
1036
|
+
total: shells2.length
|
|
1037
|
+
});
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
// src/handlers/kill-shell.ts
|
|
1041
|
+
var killShellHandler = async (rawArgs, _context) => {
|
|
1042
|
+
const args = rawArgs;
|
|
1043
|
+
if (!args.shell_id?.trim()) {
|
|
1044
|
+
return toolFailure("kill_shell", "invalid_arguments", "shell_id is required");
|
|
1045
|
+
}
|
|
1046
|
+
const killed = killShell(args.shell_id.trim());
|
|
1047
|
+
return toolSuccess("kill_shell", {
|
|
1048
|
+
shellId: args.shell_id.trim(),
|
|
1049
|
+
killed
|
|
1050
|
+
});
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
// src/handlers/read-shell-logs.ts
|
|
1054
|
+
var readShellLogsHandler = async (rawArgs, _context) => {
|
|
1055
|
+
const args = rawArgs;
|
|
1056
|
+
if (!args.shell_id?.trim()) {
|
|
1057
|
+
return toolFailure("read_shell_logs", "invalid_arguments", "shell_id is required");
|
|
1058
|
+
}
|
|
1059
|
+
const result = readShellLogs(
|
|
1060
|
+
args.shell_id.trim(),
|
|
1061
|
+
args.stream ?? "stdout",
|
|
1062
|
+
args.offset ?? 0,
|
|
1063
|
+
args.limit ?? 4096
|
|
1064
|
+
);
|
|
1065
|
+
return toolSuccess("read_shell_logs", {
|
|
1066
|
+
shellId: args.shell_id.trim(),
|
|
1067
|
+
stream: args.stream ?? "stdout",
|
|
1068
|
+
data: result.data,
|
|
1069
|
+
offset: result.offset,
|
|
1070
|
+
totalBytes: result.totalBytes,
|
|
1071
|
+
truncated: result.truncated
|
|
1072
|
+
});
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
// src/handlers/web-search.ts
|
|
1076
|
+
var webSearchHandler = async (rawArgs, _context) => {
|
|
1077
|
+
const args = rawArgs;
|
|
1078
|
+
if (!args.search_term?.trim()) {
|
|
1079
|
+
return toolFailure("web_search", "invalid_arguments", "search_term is required");
|
|
1080
|
+
}
|
|
1081
|
+
try {
|
|
1082
|
+
const results = await performWebSearch(args.search_term, args.max_results ?? 5);
|
|
1083
|
+
return toolSuccess("web_search", {
|
|
1084
|
+
query: args.search_term,
|
|
1085
|
+
results
|
|
1086
|
+
});
|
|
1087
|
+
} catch (error2) {
|
|
1088
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1089
|
+
return toolFailure("web_search", "search_error", message);
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
async function performWebSearch(query, maxResults) {
|
|
1093
|
+
try {
|
|
1094
|
+
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`;
|
|
1095
|
+
const response = await fetch(url, {
|
|
1096
|
+
signal: AbortSignal.timeout(1e4)
|
|
1097
|
+
});
|
|
1098
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1099
|
+
const data = await response.json();
|
|
1100
|
+
const results = [];
|
|
1101
|
+
if (data.AbstractText) {
|
|
1102
|
+
results.push({
|
|
1103
|
+
title: data.AbstractSource ?? "Result",
|
|
1104
|
+
url: data.AbstractURL ?? "",
|
|
1105
|
+
snippet: data.AbstractText
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
if (data.RelatedTopics) {
|
|
1109
|
+
for (const topic of data.RelatedTopics.slice(0, maxResults)) {
|
|
1110
|
+
if (topic.Text && topic.FirstURL) {
|
|
1111
|
+
results.push({
|
|
1112
|
+
title: topic.Text.split(" - ")[0] ?? "Result",
|
|
1113
|
+
url: topic.FirstURL,
|
|
1114
|
+
snippet: topic.Text
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return results.slice(0, maxResults);
|
|
1120
|
+
} catch {
|
|
1121
|
+
throw new Error(
|
|
1122
|
+
"Web search is currently unavailable. Configure a Tavily API key for web search support, or set the TAVILY_API_KEY environment variable."
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// src/handlers/browse-page.ts
|
|
1128
|
+
var browsePageHandler = async (rawArgs, _context) => {
|
|
1129
|
+
const args = rawArgs;
|
|
1130
|
+
if (!args.url?.trim()) {
|
|
1131
|
+
return toolFailure("browse_page", "invalid_arguments", "url is required");
|
|
1132
|
+
}
|
|
1133
|
+
try {
|
|
1134
|
+
const response = await fetch(args.url.trim(), {
|
|
1135
|
+
headers: {
|
|
1136
|
+
"User-Agent": "Mozilla/5.0 (compatible; CoderCLI/1.0)",
|
|
1137
|
+
Accept: "text/html,text/plain,*/*"
|
|
1138
|
+
},
|
|
1139
|
+
signal: AbortSignal.timeout(15e3)
|
|
1140
|
+
});
|
|
1141
|
+
const contentType = response.headers.get("content-type") ?? "text/plain";
|
|
1142
|
+
const finalUrl = response.url;
|
|
1143
|
+
const title = extractTitleFromUrl(finalUrl);
|
|
1144
|
+
let content = await response.text();
|
|
1145
|
+
const startLine = args.start_line ?? 1;
|
|
1146
|
+
const maxLines = args.max_lines ?? 500;
|
|
1147
|
+
const lines = content.split("\n");
|
|
1148
|
+
const totalLines = lines.length;
|
|
1149
|
+
const endLine = Math.min(startLine + maxLines - 1, totalLines);
|
|
1150
|
+
const paginatedContent = lines.slice(startLine - 1, endLine).join("\n");
|
|
1151
|
+
return toolSuccess("browse_page", {
|
|
1152
|
+
url: args.url,
|
|
1153
|
+
finalUrl,
|
|
1154
|
+
title,
|
|
1155
|
+
content: paginatedContent,
|
|
1156
|
+
truncated: endLine < totalLines,
|
|
1157
|
+
statusCode: response.status,
|
|
1158
|
+
contentType
|
|
1159
|
+
});
|
|
1160
|
+
} catch (error2) {
|
|
1161
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1162
|
+
if (message.includes("fetch")) {
|
|
1163
|
+
return toolFailure("browse_page", "network_error", `Failed to fetch URL: ${message}`);
|
|
1164
|
+
}
|
|
1165
|
+
return toolFailure("browse_page", "error", message);
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
function extractTitleFromUrl(url) {
|
|
1169
|
+
try {
|
|
1170
|
+
const parsed = new URL(url);
|
|
1171
|
+
return parsed.hostname;
|
|
1172
|
+
} catch {
|
|
1173
|
+
return void 0;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// src/handlers/todos.ts
|
|
1178
|
+
var import_node_fs10 = require("node:fs");
|
|
1179
|
+
var import_node_path11 = require("node:path");
|
|
1180
|
+
init_config();
|
|
1181
|
+
function getTodosFilePath(sessionId) {
|
|
1182
|
+
const dir = (0, import_node_path11.join)(getConfigDirPath(), "todos");
|
|
1183
|
+
if (!(0, import_node_fs10.existsSync)(dir)) {
|
|
1184
|
+
(0, import_node_fs10.mkdirSync)(dir, { recursive: true });
|
|
1185
|
+
}
|
|
1186
|
+
const safeSessionId = (sessionId ?? "default").replace(/[^a-z0-9-]/gi, "");
|
|
1187
|
+
return (0, import_node_path11.join)(dir, `${safeSessionId}.json`);
|
|
1188
|
+
}
|
|
1189
|
+
function readTodos(sessionId) {
|
|
1190
|
+
const filePath = getTodosFilePath(sessionId);
|
|
1191
|
+
if (!(0, import_node_fs10.existsSync)(filePath)) return [];
|
|
1192
|
+
try {
|
|
1193
|
+
return JSON.parse((0, import_node_fs10.readFileSync)(filePath, "utf-8"));
|
|
1194
|
+
} catch {
|
|
1195
|
+
return [];
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
function writeTodos(todos, sessionId) {
|
|
1199
|
+
(0, import_node_fs10.writeFileSync)(getTodosFilePath(sessionId), JSON.stringify(todos, null, 2), "utf-8");
|
|
1200
|
+
}
|
|
1201
|
+
var todoReadHandler = async (rawArgs, context) => {
|
|
1202
|
+
const args = rawArgs;
|
|
1203
|
+
const sessionId = args.session_id ?? context.sessionId;
|
|
1204
|
+
const todos = readTodos(sessionId);
|
|
1205
|
+
const snapshot = todos.sort((a, b) => a.order - b.order).map((t) => ({
|
|
1206
|
+
id: t.id,
|
|
1207
|
+
content: t.content,
|
|
1208
|
+
status: t.status
|
|
1209
|
+
}));
|
|
1210
|
+
return toolSuccess("todo_read", {
|
|
1211
|
+
sessionId: sessionId ?? null,
|
|
1212
|
+
todos: snapshot,
|
|
1213
|
+
total: snapshot.length,
|
|
1214
|
+
active: snapshot.filter((t) => t.status === "pending" || t.status === "in_progress").length,
|
|
1215
|
+
completed: snapshot.filter((t) => t.status === "completed").length
|
|
1216
|
+
});
|
|
1217
|
+
};
|
|
1218
|
+
var todoWriteHandler = async (rawArgs, context) => {
|
|
1219
|
+
const args = rawArgs;
|
|
1220
|
+
const sessionId = args.session_id ?? context.sessionId;
|
|
1221
|
+
if (!args.todos || !Array.isArray(args.todos)) {
|
|
1222
|
+
return toolFailure("todo_write", "invalid_arguments", "todos array is required");
|
|
1223
|
+
}
|
|
1224
|
+
const existing = readTodos(sessionId);
|
|
1225
|
+
const existingMap = new Map(existing.map((t) => [t.id, t]));
|
|
1226
|
+
for (let i = 0; i < args.todos.length; i++) {
|
|
1227
|
+
const input = args.todos[i];
|
|
1228
|
+
const existingRecord = existingMap.get(input.id);
|
|
1229
|
+
if (existingRecord) {
|
|
1230
|
+
existingRecord.content = input.content;
|
|
1231
|
+
existingRecord.status = input.status;
|
|
1232
|
+
existingRecord.updatedAt = Date.now();
|
|
1233
|
+
} else {
|
|
1234
|
+
existing.push({
|
|
1235
|
+
id: input.id,
|
|
1236
|
+
content: input.content,
|
|
1237
|
+
status: input.status,
|
|
1238
|
+
order: i,
|
|
1239
|
+
createdAt: Date.now(),
|
|
1240
|
+
updatedAt: Date.now()
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
writeTodos(existing, sessionId);
|
|
1245
|
+
return toolSuccess("todo_write", {
|
|
1246
|
+
total: existing.length,
|
|
1247
|
+
updated: args.todos.length
|
|
1248
|
+
});
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
// src/handlers/plans.ts
|
|
1252
|
+
var import_node_fs11 = require("node:fs");
|
|
1253
|
+
var import_node_path12 = require("node:path");
|
|
1254
|
+
init_config();
|
|
1255
|
+
function getPlansDir() {
|
|
1256
|
+
const dir = (0, import_node_path12.join)(getConfigDirPath(), "plans");
|
|
1257
|
+
if (!(0, import_node_fs11.existsSync)(dir)) {
|
|
1258
|
+
(0, import_node_fs11.mkdirSync)(dir, { recursive: true });
|
|
1259
|
+
}
|
|
1260
|
+
return dir;
|
|
1261
|
+
}
|
|
1262
|
+
function getPlanFilePath(name) {
|
|
1263
|
+
const safeName = name.replace(/[^a-z0-9-_.]/gi, "").toLowerCase();
|
|
1264
|
+
return (0, import_node_path12.join)(getPlansDir(), `${safeName}.json`);
|
|
1265
|
+
}
|
|
1266
|
+
function readPlanFile(name) {
|
|
1267
|
+
const filePath = getPlanFilePath(name);
|
|
1268
|
+
if (!(0, import_node_fs11.existsSync)(filePath)) return null;
|
|
1269
|
+
try {
|
|
1270
|
+
return JSON.parse((0, import_node_fs11.readFileSync)(filePath, "utf-8"));
|
|
1271
|
+
} catch {
|
|
1272
|
+
return null;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
function writePlanFile(name, plan) {
|
|
1276
|
+
(0, import_node_fs11.writeFileSync)(getPlanFilePath(name), JSON.stringify(plan, null, 2), "utf-8");
|
|
1277
|
+
}
|
|
1278
|
+
function deletePlanFile(name) {
|
|
1279
|
+
const filePath = getPlanFilePath(name);
|
|
1280
|
+
if (!(0, import_node_fs11.existsSync)(filePath)) return false;
|
|
1281
|
+
try {
|
|
1282
|
+
(0, import_node_fs11.writeFileSync)(filePath, JSON.stringify({ deleted: true }), "utf-8");
|
|
1283
|
+
return true;
|
|
1284
|
+
} catch {
|
|
1285
|
+
return false;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
function listPlanFiles() {
|
|
1289
|
+
return (0, import_node_fs11.readdirSync)(getPlansDir()).filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
|
|
1290
|
+
}
|
|
1291
|
+
var planCreateHandler = async (rawArgs, _context) => {
|
|
1292
|
+
const args = rawArgs;
|
|
1293
|
+
if (!args.name?.trim() || !args.content?.trim()) {
|
|
1294
|
+
return toolFailure("plan_create", "invalid_arguments", "name and content are required");
|
|
1295
|
+
}
|
|
1296
|
+
const name = args.name.trim().toLowerCase().replace(/[^a-z0-9-_.]/gi, "");
|
|
1297
|
+
if (readPlanFile(name)) {
|
|
1298
|
+
return toolFailure("plan_create", "exists", `Plan already exists: ${name}`);
|
|
1299
|
+
}
|
|
1300
|
+
writePlanFile(name, {
|
|
1301
|
+
name,
|
|
1302
|
+
title: args.title?.trim() ?? name,
|
|
1303
|
+
content: args.content.trim(),
|
|
1304
|
+
createdAt: Date.now(),
|
|
1305
|
+
updatedAt: Date.now()
|
|
1306
|
+
});
|
|
1307
|
+
return toolSuccess("plan_create", { name, title: args.title?.trim() ?? name });
|
|
1308
|
+
};
|
|
1309
|
+
var planReadHandler = async (rawArgs, _context) => {
|
|
1310
|
+
const args = rawArgs;
|
|
1311
|
+
if (!args.name?.trim()) {
|
|
1312
|
+
return toolFailure("plan_read", "invalid_arguments", "name is required");
|
|
1313
|
+
}
|
|
1314
|
+
const plan = readPlanFile(args.name.trim());
|
|
1315
|
+
if (!plan) {
|
|
1316
|
+
return toolFailure("plan_read", "not_found", `Plan not found: ${args.name}`);
|
|
1317
|
+
}
|
|
1318
|
+
return toolSuccess("plan_read", plan);
|
|
1319
|
+
};
|
|
1320
|
+
var planUpdateHandler = async (rawArgs, _context) => {
|
|
1321
|
+
const args = rawArgs;
|
|
1322
|
+
if (!args.name?.trim()) {
|
|
1323
|
+
return toolFailure("plan_update", "invalid_arguments", "name is required");
|
|
1324
|
+
}
|
|
1325
|
+
const existing = readPlanFile(args.name.trim());
|
|
1326
|
+
if (!existing) {
|
|
1327
|
+
return toolFailure("plan_update", "not_found", `Plan not found: ${args.name}`);
|
|
1328
|
+
}
|
|
1329
|
+
writePlanFile(args.name.trim(), {
|
|
1330
|
+
...existing,
|
|
1331
|
+
title: args.title?.trim() ?? existing.title,
|
|
1332
|
+
content: args.content?.trim() ?? existing.content,
|
|
1333
|
+
updatedAt: Date.now()
|
|
1334
|
+
});
|
|
1335
|
+
return toolSuccess("plan_update", { name: args.name.trim() });
|
|
1336
|
+
};
|
|
1337
|
+
var planEditHandler = async (rawArgs, _context) => {
|
|
1338
|
+
const args = rawArgs;
|
|
1339
|
+
if (!args.name?.trim()) {
|
|
1340
|
+
return toolFailure("plan_edit", "invalid_arguments", "name is required");
|
|
1341
|
+
}
|
|
1342
|
+
const existing = readPlanFile(args.name.trim());
|
|
1343
|
+
if (!existing) {
|
|
1344
|
+
return toolFailure("plan_edit", "not_found", `Plan not found: ${args.name}`);
|
|
1345
|
+
}
|
|
1346
|
+
writePlanFile(args.name.trim(), {
|
|
1347
|
+
...existing,
|
|
1348
|
+
content: existing.content + "\n\n## Edit Instructions\n\n" + (args.instructions?.trim() ?? ""),
|
|
1349
|
+
updatedAt: Date.now()
|
|
1350
|
+
});
|
|
1351
|
+
return toolSuccess("plan_edit", { name: args.name.trim() });
|
|
1352
|
+
};
|
|
1353
|
+
var planDeleteHandler = async (rawArgs, _context) => {
|
|
1354
|
+
const args = rawArgs;
|
|
1355
|
+
if (!args.name?.trim()) {
|
|
1356
|
+
return toolFailure("plan_delete", "invalid_arguments", "name is required");
|
|
1357
|
+
}
|
|
1358
|
+
deletePlanFile(args.name.trim());
|
|
1359
|
+
return toolSuccess("plan_delete", { name: args.name.trim(), deleted: true });
|
|
1360
|
+
};
|
|
1361
|
+
var planListHandler = async (_rawArgs, _context) => {
|
|
1362
|
+
const names = listPlanFiles();
|
|
1363
|
+
const plans = names.map((name) => readPlanFile(name)).filter((p) => p !== null).map((p) => ({ name: p.name, title: p.title }));
|
|
1364
|
+
return toolSuccess("plan_list", { plans, total: plans.length });
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
// src/handlers/workspace-tree.ts
|
|
1368
|
+
var import_node_fs12 = require("node:fs");
|
|
1369
|
+
var import_node_path13 = require("node:path");
|
|
1370
|
+
var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
1371
|
+
"node_modules",
|
|
1372
|
+
".git",
|
|
1373
|
+
"dist",
|
|
1374
|
+
"build",
|
|
1375
|
+
"target",
|
|
1376
|
+
".next",
|
|
1377
|
+
"out",
|
|
1378
|
+
".cache",
|
|
1379
|
+
".turbo",
|
|
1380
|
+
"coverage",
|
|
1381
|
+
".nyc_output",
|
|
1382
|
+
"__pycache__",
|
|
1383
|
+
".venv",
|
|
1384
|
+
"venv",
|
|
1385
|
+
".history"
|
|
1386
|
+
]);
|
|
1387
|
+
var getWorkspaceTreeHandler = async (rawArgs, context) => {
|
|
1388
|
+
const args = rawArgs;
|
|
1389
|
+
if (!context.workspaceDir) {
|
|
1390
|
+
return toolFailure("get_workspace_tree", "workspace_required", "No workspace directory set");
|
|
1391
|
+
}
|
|
1392
|
+
try {
|
|
1393
|
+
const rootDir = context.workspaceDir;
|
|
1394
|
+
const treeLines = buildTree(rootDir, rootDir, 0);
|
|
1395
|
+
const fullText = treeLines.join("\n");
|
|
1396
|
+
const totalLines = treeLines.length;
|
|
1397
|
+
const startLine = args.start_line ?? 1;
|
|
1398
|
+
const maxLines = args.max_lines ?? 500;
|
|
1399
|
+
const endLine = Math.min(startLine + maxLines - 1, totalLines);
|
|
1400
|
+
const paginatedText = treeLines.slice(startLine - 1, endLine).join("\n");
|
|
1401
|
+
return toolSuccess("get_workspace_tree", {
|
|
1402
|
+
treeText: paginatedText,
|
|
1403
|
+
totalLines,
|
|
1404
|
+
startLine,
|
|
1405
|
+
endLine,
|
|
1406
|
+
truncated: endLine < totalLines
|
|
1407
|
+
});
|
|
1408
|
+
} catch (error2) {
|
|
1409
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1410
|
+
return toolFailure("get_workspace_tree", "error", message);
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
function buildTree(rootDir, currentDir, depth) {
|
|
1414
|
+
const lines = [];
|
|
1415
|
+
const relPath = (0, import_node_path13.relative)(rootDir, currentDir);
|
|
1416
|
+
if (depth === 0) {
|
|
1417
|
+
lines.push((0, import_node_path13.relative)(rootDir, currentDir) || ".");
|
|
1418
|
+
}
|
|
1419
|
+
let entries;
|
|
1420
|
+
try {
|
|
1421
|
+
entries = (0, import_node_fs12.readdirSync)(currentDir);
|
|
1422
|
+
} catch {
|
|
1423
|
+
return lines;
|
|
1424
|
+
}
|
|
1425
|
+
const dirs = [];
|
|
1426
|
+
const files = [];
|
|
1427
|
+
for (const name of entries) {
|
|
1428
|
+
if (name.startsWith(".") && depth === 0 && name === ".git") continue;
|
|
1429
|
+
if (EXCLUDED_DIRS.has(name)) continue;
|
|
1430
|
+
if (name.startsWith(".") && depth > 0) continue;
|
|
1431
|
+
const fullPath = (0, import_node_path13.resolve)(currentDir, name);
|
|
1432
|
+
try {
|
|
1433
|
+
const stats = (0, import_node_fs12.statSync)(fullPath);
|
|
1434
|
+
if (stats.isDirectory()) {
|
|
1435
|
+
dirs.push(name);
|
|
1436
|
+
} else {
|
|
1437
|
+
files.push(name);
|
|
1438
|
+
}
|
|
1439
|
+
} catch {
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
dirs.sort();
|
|
1443
|
+
files.sort();
|
|
1444
|
+
const allEntries = [...dirs, ...files];
|
|
1445
|
+
for (let i = 0; i < allEntries.length; i++) {
|
|
1446
|
+
const name = allEntries[i];
|
|
1447
|
+
const isLast = i === allEntries.length - 1;
|
|
1448
|
+
const prefix = depth === 0 ? isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 " : "";
|
|
1449
|
+
const childPrefix = depth === 0 ? isLast ? " " : "\u2502 " : "";
|
|
1450
|
+
const fullPath = (0, import_node_path13.resolve)(currentDir, name);
|
|
1451
|
+
lines.push(`${prefix}${name}`);
|
|
1452
|
+
try {
|
|
1453
|
+
const stats = (0, import_node_fs12.statSync)(fullPath);
|
|
1454
|
+
if (stats.isDirectory()) {
|
|
1455
|
+
const childLines = buildTreeFromDepth(rootDir, fullPath, depth + 1, prefix || "", childPrefix);
|
|
1456
|
+
lines.push(...childLines);
|
|
1457
|
+
}
|
|
1458
|
+
} catch {
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
return lines;
|
|
1462
|
+
}
|
|
1463
|
+
function buildTreeFromDepth(rootDir, currentDir, depth, parentPrefix, prefix) {
|
|
1464
|
+
const lines = [];
|
|
1465
|
+
let entries;
|
|
1466
|
+
try {
|
|
1467
|
+
entries = (0, import_node_fs12.readdirSync)(currentDir);
|
|
1468
|
+
} catch {
|
|
1469
|
+
return lines;
|
|
1470
|
+
}
|
|
1471
|
+
const dirs = [];
|
|
1472
|
+
const files = [];
|
|
1473
|
+
for (const name of entries) {
|
|
1474
|
+
if (EXCLUDED_DIRS.has(name)) continue;
|
|
1475
|
+
if (name.startsWith(".")) continue;
|
|
1476
|
+
const fullPath = (0, import_node_path13.resolve)(currentDir, name);
|
|
1477
|
+
try {
|
|
1478
|
+
const stats = (0, import_node_fs12.statSync)(fullPath);
|
|
1479
|
+
if (stats.isDirectory()) {
|
|
1480
|
+
dirs.push(name);
|
|
1481
|
+
} else {
|
|
1482
|
+
files.push(name);
|
|
1483
|
+
}
|
|
1484
|
+
} catch {
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
dirs.sort();
|
|
1488
|
+
files.sort();
|
|
1489
|
+
const allEntries = [...dirs, ...files];
|
|
1490
|
+
for (let i = 0; i < allEntries.length; i++) {
|
|
1491
|
+
const name = allEntries[i];
|
|
1492
|
+
const isLast = i === allEntries.length - 1;
|
|
1493
|
+
const linePrefix = `${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}`;
|
|
1494
|
+
const childPrefix = `${prefix}${isLast ? " " : "\u2502 "}`;
|
|
1495
|
+
lines.push(`${linePrefix}${name}`);
|
|
1496
|
+
const fullPath = (0, import_node_path13.resolve)(currentDir, name);
|
|
1497
|
+
try {
|
|
1498
|
+
const stats = (0, import_node_fs12.statSync)(fullPath);
|
|
1499
|
+
if (stats.isDirectory()) {
|
|
1500
|
+
const childLines = buildTreeFromDepth(rootDir, fullPath, depth + 1, "", childPrefix);
|
|
1501
|
+
lines.push(...childLines);
|
|
1502
|
+
}
|
|
1503
|
+
} catch {
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return lines;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// src/handlers/ask-question.ts
|
|
1510
|
+
var import_node_readline = require("node:readline");
|
|
1511
|
+
var askQuestionHandler = async (rawArgs, _context) => {
|
|
1512
|
+
const args = rawArgs;
|
|
1513
|
+
if (!args.question?.trim()) {
|
|
1514
|
+
return toolFailure("ask_question", "invalid_arguments", "question is required");
|
|
1515
|
+
}
|
|
1516
|
+
const rl = (0, import_node_readline.createInterface)({
|
|
1517
|
+
input: process.stdin,
|
|
1518
|
+
output: process.stderr
|
|
1519
|
+
});
|
|
1520
|
+
const answer = await new Promise((resolve12) => {
|
|
1521
|
+
process.stderr.write(`
|
|
1522
|
+
\x1B[33m\u2753 ${args.question}\x1B[0m
|
|
1523
|
+
> `);
|
|
1524
|
+
rl.once("line", (line) => {
|
|
1525
|
+
resolve12(line.trim());
|
|
1526
|
+
});
|
|
1527
|
+
});
|
|
1528
|
+
rl.close();
|
|
1529
|
+
if (!answer) {
|
|
1530
|
+
return toolFailure("ask_question", "no_answer", "User did not provide an answer");
|
|
1531
|
+
}
|
|
1532
|
+
return toolSuccess("ask_question", {
|
|
1533
|
+
question: args.question,
|
|
1534
|
+
answer
|
|
1535
|
+
});
|
|
1536
|
+
};
|
|
1537
|
+
|
|
1538
|
+
// src/handlers/spawn-subagent.ts
|
|
1539
|
+
var spawnAgentFunction = null;
|
|
1540
|
+
var spawnSubAgentHandler = async (rawArgs, _context) => {
|
|
1541
|
+
const args = rawArgs;
|
|
1542
|
+
if (!args.task?.trim()) {
|
|
1543
|
+
return toolFailure("spawn_subagent", "invalid_arguments", "task is required");
|
|
1544
|
+
}
|
|
1545
|
+
if (!spawnAgentFunction) {
|
|
1546
|
+
return toolFailure(
|
|
1547
|
+
"spawn_subagent",
|
|
1548
|
+
"not_available",
|
|
1549
|
+
"Sub-agent spawning is not available in the current configuration."
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
try {
|
|
1553
|
+
const result = await spawnAgentFunction(args.task, args.context, args.tools);
|
|
1554
|
+
return toolSuccess("spawn_subagent", result);
|
|
1555
|
+
} catch (error2) {
|
|
1556
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1557
|
+
return toolFailure("spawn_subagent", "execution_error", message);
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
// src/handlers/index.ts
|
|
1562
|
+
var AGENT_TOOL_DEFINITIONS = [
|
|
1563
|
+
{
|
|
1564
|
+
type: "function",
|
|
1565
|
+
function: {
|
|
1566
|
+
name: "list_dir",
|
|
1567
|
+
description: "List files and directories under a path. Relative paths are resolved against the workspace root.",
|
|
1568
|
+
parameters: {
|
|
1569
|
+
type: "object",
|
|
1570
|
+
properties: {
|
|
1571
|
+
path: { type: "string", description: "Relative or absolute path within the workspace." },
|
|
1572
|
+
recursive: { type: "boolean", description: "Whether to list entries recursively.", default: false },
|
|
1573
|
+
max_depth: { type: "integer", description: "Maximum recursion depth when recursive is true.", default: 1 },
|
|
1574
|
+
show_hidden: { type: "boolean", description: "Whether to include dotfiles and dot-directories.", default: false }
|
|
1575
|
+
},
|
|
1576
|
+
required: ["path"],
|
|
1577
|
+
additionalProperties: false
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
},
|
|
1581
|
+
{
|
|
1582
|
+
type: "function",
|
|
1583
|
+
function: {
|
|
1584
|
+
name: "read_file",
|
|
1585
|
+
description: "Read a text file with line numbers and pagination. Relative paths are resolved against the workspace root.",
|
|
1586
|
+
parameters: {
|
|
1587
|
+
type: "object",
|
|
1588
|
+
properties: {
|
|
1589
|
+
path: { type: "string", description: "Relative or absolute path to the file within the workspace." },
|
|
1590
|
+
start_line: { type: "integer", description: "First line to read (1-based).", default: 1 },
|
|
1591
|
+
max_lines: { type: "integer", description: "Maximum number of lines to return.", default: 500 },
|
|
1592
|
+
respect_gitignore: { type: "boolean", description: "Whether to refuse reading paths ignored by .gitignore.", default: true }
|
|
1593
|
+
},
|
|
1594
|
+
required: ["path"],
|
|
1595
|
+
additionalProperties: false
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
},
|
|
1599
|
+
{
|
|
1600
|
+
type: "function",
|
|
1601
|
+
function: {
|
|
1602
|
+
name: "write_file",
|
|
1603
|
+
description: "Create a new text file. Fails if the file already exists. Use replace_file or edit_file to modify existing files.",
|
|
1604
|
+
parameters: {
|
|
1605
|
+
type: "object",
|
|
1606
|
+
properties: {
|
|
1607
|
+
path: { type: "string", description: "Relative or absolute path to the new file within the workspace." },
|
|
1608
|
+
content: { type: "string", description: "Full file content to write." },
|
|
1609
|
+
create_parent_dirs: { type: "boolean", description: "Whether to create missing parent directories.", default: true }
|
|
1610
|
+
},
|
|
1611
|
+
required: ["path", "content"],
|
|
1612
|
+
additionalProperties: false
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
type: "function",
|
|
1618
|
+
function: {
|
|
1619
|
+
name: "replace_file",
|
|
1620
|
+
description: "Replace the entire contents of an existing text file. Use expected_sha256 from read_file to avoid overwriting concurrent changes.",
|
|
1621
|
+
parameters: {
|
|
1622
|
+
type: "object",
|
|
1623
|
+
properties: {
|
|
1624
|
+
path: { type: "string", description: "Relative or absolute path to the file within the workspace." },
|
|
1625
|
+
content: { type: "string", description: "Full replacement file content." },
|
|
1626
|
+
expected_sha256: { type: "string", description: "SHA256 hash from read_file. Rejects the write if the file changed." },
|
|
1627
|
+
create_backup: { type: "boolean", description: "Whether to save a backup copy under .history before writing.", default: false },
|
|
1628
|
+
respect_gitignore: { type: "boolean", description: "Whether to refuse editing paths ignored by .gitignore.", default: true }
|
|
1629
|
+
},
|
|
1630
|
+
required: ["path", "content"],
|
|
1631
|
+
additionalProperties: false
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
type: "function",
|
|
1637
|
+
function: {
|
|
1638
|
+
name: "edit_file",
|
|
1639
|
+
description: "Apply a targeted search-and-replace edit to an existing text file. Prefer this over replace_file for small changes.",
|
|
1640
|
+
parameters: {
|
|
1641
|
+
type: "object",
|
|
1642
|
+
properties: {
|
|
1643
|
+
path: { type: "string", description: "Relative or absolute path to the file within the workspace." },
|
|
1644
|
+
old_string: { type: "string", description: "Exact text to replace. Must match uniquely unless replace_all is true." },
|
|
1645
|
+
new_string: { type: "string", description: "Replacement text." },
|
|
1646
|
+
expected_sha256: { type: "string", description: "SHA256 hash from read_file. Rejects the edit if the file changed." },
|
|
1647
|
+
replace_all: { type: "boolean", description: "Whether to replace every occurrence of old_string.", default: false },
|
|
1648
|
+
create_backup: { type: "boolean", description: "Whether to save a backup copy under .history before writing.", default: false },
|
|
1649
|
+
respect_gitignore: { type: "boolean", description: "Whether to refuse editing paths ignored by .gitignore.", default: true }
|
|
1650
|
+
},
|
|
1651
|
+
required: ["path", "old_string", "new_string"],
|
|
1652
|
+
additionalProperties: false
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
},
|
|
1656
|
+
{
|
|
1657
|
+
type: "function",
|
|
1658
|
+
function: {
|
|
1659
|
+
name: "replace_lines",
|
|
1660
|
+
description: "Replace a range of lines in an existing text file by line number. Read the file with read_file first to see line numbers.",
|
|
1661
|
+
parameters: {
|
|
1662
|
+
type: "object",
|
|
1663
|
+
properties: {
|
|
1664
|
+
path: { type: "string", description: "Relative or absolute path to the file within the workspace." },
|
|
1665
|
+
start_line: { type: "integer", description: "First line to replace (1-based)." },
|
|
1666
|
+
end_line: { type: "integer", description: "Last line to replace (inclusive, 1-based)." },
|
|
1667
|
+
content: { type: "string", description: "Replacement content for the specified line range." },
|
|
1668
|
+
expected_sha256: { type: "string", description: "SHA256 hash from read_file. Rejects the edit if the file changed." },
|
|
1669
|
+
create_backup: { type: "boolean", description: "Whether to save a backup copy under .history before writing.", default: false },
|
|
1670
|
+
respect_gitignore: { type: "boolean", description: "Whether to refuse editing paths ignored by .gitignore.", default: true }
|
|
1671
|
+
},
|
|
1672
|
+
required: ["path", "start_line", "end_line", "content"],
|
|
1673
|
+
additionalProperties: false
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
},
|
|
1677
|
+
{
|
|
1678
|
+
type: "function",
|
|
1679
|
+
function: {
|
|
1680
|
+
name: "glob",
|
|
1681
|
+
description: "Find files by glob pattern under a directory. Relative paths are resolved against the workspace root.",
|
|
1682
|
+
parameters: {
|
|
1683
|
+
type: "object",
|
|
1684
|
+
properties: {
|
|
1685
|
+
glob_pattern: { type: "string", description: "Glob pattern such as **/*.tsx or src/**/*.rs." },
|
|
1686
|
+
target_directory: { type: "string", description: "Directory to search from. Defaults to the workspace root." },
|
|
1687
|
+
head_limit: { type: "integer", description: "Maximum number of matching paths to return.", default: 100 },
|
|
1688
|
+
respect_gitignore: { type: "boolean", description: "Whether to skip paths ignored by .gitignore.", default: true }
|
|
1689
|
+
},
|
|
1690
|
+
required: ["glob_pattern"],
|
|
1691
|
+
additionalProperties: false
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
type: "function",
|
|
1697
|
+
function: {
|
|
1698
|
+
name: "grep",
|
|
1699
|
+
description: "Search file contents with a regex pattern. Relative paths are resolved against the workspace root.",
|
|
1700
|
+
parameters: {
|
|
1701
|
+
type: "object",
|
|
1702
|
+
properties: {
|
|
1703
|
+
pattern: { type: "string", description: "Regular expression pattern to search for." },
|
|
1704
|
+
path: { type: "string", description: "File or directory to search. Defaults to the workspace root." },
|
|
1705
|
+
glob: { type: "string", description: "Optional glob filter to limit searched files." },
|
|
1706
|
+
output_mode: { type: "string", enum: ["content", "files_with_matches", "count"], default: "content" },
|
|
1707
|
+
case_insensitive: { type: "boolean", description: "Whether to ignore letter case while matching.", default: false },
|
|
1708
|
+
context_before: { type: "integer", description: "Number of lines to include before each match." },
|
|
1709
|
+
context_after: { type: "integer", description: "Number of lines to include after each match." },
|
|
1710
|
+
context: { type: "integer", description: "Number of lines to include before and after each match." },
|
|
1711
|
+
head_limit: { type: "integer", description: "Maximum number of results to return.", default: 200 },
|
|
1712
|
+
offset: { type: "integer", description: "Number of results to skip in content mode.", default: 0 },
|
|
1713
|
+
multiline: { type: "boolean", description: "Whether . should match newlines.", default: false },
|
|
1714
|
+
respect_gitignore: { type: "boolean", description: "Whether to skip paths ignored by .gitignore.", default: true }
|
|
1715
|
+
},
|
|
1716
|
+
required: ["pattern"],
|
|
1717
|
+
additionalProperties: false
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
},
|
|
1721
|
+
{
|
|
1722
|
+
type: "function",
|
|
1723
|
+
function: {
|
|
1724
|
+
name: "shell",
|
|
1725
|
+
description: "Execute a shell command in the workspace. Use for builds, tests, git, and other CLI tasks.",
|
|
1726
|
+
parameters: {
|
|
1727
|
+
type: "object",
|
|
1728
|
+
properties: {
|
|
1729
|
+
command: { type: "string", description: "The shell command to execute." },
|
|
1730
|
+
description: { type: "string", description: "Short human-readable description for display only." },
|
|
1731
|
+
working_directory: { type: "string", description: "Directory to run the command in, relative to workspace root." },
|
|
1732
|
+
block_until_ms: { type: "integer", description: "Max wait time in ms. Default 30000. Use 0 for background mode.", default: 3e4 }
|
|
1733
|
+
},
|
|
1734
|
+
required: ["command"],
|
|
1735
|
+
additionalProperties: false
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
},
|
|
1739
|
+
{
|
|
1740
|
+
type: "function",
|
|
1741
|
+
function: {
|
|
1742
|
+
name: "await",
|
|
1743
|
+
description: "Poll a background shell started with shell(block_until_ms=0) until it completes or times out.",
|
|
1744
|
+
parameters: {
|
|
1745
|
+
type: "object",
|
|
1746
|
+
properties: {
|
|
1747
|
+
shell_id: { type: "string", description: "The shell_id returned from a background shell invocation." },
|
|
1748
|
+
block_until_ms: { type: "integer", description: "Max wait time in ms before returning current output.", default: 3e4 }
|
|
1749
|
+
},
|
|
1750
|
+
required: ["shell_id"],
|
|
1751
|
+
additionalProperties: false
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
type: "function",
|
|
1757
|
+
function: {
|
|
1758
|
+
name: "list_shells",
|
|
1759
|
+
description: "List background shell processes started by the agent.",
|
|
1760
|
+
parameters: {
|
|
1761
|
+
type: "object",
|
|
1762
|
+
properties: {
|
|
1763
|
+
status_filter: { type: "string", description: 'Filter by status. Default "running". Use "all" to include completed and failed shells.' }
|
|
1764
|
+
},
|
|
1765
|
+
additionalProperties: false
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
},
|
|
1769
|
+
{
|
|
1770
|
+
type: "function",
|
|
1771
|
+
function: {
|
|
1772
|
+
name: "kill_shell",
|
|
1773
|
+
description: "Kill a running background shell process by shell_id.",
|
|
1774
|
+
parameters: {
|
|
1775
|
+
type: "object",
|
|
1776
|
+
properties: {
|
|
1777
|
+
shell_id: { type: "string", description: "The shell_id to terminate." }
|
|
1778
|
+
},
|
|
1779
|
+
required: ["shell_id"],
|
|
1780
|
+
additionalProperties: false
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
},
|
|
1784
|
+
{
|
|
1785
|
+
type: "function",
|
|
1786
|
+
function: {
|
|
1787
|
+
name: "read_shell_logs",
|
|
1788
|
+
description: "Read logs from a shell process in batches.",
|
|
1789
|
+
parameters: {
|
|
1790
|
+
type: "object",
|
|
1791
|
+
properties: {
|
|
1792
|
+
shell_id: { type: "string", description: "The shell_id to read logs from." },
|
|
1793
|
+
stream: { type: "string", enum: ["stdout", "stderr"], default: "stdout" },
|
|
1794
|
+
offset: { type: "integer", description: "Byte offset to start reading from.", default: 0 },
|
|
1795
|
+
limit: { type: "integer", description: "Maximum bytes to return.", default: 4096 }
|
|
1796
|
+
},
|
|
1797
|
+
required: ["shell_id"],
|
|
1798
|
+
additionalProperties: false
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
},
|
|
1802
|
+
{
|
|
1803
|
+
type: "function",
|
|
1804
|
+
function: {
|
|
1805
|
+
name: "web_search",
|
|
1806
|
+
description: "Search the web for real-time information outside training data.",
|
|
1807
|
+
parameters: {
|
|
1808
|
+
type: "object",
|
|
1809
|
+
properties: {
|
|
1810
|
+
search_term: { type: "string", description: "The search term to look up on the web." },
|
|
1811
|
+
max_results: { type: "integer", description: "Maximum number of search results to return.", default: 5 },
|
|
1812
|
+
explanation: { type: "string", description: "One sentence explanation of why this search is being used." }
|
|
1813
|
+
},
|
|
1814
|
+
required: ["search_term"],
|
|
1815
|
+
additionalProperties: false
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
},
|
|
1819
|
+
{
|
|
1820
|
+
type: "function",
|
|
1821
|
+
function: {
|
|
1822
|
+
name: "browse_page",
|
|
1823
|
+
description: "Fetch a public web page and return readable Markdown content.",
|
|
1824
|
+
parameters: {
|
|
1825
|
+
type: "object",
|
|
1826
|
+
properties: {
|
|
1827
|
+
url: { type: "string", description: "The URL to fetch. Must be http or https." },
|
|
1828
|
+
max_lines: { type: "integer", description: "Maximum number of lines to return.", default: 500 },
|
|
1829
|
+
start_line: { type: "integer", description: "First line to read (1-based).", default: 1 },
|
|
1830
|
+
explanation: { type: "string", description: "One sentence explanation of why this page is being fetched." }
|
|
1831
|
+
},
|
|
1832
|
+
required: ["url"],
|
|
1833
|
+
additionalProperties: false
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
},
|
|
1837
|
+
{
|
|
1838
|
+
type: "function",
|
|
1839
|
+
function: {
|
|
1840
|
+
name: "todo_read",
|
|
1841
|
+
description: "Read the current structured todo list for the session.",
|
|
1842
|
+
parameters: {
|
|
1843
|
+
type: "object",
|
|
1844
|
+
properties: {
|
|
1845
|
+
session_id: { type: "string", description: "Optional session ID." }
|
|
1846
|
+
},
|
|
1847
|
+
additionalProperties: false
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
},
|
|
1851
|
+
{
|
|
1852
|
+
type: "function",
|
|
1853
|
+
function: {
|
|
1854
|
+
name: "todo_write",
|
|
1855
|
+
description: "Create and update a structured todo list for the session.",
|
|
1856
|
+
parameters: {
|
|
1857
|
+
type: "object",
|
|
1858
|
+
properties: {
|
|
1859
|
+
todos: {
|
|
1860
|
+
type: "array",
|
|
1861
|
+
items: {
|
|
1862
|
+
type: "object",
|
|
1863
|
+
properties: {
|
|
1864
|
+
id: { type: "string" },
|
|
1865
|
+
content: { type: "string" },
|
|
1866
|
+
status: { type: "string", enum: ["pending", "in_progress", "completed", "cancelled"] }
|
|
1867
|
+
},
|
|
1868
|
+
required: ["id", "content", "status"]
|
|
1869
|
+
}
|
|
1870
|
+
},
|
|
1871
|
+
session_id: { type: "string", description: "Optional session ID." }
|
|
1872
|
+
},
|
|
1873
|
+
required: ["todos"],
|
|
1874
|
+
additionalProperties: false
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
},
|
|
1878
|
+
{
|
|
1879
|
+
type: "function",
|
|
1880
|
+
function: {
|
|
1881
|
+
name: "get_workspace_tree",
|
|
1882
|
+
description: "Display the workspace directory tree structure with depth recursion.",
|
|
1883
|
+
parameters: {
|
|
1884
|
+
type: "object",
|
|
1885
|
+
properties: {
|
|
1886
|
+
max_lines: { type: "integer", description: "Maximum number of lines to return.", default: 500 },
|
|
1887
|
+
start_line: { type: "integer", description: "First line to return (1-based).", default: 1 }
|
|
1888
|
+
},
|
|
1889
|
+
additionalProperties: false
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
},
|
|
1893
|
+
{
|
|
1894
|
+
type: "function",
|
|
1895
|
+
function: {
|
|
1896
|
+
name: "spawn_subagent",
|
|
1897
|
+
description: "Spawn a sub-agent to complete an independent sub-task.",
|
|
1898
|
+
parameters: {
|
|
1899
|
+
type: "object",
|
|
1900
|
+
properties: {
|
|
1901
|
+
task: { type: "string", description: "The task description for the sub-agent." },
|
|
1902
|
+
context: { type: "string", description: "Optional additional context or constraints." },
|
|
1903
|
+
tools: { type: "array", items: { type: "string" }, description: "Optional whitelist of tool names." }
|
|
1904
|
+
},
|
|
1905
|
+
required: ["task"],
|
|
1906
|
+
additionalProperties: false
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
},
|
|
1910
|
+
{
|
|
1911
|
+
type: "function",
|
|
1912
|
+
function: {
|
|
1913
|
+
name: "ask_question",
|
|
1914
|
+
description: "Ask the user a question when you need additional information to proceed.",
|
|
1915
|
+
parameters: {
|
|
1916
|
+
type: "object",
|
|
1917
|
+
properties: {
|
|
1918
|
+
question: { type: "string", description: "The question to ask the user." }
|
|
1919
|
+
},
|
|
1920
|
+
required: ["question"],
|
|
1921
|
+
additionalProperties: false
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
];
|
|
1926
|
+
var HANDLER_MAP = {
|
|
1927
|
+
list_dir: listDirHandler,
|
|
1928
|
+
read_file: readFileHandler,
|
|
1929
|
+
write_file: writeFileHandler,
|
|
1930
|
+
replace_file: replaceFileHandler,
|
|
1931
|
+
edit_file: editFileHandler,
|
|
1932
|
+
replace_lines: replaceLinesHandler,
|
|
1933
|
+
glob: globHandler,
|
|
1934
|
+
grep: grepHandler,
|
|
1935
|
+
shell: shellHandler,
|
|
1936
|
+
await: awaitShellHandler,
|
|
1937
|
+
list_shells: listShellsHandler,
|
|
1938
|
+
kill_shell: killShellHandler,
|
|
1939
|
+
read_shell_logs: readShellLogsHandler,
|
|
1940
|
+
web_search: webSearchHandler,
|
|
1941
|
+
browse_page: browsePageHandler,
|
|
1942
|
+
todo_read: todoReadHandler,
|
|
1943
|
+
todo_write: todoWriteHandler,
|
|
1944
|
+
get_workspace_tree: getWorkspaceTreeHandler,
|
|
1945
|
+
spawn_subagent: spawnSubAgentHandler,
|
|
1946
|
+
ask_question: askQuestionHandler,
|
|
1947
|
+
plan_create: planCreateHandler,
|
|
1948
|
+
plan_read: planReadHandler,
|
|
1949
|
+
plan_update: planUpdateHandler,
|
|
1950
|
+
plan_edit: planEditHandler,
|
|
1951
|
+
plan_delete: planDeleteHandler,
|
|
1952
|
+
plan_list: planListHandler
|
|
1953
|
+
};
|
|
1954
|
+
var ASK_QUESTION_TOOL_NAME = "ask_question";
|
|
1955
|
+
var AGENT_MODE_EXCLUDED_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
1956
|
+
ASK_QUESTION_TOOL_NAME
|
|
1957
|
+
]);
|
|
1958
|
+
var ASK_MODE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
1959
|
+
"list_dir",
|
|
1960
|
+
"read_file",
|
|
1961
|
+
"glob",
|
|
1962
|
+
"grep",
|
|
1963
|
+
"web_search",
|
|
1964
|
+
"browse_page",
|
|
1965
|
+
"todo_read",
|
|
1966
|
+
"list_shells",
|
|
1967
|
+
"read_shell_logs",
|
|
1968
|
+
"get_workspace_tree"
|
|
1969
|
+
]);
|
|
1970
|
+
function getToolDefinitions(agentMode) {
|
|
1971
|
+
if (agentMode === "ask") {
|
|
1972
|
+
return AGENT_TOOL_DEFINITIONS.filter(
|
|
1973
|
+
(t) => ASK_MODE_TOOL_NAMES.has(t.function.name)
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
if (agentMode === "plan") {
|
|
1977
|
+
return AGENT_TOOL_DEFINITIONS;
|
|
1978
|
+
}
|
|
1979
|
+
return AGENT_TOOL_DEFINITIONS.filter(
|
|
1980
|
+
(t) => !AGENT_MODE_EXCLUDED_TOOL_NAMES.has(t.function.name)
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
function getToolHandler(name) {
|
|
1984
|
+
return HANDLER_MAP[name];
|
|
1985
|
+
}
|
|
1986
|
+
async function executeToolCall(name, rawArguments, context) {
|
|
1987
|
+
const handler = getToolHandler(name);
|
|
1988
|
+
if (!handler) {
|
|
1989
|
+
return {
|
|
1990
|
+
ok: false,
|
|
1991
|
+
tool: name,
|
|
1992
|
+
error: {
|
|
1993
|
+
code: "unknown_tool",
|
|
1994
|
+
message: `Unknown tool: ${name}`
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
let parsedArgs;
|
|
1999
|
+
try {
|
|
2000
|
+
parsedArgs = rawArguments.trim() ? JSON.parse(rawArguments) : {};
|
|
2001
|
+
} catch {
|
|
2002
|
+
return {
|
|
2003
|
+
ok: false,
|
|
2004
|
+
tool: name,
|
|
2005
|
+
error: {
|
|
2006
|
+
code: "invalid_arguments",
|
|
2007
|
+
message: "Tool arguments must be valid JSON"
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
return handler(parsedArgs, context);
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// src/agent/llm-stream.ts
|
|
2015
|
+
function chatCompletionsUrl(baseUrl) {
|
|
2016
|
+
const trimmed = baseUrl.trim().replace(/\/+$/, "");
|
|
2017
|
+
if (trimmed.endsWith("/v1")) {
|
|
2018
|
+
return `${trimmed}/chat/completions`;
|
|
2019
|
+
}
|
|
2020
|
+
return `${trimmed}/v1/chat/completions`;
|
|
2021
|
+
}
|
|
2022
|
+
async function startLLMStream(options, callbacks) {
|
|
2023
|
+
const url = chatCompletionsUrl(options.baseUrl);
|
|
2024
|
+
const body = {
|
|
2025
|
+
model: options.model,
|
|
2026
|
+
messages: options.messages.map(formatMessage),
|
|
2027
|
+
stream: true,
|
|
2028
|
+
stream_options: { include_usage: true }
|
|
2029
|
+
};
|
|
2030
|
+
if (options.tools && options.tools.length > 0) {
|
|
2031
|
+
body.tools = options.tools;
|
|
2032
|
+
body.tool_choice = "auto";
|
|
2033
|
+
}
|
|
2034
|
+
try {
|
|
2035
|
+
const response = await fetch(url, {
|
|
2036
|
+
method: "POST",
|
|
2037
|
+
headers: {
|
|
2038
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
2039
|
+
"Content-Type": "application/json",
|
|
2040
|
+
Accept: "text/event-stream"
|
|
2041
|
+
},
|
|
2042
|
+
body: JSON.stringify(body)
|
|
2043
|
+
});
|
|
2044
|
+
if (!response.ok) {
|
|
2045
|
+
const errorBody = await response.text().catch(() => "");
|
|
2046
|
+
throw new Error(`API error (${response.status}): ${errorBody || response.statusText}`);
|
|
2047
|
+
}
|
|
2048
|
+
if (!response.body) {
|
|
2049
|
+
throw new Error("Response body is empty");
|
|
2050
|
+
}
|
|
2051
|
+
const reader = response.body.getReader();
|
|
2052
|
+
const decoder = new TextDecoder();
|
|
2053
|
+
let buffer = "";
|
|
2054
|
+
const toolCallAccumulator = new ToolCallAccumulator({
|
|
2055
|
+
onIdentified: (id, name) => {
|
|
2056
|
+
}
|
|
2057
|
+
});
|
|
2058
|
+
let hasToolCalls = false;
|
|
2059
|
+
while (true) {
|
|
2060
|
+
const { done, value } = await reader.read();
|
|
2061
|
+
if (done) break;
|
|
2062
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2063
|
+
while (true) {
|
|
2064
|
+
const lineBreak = buffer.indexOf("\n");
|
|
2065
|
+
if (lineBreak === -1) break;
|
|
2066
|
+
const line = buffer.slice(0, lineBreak).trim();
|
|
2067
|
+
buffer = buffer.slice(lineBreak + 1);
|
|
2068
|
+
processLine(line, callbacks, toolCallAccumulator);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
if (buffer.trim()) {
|
|
2072
|
+
processLine(buffer.trim(), callbacks, toolCallAccumulator);
|
|
2073
|
+
}
|
|
2074
|
+
const finalToolCalls = toolCallAccumulator.finalize();
|
|
2075
|
+
for (const call of finalToolCalls) {
|
|
2076
|
+
callbacks.onToolCall(call.id, call.name, call.arguments);
|
|
2077
|
+
}
|
|
2078
|
+
callbacks.onDone();
|
|
2079
|
+
} catch (error2) {
|
|
2080
|
+
if (error2 instanceof Error) {
|
|
2081
|
+
callbacks.onError(error2);
|
|
2082
|
+
} else {
|
|
2083
|
+
callbacks.onError(new Error(String(error2)));
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
function processLine(line, callbacks, toolCalls) {
|
|
2088
|
+
if (!line || line.startsWith(":")) return;
|
|
2089
|
+
const payload = line.startsWith("data:") ? line.slice(5).trim() : line;
|
|
2090
|
+
if (payload === "[DONE]") return;
|
|
2091
|
+
let parsed;
|
|
2092
|
+
try {
|
|
2093
|
+
parsed = JSON.parse(payload);
|
|
2094
|
+
} catch {
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
const choices = parsed.choices;
|
|
2098
|
+
const choice = choices?.[0];
|
|
2099
|
+
const delta = choice?.delta;
|
|
2100
|
+
if (delta) {
|
|
2101
|
+
if (delta.reasoning_content) {
|
|
2102
|
+
callbacks.onReasoning(delta.reasoning_content);
|
|
2103
|
+
}
|
|
2104
|
+
if (delta.content) {
|
|
2105
|
+
callbacks.onContent(delta.content);
|
|
2106
|
+
}
|
|
2107
|
+
const toolCallDeltas = delta.tool_calls;
|
|
2108
|
+
if (toolCallDeltas) {
|
|
2109
|
+
for (const tcDelta of toolCallDeltas) {
|
|
2110
|
+
const index = tcDelta.index;
|
|
2111
|
+
const fn = tcDelta.function;
|
|
2112
|
+
const id = tcDelta.id;
|
|
2113
|
+
if (id) {
|
|
2114
|
+
toolCalls.startToolCall(index, id, fn?.name);
|
|
2115
|
+
} else if (fn?.arguments) {
|
|
2116
|
+
toolCalls.appendArguments(index, fn.arguments);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
const usage = parsed.usage;
|
|
2122
|
+
if (usage?.prompt_tokens !== void 0) {
|
|
2123
|
+
callbacks.onUsage({
|
|
2124
|
+
promptTokens: usage.prompt_tokens,
|
|
2125
|
+
completionTokens: usage.completion_tokens,
|
|
2126
|
+
totalTokens: usage.total_tokens
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
var ToolCallAccumulator = class {
|
|
2131
|
+
pending = /* @__PURE__ */ new Map();
|
|
2132
|
+
finalized = false;
|
|
2133
|
+
onIdentified;
|
|
2134
|
+
constructor(opts) {
|
|
2135
|
+
this.onIdentified = opts.onIdentified;
|
|
2136
|
+
}
|
|
2137
|
+
startToolCall(index, id, name) {
|
|
2138
|
+
this.pending.set(index, { index, id, name, arguments: "" });
|
|
2139
|
+
}
|
|
2140
|
+
appendArguments(index, args) {
|
|
2141
|
+
const existing = this.pending.get(index);
|
|
2142
|
+
if (existing) {
|
|
2143
|
+
existing.arguments += args;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
finalize() {
|
|
2147
|
+
if (this.finalized) return [];
|
|
2148
|
+
this.finalized = true;
|
|
2149
|
+
const calls = Array.from(this.pending.values()).sort((a, b) => a.index - b.index).map((c) => ({
|
|
2150
|
+
id: c.id,
|
|
2151
|
+
name: c.name,
|
|
2152
|
+
arguments: c.arguments
|
|
2153
|
+
}));
|
|
2154
|
+
return calls;
|
|
2155
|
+
}
|
|
2156
|
+
};
|
|
2157
|
+
function formatMessage(msg) {
|
|
2158
|
+
const formatted = {
|
|
2159
|
+
role: msg.role
|
|
2160
|
+
};
|
|
2161
|
+
if (msg.content !== void 0) {
|
|
2162
|
+
formatted.content = msg.content;
|
|
2163
|
+
}
|
|
2164
|
+
if (msg.reasoning_content) {
|
|
2165
|
+
formatted.reasoning_content = msg.reasoning_content;
|
|
2166
|
+
}
|
|
2167
|
+
if (msg.tool_calls) {
|
|
2168
|
+
formatted.tool_calls = msg.tool_calls;
|
|
2169
|
+
}
|
|
2170
|
+
if (msg.tool_call_id) {
|
|
2171
|
+
formatted.tool_call_id = msg.tool_call_id;
|
|
2172
|
+
}
|
|
2173
|
+
if (msg.name) {
|
|
2174
|
+
formatted.name = msg.name;
|
|
2175
|
+
}
|
|
2176
|
+
return formatted;
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
// src/agent/runner.ts
|
|
2180
|
+
async function runAgentWithTools(input, toolContext, onEvent) {
|
|
2181
|
+
let messages = [...input.messages];
|
|
2182
|
+
let cumulativeUsage;
|
|
2183
|
+
while (true) {
|
|
2184
|
+
const turn = await runSingleAgentTurn(input, messages, onEvent);
|
|
2185
|
+
if (turn.usage) {
|
|
2186
|
+
cumulativeUsage = cumulativeUsage ? {
|
|
2187
|
+
promptTokens: cumulativeUsage.promptTokens + turn.usage.promptTokens,
|
|
2188
|
+
completionTokens: cumulativeUsage.completionTokens + turn.usage.completionTokens,
|
|
2189
|
+
totalTokens: cumulativeUsage.totalTokens + turn.usage.totalTokens
|
|
2190
|
+
} : turn.usage;
|
|
2191
|
+
}
|
|
2192
|
+
if (turn.toolCalls.length === 0) {
|
|
2193
|
+
if (turn.content || turn.reasoningContent) {
|
|
2194
|
+
messages = [
|
|
2195
|
+
...messages,
|
|
2196
|
+
{
|
|
2197
|
+
role: "assistant",
|
|
2198
|
+
content: turn.content || void 0,
|
|
2199
|
+
reasoning_content: turn.reasoningContent || void 0
|
|
2200
|
+
}
|
|
2201
|
+
];
|
|
2202
|
+
}
|
|
2203
|
+
onEvent({ type: "done", taskId: input.taskId, usage: cumulativeUsage ?? turn.usage });
|
|
2204
|
+
onEvent({ type: "status", taskId: input.taskId, status: "completed" });
|
|
2205
|
+
return messages;
|
|
2206
|
+
}
|
|
2207
|
+
messages = await appendToolResults(messages, turn, toolContext, onEvent);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
async function runSingleAgentTurn(input, messages, onEvent) {
|
|
2211
|
+
return new Promise((resolve12, reject) => {
|
|
2212
|
+
let content = "";
|
|
2213
|
+
let reasoningContent = "";
|
|
2214
|
+
const toolCalls = [];
|
|
2215
|
+
let turnUsage;
|
|
2216
|
+
startLLMStream(
|
|
2217
|
+
{
|
|
2218
|
+
baseUrl: input.baseUrl,
|
|
2219
|
+
apiKey: input.apiKey,
|
|
2220
|
+
model: input.model,
|
|
2221
|
+
messages,
|
|
2222
|
+
tools: getToolDefinitions(input.agentMode)
|
|
2223
|
+
},
|
|
2224
|
+
{
|
|
2225
|
+
onContent: (delta) => {
|
|
2226
|
+
content += delta;
|
|
2227
|
+
onEvent({ type: "content_delta", taskId: input.taskId, delta });
|
|
2228
|
+
},
|
|
2229
|
+
onReasoning: (delta) => {
|
|
2230
|
+
reasoningContent += delta;
|
|
2231
|
+
onEvent({ type: "thinking_delta", taskId: input.taskId, delta });
|
|
2232
|
+
},
|
|
2233
|
+
onToolCall: (id, name, args) => {
|
|
2234
|
+
toolCalls.push({ id, name, arguments: args });
|
|
2235
|
+
onEvent({ type: "tool_call_pending", taskId: input.taskId, toolCallId: id, name });
|
|
2236
|
+
},
|
|
2237
|
+
onUsage: (usage) => {
|
|
2238
|
+
turnUsage = usage;
|
|
2239
|
+
},
|
|
2240
|
+
onDone: () => {
|
|
2241
|
+
if (toolCalls.length > 0) {
|
|
2242
|
+
for (const call of toolCalls) {
|
|
2243
|
+
let parsedInput;
|
|
2244
|
+
try {
|
|
2245
|
+
parsedInput = call.arguments.trim() ? JSON.parse(call.arguments) : {};
|
|
2246
|
+
} catch {
|
|
2247
|
+
parsedInput = {};
|
|
2248
|
+
}
|
|
2249
|
+
onEvent({
|
|
2250
|
+
type: "tool_call_started",
|
|
2251
|
+
taskId: input.taskId,
|
|
2252
|
+
toolCallId: call.id,
|
|
2253
|
+
name: call.name,
|
|
2254
|
+
input: parsedInput
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
resolve12({ toolCalls, content, reasoningContent, usage: turnUsage });
|
|
2259
|
+
},
|
|
2260
|
+
onError: (error2) => {
|
|
2261
|
+
reject(error2);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
);
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
async function appendToolResults(messages, turn, context, onEvent) {
|
|
2268
|
+
const assistantMessage = {
|
|
2269
|
+
role: "assistant",
|
|
2270
|
+
content: turn.content || void 0,
|
|
2271
|
+
reasoning_content: turn.reasoningContent || void 0,
|
|
2272
|
+
tool_calls: turn.toolCalls.map((tc) => ({
|
|
2273
|
+
id: tc.id,
|
|
2274
|
+
type: "function",
|
|
2275
|
+
function: { name: tc.name, arguments: tc.arguments }
|
|
2276
|
+
}))
|
|
2277
|
+
};
|
|
2278
|
+
const nextMessages = [...messages, assistantMessage];
|
|
2279
|
+
const results = await Promise.all(
|
|
2280
|
+
turn.toolCalls.map(async (call) => {
|
|
2281
|
+
try {
|
|
2282
|
+
const result = await executeToolCall(call.name, call.arguments, context);
|
|
2283
|
+
if (result.ok) {
|
|
2284
|
+
onEvent({
|
|
2285
|
+
type: "tool_call_finished",
|
|
2286
|
+
taskId: context.taskId ?? "",
|
|
2287
|
+
toolCallId: call.id,
|
|
2288
|
+
output: result.data
|
|
2289
|
+
});
|
|
2290
|
+
} else {
|
|
2291
|
+
onEvent({
|
|
2292
|
+
type: "tool_call_finished",
|
|
2293
|
+
taskId: context.taskId ?? "",
|
|
2294
|
+
toolCallId: call.id,
|
|
2295
|
+
errorText: result.error?.message
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
return { id: call.id, result };
|
|
2299
|
+
} catch (error2) {
|
|
2300
|
+
const errorText = error2 instanceof Error ? error2.message : String(error2);
|
|
2301
|
+
onEvent({
|
|
2302
|
+
type: "tool_call_finished",
|
|
2303
|
+
taskId: context.taskId ?? "",
|
|
2304
|
+
toolCallId: call.id,
|
|
2305
|
+
errorText
|
|
2306
|
+
});
|
|
2307
|
+
return {
|
|
2308
|
+
id: call.id,
|
|
2309
|
+
result: {
|
|
2310
|
+
ok: false,
|
|
2311
|
+
tool: call.name,
|
|
2312
|
+
error: { code: "execution_error", message: errorText }
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
})
|
|
2317
|
+
);
|
|
2318
|
+
for (const { id, result } of results) {
|
|
2319
|
+
const content = result.ok ? JSON.stringify(result.data, null, 2) : JSON.stringify({ error: result.error }, null, 2);
|
|
2320
|
+
nextMessages.push({
|
|
2321
|
+
role: "tool",
|
|
2322
|
+
tool_call_id: id,
|
|
2323
|
+
content
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
return nextMessages;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
// src/agent/environment/index.ts
|
|
2330
|
+
var import_node_os2 = require("node:os");
|
|
2331
|
+
var import_node_child_process2 = require("node:child_process");
|
|
2332
|
+
var import_node_fs13 = require("node:fs");
|
|
2333
|
+
var import_node_path14 = require("node:path");
|
|
2334
|
+
function resolveAgentEnvironment(workspaceDir) {
|
|
2335
|
+
const os = `${(0, import_node_os2.platform)()} (${(0, import_node_os2.release)()})`;
|
|
2336
|
+
const shell = resolveShell();
|
|
2337
|
+
const isGitRepository = workspaceDir ? checkIsGitRepository(workspaceDir) : false;
|
|
2338
|
+
const today = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
|
|
2339
|
+
weekday: "long",
|
|
2340
|
+
year: "numeric",
|
|
2341
|
+
month: "long",
|
|
2342
|
+
day: "numeric",
|
|
2343
|
+
timeZone: "UTC"
|
|
2344
|
+
});
|
|
2345
|
+
let agentsMd = null;
|
|
2346
|
+
if (workspaceDir) {
|
|
2347
|
+
const agentsMdPath = (0, import_node_path14.resolve)(workspaceDir, "AGENTS.md");
|
|
2348
|
+
if ((0, import_node_fs13.existsSync)(agentsMdPath)) {
|
|
2349
|
+
const content = (0, import_node_fs13.readFileSync)(agentsMdPath, "utf-8");
|
|
2350
|
+
agentsMd = {
|
|
2351
|
+
path: agentsMdPath,
|
|
2352
|
+
content: content.length > 4e3 ? content.slice(0, 4e3) + "\n... [truncated]" : content,
|
|
2353
|
+
truncated: content.length > 4e3
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
return {
|
|
2358
|
+
workspaceDir,
|
|
2359
|
+
os,
|
|
2360
|
+
shell,
|
|
2361
|
+
isGitRepository,
|
|
2362
|
+
today,
|
|
2363
|
+
agentsMd,
|
|
2364
|
+
enabledSystemSkills: []
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
function resolveShell() {
|
|
2368
|
+
const p = (0, import_node_os2.platform)();
|
|
2369
|
+
if (p === "win32") {
|
|
2370
|
+
return process.env.COMSPEC || "cmd.exe";
|
|
2371
|
+
}
|
|
2372
|
+
return process.env.SHELL || "/bin/sh";
|
|
2373
|
+
}
|
|
2374
|
+
function checkIsGitRepository(dir) {
|
|
2375
|
+
try {
|
|
2376
|
+
(0, import_node_child_process2.execSync)("git rev-parse --is-inside-work-tree", {
|
|
2377
|
+
cwd: dir,
|
|
2378
|
+
stdio: "pipe",
|
|
2379
|
+
timeout: 3e3
|
|
2380
|
+
});
|
|
2381
|
+
return true;
|
|
2382
|
+
} catch {
|
|
2383
|
+
return false;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
function buildSystemPrompt(env) {
|
|
2387
|
+
const workspaceLine = env.workspaceDir ?? "not selected";
|
|
2388
|
+
const gitLine = env.isGitRepository ? "yes" : env.workspaceDir ? "no" : "unknown";
|
|
2389
|
+
const lines = [
|
|
2390
|
+
"You are Coder, a helpful terminal AI assistant.",
|
|
2391
|
+
"",
|
|
2392
|
+
"## Environment",
|
|
2393
|
+
`- Workspace: ${workspaceLine}`,
|
|
2394
|
+
`- OS: ${env.os}`,
|
|
2395
|
+
`- Shell: ${env.shell}`,
|
|
2396
|
+
`- Git repository: ${gitLine}`,
|
|
2397
|
+
`- Date: ${env.today}`,
|
|
2398
|
+
"",
|
|
2399
|
+
"## Available Tools",
|
|
2400
|
+
"You have access to all standard tools: file operations, shell commands, web search, skills, and more.",
|
|
2401
|
+
"",
|
|
2402
|
+
"## Guidelines",
|
|
2403
|
+
"- Always read files before editing them.",
|
|
2404
|
+
"- Prefer targeted edits over full file replacements.",
|
|
2405
|
+
"- Use shell commands only for builds, tests, git, and non-interactive CLI tasks.",
|
|
2406
|
+
"- When a task requires multiple steps, create a plan first using todo_write.",
|
|
2407
|
+
"- After completing a task, summarize what was done.",
|
|
2408
|
+
"- Use ask_question when you need clarification from the user.",
|
|
2409
|
+
""
|
|
2410
|
+
];
|
|
2411
|
+
if (env.agentsMd) {
|
|
2412
|
+
lines.push("## Project Instructions (AGENTS.md)");
|
|
2413
|
+
lines.push("");
|
|
2414
|
+
lines.push(env.agentsMd.content);
|
|
2415
|
+
lines.push("");
|
|
2416
|
+
}
|
|
2417
|
+
return lines.join("\n");
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
// src/agent/session.ts
|
|
2421
|
+
init_config();
|
|
2422
|
+
|
|
2423
|
+
// src/ui/index.ts
|
|
2424
|
+
var import_node_tty = require("node:tty");
|
|
2425
|
+
var stdoutIsTTY = (0, import_node_tty.isatty)(process.stdout.fd);
|
|
2426
|
+
var stderrIsTTY = (0, import_node_tty.isatty)(process.stderr.fd);
|
|
2427
|
+
var colors = {
|
|
2428
|
+
reset: "\x1B[0m",
|
|
2429
|
+
bold: "\x1B[1m",
|
|
2430
|
+
dim: "\x1B[2m",
|
|
2431
|
+
italic: "\x1B[3m",
|
|
2432
|
+
red: "\x1B[31m",
|
|
2433
|
+
green: "\x1B[32m",
|
|
2434
|
+
yellow: "\x1B[33m",
|
|
2435
|
+
blue: "\x1B[34m",
|
|
2436
|
+
magenta: "\x1B[35m",
|
|
2437
|
+
cyan: "\x1B[36m",
|
|
2438
|
+
gray: "\x1B[90m"
|
|
2439
|
+
};
|
|
2440
|
+
function colorize(text, color) {
|
|
2441
|
+
if (!stdoutIsTTY) return text;
|
|
2442
|
+
return `${colors[color]}${text}${colors.reset}`;
|
|
2443
|
+
}
|
|
2444
|
+
function dim(text) {
|
|
2445
|
+
return colorize(text, "dim");
|
|
2446
|
+
}
|
|
2447
|
+
function bold(text) {
|
|
2448
|
+
if (!stdoutIsTTY) return text;
|
|
2449
|
+
return `${colors.bold}${text}${colors.reset}`;
|
|
2450
|
+
}
|
|
2451
|
+
function error(text) {
|
|
2452
|
+
return colorize(text, "red");
|
|
2453
|
+
}
|
|
2454
|
+
function success(text) {
|
|
2455
|
+
return colorize(text, "green");
|
|
2456
|
+
}
|
|
2457
|
+
function warning(text) {
|
|
2458
|
+
return colorize(text, "yellow");
|
|
2459
|
+
}
|
|
2460
|
+
function info(text) {
|
|
2461
|
+
return colorize(text, "cyan");
|
|
2462
|
+
}
|
|
2463
|
+
function writeStream(text) {
|
|
2464
|
+
process.stdout.write(text);
|
|
2465
|
+
}
|
|
2466
|
+
function writeLine(text) {
|
|
2467
|
+
process.stdout.write(text + "\n");
|
|
2468
|
+
}
|
|
2469
|
+
function writeError(text) {
|
|
2470
|
+
process.stderr.write(text + "\n");
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
// src/agent/session.ts
|
|
2474
|
+
async function runAgentSession(prompt, options) {
|
|
2475
|
+
const config = loadConfig();
|
|
2476
|
+
const workspaceDir = options.workspaceDir || null;
|
|
2477
|
+
const providerId = options.provider ?? config.activeProvider;
|
|
2478
|
+
const resolvedConfig = resolveProviderConfig(config, providerId);
|
|
2479
|
+
const apiKey = resolveApiKey(resolvedConfig);
|
|
2480
|
+
const modelId = options.model ?? config.lastModel;
|
|
2481
|
+
let messages;
|
|
2482
|
+
if (options.existingMessages && options.existingMessages.length > 0) {
|
|
2483
|
+
messages = [...options.existingMessages, { role: "user", content: prompt }];
|
|
2484
|
+
} else {
|
|
2485
|
+
const env = resolveAgentEnvironment(workspaceDir);
|
|
2486
|
+
const systemPrompt = buildSystemPrompt(env);
|
|
2487
|
+
messages = [
|
|
2488
|
+
{ role: "system", content: systemPrompt },
|
|
2489
|
+
{ role: "user", content: prompt }
|
|
2490
|
+
];
|
|
2491
|
+
}
|
|
2492
|
+
const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2493
|
+
const isStreaming = options.stream !== false;
|
|
2494
|
+
let currentContent = "";
|
|
2495
|
+
let currentThinking = "";
|
|
2496
|
+
let hasContent = false;
|
|
2497
|
+
const toolContext = {
|
|
2498
|
+
workspaceDir,
|
|
2499
|
+
sessionId: taskId,
|
|
2500
|
+
taskId,
|
|
2501
|
+
agentMode: options.agentMode
|
|
2502
|
+
};
|
|
2503
|
+
if (!isStreaming) {
|
|
2504
|
+
writeLine(`${info("\u2139")} Running agent in ${bold(options.agentMode)} mode...
|
|
2505
|
+
`);
|
|
2506
|
+
}
|
|
2507
|
+
try {
|
|
2508
|
+
const finalMessages = await runAgentWithTools(
|
|
2509
|
+
{
|
|
2510
|
+
taskId,
|
|
2511
|
+
baseUrl: resolvedConfig.baseUrl,
|
|
2512
|
+
apiKey,
|
|
2513
|
+
apiKeySource: resolvedConfig.apiKeySource,
|
|
2514
|
+
apiKeyEnvVar: resolvedConfig.apiKeyEnvVar,
|
|
2515
|
+
model: modelId,
|
|
2516
|
+
messages,
|
|
2517
|
+
agentMode: options.agentMode
|
|
2518
|
+
},
|
|
2519
|
+
toolContext,
|
|
2520
|
+
(event) => {
|
|
2521
|
+
switch (event.type) {
|
|
2522
|
+
case "thinking_delta": {
|
|
2523
|
+
currentThinking += event.delta;
|
|
2524
|
+
if (isStreaming && !hasContent) {
|
|
2525
|
+
writeStream(dim(event.delta));
|
|
2526
|
+
}
|
|
2527
|
+
break;
|
|
2528
|
+
}
|
|
2529
|
+
case "content_delta": {
|
|
2530
|
+
if (!hasContent && currentThinking && isStreaming) {
|
|
2531
|
+
writeLine("");
|
|
2532
|
+
hasContent = true;
|
|
2533
|
+
}
|
|
2534
|
+
hasContent = true;
|
|
2535
|
+
currentContent += event.delta;
|
|
2536
|
+
if (isStreaming) {
|
|
2537
|
+
writeStream(event.delta);
|
|
2538
|
+
}
|
|
2539
|
+
break;
|
|
2540
|
+
}
|
|
2541
|
+
case "tool_call_started": {
|
|
2542
|
+
if (isStreaming) {
|
|
2543
|
+
if (hasContent || currentThinking) {
|
|
2544
|
+
writeLine("");
|
|
2545
|
+
}
|
|
2546
|
+
writeLine(`${dim("\u{1F527}")} ${bold(event.name)}${dim("...")}`);
|
|
2547
|
+
}
|
|
2548
|
+
break;
|
|
2549
|
+
}
|
|
2550
|
+
case "tool_call_finished": {
|
|
2551
|
+
if (event.errorText) {
|
|
2552
|
+
writeLine(` ${error("\u2717")} ${dim(event.errorText.slice(0, 200))}`);
|
|
2553
|
+
} else if (isStreaming) {
|
|
2554
|
+
writeLine(` ${success("\u2713")} ${dim("done")}`);
|
|
2555
|
+
}
|
|
2556
|
+
break;
|
|
2557
|
+
}
|
|
2558
|
+
case "status": {
|
|
2559
|
+
if (event.status === "completed") {
|
|
2560
|
+
if (isStreaming) {
|
|
2561
|
+
writeLine("");
|
|
2562
|
+
writeLine(success(`
|
|
2563
|
+
\u2713 Task completed`));
|
|
2564
|
+
} else {
|
|
2565
|
+
writeLine(success(`\u2713 Task completed`));
|
|
2566
|
+
if (currentContent) {
|
|
2567
|
+
writeLine("\n" + currentContent);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
} else if (event.status === "failed") {
|
|
2571
|
+
writeLine(error(`
|
|
2572
|
+
\u2717 Task failed`));
|
|
2573
|
+
} else if (event.status === "cancelled") {
|
|
2574
|
+
writeLine(warning(`
|
|
2575
|
+
\u26A0 Task cancelled`));
|
|
2576
|
+
}
|
|
2577
|
+
break;
|
|
2578
|
+
}
|
|
2579
|
+
case "done": {
|
|
2580
|
+
if (event.usage && config.showUsage) {
|
|
2581
|
+
writeLine(dim(
|
|
2582
|
+
` Tokens: ${event.usage.promptTokens}\u2191 ${event.usage.completionTokens}\u2193 ${event.usage.totalTokens}\u2211`
|
|
2583
|
+
));
|
|
2584
|
+
}
|
|
2585
|
+
break;
|
|
2586
|
+
}
|
|
2587
|
+
case "error": {
|
|
2588
|
+
writeLine(error(`
|
|
2589
|
+
\u2717 Error: ${event.message}`));
|
|
2590
|
+
break;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
);
|
|
2595
|
+
return finalMessages;
|
|
2596
|
+
} catch (err) {
|
|
2597
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2598
|
+
writeError(error(`Fatal error: ${message}`));
|
|
2599
|
+
process.exit(1);
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
// src/commands/run.ts
|
|
2604
|
+
async function runCommand(prompt, options) {
|
|
2605
|
+
await runAgentSession(prompt, {
|
|
2606
|
+
...options,
|
|
2607
|
+
agentMode: "agent",
|
|
2608
|
+
workspaceDir: options.workspace ?? process.cwd(),
|
|
2609
|
+
interactive: false
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// src/commands/ask.ts
|
|
2614
|
+
async function askCommand(prompt, options) {
|
|
2615
|
+
await runAgentSession(prompt, {
|
|
2616
|
+
...options,
|
|
2617
|
+
agentMode: "ask",
|
|
2618
|
+
workspaceDir: options.workspace ?? process.cwd(),
|
|
2619
|
+
interactive: false
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
// src/commands/plan.ts
|
|
2624
|
+
async function planCommand(prompt, options) {
|
|
2625
|
+
await runAgentSession(prompt, {
|
|
2626
|
+
...options,
|
|
2627
|
+
agentMode: "plan",
|
|
2628
|
+
workspaceDir: options.workspace ?? process.cwd(),
|
|
2629
|
+
interactive: false
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// src/commands/repl.ts
|
|
2634
|
+
var import_node_readline2 = require("node:readline");
|
|
2635
|
+
init_config();
|
|
2636
|
+
async function replCommand(options) {
|
|
2637
|
+
const config = loadConfig();
|
|
2638
|
+
const workspaceDir = options.workspace ?? process.cwd();
|
|
2639
|
+
writeLine("");
|
|
2640
|
+
writeLine(bold("Coder CLI \u2014 Interactive REPL"));
|
|
2641
|
+
writeLine(dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2642
|
+
writeLine(dim(` Model: ${config.lastModel || "not set"}`));
|
|
2643
|
+
writeLine(dim(` Provider: ${config.activeProvider}`));
|
|
2644
|
+
writeLine(dim(` Workspace: ${workspaceDir}`));
|
|
2645
|
+
writeLine(dim(` Type 'exit' or Ctrl+C to quit`));
|
|
2646
|
+
writeLine(dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2647
|
+
writeLine("");
|
|
2648
|
+
let conversationMessages;
|
|
2649
|
+
const rl = (0, import_node_readline2.createInterface)({
|
|
2650
|
+
input: process.stdin,
|
|
2651
|
+
output: process.stdout,
|
|
2652
|
+
prompt: `${bold("coder")}> `,
|
|
2653
|
+
terminal: true
|
|
2654
|
+
});
|
|
2655
|
+
rl.prompt();
|
|
2656
|
+
rl.on("line", async (line) => {
|
|
2657
|
+
const trimmed = line.trim();
|
|
2658
|
+
if (!trimmed) {
|
|
2659
|
+
rl.prompt();
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
if (trimmed === "exit" || trimmed === "quit") {
|
|
2663
|
+
rl.close();
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
if (trimmed === "clear") {
|
|
2667
|
+
console.clear();
|
|
2668
|
+
rl.prompt();
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
if (trimmed === "help") {
|
|
2672
|
+
writeLine(bold("\nREPL Commands:"));
|
|
2673
|
+
writeLine(" <prompt> Ask the agent anything");
|
|
2674
|
+
writeLine(" exit / quit Exit REPL");
|
|
2675
|
+
writeLine(" clear Clear screen");
|
|
2676
|
+
writeLine(" help Show this help");
|
|
2677
|
+
writeLine(" model <id> Switch model");
|
|
2678
|
+
writeLine("");
|
|
2679
|
+
rl.prompt();
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
if (trimmed.startsWith("model ")) {
|
|
2683
|
+
const modelId = trimmed.slice(6).trim();
|
|
2684
|
+
if (modelId) {
|
|
2685
|
+
config.lastModel = modelId;
|
|
2686
|
+
const { saveConfig: saveConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2687
|
+
saveConfig2(config);
|
|
2688
|
+
writeLine(info(`Model set to: ${modelId}`));
|
|
2689
|
+
}
|
|
2690
|
+
rl.prompt();
|
|
2691
|
+
return;
|
|
2692
|
+
}
|
|
2693
|
+
rl.pause();
|
|
2694
|
+
try {
|
|
2695
|
+
conversationMessages = await runAgentSession(trimmed, {
|
|
2696
|
+
agentMode: "agent",
|
|
2697
|
+
workspaceDir,
|
|
2698
|
+
interactive: true,
|
|
2699
|
+
model: options.model,
|
|
2700
|
+
provider: options.provider,
|
|
2701
|
+
existingMessages: conversationMessages
|
|
2702
|
+
});
|
|
2703
|
+
} catch (err) {
|
|
2704
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2705
|
+
writeError(error(`Error: ${message}`));
|
|
2706
|
+
}
|
|
2707
|
+
writeLine("");
|
|
2708
|
+
rl.prompt();
|
|
2709
|
+
rl.resume();
|
|
2710
|
+
});
|
|
2711
|
+
rl.on("close", () => {
|
|
2712
|
+
writeLine(dim("\nGoodbye! \u{1F44B}"));
|
|
2713
|
+
process.exit(0);
|
|
2714
|
+
});
|
|
2715
|
+
rl.on("SIGINT", () => {
|
|
2716
|
+
rl.close();
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
// src/commands/init.ts
|
|
2721
|
+
init_config();
|
|
2722
|
+
var import_node_readline3 = require("node:readline");
|
|
2723
|
+
async function initCommand() {
|
|
2724
|
+
writeLine(bold("\nCoder CLI \u2014 Initialization"));
|
|
2725
|
+
writeLine(dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2726
|
+
writeLine(`Config directory: ${getConfigDirPath()}`);
|
|
2727
|
+
writeLine("");
|
|
2728
|
+
const config = loadConfig();
|
|
2729
|
+
writeLine(info("Select your AI provider:"));
|
|
2730
|
+
const providers = ["deepseek", "glm", "agnes", "nvidia", "minimax", "custom"];
|
|
2731
|
+
for (let i = 0; i < providers.length; i++) {
|
|
2732
|
+
writeLine(` ${i + 1}. ${providers[i]}`);
|
|
2733
|
+
}
|
|
2734
|
+
writeLine("");
|
|
2735
|
+
const providerIndex = await askQuestion("Provider number [1]: ");
|
|
2736
|
+
const providerChoice = parseInt(providerIndex || "1", 10);
|
|
2737
|
+
const selectedProvider = providers[Math.max(0, Math.min(providerChoice - 1, providers.length - 1))];
|
|
2738
|
+
config.activeProvider = selectedProvider;
|
|
2739
|
+
writeLine(` Selected: ${bold(selectedProvider)}`);
|
|
2740
|
+
writeLine("");
|
|
2741
|
+
const providerSettings = config.providers[selectedProvider];
|
|
2742
|
+
const envVar = providerSettings.apiKeyEnvVar;
|
|
2743
|
+
writeLine(info(`API Key Configuration for "${selectedProvider}":`));
|
|
2744
|
+
writeLine(` You can set the ${bold(envVar)} environment variable,`);
|
|
2745
|
+
writeLine(` or enter the key directly (stored in config file).`);
|
|
2746
|
+
writeLine("");
|
|
2747
|
+
const source = await askQuestion("Use environment variable? [Y/n]: ");
|
|
2748
|
+
const useEnv = source.toLowerCase() !== "n";
|
|
2749
|
+
if (useEnv) {
|
|
2750
|
+
providerSettings.apiKeySource = "env";
|
|
2751
|
+
writeLine(` Using ${bold(envVar)} environment variable.`);
|
|
2752
|
+
writeLine(` Make sure to set it: export ${envVar}=<your-api-key>`);
|
|
2753
|
+
} else {
|
|
2754
|
+
providerSettings.apiKeySource = "manual";
|
|
2755
|
+
const key = await askQuestion("Enter API key: ");
|
|
2756
|
+
if (key.trim()) {
|
|
2757
|
+
providerSettings.apiKey = key.trim();
|
|
2758
|
+
writeLine(" API key saved to config.");
|
|
2759
|
+
} else {
|
|
2760
|
+
writeLine(` ${error("No key entered. Set it later with: coder config")}`);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
writeLine("");
|
|
2764
|
+
if (selectedProvider === "custom") {
|
|
2765
|
+
const baseUrl = await askQuestion("Custom API base URL: ");
|
|
2766
|
+
if (baseUrl.trim()) {
|
|
2767
|
+
providerSettings.customBaseUrl = baseUrl.trim();
|
|
2768
|
+
}
|
|
2769
|
+
} else {
|
|
2770
|
+
const customUrl = await askQuestion(`Custom base URL (leave empty for default): `);
|
|
2771
|
+
if (customUrl.trim()) {
|
|
2772
|
+
providerSettings.customBaseUrl = customUrl.trim();
|
|
2773
|
+
}
|
|
2774
|
+
writeLine("");
|
|
2775
|
+
}
|
|
2776
|
+
saveConfig(config);
|
|
2777
|
+
writeLine(success("\n\u2713 Configuration saved!"));
|
|
2778
|
+
writeLine(` Config file: ${getConfigFilePathExplicit()}`);
|
|
2779
|
+
writeLine("");
|
|
2780
|
+
writeLine(info("Quick start:"));
|
|
2781
|
+
writeLine(' coder "What is in this directory?"');
|
|
2782
|
+
writeLine(' coder ask "Explain this code"');
|
|
2783
|
+
writeLine(" coder repl");
|
|
2784
|
+
writeLine("");
|
|
2785
|
+
}
|
|
2786
|
+
function askQuestion(query) {
|
|
2787
|
+
const rl = (0, import_node_readline3.createInterface)({
|
|
2788
|
+
input: process.stdin,
|
|
2789
|
+
output: process.stderr
|
|
2790
|
+
});
|
|
2791
|
+
return new Promise((resolve12) => {
|
|
2792
|
+
rl.question(query, (answer) => {
|
|
2793
|
+
rl.close();
|
|
2794
|
+
resolve12(answer);
|
|
2795
|
+
});
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
// src/commands/config.ts
|
|
2800
|
+
init_config();
|
|
2801
|
+
var PROVIDER_IDS2 = ["deepseek", "glm", "agnes", "nvidia", "minimax", "custom"];
|
|
2802
|
+
async function configCommand(key, value) {
|
|
2803
|
+
const config = loadConfig();
|
|
2804
|
+
if (!key) {
|
|
2805
|
+
showConfig(config);
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
if (key && value !== void 0) {
|
|
2809
|
+
await setConfigValue(config, key, value);
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
showConfigValue(config, key);
|
|
2813
|
+
}
|
|
2814
|
+
function showConfig(config) {
|
|
2815
|
+
writeLine(bold("\nCoder CLI Configuration"));
|
|
2816
|
+
writeLine(dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2817
|
+
writeLine(`Config directory: ${getConfigDirPath()}`);
|
|
2818
|
+
writeLine(`Config file: ${getConfigFilePathExplicit()}`);
|
|
2819
|
+
writeLine("");
|
|
2820
|
+
writeLine(bold("Active Settings:"));
|
|
2821
|
+
writeLine(` Active provider: ${config.activeProvider}`);
|
|
2822
|
+
writeLine(` Last model: ${config.lastModel}`);
|
|
2823
|
+
writeLine(` Show usage: ${config.showUsage}`);
|
|
2824
|
+
writeLine("");
|
|
2825
|
+
for (const providerId of PROVIDER_IDS2) {
|
|
2826
|
+
const p = config.providers[providerId];
|
|
2827
|
+
writeLine(bold(`Provider: ${providerId}`));
|
|
2828
|
+
writeLine(` API Key Source: ${p.apiKeySource}`);
|
|
2829
|
+
writeLine(` API Key Env Var: ${p.apiKeyEnvVar}`);
|
|
2830
|
+
writeLine(` API Key (stored): ${p.apiKey ? "***" : "(not set)"}`);
|
|
2831
|
+
writeLine(` Custom Base URL: ${p.customBaseUrl || "(default)"}`);
|
|
2832
|
+
writeLine("");
|
|
2833
|
+
}
|
|
2834
|
+
writeLine(dim("To change a value: coder config <key> <value>"));
|
|
2835
|
+
writeLine(dim("Examples:"));
|
|
2836
|
+
writeLine(dim(' coder config activeProvider "deepseek"'));
|
|
2837
|
+
writeLine(dim(' coder config lastModel "deepseek-v4-flash"'));
|
|
2838
|
+
writeLine(dim(' coder config providers.deepseek.apiKeySource "env"'));
|
|
2839
|
+
writeLine("");
|
|
2840
|
+
}
|
|
2841
|
+
function showConfigValue(config, key) {
|
|
2842
|
+
const value = getNestedValue(config, key);
|
|
2843
|
+
if (value === void 0) {
|
|
2844
|
+
writeLine(warning(`Config key not found: ${key}`));
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
writeLine(String(value));
|
|
2848
|
+
}
|
|
2849
|
+
async function setConfigValue(config, key, value) {
|
|
2850
|
+
setNestedValue(config, key, parseValue(value));
|
|
2851
|
+
saveConfig(config);
|
|
2852
|
+
writeLine(info(`Config updated: ${key} = ${value}`));
|
|
2853
|
+
}
|
|
2854
|
+
function getNestedValue(obj, path) {
|
|
2855
|
+
const parts = path.split(".");
|
|
2856
|
+
let current = obj;
|
|
2857
|
+
for (const part of parts) {
|
|
2858
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
2859
|
+
return void 0;
|
|
2860
|
+
}
|
|
2861
|
+
current = current[part];
|
|
2862
|
+
}
|
|
2863
|
+
return current;
|
|
2864
|
+
}
|
|
2865
|
+
function setNestedValue(obj, path, value) {
|
|
2866
|
+
const parts = path.split(".");
|
|
2867
|
+
let current = obj;
|
|
2868
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
2869
|
+
if (!(parts[i] in current)) {
|
|
2870
|
+
current[parts[i]] = {};
|
|
2871
|
+
}
|
|
2872
|
+
current = current[parts[i]];
|
|
2873
|
+
}
|
|
2874
|
+
current[parts[parts.length - 1]] = value;
|
|
2875
|
+
}
|
|
2876
|
+
function parseValue(value) {
|
|
2877
|
+
if (value === "true") return true;
|
|
2878
|
+
if (value === "false") return false;
|
|
2879
|
+
const num = Number(value);
|
|
2880
|
+
if (!isNaN(num) && value.trim() !== "") return num;
|
|
2881
|
+
return value;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
// src/commands/common.ts
|
|
2885
|
+
var _globalOptions = {};
|
|
2886
|
+
function setGlobalOptions(opts) {
|
|
2887
|
+
_globalOptions = opts;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// src/index.ts
|
|
2891
|
+
var VERSION = "0.1.0";
|
|
2892
|
+
async function main() {
|
|
2893
|
+
const rawArgs = process.argv.slice(2);
|
|
2894
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
2895
|
+
printHelp();
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
if (rawArgs.includes("--version") || rawArgs.includes("-V")) {
|
|
2899
|
+
console.log(VERSION);
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
if (rawArgs.length === 0) {
|
|
2903
|
+
await replCommand({});
|
|
2904
|
+
return;
|
|
2905
|
+
}
|
|
2906
|
+
const globalOpts = extractGlobalOptions(rawArgs);
|
|
2907
|
+
setGlobalOptions(globalOpts);
|
|
2908
|
+
const firstNonFlag = rawArgs.find((a) => !a.startsWith("-"));
|
|
2909
|
+
const knownSubcommands = {
|
|
2910
|
+
ask: async (args, opts) => {
|
|
2911
|
+
await askCommand(args.join(" "), opts);
|
|
2912
|
+
},
|
|
2913
|
+
plan: async (args, opts) => {
|
|
2914
|
+
await planCommand(args.join(" "), opts);
|
|
2915
|
+
},
|
|
2916
|
+
repl: async () => {
|
|
2917
|
+
await replCommand(globalOpts);
|
|
2918
|
+
},
|
|
2919
|
+
init: async () => {
|
|
2920
|
+
await initCommand();
|
|
2921
|
+
},
|
|
2922
|
+
config: async (args) => {
|
|
2923
|
+
await configCommand(args[0], args[1]);
|
|
2924
|
+
}
|
|
2925
|
+
};
|
|
2926
|
+
if (firstNonFlag && knownSubcommands[firstNonFlag]) {
|
|
2927
|
+
const cmdArgs = rawArgs.slice(rawArgs.indexOf(firstNonFlag) + 1).filter((a) => !a.startsWith("-"));
|
|
2928
|
+
await knownSubcommands[firstNonFlag](cmdArgs, globalOpts);
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
const promptParts = rawArgs.filter((a) => !a.startsWith("-"));
|
|
2932
|
+
const prompt = promptParts.join(" ");
|
|
2933
|
+
if (!prompt.trim()) {
|
|
2934
|
+
await replCommand(globalOpts);
|
|
2935
|
+
return;
|
|
2936
|
+
}
|
|
2937
|
+
await runCommand(prompt, globalOpts);
|
|
2938
|
+
}
|
|
2939
|
+
function extractGlobalOptions(args) {
|
|
2940
|
+
const opts = {};
|
|
2941
|
+
for (let i = 0; i < args.length; i++) {
|
|
2942
|
+
const arg = args[i];
|
|
2943
|
+
if (arg === "-m" || arg === "--model") {
|
|
2944
|
+
opts.model = args[++i] ?? "";
|
|
2945
|
+
} else if (arg === "-p" || arg === "--provider") {
|
|
2946
|
+
opts.provider = args[++i] ?? "";
|
|
2947
|
+
} else if (arg === "-w" || arg === "--workspace") {
|
|
2948
|
+
opts.workspace = args[++i] ?? "";
|
|
2949
|
+
} else if (arg === "-y" || arg === "--yes") {
|
|
2950
|
+
opts.yes = true;
|
|
2951
|
+
} else if (arg === "--no-stream") {
|
|
2952
|
+
opts.stream = false;
|
|
2953
|
+
} else if (arg === "--stream") {
|
|
2954
|
+
opts.stream = true;
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
return opts;
|
|
2958
|
+
}
|
|
2959
|
+
function printHelp() {
|
|
2960
|
+
console.log(`
|
|
2961
|
+
Usage: coder [options] [prompt...]
|
|
2962
|
+
|
|
2963
|
+
Coder CLI \u2014 AI-powered coding assistant in the terminal
|
|
2964
|
+
|
|
2965
|
+
Options:
|
|
2966
|
+
-V, --version output the version number
|
|
2967
|
+
-m, --model <model> Model ID to use
|
|
2968
|
+
-p, --provider <provider> Provider ID (deepseek, glm, agnes, nvidia, minimax, custom)
|
|
2969
|
+
-w, --workspace <path> Workspace directory
|
|
2970
|
+
-y, --yes Auto-confirm prompts (unattended mode)
|
|
2971
|
+
--no-stream Disable streaming output
|
|
2972
|
+
-h, --help display help for command
|
|
2973
|
+
|
|
2974
|
+
Commands:
|
|
2975
|
+
ask [prompt...] Ask a question (read-only mode)
|
|
2976
|
+
plan [prompt...] Create or work on a plan
|
|
2977
|
+
repl Start interactive REPL session
|
|
2978
|
+
init Initialize Coder CLI configuration
|
|
2979
|
+
config [key] [value] View or edit configuration
|
|
2980
|
+
`);
|
|
2981
|
+
}
|
|
2982
|
+
main().catch((err) => {
|
|
2983
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2984
|
+
writeError(error(`Fatal: ${message}`));
|
|
2985
|
+
process.exit(1);
|
|
2986
|
+
});
|
|
2987
|
+
//# sourceMappingURL=index.cjs.map
|