@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.
Files changed (3) hide show
  1. package/dist/cli.js +2262 -799
  2. package/dist/cli.js.map +1 -1
  3. 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 header = `${icon} ${chalk2.yellow(toolName)}`;
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 chalk13 from "chalk";
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 readFileSync3 } from "fs";
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(readFileSync3(path5.join(__dirname2, "..", "package.json"), "utf-8"));
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((resolve10, reject) => {
999
+ const line = await new Promise((resolve11, reject) => {
861
1000
  const onLine = (data) => {
862
1001
  cleanup();
863
- resolve10(data);
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
- resolve10("");
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 = readFileSync3(filePath);
1125
+ const data = readFileSync4(filePath);
965
1126
  const base64 = data.toString("base64");
966
- const mimeMap = {
967
- ".png": "image/png",
968
- ".jpg": "image/jpeg",
969
- ".jpeg": "image/jpeg",
970
- ".gif": "image/gif",
971
- ".webp": "image/webp",
972
- ".bmp": "image/bmp",
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 = readFileSync3(filePath, "utf-8");
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
- await this.options.onMessage(message);
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
- * Estimate rough token count (very approximate).
1343
+ * tiktoken을 사용하여 정확한 토큰 수를 계산한다.
1122
1344
  */
1123
- estimateTokens() {
1124
- let chars = this.systemPrompt.length;
1345
+ estimateTokens(model) {
1346
+ let tokens = countTokens(this.systemPrompt, model);
1125
1347
  for (const msg of this.messages) {
1126
- if (typeof msg.content === "string") {
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 Math.ceil(chars / 4);
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 ToolExecutor = class {
1148
- constructor(registry, options = {}) {
1149
- this.registry = registry;
1150
- this.options = options;
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
- async executeOne(toolCall) {
1153
- const tool = this.registry.get(toolCall.name);
1154
- if (!tool) {
1155
- return {
1156
- toolUseId: toolCall.id,
1157
- toolName: toolCall.name,
1158
- result: makeToolError(`Unknown tool: ${toolCall.name}. Available tools: ${this.registry.listNames().join(", ")}`)
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
- let result;
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
- result = await tool.execute(input3);
1202
- } catch (err) {
1203
- result = makeToolError(
1204
- `Tool '${toolCall.name}' threw an error: ${err instanceof Error ? err.message : String(err)}`
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
- if (this.options.postHook) {
1211
- try {
1212
- await this.options.postHook(toolCall.name, input3, result);
1213
- } catch {
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
- async executeMany(toolCalls) {
1223
- const safeCalls = [];
1224
- const dangerousCalls = [];
1225
- for (const tc of toolCalls) {
1226
- const tool = this.registry.get(tc.name);
1227
- if (tool?.dangerous) {
1228
- dangerousCalls.push(tc);
1229
- } else {
1230
- safeCalls.push(tc);
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
- const safePromises = safeCalls.map((tc) => this.executeOne(tc));
1234
- const dangerousResults = [];
1235
- for (const tc of dangerousCalls) {
1236
- const result = await this.executeOne(tc);
1237
- dangerousResults.push(result);
1483
+ if (error) {
1484
+ entry.error = {
1485
+ message: error.message,
1486
+ stack: error.stack
1487
+ };
1238
1488
  }
1239
- const safeResults = await Promise.allSettled(safePromises);
1240
- const results = [];
1241
- for (let i = 0; i < safeResults.length; i++) {
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
- setOptions(options) {
1259
- Object.assign(this.options, options);
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/agent/token-tracker.ts
1264
- init_esm_shims();
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/llm/types.ts
1576
+ // src/security/permission-manager.ts
1267
1577
  init_esm_shims();
1268
- var MODEL_COSTS = {
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 chalk7 from "chalk";
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(chalk7.red(`
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(chalk7.yellow("\n\u26A0 Response truncated (max tokens reached)"));
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
- tool_use_id: r.toolUseId,
1447
- content: r.result.output,
1448
- is_error: !r.result.success
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(chalk7.yellow(`
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((resolve10) => setTimeout(resolve10, ms));
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 os4 from "os";
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 (os4.platform() === "win32") {
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: ${os4.platform()} ${os4.release()}`,
1658
- `- Shell: ${os4.platform() === "win32" ? "PowerShell" : process.env["SHELL"] || "/bin/bash"}`,
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 fs4 from "fs";
1729
- import * as os5 from "os";
1730
- import * as path6 from "path";
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"] || os5.homedir();
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 = path6.basename(process.cwd());
1738
- this.memoryDir = path6.join(home, ".codi", "projects", `${projectName}-${projectHash}`, "memory");
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 (!fs4.existsSync(this.memoryDir)) {
1742
- fs4.mkdirSync(this.memoryDir, { recursive: true });
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 = path6.join(this.memoryDir, "MEMORY.md");
1750
- if (!fs4.existsSync(indexPath)) return "";
1751
- const content = fs4.readFileSync(indexPath, "utf-8");
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 = path6.join(this.memoryDir, "MEMORY.md");
1758
- fs4.writeFileSync(indexPath, content, "utf-8");
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 = path6.join(this.memoryDir, `${name}.md`);
1762
- if (!fs4.existsSync(topicPath)) return null;
1763
- return fs4.readFileSync(topicPath, "utf-8");
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 = path6.join(this.memoryDir, `${name}.md`);
1768
- fs4.writeFileSync(topicPath, content, "utf-8");
2540
+ const topicPath = path7.join(this.memoryDir, `${name}.md`);
2541
+ fs6.writeFileSync(topicPath, content, "utf-8");
1769
2542
  }
1770
2543
  listTopics() {
1771
- if (!fs4.existsSync(this.memoryDir)) return [];
1772
- return fs4.readdirSync(this.memoryDir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").map((f) => f.replace(".md", ""));
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 write_file/edit_file to update memory files as you learn patterns.",
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 fs5 from "fs";
1792
- import * as os6 from "os";
1793
- import * as path7 from "path";
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"] || os6.homedir();
1799
- this.sessionsDir = path7.join(home, ".codi", "sessions");
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 (!fs5.existsSync(this.sessionsDir)) {
1803
- fs5.mkdirSync(this.sessionsDir, { recursive: true });
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 = path7.join(this.sessionsDir, `${id}.jsonl`);
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
- fs5.writeFileSync(filePath, lines.join("\n") + "\n", "utf-8");
2598
+ fs7.writeFileSync(filePath, lines.join("\n") + "\n", "utf-8");
1826
2599
  return id;
1827
2600
  }
1828
2601
  load(id) {
1829
- const filePath = path7.join(this.sessionsDir, `${id}.jsonl`);
1830
- if (!fs5.existsSync(filePath)) return null;
1831
- const content = fs5.readFileSync(filePath, "utf-8");
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 = fs5.readdirSync(this.sessionsDir).filter((f) => f.endsWith(".jsonl"));
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 = path7.join(this.sessionsDir, file);
2632
+ const filePath = path8.join(this.sessionsDir, file);
1860
2633
  try {
1861
- const firstLine = fs5.readFileSync(filePath, "utf-8").split("\n")[0];
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 = path7.join(this.sessionsDir, `${id}.jsonl`);
1880
- if (fs5.existsSync(filePath)) {
1881
- fs5.unlinkSync(filePath);
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
- constructor() {
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
- if (this.checkpoints.length > 20) {
1944
- this.checkpoints = this.checkpoints.slice(-20);
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
- this.checkpoints = this.checkpoints.slice(0, idx + 1);
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 fs6 from "fs";
1984
- import * as path8 from "path";
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 = path8.parse(dir).root;
2832
+ const root = path10.parse(dir).root;
1989
2833
  while (dir !== root) {
1990
2834
  loadFromDir(dir, fragments);
1991
- const parent = path8.dirname(dir);
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 = path8.join(dir, "CODI.md");
1999
- if (fs6.existsSync(codiPath)) {
2842
+ const codiPath = path10.join(dir, "CODI.md");
2843
+ if (fs9.existsSync(codiPath)) {
2000
2844
  try {
2001
- let content = fs6.readFileSync(codiPath, "utf-8");
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 = path8.join(dir, "CODI.local.md");
2009
- if (fs6.existsSync(localPath)) {
2852
+ const localPath = path10.join(dir, "CODI.local.md");
2853
+ if (fs9.existsSync(localPath)) {
2010
2854
  try {
2011
- const content = fs6.readFileSync(localPath, "utf-8");
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 = path8.join(dir, ".codi", "rules");
2018
- if (fs6.existsSync(rulesDir)) {
2861
+ const rulesDir = path10.join(dir, ".codi", "rules");
2862
+ if (fs9.existsSync(rulesDir)) {
2019
2863
  try {
2020
- const files = fs6.readdirSync(rulesDir).filter((f) => f.endsWith(".md")).sort();
2864
+ const files = fs9.readdirSync(rulesDir).filter((f) => f.endsWith(".md")).sort();
2021
2865
  for (const file of files) {
2022
- const content = fs6.readFileSync(path8.join(rulesDir, file), "utf-8");
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 = path8.resolve(baseDir, importPath);
2033
- if (fs6.existsSync(resolved)) {
2876
+ const resolved = path10.resolve(baseDir, importPath);
2877
+ if (fs9.existsSync(resolved)) {
2034
2878
  try {
2035
- return fs6.readFileSync(resolved, "utf-8");
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 currentMode = "execute";
2890
+ var currentMode2 = "execute";
2047
2891
  function getMode() {
2048
- return currentMode;
2892
+ return currentMode2;
2049
2893
  }
2050
2894
  function setMode(mode) {
2051
- currentMode = mode;
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
- if (choice === "n" || choice === "no") {
2209
- return false;
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 true;
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 os7 from "os";
2222
- function getDefaultShell() {
2223
- if (os7.platform() === "win32") {
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((resolve10) => {
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 = os7.platform() === "win32";
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: getDefaultShell()
3005
+ shell: getDefaultShell2()
2267
3006
  }, (err, stdout, stderr) => {
2268
3007
  if (err) {
2269
3008
  if (err.code === 2) {
2270
- resolve10({
3009
+ resolve11({
2271
3010
  proceed: false,
2272
3011
  reason: stderr || stdout || "Blocked by hook"
2273
3012
  });
2274
3013
  return;
2275
3014
  }
2276
- resolve10({ proceed: true });
3015
+ resolve11({ proceed: true });
2277
3016
  return;
2278
3017
  }
2279
3018
  try {
2280
3019
  const output3 = JSON.parse(stdout);
2281
- resolve10({
3020
+ resolve11({
2282
3021
  proceed: output3.decision !== "block",
2283
3022
  reason: output3.reason,
2284
3023
  updatedInput: output3.updatedInput
2285
3024
  });
2286
3025
  } catch {
2287
- resolve10({ proceed: true });
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 fs7 from "fs";
2303
- import * as os8 from "os";
2304
- import * as path9 from "path";
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 chalk9 from "chalk";
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(chalk9.yellow(` \u26A0 Failed to connect MCP server '${name}': ${err instanceof Error ? err.message : String(err)}`));
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"] || os8.homedir();
3063
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || os11.homedir();
2325
3064
  const paths = [
2326
- path9.join(home, ".codi", "mcp.json"),
2327
- path9.join(process.cwd(), ".codi", "mcp.json")
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 (fs7.existsSync(p)) {
2332
- const content = JSON.parse(fs7.readFileSync(p, "utf-8"));
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(chalk9.dim(` \u2713 MCP server '${name}' connected (${toolNames.length} tools)`));
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 chalk10 from "chalk";
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(chalk10.dim(`
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(chalk10.dim(` \u25B8 ${type} agent completed.`));
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 fs8 from "fs";
2528
- import * as os9 from "os";
2529
- import * as path10 from "path";
2530
- import chalk11 from "chalk";
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(chalk11.bold("\nAvailable Commands:\n"));
3277
+ console.log(chalk12.bold("\nAvailable Commands:\n"));
2539
3278
  for (const cmd of commands) {
2540
- const aliases = cmd.aliases ? chalk11.dim(` (${cmd.aliases.join(", ")})`) : "";
2541
- console.log(` ${chalk11.cyan(cmd.name)}${aliases} - ${cmd.description}`);
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(chalk11.dim(" Prefixes: ! (bash), @ (file reference)"));
2545
- console.log(chalk11.dim(" Use \\ at end of line for multiline input"));
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(chalk11.dim("\nGoodbye!\n"));
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(chalk11.green("\u2713 Conversation cleared"));
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(chalk11.cyan(`Current model: ${info.model} (${info.provider})`));
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(chalk11.green(`\u2713 Model switched to: ${args}`));
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(chalk11.dim("Compressing conversation..."));
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(chalk11.green("\u2713 Conversation compressed"));
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
- console.log(chalk11.cyan(`
2608
- ${tokenTracker.format()}
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(chalk11.bold("\nConfiguration:\n"));
2619
- console.log(chalk11.dim(JSON.stringify(config, null, 2)));
2620
- console.log(chalk11.dim(`
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(chalk11.bold("\nPermission Rules:\n"));
2632
- console.log(chalk11.green(" Allow: ") + config.permissions.allow.join(", "));
2633
- console.log(chalk11.red(" Deny: ") + (config.permissions.deny.join(", ") || "(none)"));
2634
- console.log(chalk11.yellow(" Ask: ") + config.permissions.ask.join(", "));
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(chalk11.green(`\u2713 Session saved: ${id}`));
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(chalk11.yellow("No sessions found. Use /save to save a session first."));
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(chalk11.red(`Session not found: ${id}`));
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(chalk11.green(`\u2713 Resumed session: ${id} (${session.meta.messageCount} messages)`));
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(chalk11.green(`\u2713 Conversation forked: ${id}`));
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(chalk11.cyan(`Mode: ${newMode === "plan" ? "PLAN (read-only)" : "EXECUTE"}`));
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(chalk11.bold("\nAuto Memory:\n"));
2704
- console.log(chalk11.dim(`Directory: ${memoryManager.getMemoryDir()}`));
3464
+ console.log(chalk12.bold("\nAuto Memory:\n"));
3465
+ console.log(chalk12.dim(`Directory: ${memoryManager.getMemoryDir()}`));
2705
3466
  if (index) {
2706
- console.log(chalk11.dim("\nMEMORY.md:"));
3467
+ console.log(chalk12.dim("\nMEMORY.md:"));
2707
3468
  console.log(index);
2708
3469
  } else {
2709
- console.log(chalk11.dim("\nNo memory saved yet."));
3470
+ console.log(chalk12.dim("\nNo memory saved yet."));
2710
3471
  }
2711
3472
  if (topics.length > 0) {
2712
- console.log(chalk11.dim(`
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 = path10.join(process.cwd(), "CODI.md");
2724
- if (fs8.existsSync(codiPath)) {
2725
- console.log(chalk11.yellow("CODI.md already exists"));
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: ${path10.basename(process.cwd())}
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
- fs8.writeFileSync(codiPath, content, "utf-8");
2743
- console.log(chalk11.green("\u2713 Created CODI.md"));
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
- fs8.writeFileSync(filePath, md, "utf-8");
2772
- console.log(chalk11.green(`\u2713 Exported to ${filePath}`));
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(chalk11.dim("\nNo tasks.\n"));
3544
+ console.log(chalk12.dim("\nNo tasks.\n"));
2784
3545
  return true;
2785
3546
  }
2786
- console.log(chalk11.bold("\nTasks:\n"));
3547
+ console.log(chalk12.bold("\nTasks:\n"));
2787
3548
  for (const task of tasks) {
2788
- const statusIcon = task.status === "completed" ? chalk11.green("\u2713") : task.status === "in_progress" ? chalk11.yellow("\u27F3") : chalk11.dim("\u25CB");
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(chalk11.bold("\nCodi Status:\n"));
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(chalk11.bold("\nContext Window:\n"));
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(chalk11.yellow("No checkpoints available."));
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(chalk11.yellow("No checkpoint to rewind to."));
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(chalk11.green(`\u2713 Rewound to checkpoint${result.description ? `: ${result.description}` : ""}`));
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(chalk11.dim("\nNo changes.\n"));
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(chalk11.yellow("Not a git repository or git not available."));
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
- const diff = staged + unstaged;
2886
- if (!diff.trim()) {
2887
- console.log(chalk11.dim("\nNo changes to commit.\n"));
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 \uBCC0\uACBD\uB41C \uD30C\uC77C\uC744 add\uD558\uACE0 \uCEE4\uBC0B\uD574\uC918.
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
- ${diff}
3698
+ ${finalDiff}
2895
3699
  \`\`\``
2896
3700
  );
2897
3701
  return false;
2898
3702
  } catch {
2899
- console.log(chalk11.yellow("Not a git repository or git not available."));
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(chalk11.dim("\nNo changes to review.\n"));
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(chalk11.yellow("Not a git repository or git not available."));
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(chalk11.yellow("Usage: /search <keyword>"));
3740
+ console.log(chalk12.yellow("Usage: /search <keyword>"));
2937
3741
  return true;
2938
3742
  }
2939
- const home = process.env["HOME"] || process.env["USERPROFILE"] || os9.homedir();
2940
- const sessionsDir = path10.join(home, ".codi", "sessions");
2941
- if (!fs8.existsSync(sessionsDir)) {
2942
- console.log(chalk11.dim("\nNo sessions found.\n"));
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 = fs8.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
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 = path10.join(sessionsDir, file);
2951
- const lines = fs8.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
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 = fs8.statSync(filePath);
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(chalk11.dim(`
3769
+ console.log(chalk12.dim(`
2966
3770
  No results for "${args}".
2967
3771
  `));
2968
3772
  } else {
2969
- console.log(chalk11.bold(`
3773
+ console.log(chalk12.bold(`
2970
3774
  Search results for "${args}":
2971
3775
  `));
2972
3776
  for (const r of results) {
2973
- console.log(` ${chalk11.cyan(r.sessionId)} ${chalk11.dim(r.date)}`);
2974
- console.log(` ${chalk11.dim(r.preview)}`);
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(chalk11.yellow("Usage: /fix <command>"));
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 = os9.platform() === "win32";
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(chalk11.green(`
3799
+ console.log(chalk12.green(`
2996
3800
  \u2713 Command succeeded. No errors to fix.
2997
3801
  `));
2998
- if (output3.trim()) console.log(chalk11.dim(output3));
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(chalk11.red(`
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(chalk11.dim("\nNo MCP servers connected.\n"));
3026
- console.log(chalk11.dim("Add servers in .codi/mcp.json or ~/.codi/mcp.json"));
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(chalk11.bold("\nMCP Servers:\n"));
4048
+ console.log(chalk12.bold("\nMCP Servers:\n"));
3030
4049
  for (const s of servers) {
3031
- console.log(` ${chalk11.green("\u25CF")} ${s.name}`);
4050
+ console.log(` ${chalk12.green("\u25CF")} ${s.name}`);
3032
4051
  for (const t of s.tools) {
3033
- console.log(chalk11.dim(` - ${t}`));
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"] || os9.homedir();
4068
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || os13.homedir();
3045
4069
  const dirs = [
3046
- path10.join(home, ".codi", "commands"),
3047
- path10.join(process.cwd(), ".codi", "commands")
4070
+ path13.join(home, ".codi", "commands"),
4071
+ path13.join(process.cwd(), ".codi", "commands")
3048
4072
  ];
3049
4073
  for (const dir of dirs) {
3050
- if (!fs8.existsSync(dir)) continue;
3051
- const files = fs8.readdirSync(dir).filter((f) => f.endsWith(".md"));
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 = path10.join(dir, file);
4078
+ const filePath = path13.join(dir, file);
3055
4079
  commands.push({
3056
4080
  name,
3057
- description: `Custom command from ${path10.relative(process.cwd(), filePath)}`,
4081
+ description: `Custom command from ${path13.relative(process.cwd(), filePath)}`,
3058
4082
  handler: async (_args, ctx) => {
3059
- let content = fs8.readFileSync(filePath, "utf-8");
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 fs9 from "fs";
3074
- import * as path11 from "path";
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 = path11.resolve(filePath);
3095
- if (!fs9.existsSync(resolved)) {
4118
+ const resolved = path14.resolve(filePath);
4119
+ if (!fs13.existsSync(resolved)) {
3096
4120
  return makeToolError(`File not found: ${resolved}`);
3097
4121
  }
3098
- const stat = fs9.statSync(resolved);
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 = path11.extname(resolved).toLowerCase();
4126
+ const ext = path14.extname(resolved).toLowerCase();
3103
4127
  if ([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"].includes(ext)) {
3104
- const data = fs9.readFileSync(resolved);
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: ${path11.basename(resolved)}]`, {
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 = fs9.readFileSync(resolved);
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 = fs9.readFileSync(resolved, "utf-8");
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 = fs9.readFileSync(resolved, "utf-8");
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
- import * as fs10 from "fs";
3201
- import * as path12 from "path";
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 = path12.resolve(filePath);
4243
+ const resolved = path15.resolve(filePath);
3219
4244
  try {
3220
- const dir = path12.dirname(resolved);
3221
- if (!fs10.existsSync(dir)) {
3222
- fs10.mkdirSync(dir, { recursive: true });
4245
+ const dir = path15.dirname(resolved);
4246
+ if (!fs14.existsSync(dir)) {
4247
+ fs14.mkdirSync(dir, { recursive: true });
3223
4248
  }
3224
- const existed = fs10.existsSync(resolved);
3225
- fs10.writeFileSync(resolved, content, "utf-8");
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
- import * as fs11 from "fs";
3242
- import * as path13 from "path";
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 = path13.resolve(filePath);
3264
- if (!fs11.existsSync(resolved)) {
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 = fs11.readFileSync(resolved, "utf-8");
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
- fs11.writeFileSync(resolved, output3, "utf-8");
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
- import * as fs12 from "fs";
3322
- import * as path14 from "path";
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 = path14.resolve(filePath);
3351
- if (!fs12.existsSync(resolved)) {
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 = fs12.readFileSync(resolved, "utf-8");
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
- fs12.writeFileSync(resolved, output3, "utf-8");
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 fs13 from "fs";
3398
- import * as path15 from "path";
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 = path15.resolve(searchPath);
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 = fs13.statSync(f);
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 fs14 from "fs";
3453
- import * as path16 from "path";
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 = path16.resolve(input3["path"] ? String(input3["path"]) : process.cwd());
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((resolve10) => {
3549
- execFile(checkCmd, [cmd], (err) => resolve10(!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((resolve10, reject) => {
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
- resolve10(stdout);
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 = fs14.readFileSync(filePath, "utf-8");
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 = fs14.readdirSync(dir, { withFileTypes: true });
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
- if (runInBackground) {
3734
- return runBackgroundTask(command);
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
- function runBackgroundTask(command) {
3773
- const taskId = `bg_${++taskCounter}`;
3774
- const bgCommand = os10.platform() === "win32" ? `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ${command}` : command;
3775
- const proc = spawn(bgCommand, {
3776
- shell: getDefaultShell2(),
3777
- cwd: process.cwd(),
3778
- env: { ...process.env },
3779
- stdio: ["ignore", "pipe", "pipe"],
3780
- detached: false
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 fs15 from "fs";
3807
- import * as path17 from "path";
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 = path17.resolve(input3["path"] ? String(input3["path"]) : process.cwd());
3822
- if (!fs15.existsSync(dirPath)) {
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 = fs15.statSync(dirPath);
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 = fs15.readdirSync(dirPath, { withFileTypes: true });
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 = fs15.readlinkSync(path17.join(dirPath, entry.name));
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 isReadOnly = readOnlyPrefixes.some((p) => command.startsWith(p));
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 CACHE_TTL = 15 * 60 * 1e3;
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, convert HTML to text, and return the content. Includes a 15-minute cache. HTTP URLs are upgraded to HTTPS.`,
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
- const cached = cache.get(url);
3941
- if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
3942
- return makeToolResult(`[Cached] ${prompt ? `Query: ${prompt}
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": "Codi/0.1 (AI Code Agent)",
3950
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
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
- return makeToolError(`HTTP ${response.status}: ${response.statusText}`);
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 (contentType.includes("text/html") || contentType.includes("application/xhtml")) {
3961
- const html = await response.text();
3962
- const { load } = await import("cheerio");
3963
- const $ = load(html);
3964
- $("script, style, nav, header, footer, iframe, noscript").remove();
3965
- const main2 = $("main, article, .content, #content, .main").first();
3966
- text = (main2.length ? main2.text() : $("body").text()).replace(/\s+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
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.text();
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 MAX_LEN = 5e4;
3971
- if (text.length > MAX_LEN) {
3972
- text = text.slice(0, MAX_LEN) + "\n\n... (truncated)";
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
- cache.set(url, { content: text, timestamp: Date.now() });
3975
- return makeToolResult(prompt ? `URL: ${url}
3976
- Query: ${prompt}
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}` : text);
5131
+ ${text}`);
3979
5132
  } catch (err) {
3980
- return makeToolError(`Failed to fetch URL: ${err instanceof Error ? err.message : String(err)}`);
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 as the search engine.`,
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
- const encoded = encodeURIComponent(query);
4004
- const url = `https://html.duckduckgo.com/html/?q=${encoded}`;
4005
- const response = await fetch(url, {
4006
- headers: {
4007
- "User-Agent": "Mozilla/5.0 (compatible; Codi/0.1)"
4008
- },
4009
- signal: AbortSignal.timeout(15e3)
4010
- });
4011
- if (!response.ok) {
4012
- return makeToolError(`Search failed: HTTP ${response.status}`);
4013
- }
4014
- const html = await response.text();
4015
- const { load } = await import("cheerio");
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 fs16 from "fs";
4054
- import * as path18 from "path";
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 = path18.resolve(String(input3["notebook_path"]));
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 (!fs16.existsSync(nbPath)) {
5345
+ if (!fs20.existsSync(nbPath)) {
4078
5346
  return makeToolError(`Notebook not found: ${nbPath}`);
4079
5347
  }
4080
5348
  try {
4081
- const content = fs16.readFileSync(nbPath, "utf-8");
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
- fs16.writeFileSync(nbPath, JSON.stringify(nb, null, 1), "utf-8");
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 chalk12 from "chalk";
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(chalk12.cyan.bold("? ") + chalk12.bold(question));
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(chalk12.cyan(` ${i + 1}.`) + ` ${opt.label}${opt.description ? chalk12.dim(` - ${opt.description}`) : ""}`);
5445
+ console.log(chalk13.cyan(` ${i + 1}.`) + ` ${opt.label}${opt.description ? chalk13.dim(` - ${opt.description}`) : ""}`);
4178
5446
  }
4179
- console.log(chalk12.dim(` ${options.length + 1}. Other (type custom response)`));
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 ? chalk12.dim("Enter numbers separated by commas: ") : chalk12.dim("Enter number or type response: ");
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(chalk12.dim("> "));
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: typeof block.content === "string" ? block.content : JSON.stringify(block.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
- result.push({
4526
- role: "tool",
4527
- tool_call_id: block.tool_use_id,
4528
- content: typeof block.content === "string" ? block.content : JSON.stringify(block.content)
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
- textParts.push(`[Tool Result: ${typeof block.content === "string" ? block.content : JSON.stringify(block.content)}]`);
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
- ${chalk13.cyan.bold("Codi (\uCF54\uB514)")} - AI Code Agent for Terminal
6272
+ ${chalk14.cyan.bold("Codi (\uCF54\uB514)")} - AI Code Agent for Terminal
4818
6273
 
4819
- ${chalk13.bold("Usage:")}
6274
+ ${chalk14.bold("Usage:")}
4820
6275
  codi [options] [prompt]
4821
6276
 
4822
- ${chalk13.bold("Options:")}
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
- ${chalk13.bold("Environment:")}
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
- ${chalk13.bold("Examples:")}
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: readFileSync14 } = await import("fs");
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(readFileSync14(p.join(dir, "..", "package.json"), "utf-8"));
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(chalk13.dim(`Resumed session: ${id}`));
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(chalk13.dim("\nSaving session..."));
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
- checkpointManager.create(conversation, message.slice(0, 50));
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(chalk13.dim("Auto-compacting conversation..."));
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
- console.log(chalk13.dim("\nSaving session..."));
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
- console.error(chalk13.red(`Fatal error: ${err.message}`));
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
  });