@gemdoq/codi 0.1.8 → 0.2.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/dist/cli.js +2262 -799
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
4
10
|
var __esm = (fn, res) => function __init() {
|
|
5
11
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
12
|
};
|
|
@@ -95,15 +101,20 @@ function renderToolCall(toolName, args) {
|
|
|
95
101
|
return `${header}
|
|
96
102
|
${argStr}`;
|
|
97
103
|
}
|
|
98
|
-
function renderToolResult(toolName, result, isError) {
|
|
104
|
+
function renderToolResult(toolName, result, isError, durationMs) {
|
|
99
105
|
const icon = isError ? chalk2.red("\u2717") : chalk2.green("\u2713");
|
|
100
|
-
const
|
|
106
|
+
const duration = durationMs != null ? chalk2.dim(` (${formatDuration(durationMs)})`) : "";
|
|
107
|
+
const header = `${icon} ${chalk2.yellow(toolName)}${duration}`;
|
|
101
108
|
const content = isError ? chalk2.red(result) : chalk2.dim(result);
|
|
102
109
|
const maxLen = 500;
|
|
103
110
|
const truncated = content.length > maxLen ? content.slice(0, maxLen) + chalk2.dim("\n... (truncated)") : content;
|
|
104
111
|
return `${header}
|
|
105
112
|
${truncated}`;
|
|
106
113
|
}
|
|
114
|
+
function formatDuration(ms) {
|
|
115
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
116
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
117
|
+
}
|
|
107
118
|
function renderError(message) {
|
|
108
119
|
return chalk2.red(`\u2717 ${message}`);
|
|
109
120
|
}
|
|
@@ -323,9 +334,98 @@ var init_task_tools = __esm({
|
|
|
323
334
|
}
|
|
324
335
|
});
|
|
325
336
|
|
|
337
|
+
// src/tools/file-backup.ts
|
|
338
|
+
var file_backup_exports = {};
|
|
339
|
+
__export(file_backup_exports, {
|
|
340
|
+
backupFile: () => backupFile,
|
|
341
|
+
cleanupBackups: () => cleanupBackups,
|
|
342
|
+
getBackupHistory: () => getBackupHistory,
|
|
343
|
+
restoreFile: () => restoreFile,
|
|
344
|
+
undoLast: () => undoLast
|
|
345
|
+
});
|
|
346
|
+
import * as fs11 from "fs";
|
|
347
|
+
import * as os12 from "os";
|
|
348
|
+
import * as path12 from "path";
|
|
349
|
+
function ensureBackupDir() {
|
|
350
|
+
if (!fs11.existsSync(backupDir)) {
|
|
351
|
+
fs11.mkdirSync(backupDir, { recursive: true });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function registerCleanup() {
|
|
355
|
+
if (cleanupRegistered) return;
|
|
356
|
+
cleanupRegistered = true;
|
|
357
|
+
process.on("exit", () => {
|
|
358
|
+
cleanupBackups();
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
function backupFile(filePath) {
|
|
362
|
+
const resolved = path12.resolve(filePath);
|
|
363
|
+
ensureBackupDir();
|
|
364
|
+
registerCleanup();
|
|
365
|
+
const wasNew = !fs11.existsSync(resolved);
|
|
366
|
+
const backupName = `${Date.now()}-${Math.random().toString(36).slice(2)}-${path12.basename(resolved)}`;
|
|
367
|
+
const backupPath = path12.join(backupDir, backupName);
|
|
368
|
+
if (!wasNew) {
|
|
369
|
+
fs11.copyFileSync(resolved, backupPath);
|
|
370
|
+
}
|
|
371
|
+
const entry = {
|
|
372
|
+
backupPath,
|
|
373
|
+
originalPath: resolved,
|
|
374
|
+
wasNew,
|
|
375
|
+
timestamp: Date.now()
|
|
376
|
+
};
|
|
377
|
+
backupHistory.push(entry);
|
|
378
|
+
return backupPath;
|
|
379
|
+
}
|
|
380
|
+
function restoreFile(entry) {
|
|
381
|
+
if (entry.wasNew) {
|
|
382
|
+
if (fs11.existsSync(entry.originalPath)) {
|
|
383
|
+
fs11.unlinkSync(entry.originalPath);
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
if (fs11.existsSync(entry.backupPath)) {
|
|
387
|
+
fs11.copyFileSync(entry.backupPath, entry.originalPath);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function cleanupBackups() {
|
|
392
|
+
if (fs11.existsSync(backupDir)) {
|
|
393
|
+
try {
|
|
394
|
+
fs11.rmSync(backupDir, { recursive: true, force: true });
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
backupHistory.length = 0;
|
|
399
|
+
}
|
|
400
|
+
function getBackupHistory() {
|
|
401
|
+
return backupHistory;
|
|
402
|
+
}
|
|
403
|
+
function undoLast() {
|
|
404
|
+
const entry = backupHistory.pop();
|
|
405
|
+
if (!entry) return null;
|
|
406
|
+
restoreFile(entry);
|
|
407
|
+
if (!entry.wasNew && fs11.existsSync(entry.backupPath)) {
|
|
408
|
+
try {
|
|
409
|
+
fs11.unlinkSync(entry.backupPath);
|
|
410
|
+
} catch {
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return entry;
|
|
414
|
+
}
|
|
415
|
+
var backupDir, backupHistory, cleanupRegistered;
|
|
416
|
+
var init_file_backup = __esm({
|
|
417
|
+
"src/tools/file-backup.ts"() {
|
|
418
|
+
"use strict";
|
|
419
|
+
init_esm_shims();
|
|
420
|
+
backupDir = path12.join(os12.tmpdir(), `codi-backups-${process.pid}`);
|
|
421
|
+
backupHistory = [];
|
|
422
|
+
cleanupRegistered = false;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
326
426
|
// src/cli.ts
|
|
327
427
|
init_esm_shims();
|
|
328
|
-
import
|
|
428
|
+
import chalk14 from "chalk";
|
|
329
429
|
|
|
330
430
|
// src/setup-wizard.ts
|
|
331
431
|
init_esm_shims();
|
|
@@ -637,6 +737,7 @@ init_esm_shims();
|
|
|
637
737
|
import * as readline2 from "readline/promises";
|
|
638
738
|
import { stdin as input2, stdout as output2 } from "process";
|
|
639
739
|
import * as os3 from "os";
|
|
740
|
+
import * as fs4 from "fs";
|
|
640
741
|
import chalk4 from "chalk";
|
|
641
742
|
import { execSync } from "child_process";
|
|
642
743
|
import { edit } from "external-editor";
|
|
@@ -779,14 +880,17 @@ function completer(line) {
|
|
|
779
880
|
}
|
|
780
881
|
|
|
781
882
|
// src/repl.ts
|
|
782
|
-
import { readFileSync as
|
|
883
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
783
884
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
784
885
|
import * as path5 from "path";
|
|
785
886
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
786
887
|
var __dirname2 = path5.dirname(__filename2);
|
|
888
|
+
var HISTORY_DIR = path5.join(os3.homedir(), ".codi");
|
|
889
|
+
var HISTORY_FILE = path5.join(HISTORY_DIR, "history");
|
|
890
|
+
var MAX_HISTORY = 1e3;
|
|
787
891
|
function getVersion() {
|
|
788
892
|
try {
|
|
789
|
-
const pkg = JSON.parse(
|
|
893
|
+
const pkg = JSON.parse(readFileSync4(path5.join(__dirname2, "..", "package.json"), "utf-8"));
|
|
790
894
|
return `v${pkg.version}`;
|
|
791
895
|
} catch {
|
|
792
896
|
return "v0.1.4";
|
|
@@ -815,15 +919,50 @@ var Repl = class {
|
|
|
815
919
|
description: "Clear screen"
|
|
816
920
|
});
|
|
817
921
|
}
|
|
922
|
+
loadHistory() {
|
|
923
|
+
try {
|
|
924
|
+
const content = fs4.readFileSync(HISTORY_FILE, "utf-8");
|
|
925
|
+
return content.split("\n").filter(Boolean).slice(-MAX_HISTORY);
|
|
926
|
+
} catch {
|
|
927
|
+
return [];
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
saveHistory() {
|
|
931
|
+
if (!this.rl) return;
|
|
932
|
+
try {
|
|
933
|
+
const rlAny = this.rl;
|
|
934
|
+
const history = rlAny.history ?? [];
|
|
935
|
+
const entries = history.slice(0, MAX_HISTORY).reverse();
|
|
936
|
+
fs4.mkdirSync(HISTORY_DIR, { recursive: true });
|
|
937
|
+
fs4.writeFileSync(HISTORY_FILE, entries.join("\n") + "\n", "utf-8");
|
|
938
|
+
} catch {
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
shouldSaveToHistory(line) {
|
|
942
|
+
const trimmed = line.trim();
|
|
943
|
+
if (!trimmed) return false;
|
|
944
|
+
if (trimmed.startsWith("/")) return false;
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
818
947
|
async start() {
|
|
819
948
|
this.running = true;
|
|
949
|
+
const loadedHistory = this.loadHistory();
|
|
820
950
|
this.rl = readline2.createInterface({
|
|
821
951
|
input: input2,
|
|
822
952
|
output: output2,
|
|
823
953
|
prompt: renderPrompt(),
|
|
824
954
|
completer: (line) => completer(line),
|
|
825
|
-
terminal: true
|
|
955
|
+
terminal: true,
|
|
956
|
+
history: loadedHistory,
|
|
957
|
+
historySize: MAX_HISTORY
|
|
826
958
|
});
|
|
959
|
+
const rlAny = this.rl;
|
|
960
|
+
if (rlAny.history && loadedHistory.length > 0) {
|
|
961
|
+
rlAny.history.length = 0;
|
|
962
|
+
for (let i = loadedHistory.length - 1; i >= 0; i--) {
|
|
963
|
+
rlAny.history.push(loadedHistory[i]);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
827
966
|
if (process.stdin.isTTY && os3.platform() !== "win32") {
|
|
828
967
|
process.stdout.write("\x1B[?2004h");
|
|
829
968
|
}
|
|
@@ -857,10 +996,10 @@ var Repl = class {
|
|
|
857
996
|
}
|
|
858
997
|
this.rl.setPrompt(renderPrompt());
|
|
859
998
|
this.rl.prompt();
|
|
860
|
-
const line = await new Promise((
|
|
999
|
+
const line = await new Promise((resolve11, reject) => {
|
|
861
1000
|
const onLine = (data) => {
|
|
862
1001
|
cleanup();
|
|
863
|
-
|
|
1002
|
+
resolve11(data);
|
|
864
1003
|
};
|
|
865
1004
|
const onClose = () => {
|
|
866
1005
|
cleanup();
|
|
@@ -876,7 +1015,7 @@ var Repl = class {
|
|
|
876
1015
|
this.lastInterruptTime = now;
|
|
877
1016
|
this.options.onInterrupt();
|
|
878
1017
|
console.log(chalk4.dim("\n(Press Ctrl+C again to exit)"));
|
|
879
|
-
|
|
1018
|
+
resolve11("");
|
|
880
1019
|
};
|
|
881
1020
|
const cleanup = () => {
|
|
882
1021
|
this.rl.removeListener("line", onLine);
|
|
@@ -889,6 +1028,17 @@ var Repl = class {
|
|
|
889
1028
|
});
|
|
890
1029
|
const trimmed = line.trim();
|
|
891
1030
|
if (!trimmed) continue;
|
|
1031
|
+
{
|
|
1032
|
+
const rlAny2 = this.rl;
|
|
1033
|
+
const hist = rlAny2.history;
|
|
1034
|
+
if (hist && hist.length > 0) {
|
|
1035
|
+
if (!this.shouldSaveToHistory(trimmed)) {
|
|
1036
|
+
hist.shift();
|
|
1037
|
+
} else if (hist.length > 1 && hist[0] === hist[1]) {
|
|
1038
|
+
hist.shift();
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
892
1042
|
if (trimmed.endsWith("\\")) {
|
|
893
1043
|
this.multilineBuffer.push(trimmed.slice(0, -1));
|
|
894
1044
|
this.inMultiline = true;
|
|
@@ -952,32 +1102,37 @@ var Repl = class {
|
|
|
952
1102
|
}
|
|
953
1103
|
return;
|
|
954
1104
|
}
|
|
955
|
-
let message = input3;
|
|
956
1105
|
const IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"]);
|
|
1106
|
+
const MIME_MAP = {
|
|
1107
|
+
".png": "image/png",
|
|
1108
|
+
".jpg": "image/jpeg",
|
|
1109
|
+
".jpeg": "image/jpeg",
|
|
1110
|
+
".gif": "image/gif",
|
|
1111
|
+
".webp": "image/webp",
|
|
1112
|
+
".bmp": "image/bmp",
|
|
1113
|
+
".svg": "image/svg+xml"
|
|
1114
|
+
};
|
|
957
1115
|
const atMatches = input3.match(/@([\w.\/\\:~-]+)/g);
|
|
1116
|
+
let hasImages = false;
|
|
1117
|
+
const imageBlocks = [];
|
|
1118
|
+
let message = input3;
|
|
958
1119
|
if (atMatches) {
|
|
959
1120
|
for (const match of atMatches) {
|
|
960
1121
|
const filePath = match.slice(1);
|
|
961
1122
|
try {
|
|
962
1123
|
const ext = path5.extname(filePath).toLowerCase();
|
|
963
1124
|
if (IMAGE_EXTS.has(ext)) {
|
|
964
|
-
const data =
|
|
1125
|
+
const data = readFileSync4(filePath);
|
|
965
1126
|
const base64 = data.toString("base64");
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
".svg": "image/svg+xml"
|
|
974
|
-
};
|
|
975
|
-
const mime = mimeMap[ext] || "image/png";
|
|
976
|
-
message = message.replace(match, `
|
|
977
|
-
[Image: ${filePath}](data:${mime};base64,${base64})
|
|
978
|
-
`);
|
|
1127
|
+
const mime = MIME_MAP[ext] || "image/png";
|
|
1128
|
+
imageBlocks.push({
|
|
1129
|
+
type: "image",
|
|
1130
|
+
source: { type: "base64", media_type: mime, data: base64 }
|
|
1131
|
+
});
|
|
1132
|
+
message = message.replace(match, `[\uC774\uBBF8\uC9C0: ${path5.basename(filePath)}]`);
|
|
1133
|
+
hasImages = true;
|
|
979
1134
|
} else {
|
|
980
|
-
const content =
|
|
1135
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
981
1136
|
message = message.replace(match, `
|
|
982
1137
|
[File: ${filePath}]
|
|
983
1138
|
\`\`\`
|
|
@@ -989,7 +1144,15 @@ ${content}
|
|
|
989
1144
|
}
|
|
990
1145
|
}
|
|
991
1146
|
}
|
|
992
|
-
|
|
1147
|
+
if (hasImages) {
|
|
1148
|
+
const blocks = [
|
|
1149
|
+
{ type: "text", text: message.trim() },
|
|
1150
|
+
...imageBlocks
|
|
1151
|
+
];
|
|
1152
|
+
await this.options.onMessage(blocks);
|
|
1153
|
+
} else {
|
|
1154
|
+
await this.options.onMessage(message);
|
|
1155
|
+
}
|
|
993
1156
|
}
|
|
994
1157
|
openEditor() {
|
|
995
1158
|
try {
|
|
@@ -1007,6 +1170,7 @@ ${content}
|
|
|
1007
1170
|
}
|
|
1008
1171
|
}
|
|
1009
1172
|
async gracefulExit() {
|
|
1173
|
+
this.saveHistory();
|
|
1010
1174
|
this.stop();
|
|
1011
1175
|
if (this.options.onExit) {
|
|
1012
1176
|
await this.options.onExit();
|
|
@@ -1033,6 +1197,64 @@ init_esm_shims();
|
|
|
1033
1197
|
|
|
1034
1198
|
// src/agent/conversation.ts
|
|
1035
1199
|
init_esm_shims();
|
|
1200
|
+
|
|
1201
|
+
// src/utils/tokenizer.ts
|
|
1202
|
+
init_esm_shims();
|
|
1203
|
+
import { getEncoding, encodingForModel } from "js-tiktoken";
|
|
1204
|
+
var encoderCache = /* @__PURE__ */ new Map();
|
|
1205
|
+
function getEncoder(model) {
|
|
1206
|
+
const cacheKey = model ?? "cl100k_base";
|
|
1207
|
+
const cached = encoderCache.get(cacheKey);
|
|
1208
|
+
if (cached) return cached;
|
|
1209
|
+
try {
|
|
1210
|
+
const encoder = model ? encodingForModel(model) : getEncoding("cl100k_base");
|
|
1211
|
+
encoderCache.set(cacheKey, encoder);
|
|
1212
|
+
return encoder;
|
|
1213
|
+
} catch {
|
|
1214
|
+
const fallback = encoderCache.get("cl100k_base");
|
|
1215
|
+
if (fallback) return fallback;
|
|
1216
|
+
const encoder = getEncoding("cl100k_base");
|
|
1217
|
+
encoderCache.set("cl100k_base", encoder);
|
|
1218
|
+
return encoder;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
function countTokens(text, model) {
|
|
1222
|
+
if (!text) return 0;
|
|
1223
|
+
try {
|
|
1224
|
+
const encoder = getEncoder(model);
|
|
1225
|
+
return encoder.encode(text).length;
|
|
1226
|
+
} catch {
|
|
1227
|
+
return Math.ceil(text.length / 4);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
function countContentBlockTokens(blocks, model) {
|
|
1231
|
+
let total = 0;
|
|
1232
|
+
for (const block of blocks) {
|
|
1233
|
+
if (block.type === "text") {
|
|
1234
|
+
total += countTokens(block.text, model);
|
|
1235
|
+
} else if (block.type === "tool_use") {
|
|
1236
|
+
total += countTokens(block.name, model);
|
|
1237
|
+
total += countTokens(JSON.stringify(block.input), model);
|
|
1238
|
+
} else if (block.type === "tool_result") {
|
|
1239
|
+
if (typeof block.content === "string") {
|
|
1240
|
+
total += countTokens(block.content, model);
|
|
1241
|
+
} else {
|
|
1242
|
+
total += countTokens(JSON.stringify(block.content), model);
|
|
1243
|
+
}
|
|
1244
|
+
} else {
|
|
1245
|
+
total += countTokens(JSON.stringify(block), model);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return total;
|
|
1249
|
+
}
|
|
1250
|
+
function countMessageTokens(content, model) {
|
|
1251
|
+
if (typeof content === "string") {
|
|
1252
|
+
return countTokens(content, model);
|
|
1253
|
+
}
|
|
1254
|
+
return countContentBlockTokens(content, model);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// src/agent/conversation.ts
|
|
1036
1258
|
var Conversation = class _Conversation {
|
|
1037
1259
|
messages = [];
|
|
1038
1260
|
systemPrompt = "";
|
|
@@ -1118,24 +1340,14 @@ ${summary}` },
|
|
|
1118
1340
|
return conv;
|
|
1119
1341
|
}
|
|
1120
1342
|
/**
|
|
1121
|
-
*
|
|
1343
|
+
* tiktoken을 사용하여 정확한 토큰 수를 계산한다.
|
|
1122
1344
|
*/
|
|
1123
|
-
estimateTokens() {
|
|
1124
|
-
let
|
|
1345
|
+
estimateTokens(model) {
|
|
1346
|
+
let tokens = countTokens(this.systemPrompt, model);
|
|
1125
1347
|
for (const msg of this.messages) {
|
|
1126
|
-
|
|
1127
|
-
chars += msg.content.length;
|
|
1128
|
-
} else {
|
|
1129
|
-
for (const block of msg.content) {
|
|
1130
|
-
if (block.type === "text") chars += block.text.length;
|
|
1131
|
-
else if (block.type === "tool_use") chars += JSON.stringify(block.input).length;
|
|
1132
|
-
else if (block.type === "tool_result") {
|
|
1133
|
-
chars += typeof block.content === "string" ? block.content.length : JSON.stringify(block.content).length;
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1348
|
+
tokens += countMessageTokens(msg.content, model);
|
|
1137
1349
|
}
|
|
1138
|
-
return
|
|
1350
|
+
return tokens;
|
|
1139
1351
|
}
|
|
1140
1352
|
};
|
|
1141
1353
|
|
|
@@ -1143,147 +1355,679 @@ ${summary}` },
|
|
|
1143
1355
|
init_esm_shims();
|
|
1144
1356
|
init_tool();
|
|
1145
1357
|
init_renderer();
|
|
1358
|
+
|
|
1359
|
+
// src/ui/spinner.ts
|
|
1360
|
+
init_esm_shims();
|
|
1361
|
+
import ora from "ora";
|
|
1146
1362
|
import chalk5 from "chalk";
|
|
1147
|
-
var
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1363
|
+
var currentSpinner = null;
|
|
1364
|
+
function startSpinner(text) {
|
|
1365
|
+
stopSpinner();
|
|
1366
|
+
currentSpinner = ora({
|
|
1367
|
+
text: chalk5.dim(text),
|
|
1368
|
+
spinner: "dots",
|
|
1369
|
+
color: "cyan"
|
|
1370
|
+
}).start();
|
|
1371
|
+
return currentSpinner;
|
|
1372
|
+
}
|
|
1373
|
+
function updateSpinner(text) {
|
|
1374
|
+
if (currentSpinner) {
|
|
1375
|
+
currentSpinner.text = chalk5.dim(text);
|
|
1151
1376
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
};
|
|
1160
|
-
}
|
|
1161
|
-
if (this.options.planMode && !tool.readOnly) {
|
|
1162
|
-
return {
|
|
1163
|
-
toolUseId: toolCall.id,
|
|
1164
|
-
toolName: toolCall.name,
|
|
1165
|
-
result: makeToolError(`Tool '${toolCall.name}' is not available in plan mode (read-only). Use only read-only tools.`)
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
|
-
if (tool.dangerous && this.options.permissionCheck) {
|
|
1169
|
-
const allowed = await this.options.permissionCheck(tool, toolCall.input);
|
|
1170
|
-
if (!allowed) {
|
|
1171
|
-
return {
|
|
1172
|
-
toolUseId: toolCall.id,
|
|
1173
|
-
toolName: toolCall.name,
|
|
1174
|
-
result: makeToolError(`Permission denied for tool: ${toolCall.name}`)
|
|
1175
|
-
};
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
let input3 = toolCall.input;
|
|
1179
|
-
if (this.options.preHook) {
|
|
1180
|
-
try {
|
|
1181
|
-
const hookResult = await this.options.preHook(toolCall.name, input3);
|
|
1182
|
-
if (!hookResult.proceed) {
|
|
1183
|
-
return {
|
|
1184
|
-
toolUseId: toolCall.id,
|
|
1185
|
-
toolName: toolCall.name,
|
|
1186
|
-
result: makeToolError(`Tool execution blocked by hook for: ${toolCall.name}`)
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
if (hookResult.updatedInput) {
|
|
1190
|
-
input3 = hookResult.updatedInput;
|
|
1191
|
-
}
|
|
1192
|
-
} catch (err) {
|
|
1193
|
-
console.error(chalk5.yellow(`Hook error for ${toolCall.name}: ${err}`));
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
if (this.options.showToolCalls) {
|
|
1197
|
-
console.log(renderToolCall(toolCall.name, input3));
|
|
1377
|
+
}
|
|
1378
|
+
function stopSpinner(symbol) {
|
|
1379
|
+
if (currentSpinner) {
|
|
1380
|
+
if (symbol) {
|
|
1381
|
+
currentSpinner.stopAndPersist({ symbol });
|
|
1382
|
+
} else {
|
|
1383
|
+
currentSpinner.stop();
|
|
1198
1384
|
}
|
|
1199
|
-
|
|
1385
|
+
currentSpinner = null;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// src/tools/bash.ts
|
|
1390
|
+
init_esm_shims();
|
|
1391
|
+
init_tool();
|
|
1392
|
+
import { spawn } from "child_process";
|
|
1393
|
+
import * as os5 from "os";
|
|
1394
|
+
import chalk7 from "chalk";
|
|
1395
|
+
|
|
1396
|
+
// src/security/command-validator.ts
|
|
1397
|
+
init_esm_shims();
|
|
1398
|
+
|
|
1399
|
+
// src/utils/logger.ts
|
|
1400
|
+
init_esm_shims();
|
|
1401
|
+
import * as fs5 from "fs";
|
|
1402
|
+
import * as path6 from "path";
|
|
1403
|
+
import * as os4 from "os";
|
|
1404
|
+
var LOG_LEVEL_PRIORITY = {
|
|
1405
|
+
debug: 0,
|
|
1406
|
+
info: 1,
|
|
1407
|
+
warn: 2,
|
|
1408
|
+
error: 3
|
|
1409
|
+
};
|
|
1410
|
+
var Logger = class _Logger {
|
|
1411
|
+
static instance;
|
|
1412
|
+
level;
|
|
1413
|
+
logDir;
|
|
1414
|
+
initialized = false;
|
|
1415
|
+
constructor(logDir) {
|
|
1416
|
+
const envLevel = process.env["CODI_LOG_LEVEL"]?.toLowerCase();
|
|
1417
|
+
this.level = this.isValidLevel(envLevel) ? envLevel : "info";
|
|
1418
|
+
this.logDir = logDir ?? path6.join(os4.homedir(), ".codi", "logs");
|
|
1419
|
+
}
|
|
1420
|
+
static getInstance() {
|
|
1421
|
+
if (!_Logger.instance) {
|
|
1422
|
+
_Logger.instance = new _Logger();
|
|
1423
|
+
}
|
|
1424
|
+
return _Logger.instance;
|
|
1425
|
+
}
|
|
1426
|
+
/** 테스트용: 커스텀 logDir로 새 인스턴스 생성 */
|
|
1427
|
+
static createForTest(logDir, level = "debug") {
|
|
1428
|
+
const instance = new _Logger(logDir);
|
|
1429
|
+
instance.level = level;
|
|
1430
|
+
return instance;
|
|
1431
|
+
}
|
|
1432
|
+
isValidLevel(val) {
|
|
1433
|
+
return val !== void 0 && val in LOG_LEVEL_PRIORITY;
|
|
1434
|
+
}
|
|
1435
|
+
ensureDir() {
|
|
1436
|
+
if (this.initialized) return;
|
|
1200
1437
|
try {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
);
|
|
1206
|
-
}
|
|
1207
|
-
if (this.options.showToolCalls) {
|
|
1208
|
-
console.log(renderToolResult(toolCall.name, result.output, !result.success));
|
|
1438
|
+
fs5.mkdirSync(this.logDir, { recursive: true });
|
|
1439
|
+
this.initialized = true;
|
|
1440
|
+
this.rotateOldLogs();
|
|
1441
|
+
} catch {
|
|
1209
1442
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1443
|
+
}
|
|
1444
|
+
/** 7일 이상 된 로그 파일 삭제 */
|
|
1445
|
+
rotateOldLogs() {
|
|
1446
|
+
try {
|
|
1447
|
+
const files = fs5.readdirSync(this.logDir);
|
|
1448
|
+
const now = Date.now();
|
|
1449
|
+
const maxAge = 7 * 24 * 60 * 60 * 1e3;
|
|
1450
|
+
for (const file of files) {
|
|
1451
|
+
if (!file.startsWith("codi-") || !file.endsWith(".log")) continue;
|
|
1452
|
+
const filePath = path6.join(this.logDir, file);
|
|
1453
|
+
try {
|
|
1454
|
+
const stat = fs5.statSync(filePath);
|
|
1455
|
+
if (now - stat.mtimeMs > maxAge) {
|
|
1456
|
+
fs5.unlinkSync(filePath);
|
|
1457
|
+
}
|
|
1458
|
+
} catch {
|
|
1459
|
+
}
|
|
1214
1460
|
}
|
|
1461
|
+
} catch {
|
|
1215
1462
|
}
|
|
1216
|
-
return {
|
|
1217
|
-
toolUseId: toolCall.id,
|
|
1218
|
-
toolName: toolCall.name,
|
|
1219
|
-
result
|
|
1220
|
-
};
|
|
1221
1463
|
}
|
|
1222
|
-
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1464
|
+
getLogFilePath() {
|
|
1465
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1466
|
+
return path6.join(this.logDir, `codi-${date}.log`);
|
|
1467
|
+
}
|
|
1468
|
+
shouldLog(level) {
|
|
1469
|
+
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this.level];
|
|
1470
|
+
}
|
|
1471
|
+
write(level, message, context, error) {
|
|
1472
|
+
if (!this.shouldLog(level)) return;
|
|
1473
|
+
this.ensureDir();
|
|
1474
|
+
if (!this.initialized) return;
|
|
1475
|
+
const entry = {
|
|
1476
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1477
|
+
level,
|
|
1478
|
+
message
|
|
1479
|
+
};
|
|
1480
|
+
if (context && Object.keys(context).length > 0) {
|
|
1481
|
+
entry.context = context;
|
|
1232
1482
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1483
|
+
if (error) {
|
|
1484
|
+
entry.error = {
|
|
1485
|
+
message: error.message,
|
|
1486
|
+
stack: error.stack
|
|
1487
|
+
};
|
|
1238
1488
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
const r = safeResults[i];
|
|
1243
|
-
if (r.status === "fulfilled") {
|
|
1244
|
-
results.push(r.value);
|
|
1245
|
-
} else {
|
|
1246
|
-
results.push({
|
|
1247
|
-
toolUseId: safeCalls[i].id,
|
|
1248
|
-
toolName: safeCalls[i].name,
|
|
1249
|
-
result: makeToolError(`Tool execution failed: ${r.reason}`)
|
|
1250
|
-
});
|
|
1251
|
-
}
|
|
1489
|
+
try {
|
|
1490
|
+
fs5.appendFileSync(this.getLogFilePath(), JSON.stringify(entry) + "\n");
|
|
1491
|
+
} catch {
|
|
1252
1492
|
}
|
|
1253
|
-
results.push(...dangerousResults);
|
|
1254
|
-
const orderMap = new Map(toolCalls.map((tc, i) => [tc.id, i]));
|
|
1255
|
-
results.sort((a, b) => (orderMap.get(a.toolUseId) ?? 0) - (orderMap.get(b.toolUseId) ?? 0));
|
|
1256
|
-
return results;
|
|
1257
1493
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1494
|
+
debug(message, context) {
|
|
1495
|
+
this.write("debug", message, context);
|
|
1496
|
+
}
|
|
1497
|
+
info(message, context) {
|
|
1498
|
+
this.write("info", message, context);
|
|
1499
|
+
}
|
|
1500
|
+
warn(message, context) {
|
|
1501
|
+
this.write("warn", message, context);
|
|
1502
|
+
}
|
|
1503
|
+
error(message, context, error) {
|
|
1504
|
+
this.write("error", message, context, error);
|
|
1260
1505
|
}
|
|
1261
1506
|
};
|
|
1507
|
+
var logger = Logger.getInstance();
|
|
1262
1508
|
|
|
1263
|
-
// src/
|
|
1264
|
-
|
|
1509
|
+
// src/security/command-validator.ts
|
|
1510
|
+
var BLOCKED_PATTERNS = [
|
|
1511
|
+
// 광범위 삭제
|
|
1512
|
+
{ pattern: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|(-[a-zA-Z]*f[a-zA-Z]*r))\s+[/~*]/, reason: "\uAD11\uBC94\uC704 \uC0AD\uC81C \uBA85\uB839\uC5B4(rm -rf /, ~, *)\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1513
|
+
{ pattern: /\brm\s+-rf\s*$/, reason: "\uB300\uC0C1 \uC5C6\uB294 rm -rf\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1514
|
+
{ pattern: /\bRemove-Item\s+.*-Recurse.*[/\\]\s*$/, reason: "PowerShell \uAD11\uBC94\uC704 \uC0AD\uC81C\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1515
|
+
{ pattern: /\bRemove-Item\s+.*-Recurse.*(\*|~|[A-Z]:\\)/, reason: "PowerShell \uAD11\uBC94\uC704 \uC0AD\uC81C\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1516
|
+
// 디스크 연산
|
|
1517
|
+
{ pattern: /\bmkfs\b/, reason: "\uD30C\uC77C\uC2DC\uC2A4\uD15C \uD3EC\uB9F7(mkfs) \uBA85\uB839\uC5B4\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1518
|
+
{ pattern: /\bformat\s+[A-Z]:/i, reason: "\uB514\uC2A4\uD06C \uD3EC\uB9F7(format) \uBA85\uB839\uC5B4\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1519
|
+
{ pattern: /\bdd\s+if=/, reason: "dd \uB514\uC2A4\uD06C \uC4F0\uAE30 \uBA85\uB839\uC5B4\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1520
|
+
// 광범위 권한 변경
|
|
1521
|
+
{ pattern: /\bchmod\s+(-[a-zA-Z]*R[a-zA-Z]*\s+)?777\b/, reason: "\uAD11\uBC94\uC704 \uAD8C\uD55C \uBCC0\uACBD(chmod 777)\uC774 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1522
|
+
{ pattern: /\bchown\s+-[a-zA-Z]*R/, reason: "\uC7AC\uADC0\uC801 \uC18C\uC720\uC790 \uBCC0\uACBD(chown -R)\uC774 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1523
|
+
// 포크 폭탄
|
|
1524
|
+
{ pattern: /:\(\)\s*\{.*\|.*&\s*\}\s*;?\s*:/, reason: "\uD3EC\uD06C \uD3ED\uD0C4\uC774 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1525
|
+
{ pattern: /\bwhile\s+true\s*;\s*do\s+fork/, reason: "\uD3EC\uD06C \uD3ED\uD0C4 \uD328\uD134\uC774 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1526
|
+
// 디바이스 리디렉션
|
|
1527
|
+
{ pattern: />\s*\/dev\/sd[a-z]/, reason: "\uB514\uBC14\uC774\uC2A4 \uC9C1\uC811 \uC4F0\uAE30\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1528
|
+
{ pattern: />\s*\/dev\/nvme/, reason: "\uB514\uBC14\uC774\uC2A4 \uC9C1\uC811 \uC4F0\uAE30\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1529
|
+
{ pattern: />\s*\\\\\.\\PhysicalDrive/, reason: "Windows \uB514\uBC14\uC774\uC2A4 \uC9C1\uC811 \uC4F0\uAE30\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1530
|
+
// 인터넷에서 받아서 바로 실행
|
|
1531
|
+
{ pattern: /\bcurl\b.*\|\s*(sh|bash|zsh|powershell|pwsh)\b/, reason: "\uC6D0\uACA9 \uC2A4\uD06C\uB9BD\uD2B8 \uD30C\uC774\uD504 \uC2E4\uD589(curl | sh)\uC774 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1532
|
+
{ pattern: /\bwget\b.*\|\s*(sh|bash|zsh|powershell|pwsh)\b/, reason: "\uC6D0\uACA9 \uC2A4\uD06C\uB9BD\uD2B8 \uD30C\uC774\uD504 \uC2E4\uD589(wget | sh)\uC774 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1533
|
+
{ pattern: /\bInvoke-WebRequest\b.*\|\s*Invoke-Expression\b/, reason: "PowerShell \uC6D0\uACA9 \uC2A4\uD06C\uB9BD\uD2B8 \uC2E4\uD589\uC774 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1534
|
+
{ pattern: /\biwr\b.*\|\s*iex\b/, reason: "PowerShell \uC6D0\uACA9 \uC2A4\uD06C\uB9BD\uD2B8 \uC2E4\uD589(iwr | iex)\uC774 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1535
|
+
{ pattern: /\bIEX\s*\(\s*(New-Object|Invoke-WebRequest|iwr)\b/, reason: "PowerShell \uC6D0\uACA9 \uC2A4\uD06C\uB9BD\uD2B8 \uC2E4\uD589\uC774 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1536
|
+
// 시스템 종료/재부팅
|
|
1537
|
+
{ pattern: /\bshutdown\b/, reason: "\uC2DC\uC2A4\uD15C \uC885\uB8CC(shutdown) \uBA85\uB839\uC5B4\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1538
|
+
{ pattern: /\breboot\b/, reason: "\uC2DC\uC2A4\uD15C \uC7AC\uBD80\uD305(reboot) \uBA85\uB839\uC5B4\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1539
|
+
{ pattern: /\bhalt\b/, reason: "\uC2DC\uC2A4\uD15C \uC911\uC9C0(halt) \uBA85\uB839\uC5B4\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1540
|
+
{ pattern: /\bStop-Computer\b/, reason: "PowerShell \uC2DC\uC2A4\uD15C \uC885\uB8CC\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1541
|
+
{ pattern: /\bRestart-Computer\b/, reason: "PowerShell \uC2DC\uC2A4\uD15C \uC7AC\uBD80\uD305\uC774 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1542
|
+
// 프로세스 종료
|
|
1543
|
+
{ pattern: /\bkill\s+-9\s+1\b/, reason: "init \uD504\uB85C\uC138\uC2A4 \uC885\uB8CC(kill -9 1)\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1544
|
+
{ pattern: /\bkillall\b/, reason: "\uC804\uCCB4 \uD504\uB85C\uC138\uC2A4 \uC885\uB8CC(killall)\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1545
|
+
{ pattern: /\bStop-Process\s+.*-Id\s+1\b/, reason: "PowerShell init \uD504\uB85C\uC138\uC2A4 \uC885\uB8CC\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." }
|
|
1546
|
+
];
|
|
1547
|
+
var WARNED_PATTERNS = [
|
|
1548
|
+
{ pattern: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|(-[a-zA-Z]*f[a-zA-Z]*r))\s+/, reason: "rm -rf \uBA85\uB839\uC5B4\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uACBD\uB85C\uB97C \uD655\uC778\uD558\uC138\uC694." },
|
|
1549
|
+
{ pattern: /\bRemove-Item\s+.*-Recurse/, reason: "PowerShell \uC7AC\uADC0 \uC0AD\uC81C\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uACBD\uB85C\uB97C \uD655\uC778\uD558\uC138\uC694." },
|
|
1550
|
+
{ pattern: /\bsudo\b/, reason: "sudo \uBA85\uB839\uC5B4\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1551
|
+
{ pattern: /\bRunAs\b/i, reason: "Windows \uAD00\uB9AC\uC790 \uAD8C\uD55C \uC2E4\uD589\uC774 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1552
|
+
{ pattern: /\bnpm\s+publish\b/, reason: "npm \uD328\uD0A4\uC9C0 \uBC30\uD3EC(npm publish)\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1553
|
+
{ pattern: /\bdocker\s+push\b/, reason: "Docker \uC774\uBBF8\uC9C0 \uD478\uC2DC(docker push)\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
|
|
1554
|
+
{ pattern: /\bgit\s+push\b/, reason: "git push\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." }
|
|
1555
|
+
];
|
|
1556
|
+
function validateCommand(command) {
|
|
1557
|
+
const trimmed = command.trim();
|
|
1558
|
+
if (!trimmed) {
|
|
1559
|
+
return { allowed: true, level: "allowed" };
|
|
1560
|
+
}
|
|
1561
|
+
for (const { pattern, reason } of BLOCKED_PATTERNS) {
|
|
1562
|
+
if (pattern.test(trimmed)) {
|
|
1563
|
+
logger.warn("\uBA85\uB839\uC5B4 \uCC28\uB2E8\uB428", { command: trimmed, reason });
|
|
1564
|
+
return { allowed: false, level: "blocked", reason };
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
for (const { pattern, reason } of WARNED_PATTERNS) {
|
|
1568
|
+
if (pattern.test(trimmed)) {
|
|
1569
|
+
logger.info("\uBA85\uB839\uC5B4 \uACBD\uACE0", { command: trimmed, reason });
|
|
1570
|
+
return { allowed: true, level: "warned", reason };
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return { allowed: true, level: "allowed" };
|
|
1574
|
+
}
|
|
1265
1575
|
|
|
1266
|
-
// src/
|
|
1576
|
+
// src/security/permission-manager.ts
|
|
1267
1577
|
init_esm_shims();
|
|
1268
|
-
|
|
1578
|
+
import * as readline3 from "readline/promises";
|
|
1579
|
+
import chalk6 from "chalk";
|
|
1580
|
+
|
|
1581
|
+
// src/config/permissions.ts
|
|
1582
|
+
init_esm_shims();
|
|
1583
|
+
function parseRule(rule) {
|
|
1584
|
+
const match = rule.match(/^(\w+)(?:\((.+)\))?$/);
|
|
1585
|
+
if (match) {
|
|
1586
|
+
return { tool: match[1], pattern: match[2] };
|
|
1587
|
+
}
|
|
1588
|
+
return { tool: rule };
|
|
1589
|
+
}
|
|
1590
|
+
function matchesRule(rule, toolName, input3) {
|
|
1591
|
+
if (rule.tool !== toolName) return false;
|
|
1592
|
+
if (!rule.pattern) return true;
|
|
1593
|
+
const pattern = new RegExp(rule.pattern.replace(/\*/g, ".*"));
|
|
1594
|
+
for (const value of Object.values(input3)) {
|
|
1595
|
+
if (typeof value === "string" && pattern.test(value)) {
|
|
1596
|
+
return true;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
return false;
|
|
1600
|
+
}
|
|
1601
|
+
function evaluatePermission(toolName, input3, rules) {
|
|
1602
|
+
for (const rule of rules.deny) {
|
|
1603
|
+
if (matchesRule(parseRule(rule), toolName, input3)) {
|
|
1604
|
+
return "deny";
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
for (const rule of rules.ask) {
|
|
1608
|
+
if (matchesRule(parseRule(rule), toolName, input3)) {
|
|
1609
|
+
return "ask";
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
for (const rule of rules.allow) {
|
|
1613
|
+
if (matchesRule(parseRule(rule), toolName, input3)) {
|
|
1614
|
+
return "allow";
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
return "ask";
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/security/permission-manager.ts
|
|
1621
|
+
var sessionAllowed = /* @__PURE__ */ new Set();
|
|
1622
|
+
var sessionDenied = /* @__PURE__ */ new Set();
|
|
1623
|
+
var currentMode = "default";
|
|
1624
|
+
function setPermissionMode(mode) {
|
|
1625
|
+
currentMode = mode;
|
|
1626
|
+
}
|
|
1627
|
+
function getPermissionMode() {
|
|
1628
|
+
return currentMode;
|
|
1629
|
+
}
|
|
1630
|
+
async function checkPermission(tool, input3) {
|
|
1631
|
+
if (!tool.dangerous) return true;
|
|
1632
|
+
if (currentMode === "yolo") return true;
|
|
1633
|
+
if (currentMode === "acceptEdits" && ["write_file", "edit_file", "multi_edit"].includes(tool.name)) {
|
|
1634
|
+
return true;
|
|
1635
|
+
}
|
|
1636
|
+
if (currentMode === "plan" && !tool.readOnly) {
|
|
1637
|
+
return false;
|
|
1638
|
+
}
|
|
1639
|
+
const config = configManager.get();
|
|
1640
|
+
const decision = evaluatePermission(tool.name, input3, config.permissions);
|
|
1641
|
+
if (decision === "allow") return true;
|
|
1642
|
+
if (decision === "deny") {
|
|
1643
|
+
console.log(chalk6.red(`\u2717 Permission denied for ${tool.name} (denied by rule)`));
|
|
1644
|
+
return false;
|
|
1645
|
+
}
|
|
1646
|
+
const key = `${tool.name}:${JSON.stringify(input3)}`;
|
|
1647
|
+
if (sessionAllowed.has(tool.name)) return true;
|
|
1648
|
+
if (sessionDenied.has(tool.name)) return false;
|
|
1649
|
+
return promptUser(tool, input3);
|
|
1650
|
+
}
|
|
1651
|
+
async function promptUser(tool, input3) {
|
|
1652
|
+
console.log("");
|
|
1653
|
+
console.log(chalk6.yellow.bold(`\u26A0 Permission Required: ${tool.name}`));
|
|
1654
|
+
const relevantParams = Object.entries(input3).filter(([, v]) => v !== void 0);
|
|
1655
|
+
for (const [key, value] of relevantParams) {
|
|
1656
|
+
const displayValue = typeof value === "string" && value.length > 200 ? value.slice(0, 200) + "..." : String(value);
|
|
1657
|
+
console.log(chalk6.dim(` ${key}: `) + displayValue);
|
|
1658
|
+
}
|
|
1659
|
+
console.log("");
|
|
1660
|
+
const rl = readline3.createInterface({
|
|
1661
|
+
input: process.stdin,
|
|
1662
|
+
output: process.stdout
|
|
1663
|
+
});
|
|
1664
|
+
try {
|
|
1665
|
+
const answer = await rl.question(
|
|
1666
|
+
chalk6.yellow(`Allow? [${chalk6.bold("Y")}es / ${chalk6.bold("n")}o / ${chalk6.bold("a")}lways for this tool] `)
|
|
1667
|
+
);
|
|
1668
|
+
rl.close();
|
|
1669
|
+
const choice = answer.trim().toLowerCase();
|
|
1670
|
+
if (choice === "a" || choice === "always") {
|
|
1671
|
+
sessionAllowed.add(tool.name);
|
|
1672
|
+
return true;
|
|
1673
|
+
}
|
|
1674
|
+
if (choice === "n" || choice === "no") {
|
|
1675
|
+
return false;
|
|
1676
|
+
}
|
|
1677
|
+
return true;
|
|
1678
|
+
} catch {
|
|
1679
|
+
rl.close();
|
|
1680
|
+
return false;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// src/tools/bash.ts
|
|
1685
|
+
var _currentOnOutput = null;
|
|
1686
|
+
function setBashOutputCallback(cb) {
|
|
1687
|
+
_currentOnOutput = cb;
|
|
1688
|
+
}
|
|
1689
|
+
function getDefaultShell() {
|
|
1690
|
+
if (os5.platform() === "win32") {
|
|
1691
|
+
return "powershell.exe";
|
|
1692
|
+
}
|
|
1693
|
+
return process.env["SHELL"] || "/bin/bash";
|
|
1694
|
+
}
|
|
1695
|
+
var backgroundTasks = /* @__PURE__ */ new Map();
|
|
1696
|
+
var taskCounter = 0;
|
|
1697
|
+
var bashTool = {
|
|
1698
|
+
name: "bash",
|
|
1699
|
+
description: `Execute a shell command. Supports timeout (max 600s, default 120s) and background execution. The working directory persists between calls. Uses the platform default shell (bash on Unix, PowerShell on Windows).`,
|
|
1700
|
+
inputSchema: {
|
|
1701
|
+
type: "object",
|
|
1702
|
+
properties: {
|
|
1703
|
+
command: { type: "string", description: "The bash command to execute" },
|
|
1704
|
+
description: { type: "string", description: "Brief description of what the command does" },
|
|
1705
|
+
timeout: { type: "number", description: "Timeout in milliseconds (max 600000, default 120000)" },
|
|
1706
|
+
run_in_background: { type: "boolean", description: "Run in background and return a task ID" }
|
|
1707
|
+
},
|
|
1708
|
+
required: ["command"]
|
|
1709
|
+
},
|
|
1710
|
+
dangerous: true,
|
|
1711
|
+
readOnly: false,
|
|
1712
|
+
async execute(input3) {
|
|
1713
|
+
const command = String(input3["command"]);
|
|
1714
|
+
const timeout = Math.min(Number(input3["timeout"]) || 12e4, 6e5);
|
|
1715
|
+
const runInBackground = input3["run_in_background"] === true;
|
|
1716
|
+
if (!command.trim()) {
|
|
1717
|
+
return makeToolError("Command cannot be empty");
|
|
1718
|
+
}
|
|
1719
|
+
if (getPermissionMode() !== "yolo") {
|
|
1720
|
+
const validation = validateCommand(command);
|
|
1721
|
+
if (!validation.allowed) {
|
|
1722
|
+
return makeToolError(`\uBA85\uB839\uC5B4\uAC00 \uCC28\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${validation.reason}`);
|
|
1723
|
+
}
|
|
1724
|
+
if (validation.level === "warned") {
|
|
1725
|
+
console.log(chalk7.yellow(`\u26A0 \uACBD\uACE0: ${validation.reason}`));
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (runInBackground) {
|
|
1729
|
+
return runBackgroundTask(command);
|
|
1730
|
+
}
|
|
1731
|
+
return new Promise((resolve11) => {
|
|
1732
|
+
const finalCommand = os5.platform() === "win32" ? `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ${command}` : command;
|
|
1733
|
+
const startTime = Date.now();
|
|
1734
|
+
let stdout = "";
|
|
1735
|
+
let stderr = "";
|
|
1736
|
+
let timedOut = false;
|
|
1737
|
+
const proc = spawn(finalCommand, {
|
|
1738
|
+
shell: getDefaultShell(),
|
|
1739
|
+
cwd: process.cwd(),
|
|
1740
|
+
env: { ...process.env },
|
|
1741
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1742
|
+
});
|
|
1743
|
+
const timer = setTimeout(() => {
|
|
1744
|
+
timedOut = true;
|
|
1745
|
+
proc.kill("SIGTERM");
|
|
1746
|
+
setTimeout(() => proc.kill("SIGKILL"), 5e3);
|
|
1747
|
+
}, timeout);
|
|
1748
|
+
const onOutput = _currentOnOutput;
|
|
1749
|
+
proc.stdout?.on("data", (data) => {
|
|
1750
|
+
const chunk = data.toString();
|
|
1751
|
+
stdout += chunk;
|
|
1752
|
+
if (onOutput) {
|
|
1753
|
+
onOutput(chunk);
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1756
|
+
proc.stderr?.on("data", (data) => {
|
|
1757
|
+
const chunk = data.toString();
|
|
1758
|
+
stderr += chunk;
|
|
1759
|
+
if (onOutput) {
|
|
1760
|
+
onOutput(chunk);
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
proc.on("close", (code) => {
|
|
1764
|
+
clearTimeout(timer);
|
|
1765
|
+
const durationMs = Date.now() - startTime;
|
|
1766
|
+
const exitCode = code ?? 1;
|
|
1767
|
+
if (timedOut) {
|
|
1768
|
+
logger.info("bash \uBA85\uB839\uC5B4 \uC2E4\uD589", { command, exitCode, durationMs, timedOut: true });
|
|
1769
|
+
const output4 = [
|
|
1770
|
+
stdout ? `stdout:
|
|
1771
|
+
${stdout}` : "",
|
|
1772
|
+
stderr ? `stderr:
|
|
1773
|
+
${stderr}` : "",
|
|
1774
|
+
`Exit code: ${exitCode}`
|
|
1775
|
+
].filter(Boolean).join("\n\n");
|
|
1776
|
+
resolve11(makeToolError(`Command timed out after ${timeout / 1e3}s
|
|
1777
|
+
${output4}`));
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
if (exitCode !== 0) {
|
|
1781
|
+
logger.info("bash \uBA85\uB839\uC5B4 \uC2E4\uD589", { command, exitCode, durationMs, timedOut: false });
|
|
1782
|
+
const output4 = [
|
|
1783
|
+
stdout ? `stdout:
|
|
1784
|
+
${stdout}` : "",
|
|
1785
|
+
stderr ? `stderr:
|
|
1786
|
+
${stderr}` : "",
|
|
1787
|
+
`Exit code: ${exitCode}`
|
|
1788
|
+
].filter(Boolean).join("\n\n");
|
|
1789
|
+
resolve11(makeToolResult(output4 || `Command failed with exit code ${exitCode}`));
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
logger.info("bash \uBA85\uB839\uC5B4 \uC2E4\uD589", { command, exitCode: 0, durationMs });
|
|
1793
|
+
const output3 = [
|
|
1794
|
+
stdout ? stdout : "",
|
|
1795
|
+
stderr ? `stderr:
|
|
1796
|
+
${stderr}` : ""
|
|
1797
|
+
].filter(Boolean).join("\n");
|
|
1798
|
+
resolve11(makeToolResult(output3 || "(no output)"));
|
|
1799
|
+
});
|
|
1800
|
+
proc.on("error", (err) => {
|
|
1801
|
+
clearTimeout(timer);
|
|
1802
|
+
const durationMs = Date.now() - startTime;
|
|
1803
|
+
logger.info("bash \uBA85\uB839\uC5B4 \uC2E4\uD589 \uC2E4\uD328", { command, durationMs, error: err.message });
|
|
1804
|
+
resolve11(makeToolError(`Failed to execute command: ${err.message}`));
|
|
1805
|
+
});
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1809
|
+
function runBackgroundTask(command) {
|
|
1810
|
+
const taskId = `bg_${++taskCounter}`;
|
|
1811
|
+
const bgCommand = os5.platform() === "win32" ? `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ${command}` : command;
|
|
1812
|
+
const proc = spawn(bgCommand, {
|
|
1813
|
+
shell: getDefaultShell(),
|
|
1814
|
+
cwd: process.cwd(),
|
|
1815
|
+
env: { ...process.env },
|
|
1816
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1817
|
+
detached: false
|
|
1818
|
+
});
|
|
1819
|
+
const task = { process: proc, output: "", status: "running", exitCode: void 0 };
|
|
1820
|
+
backgroundTasks.set(taskId, task);
|
|
1821
|
+
proc.stdout?.on("data", (data) => {
|
|
1822
|
+
task.output += data.toString();
|
|
1823
|
+
});
|
|
1824
|
+
proc.stderr?.on("data", (data) => {
|
|
1825
|
+
task.output += data.toString();
|
|
1826
|
+
});
|
|
1827
|
+
proc.on("close", (code) => {
|
|
1828
|
+
task.status = code === 0 ? "done" : "error";
|
|
1829
|
+
task.exitCode = code ?? 1;
|
|
1830
|
+
});
|
|
1831
|
+
proc.on("error", (err) => {
|
|
1832
|
+
task.status = "error";
|
|
1833
|
+
task.output += `
|
|
1834
|
+
Process error: ${err.message}`;
|
|
1835
|
+
});
|
|
1836
|
+
return makeToolResult(`Background task started with ID: ${taskId}
|
|
1837
|
+
Use task_output tool to check results.`);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// src/tools/executor.ts
|
|
1841
|
+
import chalk8 from "chalk";
|
|
1842
|
+
var ToolExecutor = class {
|
|
1843
|
+
constructor(registry, options = {}) {
|
|
1844
|
+
this.registry = registry;
|
|
1845
|
+
this.options = options;
|
|
1846
|
+
}
|
|
1847
|
+
async executeOne(toolCall) {
|
|
1848
|
+
const tool = this.registry.get(toolCall.name);
|
|
1849
|
+
if (!tool) {
|
|
1850
|
+
return {
|
|
1851
|
+
toolUseId: toolCall.id,
|
|
1852
|
+
toolName: toolCall.name,
|
|
1853
|
+
result: makeToolError(`Unknown tool: ${toolCall.name}. Available tools: ${this.registry.listNames().join(", ")}`)
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
if (this.options.planMode && !tool.readOnly) {
|
|
1857
|
+
return {
|
|
1858
|
+
toolUseId: toolCall.id,
|
|
1859
|
+
toolName: toolCall.name,
|
|
1860
|
+
result: makeToolError(`Tool '${toolCall.name}' is not available in plan mode (read-only). Use only read-only tools.`)
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
if (tool.dangerous && this.options.permissionCheck) {
|
|
1864
|
+
const allowed = await this.options.permissionCheck(tool, toolCall.input);
|
|
1865
|
+
if (!allowed) {
|
|
1866
|
+
return {
|
|
1867
|
+
toolUseId: toolCall.id,
|
|
1868
|
+
toolName: toolCall.name,
|
|
1869
|
+
result: makeToolError(`Permission denied for tool: ${toolCall.name}`)
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
let input3 = toolCall.input;
|
|
1874
|
+
if (this.options.preHook) {
|
|
1875
|
+
try {
|
|
1876
|
+
const hookResult = await this.options.preHook(toolCall.name, input3);
|
|
1877
|
+
if (!hookResult.proceed) {
|
|
1878
|
+
return {
|
|
1879
|
+
toolUseId: toolCall.id,
|
|
1880
|
+
toolName: toolCall.name,
|
|
1881
|
+
result: makeToolError(`Tool execution blocked by hook for: ${toolCall.name}`)
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
if (hookResult.updatedInput) {
|
|
1885
|
+
input3 = hookResult.updatedInput;
|
|
1886
|
+
}
|
|
1887
|
+
} catch (err) {
|
|
1888
|
+
console.error(chalk8.yellow(`Hook error for ${toolCall.name}: ${err}`));
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
if (this.options.showToolCalls) {
|
|
1892
|
+
console.log(renderToolCall(toolCall.name, input3));
|
|
1893
|
+
}
|
|
1894
|
+
let result;
|
|
1895
|
+
const execStart = Date.now();
|
|
1896
|
+
let elapsedTimer = null;
|
|
1897
|
+
if (this.options.showToolCalls) {
|
|
1898
|
+
elapsedTimer = setInterval(() => {
|
|
1899
|
+
const elapsed = ((Date.now() - execStart) / 1e3).toFixed(1);
|
|
1900
|
+
updateSpinner(`${toolCall.name} (${elapsed}s...)`);
|
|
1901
|
+
}, 200);
|
|
1902
|
+
startSpinner(`${toolCall.name}...`);
|
|
1903
|
+
if (toolCall.name === "bash") {
|
|
1904
|
+
setBashOutputCallback((_chunk) => {
|
|
1905
|
+
const elapsed = ((Date.now() - execStart) / 1e3).toFixed(1);
|
|
1906
|
+
updateSpinner(`${toolCall.name} (${elapsed}s...) \u25B8 \uCD9C\uB825 \uC218\uC2E0 \uC911`);
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
try {
|
|
1911
|
+
result = await tool.execute(input3);
|
|
1912
|
+
} catch (err) {
|
|
1913
|
+
result = makeToolError(
|
|
1914
|
+
`Tool '${toolCall.name}' threw an error: ${err instanceof Error ? err.message : String(err)}`
|
|
1915
|
+
);
|
|
1916
|
+
} finally {
|
|
1917
|
+
if (elapsedTimer) {
|
|
1918
|
+
clearInterval(elapsedTimer);
|
|
1919
|
+
}
|
|
1920
|
+
if (toolCall.name === "bash") {
|
|
1921
|
+
setBashOutputCallback(null);
|
|
1922
|
+
}
|
|
1923
|
+
stopSpinner();
|
|
1924
|
+
}
|
|
1925
|
+
const execDuration = Date.now() - execStart;
|
|
1926
|
+
logger.debug("\uB3C4\uAD6C \uC2E4\uD589 \uC644\uB8CC", {
|
|
1927
|
+
tool: toolCall.name,
|
|
1928
|
+
durationMs: execDuration,
|
|
1929
|
+
success: result.success
|
|
1930
|
+
});
|
|
1931
|
+
if (this.options.showToolCalls) {
|
|
1932
|
+
console.log(renderToolResult(toolCall.name, result.output, !result.success, execDuration));
|
|
1933
|
+
}
|
|
1934
|
+
if (this.options.postHook) {
|
|
1935
|
+
try {
|
|
1936
|
+
await this.options.postHook(toolCall.name, input3, result);
|
|
1937
|
+
} catch {
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
return {
|
|
1941
|
+
toolUseId: toolCall.id,
|
|
1942
|
+
toolName: toolCall.name,
|
|
1943
|
+
result
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
async executeMany(toolCalls) {
|
|
1947
|
+
const safeCalls = [];
|
|
1948
|
+
const dangerousCalls = [];
|
|
1949
|
+
for (const tc of toolCalls) {
|
|
1950
|
+
const tool = this.registry.get(tc.name);
|
|
1951
|
+
if (tool?.dangerous) {
|
|
1952
|
+
dangerousCalls.push(tc);
|
|
1953
|
+
} else {
|
|
1954
|
+
safeCalls.push(tc);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
const safePromises = safeCalls.map((tc) => this.executeOne(tc));
|
|
1958
|
+
const dangerousResults = [];
|
|
1959
|
+
for (const tc of dangerousCalls) {
|
|
1960
|
+
const result = await this.executeOne(tc);
|
|
1961
|
+
dangerousResults.push(result);
|
|
1962
|
+
}
|
|
1963
|
+
const safeResults = await Promise.allSettled(safePromises);
|
|
1964
|
+
const results = [];
|
|
1965
|
+
for (let i = 0; i < safeResults.length; i++) {
|
|
1966
|
+
const r = safeResults[i];
|
|
1967
|
+
if (r.status === "fulfilled") {
|
|
1968
|
+
results.push(r.value);
|
|
1969
|
+
} else {
|
|
1970
|
+
results.push({
|
|
1971
|
+
toolUseId: safeCalls[i].id,
|
|
1972
|
+
toolName: safeCalls[i].name,
|
|
1973
|
+
result: makeToolError(`Tool execution failed: ${r.reason}`)
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
results.push(...dangerousResults);
|
|
1978
|
+
const orderMap = new Map(toolCalls.map((tc, i) => [tc.id, i]));
|
|
1979
|
+
results.sort((a, b) => (orderMap.get(a.toolUseId) ?? 0) - (orderMap.get(b.toolUseId) ?? 0));
|
|
1980
|
+
return results;
|
|
1981
|
+
}
|
|
1982
|
+
setOptions(options) {
|
|
1983
|
+
Object.assign(this.options, options);
|
|
1984
|
+
}
|
|
1985
|
+
};
|
|
1986
|
+
|
|
1987
|
+
// src/agent/token-tracker.ts
|
|
1988
|
+
init_esm_shims();
|
|
1989
|
+
|
|
1990
|
+
// src/llm/types.ts
|
|
1991
|
+
init_esm_shims();
|
|
1992
|
+
var MODEL_COSTS = {
|
|
1993
|
+
// Claude models
|
|
1269
1994
|
"claude-sonnet-4-20250514": { input: 3e-3, output: 0.015 },
|
|
1270
1995
|
"claude-opus-4-20250514": { input: 0.015, output: 0.075 },
|
|
1271
1996
|
"claude-haiku-3-5-20241022": { input: 8e-4, output: 4e-3 },
|
|
1997
|
+
// GPT models
|
|
1272
1998
|
"gpt-4o": { input: 25e-4, output: 0.01 },
|
|
1273
1999
|
"gpt-4o-mini": { input: 15e-5, output: 6e-4 },
|
|
1274
2000
|
"gpt-4.1": { input: 2e-3, output: 8e-3 },
|
|
1275
2001
|
"gpt-4.1-mini": { input: 4e-4, output: 16e-4 },
|
|
1276
|
-
"gpt-4.1-nano": { input: 1e-4, output: 4e-4 }
|
|
2002
|
+
"gpt-4.1-nano": { input: 1e-4, output: 4e-4 },
|
|
2003
|
+
"o1": { input: 0.015, output: 0.06 },
|
|
2004
|
+
"o1-mini": { input: 3e-3, output: 0.012 },
|
|
2005
|
+
"o3-mini": { input: 11e-4, output: 44e-4 },
|
|
2006
|
+
// Gemini models
|
|
2007
|
+
"gemini-2.5-flash": { input: 15e-5, output: 6e-4 },
|
|
2008
|
+
"gemini-2.5-pro": { input: 125e-5, output: 0.01 },
|
|
2009
|
+
"gemini-2.0-flash": { input: 1e-4, output: 4e-4 },
|
|
2010
|
+
"gemini-1.5-flash": { input: 75e-6, output: 3e-4 },
|
|
2011
|
+
"gemini-1.5-pro": { input: 125e-5, output: 5e-3 }
|
|
1277
2012
|
};
|
|
1278
2013
|
function getModelCost(model) {
|
|
2014
|
+
if (model.startsWith("ollama:") || model.startsWith("ollama/")) {
|
|
2015
|
+
return { input: 0, output: 0 };
|
|
2016
|
+
}
|
|
1279
2017
|
return MODEL_COSTS[model] ?? { input: 0, output: 0 };
|
|
1280
2018
|
}
|
|
1281
2019
|
|
|
1282
2020
|
// src/agent/token-tracker.ts
|
|
1283
2021
|
var TokenTracker = class {
|
|
2022
|
+
// Global (accumulated) counters
|
|
1284
2023
|
inputTokens = 0;
|
|
1285
2024
|
outputTokens = 0;
|
|
1286
2025
|
requests = 0;
|
|
2026
|
+
// Per-session counters
|
|
2027
|
+
sessionInputTokens = 0;
|
|
2028
|
+
sessionOutputTokens = 0;
|
|
2029
|
+
sessionRequests = 0;
|
|
2030
|
+
lastRequestCost = null;
|
|
1287
2031
|
model = "";
|
|
1288
2032
|
setModel(model) {
|
|
1289
2033
|
this.model = model;
|
|
@@ -1292,6 +2036,17 @@ var TokenTracker = class {
|
|
|
1292
2036
|
this.inputTokens += usage.input_tokens;
|
|
1293
2037
|
this.outputTokens += usage.output_tokens;
|
|
1294
2038
|
this.requests++;
|
|
2039
|
+
this.sessionInputTokens += usage.input_tokens;
|
|
2040
|
+
this.sessionOutputTokens += usage.output_tokens;
|
|
2041
|
+
this.sessionRequests++;
|
|
2042
|
+
const costs = getModelCost(this.model);
|
|
2043
|
+
const reqCost = usage.input_tokens / 1e3 * costs.input + usage.output_tokens / 1e3 * costs.output;
|
|
2044
|
+
this.lastRequestCost = {
|
|
2045
|
+
inputTokens: usage.input_tokens,
|
|
2046
|
+
outputTokens: usage.output_tokens,
|
|
2047
|
+
cost: reqCost,
|
|
2048
|
+
timestamp: Date.now()
|
|
2049
|
+
};
|
|
1295
2050
|
}
|
|
1296
2051
|
getStats() {
|
|
1297
2052
|
const costs = getModelCost(this.model);
|
|
@@ -1304,6 +2059,25 @@ var TokenTracker = class {
|
|
|
1304
2059
|
requests: this.requests
|
|
1305
2060
|
};
|
|
1306
2061
|
}
|
|
2062
|
+
getSessionStats() {
|
|
2063
|
+
const costs = getModelCost(this.model);
|
|
2064
|
+
const cost = this.sessionInputTokens / 1e3 * costs.input + this.sessionOutputTokens / 1e3 * costs.output;
|
|
2065
|
+
return {
|
|
2066
|
+
inputTokens: this.sessionInputTokens,
|
|
2067
|
+
outputTokens: this.sessionOutputTokens,
|
|
2068
|
+
totalTokens: this.sessionInputTokens + this.sessionOutputTokens,
|
|
2069
|
+
cost,
|
|
2070
|
+
requests: this.sessionRequests,
|
|
2071
|
+
lastRequestCost: this.lastRequestCost,
|
|
2072
|
+
avgCostPerRequest: this.sessionRequests > 0 ? cost / this.sessionRequests : 0
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
resetSession() {
|
|
2076
|
+
this.sessionInputTokens = 0;
|
|
2077
|
+
this.sessionOutputTokens = 0;
|
|
2078
|
+
this.sessionRequests = 0;
|
|
2079
|
+
this.lastRequestCost = null;
|
|
2080
|
+
}
|
|
1307
2081
|
getCost() {
|
|
1308
2082
|
return this.getStats().cost;
|
|
1309
2083
|
}
|
|
@@ -1333,39 +2107,9 @@ var TokenTracker = class {
|
|
|
1333
2107
|
};
|
|
1334
2108
|
var tokenTracker = new TokenTracker();
|
|
1335
2109
|
|
|
1336
|
-
// src/ui/spinner.ts
|
|
1337
|
-
init_esm_shims();
|
|
1338
|
-
import ora from "ora";
|
|
1339
|
-
import chalk6 from "chalk";
|
|
1340
|
-
var currentSpinner = null;
|
|
1341
|
-
function startSpinner(text) {
|
|
1342
|
-
stopSpinner();
|
|
1343
|
-
currentSpinner = ora({
|
|
1344
|
-
text: chalk6.dim(text),
|
|
1345
|
-
spinner: "dots",
|
|
1346
|
-
color: "cyan"
|
|
1347
|
-
}).start();
|
|
1348
|
-
return currentSpinner;
|
|
1349
|
-
}
|
|
1350
|
-
function updateSpinner(text) {
|
|
1351
|
-
if (currentSpinner) {
|
|
1352
|
-
currentSpinner.text = chalk6.dim(text);
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
function stopSpinner(symbol) {
|
|
1356
|
-
if (currentSpinner) {
|
|
1357
|
-
if (symbol) {
|
|
1358
|
-
currentSpinner.stopAndPersist({ symbol });
|
|
1359
|
-
} else {
|
|
1360
|
-
currentSpinner.stop();
|
|
1361
|
-
}
|
|
1362
|
-
currentSpinner = null;
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
2110
|
// src/agent/agent-loop.ts
|
|
1367
2111
|
init_renderer();
|
|
1368
|
-
import
|
|
2112
|
+
import chalk9 from "chalk";
|
|
1369
2113
|
var MAX_RETRIES = 3;
|
|
1370
2114
|
var RETRY_DELAYS = [1e3, 2e3, 4e3];
|
|
1371
2115
|
async function agentLoop(userMessage, options) {
|
|
@@ -1399,8 +2143,9 @@ async function agentLoop(userMessage, options) {
|
|
|
1399
2143
|
} catch (err) {
|
|
1400
2144
|
stopSpinner();
|
|
1401
2145
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2146
|
+
logger.error("LLM \uD638\uCD9C \uC2E4\uD328", { model: provider.model }, err instanceof Error ? err : new Error(errMsg));
|
|
1402
2147
|
if (showOutput) {
|
|
1403
|
-
console.error(
|
|
2148
|
+
console.error(chalk9.red(`
|
|
1404
2149
|
LLM Error: ${errMsg}`));
|
|
1405
2150
|
}
|
|
1406
2151
|
return `Error communicating with LLM: ${errMsg}`;
|
|
@@ -1414,6 +2159,13 @@ LLM Error: ${errMsg}`));
|
|
|
1414
2159
|
outputTokens: stats.outputTokens,
|
|
1415
2160
|
cost: stats.cost
|
|
1416
2161
|
});
|
|
2162
|
+
logger.debug("LLM \uC751\uB2F5 \uC218\uC2E0", {
|
|
2163
|
+
model: provider.model,
|
|
2164
|
+
inputTokens: response.usage.input_tokens,
|
|
2165
|
+
outputTokens: response.usage.output_tokens,
|
|
2166
|
+
stopReason: response.stopReason,
|
|
2167
|
+
toolCalls: response.toolCalls?.length ?? 0
|
|
2168
|
+
});
|
|
1417
2169
|
}
|
|
1418
2170
|
conversation.addAssistantMessage(response.content);
|
|
1419
2171
|
const wasStreamed = response._streamed === true;
|
|
@@ -1434,7 +2186,7 @@ LLM Error: ${errMsg}`));
|
|
|
1434
2186
|
}
|
|
1435
2187
|
if (response.stopReason === "max_tokens") {
|
|
1436
2188
|
if (showOutput) {
|
|
1437
|
-
console.log(
|
|
2189
|
+
console.log(chalk9.yellow("\n\u26A0 Response truncated (max tokens reached)"));
|
|
1438
2190
|
}
|
|
1439
2191
|
}
|
|
1440
2192
|
if (response.toolCalls && response.toolCalls.length > 0) {
|
|
@@ -1442,17 +2194,37 @@ LLM Error: ${errMsg}`));
|
|
|
1442
2194
|
console.log("");
|
|
1443
2195
|
}
|
|
1444
2196
|
const results = await executor.executeMany(response.toolCalls);
|
|
1445
|
-
const toolResults = results.map((r) =>
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
2197
|
+
const toolResults = results.map((r) => {
|
|
2198
|
+
if (r.result.metadata?.isImage && r.result.metadata.imageData) {
|
|
2199
|
+
const blocks = [
|
|
2200
|
+
{ type: "text", text: r.result.output },
|
|
2201
|
+
{
|
|
2202
|
+
type: "image",
|
|
2203
|
+
source: {
|
|
2204
|
+
type: "base64",
|
|
2205
|
+
media_type: r.result.metadata.imageMimeType || "image/png",
|
|
2206
|
+
data: r.result.metadata.imageData
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
];
|
|
2210
|
+
return {
|
|
2211
|
+
tool_use_id: r.toolUseId,
|
|
2212
|
+
content: blocks,
|
|
2213
|
+
is_error: !r.result.success
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
return {
|
|
2217
|
+
tool_use_id: r.toolUseId,
|
|
2218
|
+
content: r.result.output,
|
|
2219
|
+
is_error: !r.result.success
|
|
2220
|
+
};
|
|
2221
|
+
});
|
|
1450
2222
|
conversation.addToolResults(toolResults);
|
|
1451
2223
|
}
|
|
1452
2224
|
}
|
|
1453
2225
|
if (iterations >= maxIterations) {
|
|
1454
2226
|
if (showOutput) {
|
|
1455
|
-
console.log(
|
|
2227
|
+
console.log(chalk9.yellow(`
|
|
1456
2228
|
\u26A0 Agent loop reached maximum iterations (${maxIterations})`));
|
|
1457
2229
|
}
|
|
1458
2230
|
}
|
|
@@ -1506,12 +2278,12 @@ async function callLlmWithRetry(provider, conversation, registry, stream, option
|
|
|
1506
2278
|
throw lastError || new Error("Max retries exceeded");
|
|
1507
2279
|
}
|
|
1508
2280
|
function sleep(ms) {
|
|
1509
|
-
return new Promise((
|
|
2281
|
+
return new Promise((resolve11) => setTimeout(resolve11, ms));
|
|
1510
2282
|
}
|
|
1511
2283
|
|
|
1512
2284
|
// src/agent/system-prompt.ts
|
|
1513
2285
|
init_esm_shims();
|
|
1514
|
-
import * as
|
|
2286
|
+
import * as os6 from "os";
|
|
1515
2287
|
import { execSync as execSync2 } from "child_process";
|
|
1516
2288
|
var ROLE_DEFINITION = `You are Codi (\uCF54\uB514), a terminal-based AI coding agent. You help users with software engineering tasks including writing code, debugging, refactoring, and explaining code. You have access to tools for file manipulation, code search, shell execution, and more.
|
|
1517
2289
|
|
|
@@ -1571,7 +2343,8 @@ var TOOL_HIERARCHY = `# Tool Usage Rules
|
|
|
1571
2343
|
- Use grep instead of bash grep/rg for content search
|
|
1572
2344
|
- Reserve bash for system commands that have no dedicated tool
|
|
1573
2345
|
- Use sub_agent for complex multi-step exploration tasks
|
|
1574
|
-
- Call multiple tools in parallel when they are independent
|
|
2346
|
+
- Call multiple tools in parallel when they are independent
|
|
2347
|
+
- Use update_memory to persist important information (architecture, user preferences, patterns, decisions) across conversations. Proactively save useful context when you discover it.`;
|
|
1575
2348
|
var WINDOWS_RULES = `# Windows Shell Rules
|
|
1576
2349
|
You are running on Windows. The shell is PowerShell. Follow these rules:
|
|
1577
2350
|
- Use PowerShell syntax, NOT bash/sh syntax
|
|
@@ -1623,7 +2396,7 @@ function buildSystemPrompt(context) {
|
|
|
1623
2396
|
fragments.push(buildEnvironmentInfo(context));
|
|
1624
2397
|
fragments.push(CONVERSATION_RULES);
|
|
1625
2398
|
fragments.push(TOOL_HIERARCHY);
|
|
1626
|
-
if (
|
|
2399
|
+
if (os6.platform() === "win32") {
|
|
1627
2400
|
fragments.push(WINDOWS_RULES);
|
|
1628
2401
|
}
|
|
1629
2402
|
fragments.push(CODE_RULES);
|
|
@@ -1654,8 +2427,8 @@ function buildEnvironmentInfo(context) {
|
|
|
1654
2427
|
const lines = [
|
|
1655
2428
|
"# Environment",
|
|
1656
2429
|
`- Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
1657
|
-
`- OS: ${
|
|
1658
|
-
`- Shell: ${
|
|
2430
|
+
`- OS: ${os6.platform()} ${os6.release()}`,
|
|
2431
|
+
`- Shell: ${os6.platform() === "win32" ? "PowerShell" : process.env["SHELL"] || "/bin/bash"}`,
|
|
1659
2432
|
`- Working Directory: ${context.cwd}`,
|
|
1660
2433
|
`- Model: ${context.model}`,
|
|
1661
2434
|
`- Provider: ${context.provider}`
|
|
@@ -1687,7 +2460,7 @@ var ContextCompressor = class {
|
|
|
1687
2460
|
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
1688
2461
|
}
|
|
1689
2462
|
shouldCompress(conversation) {
|
|
1690
|
-
const estimatedTokens = conversation.estimateTokens();
|
|
2463
|
+
const estimatedTokens = conversation.estimateTokens(this.options.model);
|
|
1691
2464
|
return estimatedTokens > this.options.maxContextTokens * this.options.threshold;
|
|
1692
2465
|
}
|
|
1693
2466
|
async compress(conversation, provider, focusHint) {
|
|
@@ -1725,58 +2498,58 @@ ${summaryContent}`;
|
|
|
1725
2498
|
|
|
1726
2499
|
// src/agent/memory.ts
|
|
1727
2500
|
init_esm_shims();
|
|
1728
|
-
import * as
|
|
1729
|
-
import * as
|
|
1730
|
-
import * as
|
|
2501
|
+
import * as fs6 from "fs";
|
|
2502
|
+
import * as os7 from "os";
|
|
2503
|
+
import * as path7 from "path";
|
|
1731
2504
|
import * as crypto from "crypto";
|
|
1732
2505
|
var MemoryManager = class {
|
|
1733
2506
|
memoryDir;
|
|
1734
2507
|
constructor() {
|
|
1735
|
-
const home = process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
2508
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || os7.homedir();
|
|
1736
2509
|
const projectHash = crypto.createHash("md5").update(process.cwd()).digest("hex").slice(0, 8);
|
|
1737
|
-
const projectName =
|
|
1738
|
-
this.memoryDir =
|
|
2510
|
+
const projectName = path7.basename(process.cwd());
|
|
2511
|
+
this.memoryDir = path7.join(home, ".codi", "projects", `${projectName}-${projectHash}`, "memory");
|
|
1739
2512
|
}
|
|
1740
2513
|
ensureDir() {
|
|
1741
|
-
if (!
|
|
1742
|
-
|
|
2514
|
+
if (!fs6.existsSync(this.memoryDir)) {
|
|
2515
|
+
fs6.mkdirSync(this.memoryDir, { recursive: true });
|
|
1743
2516
|
}
|
|
1744
2517
|
}
|
|
1745
2518
|
getMemoryDir() {
|
|
1746
2519
|
return this.memoryDir;
|
|
1747
2520
|
}
|
|
1748
2521
|
loadIndex() {
|
|
1749
|
-
const indexPath =
|
|
1750
|
-
if (!
|
|
1751
|
-
const content =
|
|
2522
|
+
const indexPath = path7.join(this.memoryDir, "MEMORY.md");
|
|
2523
|
+
if (!fs6.existsSync(indexPath)) return "";
|
|
2524
|
+
const content = fs6.readFileSync(indexPath, "utf-8");
|
|
1752
2525
|
const lines = content.split("\n");
|
|
1753
2526
|
return lines.slice(0, 200).join("\n");
|
|
1754
2527
|
}
|
|
1755
2528
|
saveIndex(content) {
|
|
1756
2529
|
this.ensureDir();
|
|
1757
|
-
const indexPath =
|
|
1758
|
-
|
|
2530
|
+
const indexPath = path7.join(this.memoryDir, "MEMORY.md");
|
|
2531
|
+
fs6.writeFileSync(indexPath, content, "utf-8");
|
|
1759
2532
|
}
|
|
1760
2533
|
loadTopic(name) {
|
|
1761
|
-
const topicPath =
|
|
1762
|
-
if (!
|
|
1763
|
-
return
|
|
2534
|
+
const topicPath = path7.join(this.memoryDir, `${name}.md`);
|
|
2535
|
+
if (!fs6.existsSync(topicPath)) return null;
|
|
2536
|
+
return fs6.readFileSync(topicPath, "utf-8");
|
|
1764
2537
|
}
|
|
1765
2538
|
saveTopic(name, content) {
|
|
1766
2539
|
this.ensureDir();
|
|
1767
|
-
const topicPath =
|
|
1768
|
-
|
|
2540
|
+
const topicPath = path7.join(this.memoryDir, `${name}.md`);
|
|
2541
|
+
fs6.writeFileSync(topicPath, content, "utf-8");
|
|
1769
2542
|
}
|
|
1770
2543
|
listTopics() {
|
|
1771
|
-
if (!
|
|
1772
|
-
return
|
|
2544
|
+
if (!fs6.existsSync(this.memoryDir)) return [];
|
|
2545
|
+
return fs6.readdirSync(this.memoryDir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").map((f) => f.replace(".md", ""));
|
|
1773
2546
|
}
|
|
1774
2547
|
buildMemoryPrompt() {
|
|
1775
2548
|
const index = this.loadIndex();
|
|
1776
2549
|
if (!index) return "";
|
|
1777
2550
|
const lines = [
|
|
1778
2551
|
`You have a persistent memory directory at ${this.memoryDir}.`,
|
|
1779
|
-
"Use
|
|
2552
|
+
"Use the update_memory tool to save, delete, or list memory topics as you learn patterns.",
|
|
1780
2553
|
"",
|
|
1781
2554
|
"Current MEMORY.md:",
|
|
1782
2555
|
index
|
|
@@ -1788,25 +2561,25 @@ var memoryManager = new MemoryManager();
|
|
|
1788
2561
|
|
|
1789
2562
|
// src/agent/session.ts
|
|
1790
2563
|
init_esm_shims();
|
|
1791
|
-
import * as
|
|
1792
|
-
import * as
|
|
1793
|
-
import * as
|
|
2564
|
+
import * as fs7 from "fs";
|
|
2565
|
+
import * as os8 from "os";
|
|
2566
|
+
import * as path8 from "path";
|
|
1794
2567
|
import * as crypto2 from "crypto";
|
|
1795
2568
|
var SessionManager = class {
|
|
1796
2569
|
sessionsDir;
|
|
1797
2570
|
constructor() {
|
|
1798
|
-
const home = process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
1799
|
-
this.sessionsDir =
|
|
2571
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || os8.homedir();
|
|
2572
|
+
this.sessionsDir = path8.join(home, ".codi", "sessions");
|
|
1800
2573
|
}
|
|
1801
2574
|
ensureDir() {
|
|
1802
|
-
if (!
|
|
1803
|
-
|
|
2575
|
+
if (!fs7.existsSync(this.sessionsDir)) {
|
|
2576
|
+
fs7.mkdirSync(this.sessionsDir, { recursive: true });
|
|
1804
2577
|
}
|
|
1805
2578
|
}
|
|
1806
2579
|
save(conversation, name, model) {
|
|
1807
2580
|
this.ensureDir();
|
|
1808
2581
|
const id = name || crypto2.randomUUID().slice(0, 8);
|
|
1809
|
-
const filePath =
|
|
2582
|
+
const filePath = path8.join(this.sessionsDir, `${id}.jsonl`);
|
|
1810
2583
|
const data = conversation.serialize();
|
|
1811
2584
|
const meta = {
|
|
1812
2585
|
id,
|
|
@@ -1822,13 +2595,13 @@ var SessionManager = class {
|
|
|
1822
2595
|
JSON.stringify({ type: "system", content: data.systemPrompt }),
|
|
1823
2596
|
...data.messages.map((m) => JSON.stringify({ type: "message", ...m }))
|
|
1824
2597
|
];
|
|
1825
|
-
|
|
2598
|
+
fs7.writeFileSync(filePath, lines.join("\n") + "\n", "utf-8");
|
|
1826
2599
|
return id;
|
|
1827
2600
|
}
|
|
1828
2601
|
load(id) {
|
|
1829
|
-
const filePath =
|
|
1830
|
-
if (!
|
|
1831
|
-
const content =
|
|
2602
|
+
const filePath = path8.join(this.sessionsDir, `${id}.jsonl`);
|
|
2603
|
+
if (!fs7.existsSync(filePath)) return null;
|
|
2604
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
1832
2605
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
1833
2606
|
let meta = null;
|
|
1834
2607
|
let systemPrompt = "";
|
|
@@ -1853,12 +2626,12 @@ var SessionManager = class {
|
|
|
1853
2626
|
}
|
|
1854
2627
|
list() {
|
|
1855
2628
|
this.ensureDir();
|
|
1856
|
-
const files =
|
|
2629
|
+
const files = fs7.readdirSync(this.sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
1857
2630
|
const sessions = [];
|
|
1858
2631
|
for (const file of files) {
|
|
1859
|
-
const filePath =
|
|
2632
|
+
const filePath = path8.join(this.sessionsDir, file);
|
|
1860
2633
|
try {
|
|
1861
|
-
const firstLine =
|
|
2634
|
+
const firstLine = fs7.readFileSync(filePath, "utf-8").split("\n")[0];
|
|
1862
2635
|
if (firstLine) {
|
|
1863
2636
|
const meta = JSON.parse(firstLine);
|
|
1864
2637
|
if (meta.type === "meta") {
|
|
@@ -1876,9 +2649,9 @@ var SessionManager = class {
|
|
|
1876
2649
|
return sessions[0] ?? null;
|
|
1877
2650
|
}
|
|
1878
2651
|
delete(id) {
|
|
1879
|
-
const filePath =
|
|
1880
|
-
if (
|
|
1881
|
-
|
|
2652
|
+
const filePath = path8.join(this.sessionsDir, `${id}.jsonl`);
|
|
2653
|
+
if (fs7.existsSync(filePath)) {
|
|
2654
|
+
fs7.unlinkSync(filePath);
|
|
1882
2655
|
return true;
|
|
1883
2656
|
}
|
|
1884
2657
|
return false;
|
|
@@ -1900,25 +2673,79 @@ var sessionManager = new SessionManager();
|
|
|
1900
2673
|
|
|
1901
2674
|
// src/agent/checkpoint.ts
|
|
1902
2675
|
init_esm_shims();
|
|
2676
|
+
import * as fs8 from "fs";
|
|
2677
|
+
import * as os9 from "os";
|
|
2678
|
+
import * as path9 from "path";
|
|
2679
|
+
import * as crypto3 from "crypto";
|
|
1903
2680
|
import { execSync as execSync3 } from "child_process";
|
|
2681
|
+
var MAX_CHECKPOINTS = 20;
|
|
1904
2682
|
var CheckpointManager = class {
|
|
1905
2683
|
checkpoints = [];
|
|
1906
2684
|
nextId = 1;
|
|
1907
2685
|
isGitRepo;
|
|
1908
|
-
|
|
2686
|
+
sessionId;
|
|
2687
|
+
checkpointDir;
|
|
2688
|
+
constructor(sessionId) {
|
|
2689
|
+
this.sessionId = sessionId || crypto3.randomUUID().slice(0, 8);
|
|
2690
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || os9.homedir();
|
|
2691
|
+
this.checkpointDir = path9.join(home, ".codi", "checkpoints", this.sessionId);
|
|
1909
2692
|
try {
|
|
1910
2693
|
execSync3("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
|
|
1911
2694
|
this.isGitRepo = true;
|
|
1912
2695
|
} catch {
|
|
1913
2696
|
this.isGitRepo = false;
|
|
1914
2697
|
}
|
|
2698
|
+
this.loadFromDisk();
|
|
2699
|
+
}
|
|
2700
|
+
getSessionId() {
|
|
2701
|
+
return this.sessionId;
|
|
2702
|
+
}
|
|
2703
|
+
ensureDir() {
|
|
2704
|
+
if (!fs8.existsSync(this.checkpointDir)) {
|
|
2705
|
+
fs8.mkdirSync(this.checkpointDir, { recursive: true });
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
checkpointPath(id) {
|
|
2709
|
+
return path9.join(this.checkpointDir, `checkpoint-${id}.json`);
|
|
2710
|
+
}
|
|
2711
|
+
saveToDisk(checkpoint) {
|
|
2712
|
+
this.ensureDir();
|
|
2713
|
+
const filePath = this.checkpointPath(checkpoint.id);
|
|
2714
|
+
fs8.writeFileSync(filePath, JSON.stringify(checkpoint), "utf-8");
|
|
2715
|
+
}
|
|
2716
|
+
deleteFromDisk(id) {
|
|
2717
|
+
const filePath = this.checkpointPath(id);
|
|
2718
|
+
if (fs8.existsSync(filePath)) {
|
|
2719
|
+
fs8.unlinkSync(filePath);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
loadFromDisk() {
|
|
2723
|
+
if (!fs8.existsSync(this.checkpointDir)) return;
|
|
2724
|
+
const files = fs8.readdirSync(this.checkpointDir).filter((f) => f.startsWith("checkpoint-") && f.endsWith(".json")).sort((a, b) => {
|
|
2725
|
+
const idA = parseInt(a.replace("checkpoint-", "").replace(".json", ""), 10);
|
|
2726
|
+
const idB = parseInt(b.replace("checkpoint-", "").replace(".json", ""), 10);
|
|
2727
|
+
return idA - idB;
|
|
2728
|
+
});
|
|
2729
|
+
for (const file of files) {
|
|
2730
|
+
try {
|
|
2731
|
+
const content = fs8.readFileSync(path9.join(this.checkpointDir, file), "utf-8");
|
|
2732
|
+
const checkpoint = JSON.parse(content);
|
|
2733
|
+
this.checkpoints.push(checkpoint);
|
|
2734
|
+
if (checkpoint.id >= this.nextId) {
|
|
2735
|
+
this.nextId = checkpoint.id + 1;
|
|
2736
|
+
}
|
|
2737
|
+
} catch {
|
|
2738
|
+
continue;
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
1915
2741
|
}
|
|
1916
2742
|
create(conversation, description) {
|
|
1917
2743
|
const checkpoint = {
|
|
1918
2744
|
id: this.nextId++,
|
|
1919
2745
|
timestamp: Date.now(),
|
|
1920
2746
|
conversation: conversation.serialize(),
|
|
1921
|
-
description
|
|
2747
|
+
description,
|
|
2748
|
+
messageCount: conversation.getMessageCount()
|
|
1922
2749
|
};
|
|
1923
2750
|
if (this.isGitRepo) {
|
|
1924
2751
|
try {
|
|
@@ -1940,8 +2767,12 @@ var CheckpointManager = class {
|
|
|
1940
2767
|
}
|
|
1941
2768
|
}
|
|
1942
2769
|
this.checkpoints.push(checkpoint);
|
|
1943
|
-
|
|
1944
|
-
|
|
2770
|
+
this.saveToDisk(checkpoint);
|
|
2771
|
+
if (this.checkpoints.length > MAX_CHECKPOINTS) {
|
|
2772
|
+
const removed = this.checkpoints.splice(0, this.checkpoints.length - MAX_CHECKPOINTS);
|
|
2773
|
+
for (const cp of removed) {
|
|
2774
|
+
this.deleteFromDisk(cp.id);
|
|
2775
|
+
}
|
|
1945
2776
|
}
|
|
1946
2777
|
return checkpoint.id;
|
|
1947
2778
|
}
|
|
@@ -1961,7 +2792,10 @@ var CheckpointManager = class {
|
|
|
1961
2792
|
}
|
|
1962
2793
|
const idx = this.checkpoints.indexOf(checkpoint);
|
|
1963
2794
|
if (idx >= 0) {
|
|
1964
|
-
|
|
2795
|
+
const removed = this.checkpoints.splice(idx + 1);
|
|
2796
|
+
for (const cp of removed) {
|
|
2797
|
+
this.deleteFromDisk(cp.id);
|
|
2798
|
+
}
|
|
1965
2799
|
}
|
|
1966
2800
|
return {
|
|
1967
2801
|
conversation: Conversation.deserialize(checkpoint.conversation),
|
|
@@ -1972,54 +2806,64 @@ var CheckpointManager = class {
|
|
|
1972
2806
|
return this.checkpoints.map((cp) => ({
|
|
1973
2807
|
id: cp.id,
|
|
1974
2808
|
timestamp: cp.timestamp,
|
|
1975
|
-
description: cp.description
|
|
2809
|
+
description: cp.description,
|
|
2810
|
+
messageCount: cp.messageCount
|
|
1976
2811
|
}));
|
|
1977
2812
|
}
|
|
2813
|
+
/**
|
|
2814
|
+
* 세션 종료 시 체크포인트 파일 정리
|
|
2815
|
+
*/
|
|
2816
|
+
cleanup() {
|
|
2817
|
+
if (fs8.existsSync(this.checkpointDir)) {
|
|
2818
|
+
fs8.rmSync(this.checkpointDir, { recursive: true, force: true });
|
|
2819
|
+
}
|
|
2820
|
+
this.checkpoints = [];
|
|
2821
|
+
}
|
|
1978
2822
|
};
|
|
1979
2823
|
var checkpointManager = new CheckpointManager();
|
|
1980
2824
|
|
|
1981
2825
|
// src/agent/codi-md.ts
|
|
1982
2826
|
init_esm_shims();
|
|
1983
|
-
import * as
|
|
1984
|
-
import * as
|
|
2827
|
+
import * as fs9 from "fs";
|
|
2828
|
+
import * as path10 from "path";
|
|
1985
2829
|
function loadCodiMd() {
|
|
1986
2830
|
const fragments = [];
|
|
1987
2831
|
let dir = process.cwd();
|
|
1988
|
-
const root =
|
|
2832
|
+
const root = path10.parse(dir).root;
|
|
1989
2833
|
while (dir !== root) {
|
|
1990
2834
|
loadFromDir(dir, fragments);
|
|
1991
|
-
const parent =
|
|
2835
|
+
const parent = path10.dirname(dir);
|
|
1992
2836
|
if (parent === dir) break;
|
|
1993
2837
|
dir = parent;
|
|
1994
2838
|
}
|
|
1995
2839
|
return fragments.join("\n\n---\n\n");
|
|
1996
2840
|
}
|
|
1997
2841
|
function loadFromDir(dir, fragments) {
|
|
1998
|
-
const codiPath =
|
|
1999
|
-
if (
|
|
2842
|
+
const codiPath = path10.join(dir, "CODI.md");
|
|
2843
|
+
if (fs9.existsSync(codiPath)) {
|
|
2000
2844
|
try {
|
|
2001
|
-
let content =
|
|
2845
|
+
let content = fs9.readFileSync(codiPath, "utf-8");
|
|
2002
2846
|
content = processImports(content, dir);
|
|
2003
2847
|
fragments.push(`[CODI.md from ${dir}]
|
|
2004
2848
|
${content}`);
|
|
2005
2849
|
} catch {
|
|
2006
2850
|
}
|
|
2007
2851
|
}
|
|
2008
|
-
const localPath =
|
|
2009
|
-
if (
|
|
2852
|
+
const localPath = path10.join(dir, "CODI.local.md");
|
|
2853
|
+
if (fs9.existsSync(localPath)) {
|
|
2010
2854
|
try {
|
|
2011
|
-
const content =
|
|
2855
|
+
const content = fs9.readFileSync(localPath, "utf-8");
|
|
2012
2856
|
fragments.push(`[CODI.local.md from ${dir}]
|
|
2013
2857
|
${content}`);
|
|
2014
2858
|
} catch {
|
|
2015
2859
|
}
|
|
2016
2860
|
}
|
|
2017
|
-
const rulesDir =
|
|
2018
|
-
if (
|
|
2861
|
+
const rulesDir = path10.join(dir, ".codi", "rules");
|
|
2862
|
+
if (fs9.existsSync(rulesDir)) {
|
|
2019
2863
|
try {
|
|
2020
|
-
const files =
|
|
2864
|
+
const files = fs9.readdirSync(rulesDir).filter((f) => f.endsWith(".md")).sort();
|
|
2021
2865
|
for (const file of files) {
|
|
2022
|
-
const content =
|
|
2866
|
+
const content = fs9.readFileSync(path10.join(rulesDir, file), "utf-8");
|
|
2023
2867
|
fragments.push(`[Rule: ${file}]
|
|
2024
2868
|
${content}`);
|
|
2025
2869
|
}
|
|
@@ -2029,10 +2873,10 @@ ${content}`);
|
|
|
2029
2873
|
}
|
|
2030
2874
|
function processImports(content, baseDir) {
|
|
2031
2875
|
return content.replace(/@([\w./-]+)/g, (match, importPath) => {
|
|
2032
|
-
const resolved =
|
|
2033
|
-
if (
|
|
2876
|
+
const resolved = path10.resolve(baseDir, importPath);
|
|
2877
|
+
if (fs9.existsSync(resolved)) {
|
|
2034
2878
|
try {
|
|
2035
|
-
return
|
|
2879
|
+
return fs9.readFileSync(resolved, "utf-8");
|
|
2036
2880
|
} catch {
|
|
2037
2881
|
return match;
|
|
2038
2882
|
}
|
|
@@ -2043,12 +2887,12 @@ function processImports(content, baseDir) {
|
|
|
2043
2887
|
|
|
2044
2888
|
// src/agent/mode-manager.ts
|
|
2045
2889
|
init_esm_shims();
|
|
2046
|
-
var
|
|
2890
|
+
var currentMode2 = "execute";
|
|
2047
2891
|
function getMode() {
|
|
2048
|
-
return
|
|
2892
|
+
return currentMode2;
|
|
2049
2893
|
}
|
|
2050
2894
|
function setMode(mode) {
|
|
2051
|
-
|
|
2895
|
+
currentMode2 = mode;
|
|
2052
2896
|
}
|
|
2053
2897
|
|
|
2054
2898
|
// src/tools/registry.ts
|
|
@@ -2087,140 +2931,35 @@ var ToolRegistry = class _ToolRegistry {
|
|
|
2087
2931
|
const nameSet = new Set(options.names);
|
|
2088
2932
|
tools = tools.filter((t) => nameSet.has(t.name));
|
|
2089
2933
|
}
|
|
2090
|
-
return tools.map((t) => ({
|
|
2091
|
-
name: t.name,
|
|
2092
|
-
description: t.description,
|
|
2093
|
-
input_schema: t.inputSchema
|
|
2094
|
-
}));
|
|
2095
|
-
}
|
|
2096
|
-
clone() {
|
|
2097
|
-
const newRegistry = new _ToolRegistry();
|
|
2098
|
-
for (const [, tool] of this.tools) {
|
|
2099
|
-
newRegistry.register(tool);
|
|
2100
|
-
}
|
|
2101
|
-
return newRegistry;
|
|
2102
|
-
}
|
|
2103
|
-
subset(names) {
|
|
2104
|
-
const newRegistry = new _ToolRegistry();
|
|
2105
|
-
for (const name of names) {
|
|
2106
|
-
const tool = this.tools.get(name);
|
|
2107
|
-
if (tool) newRegistry.register(tool);
|
|
2108
|
-
}
|
|
2109
|
-
return newRegistry;
|
|
2110
|
-
}
|
|
2111
|
-
};
|
|
2112
|
-
|
|
2113
|
-
// src/security/permission-manager.ts
|
|
2114
|
-
init_esm_shims();
|
|
2115
|
-
import * as readline3 from "readline/promises";
|
|
2116
|
-
import chalk8 from "chalk";
|
|
2117
|
-
|
|
2118
|
-
// src/config/permissions.ts
|
|
2119
|
-
init_esm_shims();
|
|
2120
|
-
function parseRule(rule) {
|
|
2121
|
-
const match = rule.match(/^(\w+)(?:\((.+)\))?$/);
|
|
2122
|
-
if (match) {
|
|
2123
|
-
return { tool: match[1], pattern: match[2] };
|
|
2124
|
-
}
|
|
2125
|
-
return { tool: rule };
|
|
2126
|
-
}
|
|
2127
|
-
function matchesRule(rule, toolName, input3) {
|
|
2128
|
-
if (rule.tool !== toolName) return false;
|
|
2129
|
-
if (!rule.pattern) return true;
|
|
2130
|
-
const pattern = new RegExp(rule.pattern.replace(/\*/g, ".*"));
|
|
2131
|
-
for (const value of Object.values(input3)) {
|
|
2132
|
-
if (typeof value === "string" && pattern.test(value)) {
|
|
2133
|
-
return true;
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
return false;
|
|
2137
|
-
}
|
|
2138
|
-
function evaluatePermission(toolName, input3, rules) {
|
|
2139
|
-
for (const rule of rules.deny) {
|
|
2140
|
-
if (matchesRule(parseRule(rule), toolName, input3)) {
|
|
2141
|
-
return "deny";
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
|
-
for (const rule of rules.ask) {
|
|
2145
|
-
if (matchesRule(parseRule(rule), toolName, input3)) {
|
|
2146
|
-
return "ask";
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
for (const rule of rules.allow) {
|
|
2150
|
-
if (matchesRule(parseRule(rule), toolName, input3)) {
|
|
2151
|
-
return "allow";
|
|
2152
|
-
}
|
|
2153
|
-
}
|
|
2154
|
-
return "ask";
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
// src/security/permission-manager.ts
|
|
2158
|
-
var sessionAllowed = /* @__PURE__ */ new Set();
|
|
2159
|
-
var sessionDenied = /* @__PURE__ */ new Set();
|
|
2160
|
-
var currentMode2 = "default";
|
|
2161
|
-
function setPermissionMode(mode) {
|
|
2162
|
-
currentMode2 = mode;
|
|
2163
|
-
}
|
|
2164
|
-
async function checkPermission(tool, input3) {
|
|
2165
|
-
if (!tool.dangerous) return true;
|
|
2166
|
-
if (currentMode2 === "yolo") return true;
|
|
2167
|
-
if (currentMode2 === "acceptEdits" && ["write_file", "edit_file", "multi_edit"].includes(tool.name)) {
|
|
2168
|
-
return true;
|
|
2169
|
-
}
|
|
2170
|
-
if (currentMode2 === "plan" && !tool.readOnly) {
|
|
2171
|
-
return false;
|
|
2172
|
-
}
|
|
2173
|
-
const config = configManager.get();
|
|
2174
|
-
const decision = evaluatePermission(tool.name, input3, config.permissions);
|
|
2175
|
-
if (decision === "allow") return true;
|
|
2176
|
-
if (decision === "deny") {
|
|
2177
|
-
console.log(chalk8.red(`\u2717 Permission denied for ${tool.name} (denied by rule)`));
|
|
2178
|
-
return false;
|
|
2179
|
-
}
|
|
2180
|
-
const key = `${tool.name}:${JSON.stringify(input3)}`;
|
|
2181
|
-
if (sessionAllowed.has(tool.name)) return true;
|
|
2182
|
-
if (sessionDenied.has(tool.name)) return false;
|
|
2183
|
-
return promptUser(tool, input3);
|
|
2184
|
-
}
|
|
2185
|
-
async function promptUser(tool, input3) {
|
|
2186
|
-
console.log("");
|
|
2187
|
-
console.log(chalk8.yellow.bold(`\u26A0 Permission Required: ${tool.name}`));
|
|
2188
|
-
const relevantParams = Object.entries(input3).filter(([, v]) => v !== void 0);
|
|
2189
|
-
for (const [key, value] of relevantParams) {
|
|
2190
|
-
const displayValue = typeof value === "string" && value.length > 200 ? value.slice(0, 200) + "..." : String(value);
|
|
2191
|
-
console.log(chalk8.dim(` ${key}: `) + displayValue);
|
|
2192
|
-
}
|
|
2193
|
-
console.log("");
|
|
2194
|
-
const rl = readline3.createInterface({
|
|
2195
|
-
input: process.stdin,
|
|
2196
|
-
output: process.stdout
|
|
2197
|
-
});
|
|
2198
|
-
try {
|
|
2199
|
-
const answer = await rl.question(
|
|
2200
|
-
chalk8.yellow(`Allow? [${chalk8.bold("Y")}es / ${chalk8.bold("n")}o / ${chalk8.bold("a")}lways for this tool] `)
|
|
2201
|
-
);
|
|
2202
|
-
rl.close();
|
|
2203
|
-
const choice = answer.trim().toLowerCase();
|
|
2204
|
-
if (choice === "a" || choice === "always") {
|
|
2205
|
-
sessionAllowed.add(tool.name);
|
|
2206
|
-
return true;
|
|
2934
|
+
return tools.map((t) => ({
|
|
2935
|
+
name: t.name,
|
|
2936
|
+
description: t.description,
|
|
2937
|
+
input_schema: t.inputSchema
|
|
2938
|
+
}));
|
|
2939
|
+
}
|
|
2940
|
+
clone() {
|
|
2941
|
+
const newRegistry = new _ToolRegistry();
|
|
2942
|
+
for (const [, tool] of this.tools) {
|
|
2943
|
+
newRegistry.register(tool);
|
|
2207
2944
|
}
|
|
2208
|
-
|
|
2209
|
-
|
|
2945
|
+
return newRegistry;
|
|
2946
|
+
}
|
|
2947
|
+
subset(names) {
|
|
2948
|
+
const newRegistry = new _ToolRegistry();
|
|
2949
|
+
for (const name of names) {
|
|
2950
|
+
const tool = this.tools.get(name);
|
|
2951
|
+
if (tool) newRegistry.register(tool);
|
|
2210
2952
|
}
|
|
2211
|
-
return
|
|
2212
|
-
} catch {
|
|
2213
|
-
rl.close();
|
|
2214
|
-
return false;
|
|
2953
|
+
return newRegistry;
|
|
2215
2954
|
}
|
|
2216
|
-
}
|
|
2955
|
+
};
|
|
2217
2956
|
|
|
2218
2957
|
// src/hooks/hook-manager.ts
|
|
2219
2958
|
init_esm_shims();
|
|
2220
2959
|
import { exec } from "child_process";
|
|
2221
|
-
import * as
|
|
2222
|
-
function
|
|
2223
|
-
if (
|
|
2960
|
+
import * as os10 from "os";
|
|
2961
|
+
function getDefaultShell2() {
|
|
2962
|
+
if (os10.platform() === "win32") {
|
|
2224
2963
|
return "powershell.exe";
|
|
2225
2964
|
}
|
|
2226
2965
|
return void 0;
|
|
@@ -2250,41 +2989,41 @@ var HookManager = class {
|
|
|
2250
2989
|
return { proceed: true };
|
|
2251
2990
|
}
|
|
2252
2991
|
async runCommandHook(command, context, timeout) {
|
|
2253
|
-
return new Promise((
|
|
2992
|
+
return new Promise((resolve11) => {
|
|
2254
2993
|
const stdinData = JSON.stringify({
|
|
2255
2994
|
tool: context["tool"],
|
|
2256
2995
|
args: context["args"],
|
|
2257
2996
|
session_id: context["sessionId"],
|
|
2258
2997
|
cwd: context["cwd"] || process.cwd()
|
|
2259
2998
|
});
|
|
2260
|
-
const isWin =
|
|
2999
|
+
const isWin = os10.platform() === "win32";
|
|
2261
3000
|
const finalCommand = isWin ? `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ${command}` : command;
|
|
2262
3001
|
const proc = exec(finalCommand, {
|
|
2263
3002
|
timeout: timeout || 5e3,
|
|
2264
3003
|
cwd: process.cwd(),
|
|
2265
3004
|
env: { ...process.env },
|
|
2266
|
-
shell:
|
|
3005
|
+
shell: getDefaultShell2()
|
|
2267
3006
|
}, (err, stdout, stderr) => {
|
|
2268
3007
|
if (err) {
|
|
2269
3008
|
if (err.code === 2) {
|
|
2270
|
-
|
|
3009
|
+
resolve11({
|
|
2271
3010
|
proceed: false,
|
|
2272
3011
|
reason: stderr || stdout || "Blocked by hook"
|
|
2273
3012
|
});
|
|
2274
3013
|
return;
|
|
2275
3014
|
}
|
|
2276
|
-
|
|
3015
|
+
resolve11({ proceed: true });
|
|
2277
3016
|
return;
|
|
2278
3017
|
}
|
|
2279
3018
|
try {
|
|
2280
3019
|
const output3 = JSON.parse(stdout);
|
|
2281
|
-
|
|
3020
|
+
resolve11({
|
|
2282
3021
|
proceed: output3.decision !== "block",
|
|
2283
3022
|
reason: output3.reason,
|
|
2284
3023
|
updatedInput: output3.updatedInput
|
|
2285
3024
|
});
|
|
2286
3025
|
} catch {
|
|
2287
|
-
|
|
3026
|
+
resolve11({ proceed: true });
|
|
2288
3027
|
}
|
|
2289
3028
|
});
|
|
2290
3029
|
if (proc.stdin) {
|
|
@@ -2299,12 +3038,12 @@ var hookManager = new HookManager();
|
|
|
2299
3038
|
// src/mcp/mcp-manager.ts
|
|
2300
3039
|
init_esm_shims();
|
|
2301
3040
|
init_tool();
|
|
2302
|
-
import * as
|
|
2303
|
-
import * as
|
|
2304
|
-
import * as
|
|
3041
|
+
import * as fs10 from "fs";
|
|
3042
|
+
import * as os11 from "os";
|
|
3043
|
+
import * as path11 from "path";
|
|
2305
3044
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2306
3045
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
2307
|
-
import
|
|
3046
|
+
import chalk10 from "chalk";
|
|
2308
3047
|
var McpManager = class {
|
|
2309
3048
|
servers = /* @__PURE__ */ new Map();
|
|
2310
3049
|
async initialize(registry) {
|
|
@@ -2315,21 +3054,21 @@ var McpManager = class {
|
|
|
2315
3054
|
try {
|
|
2316
3055
|
await this.connectServer(name, serverConfig, registry);
|
|
2317
3056
|
} catch (err) {
|
|
2318
|
-
console.error(
|
|
3057
|
+
console.error(chalk10.yellow(` \u26A0 Failed to connect MCP server '${name}': ${err instanceof Error ? err.message : String(err)}`));
|
|
2319
3058
|
}
|
|
2320
3059
|
}
|
|
2321
3060
|
}
|
|
2322
3061
|
loadMcpConfigs() {
|
|
2323
3062
|
const configs = {};
|
|
2324
|
-
const home = process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
3063
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || os11.homedir();
|
|
2325
3064
|
const paths = [
|
|
2326
|
-
|
|
2327
|
-
|
|
3065
|
+
path11.join(home, ".codi", "mcp.json"),
|
|
3066
|
+
path11.join(process.cwd(), ".codi", "mcp.json")
|
|
2328
3067
|
];
|
|
2329
3068
|
for (const p of paths) {
|
|
2330
3069
|
try {
|
|
2331
|
-
if (
|
|
2332
|
-
const content = JSON.parse(
|
|
3070
|
+
if (fs10.existsSync(p)) {
|
|
3071
|
+
const content = JSON.parse(fs10.readFileSync(p, "utf-8"));
|
|
2333
3072
|
if (content.mcpServers) {
|
|
2334
3073
|
Object.assign(configs, content.mcpServers);
|
|
2335
3074
|
}
|
|
@@ -2381,7 +3120,7 @@ var McpManager = class {
|
|
|
2381
3120
|
registry.register(tool);
|
|
2382
3121
|
}
|
|
2383
3122
|
this.servers.set(name, { name, client, transport, tools: toolNames });
|
|
2384
|
-
console.log(
|
|
3123
|
+
console.log(chalk10.dim(` \u2713 MCP server '${name}' connected (${toolNames.length} tools)`));
|
|
2385
3124
|
}
|
|
2386
3125
|
async disconnect(name) {
|
|
2387
3126
|
const server = this.servers.get(name);
|
|
@@ -2412,7 +3151,7 @@ var mcpManager = new McpManager();
|
|
|
2412
3151
|
// src/agent/sub-agent.ts
|
|
2413
3152
|
init_esm_shims();
|
|
2414
3153
|
init_tool();
|
|
2415
|
-
import
|
|
3154
|
+
import chalk11 from "chalk";
|
|
2416
3155
|
var AGENT_PRESETS = {
|
|
2417
3156
|
explore: {
|
|
2418
3157
|
tools: ["read_file", "glob", "grep", "list_dir"],
|
|
@@ -2451,7 +3190,7 @@ function createSubAgentHandler(provider, mainRegistry) {
|
|
|
2451
3190
|
}
|
|
2452
3191
|
const conversation = new Conversation();
|
|
2453
3192
|
const maxIterations = input3["maxIterations"] || preset.maxIterations;
|
|
2454
|
-
console.log(
|
|
3193
|
+
console.log(chalk11.dim(`
|
|
2455
3194
|
\u25B8 Launching ${type} agent: ${task.slice(0, 80)}...`));
|
|
2456
3195
|
const runAgent = async () => {
|
|
2457
3196
|
const result = await agentLoop(task, {
|
|
@@ -2480,7 +3219,7 @@ function createSubAgentHandler(provider, mainRegistry) {
|
|
|
2480
3219
|
}
|
|
2481
3220
|
try {
|
|
2482
3221
|
const result = await runAgent();
|
|
2483
|
-
console.log(
|
|
3222
|
+
console.log(chalk11.dim(` \u25B8 ${type} agent completed.`));
|
|
2484
3223
|
return makeToolResult(result);
|
|
2485
3224
|
} catch (err) {
|
|
2486
3225
|
return makeToolError(`Sub-agent failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -2524,10 +3263,10 @@ var subAgentTool = {
|
|
|
2524
3263
|
|
|
2525
3264
|
// src/config/slash-commands.ts
|
|
2526
3265
|
init_esm_shims();
|
|
2527
|
-
import * as
|
|
2528
|
-
import * as
|
|
2529
|
-
import * as
|
|
2530
|
-
import
|
|
3266
|
+
import * as fs12 from "fs";
|
|
3267
|
+
import * as os13 from "os";
|
|
3268
|
+
import * as path13 from "path";
|
|
3269
|
+
import chalk12 from "chalk";
|
|
2531
3270
|
function createBuiltinCommands() {
|
|
2532
3271
|
return [
|
|
2533
3272
|
{
|
|
@@ -2535,14 +3274,14 @@ function createBuiltinCommands() {
|
|
|
2535
3274
|
description: "Show available commands",
|
|
2536
3275
|
handler: async () => {
|
|
2537
3276
|
const commands = createBuiltinCommands();
|
|
2538
|
-
console.log(
|
|
3277
|
+
console.log(chalk12.bold("\nAvailable Commands:\n"));
|
|
2539
3278
|
for (const cmd of commands) {
|
|
2540
|
-
const aliases = cmd.aliases ?
|
|
2541
|
-
console.log(` ${
|
|
3279
|
+
const aliases = cmd.aliases ? chalk12.dim(` (${cmd.aliases.join(", ")})`) : "";
|
|
3280
|
+
console.log(` ${chalk12.cyan(cmd.name)}${aliases} - ${cmd.description}`);
|
|
2542
3281
|
}
|
|
2543
3282
|
console.log("");
|
|
2544
|
-
console.log(
|
|
2545
|
-
console.log(
|
|
3283
|
+
console.log(chalk12.dim(" Prefixes: ! (bash), @ (file reference)"));
|
|
3284
|
+
console.log(chalk12.dim(" Use \\ at end of line for multiline input"));
|
|
2546
3285
|
console.log("");
|
|
2547
3286
|
return true;
|
|
2548
3287
|
}
|
|
@@ -2555,7 +3294,7 @@ function createBuiltinCommands() {
|
|
|
2555
3294
|
if (ctx.exitFn) {
|
|
2556
3295
|
await ctx.exitFn();
|
|
2557
3296
|
}
|
|
2558
|
-
console.log(
|
|
3297
|
+
console.log(chalk12.dim("\nGoodbye!\n"));
|
|
2559
3298
|
process.exit(0);
|
|
2560
3299
|
}
|
|
2561
3300
|
},
|
|
@@ -2566,7 +3305,7 @@ function createBuiltinCommands() {
|
|
|
2566
3305
|
handler: async (_args, ctx) => {
|
|
2567
3306
|
ctx.conversation.clear();
|
|
2568
3307
|
ctx.reloadSystemPrompt();
|
|
2569
|
-
console.log(
|
|
3308
|
+
console.log(chalk12.green("\u2713 Conversation cleared"));
|
|
2570
3309
|
return true;
|
|
2571
3310
|
}
|
|
2572
3311
|
},
|
|
@@ -2576,7 +3315,7 @@ function createBuiltinCommands() {
|
|
|
2576
3315
|
handler: async (args, ctx) => {
|
|
2577
3316
|
if (!args) {
|
|
2578
3317
|
const info = statusLine.getInfo();
|
|
2579
|
-
console.log(
|
|
3318
|
+
console.log(chalk12.cyan(`Current model: ${info.model} (${info.provider})`));
|
|
2580
3319
|
return true;
|
|
2581
3320
|
}
|
|
2582
3321
|
if (args.includes(":")) {
|
|
@@ -2585,7 +3324,7 @@ function createBuiltinCommands() {
|
|
|
2585
3324
|
} else {
|
|
2586
3325
|
ctx.setProvider("", args);
|
|
2587
3326
|
}
|
|
2588
|
-
console.log(
|
|
3327
|
+
console.log(chalk12.green(`\u2713 Model switched to: ${args}`));
|
|
2589
3328
|
return true;
|
|
2590
3329
|
}
|
|
2591
3330
|
},
|
|
@@ -2593,10 +3332,10 @@ function createBuiltinCommands() {
|
|
|
2593
3332
|
name: "/compact",
|
|
2594
3333
|
description: "Compress conversation history (optional: focus hint)",
|
|
2595
3334
|
handler: async (args, ctx) => {
|
|
2596
|
-
console.log(
|
|
3335
|
+
console.log(chalk12.dim("Compressing conversation..."));
|
|
2597
3336
|
await ctx.compressor.compress(ctx.conversation, ctx.provider, args || void 0);
|
|
2598
3337
|
ctx.reloadSystemPrompt();
|
|
2599
|
-
console.log(
|
|
3338
|
+
console.log(chalk12.green("\u2713 Conversation compressed"));
|
|
2600
3339
|
return true;
|
|
2601
3340
|
}
|
|
2602
3341
|
},
|
|
@@ -2604,9 +3343,31 @@ function createBuiltinCommands() {
|
|
|
2604
3343
|
name: "/cost",
|
|
2605
3344
|
description: "Show token usage and cost",
|
|
2606
3345
|
handler: async () => {
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
3346
|
+
const session = tokenTracker.getSessionStats();
|
|
3347
|
+
const total = tokenTracker.getStats();
|
|
3348
|
+
console.log(chalk12.bold("\nToken Usage & Cost:\n"));
|
|
3349
|
+
console.log(chalk12.cyan(" [Current Session]"));
|
|
3350
|
+
console.log(` Requests: ${session.requests}`);
|
|
3351
|
+
console.log(` Input: ${formatTokens(session.inputTokens)}`);
|
|
3352
|
+
console.log(` Output: ${formatTokens(session.outputTokens)}`);
|
|
3353
|
+
console.log(` Total: ${formatTokens(session.totalTokens)}`);
|
|
3354
|
+
console.log(` Cost: $${session.cost.toFixed(4)}`);
|
|
3355
|
+
if (session.requests > 0) {
|
|
3356
|
+
console.log(` Avg/Request: $${session.avgCostPerRequest.toFixed(4)}`);
|
|
3357
|
+
}
|
|
3358
|
+
if (session.lastRequestCost) {
|
|
3359
|
+
console.log(chalk12.dim(` Last Request: $${session.lastRequestCost.cost.toFixed(4)} (${formatTokens(session.lastRequestCost.inputTokens)} in / ${formatTokens(session.lastRequestCost.outputTokens)} out)`));
|
|
3360
|
+
}
|
|
3361
|
+
if (total.requests !== session.requests) {
|
|
3362
|
+
console.log("");
|
|
3363
|
+
console.log(chalk12.cyan(" [Total Accumulated]"));
|
|
3364
|
+
console.log(` Requests: ${total.requests}`);
|
|
3365
|
+
console.log(` Input: ${formatTokens(total.inputTokens)}`);
|
|
3366
|
+
console.log(` Output: ${formatTokens(total.outputTokens)}`);
|
|
3367
|
+
console.log(` Total: ${formatTokens(total.totalTokens)}`);
|
|
3368
|
+
console.log(` Cost: $${total.cost.toFixed(4)}`);
|
|
3369
|
+
}
|
|
3370
|
+
console.log("");
|
|
2610
3371
|
return true;
|
|
2611
3372
|
}
|
|
2612
3373
|
},
|
|
@@ -2615,9 +3376,9 @@ ${tokenTracker.format()}
|
|
|
2615
3376
|
description: "Show current configuration",
|
|
2616
3377
|
handler: async () => {
|
|
2617
3378
|
const config = configManager.get();
|
|
2618
|
-
console.log(
|
|
2619
|
-
console.log(
|
|
2620
|
-
console.log(
|
|
3379
|
+
console.log(chalk12.bold("\nConfiguration:\n"));
|
|
3380
|
+
console.log(chalk12.dim(JSON.stringify(config, null, 2)));
|
|
3381
|
+
console.log(chalk12.dim(`
|
|
2621
3382
|
Config files: ${configManager.getConfigPaths().join(", ") || "(none)"}`));
|
|
2622
3383
|
console.log("");
|
|
2623
3384
|
return true;
|
|
@@ -2628,10 +3389,10 @@ Config files: ${configManager.getConfigPaths().join(", ") || "(none)"}`));
|
|
|
2628
3389
|
description: "Show permission rules",
|
|
2629
3390
|
handler: async () => {
|
|
2630
3391
|
const config = configManager.get();
|
|
2631
|
-
console.log(
|
|
2632
|
-
console.log(
|
|
2633
|
-
console.log(
|
|
2634
|
-
console.log(
|
|
3392
|
+
console.log(chalk12.bold("\nPermission Rules:\n"));
|
|
3393
|
+
console.log(chalk12.green(" Allow: ") + config.permissions.allow.join(", "));
|
|
3394
|
+
console.log(chalk12.red(" Deny: ") + (config.permissions.deny.join(", ") || "(none)"));
|
|
3395
|
+
console.log(chalk12.yellow(" Ask: ") + config.permissions.ask.join(", "));
|
|
2635
3396
|
console.log("");
|
|
2636
3397
|
return true;
|
|
2637
3398
|
}
|
|
@@ -2642,7 +3403,7 @@ Config files: ${configManager.getConfigPaths().join(", ") || "(none)"}`));
|
|
|
2642
3403
|
handler: async (args, ctx) => {
|
|
2643
3404
|
const name = args || void 0;
|
|
2644
3405
|
const id = sessionManager.save(ctx.conversation, name, statusLine.getInfo().model);
|
|
2645
|
-
console.log(
|
|
3406
|
+
console.log(chalk12.green(`\u2713 Session saved: ${id}`));
|
|
2646
3407
|
return true;
|
|
2647
3408
|
}
|
|
2648
3409
|
},
|
|
@@ -2653,12 +3414,12 @@ Config files: ${configManager.getConfigPaths().join(", ") || "(none)"}`));
|
|
|
2653
3414
|
handler: async (args, ctx) => {
|
|
2654
3415
|
const id = args || sessionManager.getLatest()?.id;
|
|
2655
3416
|
if (!id) {
|
|
2656
|
-
console.log(
|
|
3417
|
+
console.log(chalk12.yellow("No sessions found. Use /save to save a session first."));
|
|
2657
3418
|
return true;
|
|
2658
3419
|
}
|
|
2659
3420
|
const session = sessionManager.load(id);
|
|
2660
3421
|
if (!session) {
|
|
2661
|
-
console.log(
|
|
3422
|
+
console.log(chalk12.red(`Session not found: ${id}`));
|
|
2662
3423
|
return true;
|
|
2663
3424
|
}
|
|
2664
3425
|
const data = session.conversation.serialize();
|
|
@@ -2668,7 +3429,7 @@ Config files: ${configManager.getConfigPaths().join(", ") || "(none)"}`));
|
|
|
2668
3429
|
if (msg.role === "user") ctx.conversation.addUserMessage(msg.content);
|
|
2669
3430
|
else if (msg.role === "assistant") ctx.conversation.addAssistantMessage(msg.content);
|
|
2670
3431
|
}
|
|
2671
|
-
console.log(
|
|
3432
|
+
console.log(chalk12.green(`\u2713 Resumed session: ${id} (${session.meta.messageCount} messages)`));
|
|
2672
3433
|
return true;
|
|
2673
3434
|
}
|
|
2674
3435
|
},
|
|
@@ -2678,7 +3439,7 @@ Config files: ${configManager.getConfigPaths().join(", ") || "(none)"}`));
|
|
|
2678
3439
|
handler: async (args, ctx) => {
|
|
2679
3440
|
const name = args || `fork-${Date.now()}`;
|
|
2680
3441
|
const id = sessionManager.save(ctx.conversation, name, statusLine.getInfo().model);
|
|
2681
|
-
console.log(
|
|
3442
|
+
console.log(chalk12.green(`\u2713 Conversation forked: ${id}`));
|
|
2682
3443
|
return true;
|
|
2683
3444
|
}
|
|
2684
3445
|
},
|
|
@@ -2690,7 +3451,7 @@ Config files: ${configManager.getConfigPaths().join(", ") || "(none)"}`));
|
|
|
2690
3451
|
const newMode = current === "plan" ? "execute" : "plan";
|
|
2691
3452
|
setMode(newMode);
|
|
2692
3453
|
statusLine.update({ mode: newMode });
|
|
2693
|
-
console.log(
|
|
3454
|
+
console.log(chalk12.cyan(`Mode: ${newMode === "plan" ? "PLAN (read-only)" : "EXECUTE"}`));
|
|
2694
3455
|
return true;
|
|
2695
3456
|
}
|
|
2696
3457
|
},
|
|
@@ -2700,16 +3461,16 @@ Config files: ${configManager.getConfigPaths().join(", ") || "(none)"}`));
|
|
|
2700
3461
|
handler: async () => {
|
|
2701
3462
|
const index = memoryManager.loadIndex();
|
|
2702
3463
|
const topics = memoryManager.listTopics();
|
|
2703
|
-
console.log(
|
|
2704
|
-
console.log(
|
|
3464
|
+
console.log(chalk12.bold("\nAuto Memory:\n"));
|
|
3465
|
+
console.log(chalk12.dim(`Directory: ${memoryManager.getMemoryDir()}`));
|
|
2705
3466
|
if (index) {
|
|
2706
|
-
console.log(
|
|
3467
|
+
console.log(chalk12.dim("\nMEMORY.md:"));
|
|
2707
3468
|
console.log(index);
|
|
2708
3469
|
} else {
|
|
2709
|
-
console.log(
|
|
3470
|
+
console.log(chalk12.dim("\nNo memory saved yet."));
|
|
2710
3471
|
}
|
|
2711
3472
|
if (topics.length > 0) {
|
|
2712
|
-
console.log(
|
|
3473
|
+
console.log(chalk12.dim(`
|
|
2713
3474
|
Topics: ${topics.join(", ")}`));
|
|
2714
3475
|
}
|
|
2715
3476
|
console.log("");
|
|
@@ -2720,12 +3481,12 @@ Topics: ${topics.join(", ")}`));
|
|
|
2720
3481
|
name: "/init",
|
|
2721
3482
|
description: "Initialize CODI.md in the current project",
|
|
2722
3483
|
handler: async () => {
|
|
2723
|
-
const codiPath =
|
|
2724
|
-
if (
|
|
2725
|
-
console.log(
|
|
3484
|
+
const codiPath = path13.join(process.cwd(), "CODI.md");
|
|
3485
|
+
if (fs12.existsSync(codiPath)) {
|
|
3486
|
+
console.log(chalk12.yellow("CODI.md already exists"));
|
|
2726
3487
|
return true;
|
|
2727
3488
|
}
|
|
2728
|
-
const content = `# Project: ${
|
|
3489
|
+
const content = `# Project: ${path13.basename(process.cwd())}
|
|
2729
3490
|
|
|
2730
3491
|
## Overview
|
|
2731
3492
|
<!-- Describe your project here -->
|
|
@@ -2739,8 +3500,8 @@ Topics: ${topics.join(", ")}`));
|
|
|
2739
3500
|
## Conventions
|
|
2740
3501
|
<!-- Code style, naming conventions, etc. -->
|
|
2741
3502
|
`;
|
|
2742
|
-
|
|
2743
|
-
console.log(
|
|
3503
|
+
fs12.writeFileSync(codiPath, content, "utf-8");
|
|
3504
|
+
console.log(chalk12.green("\u2713 Created CODI.md"));
|
|
2744
3505
|
return true;
|
|
2745
3506
|
}
|
|
2746
3507
|
},
|
|
@@ -2768,8 +3529,8 @@ ${content}
|
|
|
2768
3529
|
|
|
2769
3530
|
`;
|
|
2770
3531
|
}
|
|
2771
|
-
|
|
2772
|
-
console.log(
|
|
3532
|
+
fs12.writeFileSync(filePath, md, "utf-8");
|
|
3533
|
+
console.log(chalk12.green(`\u2713 Exported to ${filePath}`));
|
|
2773
3534
|
return true;
|
|
2774
3535
|
}
|
|
2775
3536
|
},
|
|
@@ -2780,12 +3541,12 @@ ${content}
|
|
|
2780
3541
|
const { taskManager: taskManager2 } = await Promise.resolve().then(() => (init_task_tools(), task_tools_exports));
|
|
2781
3542
|
const tasks = taskManager2.list();
|
|
2782
3543
|
if (tasks.length === 0) {
|
|
2783
|
-
console.log(
|
|
3544
|
+
console.log(chalk12.dim("\nNo tasks.\n"));
|
|
2784
3545
|
return true;
|
|
2785
3546
|
}
|
|
2786
|
-
console.log(
|
|
3547
|
+
console.log(chalk12.bold("\nTasks:\n"));
|
|
2787
3548
|
for (const task of tasks) {
|
|
2788
|
-
const statusIcon = task.status === "completed" ?
|
|
3549
|
+
const statusIcon = task.status === "completed" ? chalk12.green("\u2713") : task.status === "in_progress" ? chalk12.yellow("\u27F3") : chalk12.dim("\u25CB");
|
|
2789
3550
|
console.log(` ${statusIcon} #${task.id} ${task.subject} [${task.status}]`);
|
|
2790
3551
|
}
|
|
2791
3552
|
console.log("");
|
|
@@ -2800,7 +3561,7 @@ ${content}
|
|
|
2800
3561
|
const info = statusLine.getInfo();
|
|
2801
3562
|
const stats = tokenTracker.getStats();
|
|
2802
3563
|
const mcpServers = mcpManager.listServers();
|
|
2803
|
-
console.log(
|
|
3564
|
+
console.log(chalk12.bold("\nCodi Status:\n"));
|
|
2804
3565
|
console.log(` Version: 0.1.0`);
|
|
2805
3566
|
console.log(` Model: ${info.model}`);
|
|
2806
3567
|
console.log(` Provider: ${config.provider}`);
|
|
@@ -2822,7 +3583,7 @@ ${content}
|
|
|
2822
3583
|
const max = 2e5;
|
|
2823
3584
|
const pct = Math.round(estimated / max * 100);
|
|
2824
3585
|
const bar = "\u2588".repeat(Math.round(pct / 5)) + "\u2591".repeat(20 - Math.round(pct / 5));
|
|
2825
|
-
console.log(
|
|
3586
|
+
console.log(chalk12.bold("\nContext Window:\n"));
|
|
2826
3587
|
console.log(` ${bar} ${pct}%`);
|
|
2827
3588
|
console.log(` ~${estimated.toLocaleString()} / ${max.toLocaleString()} tokens`);
|
|
2828
3589
|
console.log(` Messages: ${ctx.conversation.getMessageCount()}`);
|
|
@@ -2836,12 +3597,12 @@ ${content}
|
|
|
2836
3597
|
handler: async (_args, ctx) => {
|
|
2837
3598
|
const checkpoints = checkpointManager.list();
|
|
2838
3599
|
if (checkpoints.length === 0) {
|
|
2839
|
-
console.log(
|
|
3600
|
+
console.log(chalk12.yellow("No checkpoints available."));
|
|
2840
3601
|
return true;
|
|
2841
3602
|
}
|
|
2842
3603
|
const result = checkpointManager.rewind();
|
|
2843
3604
|
if (!result) {
|
|
2844
|
-
console.log(
|
|
3605
|
+
console.log(chalk12.yellow("No checkpoint to rewind to."));
|
|
2845
3606
|
return true;
|
|
2846
3607
|
}
|
|
2847
3608
|
const data = result.conversation.serialize();
|
|
@@ -2851,7 +3612,7 @@ ${content}
|
|
|
2851
3612
|
if (msg.role === "user") ctx.conversation.addUserMessage(msg.content);
|
|
2852
3613
|
else if (msg.role === "assistant") ctx.conversation.addAssistantMessage(msg.content);
|
|
2853
3614
|
}
|
|
2854
|
-
console.log(
|
|
3615
|
+
console.log(chalk12.green(`\u2713 Rewound to checkpoint${result.description ? `: ${result.description}` : ""}`));
|
|
2855
3616
|
return true;
|
|
2856
3617
|
}
|
|
2857
3618
|
},
|
|
@@ -2863,13 +3624,13 @@ ${content}
|
|
|
2863
3624
|
const { execSync: execSync5 } = await import("child_process");
|
|
2864
3625
|
const diff = execSync5("git diff", { encoding: "utf-8", cwd: process.cwd() });
|
|
2865
3626
|
if (!diff.trim()) {
|
|
2866
|
-
console.log(
|
|
3627
|
+
console.log(chalk12.dim("\nNo changes.\n"));
|
|
2867
3628
|
} else {
|
|
2868
3629
|
const { renderDiff: renderDiff2 } = await Promise.resolve().then(() => (init_renderer(), renderer_exports));
|
|
2869
3630
|
console.log("\n" + renderDiff2("", "", diff) + "\n");
|
|
2870
3631
|
}
|
|
2871
3632
|
} catch {
|
|
2872
|
-
console.log(
|
|
3633
|
+
console.log(chalk12.yellow("Not a git repository or git not available."));
|
|
2873
3634
|
}
|
|
2874
3635
|
return true;
|
|
2875
3636
|
}
|
|
@@ -2882,21 +3643,64 @@ ${content}
|
|
|
2882
3643
|
try {
|
|
2883
3644
|
const staged = execSync5("git diff --cached", { encoding: "utf-8", cwd: process.cwd() });
|
|
2884
3645
|
const unstaged = execSync5("git diff", { encoding: "utf-8", cwd: process.cwd() });
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
3646
|
+
if (!staged.trim() && unstaged.trim()) {
|
|
3647
|
+
const untracked = execSync5("git ls-files --others --exclude-standard", {
|
|
3648
|
+
encoding: "utf-8",
|
|
3649
|
+
cwd: process.cwd()
|
|
3650
|
+
}).trim();
|
|
3651
|
+
if (untracked) {
|
|
3652
|
+
console.log(chalk12.yellow(`
|
|
3653
|
+
\uC8FC\uC758: \uCD94\uC801\uB418\uC9C0 \uC54A\uB294 \uD30C\uC77C\uC774 \uC788\uC2B5\uB2C8\uB2E4 (\uC790\uB3D9 \uC2A4\uD14C\uC774\uC9D5 \uC548 \uB428):`));
|
|
3654
|
+
for (const f of untracked.split("\n").slice(0, 10)) {
|
|
3655
|
+
console.log(chalk12.dim(` ${f}`));
|
|
3656
|
+
}
|
|
3657
|
+
if (untracked.split("\n").length > 10) {
|
|
3658
|
+
console.log(chalk12.dim(` ... \uC678 ${untracked.split("\n").length - 10}\uAC1C`));
|
|
3659
|
+
}
|
|
3660
|
+
console.log("");
|
|
3661
|
+
}
|
|
3662
|
+
console.log(chalk12.dim("\uC218\uC815\uB41C \uD30C\uC77C\uC744 \uC790\uB3D9 \uC2A4\uD14C\uC774\uC9D5\uD569\uB2C8\uB2E4..."));
|
|
3663
|
+
execSync5("git add -u", { encoding: "utf-8", cwd: process.cwd() });
|
|
3664
|
+
}
|
|
3665
|
+
const finalDiff = execSync5("git diff --cached", { encoding: "utf-8", cwd: process.cwd() });
|
|
3666
|
+
if (!finalDiff.trim()) {
|
|
3667
|
+
console.log(chalk12.dim("\nNo changes to commit.\n"));
|
|
2888
3668
|
return true;
|
|
2889
3669
|
}
|
|
3670
|
+
let conventionHint = "";
|
|
3671
|
+
try {
|
|
3672
|
+
const recentLog = execSync5("git log --oneline -10", {
|
|
3673
|
+
encoding: "utf-8",
|
|
3674
|
+
cwd: process.cwd()
|
|
3675
|
+
}).trim();
|
|
3676
|
+
if (recentLog) {
|
|
3677
|
+
const conventionalPattern = /^[a-f0-9]+\s+(feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(\(.+\))?[!]?:/;
|
|
3678
|
+
const lines = recentLog.split("\n");
|
|
3679
|
+
const conventionalCount = lines.filter((l) => conventionalPattern.test(l)).length;
|
|
3680
|
+
if (conventionalCount >= 3) {
|
|
3681
|
+
conventionHint = `
|
|
3682
|
+
|
|
3683
|
+
\uC774 \uD504\uB85C\uC81D\uD2B8\uB294 Conventional Commits \uD615\uC2DD\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4 (\uC608: feat:, fix:, chore: \uB4F1). \uAC19\uC740 \uD615\uC2DD\uC744 \uB530\uB77C\uC8FC\uC138\uC694.`;
|
|
3684
|
+
}
|
|
3685
|
+
conventionHint += `
|
|
3686
|
+
|
|
3687
|
+
\uCD5C\uADFC \uCEE4\uBC0B \uCC38\uACE0:
|
|
3688
|
+
\`\`\`
|
|
3689
|
+
${recentLog}
|
|
3690
|
+
\`\`\``;
|
|
3691
|
+
}
|
|
3692
|
+
} catch {
|
|
3693
|
+
}
|
|
2890
3694
|
ctx.conversation.addUserMessage(
|
|
2891
|
-
`\uB2E4\uC74C git diff\uB97C \uBD84\uC11D\uD574\uC11C \uC801\uC808\uD55C \uCEE4\uBC0B \uBA54\uC2DC\uC9C0\uB97C \uC0DD\uC131\uD558\uACE0, git \uB3C4\uAD6C\uB85C \
|
|
3695
|
+
`\uB2E4\uC74C git diff\uB97C \uBD84\uC11D\uD574\uC11C \uC801\uC808\uD55C \uCEE4\uBC0B \uBA54\uC2DC\uC9C0\uB97C \uC0DD\uC131\uD558\uACE0, git \uB3C4\uAD6C\uB85C \uCEE4\uBC0B\uD574\uC918. \uC774\uBBF8 \uC2A4\uD14C\uC774\uC9D5 \uC644\uB8CC\uB418\uC5C8\uC73C\uBBC0\uB85C add \uC5C6\uC774 commit\uB9CC \uD558\uBA74 \uB429\uB2C8\uB2E4.${conventionHint}
|
|
2892
3696
|
|
|
2893
3697
|
\`\`\`diff
|
|
2894
|
-
${
|
|
3698
|
+
${finalDiff}
|
|
2895
3699
|
\`\`\``
|
|
2896
3700
|
);
|
|
2897
3701
|
return false;
|
|
2898
3702
|
} catch {
|
|
2899
|
-
console.log(
|
|
3703
|
+
console.log(chalk12.yellow("Not a git repository or git not available."));
|
|
2900
3704
|
return true;
|
|
2901
3705
|
}
|
|
2902
3706
|
}
|
|
@@ -2911,7 +3715,7 @@ ${diff}
|
|
|
2911
3715
|
const unstaged = execSync5("git diff", { encoding: "utf-8", cwd: process.cwd() });
|
|
2912
3716
|
const diff = staged + unstaged;
|
|
2913
3717
|
if (!diff.trim()) {
|
|
2914
|
-
console.log(
|
|
3718
|
+
console.log(chalk12.dim("\nNo changes to review.\n"));
|
|
2915
3719
|
return true;
|
|
2916
3720
|
}
|
|
2917
3721
|
ctx.conversation.addUserMessage(
|
|
@@ -2923,7 +3727,7 @@ ${diff}
|
|
|
2923
3727
|
);
|
|
2924
3728
|
return false;
|
|
2925
3729
|
} catch {
|
|
2926
|
-
console.log(
|
|
3730
|
+
console.log(chalk12.yellow("Not a git repository or git not available."));
|
|
2927
3731
|
return true;
|
|
2928
3732
|
}
|
|
2929
3733
|
}
|
|
@@ -2933,27 +3737,27 @@ ${diff}
|
|
|
2933
3737
|
description: "Search past conversation sessions",
|
|
2934
3738
|
handler: async (args) => {
|
|
2935
3739
|
if (!args) {
|
|
2936
|
-
console.log(
|
|
3740
|
+
console.log(chalk12.yellow("Usage: /search <keyword>"));
|
|
2937
3741
|
return true;
|
|
2938
3742
|
}
|
|
2939
|
-
const home = process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
2940
|
-
const sessionsDir =
|
|
2941
|
-
if (!
|
|
2942
|
-
console.log(
|
|
3743
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || os13.homedir();
|
|
3744
|
+
const sessionsDir = path13.join(home, ".codi", "sessions");
|
|
3745
|
+
if (!fs12.existsSync(sessionsDir)) {
|
|
3746
|
+
console.log(chalk12.dim("\nNo sessions found.\n"));
|
|
2943
3747
|
return true;
|
|
2944
3748
|
}
|
|
2945
|
-
const files =
|
|
3749
|
+
const files = fs12.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
2946
3750
|
const results = [];
|
|
2947
3751
|
const keyword = args.toLowerCase();
|
|
2948
3752
|
for (const file of files) {
|
|
2949
3753
|
if (results.length >= 10) break;
|
|
2950
|
-
const filePath =
|
|
2951
|
-
const lines =
|
|
3754
|
+
const filePath = path13.join(sessionsDir, file);
|
|
3755
|
+
const lines = fs12.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
|
|
2952
3756
|
for (const line of lines) {
|
|
2953
3757
|
if (results.length >= 10) break;
|
|
2954
3758
|
if (line.toLowerCase().includes(keyword)) {
|
|
2955
3759
|
const sessionId = file.replace(".jsonl", "");
|
|
2956
|
-
const stat =
|
|
3760
|
+
const stat = fs12.statSync(filePath);
|
|
2957
3761
|
const date = stat.mtime.toISOString().split("T")[0];
|
|
2958
3762
|
const preview = line.length > 100 ? line.slice(0, 100) + "..." : line;
|
|
2959
3763
|
results.push({ sessionId, date, preview });
|
|
@@ -2962,16 +3766,16 @@ ${diff}
|
|
|
2962
3766
|
}
|
|
2963
3767
|
}
|
|
2964
3768
|
if (results.length === 0) {
|
|
2965
|
-
console.log(
|
|
3769
|
+
console.log(chalk12.dim(`
|
|
2966
3770
|
No results for "${args}".
|
|
2967
3771
|
`));
|
|
2968
3772
|
} else {
|
|
2969
|
-
console.log(
|
|
3773
|
+
console.log(chalk12.bold(`
|
|
2970
3774
|
Search results for "${args}":
|
|
2971
3775
|
`));
|
|
2972
3776
|
for (const r of results) {
|
|
2973
|
-
console.log(` ${
|
|
2974
|
-
console.log(` ${
|
|
3777
|
+
console.log(` ${chalk12.cyan(r.sessionId)} ${chalk12.dim(r.date)}`);
|
|
3778
|
+
console.log(` ${chalk12.dim(r.preview)}`);
|
|
2975
3779
|
}
|
|
2976
3780
|
console.log("");
|
|
2977
3781
|
}
|
|
@@ -2983,24 +3787,24 @@ Search results for "${args}":
|
|
|
2983
3787
|
description: "Run a command and auto-fix errors (e.g., /fix npm run build)",
|
|
2984
3788
|
handler: async (args, ctx) => {
|
|
2985
3789
|
if (!args) {
|
|
2986
|
-
console.log(
|
|
3790
|
+
console.log(chalk12.yellow("Usage: /fix <command>"));
|
|
2987
3791
|
return true;
|
|
2988
3792
|
}
|
|
2989
3793
|
const { execSync: execSync5 } = await import("child_process");
|
|
2990
3794
|
try {
|
|
2991
|
-
const isWin =
|
|
3795
|
+
const isWin = os13.platform() === "win32";
|
|
2992
3796
|
const shell = isWin ? "powershell.exe" : void 0;
|
|
2993
3797
|
const fixCmd = isWin ? `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ${args}` : args;
|
|
2994
3798
|
const output3 = execSync5(fixCmd, { encoding: "utf-8", cwd: process.cwd(), stdio: "pipe", shell });
|
|
2995
|
-
console.log(
|
|
3799
|
+
console.log(chalk12.green(`
|
|
2996
3800
|
\u2713 Command succeeded. No errors to fix.
|
|
2997
3801
|
`));
|
|
2998
|
-
if (output3.trim()) console.log(
|
|
3802
|
+
if (output3.trim()) console.log(chalk12.dim(output3));
|
|
2999
3803
|
return true;
|
|
3000
3804
|
} catch (err) {
|
|
3001
3805
|
const error = err;
|
|
3002
3806
|
const errorOutput = (error.stderr || "") + (error.stdout || "");
|
|
3003
|
-
console.log(
|
|
3807
|
+
console.log(chalk12.red(`
|
|
3004
3808
|
Command failed: ${args}
|
|
3005
3809
|
`));
|
|
3006
3810
|
ctx.conversation.addUserMessage(
|
|
@@ -3016,21 +3820,236 @@ ${errorOutput}
|
|
|
3016
3820
|
}
|
|
3017
3821
|
}
|
|
3018
3822
|
},
|
|
3823
|
+
{
|
|
3824
|
+
name: "/undo",
|
|
3825
|
+
description: "Undo the most recent file edit (rollback from backup)",
|
|
3826
|
+
handler: async (args) => {
|
|
3827
|
+
const { getBackupHistory: getBackupHistory2, undoLast: undoLast2 } = await Promise.resolve().then(() => (init_file_backup(), file_backup_exports));
|
|
3828
|
+
if (args === "list") {
|
|
3829
|
+
const history = getBackupHistory2();
|
|
3830
|
+
if (history.length === 0) {
|
|
3831
|
+
console.log(chalk12.dim("\n\uB418\uB3CC\uB9B4 \uD30C\uC77C \uBCC0\uACBD \uC774\uB825\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n"));
|
|
3832
|
+
return true;
|
|
3833
|
+
}
|
|
3834
|
+
console.log(chalk12.bold(`
|
|
3835
|
+
\uD30C\uC77C \uBCC0\uACBD \uC774\uB825 (\uCD5C\uADFC ${Math.min(history.length, 20)}\uAC1C):
|
|
3836
|
+
`));
|
|
3837
|
+
const recent = history.slice(-20).reverse();
|
|
3838
|
+
for (let i = 0; i < recent.length; i++) {
|
|
3839
|
+
const entry2 = recent[i];
|
|
3840
|
+
const time = new Date(entry2.timestamp).toLocaleTimeString();
|
|
3841
|
+
const tag = entry2.wasNew ? chalk12.yellow("[\uC0C8 \uD30C\uC77C]") : chalk12.cyan("[\uC218\uC815]");
|
|
3842
|
+
console.log(` ${i + 1}. ${tag} ${entry2.originalPath} ${chalk12.dim(time)}`);
|
|
3843
|
+
}
|
|
3844
|
+
console.log("");
|
|
3845
|
+
return true;
|
|
3846
|
+
}
|
|
3847
|
+
const entry = undoLast2();
|
|
3848
|
+
if (!entry) {
|
|
3849
|
+
console.log(chalk12.yellow("\n\uB418\uB3CC\uB9B4 \uBCC0\uACBD \uC0AC\uD56D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n"));
|
|
3850
|
+
return true;
|
|
3851
|
+
}
|
|
3852
|
+
const action = entry.wasNew ? "\uC0AD\uC81C\uB428 (\uC0C8\uB85C \uC0DD\uC131\uB41C \uD30C\uC77C)" : "\uC774\uC804 \uC0C1\uD0DC\uB85C \uBCF5\uC6D0\uB428";
|
|
3853
|
+
console.log(chalk12.green(`
|
|
3854
|
+
\u2713 \uB418\uB3CC\uB9AC\uAE30 \uC644\uB8CC: ${entry.originalPath}`));
|
|
3855
|
+
console.log(chalk12.dim(` ${action}`));
|
|
3856
|
+
console.log("");
|
|
3857
|
+
return true;
|
|
3858
|
+
}
|
|
3859
|
+
},
|
|
3860
|
+
{
|
|
3861
|
+
name: "/branch",
|
|
3862
|
+
description: "Create and switch to a new branch, or show current branch",
|
|
3863
|
+
handler: async (args) => {
|
|
3864
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
3865
|
+
try {
|
|
3866
|
+
if (!args) {
|
|
3867
|
+
const current = execSync5("git branch --show-current", {
|
|
3868
|
+
encoding: "utf-8",
|
|
3869
|
+
cwd: process.cwd()
|
|
3870
|
+
}).trim();
|
|
3871
|
+
const branches = execSync5("git branch -a", {
|
|
3872
|
+
encoding: "utf-8",
|
|
3873
|
+
cwd: process.cwd()
|
|
3874
|
+
}).trim();
|
|
3875
|
+
console.log(chalk12.bold(`
|
|
3876
|
+
Current branch: ${chalk12.green(current || "(detached HEAD)")}
|
|
3877
|
+
`));
|
|
3878
|
+
console.log(branches);
|
|
3879
|
+
console.log("");
|
|
3880
|
+
} else {
|
|
3881
|
+
const name = args.trim();
|
|
3882
|
+
execSync5(`git checkout -b ${name}`, {
|
|
3883
|
+
encoding: "utf-8",
|
|
3884
|
+
cwd: process.cwd()
|
|
3885
|
+
});
|
|
3886
|
+
console.log(chalk12.green(`
|
|
3887
|
+
\u2713 Created and switched to branch: ${name}
|
|
3888
|
+
`));
|
|
3889
|
+
}
|
|
3890
|
+
} catch (err) {
|
|
3891
|
+
const error = err;
|
|
3892
|
+
const msg = error.stderr || error.message || "Unknown error";
|
|
3893
|
+
console.log(chalk12.red(`
|
|
3894
|
+
Branch operation failed: ${msg.trim()}
|
|
3895
|
+
`));
|
|
3896
|
+
}
|
|
3897
|
+
return true;
|
|
3898
|
+
}
|
|
3899
|
+
},
|
|
3900
|
+
{
|
|
3901
|
+
name: "/stash",
|
|
3902
|
+
description: "Git stash management (pop, list, drop, or save)",
|
|
3903
|
+
handler: async (args) => {
|
|
3904
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
3905
|
+
const sub = args.trim().split(/\s+/);
|
|
3906
|
+
const action = sub[0] || "push";
|
|
3907
|
+
const allowed = ["push", "pop", "list", "drop", "show", "apply", "clear"];
|
|
3908
|
+
if (!allowed.includes(action)) {
|
|
3909
|
+
console.log(chalk12.yellow(`Usage: /stash [${allowed.join("|")}]`));
|
|
3910
|
+
return true;
|
|
3911
|
+
}
|
|
3912
|
+
try {
|
|
3913
|
+
if (action === "clear") {
|
|
3914
|
+
console.log(chalk12.yellow("\u26A0 This will drop all stashes permanently."));
|
|
3915
|
+
}
|
|
3916
|
+
const cmd = `git stash ${args.trim() || "push"}`;
|
|
3917
|
+
const output3 = execSync5(cmd, {
|
|
3918
|
+
encoding: "utf-8",
|
|
3919
|
+
cwd: process.cwd()
|
|
3920
|
+
});
|
|
3921
|
+
console.log(output3.trim() ? `
|
|
3922
|
+
${output3.trim()}
|
|
3923
|
+
` : chalk12.dim("\n(no output)\n"));
|
|
3924
|
+
} catch (err) {
|
|
3925
|
+
const error = err;
|
|
3926
|
+
const msg = error.stderr || error.stdout || error.message || "Unknown error";
|
|
3927
|
+
console.log(chalk12.red(`
|
|
3928
|
+
Stash operation failed: ${msg.trim()}
|
|
3929
|
+
`));
|
|
3930
|
+
}
|
|
3931
|
+
return true;
|
|
3932
|
+
}
|
|
3933
|
+
},
|
|
3934
|
+
{
|
|
3935
|
+
name: "/pr",
|
|
3936
|
+
description: "Generate a pull request description from current branch diff",
|
|
3937
|
+
handler: async (_args, ctx) => {
|
|
3938
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
3939
|
+
try {
|
|
3940
|
+
const currentBranch = execSync5("git branch --show-current", {
|
|
3941
|
+
encoding: "utf-8",
|
|
3942
|
+
cwd: process.cwd()
|
|
3943
|
+
}).trim();
|
|
3944
|
+
if (!currentBranch) {
|
|
3945
|
+
console.log(chalk12.yellow("Not on a branch (detached HEAD)."));
|
|
3946
|
+
return true;
|
|
3947
|
+
}
|
|
3948
|
+
let baseBranch = "main";
|
|
3949
|
+
try {
|
|
3950
|
+
execSync5("git rev-parse --verify main", {
|
|
3951
|
+
encoding: "utf-8",
|
|
3952
|
+
cwd: process.cwd(),
|
|
3953
|
+
stdio: "pipe"
|
|
3954
|
+
});
|
|
3955
|
+
} catch {
|
|
3956
|
+
try {
|
|
3957
|
+
execSync5("git rev-parse --verify master", {
|
|
3958
|
+
encoding: "utf-8",
|
|
3959
|
+
cwd: process.cwd(),
|
|
3960
|
+
stdio: "pipe"
|
|
3961
|
+
});
|
|
3962
|
+
baseBranch = "master";
|
|
3963
|
+
} catch {
|
|
3964
|
+
console.log(chalk12.yellow("Cannot find base branch (main or master)."));
|
|
3965
|
+
return true;
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
if (currentBranch === baseBranch) {
|
|
3969
|
+
console.log(chalk12.yellow(`Already on ${baseBranch}. Switch to a feature branch first.`));
|
|
3970
|
+
return true;
|
|
3971
|
+
}
|
|
3972
|
+
let commitLog = "";
|
|
3973
|
+
try {
|
|
3974
|
+
commitLog = execSync5(`git log ${baseBranch}..HEAD --oneline`, {
|
|
3975
|
+
encoding: "utf-8",
|
|
3976
|
+
cwd: process.cwd()
|
|
3977
|
+
}).trim();
|
|
3978
|
+
} catch {
|
|
3979
|
+
}
|
|
3980
|
+
if (!commitLog) {
|
|
3981
|
+
console.log(chalk12.yellow(`No commits ahead of ${baseBranch}.`));
|
|
3982
|
+
return true;
|
|
3983
|
+
}
|
|
3984
|
+
let diffStat = "";
|
|
3985
|
+
try {
|
|
3986
|
+
diffStat = execSync5(`git diff ${baseBranch}...HEAD --stat`, {
|
|
3987
|
+
encoding: "utf-8",
|
|
3988
|
+
cwd: process.cwd()
|
|
3989
|
+
}).trim();
|
|
3990
|
+
} catch {
|
|
3991
|
+
}
|
|
3992
|
+
let diff = "";
|
|
3993
|
+
try {
|
|
3994
|
+
diff = execSync5(`git diff ${baseBranch}...HEAD`, {
|
|
3995
|
+
encoding: "utf-8",
|
|
3996
|
+
cwd: process.cwd(),
|
|
3997
|
+
maxBuffer: 10 * 1024 * 1024
|
|
3998
|
+
});
|
|
3999
|
+
if (diff.length > 5e4) {
|
|
4000
|
+
diff = diff.slice(0, 5e4) + "\n\n... (diff truncated, too large)";
|
|
4001
|
+
}
|
|
4002
|
+
} catch {
|
|
4003
|
+
}
|
|
4004
|
+
console.log(chalk12.dim(`
|
|
4005
|
+
Analyzing ${commitLog.split("\n").length} commit(s) from ${currentBranch}...
|
|
4006
|
+
`));
|
|
4007
|
+
ctx.conversation.addUserMessage(
|
|
4008
|
+
`\uD604\uC7AC \uBE0C\uB79C\uCE58 \`${currentBranch}\`\uC5D0\uC11C \`${baseBranch}\`\uB85C \uBCF4\uB0BC Pull Request \uC124\uBA85\uC744 \uC0DD\uC131\uD574\uC918.
|
|
4009
|
+
|
|
4010
|
+
\uB2E4\uC74C \uD615\uC2DD\uC758 \uB9C8\uD06C\uB2E4\uC6B4\uC73C\uB85C \uCD9C\uB825\uD574\uC918:
|
|
4011
|
+
- **Title**: PR \uC81C\uBAA9 (70\uC790 \uC774\uB0B4, \uC601\uBB38)
|
|
4012
|
+
- **## Summary**: \uBCC0\uACBD \uC0AC\uD56D \uC694\uC57D (1-3 bullet points)
|
|
4013
|
+
- **## Changes**: \uC8FC\uC694 \uBCC0\uACBD \uD30C\uC77C \uBC0F \uB0B4\uC6A9
|
|
4014
|
+
- **## Test Plan**: \uD14C\uC2A4\uD2B8 \uACC4\uD68D \uCCB4\uD06C\uB9AC\uC2A4\uD2B8
|
|
4015
|
+
|
|
4016
|
+
### Commits:
|
|
4017
|
+
\`\`\`
|
|
4018
|
+
${commitLog}
|
|
4019
|
+
\`\`\`
|
|
4020
|
+
|
|
4021
|
+
### Diff stat:
|
|
4022
|
+
\`\`\`
|
|
4023
|
+
${diffStat}
|
|
4024
|
+
\`\`\`
|
|
4025
|
+
|
|
4026
|
+
### Full diff:
|
|
4027
|
+
\`\`\`diff
|
|
4028
|
+
${diff}
|
|
4029
|
+
\`\`\``
|
|
4030
|
+
);
|
|
4031
|
+
return false;
|
|
4032
|
+
} catch {
|
|
4033
|
+
console.log(chalk12.yellow("Not a git repository or git not available."));
|
|
4034
|
+
return true;
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
},
|
|
3019
4038
|
{
|
|
3020
4039
|
name: "/mcp",
|
|
3021
4040
|
description: "Show MCP server status",
|
|
3022
4041
|
handler: async () => {
|
|
3023
4042
|
const servers = mcpManager.listServers();
|
|
3024
4043
|
if (servers.length === 0) {
|
|
3025
|
-
console.log(
|
|
3026
|
-
console.log(
|
|
4044
|
+
console.log(chalk12.dim("\nNo MCP servers connected.\n"));
|
|
4045
|
+
console.log(chalk12.dim("Add servers in .codi/mcp.json or ~/.codi/mcp.json"));
|
|
3027
4046
|
return true;
|
|
3028
4047
|
}
|
|
3029
|
-
console.log(
|
|
4048
|
+
console.log(chalk12.bold("\nMCP Servers:\n"));
|
|
3030
4049
|
for (const s of servers) {
|
|
3031
|
-
console.log(` ${
|
|
4050
|
+
console.log(` ${chalk12.green("\u25CF")} ${s.name}`);
|
|
3032
4051
|
for (const t of s.tools) {
|
|
3033
|
-
console.log(
|
|
4052
|
+
console.log(chalk12.dim(` - ${t}`));
|
|
3034
4053
|
}
|
|
3035
4054
|
}
|
|
3036
4055
|
console.log("");
|
|
@@ -3039,24 +4058,29 @@ ${errorOutput}
|
|
|
3039
4058
|
}
|
|
3040
4059
|
];
|
|
3041
4060
|
}
|
|
4061
|
+
function formatTokens(n) {
|
|
4062
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
4063
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
4064
|
+
return String(n);
|
|
4065
|
+
}
|
|
3042
4066
|
function loadCustomCommands() {
|
|
3043
4067
|
const commands = [];
|
|
3044
|
-
const home = process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
4068
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || os13.homedir();
|
|
3045
4069
|
const dirs = [
|
|
3046
|
-
|
|
3047
|
-
|
|
4070
|
+
path13.join(home, ".codi", "commands"),
|
|
4071
|
+
path13.join(process.cwd(), ".codi", "commands")
|
|
3048
4072
|
];
|
|
3049
4073
|
for (const dir of dirs) {
|
|
3050
|
-
if (!
|
|
3051
|
-
const files =
|
|
4074
|
+
if (!fs12.existsSync(dir)) continue;
|
|
4075
|
+
const files = fs12.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
3052
4076
|
for (const file of files) {
|
|
3053
4077
|
const name = "/" + file.replace(".md", "");
|
|
3054
|
-
const filePath =
|
|
4078
|
+
const filePath = path13.join(dir, file);
|
|
3055
4079
|
commands.push({
|
|
3056
4080
|
name,
|
|
3057
|
-
description: `Custom command from ${
|
|
4081
|
+
description: `Custom command from ${path13.relative(process.cwd(), filePath)}`,
|
|
3058
4082
|
handler: async (_args, ctx) => {
|
|
3059
|
-
let content =
|
|
4083
|
+
let content = fs12.readFileSync(filePath, "utf-8");
|
|
3060
4084
|
content = content.replace(/\{\{cwd\}\}/g, process.cwd()).replace(/\{\{date\}\}/g, (/* @__PURE__ */ new Date()).toISOString().split("T")[0]).replace(/\{\{file_path\}\}/g, _args || "");
|
|
3061
4085
|
await ctx.conversation.addUserMessage(content);
|
|
3062
4086
|
return false;
|
|
@@ -3070,8 +4094,8 @@ function loadCustomCommands() {
|
|
|
3070
4094
|
// src/tools/file-read.ts
|
|
3071
4095
|
init_esm_shims();
|
|
3072
4096
|
init_tool();
|
|
3073
|
-
import * as
|
|
3074
|
-
import * as
|
|
4097
|
+
import * as fs13 from "fs";
|
|
4098
|
+
import * as path14 from "path";
|
|
3075
4099
|
var fileReadTool = {
|
|
3076
4100
|
name: "read_file",
|
|
3077
4101
|
description: `Read a file from the filesystem. Supports text files with line numbers (cat -n format), PDF files, images (returns base64 for multimodal), and Jupyter notebooks (.ipynb). Use offset/limit for large files.`,
|
|
@@ -3091,17 +4115,17 @@ var fileReadTool = {
|
|
|
3091
4115
|
const filePath = String(input3["file_path"]);
|
|
3092
4116
|
const offset = input3["offset"];
|
|
3093
4117
|
const limit = input3["limit"];
|
|
3094
|
-
const resolved =
|
|
3095
|
-
if (!
|
|
4118
|
+
const resolved = path14.resolve(filePath);
|
|
4119
|
+
if (!fs13.existsSync(resolved)) {
|
|
3096
4120
|
return makeToolError(`File not found: ${resolved}`);
|
|
3097
4121
|
}
|
|
3098
|
-
const stat =
|
|
4122
|
+
const stat = fs13.statSync(resolved);
|
|
3099
4123
|
if (stat.isDirectory()) {
|
|
3100
4124
|
return makeToolError(`Path is a directory, not a file: ${resolved}. Use list_dir instead.`);
|
|
3101
4125
|
}
|
|
3102
|
-
const ext =
|
|
4126
|
+
const ext = path14.extname(resolved).toLowerCase();
|
|
3103
4127
|
if ([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"].includes(ext)) {
|
|
3104
|
-
const data =
|
|
4128
|
+
const data = fs13.readFileSync(resolved);
|
|
3105
4129
|
const base64 = data.toString("base64");
|
|
3106
4130
|
const mimeMap = {
|
|
3107
4131
|
".png": "image/png",
|
|
@@ -3112,7 +4136,7 @@ var fileReadTool = {
|
|
|
3112
4136
|
".bmp": "image/bmp",
|
|
3113
4137
|
".svg": "image/svg+xml"
|
|
3114
4138
|
};
|
|
3115
|
-
return makeToolResult(`[Image: ${
|
|
4139
|
+
return makeToolResult(`[Image: ${path14.basename(resolved)}]`, {
|
|
3116
4140
|
filePath: resolved,
|
|
3117
4141
|
isImage: true,
|
|
3118
4142
|
imageData: base64,
|
|
@@ -3123,7 +4147,7 @@ var fileReadTool = {
|
|
|
3123
4147
|
try {
|
|
3124
4148
|
const pdfModule = await import("pdf-parse");
|
|
3125
4149
|
const pdfParse = pdfModule.default || pdfModule;
|
|
3126
|
-
const buffer =
|
|
4150
|
+
const buffer = fs13.readFileSync(resolved);
|
|
3127
4151
|
const data = await pdfParse(buffer);
|
|
3128
4152
|
const pages = input3["pages"];
|
|
3129
4153
|
let text = data.text;
|
|
@@ -3141,7 +4165,7 @@ var fileReadTool = {
|
|
|
3141
4165
|
}
|
|
3142
4166
|
if (ext === ".ipynb") {
|
|
3143
4167
|
try {
|
|
3144
|
-
const content =
|
|
4168
|
+
const content = fs13.readFileSync(resolved, "utf-8");
|
|
3145
4169
|
const nb = JSON.parse(content);
|
|
3146
4170
|
const output3 = [];
|
|
3147
4171
|
for (let i = 0; i < (nb.cells || []).length; i++) {
|
|
@@ -3169,7 +4193,7 @@ var fileReadTool = {
|
|
|
3169
4193
|
}
|
|
3170
4194
|
}
|
|
3171
4195
|
try {
|
|
3172
|
-
const raw =
|
|
4196
|
+
const raw = fs13.readFileSync(resolved, "utf-8");
|
|
3173
4197
|
const content = raw.replace(/\r\n/g, "\n");
|
|
3174
4198
|
const lines = content.split("\n");
|
|
3175
4199
|
const totalLines = lines.length;
|
|
@@ -3197,8 +4221,9 @@ var fileReadTool = {
|
|
|
3197
4221
|
// src/tools/file-write.ts
|
|
3198
4222
|
init_esm_shims();
|
|
3199
4223
|
init_tool();
|
|
3200
|
-
|
|
3201
|
-
import * as
|
|
4224
|
+
init_file_backup();
|
|
4225
|
+
import * as fs14 from "fs";
|
|
4226
|
+
import * as path15 from "path";
|
|
3202
4227
|
var fileWriteTool = {
|
|
3203
4228
|
name: "write_file",
|
|
3204
4229
|
description: `Create a new file or overwrite an existing file. Creates parent directories if needed. For modifying existing files, prefer edit_file instead.`,
|
|
@@ -3215,14 +4240,15 @@ var fileWriteTool = {
|
|
|
3215
4240
|
async execute(input3) {
|
|
3216
4241
|
const filePath = String(input3["file_path"]);
|
|
3217
4242
|
const content = String(input3["content"]);
|
|
3218
|
-
const resolved =
|
|
4243
|
+
const resolved = path15.resolve(filePath);
|
|
3219
4244
|
try {
|
|
3220
|
-
const dir =
|
|
3221
|
-
if (!
|
|
3222
|
-
|
|
4245
|
+
const dir = path15.dirname(resolved);
|
|
4246
|
+
if (!fs14.existsSync(dir)) {
|
|
4247
|
+
fs14.mkdirSync(dir, { recursive: true });
|
|
3223
4248
|
}
|
|
3224
|
-
const existed =
|
|
3225
|
-
|
|
4249
|
+
const existed = fs14.existsSync(resolved);
|
|
4250
|
+
backupFile(resolved);
|
|
4251
|
+
fs14.writeFileSync(resolved, content, "utf-8");
|
|
3226
4252
|
const lines = content.split("\n").length;
|
|
3227
4253
|
const action = existed ? "Overwrote" : "Created";
|
|
3228
4254
|
return makeToolResult(`${action} ${resolved} (${lines} lines)`, {
|
|
@@ -3238,8 +4264,9 @@ var fileWriteTool = {
|
|
|
3238
4264
|
// src/tools/file-edit.ts
|
|
3239
4265
|
init_esm_shims();
|
|
3240
4266
|
init_tool();
|
|
3241
|
-
|
|
3242
|
-
import * as
|
|
4267
|
+
init_file_backup();
|
|
4268
|
+
import * as fs15 from "fs";
|
|
4269
|
+
import * as path16 from "path";
|
|
3243
4270
|
var fileEditTool = {
|
|
3244
4271
|
name: "edit_file",
|
|
3245
4272
|
description: `Perform exact string replacement in a file. The old_string must be unique in the file unless replace_all is true. Preserves indentation exactly.`,
|
|
@@ -3260,12 +4287,12 @@ var fileEditTool = {
|
|
|
3260
4287
|
const oldString = String(input3["old_string"]);
|
|
3261
4288
|
const newString = String(input3["new_string"]);
|
|
3262
4289
|
const replaceAll = input3["replace_all"] === true;
|
|
3263
|
-
const resolved =
|
|
3264
|
-
if (!
|
|
4290
|
+
const resolved = path16.resolve(filePath);
|
|
4291
|
+
if (!fs15.existsSync(resolved)) {
|
|
3265
4292
|
return makeToolError(`File not found: ${resolved}`);
|
|
3266
4293
|
}
|
|
3267
4294
|
try {
|
|
3268
|
-
const raw =
|
|
4295
|
+
const raw = fs15.readFileSync(resolved, "utf-8");
|
|
3269
4296
|
const hasCrlf = raw.includes("\r\n");
|
|
3270
4297
|
let content = hasCrlf ? raw.replace(/\r\n/g, "\n") : raw;
|
|
3271
4298
|
if (oldString === newString) {
|
|
@@ -3299,8 +4326,9 @@ Did you mean this line?
|
|
|
3299
4326
|
const idx = content.indexOf(oldString);
|
|
3300
4327
|
content = content.slice(0, idx) + newString + content.slice(idx + oldString.length);
|
|
3301
4328
|
}
|
|
4329
|
+
backupFile(resolved);
|
|
3302
4330
|
const output3 = hasCrlf ? content.replace(/\n/g, "\r\n") : content;
|
|
3303
|
-
|
|
4331
|
+
fs15.writeFileSync(resolved, output3, "utf-8");
|
|
3304
4332
|
const linesChanged = Math.max(
|
|
3305
4333
|
oldString.split("\n").length,
|
|
3306
4334
|
newString.split("\n").length
|
|
@@ -3318,8 +4346,9 @@ Did you mean this line?
|
|
|
3318
4346
|
// src/tools/file-multi-edit.ts
|
|
3319
4347
|
init_esm_shims();
|
|
3320
4348
|
init_tool();
|
|
3321
|
-
|
|
3322
|
-
import * as
|
|
4349
|
+
init_file_backup();
|
|
4350
|
+
import * as fs16 from "fs";
|
|
4351
|
+
import * as path17 from "path";
|
|
3323
4352
|
var fileMultiEditTool = {
|
|
3324
4353
|
name: "multi_edit",
|
|
3325
4354
|
description: `Apply multiple edits to a single file atomically. Each edit is an old_string \u2192 new_string replacement. All edits are validated before any are applied.`,
|
|
@@ -3347,15 +4376,15 @@ var fileMultiEditTool = {
|
|
|
3347
4376
|
async execute(input3) {
|
|
3348
4377
|
const filePath = String(input3["file_path"]);
|
|
3349
4378
|
const edits = input3["edits"];
|
|
3350
|
-
const resolved =
|
|
3351
|
-
if (!
|
|
4379
|
+
const resolved = path17.resolve(filePath);
|
|
4380
|
+
if (!fs16.existsSync(resolved)) {
|
|
3352
4381
|
return makeToolError(`File not found: ${resolved}`);
|
|
3353
4382
|
}
|
|
3354
4383
|
if (!Array.isArray(edits) || edits.length === 0) {
|
|
3355
4384
|
return makeToolError("edits must be a non-empty array of {old_string, new_string} objects");
|
|
3356
4385
|
}
|
|
3357
4386
|
try {
|
|
3358
|
-
const raw =
|
|
4387
|
+
const raw = fs16.readFileSync(resolved, "utf-8");
|
|
3359
4388
|
const hasCrlf = raw.includes("\r\n");
|
|
3360
4389
|
let content = hasCrlf ? raw.replace(/\r\n/g, "\n") : raw;
|
|
3361
4390
|
for (let i = 0; i < edits.length; i++) {
|
|
@@ -3379,8 +4408,9 @@ Searching for: ${edit2.old_string.slice(0, 100)}...`
|
|
|
3379
4408
|
edit2.new_string.split("\n").length
|
|
3380
4409
|
);
|
|
3381
4410
|
}
|
|
4411
|
+
backupFile(resolved);
|
|
3382
4412
|
const output3 = hasCrlf ? content.replace(/\n/g, "\r\n") : content;
|
|
3383
|
-
|
|
4413
|
+
fs16.writeFileSync(resolved, output3, "utf-8");
|
|
3384
4414
|
return makeToolResult(`Applied ${edits.length} edits to ${resolved}`, {
|
|
3385
4415
|
filePath: resolved,
|
|
3386
4416
|
linesChanged: totalLinesChanged
|
|
@@ -3394,8 +4424,8 @@ Searching for: ${edit2.old_string.slice(0, 100)}...`
|
|
|
3394
4424
|
// src/tools/glob.ts
|
|
3395
4425
|
init_esm_shims();
|
|
3396
4426
|
init_tool();
|
|
3397
|
-
import * as
|
|
3398
|
-
import * as
|
|
4427
|
+
import * as fs17 from "fs";
|
|
4428
|
+
import * as path18 from "path";
|
|
3399
4429
|
import { globby } from "globby";
|
|
3400
4430
|
var globTool = {
|
|
3401
4431
|
name: "glob",
|
|
@@ -3413,7 +4443,7 @@ var globTool = {
|
|
|
3413
4443
|
async execute(input3) {
|
|
3414
4444
|
const pattern = String(input3["pattern"]);
|
|
3415
4445
|
const searchPath = input3["path"] ? String(input3["path"]) : process.cwd();
|
|
3416
|
-
const resolved =
|
|
4446
|
+
const resolved = path18.resolve(searchPath);
|
|
3417
4447
|
try {
|
|
3418
4448
|
const files = await globby(pattern, {
|
|
3419
4449
|
cwd: resolved,
|
|
@@ -3424,7 +4454,7 @@ var globTool = {
|
|
|
3424
4454
|
});
|
|
3425
4455
|
const withStats = files.map((f) => {
|
|
3426
4456
|
try {
|
|
3427
|
-
const stat =
|
|
4457
|
+
const stat = fs17.statSync(f);
|
|
3428
4458
|
return { path: f, mtime: stat.mtimeMs };
|
|
3429
4459
|
} catch {
|
|
3430
4460
|
return { path: f, mtime: 0 };
|
|
@@ -3449,8 +4479,8 @@ ${result.join("\n")}`
|
|
|
3449
4479
|
init_esm_shims();
|
|
3450
4480
|
init_tool();
|
|
3451
4481
|
import { execFile } from "child_process";
|
|
3452
|
-
import * as
|
|
3453
|
-
import * as
|
|
4482
|
+
import * as fs18 from "fs";
|
|
4483
|
+
import * as path19 from "path";
|
|
3454
4484
|
var grepTool = {
|
|
3455
4485
|
name: "grep",
|
|
3456
4486
|
description: `Search file contents using regex patterns. Uses ripgrep (rg) if available, falls back to grep, then to a built-in Node.js search. Supports context lines, file type filters, and multiple output modes.`,
|
|
@@ -3480,7 +4510,7 @@ var grepTool = {
|
|
|
3480
4510
|
readOnly: true,
|
|
3481
4511
|
async execute(input3) {
|
|
3482
4512
|
const pattern = String(input3["pattern"]);
|
|
3483
|
-
const searchPath =
|
|
4513
|
+
const searchPath = path19.resolve(input3["path"] ? String(input3["path"]) : process.cwd());
|
|
3484
4514
|
const outputMode = input3["output_mode"] || "files_with_matches";
|
|
3485
4515
|
const headLimit = input3["head_limit"] || 0;
|
|
3486
4516
|
const hasRg = await hasCommand("rg");
|
|
@@ -3545,19 +4575,19 @@ var grepTool = {
|
|
|
3545
4575
|
};
|
|
3546
4576
|
function hasCommand(cmd) {
|
|
3547
4577
|
const checkCmd = process.platform === "win32" ? "where" : "which";
|
|
3548
|
-
return new Promise((
|
|
3549
|
-
execFile(checkCmd, [cmd], (err) =>
|
|
4578
|
+
return new Promise((resolve11) => {
|
|
4579
|
+
execFile(checkCmd, [cmd], (err) => resolve11(!err));
|
|
3550
4580
|
});
|
|
3551
4581
|
}
|
|
3552
4582
|
function runCommand(cmd, args) {
|
|
3553
|
-
return new Promise((
|
|
4583
|
+
return new Promise((resolve11, reject) => {
|
|
3554
4584
|
execFile(cmd, args, { maxBuffer: 10 * 1024 * 1024, timeout: 3e4 }, (err, stdout, stderr) => {
|
|
3555
4585
|
if (err) {
|
|
3556
4586
|
err.code = err.code;
|
|
3557
4587
|
reject(err);
|
|
3558
4588
|
return;
|
|
3559
4589
|
}
|
|
3560
|
-
|
|
4590
|
+
resolve11(stdout);
|
|
3561
4591
|
});
|
|
3562
4592
|
});
|
|
3563
4593
|
}
|
|
@@ -3600,7 +4630,7 @@ async function builtinSearch(pattern, searchPath, input3, outputMode, headLimit)
|
|
|
3600
4630
|
if (headLimit > 0 && entryCount >= headLimit) break;
|
|
3601
4631
|
let content;
|
|
3602
4632
|
try {
|
|
3603
|
-
content =
|
|
4633
|
+
content = fs18.readFileSync(filePath, "utf-8");
|
|
3604
4634
|
} catch {
|
|
3605
4635
|
continue;
|
|
3606
4636
|
}
|
|
@@ -3653,158 +4683,53 @@ async function builtinSearch(pattern, searchPath, input3, outputMode, headLimit)
|
|
|
3653
4683
|
}
|
|
3654
4684
|
return makeToolResult(results.join("\n"));
|
|
3655
4685
|
}
|
|
3656
|
-
function collectFiles(dirPath, typeFilter, globFilter) {
|
|
3657
|
-
const files = [];
|
|
3658
|
-
const allowedExtensions = typeFilter && TYPE_EXTENSIONS[typeFilter] ? new Set(TYPE_EXTENSIONS[typeFilter]) : null;
|
|
3659
|
-
let globRegex = null;
|
|
3660
|
-
if (globFilter) {
|
|
3661
|
-
const escaped = globFilter.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
3662
|
-
globRegex = new RegExp(`^${escaped}$`);
|
|
3663
|
-
}
|
|
3664
|
-
function walk(dir) {
|
|
3665
|
-
let entries;
|
|
3666
|
-
try {
|
|
3667
|
-
entries =
|
|
3668
|
-
} catch {
|
|
3669
|
-
return;
|
|
3670
|
-
}
|
|
3671
|
-
for (const entry of entries) {
|
|
3672
|
-
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
3673
|
-
if (entry.name.startsWith(".") && entry.name !== ".") continue;
|
|
3674
|
-
const fullPath = path16.join(dir, entry.name);
|
|
3675
|
-
if (entry.isDirectory()) {
|
|
3676
|
-
walk(fullPath);
|
|
3677
|
-
} else if (entry.isFile()) {
|
|
3678
|
-
const ext = path16.extname(entry.name).toLowerCase();
|
|
3679
|
-
if (BINARY_EXTENSIONS.has(ext)) continue;
|
|
3680
|
-
if (allowedExtensions && !allowedExtensions.has(ext)) continue;
|
|
3681
|
-
if (globRegex && !globRegex.test(entry.name)) continue;
|
|
3682
|
-
files.push(fullPath);
|
|
3683
|
-
}
|
|
3684
|
-
}
|
|
3685
|
-
}
|
|
3686
|
-
try {
|
|
3687
|
-
const stat = fs14.statSync(dirPath);
|
|
3688
|
-
if (stat.isFile()) {
|
|
3689
|
-
return [dirPath];
|
|
3690
|
-
}
|
|
3691
|
-
} catch {
|
|
3692
|
-
return [];
|
|
3693
|
-
}
|
|
3694
|
-
walk(dirPath);
|
|
3695
|
-
return files;
|
|
3696
|
-
}
|
|
3697
|
-
|
|
3698
|
-
// src/tools/bash.ts
|
|
3699
|
-
init_esm_shims();
|
|
3700
|
-
init_tool();
|
|
3701
|
-
import { exec as exec2, spawn } from "child_process";
|
|
3702
|
-
import * as os10 from "os";
|
|
3703
|
-
function getDefaultShell2() {
|
|
3704
|
-
if (os10.platform() === "win32") {
|
|
3705
|
-
return "powershell.exe";
|
|
3706
|
-
}
|
|
3707
|
-
return process.env["SHELL"] || "/bin/bash";
|
|
3708
|
-
}
|
|
3709
|
-
var backgroundTasks = /* @__PURE__ */ new Map();
|
|
3710
|
-
var taskCounter = 0;
|
|
3711
|
-
var bashTool = {
|
|
3712
|
-
name: "bash",
|
|
3713
|
-
description: `Execute a shell command. Supports timeout (max 600s, default 120s) and background execution. The working directory persists between calls. Uses the platform default shell (bash on Unix, PowerShell on Windows).`,
|
|
3714
|
-
inputSchema: {
|
|
3715
|
-
type: "object",
|
|
3716
|
-
properties: {
|
|
3717
|
-
command: { type: "string", description: "The bash command to execute" },
|
|
3718
|
-
description: { type: "string", description: "Brief description of what the command does" },
|
|
3719
|
-
timeout: { type: "number", description: "Timeout in milliseconds (max 600000, default 120000)" },
|
|
3720
|
-
run_in_background: { type: "boolean", description: "Run in background and return a task ID" }
|
|
3721
|
-
},
|
|
3722
|
-
required: ["command"]
|
|
3723
|
-
},
|
|
3724
|
-
dangerous: true,
|
|
3725
|
-
readOnly: false,
|
|
3726
|
-
async execute(input3) {
|
|
3727
|
-
const command = String(input3["command"]);
|
|
3728
|
-
const timeout = Math.min(Number(input3["timeout"]) || 12e4, 6e5);
|
|
3729
|
-
const runInBackground = input3["run_in_background"] === true;
|
|
3730
|
-
if (!command.trim()) {
|
|
3731
|
-
return makeToolError("Command cannot be empty");
|
|
4686
|
+
function collectFiles(dirPath, typeFilter, globFilter) {
|
|
4687
|
+
const files = [];
|
|
4688
|
+
const allowedExtensions = typeFilter && TYPE_EXTENSIONS[typeFilter] ? new Set(TYPE_EXTENSIONS[typeFilter]) : null;
|
|
4689
|
+
let globRegex = null;
|
|
4690
|
+
if (globFilter) {
|
|
4691
|
+
const escaped = globFilter.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
4692
|
+
globRegex = new RegExp(`^${escaped}$`);
|
|
4693
|
+
}
|
|
4694
|
+
function walk(dir) {
|
|
4695
|
+
let entries;
|
|
4696
|
+
try {
|
|
4697
|
+
entries = fs18.readdirSync(dir, { withFileTypes: true });
|
|
4698
|
+
} catch {
|
|
4699
|
+
return;
|
|
3732
4700
|
}
|
|
3733
|
-
|
|
3734
|
-
|
|
4701
|
+
for (const entry of entries) {
|
|
4702
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
4703
|
+
if (entry.name.startsWith(".") && entry.name !== ".") continue;
|
|
4704
|
+
const fullPath = path19.join(dir, entry.name);
|
|
4705
|
+
if (entry.isDirectory()) {
|
|
4706
|
+
walk(fullPath);
|
|
4707
|
+
} else if (entry.isFile()) {
|
|
4708
|
+
const ext = path19.extname(entry.name).toLowerCase();
|
|
4709
|
+
if (BINARY_EXTENSIONS.has(ext)) continue;
|
|
4710
|
+
if (allowedExtensions && !allowedExtensions.has(ext)) continue;
|
|
4711
|
+
if (globRegex && !globRegex.test(entry.name)) continue;
|
|
4712
|
+
files.push(fullPath);
|
|
4713
|
+
}
|
|
3735
4714
|
}
|
|
3736
|
-
return new Promise((resolve10) => {
|
|
3737
|
-
const finalCommand = os10.platform() === "win32" ? `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ${command}` : command;
|
|
3738
|
-
exec2(finalCommand, {
|
|
3739
|
-
timeout,
|
|
3740
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
3741
|
-
shell: getDefaultShell2(),
|
|
3742
|
-
cwd: process.cwd(),
|
|
3743
|
-
env: { ...process.env }
|
|
3744
|
-
}, (err, stdout, stderr) => {
|
|
3745
|
-
if (err) {
|
|
3746
|
-
const exitCode = err.code;
|
|
3747
|
-
const output4 = [
|
|
3748
|
-
stdout ? `stdout:
|
|
3749
|
-
${stdout}` : "",
|
|
3750
|
-
stderr ? `stderr:
|
|
3751
|
-
${stderr}` : "",
|
|
3752
|
-
`Exit code: ${exitCode}`
|
|
3753
|
-
].filter(Boolean).join("\n\n");
|
|
3754
|
-
if (err.killed) {
|
|
3755
|
-
resolve10(makeToolError(`Command timed out after ${timeout / 1e3}s
|
|
3756
|
-
${output4}`));
|
|
3757
|
-
} else {
|
|
3758
|
-
resolve10(makeToolResult(output4 || `Command failed with exit code ${exitCode}`));
|
|
3759
|
-
}
|
|
3760
|
-
return;
|
|
3761
|
-
}
|
|
3762
|
-
const output3 = [
|
|
3763
|
-
stdout ? stdout : "",
|
|
3764
|
-
stderr ? `stderr:
|
|
3765
|
-
${stderr}` : ""
|
|
3766
|
-
].filter(Boolean).join("\n");
|
|
3767
|
-
resolve10(makeToolResult(output3 || "(no output)"));
|
|
3768
|
-
});
|
|
3769
|
-
});
|
|
3770
4715
|
}
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
});
|
|
3782
|
-
const task = { process: proc, output: "", status: "running", exitCode: void 0 };
|
|
3783
|
-
backgroundTasks.set(taskId, task);
|
|
3784
|
-
proc.stdout?.on("data", (data) => {
|
|
3785
|
-
task.output += data.toString();
|
|
3786
|
-
});
|
|
3787
|
-
proc.stderr?.on("data", (data) => {
|
|
3788
|
-
task.output += data.toString();
|
|
3789
|
-
});
|
|
3790
|
-
proc.on("close", (code) => {
|
|
3791
|
-
task.status = code === 0 ? "done" : "error";
|
|
3792
|
-
task.exitCode = code ?? 1;
|
|
3793
|
-
});
|
|
3794
|
-
proc.on("error", (err) => {
|
|
3795
|
-
task.status = "error";
|
|
3796
|
-
task.output += `
|
|
3797
|
-
Process error: ${err.message}`;
|
|
3798
|
-
});
|
|
3799
|
-
return makeToolResult(`Background task started with ID: ${taskId}
|
|
3800
|
-
Use task_output tool to check results.`);
|
|
4716
|
+
try {
|
|
4717
|
+
const stat = fs18.statSync(dirPath);
|
|
4718
|
+
if (stat.isFile()) {
|
|
4719
|
+
return [dirPath];
|
|
4720
|
+
}
|
|
4721
|
+
} catch {
|
|
4722
|
+
return [];
|
|
4723
|
+
}
|
|
4724
|
+
walk(dirPath);
|
|
4725
|
+
return files;
|
|
3801
4726
|
}
|
|
3802
4727
|
|
|
3803
4728
|
// src/tools/list-dir.ts
|
|
3804
4729
|
init_esm_shims();
|
|
3805
4730
|
init_tool();
|
|
3806
|
-
import * as
|
|
3807
|
-
import * as
|
|
4731
|
+
import * as fs19 from "fs";
|
|
4732
|
+
import * as path20 from "path";
|
|
3808
4733
|
var listDirTool = {
|
|
3809
4734
|
name: "list_dir",
|
|
3810
4735
|
description: `List directory contents with file/folder distinction and basic metadata.`,
|
|
@@ -3818,16 +4743,16 @@ var listDirTool = {
|
|
|
3818
4743
|
dangerous: false,
|
|
3819
4744
|
readOnly: true,
|
|
3820
4745
|
async execute(input3) {
|
|
3821
|
-
const dirPath =
|
|
3822
|
-
if (!
|
|
4746
|
+
const dirPath = path20.resolve(input3["path"] ? String(input3["path"]) : process.cwd());
|
|
4747
|
+
if (!fs19.existsSync(dirPath)) {
|
|
3823
4748
|
return makeToolError(`Directory not found: ${dirPath}`);
|
|
3824
4749
|
}
|
|
3825
|
-
const stat =
|
|
4750
|
+
const stat = fs19.statSync(dirPath);
|
|
3826
4751
|
if (!stat.isDirectory()) {
|
|
3827
4752
|
return makeToolError(`Not a directory: ${dirPath}`);
|
|
3828
4753
|
}
|
|
3829
4754
|
try {
|
|
3830
|
-
const entries =
|
|
4755
|
+
const entries = fs19.readdirSync(dirPath, { withFileTypes: true });
|
|
3831
4756
|
const IGNORE = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store", "__pycache__", ".next", "dist", "build"]);
|
|
3832
4757
|
const lines = [];
|
|
3833
4758
|
const dirs = [];
|
|
@@ -3838,7 +4763,7 @@ var listDirTool = {
|
|
|
3838
4763
|
dirs.push(`${entry.name}/`);
|
|
3839
4764
|
} else if (entry.isSymbolicLink()) {
|
|
3840
4765
|
try {
|
|
3841
|
-
const target =
|
|
4766
|
+
const target = fs19.readlinkSync(path20.join(dirPath, entry.name));
|
|
3842
4767
|
files.push(`${entry.name} -> ${target}`);
|
|
3843
4768
|
} catch {
|
|
3844
4769
|
files.push(`${entry.name} -> (broken link)`);
|
|
@@ -3865,9 +4790,95 @@ ${lines.join("\n")}`);
|
|
|
3865
4790
|
init_esm_shims();
|
|
3866
4791
|
init_tool();
|
|
3867
4792
|
import { execSync as execSync4 } from "child_process";
|
|
4793
|
+
function detectConflictFiles(cwd) {
|
|
4794
|
+
try {
|
|
4795
|
+
const output3 = execSync4("git diff --name-only --diff-filter=U", {
|
|
4796
|
+
encoding: "utf-8",
|
|
4797
|
+
cwd,
|
|
4798
|
+
timeout: 1e4
|
|
4799
|
+
});
|
|
4800
|
+
return output3.trim().split("\n").filter(Boolean);
|
|
4801
|
+
} catch {
|
|
4802
|
+
return [];
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4805
|
+
function parseConflictMarkers(fileContent, filePath) {
|
|
4806
|
+
const sections = [];
|
|
4807
|
+
const lines = fileContent.split("\n");
|
|
4808
|
+
let inConflict = false;
|
|
4809
|
+
let startLine = 0;
|
|
4810
|
+
let oursLines = [];
|
|
4811
|
+
let theirsLines = [];
|
|
4812
|
+
let inTheirs = false;
|
|
4813
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4814
|
+
const line = lines[i];
|
|
4815
|
+
if (line.startsWith("<<<<<<<")) {
|
|
4816
|
+
inConflict = true;
|
|
4817
|
+
inTheirs = false;
|
|
4818
|
+
startLine = i + 1;
|
|
4819
|
+
oursLines = [];
|
|
4820
|
+
theirsLines = [];
|
|
4821
|
+
} else if (line.startsWith("=======") && inConflict) {
|
|
4822
|
+
inTheirs = true;
|
|
4823
|
+
} else if (line.startsWith(">>>>>>>") && inConflict) {
|
|
4824
|
+
sections.push({
|
|
4825
|
+
file: filePath,
|
|
4826
|
+
startLine,
|
|
4827
|
+
ours: oursLines.join("\n"),
|
|
4828
|
+
theirs: theirsLines.join("\n")
|
|
4829
|
+
});
|
|
4830
|
+
inConflict = false;
|
|
4831
|
+
inTheirs = false;
|
|
4832
|
+
} else if (inConflict) {
|
|
4833
|
+
if (inTheirs) {
|
|
4834
|
+
theirsLines.push(line);
|
|
4835
|
+
} else {
|
|
4836
|
+
oursLines.push(line);
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
return sections;
|
|
4841
|
+
}
|
|
4842
|
+
function formatConflictReport(cwd) {
|
|
4843
|
+
const conflictFiles = detectConflictFiles(cwd);
|
|
4844
|
+
if (conflictFiles.length === 0) return null;
|
|
4845
|
+
const fs22 = __require("fs");
|
|
4846
|
+
const path23 = __require("path");
|
|
4847
|
+
const lines = [
|
|
4848
|
+
`\u26A0 Merge conflicts detected in ${conflictFiles.length} file(s):`,
|
|
4849
|
+
""
|
|
4850
|
+
];
|
|
4851
|
+
for (const file of conflictFiles) {
|
|
4852
|
+
const fullPath = path23.join(cwd, file);
|
|
4853
|
+
let content;
|
|
4854
|
+
try {
|
|
4855
|
+
content = fs22.readFileSync(fullPath, "utf-8");
|
|
4856
|
+
} catch {
|
|
4857
|
+
lines.push(` - ${file} (cannot read)`);
|
|
4858
|
+
continue;
|
|
4859
|
+
}
|
|
4860
|
+
const sections = parseConflictMarkers(content, file);
|
|
4861
|
+
lines.push(` - ${file} (${sections.length} conflict(s))`);
|
|
4862
|
+
for (let i = 0; i < sections.length; i++) {
|
|
4863
|
+
const s = sections[i];
|
|
4864
|
+
lines.push(` [Conflict ${i + 1} at line ${s.startLine}]`);
|
|
4865
|
+
lines.push(` OURS:`);
|
|
4866
|
+
for (const l of s.ours.split("\n").slice(0, 5)) {
|
|
4867
|
+
lines.push(` ${l}`);
|
|
4868
|
+
}
|
|
4869
|
+
if (s.ours.split("\n").length > 5) lines.push(` ... (${s.ours.split("\n").length} lines)`);
|
|
4870
|
+
lines.push(` THEIRS:`);
|
|
4871
|
+
for (const l of s.theirs.split("\n").slice(0, 5)) {
|
|
4872
|
+
lines.push(` ${l}`);
|
|
4873
|
+
}
|
|
4874
|
+
if (s.theirs.split("\n").length > 5) lines.push(` ... (${s.theirs.split("\n").length} lines)`);
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
return lines.join("\n");
|
|
4878
|
+
}
|
|
3868
4879
|
var gitTool = {
|
|
3869
4880
|
name: "git",
|
|
3870
|
-
description: `Execute git commands with safety checks. Blocks dangerous operations like force push, hard reset, and amend. Use for status, diff, log, commit, branch operations.`,
|
|
4881
|
+
description: `Execute git commands with safety checks. Blocks dangerous operations like force push, hard reset, and amend. Use for status, diff, log, commit, branch operations. When merge/pull results in conflicts, automatically detects and reports them.`,
|
|
3871
4882
|
inputSchema: {
|
|
3872
4883
|
type: "object",
|
|
3873
4884
|
properties: {
|
|
@@ -3896,7 +4907,9 @@ var gitTool = {
|
|
|
3896
4907
|
}
|
|
3897
4908
|
}
|
|
3898
4909
|
const readOnlyPrefixes = ["status", "diff", "log", "show", "branch", "tag", "remote", "stash list", "ls-files"];
|
|
3899
|
-
const
|
|
4910
|
+
const _isReadOnly = readOnlyPrefixes.some((p) => command.startsWith(p));
|
|
4911
|
+
const mergeCommands = /^(merge|pull|rebase|cherry-pick)\b/;
|
|
4912
|
+
const isMergeCommand = mergeCommands.test(command);
|
|
3900
4913
|
try {
|
|
3901
4914
|
const result = execSync4(`git ${command}`, {
|
|
3902
4915
|
encoding: "utf-8",
|
|
@@ -3904,9 +4917,26 @@ var gitTool = {
|
|
|
3904
4917
|
timeout: 3e4,
|
|
3905
4918
|
cwd: process.cwd()
|
|
3906
4919
|
});
|
|
4920
|
+
if (isMergeCommand) {
|
|
4921
|
+
const conflictReport = formatConflictReport(process.cwd());
|
|
4922
|
+
if (conflictReport) {
|
|
4923
|
+
return makeToolResult(`${result}
|
|
4924
|
+
|
|
4925
|
+
${conflictReport}`);
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
3907
4928
|
return makeToolResult(result || "(no output)");
|
|
3908
4929
|
} catch (err) {
|
|
3909
4930
|
const output3 = [err.stdout, err.stderr].filter(Boolean).join("\n");
|
|
4931
|
+
if (isMergeCommand) {
|
|
4932
|
+
const conflictReport = formatConflictReport(process.cwd());
|
|
4933
|
+
if (conflictReport) {
|
|
4934
|
+
return makeToolError(`git ${command} failed with conflicts:
|
|
4935
|
+
${output3}
|
|
4936
|
+
|
|
4937
|
+
${conflictReport}`);
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
3910
4940
|
return makeToolError(`git ${command} failed:
|
|
3911
4941
|
${output3 || err.message}`);
|
|
3912
4942
|
}
|
|
@@ -3917,15 +4947,92 @@ ${output3 || err.message}`);
|
|
|
3917
4947
|
init_esm_shims();
|
|
3918
4948
|
init_tool();
|
|
3919
4949
|
var cache = /* @__PURE__ */ new Map();
|
|
3920
|
-
var
|
|
4950
|
+
var DEFAULT_CACHE_TTL = 15 * 60 * 1e3;
|
|
4951
|
+
var MAX_RESPONSE_SIZE = 5 * 1024 * 1024;
|
|
4952
|
+
var MAX_TEXT_LEN = 5e4;
|
|
4953
|
+
var USER_AGENT = "Mozilla/5.0 (compatible; Codi/0.1; +https://github.com/gemdoq/codi)";
|
|
4954
|
+
function extractCharset(contentType) {
|
|
4955
|
+
const match = contentType.match(/charset=([^\s;]+)/i);
|
|
4956
|
+
return match && match[1] ? match[1].replace(/['"]/g, "") : null;
|
|
4957
|
+
}
|
|
4958
|
+
function parseCacheMaxAge(headers) {
|
|
4959
|
+
const cc = headers.get("cache-control");
|
|
4960
|
+
if (!cc) return null;
|
|
4961
|
+
const match = cc.match(/max-age=(\d+)/);
|
|
4962
|
+
if (!match || !match[1]) return null;
|
|
4963
|
+
const seconds = parseInt(match[1], 10);
|
|
4964
|
+
if (isNaN(seconds) || seconds <= 0) return null;
|
|
4965
|
+
const clamped = Math.max(60, Math.min(seconds, 3600));
|
|
4966
|
+
return clamped * 1e3;
|
|
4967
|
+
}
|
|
4968
|
+
function isPdf(url, contentType) {
|
|
4969
|
+
return contentType.includes("application/pdf") || /\.pdf(\?|#|$)/i.test(url);
|
|
4970
|
+
}
|
|
4971
|
+
function isJson(contentType) {
|
|
4972
|
+
return contentType.includes("application/json") || contentType.includes("+json");
|
|
4973
|
+
}
|
|
4974
|
+
async function extractHtmlText(html) {
|
|
4975
|
+
const { load } = await import("cheerio");
|
|
4976
|
+
const $ = load(html);
|
|
4977
|
+
$('script, style, nav, header, footer, iframe, noscript, svg, [role="navigation"], [role="banner"], .sidebar, .ad, .ads, .advertisement').remove();
|
|
4978
|
+
const main2 = $('main, article, .content, #content, .main, [role="main"]').first();
|
|
4979
|
+
let text = main2.length ? main2.text() : $("body").text();
|
|
4980
|
+
text = text.split("\n").map((line) => line.replace(/\s+/g, " ").trim()).filter((line) => line.length > 0).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
4981
|
+
return text;
|
|
4982
|
+
}
|
|
4983
|
+
async function extractPdfText(buffer) {
|
|
4984
|
+
const { PDFParse } = await import("pdf-parse");
|
|
4985
|
+
const parser = new PDFParse({ data: new Uint8Array(buffer) });
|
|
4986
|
+
try {
|
|
4987
|
+
const textResult = await parser.getText();
|
|
4988
|
+
const text = textResult.text?.trim() || "";
|
|
4989
|
+
const totalPages = textResult.total ?? "unknown";
|
|
4990
|
+
let infoStr = `Pages: ${totalPages}`;
|
|
4991
|
+
try {
|
|
4992
|
+
const info = await parser.getInfo();
|
|
4993
|
+
if (info.info?.Title) infoStr += ` | Title: ${info.info.Title}`;
|
|
4994
|
+
if (info.info?.Author) infoStr += ` | Author: ${info.info.Author}`;
|
|
4995
|
+
} catch {
|
|
4996
|
+
}
|
|
4997
|
+
return `[PDF] ${infoStr}
|
|
4998
|
+
|
|
4999
|
+
${text}`;
|
|
5000
|
+
} finally {
|
|
5001
|
+
await parser.destroy().catch(() => {
|
|
5002
|
+
});
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
function formatJson(raw) {
|
|
5006
|
+
try {
|
|
5007
|
+
const parsed = JSON.parse(raw);
|
|
5008
|
+
return `[JSON Response]
|
|
5009
|
+
${JSON.stringify(parsed, null, 2)}`;
|
|
5010
|
+
} catch {
|
|
5011
|
+
return `[JSON Response - parse error]
|
|
5012
|
+
${raw}`;
|
|
5013
|
+
}
|
|
5014
|
+
}
|
|
5015
|
+
async function decodeResponse(response, contentType) {
|
|
5016
|
+
const charset = extractCharset(contentType);
|
|
5017
|
+
if (charset && charset.toLowerCase() !== "utf-8" && charset.toLowerCase() !== "utf8") {
|
|
5018
|
+
const buffer = await response.arrayBuffer();
|
|
5019
|
+
const decoder = new TextDecoder(charset);
|
|
5020
|
+
return decoder.decode(buffer);
|
|
5021
|
+
}
|
|
5022
|
+
return response.text();
|
|
5023
|
+
}
|
|
3921
5024
|
var webFetchTool = {
|
|
3922
5025
|
name: "web_fetch",
|
|
3923
|
-
description: `Fetch content from a URL
|
|
5026
|
+
description: `Fetch content from a URL and return extracted text. Supports HTML (cheerio), PDF (pdf-parse), and JSON. Includes caching. HTTP URLs are upgraded to HTTPS.`,
|
|
3924
5027
|
inputSchema: {
|
|
3925
5028
|
type: "object",
|
|
3926
5029
|
properties: {
|
|
3927
5030
|
url: { type: "string", description: "URL to fetch" },
|
|
3928
|
-
prompt: { type: "string", description: "What information to extract from the page" }
|
|
5031
|
+
prompt: { type: "string", description: "What information to extract from the page" },
|
|
5032
|
+
cache_ttl: {
|
|
5033
|
+
type: "number",
|
|
5034
|
+
description: "Cache TTL in seconds (default: 900, i.e. 15 minutes). Set to 0 to bypass cache."
|
|
5035
|
+
}
|
|
3929
5036
|
},
|
|
3930
5037
|
required: ["url", "prompt"]
|
|
3931
5038
|
},
|
|
@@ -3934,50 +5041,103 @@ var webFetchTool = {
|
|
|
3934
5041
|
async execute(input3) {
|
|
3935
5042
|
let url = String(input3["url"]);
|
|
3936
5043
|
const prompt = String(input3["prompt"] || "");
|
|
5044
|
+
const cacheTtlInput = input3["cache_ttl"];
|
|
5045
|
+
const requestTtl = typeof cacheTtlInput === "number" ? cacheTtlInput * 1e3 : null;
|
|
5046
|
+
const bypassCache = requestTtl === 0;
|
|
3937
5047
|
if (url.startsWith("http://")) {
|
|
3938
5048
|
url = url.replace("http://", "https://");
|
|
3939
5049
|
}
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
5050
|
+
if (!bypassCache) {
|
|
5051
|
+
const cached = cache.get(url);
|
|
5052
|
+
if (cached && Date.now() - cached.timestamp < cached.ttl) {
|
|
5053
|
+
return makeToolResult(
|
|
5054
|
+
`[Cached] ${prompt ? `Query: ${prompt}
|
|
3943
5055
|
|
|
3944
|
-
` : ""}${cached.content}`
|
|
5056
|
+
` : ""}${cached.content}`
|
|
5057
|
+
);
|
|
5058
|
+
}
|
|
3945
5059
|
}
|
|
3946
5060
|
try {
|
|
3947
5061
|
const response = await fetch(url, {
|
|
3948
5062
|
headers: {
|
|
3949
|
-
"User-Agent":
|
|
3950
|
-
|
|
5063
|
+
"User-Agent": USER_AGENT,
|
|
5064
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,application/json;q=0.8,application/pdf;q=0.7,*/*;q=0.5"
|
|
3951
5065
|
},
|
|
3952
5066
|
redirect: "follow",
|
|
3953
5067
|
signal: AbortSignal.timeout(3e4)
|
|
3954
5068
|
});
|
|
3955
5069
|
if (!response.ok) {
|
|
3956
|
-
|
|
5070
|
+
const status = response.status;
|
|
5071
|
+
const statusText = response.statusText || "Unknown";
|
|
5072
|
+
let detail = `HTTP ${status} ${statusText}`;
|
|
5073
|
+
if (status === 403 || status === 401) {
|
|
5074
|
+
detail += " - \uC811\uADFC\uC774 \uCC28\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC778\uC99D\uC774 \uD544\uC694\uD558\uAC70\uB098 \uBD07 \uCC28\uB2E8\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4.";
|
|
5075
|
+
} else if (status === 404) {
|
|
5076
|
+
detail += " - \uD398\uC774\uC9C0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.";
|
|
5077
|
+
} else if (status === 429) {
|
|
5078
|
+
detail += " - \uC694\uCCAD\uC774 \uB108\uBB34 \uB9CE\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.";
|
|
5079
|
+
} else if (status >= 500) {
|
|
5080
|
+
detail += " - \uC11C\uBC84 \uC624\uB958\uC785\uB2C8\uB2E4.";
|
|
5081
|
+
}
|
|
5082
|
+
if (response.redirected) {
|
|
5083
|
+
detail += `
|
|
5084
|
+
Redirected to: ${response.url}`;
|
|
5085
|
+
}
|
|
5086
|
+
return makeToolError(detail);
|
|
5087
|
+
}
|
|
5088
|
+
const contentLength = response.headers.get("content-length");
|
|
5089
|
+
if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) {
|
|
5090
|
+
return makeToolError(
|
|
5091
|
+
`\uC751\uB2F5 \uD06C\uAE30\uAC00 \uB108\uBB34 \uD07D\uB2C8\uB2E4 (${(parseInt(contentLength, 10) / 1024 / 1024).toFixed(1)}MB). \uCD5C\uB300 5MB\uAE4C\uC9C0 \uC9C0\uC6D0\uD569\uB2C8\uB2E4.`
|
|
5092
|
+
);
|
|
3957
5093
|
}
|
|
3958
5094
|
const contentType = response.headers.get("content-type") || "";
|
|
3959
5095
|
let text;
|
|
3960
|
-
if (
|
|
3961
|
-
const
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
5096
|
+
if (isPdf(url, contentType)) {
|
|
5097
|
+
const buffer = await response.arrayBuffer();
|
|
5098
|
+
if (buffer.byteLength > MAX_RESPONSE_SIZE) {
|
|
5099
|
+
return makeToolError(
|
|
5100
|
+
`PDF \uD06C\uAE30\uAC00 \uB108\uBB34 \uD07D\uB2C8\uB2E4 (${(buffer.byteLength / 1024 / 1024).toFixed(1)}MB). \uCD5C\uB300 5MB\uAE4C\uC9C0 \uC9C0\uC6D0\uD569\uB2C8\uB2E4.`
|
|
5101
|
+
);
|
|
5102
|
+
}
|
|
5103
|
+
text = await extractPdfText(buffer);
|
|
5104
|
+
} else if (isJson(contentType)) {
|
|
5105
|
+
const raw = await decodeResponse(response, contentType);
|
|
5106
|
+
text = formatJson(raw);
|
|
5107
|
+
} else if (contentType.includes("text/html") || contentType.includes("application/xhtml")) {
|
|
5108
|
+
const html = await decodeResponse(response, contentType);
|
|
5109
|
+
text = await extractHtmlText(html);
|
|
3967
5110
|
} else {
|
|
3968
|
-
text = await response
|
|
5111
|
+
text = await decodeResponse(response, contentType);
|
|
5112
|
+
}
|
|
5113
|
+
if (text.length > MAX_TEXT_LEN) {
|
|
5114
|
+
text = text.slice(0, MAX_TEXT_LEN) + "\n\n... (truncated)";
|
|
3969
5115
|
}
|
|
3970
|
-
const
|
|
3971
|
-
if (
|
|
3972
|
-
text
|
|
5116
|
+
const effectiveTtl = requestTtl ?? parseCacheMaxAge(response.headers) ?? DEFAULT_CACHE_TTL;
|
|
5117
|
+
if (!bypassCache) {
|
|
5118
|
+
cache.set(url, { content: text, timestamp: Date.now(), ttl: effectiveTtl });
|
|
3973
5119
|
}
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
5120
|
+
let prefix = `URL: ${url}`;
|
|
5121
|
+
if (response.redirected && response.url !== url) {
|
|
5122
|
+
prefix += `
|
|
5123
|
+
Redirected to: ${response.url}`;
|
|
5124
|
+
}
|
|
5125
|
+
if (prompt) {
|
|
5126
|
+
prefix += `
|
|
5127
|
+
Query: ${prompt}`;
|
|
5128
|
+
}
|
|
5129
|
+
return makeToolResult(`${prefix}
|
|
3977
5130
|
|
|
3978
|
-
${text}`
|
|
5131
|
+
${text}`);
|
|
3979
5132
|
} catch (err) {
|
|
3980
|
-
|
|
5133
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5134
|
+
if (message.includes("TimeoutError") || message.includes("aborted")) {
|
|
5135
|
+
return makeToolError(`\uC694\uCCAD \uC2DC\uAC04 \uCD08\uACFC (30\uCD08): ${url}`);
|
|
5136
|
+
}
|
|
5137
|
+
if (message.includes("ENOTFOUND") || message.includes("getaddrinfo")) {
|
|
5138
|
+
return makeToolError(`\uB3C4\uBA54\uC778\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${url}`);
|
|
5139
|
+
}
|
|
5140
|
+
return makeToolError(`URL \uAC00\uC838\uC624\uAE30 \uC2E4\uD328: ${message}`);
|
|
3981
5141
|
}
|
|
3982
5142
|
}
|
|
3983
5143
|
};
|
|
@@ -3985,73 +5145,181 @@ ${text}` : text);
|
|
|
3985
5145
|
// src/tools/web-search.ts
|
|
3986
5146
|
init_esm_shims();
|
|
3987
5147
|
init_tool();
|
|
5148
|
+
var searchCache = /* @__PURE__ */ new Map();
|
|
5149
|
+
var SEARCH_CACHE_TTL = 10 * 60 * 1e3;
|
|
5150
|
+
var USER_AGENT2 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
5151
|
+
function extractRealUrl(href) {
|
|
5152
|
+
try {
|
|
5153
|
+
const urlObj = new URL(href, "https://duckduckgo.com");
|
|
5154
|
+
return urlObj.searchParams.get("uddg") || href;
|
|
5155
|
+
} catch {
|
|
5156
|
+
return href;
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
5159
|
+
function normalizeUrl(url) {
|
|
5160
|
+
try {
|
|
5161
|
+
const u = new URL(url);
|
|
5162
|
+
const trackingParams = ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term", "ref", "fbclid", "gclid"];
|
|
5163
|
+
for (const param of trackingParams) {
|
|
5164
|
+
u.searchParams.delete(param);
|
|
5165
|
+
}
|
|
5166
|
+
let path23 = u.pathname.replace(/\/+$/, "") || "/";
|
|
5167
|
+
return `${u.hostname}${path23}${u.search}`;
|
|
5168
|
+
} catch {
|
|
5169
|
+
return url;
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5172
|
+
function deduplicateResults(results) {
|
|
5173
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5174
|
+
const deduped = [];
|
|
5175
|
+
for (const result of results) {
|
|
5176
|
+
const normalized = normalizeUrl(result.url);
|
|
5177
|
+
if (!seen.has(normalized) && result.title.length > 0) {
|
|
5178
|
+
seen.add(normalized);
|
|
5179
|
+
deduped.push(result);
|
|
5180
|
+
}
|
|
5181
|
+
}
|
|
5182
|
+
return deduped;
|
|
5183
|
+
}
|
|
5184
|
+
async function searchDuckDuckGo(query, maxResults) {
|
|
5185
|
+
const encoded = encodeURIComponent(query);
|
|
5186
|
+
const url = `https://html.duckduckgo.com/html/?q=${encoded}`;
|
|
5187
|
+
const response = await fetch(url, {
|
|
5188
|
+
headers: {
|
|
5189
|
+
"User-Agent": USER_AGENT2,
|
|
5190
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
5191
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
5192
|
+
},
|
|
5193
|
+
signal: AbortSignal.timeout(15e3)
|
|
5194
|
+
});
|
|
5195
|
+
if (!response.ok) {
|
|
5196
|
+
throw new Error(`DuckDuckGo HTTP ${response.status} ${response.statusText}`);
|
|
5197
|
+
}
|
|
5198
|
+
const html = await response.text();
|
|
5199
|
+
if (html.includes("If this error persists") || html.includes("blocked")) {
|
|
5200
|
+
throw new Error("DuckDuckGo\uC5D0\uC11C \uC694\uCCAD\uC774 \uCC28\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4");
|
|
5201
|
+
}
|
|
5202
|
+
const { load } = await import("cheerio");
|
|
5203
|
+
const $ = load(html);
|
|
5204
|
+
const results = [];
|
|
5205
|
+
$(".result").each((_i, el) => {
|
|
5206
|
+
const $el = $(el);
|
|
5207
|
+
if ($el.hasClass("result--ad") || $el.find(".badge--ad").length > 0) {
|
|
5208
|
+
return;
|
|
5209
|
+
}
|
|
5210
|
+
const titleEl = $el.find(".result__title a, .result__a");
|
|
5211
|
+
const title = titleEl.text().trim();
|
|
5212
|
+
const href = titleEl.attr("href") || "";
|
|
5213
|
+
const snippet = $el.find(".result__snippet").text().trim();
|
|
5214
|
+
if (title && href) {
|
|
5215
|
+
const actualUrl = extractRealUrl(href);
|
|
5216
|
+
if (actualUrl.startsWith("http://") || actualUrl.startsWith("https://")) {
|
|
5217
|
+
results.push({ title, url: actualUrl, snippet });
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
5220
|
+
});
|
|
5221
|
+
return deduplicateResults(results).slice(0, maxResults);
|
|
5222
|
+
}
|
|
5223
|
+
async function searchDuckDuckGoLite(query, maxResults) {
|
|
5224
|
+
const encoded = encodeURIComponent(query);
|
|
5225
|
+
const url = `https://lite.duckduckgo.com/lite/?q=${encoded}`;
|
|
5226
|
+
const response = await fetch(url, {
|
|
5227
|
+
headers: {
|
|
5228
|
+
"User-Agent": USER_AGENT2,
|
|
5229
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
5230
|
+
},
|
|
5231
|
+
signal: AbortSignal.timeout(15e3)
|
|
5232
|
+
});
|
|
5233
|
+
if (!response.ok) {
|
|
5234
|
+
throw new Error(`DuckDuckGo Lite HTTP ${response.status}`);
|
|
5235
|
+
}
|
|
5236
|
+
const html = await response.text();
|
|
5237
|
+
const { load } = await import("cheerio");
|
|
5238
|
+
const $ = load(html);
|
|
5239
|
+
const results = [];
|
|
5240
|
+
$("a.result-link").each((_i, el) => {
|
|
5241
|
+
const $a = $(el);
|
|
5242
|
+
const title = $a.text().trim();
|
|
5243
|
+
const href = $a.attr("href") || "";
|
|
5244
|
+
if (title && href) {
|
|
5245
|
+
const actualUrl = extractRealUrl(href);
|
|
5246
|
+
const $row = $a.closest("tr");
|
|
5247
|
+
const snippet = $row.next("tr").find(".result-snippet").text().trim() || $row.next("tr").find("td").last().text().trim();
|
|
5248
|
+
if (actualUrl.startsWith("http://") || actualUrl.startsWith("https://")) {
|
|
5249
|
+
results.push({ title, url: actualUrl, snippet });
|
|
5250
|
+
}
|
|
5251
|
+
}
|
|
5252
|
+
});
|
|
5253
|
+
return deduplicateResults(results).slice(0, maxResults);
|
|
5254
|
+
}
|
|
3988
5255
|
var webSearchTool = {
|
|
3989
5256
|
name: "web_search",
|
|
3990
|
-
description: `Search the web for information. Returns search results with titles, URLs, and snippets. Uses DuckDuckGo
|
|
5257
|
+
description: `Search the web for information. Returns search results with titles, URLs, and snippets. Uses DuckDuckGo. Falls back to DuckDuckGo Lite if the main search fails.`,
|
|
3991
5258
|
inputSchema: {
|
|
3992
5259
|
type: "object",
|
|
3993
5260
|
properties: {
|
|
3994
|
-
query: { type: "string", description: "Search query" }
|
|
5261
|
+
query: { type: "string", description: "Search query" },
|
|
5262
|
+
max_results: {
|
|
5263
|
+
type: "number",
|
|
5264
|
+
description: "Maximum number of results (default: 10, max: 20)"
|
|
5265
|
+
}
|
|
3995
5266
|
},
|
|
3996
5267
|
required: ["query"]
|
|
3997
5268
|
},
|
|
3998
5269
|
dangerous: true,
|
|
3999
5270
|
readOnly: true,
|
|
4000
5271
|
async execute(input3) {
|
|
4001
|
-
const query = String(input3["query"]);
|
|
5272
|
+
const query = String(input3["query"]).trim();
|
|
5273
|
+
if (!query) {
|
|
5274
|
+
return makeToolError("\uAC80\uC0C9\uC5B4\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4.");
|
|
5275
|
+
}
|
|
5276
|
+
const rawMax = typeof input3["max_results"] === "number" ? input3["max_results"] : 10;
|
|
5277
|
+
const maxResults = Math.max(1, Math.min(20, Math.round(rawMax)));
|
|
5278
|
+
const cacheKey = `${query}|${maxResults}`;
|
|
5279
|
+
const cached = searchCache.get(cacheKey);
|
|
5280
|
+
if (cached && Date.now() - cached.timestamp < SEARCH_CACHE_TTL) {
|
|
5281
|
+
return makeToolResult(`[Cached] ${cached.results}`);
|
|
5282
|
+
}
|
|
5283
|
+
let results = [];
|
|
5284
|
+
let fallbackUsed = false;
|
|
4002
5285
|
try {
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
const $ = load(html);
|
|
4017
|
-
const results = [];
|
|
4018
|
-
$(".result").each((i, el) => {
|
|
4019
|
-
if (i >= 10) return false;
|
|
4020
|
-
const $el = $(el);
|
|
4021
|
-
const title = $el.find(".result__title a").text().trim();
|
|
4022
|
-
const href = $el.find(".result__title a").attr("href") || "";
|
|
4023
|
-
const snippet = $el.find(".result__snippet").text().trim();
|
|
4024
|
-
if (title && href) {
|
|
4025
|
-
let actualUrl = href;
|
|
4026
|
-
try {
|
|
4027
|
-
const urlObj = new URL(href, "https://duckduckgo.com");
|
|
4028
|
-
actualUrl = urlObj.searchParams.get("uddg") || href;
|
|
4029
|
-
} catch {
|
|
4030
|
-
actualUrl = href;
|
|
4031
|
-
}
|
|
4032
|
-
results.push({ title, url: actualUrl, snippet });
|
|
4033
|
-
}
|
|
4034
|
-
});
|
|
4035
|
-
if (results.length === 0) {
|
|
4036
|
-
return makeToolResult(`No results found for: ${query}`);
|
|
5286
|
+
results = await searchDuckDuckGo(query, maxResults);
|
|
5287
|
+
} catch (primaryErr) {
|
|
5288
|
+
try {
|
|
5289
|
+
results = await searchDuckDuckGoLite(query, maxResults);
|
|
5290
|
+
fallbackUsed = true;
|
|
5291
|
+
} catch (fallbackErr) {
|
|
5292
|
+
const primaryMsg = primaryErr instanceof Error ? primaryErr.message : String(primaryErr);
|
|
5293
|
+
const fallbackMsg = fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr);
|
|
5294
|
+
return makeToolError(
|
|
5295
|
+
`\uAC80\uC0C9 \uC2E4\uD328:
|
|
5296
|
+
Primary: ${primaryMsg}
|
|
5297
|
+
Fallback: ${fallbackMsg}`
|
|
5298
|
+
);
|
|
4037
5299
|
}
|
|
4038
|
-
const formatted = results.map((r, i) => `${i + 1}. ${r.title}
|
|
4039
|
-
${r.url}
|
|
4040
|
-
${r.snippet}`).join("\n\n");
|
|
4041
|
-
return makeToolResult(`Search results for: ${query}
|
|
4042
|
-
|
|
4043
|
-
${formatted}`);
|
|
4044
|
-
} catch (err) {
|
|
4045
|
-
return makeToolError(`Search failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4046
5300
|
}
|
|
5301
|
+
if (results.length === 0) {
|
|
5302
|
+
return makeToolResult(`"${query}"\uC5D0 \uB300\uD55C \uAC80\uC0C9 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`);
|
|
5303
|
+
}
|
|
5304
|
+
const formatted = results.map(
|
|
5305
|
+
(r, i) => `${i + 1}. ${r.title}
|
|
5306
|
+
${r.url}${r.snippet ? `
|
|
5307
|
+
${r.snippet}` : ""}`
|
|
5308
|
+
).join("\n\n");
|
|
5309
|
+
const header = fallbackUsed ? `Search results for: ${query} (fallback engine used)` : `Search results for: ${query}`;
|
|
5310
|
+
const output3 = `${header}
|
|
5311
|
+
|
|
5312
|
+
${formatted}`;
|
|
5313
|
+
searchCache.set(cacheKey, { results: output3, timestamp: Date.now() });
|
|
5314
|
+
return makeToolResult(output3);
|
|
4047
5315
|
}
|
|
4048
5316
|
};
|
|
4049
5317
|
|
|
4050
5318
|
// src/tools/notebook-edit.ts
|
|
4051
5319
|
init_esm_shims();
|
|
4052
5320
|
init_tool();
|
|
4053
|
-
import * as
|
|
4054
|
-
import * as
|
|
5321
|
+
import * as fs20 from "fs";
|
|
5322
|
+
import * as path21 from "path";
|
|
4055
5323
|
var notebookEditTool = {
|
|
4056
5324
|
name: "notebook_edit",
|
|
4057
5325
|
description: `Edit Jupyter notebook (.ipynb) cells. Supports replacing, inserting, and deleting cells.`,
|
|
@@ -4069,16 +5337,16 @@ var notebookEditTool = {
|
|
|
4069
5337
|
dangerous: true,
|
|
4070
5338
|
readOnly: false,
|
|
4071
5339
|
async execute(input3) {
|
|
4072
|
-
const nbPath =
|
|
5340
|
+
const nbPath = path21.resolve(String(input3["notebook_path"]));
|
|
4073
5341
|
const cellNumber = input3["cell_number"];
|
|
4074
5342
|
const newSource = String(input3["new_source"]);
|
|
4075
5343
|
const cellType = input3["cell_type"] || "code";
|
|
4076
5344
|
const editMode = input3["edit_mode"] || "replace";
|
|
4077
|
-
if (!
|
|
5345
|
+
if (!fs20.existsSync(nbPath)) {
|
|
4078
5346
|
return makeToolError(`Notebook not found: ${nbPath}`);
|
|
4079
5347
|
}
|
|
4080
5348
|
try {
|
|
4081
|
-
const content =
|
|
5349
|
+
const content = fs20.readFileSync(nbPath, "utf-8");
|
|
4082
5350
|
const nb = JSON.parse(content);
|
|
4083
5351
|
if (!nb.cells || !Array.isArray(nb.cells)) {
|
|
4084
5352
|
return makeToolError("Invalid notebook format: no cells array");
|
|
@@ -4123,7 +5391,7 @@ var notebookEditTool = {
|
|
|
4123
5391
|
default:
|
|
4124
5392
|
return makeToolError(`Unknown edit_mode: ${editMode}`);
|
|
4125
5393
|
}
|
|
4126
|
-
|
|
5394
|
+
fs20.writeFileSync(nbPath, JSON.stringify(nb, null, 1), "utf-8");
|
|
4127
5395
|
return makeToolResult(`Notebook ${editMode}d cell in ${nbPath} (${nb.cells.length} cells total)`);
|
|
4128
5396
|
} catch (err) {
|
|
4129
5397
|
return makeToolError(`Notebook edit failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -4138,7 +5406,7 @@ init_task_tools();
|
|
|
4138
5406
|
init_esm_shims();
|
|
4139
5407
|
init_tool();
|
|
4140
5408
|
import * as readline4 from "readline/promises";
|
|
4141
|
-
import
|
|
5409
|
+
import chalk13 from "chalk";
|
|
4142
5410
|
var askUserTool = {
|
|
4143
5411
|
name: "ask_user",
|
|
4144
5412
|
description: `Ask the user a question with optional choices. Use to gather preferences, clarify requirements, or get decisions.`,
|
|
@@ -4169,21 +5437,21 @@ var askUserTool = {
|
|
|
4169
5437
|
const options = input3["options"];
|
|
4170
5438
|
const multiSelect = input3["multiSelect"] === true;
|
|
4171
5439
|
console.log("");
|
|
4172
|
-
console.log(
|
|
5440
|
+
console.log(chalk13.cyan.bold("? ") + chalk13.bold(question));
|
|
4173
5441
|
if (options && options.length > 0) {
|
|
4174
5442
|
console.log("");
|
|
4175
5443
|
for (let i = 0; i < options.length; i++) {
|
|
4176
5444
|
const opt = options[i];
|
|
4177
|
-
console.log(
|
|
5445
|
+
console.log(chalk13.cyan(` ${i + 1}.`) + ` ${opt.label}${opt.description ? chalk13.dim(` - ${opt.description}`) : ""}`);
|
|
4178
5446
|
}
|
|
4179
|
-
console.log(
|
|
5447
|
+
console.log(chalk13.dim(` ${options.length + 1}. Other (type custom response)`));
|
|
4180
5448
|
console.log("");
|
|
4181
5449
|
const rl2 = readline4.createInterface({
|
|
4182
5450
|
input: process.stdin,
|
|
4183
5451
|
output: process.stdout
|
|
4184
5452
|
});
|
|
4185
5453
|
try {
|
|
4186
|
-
const prompt = multiSelect ?
|
|
5454
|
+
const prompt = multiSelect ? chalk13.dim("Enter numbers separated by commas: ") : chalk13.dim("Enter number or type response: ");
|
|
4187
5455
|
const answer = await rl2.question(prompt);
|
|
4188
5456
|
rl2.close();
|
|
4189
5457
|
if (multiSelect) {
|
|
@@ -4209,7 +5477,7 @@ var askUserTool = {
|
|
|
4209
5477
|
output: process.stdout
|
|
4210
5478
|
});
|
|
4211
5479
|
try {
|
|
4212
|
-
const answer = await rl.question(
|
|
5480
|
+
const answer = await rl.question(chalk13.dim("> "));
|
|
4213
5481
|
rl.close();
|
|
4214
5482
|
return makeToolResult(`User response: ${answer}`);
|
|
4215
5483
|
} catch {
|
|
@@ -4219,6 +5487,133 @@ var askUserTool = {
|
|
|
4219
5487
|
}
|
|
4220
5488
|
};
|
|
4221
5489
|
|
|
5490
|
+
// src/tools/memory-tool.ts
|
|
5491
|
+
init_esm_shims();
|
|
5492
|
+
init_tool();
|
|
5493
|
+
import * as fs21 from "fs";
|
|
5494
|
+
import * as path22 from "path";
|
|
5495
|
+
function buildFrontmatter(topic, description) {
|
|
5496
|
+
return `---
|
|
5497
|
+
name: ${topic}
|
|
5498
|
+
description: ${description}
|
|
5499
|
+
type: project
|
|
5500
|
+
---
|
|
5501
|
+
`;
|
|
5502
|
+
}
|
|
5503
|
+
function parseFrontmatter(content) {
|
|
5504
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
5505
|
+
if (!match) return { body: content };
|
|
5506
|
+
const meta = {};
|
|
5507
|
+
for (const line of match[1].split("\n")) {
|
|
5508
|
+
const idx = line.indexOf(":");
|
|
5509
|
+
if (idx !== -1) {
|
|
5510
|
+
meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
return { name: meta["name"], description: meta["description"], body: match[2] };
|
|
5514
|
+
}
|
|
5515
|
+
function updateIndex(memoryDir) {
|
|
5516
|
+
const indexPath = path22.join(memoryDir, "MEMORY.md");
|
|
5517
|
+
const files = fs21.readdirSync(memoryDir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort();
|
|
5518
|
+
const lines = ["# Project Memory", ""];
|
|
5519
|
+
if (files.length === 0) {
|
|
5520
|
+
lines.push("No topics saved yet.");
|
|
5521
|
+
} else {
|
|
5522
|
+
lines.push("| Topic | Description |");
|
|
5523
|
+
lines.push("|-------|-------------|");
|
|
5524
|
+
for (const file of files) {
|
|
5525
|
+
const content = fs21.readFileSync(path22.join(memoryDir, file), "utf-8");
|
|
5526
|
+
const parsed = parseFrontmatter(content);
|
|
5527
|
+
const topic = file.replace(".md", "");
|
|
5528
|
+
const desc = parsed.description || "";
|
|
5529
|
+
lines.push(`| [${topic}](${file}) | ${desc} |`);
|
|
5530
|
+
}
|
|
5531
|
+
}
|
|
5532
|
+
lines.push("");
|
|
5533
|
+
fs21.writeFileSync(indexPath, lines.join("\n"), "utf-8");
|
|
5534
|
+
}
|
|
5535
|
+
var updateMemoryTool = {
|
|
5536
|
+
name: "update_memory",
|
|
5537
|
+
description: `Save, delete, or list project memory topics. Use this to persist important information (architecture decisions, user preferences, patterns, etc.) across conversations.
|
|
5538
|
+
- save: Create or update a memory topic file with content
|
|
5539
|
+
- delete: Remove a memory topic file
|
|
5540
|
+
- list: List all existing memory topics with descriptions`,
|
|
5541
|
+
inputSchema: {
|
|
5542
|
+
type: "object",
|
|
5543
|
+
properties: {
|
|
5544
|
+
action: {
|
|
5545
|
+
type: "string",
|
|
5546
|
+
enum: ["save", "delete", "list"],
|
|
5547
|
+
description: "Action to perform"
|
|
5548
|
+
},
|
|
5549
|
+
topic: {
|
|
5550
|
+
type: "string",
|
|
5551
|
+
description: 'Topic name (used as filename, e.g. "architecture" -> architecture.md). Required for save/delete.'
|
|
5552
|
+
},
|
|
5553
|
+
content: {
|
|
5554
|
+
type: "string",
|
|
5555
|
+
description: "Content to save. First line is used as description. Required for save."
|
|
5556
|
+
}
|
|
5557
|
+
},
|
|
5558
|
+
required: ["action", "topic"]
|
|
5559
|
+
},
|
|
5560
|
+
dangerous: false,
|
|
5561
|
+
readOnly: false,
|
|
5562
|
+
async execute(input3) {
|
|
5563
|
+
const action = String(input3["action"]);
|
|
5564
|
+
const topic = String(input3["topic"] || "");
|
|
5565
|
+
const content = input3["content"] != null ? String(input3["content"]) : void 0;
|
|
5566
|
+
const memoryDir = memoryManager.getMemoryDir();
|
|
5567
|
+
switch (action) {
|
|
5568
|
+
case "save": {
|
|
5569
|
+
if (!topic) return makeToolError("topic is required for save action");
|
|
5570
|
+
if (!content) return makeToolError("content is required for save action");
|
|
5571
|
+
memoryManager.ensureDir();
|
|
5572
|
+
const firstLine = content.split("\n")[0].trim();
|
|
5573
|
+
const description = firstLine.length > 100 ? firstLine.slice(0, 100) + "..." : firstLine;
|
|
5574
|
+
const fileContent = buildFrontmatter(topic, description) + content;
|
|
5575
|
+
const topicPath = path22.join(memoryDir, `${topic}.md`);
|
|
5576
|
+
fs21.writeFileSync(topicPath, fileContent, "utf-8");
|
|
5577
|
+
updateIndex(memoryDir);
|
|
5578
|
+
return makeToolResult(`Memory topic "${topic}" saved to ${topicPath}`);
|
|
5579
|
+
}
|
|
5580
|
+
case "delete": {
|
|
5581
|
+
if (!topic) return makeToolError("topic is required for delete action");
|
|
5582
|
+
const topicPath = path22.join(memoryDir, `${topic}.md`);
|
|
5583
|
+
if (!fs21.existsSync(topicPath)) {
|
|
5584
|
+
return makeToolError(`Memory topic "${topic}" not found`);
|
|
5585
|
+
}
|
|
5586
|
+
fs21.unlinkSync(topicPath);
|
|
5587
|
+
memoryManager.ensureDir();
|
|
5588
|
+
updateIndex(memoryDir);
|
|
5589
|
+
return makeToolResult(`Memory topic "${topic}" deleted`);
|
|
5590
|
+
}
|
|
5591
|
+
case "list": {
|
|
5592
|
+
const topics = memoryManager.listTopics();
|
|
5593
|
+
if (topics.length === 0) {
|
|
5594
|
+
return makeToolResult("No memory topics saved yet.");
|
|
5595
|
+
}
|
|
5596
|
+
const lines = [];
|
|
5597
|
+
for (const t of topics) {
|
|
5598
|
+
const raw = memoryManager.loadTopic(t);
|
|
5599
|
+
if (raw) {
|
|
5600
|
+
const parsed = parseFrontmatter(raw);
|
|
5601
|
+
lines.push(`- ${t}: ${parsed.description || "(no description)"}`);
|
|
5602
|
+
} else {
|
|
5603
|
+
lines.push(`- ${t}: (no description)`);
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
return makeToolResult(`Memory topics (${topics.length}):
|
|
5607
|
+
${lines.join("\n")}
|
|
5608
|
+
|
|
5609
|
+
Memory dir: ${memoryDir}`);
|
|
5610
|
+
}
|
|
5611
|
+
default:
|
|
5612
|
+
return makeToolError(`Unknown action: ${action}. Use save, delete, or list.`);
|
|
5613
|
+
}
|
|
5614
|
+
}
|
|
5615
|
+
};
|
|
5616
|
+
|
|
4222
5617
|
// src/llm/anthropic.ts
|
|
4223
5618
|
init_esm_shims();
|
|
4224
5619
|
import Anthropic from "@anthropic-ai/sdk";
|
|
@@ -4310,13 +5705,38 @@ var AnthropicProvider = class {
|
|
|
4310
5705
|
name: block.name,
|
|
4311
5706
|
input: block.input
|
|
4312
5707
|
};
|
|
4313
|
-
case "tool_result":
|
|
5708
|
+
case "tool_result": {
|
|
5709
|
+
let trContent;
|
|
5710
|
+
if (typeof block.content === "string") {
|
|
5711
|
+
trContent = block.content;
|
|
5712
|
+
} else if (Array.isArray(block.content)) {
|
|
5713
|
+
trContent = [];
|
|
5714
|
+
for (const cb of block.content) {
|
|
5715
|
+
if (cb.type === "text") {
|
|
5716
|
+
trContent.push({ type: "text", text: cb.text });
|
|
5717
|
+
} else if (cb.type === "image") {
|
|
5718
|
+
trContent.push({
|
|
5719
|
+
type: "image",
|
|
5720
|
+
source: {
|
|
5721
|
+
type: "base64",
|
|
5722
|
+
media_type: cb.source.media_type,
|
|
5723
|
+
data: cb.source.data
|
|
5724
|
+
}
|
|
5725
|
+
});
|
|
5726
|
+
} else {
|
|
5727
|
+
trContent.push({ type: "text", text: JSON.stringify(cb) });
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
} else {
|
|
5731
|
+
trContent = JSON.stringify(block.content);
|
|
5732
|
+
}
|
|
4314
5733
|
return {
|
|
4315
5734
|
type: "tool_result",
|
|
4316
5735
|
tool_use_id: block.tool_use_id,
|
|
4317
|
-
content:
|
|
5736
|
+
content: trContent,
|
|
4318
5737
|
...block.is_error ? { is_error: true } : {}
|
|
4319
5738
|
};
|
|
5739
|
+
}
|
|
4320
5740
|
default:
|
|
4321
5741
|
return { type: "text", text: JSON.stringify(block) };
|
|
4322
5742
|
}
|
|
@@ -4520,15 +5940,43 @@ var OpenAIProvider = class {
|
|
|
4520
5940
|
}
|
|
4521
5941
|
const hasToolResults = m.content.some((b) => b.type === "tool_result");
|
|
4522
5942
|
if (hasToolResults) {
|
|
5943
|
+
const pendingImages = [];
|
|
4523
5944
|
for (const block of m.content) {
|
|
4524
5945
|
if (block.type === "tool_result") {
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
5946
|
+
if (Array.isArray(block.content)) {
|
|
5947
|
+
const textParts = [];
|
|
5948
|
+
for (const cb of block.content) {
|
|
5949
|
+
if (cb.type === "text") textParts.push(cb.text);
|
|
5950
|
+
else if (cb.type === "image") {
|
|
5951
|
+
pendingImages.push({
|
|
5952
|
+
type: "image_url",
|
|
5953
|
+
image_url: { url: `data:${cb.source.media_type};base64,${cb.source.data}` }
|
|
5954
|
+
});
|
|
5955
|
+
}
|
|
5956
|
+
}
|
|
5957
|
+
result.push({
|
|
5958
|
+
role: "tool",
|
|
5959
|
+
tool_call_id: block.tool_use_id,
|
|
5960
|
+
content: textParts.join("\n") || "(image)"
|
|
5961
|
+
});
|
|
5962
|
+
} else {
|
|
5963
|
+
result.push({
|
|
5964
|
+
role: "tool",
|
|
5965
|
+
tool_call_id: block.tool_use_id,
|
|
5966
|
+
content: block.content
|
|
5967
|
+
});
|
|
5968
|
+
}
|
|
4530
5969
|
}
|
|
4531
5970
|
}
|
|
5971
|
+
if (pendingImages.length > 0) {
|
|
5972
|
+
result.push({
|
|
5973
|
+
role: "user",
|
|
5974
|
+
content: [
|
|
5975
|
+
{ type: "text", text: "\uC704 \uB3C4\uAD6C\uAC00 \uBC18\uD658\uD55C \uC774\uBBF8\uC9C0\uC785\uB2C8\uB2E4. \uC774 \uC774\uBBF8\uC9C0\uB97C \uBD84\uC11D\uC5D0 \uD65C\uC6A9\uD558\uC138\uC694." },
|
|
5976
|
+
...pendingImages
|
|
5977
|
+
]
|
|
5978
|
+
});
|
|
5979
|
+
}
|
|
4532
5980
|
continue;
|
|
4533
5981
|
}
|
|
4534
5982
|
const hasToolUse = m.content.some((b) => b.type === "tool_use");
|
|
@@ -4719,7 +6167,14 @@ var OllamaProvider = class {
|
|
|
4719
6167
|
} else if (block.type === "image") {
|
|
4720
6168
|
images.push(block.source.data);
|
|
4721
6169
|
} else if (block.type === "tool_result") {
|
|
4722
|
-
|
|
6170
|
+
if (Array.isArray(block.content)) {
|
|
6171
|
+
for (const cb of block.content) {
|
|
6172
|
+
if (cb.type === "text") textParts.push(cb.text);
|
|
6173
|
+
else if (cb.type === "image") images.push(cb.source.data);
|
|
6174
|
+
}
|
|
6175
|
+
} else {
|
|
6176
|
+
textParts.push(`[Tool Result: ${block.content}]`);
|
|
6177
|
+
}
|
|
4723
6178
|
} else if (block.type === "tool_use") {
|
|
4724
6179
|
textParts.push(`[Tool Call: ${block.name}(${JSON.stringify(block.input)})]`);
|
|
4725
6180
|
}
|
|
@@ -4814,12 +6269,12 @@ function parseArgs(argv) {
|
|
|
4814
6269
|
}
|
|
4815
6270
|
function printHelp() {
|
|
4816
6271
|
console.log(`
|
|
4817
|
-
${
|
|
6272
|
+
${chalk14.cyan.bold("Codi (\uCF54\uB514)")} - AI Code Agent for Terminal
|
|
4818
6273
|
|
|
4819
|
-
${
|
|
6274
|
+
${chalk14.bold("Usage:")}
|
|
4820
6275
|
codi [options] [prompt]
|
|
4821
6276
|
|
|
4822
|
-
${
|
|
6277
|
+
${chalk14.bold("Options:")}
|
|
4823
6278
|
-m, --model <model> Set the model (default: gemini-2.5-flash)
|
|
4824
6279
|
--provider <name> Set the provider (openai, anthropic, ollama)
|
|
4825
6280
|
-p <prompt> Run a single prompt and exit
|
|
@@ -4830,12 +6285,12 @@ ${chalk13.bold("Options:")}
|
|
|
4830
6285
|
-h, --help Show this help
|
|
4831
6286
|
-v, --version Show version
|
|
4832
6287
|
|
|
4833
|
-
${
|
|
6288
|
+
${chalk14.bold("Environment:")}
|
|
4834
6289
|
GEMINI_API_KEY Google Gemini API key (default provider)
|
|
4835
6290
|
OPENAI_API_KEY OpenAI API key
|
|
4836
6291
|
ANTHROPIC_API_KEY Anthropic API key
|
|
4837
6292
|
|
|
4838
|
-
${
|
|
6293
|
+
${chalk14.bold("Examples:")}
|
|
4839
6294
|
codi # Start interactive session
|
|
4840
6295
|
codi -p "explain main.ts" # Single prompt
|
|
4841
6296
|
codi --provider anthropic # Use Anthropic Claude
|
|
@@ -4851,11 +6306,11 @@ async function main() {
|
|
|
4851
6306
|
}
|
|
4852
6307
|
if (args.version) {
|
|
4853
6308
|
try {
|
|
4854
|
-
const { readFileSync:
|
|
6309
|
+
const { readFileSync: readFileSync17 } = await import("fs");
|
|
4855
6310
|
const { fileURLToPath: fileURLToPath3 } = await import("url");
|
|
4856
6311
|
const p = await import("path");
|
|
4857
6312
|
const dir = p.dirname(fileURLToPath3(import.meta.url));
|
|
4858
|
-
const pkg = JSON.parse(
|
|
6313
|
+
const pkg = JSON.parse(readFileSync17(p.join(dir, "..", "package.json"), "utf-8"));
|
|
4859
6314
|
console.log(`codi v${pkg.version}`);
|
|
4860
6315
|
} catch {
|
|
4861
6316
|
console.log("codi v0.1.8");
|
|
@@ -4919,7 +6374,8 @@ async function main() {
|
|
|
4919
6374
|
taskUpdateTool,
|
|
4920
6375
|
taskListTool,
|
|
4921
6376
|
taskGetTool,
|
|
4922
|
-
askUserTool
|
|
6377
|
+
askUserTool,
|
|
6378
|
+
updateMemoryTool
|
|
4923
6379
|
]);
|
|
4924
6380
|
const subAgentHandler2 = createSubAgentHandler(provider, registry);
|
|
4925
6381
|
setSubAgentHandler(subAgentHandler2);
|
|
@@ -4962,10 +6418,11 @@ async function main() {
|
|
|
4962
6418
|
if (msg.role === "user") conversation.addUserMessage(msg.content);
|
|
4963
6419
|
else if (msg.role === "assistant") conversation.addAssistantMessage(msg.content);
|
|
4964
6420
|
}
|
|
4965
|
-
console.log(
|
|
6421
|
+
console.log(chalk14.dim(`Resumed session: ${id}`));
|
|
4966
6422
|
}
|
|
4967
6423
|
}
|
|
4968
6424
|
}
|
|
6425
|
+
logger.info("\uC138\uC158 \uC2DC\uC791", { provider: providerName, model: modelName, cwd: process.cwd(), planMode: getMode() === "plan", yolo: !!args.yolo });
|
|
4969
6426
|
await hookManager.runHooks("SessionStart", { cwd: process.cwd() });
|
|
4970
6427
|
if (args.prompt) {
|
|
4971
6428
|
await agentLoop(args.prompt, {
|
|
@@ -4980,6 +6437,7 @@ async function main() {
|
|
|
4980
6437
|
},
|
|
4981
6438
|
planMode: getMode() === "plan"
|
|
4982
6439
|
});
|
|
6440
|
+
logger.info("\uC138\uC158 \uC885\uB8CC (single prompt)");
|
|
4983
6441
|
await hookManager.runHooks("SessionEnd", {});
|
|
4984
6442
|
await mcpManager.disconnectAll();
|
|
4985
6443
|
process.exit(0);
|
|
@@ -4991,7 +6449,7 @@ async function main() {
|
|
|
4991
6449
|
compressor,
|
|
4992
6450
|
exitFn: async () => {
|
|
4993
6451
|
stopSpinner();
|
|
4994
|
-
console.log(
|
|
6452
|
+
console.log(chalk14.dim("\nSaving session..."));
|
|
4995
6453
|
sessionManager.save(conversation, void 0, provider.model);
|
|
4996
6454
|
await hookManager.runHooks("SessionEnd", {});
|
|
4997
6455
|
await mcpManager.disconnectAll();
|
|
@@ -5018,9 +6476,10 @@ async function main() {
|
|
|
5018
6476
|
};
|
|
5019
6477
|
const repl = new Repl({
|
|
5020
6478
|
onMessage: async (message) => {
|
|
5021
|
-
|
|
6479
|
+
const preview = typeof message === "string" ? message.slice(0, 50) : message.find((b) => b.type === "text")?.text?.slice(0, 50) || "image";
|
|
6480
|
+
checkpointManager.create(conversation, preview);
|
|
5022
6481
|
if (compressor.shouldCompress(conversation)) {
|
|
5023
|
-
console.log(
|
|
6482
|
+
console.log(chalk14.dim("Auto-compacting conversation..."));
|
|
5024
6483
|
await compressor.compress(conversation, provider);
|
|
5025
6484
|
conversation.setSystemPrompt(buildPrompt());
|
|
5026
6485
|
}
|
|
@@ -5049,14 +6508,17 @@ async function main() {
|
|
|
5049
6508
|
},
|
|
5050
6509
|
onExit: async () => {
|
|
5051
6510
|
stopSpinner();
|
|
5052
|
-
|
|
6511
|
+
logger.info("\uC138\uC158 \uC885\uB8CC (REPL exit)");
|
|
6512
|
+
console.log(chalk14.dim("\nSaving session..."));
|
|
5053
6513
|
sessionManager.save(conversation, void 0, provider.model);
|
|
6514
|
+
checkpointManager.cleanup();
|
|
5054
6515
|
await hookManager.runHooks("SessionEnd", {});
|
|
5055
6516
|
await mcpManager.disconnectAll();
|
|
5056
6517
|
}
|
|
5057
6518
|
});
|
|
5058
6519
|
process.on("SIGTERM", async () => {
|
|
5059
6520
|
stopSpinner();
|
|
6521
|
+
logger.info("\uC138\uC158 \uC885\uB8CC (SIGTERM)");
|
|
5060
6522
|
sessionManager.save(conversation, void 0, provider.model);
|
|
5061
6523
|
await hookManager.runHooks("SessionEnd", {});
|
|
5062
6524
|
await mcpManager.disconnectAll();
|
|
@@ -5065,7 +6527,8 @@ async function main() {
|
|
|
5065
6527
|
await repl.start();
|
|
5066
6528
|
}
|
|
5067
6529
|
main().catch((err) => {
|
|
5068
|
-
|
|
6530
|
+
logger.error("\uCE58\uBA85\uC801 \uC624\uB958", {}, err instanceof Error ? err : new Error(String(err)));
|
|
6531
|
+
console.error(chalk14.red(`Fatal error: ${err.message}`));
|
|
5069
6532
|
console.error(err.stack);
|
|
5070
6533
|
process.exit(1);
|
|
5071
6534
|
});
|