@gemdoq/codi 0.1.9 → 0.2.2

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 +2122 -733
  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,47 @@ 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";
886
+
887
+ // src/ui/stdin-prompt.ts
888
+ init_esm_shims();
889
+ var _handler = null;
890
+ function registerPromptHandler(handler) {
891
+ _handler = handler;
892
+ }
893
+ function unregisterPromptHandler() {
894
+ _handler = null;
895
+ }
896
+ async function sharedPrompt(prompt) {
897
+ if (_handler) {
898
+ return _handler(prompt);
899
+ }
900
+ const readline3 = await import("readline/promises");
901
+ const rl = readline3.createInterface({
902
+ input: process.stdin,
903
+ output: process.stdout
904
+ });
905
+ try {
906
+ const answer = await rl.question(prompt);
907
+ return answer;
908
+ } finally {
909
+ rl.removeAllListeners();
910
+ rl.terminal = false;
911
+ rl.close();
912
+ }
913
+ }
914
+
915
+ // src/repl.ts
785
916
  var __filename2 = fileURLToPath2(import.meta.url);
786
917
  var __dirname2 = path5.dirname(__filename2);
918
+ var HISTORY_DIR = path5.join(os3.homedir(), ".codi");
919
+ var HISTORY_FILE = path5.join(HISTORY_DIR, "history");
920
+ var MAX_HISTORY = 1e3;
787
921
  function getVersion() {
788
922
  try {
789
- const pkg = JSON.parse(readFileSync3(path5.join(__dirname2, "..", "package.json"), "utf-8"));
923
+ const pkg = JSON.parse(readFileSync4(path5.join(__dirname2, "..", "package.json"), "utf-8"));
790
924
  return `v${pkg.version}`;
791
925
  } catch {
792
926
  return "v0.1.4";
@@ -815,15 +949,50 @@ var Repl = class {
815
949
  description: "Clear screen"
816
950
  });
817
951
  }
952
+ loadHistory() {
953
+ try {
954
+ const content = fs4.readFileSync(HISTORY_FILE, "utf-8");
955
+ return content.split("\n").filter(Boolean).slice(-MAX_HISTORY);
956
+ } catch {
957
+ return [];
958
+ }
959
+ }
960
+ saveHistory() {
961
+ if (!this.rl) return;
962
+ try {
963
+ const rlAny = this.rl;
964
+ const history = rlAny.history ?? [];
965
+ const entries = history.slice(0, MAX_HISTORY).reverse();
966
+ fs4.mkdirSync(HISTORY_DIR, { recursive: true });
967
+ fs4.writeFileSync(HISTORY_FILE, entries.join("\n") + "\n", "utf-8");
968
+ } catch {
969
+ }
970
+ }
971
+ shouldSaveToHistory(line) {
972
+ const trimmed = line.trim();
973
+ if (!trimmed) return false;
974
+ if (trimmed.startsWith("/")) return false;
975
+ return true;
976
+ }
818
977
  async start() {
819
978
  this.running = true;
979
+ const loadedHistory = this.loadHistory();
820
980
  this.rl = readline2.createInterface({
821
981
  input: input2,
822
982
  output: output2,
823
983
  prompt: renderPrompt(),
824
984
  completer: (line) => completer(line),
825
- terminal: true
985
+ terminal: true,
986
+ history: loadedHistory,
987
+ historySize: MAX_HISTORY
826
988
  });
989
+ const rlAny = this.rl;
990
+ if (rlAny.history && loadedHistory.length > 0) {
991
+ rlAny.history.length = 0;
992
+ for (let i = loadedHistory.length - 1; i >= 0; i--) {
993
+ rlAny.history.push(loadedHistory[i]);
994
+ }
995
+ }
827
996
  if (process.stdin.isTTY && os3.platform() !== "win32") {
828
997
  process.stdout.write("\x1B[?2004h");
829
998
  }
@@ -848,6 +1017,15 @@ var Repl = class {
848
1017
  }
849
1018
  });
850
1019
  }
1020
+ registerPromptHandler((prompt) => {
1021
+ if (!this.rl) return Promise.reject(new Error("REPL not running"));
1022
+ process.stdout.write(prompt);
1023
+ return new Promise((resolve11) => {
1024
+ this.rl.once("line", (answer) => {
1025
+ resolve11(answer);
1026
+ });
1027
+ });
1028
+ });
851
1029
  this.printWelcome();
852
1030
  while (this.running) {
853
1031
  try {
@@ -857,10 +1035,10 @@ var Repl = class {
857
1035
  }
858
1036
  this.rl.setPrompt(renderPrompt());
859
1037
  this.rl.prompt();
860
- const line = await new Promise((resolve10, reject) => {
1038
+ const line = await new Promise((resolve11, reject) => {
861
1039
  const onLine = (data) => {
862
1040
  cleanup();
863
- resolve10(data);
1041
+ resolve11(data);
864
1042
  };
865
1043
  const onClose = () => {
866
1044
  cleanup();
@@ -876,7 +1054,7 @@ var Repl = class {
876
1054
  this.lastInterruptTime = now;
877
1055
  this.options.onInterrupt();
878
1056
  console.log(chalk4.dim("\n(Press Ctrl+C again to exit)"));
879
- resolve10("");
1057
+ resolve11("");
880
1058
  };
881
1059
  const cleanup = () => {
882
1060
  this.rl.removeListener("line", onLine);
@@ -889,6 +1067,17 @@ var Repl = class {
889
1067
  });
890
1068
  const trimmed = line.trim();
891
1069
  if (!trimmed) continue;
1070
+ {
1071
+ const rlAny2 = this.rl;
1072
+ const hist = rlAny2.history;
1073
+ if (hist && hist.length > 0) {
1074
+ if (!this.shouldSaveToHistory(trimmed)) {
1075
+ hist.shift();
1076
+ } else if (hist.length > 1 && hist[0] === hist[1]) {
1077
+ hist.shift();
1078
+ }
1079
+ }
1080
+ }
892
1081
  if (trimmed.endsWith("\\")) {
893
1082
  this.multilineBuffer.push(trimmed.slice(0, -1));
894
1083
  this.inMultiline = true;
@@ -972,7 +1161,7 @@ var Repl = class {
972
1161
  try {
973
1162
  const ext = path5.extname(filePath).toLowerCase();
974
1163
  if (IMAGE_EXTS.has(ext)) {
975
- const data = readFileSync3(filePath);
1164
+ const data = readFileSync4(filePath);
976
1165
  const base64 = data.toString("base64");
977
1166
  const mime = MIME_MAP[ext] || "image/png";
978
1167
  imageBlocks.push({
@@ -982,7 +1171,7 @@ var Repl = class {
982
1171
  message = message.replace(match, `[\uC774\uBBF8\uC9C0: ${path5.basename(filePath)}]`);
983
1172
  hasImages = true;
984
1173
  } else {
985
- const content = readFileSync3(filePath, "utf-8");
1174
+ const content = readFileSync4(filePath, "utf-8");
986
1175
  message = message.replace(match, `
987
1176
  [File: ${filePath}]
988
1177
  \`\`\`
@@ -1014,12 +1203,14 @@ ${content}
1014
1203
  }
1015
1204
  stop() {
1016
1205
  this.running = false;
1206
+ unregisterPromptHandler();
1017
1207
  if (this.rl) {
1018
1208
  this.rl.close();
1019
1209
  this.rl = null;
1020
1210
  }
1021
1211
  }
1022
1212
  async gracefulExit() {
1213
+ this.saveHistory();
1023
1214
  this.stop();
1024
1215
  if (this.options.onExit) {
1025
1216
  await this.options.onExit();
@@ -1046,6 +1237,64 @@ init_esm_shims();
1046
1237
 
1047
1238
  // src/agent/conversation.ts
1048
1239
  init_esm_shims();
1240
+
1241
+ // src/utils/tokenizer.ts
1242
+ init_esm_shims();
1243
+ import { getEncoding, encodingForModel } from "js-tiktoken";
1244
+ var encoderCache = /* @__PURE__ */ new Map();
1245
+ function getEncoder(model) {
1246
+ const cacheKey = model ?? "cl100k_base";
1247
+ const cached = encoderCache.get(cacheKey);
1248
+ if (cached) return cached;
1249
+ try {
1250
+ const encoder = model ? encodingForModel(model) : getEncoding("cl100k_base");
1251
+ encoderCache.set(cacheKey, encoder);
1252
+ return encoder;
1253
+ } catch {
1254
+ const fallback = encoderCache.get("cl100k_base");
1255
+ if (fallback) return fallback;
1256
+ const encoder = getEncoding("cl100k_base");
1257
+ encoderCache.set("cl100k_base", encoder);
1258
+ return encoder;
1259
+ }
1260
+ }
1261
+ function countTokens(text, model) {
1262
+ if (!text) return 0;
1263
+ try {
1264
+ const encoder = getEncoder(model);
1265
+ return encoder.encode(text).length;
1266
+ } catch {
1267
+ return Math.ceil(text.length / 4);
1268
+ }
1269
+ }
1270
+ function countContentBlockTokens(blocks, model) {
1271
+ let total = 0;
1272
+ for (const block of blocks) {
1273
+ if (block.type === "text") {
1274
+ total += countTokens(block.text, model);
1275
+ } else if (block.type === "tool_use") {
1276
+ total += countTokens(block.name, model);
1277
+ total += countTokens(JSON.stringify(block.input), model);
1278
+ } else if (block.type === "tool_result") {
1279
+ if (typeof block.content === "string") {
1280
+ total += countTokens(block.content, model);
1281
+ } else {
1282
+ total += countTokens(JSON.stringify(block.content), model);
1283
+ }
1284
+ } else {
1285
+ total += countTokens(JSON.stringify(block), model);
1286
+ }
1287
+ }
1288
+ return total;
1289
+ }
1290
+ function countMessageTokens(content, model) {
1291
+ if (typeof content === "string") {
1292
+ return countTokens(content, model);
1293
+ }
1294
+ return countContentBlockTokens(content, model);
1295
+ }
1296
+
1297
+ // src/agent/conversation.ts
1049
1298
  var Conversation = class _Conversation {
1050
1299
  messages = [];
1051
1300
  systemPrompt = "";
@@ -1131,24 +1380,14 @@ ${summary}` },
1131
1380
  return conv;
1132
1381
  }
1133
1382
  /**
1134
- * Estimate rough token count (very approximate).
1383
+ * tiktoken을 사용하여 정확한 토큰 수를 계산한다.
1135
1384
  */
1136
- estimateTokens() {
1137
- let chars = this.systemPrompt.length;
1385
+ estimateTokens(model) {
1386
+ let tokens = countTokens(this.systemPrompt, model);
1138
1387
  for (const msg of this.messages) {
1139
- if (typeof msg.content === "string") {
1140
- chars += msg.content.length;
1141
- } else {
1142
- for (const block of msg.content) {
1143
- if (block.type === "text") chars += block.text.length;
1144
- else if (block.type === "tool_use") chars += JSON.stringify(block.input).length;
1145
- else if (block.type === "tool_result") {
1146
- chars += typeof block.content === "string" ? block.content.length : JSON.stringify(block.content).length;
1147
- }
1148
- }
1149
- }
1388
+ tokens += countMessageTokens(msg.content, model);
1150
1389
  }
1151
- return Math.ceil(chars / 4);
1390
+ return tokens;
1152
1391
  }
1153
1392
  };
1154
1393
 
@@ -1156,147 +1395,672 @@ ${summary}` },
1156
1395
  init_esm_shims();
1157
1396
  init_tool();
1158
1397
  init_renderer();
1398
+
1399
+ // src/ui/spinner.ts
1400
+ init_esm_shims();
1401
+ import ora from "ora";
1159
1402
  import chalk5 from "chalk";
1160
- var ToolExecutor = class {
1161
- constructor(registry, options = {}) {
1162
- this.registry = registry;
1163
- this.options = options;
1403
+ var currentSpinner = null;
1404
+ function startSpinner(text) {
1405
+ stopSpinner();
1406
+ currentSpinner = ora({
1407
+ text: chalk5.dim(text),
1408
+ spinner: "dots",
1409
+ color: "cyan"
1410
+ }).start();
1411
+ return currentSpinner;
1412
+ }
1413
+ function updateSpinner(text) {
1414
+ if (currentSpinner) {
1415
+ currentSpinner.text = chalk5.dim(text);
1164
1416
  }
1165
- async executeOne(toolCall) {
1166
- const tool = this.registry.get(toolCall.name);
1167
- if (!tool) {
1168
- return {
1169
- toolUseId: toolCall.id,
1170
- toolName: toolCall.name,
1171
- result: makeToolError(`Unknown tool: ${toolCall.name}. Available tools: ${this.registry.listNames().join(", ")}`)
1172
- };
1173
- }
1174
- if (this.options.planMode && !tool.readOnly) {
1175
- return {
1176
- toolUseId: toolCall.id,
1177
- toolName: toolCall.name,
1178
- result: makeToolError(`Tool '${toolCall.name}' is not available in plan mode (read-only). Use only read-only tools.`)
1179
- };
1180
- }
1181
- if (tool.dangerous && this.options.permissionCheck) {
1182
- const allowed = await this.options.permissionCheck(tool, toolCall.input);
1183
- if (!allowed) {
1184
- return {
1185
- toolUseId: toolCall.id,
1186
- toolName: toolCall.name,
1187
- result: makeToolError(`Permission denied for tool: ${toolCall.name}`)
1188
- };
1189
- }
1190
- }
1191
- let input3 = toolCall.input;
1192
- if (this.options.preHook) {
1193
- try {
1194
- const hookResult = await this.options.preHook(toolCall.name, input3);
1195
- if (!hookResult.proceed) {
1196
- return {
1197
- toolUseId: toolCall.id,
1198
- toolName: toolCall.name,
1199
- result: makeToolError(`Tool execution blocked by hook for: ${toolCall.name}`)
1200
- };
1201
- }
1202
- if (hookResult.updatedInput) {
1203
- input3 = hookResult.updatedInput;
1204
- }
1205
- } catch (err) {
1206
- console.error(chalk5.yellow(`Hook error for ${toolCall.name}: ${err}`));
1207
- }
1208
- }
1209
- if (this.options.showToolCalls) {
1210
- console.log(renderToolCall(toolCall.name, input3));
1417
+ }
1418
+ function stopSpinner(symbol) {
1419
+ if (currentSpinner) {
1420
+ if (symbol) {
1421
+ currentSpinner.stopAndPersist({ symbol });
1422
+ } else {
1423
+ currentSpinner.stop();
1211
1424
  }
1212
- let result;
1425
+ currentSpinner = null;
1426
+ }
1427
+ }
1428
+
1429
+ // src/tools/bash.ts
1430
+ init_esm_shims();
1431
+ init_tool();
1432
+ import { spawn } from "child_process";
1433
+ import * as os5 from "os";
1434
+ import chalk7 from "chalk";
1435
+
1436
+ // src/security/command-validator.ts
1437
+ init_esm_shims();
1438
+
1439
+ // src/utils/logger.ts
1440
+ init_esm_shims();
1441
+ import * as fs5 from "fs";
1442
+ import * as path6 from "path";
1443
+ import * as os4 from "os";
1444
+ var LOG_LEVEL_PRIORITY = {
1445
+ debug: 0,
1446
+ info: 1,
1447
+ warn: 2,
1448
+ error: 3
1449
+ };
1450
+ var Logger = class _Logger {
1451
+ static instance;
1452
+ level;
1453
+ logDir;
1454
+ initialized = false;
1455
+ constructor(logDir) {
1456
+ const envLevel = process.env["CODI_LOG_LEVEL"]?.toLowerCase();
1457
+ this.level = this.isValidLevel(envLevel) ? envLevel : "info";
1458
+ this.logDir = logDir ?? path6.join(os4.homedir(), ".codi", "logs");
1459
+ }
1460
+ static getInstance() {
1461
+ if (!_Logger.instance) {
1462
+ _Logger.instance = new _Logger();
1463
+ }
1464
+ return _Logger.instance;
1465
+ }
1466
+ /** 테스트용: 커스텀 logDir로 새 인스턴스 생성 */
1467
+ static createForTest(logDir, level = "debug") {
1468
+ const instance = new _Logger(logDir);
1469
+ instance.level = level;
1470
+ return instance;
1471
+ }
1472
+ isValidLevel(val) {
1473
+ return val !== void 0 && val in LOG_LEVEL_PRIORITY;
1474
+ }
1475
+ ensureDir() {
1476
+ if (this.initialized) return;
1213
1477
  try {
1214
- result = await tool.execute(input3);
1215
- } catch (err) {
1216
- result = makeToolError(
1217
- `Tool '${toolCall.name}' threw an error: ${err instanceof Error ? err.message : String(err)}`
1218
- );
1219
- }
1220
- if (this.options.showToolCalls) {
1221
- console.log(renderToolResult(toolCall.name, result.output, !result.success));
1478
+ fs5.mkdirSync(this.logDir, { recursive: true });
1479
+ this.initialized = true;
1480
+ this.rotateOldLogs();
1481
+ } catch {
1222
1482
  }
1223
- if (this.options.postHook) {
1224
- try {
1225
- await this.options.postHook(toolCall.name, input3, result);
1226
- } catch {
1483
+ }
1484
+ /** 7일 이상 된 로그 파일 삭제 */
1485
+ rotateOldLogs() {
1486
+ try {
1487
+ const files = fs5.readdirSync(this.logDir);
1488
+ const now = Date.now();
1489
+ const maxAge = 7 * 24 * 60 * 60 * 1e3;
1490
+ for (const file of files) {
1491
+ if (!file.startsWith("codi-") || !file.endsWith(".log")) continue;
1492
+ const filePath = path6.join(this.logDir, file);
1493
+ try {
1494
+ const stat = fs5.statSync(filePath);
1495
+ if (now - stat.mtimeMs > maxAge) {
1496
+ fs5.unlinkSync(filePath);
1497
+ }
1498
+ } catch {
1499
+ }
1227
1500
  }
1501
+ } catch {
1228
1502
  }
1229
- return {
1230
- toolUseId: toolCall.id,
1231
- toolName: toolCall.name,
1232
- result
1233
- };
1234
1503
  }
1235
- async executeMany(toolCalls) {
1236
- const safeCalls = [];
1237
- const dangerousCalls = [];
1238
- for (const tc of toolCalls) {
1239
- const tool = this.registry.get(tc.name);
1240
- if (tool?.dangerous) {
1241
- dangerousCalls.push(tc);
1242
- } else {
1243
- safeCalls.push(tc);
1244
- }
1504
+ getLogFilePath() {
1505
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1506
+ return path6.join(this.logDir, `codi-${date}.log`);
1507
+ }
1508
+ shouldLog(level) {
1509
+ return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this.level];
1510
+ }
1511
+ write(level, message, context, error) {
1512
+ if (!this.shouldLog(level)) return;
1513
+ this.ensureDir();
1514
+ if (!this.initialized) return;
1515
+ const entry = {
1516
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1517
+ level,
1518
+ message
1519
+ };
1520
+ if (context && Object.keys(context).length > 0) {
1521
+ entry.context = context;
1245
1522
  }
1246
- const safePromises = safeCalls.map((tc) => this.executeOne(tc));
1247
- const dangerousResults = [];
1248
- for (const tc of dangerousCalls) {
1249
- const result = await this.executeOne(tc);
1250
- dangerousResults.push(result);
1523
+ if (error) {
1524
+ entry.error = {
1525
+ message: error.message,
1526
+ stack: error.stack
1527
+ };
1251
1528
  }
1252
- const safeResults = await Promise.allSettled(safePromises);
1253
- const results = [];
1254
- for (let i = 0; i < safeResults.length; i++) {
1255
- const r = safeResults[i];
1256
- if (r.status === "fulfilled") {
1257
- results.push(r.value);
1258
- } else {
1259
- results.push({
1260
- toolUseId: safeCalls[i].id,
1261
- toolName: safeCalls[i].name,
1262
- result: makeToolError(`Tool execution failed: ${r.reason}`)
1263
- });
1264
- }
1529
+ try {
1530
+ fs5.appendFileSync(this.getLogFilePath(), JSON.stringify(entry) + "\n");
1531
+ } catch {
1265
1532
  }
1266
- results.push(...dangerousResults);
1267
- const orderMap = new Map(toolCalls.map((tc, i) => [tc.id, i]));
1268
- results.sort((a, b) => (orderMap.get(a.toolUseId) ?? 0) - (orderMap.get(b.toolUseId) ?? 0));
1269
- return results;
1270
1533
  }
1271
- setOptions(options) {
1272
- Object.assign(this.options, options);
1534
+ debug(message, context) {
1535
+ this.write("debug", message, context);
1536
+ }
1537
+ info(message, context) {
1538
+ this.write("info", message, context);
1539
+ }
1540
+ warn(message, context) {
1541
+ this.write("warn", message, context);
1542
+ }
1543
+ error(message, context, error) {
1544
+ this.write("error", message, context, error);
1273
1545
  }
1274
1546
  };
1547
+ var logger = Logger.getInstance();
1275
1548
 
1276
- // src/agent/token-tracker.ts
1277
- init_esm_shims();
1278
-
1279
- // src/llm/types.ts
1280
- init_esm_shims();
1549
+ // src/security/command-validator.ts
1550
+ var BLOCKED_PATTERNS = [
1551
+ // 광범위 삭제
1552
+ { 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." },
1553
+ { pattern: /\brm\s+-rf\s*$/, reason: "\uB300\uC0C1 \uC5C6\uB294 rm -rf\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1554
+ { pattern: /\bRemove-Item\s+.*-Recurse.*[/\\]\s*$/, reason: "PowerShell \uAD11\uBC94\uC704 \uC0AD\uC81C\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1555
+ { pattern: /\bRemove-Item\s+.*-Recurse.*(\*|~|[A-Z]:\\)/, reason: "PowerShell \uAD11\uBC94\uC704 \uC0AD\uC81C\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1556
+ // 디스크 연산
1557
+ { pattern: /\bmkfs\b/, reason: "\uD30C\uC77C\uC2DC\uC2A4\uD15C \uD3EC\uB9F7(mkfs) \uBA85\uB839\uC5B4\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1558
+ { pattern: /\bformat\s+[A-Z]:/i, reason: "\uB514\uC2A4\uD06C \uD3EC\uB9F7(format) \uBA85\uB839\uC5B4\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1559
+ { pattern: /\bdd\s+if=/, reason: "dd \uB514\uC2A4\uD06C \uC4F0\uAE30 \uBA85\uB839\uC5B4\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1560
+ // 광범위 권한 변경
1561
+ { 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." },
1562
+ { 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." },
1563
+ // 포크 폭탄
1564
+ { pattern: /:\(\)\s*\{.*\|.*&\s*\}\s*;?\s*:/, reason: "\uD3EC\uD06C \uD3ED\uD0C4\uC774 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1565
+ { pattern: /\bwhile\s+true\s*;\s*do\s+fork/, reason: "\uD3EC\uD06C \uD3ED\uD0C4 \uD328\uD134\uC774 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1566
+ // 디바이스 리디렉션
1567
+ { pattern: />\s*\/dev\/sd[a-z]/, reason: "\uB514\uBC14\uC774\uC2A4 \uC9C1\uC811 \uC4F0\uAE30\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1568
+ { pattern: />\s*\/dev\/nvme/, reason: "\uB514\uBC14\uC774\uC2A4 \uC9C1\uC811 \uC4F0\uAE30\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1569
+ { pattern: />\s*\\\\\.\\PhysicalDrive/, reason: "Windows \uB514\uBC14\uC774\uC2A4 \uC9C1\uC811 \uC4F0\uAE30\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1570
+ // 인터넷에서 받아서 바로 실행
1571
+ { 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." },
1572
+ { 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." },
1573
+ { 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." },
1574
+ { 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." },
1575
+ { 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." },
1576
+ // 시스템 종료/재부팅
1577
+ { pattern: /\bshutdown\b/, reason: "\uC2DC\uC2A4\uD15C \uC885\uB8CC(shutdown) \uBA85\uB839\uC5B4\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1578
+ { pattern: /\breboot\b/, reason: "\uC2DC\uC2A4\uD15C \uC7AC\uBD80\uD305(reboot) \uBA85\uB839\uC5B4\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1579
+ { pattern: /\bhalt\b/, reason: "\uC2DC\uC2A4\uD15C \uC911\uC9C0(halt) \uBA85\uB839\uC5B4\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1580
+ { pattern: /\bStop-Computer\b/, reason: "PowerShell \uC2DC\uC2A4\uD15C \uC885\uB8CC\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1581
+ { pattern: /\bRestart-Computer\b/, reason: "PowerShell \uC2DC\uC2A4\uD15C \uC7AC\uBD80\uD305\uC774 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1582
+ // 프로세스 종료
1583
+ { 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." },
1584
+ { pattern: /\bkillall\b/, reason: "\uC804\uCCB4 \uD504\uB85C\uC138\uC2A4 \uC885\uB8CC(killall)\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1585
+ { 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." }
1586
+ ];
1587
+ var WARNED_PATTERNS = [
1588
+ { 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." },
1589
+ { 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." },
1590
+ { pattern: /\bsudo\b/, reason: "sudo \uBA85\uB839\uC5B4\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1591
+ { pattern: /\bRunAs\b/i, reason: "Windows \uAD00\uB9AC\uC790 \uAD8C\uD55C \uC2E4\uD589\uC774 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1592
+ { pattern: /\bnpm\s+publish\b/, reason: "npm \uD328\uD0A4\uC9C0 \uBC30\uD3EC(npm publish)\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1593
+ { pattern: /\bdocker\s+push\b/, reason: "Docker \uC774\uBBF8\uC9C0 \uD478\uC2DC(docker push)\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." },
1594
+ { pattern: /\bgit\s+push\b/, reason: "git push\uAC00 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." }
1595
+ ];
1596
+ function validateCommand(command) {
1597
+ const trimmed = command.trim();
1598
+ if (!trimmed) {
1599
+ return { allowed: true, level: "allowed" };
1600
+ }
1601
+ for (const { pattern, reason } of BLOCKED_PATTERNS) {
1602
+ if (pattern.test(trimmed)) {
1603
+ logger.warn("\uBA85\uB839\uC5B4 \uCC28\uB2E8\uB428", { command: trimmed, reason });
1604
+ return { allowed: false, level: "blocked", reason };
1605
+ }
1606
+ }
1607
+ for (const { pattern, reason } of WARNED_PATTERNS) {
1608
+ if (pattern.test(trimmed)) {
1609
+ logger.info("\uBA85\uB839\uC5B4 \uACBD\uACE0", { command: trimmed, reason });
1610
+ return { allowed: true, level: "warned", reason };
1611
+ }
1612
+ }
1613
+ return { allowed: true, level: "allowed" };
1614
+ }
1615
+
1616
+ // src/security/permission-manager.ts
1617
+ init_esm_shims();
1618
+ import chalk6 from "chalk";
1619
+
1620
+ // src/config/permissions.ts
1621
+ init_esm_shims();
1622
+ function parseRule(rule) {
1623
+ const match = rule.match(/^(\w+)(?:\((.+)\))?$/);
1624
+ if (match) {
1625
+ return { tool: match[1], pattern: match[2] };
1626
+ }
1627
+ return { tool: rule };
1628
+ }
1629
+ function matchesRule(rule, toolName, input3) {
1630
+ if (rule.tool !== toolName) return false;
1631
+ if (!rule.pattern) return true;
1632
+ const pattern = new RegExp(rule.pattern.replace(/\*/g, ".*"));
1633
+ for (const value of Object.values(input3)) {
1634
+ if (typeof value === "string" && pattern.test(value)) {
1635
+ return true;
1636
+ }
1637
+ }
1638
+ return false;
1639
+ }
1640
+ function evaluatePermission(toolName, input3, rules) {
1641
+ for (const rule of rules.deny) {
1642
+ if (matchesRule(parseRule(rule), toolName, input3)) {
1643
+ return "deny";
1644
+ }
1645
+ }
1646
+ for (const rule of rules.ask) {
1647
+ if (matchesRule(parseRule(rule), toolName, input3)) {
1648
+ return "ask";
1649
+ }
1650
+ }
1651
+ for (const rule of rules.allow) {
1652
+ if (matchesRule(parseRule(rule), toolName, input3)) {
1653
+ return "allow";
1654
+ }
1655
+ }
1656
+ return "ask";
1657
+ }
1658
+
1659
+ // src/security/permission-manager.ts
1660
+ var sessionAllowed = /* @__PURE__ */ new Set();
1661
+ var sessionDenied = /* @__PURE__ */ new Set();
1662
+ var currentMode = "default";
1663
+ function setPermissionMode(mode) {
1664
+ currentMode = mode;
1665
+ }
1666
+ function getPermissionMode() {
1667
+ return currentMode;
1668
+ }
1669
+ async function checkPermission(tool, input3) {
1670
+ if (!tool.dangerous) return true;
1671
+ if (currentMode === "yolo") return true;
1672
+ if (currentMode === "acceptEdits" && ["write_file", "edit_file", "multi_edit"].includes(tool.name)) {
1673
+ return true;
1674
+ }
1675
+ if (currentMode === "plan" && !tool.readOnly) {
1676
+ return false;
1677
+ }
1678
+ const config = configManager.get();
1679
+ const decision = evaluatePermission(tool.name, input3, config.permissions);
1680
+ if (decision === "allow") return true;
1681
+ if (decision === "deny") {
1682
+ console.log(chalk6.red(`\u2717 Permission denied for ${tool.name} (denied by rule)`));
1683
+ return false;
1684
+ }
1685
+ const key = `${tool.name}:${JSON.stringify(input3)}`;
1686
+ if (sessionAllowed.has(tool.name)) return true;
1687
+ if (sessionDenied.has(tool.name)) return false;
1688
+ return promptUser(tool, input3);
1689
+ }
1690
+ async function promptUser(tool, input3) {
1691
+ console.log("");
1692
+ console.log(chalk6.yellow.bold(`\u26A0 Permission Required: ${tool.name}`));
1693
+ const relevantParams = Object.entries(input3).filter(([, v]) => v !== void 0);
1694
+ for (const [key, value] of relevantParams) {
1695
+ const displayValue = typeof value === "string" && value.length > 200 ? value.slice(0, 200) + "..." : String(value);
1696
+ console.log(chalk6.dim(` ${key}: `) + displayValue);
1697
+ }
1698
+ console.log("");
1699
+ try {
1700
+ const answer = await sharedPrompt(
1701
+ chalk6.yellow(`Allow? [${chalk6.bold("Y")}es / ${chalk6.bold("n")}o / ${chalk6.bold("a")}lways for this tool] `)
1702
+ );
1703
+ const choice = answer.trim().toLowerCase();
1704
+ if (choice === "a" || choice === "always") {
1705
+ sessionAllowed.add(tool.name);
1706
+ return true;
1707
+ }
1708
+ if (choice === "n" || choice === "no") {
1709
+ return false;
1710
+ }
1711
+ return true;
1712
+ } catch {
1713
+ return false;
1714
+ }
1715
+ }
1716
+
1717
+ // src/tools/bash.ts
1718
+ var _currentOnOutput = null;
1719
+ function setBashOutputCallback(cb) {
1720
+ _currentOnOutput = cb;
1721
+ }
1722
+ function getDefaultShell() {
1723
+ if (os5.platform() === "win32") {
1724
+ return "powershell.exe";
1725
+ }
1726
+ return process.env["SHELL"] || "/bin/bash";
1727
+ }
1728
+ var backgroundTasks = /* @__PURE__ */ new Map();
1729
+ var taskCounter = 0;
1730
+ var bashTool = {
1731
+ name: "bash",
1732
+ 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).`,
1733
+ inputSchema: {
1734
+ type: "object",
1735
+ properties: {
1736
+ command: { type: "string", description: "The bash command to execute" },
1737
+ description: { type: "string", description: "Brief description of what the command does" },
1738
+ timeout: { type: "number", description: "Timeout in milliseconds (max 600000, default 120000)" },
1739
+ run_in_background: { type: "boolean", description: "Run in background and return a task ID" }
1740
+ },
1741
+ required: ["command"]
1742
+ },
1743
+ dangerous: true,
1744
+ readOnly: false,
1745
+ async execute(input3) {
1746
+ const command = String(input3["command"]);
1747
+ const timeout = Math.min(Number(input3["timeout"]) || 12e4, 6e5);
1748
+ const runInBackground = input3["run_in_background"] === true;
1749
+ if (!command.trim()) {
1750
+ return makeToolError("Command cannot be empty");
1751
+ }
1752
+ if (getPermissionMode() !== "yolo") {
1753
+ const validation = validateCommand(command);
1754
+ if (!validation.allowed) {
1755
+ return makeToolError(`\uBA85\uB839\uC5B4\uAC00 \uCC28\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${validation.reason}`);
1756
+ }
1757
+ if (validation.level === "warned") {
1758
+ console.log(chalk7.yellow(`\u26A0 \uACBD\uACE0: ${validation.reason}`));
1759
+ }
1760
+ }
1761
+ if (runInBackground) {
1762
+ return runBackgroundTask(command);
1763
+ }
1764
+ return new Promise((resolve11) => {
1765
+ const finalCommand = os5.platform() === "win32" ? `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ${command}` : command;
1766
+ const startTime = Date.now();
1767
+ let stdout = "";
1768
+ let stderr = "";
1769
+ let timedOut = false;
1770
+ const proc = spawn(finalCommand, {
1771
+ shell: getDefaultShell(),
1772
+ cwd: process.cwd(),
1773
+ env: { ...process.env },
1774
+ stdio: ["ignore", "pipe", "pipe"]
1775
+ });
1776
+ const timer = setTimeout(() => {
1777
+ timedOut = true;
1778
+ proc.kill("SIGTERM");
1779
+ setTimeout(() => proc.kill("SIGKILL"), 5e3);
1780
+ }, timeout);
1781
+ const onOutput = _currentOnOutput;
1782
+ proc.stdout?.on("data", (data) => {
1783
+ const chunk = data.toString();
1784
+ stdout += chunk;
1785
+ if (onOutput) {
1786
+ onOutput(chunk);
1787
+ }
1788
+ });
1789
+ proc.stderr?.on("data", (data) => {
1790
+ const chunk = data.toString();
1791
+ stderr += chunk;
1792
+ if (onOutput) {
1793
+ onOutput(chunk);
1794
+ }
1795
+ });
1796
+ proc.on("close", (code) => {
1797
+ clearTimeout(timer);
1798
+ const durationMs = Date.now() - startTime;
1799
+ const exitCode = code ?? 1;
1800
+ if (timedOut) {
1801
+ logger.info("bash \uBA85\uB839\uC5B4 \uC2E4\uD589", { command, exitCode, durationMs, timedOut: true });
1802
+ const output4 = [
1803
+ stdout ? `stdout:
1804
+ ${stdout}` : "",
1805
+ stderr ? `stderr:
1806
+ ${stderr}` : "",
1807
+ `Exit code: ${exitCode}`
1808
+ ].filter(Boolean).join("\n\n");
1809
+ resolve11(makeToolError(`Command timed out after ${timeout / 1e3}s
1810
+ ${output4}`));
1811
+ return;
1812
+ }
1813
+ if (exitCode !== 0) {
1814
+ logger.info("bash \uBA85\uB839\uC5B4 \uC2E4\uD589", { command, exitCode, durationMs, timedOut: false });
1815
+ const output4 = [
1816
+ stdout ? `stdout:
1817
+ ${stdout}` : "",
1818
+ stderr ? `stderr:
1819
+ ${stderr}` : "",
1820
+ `Exit code: ${exitCode}`
1821
+ ].filter(Boolean).join("\n\n");
1822
+ resolve11(makeToolResult(output4 || `Command failed with exit code ${exitCode}`));
1823
+ return;
1824
+ }
1825
+ logger.info("bash \uBA85\uB839\uC5B4 \uC2E4\uD589", { command, exitCode: 0, durationMs });
1826
+ const output3 = [
1827
+ stdout ? stdout : "",
1828
+ stderr ? `stderr:
1829
+ ${stderr}` : ""
1830
+ ].filter(Boolean).join("\n");
1831
+ resolve11(makeToolResult(output3 || "(no output)"));
1832
+ });
1833
+ proc.on("error", (err) => {
1834
+ clearTimeout(timer);
1835
+ const durationMs = Date.now() - startTime;
1836
+ logger.info("bash \uBA85\uB839\uC5B4 \uC2E4\uD589 \uC2E4\uD328", { command, durationMs, error: err.message });
1837
+ resolve11(makeToolError(`Failed to execute command: ${err.message}`));
1838
+ });
1839
+ });
1840
+ }
1841
+ };
1842
+ function runBackgroundTask(command) {
1843
+ const taskId = `bg_${++taskCounter}`;
1844
+ const bgCommand = os5.platform() === "win32" ? `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ${command}` : command;
1845
+ const proc = spawn(bgCommand, {
1846
+ shell: getDefaultShell(),
1847
+ cwd: process.cwd(),
1848
+ env: { ...process.env },
1849
+ stdio: ["ignore", "pipe", "pipe"],
1850
+ detached: false
1851
+ });
1852
+ const task = { process: proc, output: "", status: "running", exitCode: void 0 };
1853
+ backgroundTasks.set(taskId, task);
1854
+ proc.stdout?.on("data", (data) => {
1855
+ task.output += data.toString();
1856
+ });
1857
+ proc.stderr?.on("data", (data) => {
1858
+ task.output += data.toString();
1859
+ });
1860
+ proc.on("close", (code) => {
1861
+ task.status = code === 0 ? "done" : "error";
1862
+ task.exitCode = code ?? 1;
1863
+ });
1864
+ proc.on("error", (err) => {
1865
+ task.status = "error";
1866
+ task.output += `
1867
+ Process error: ${err.message}`;
1868
+ });
1869
+ return makeToolResult(`Background task started with ID: ${taskId}
1870
+ Use task_output tool to check results.`);
1871
+ }
1872
+
1873
+ // src/tools/executor.ts
1874
+ import chalk8 from "chalk";
1875
+ var ToolExecutor = class {
1876
+ constructor(registry, options = {}) {
1877
+ this.registry = registry;
1878
+ this.options = options;
1879
+ }
1880
+ async executeOne(toolCall) {
1881
+ const tool = this.registry.get(toolCall.name);
1882
+ if (!tool) {
1883
+ return {
1884
+ toolUseId: toolCall.id,
1885
+ toolName: toolCall.name,
1886
+ result: makeToolError(`Unknown tool: ${toolCall.name}. Available tools: ${this.registry.listNames().join(", ")}`)
1887
+ };
1888
+ }
1889
+ if (this.options.planMode && !tool.readOnly) {
1890
+ return {
1891
+ toolUseId: toolCall.id,
1892
+ toolName: toolCall.name,
1893
+ result: makeToolError(`Tool '${toolCall.name}' is not available in plan mode (read-only). Use only read-only tools.`)
1894
+ };
1895
+ }
1896
+ if (tool.dangerous && this.options.permissionCheck) {
1897
+ const allowed = await this.options.permissionCheck(tool, toolCall.input);
1898
+ if (!allowed) {
1899
+ return {
1900
+ toolUseId: toolCall.id,
1901
+ toolName: toolCall.name,
1902
+ result: makeToolError(`Permission denied for tool: ${toolCall.name}`)
1903
+ };
1904
+ }
1905
+ }
1906
+ let input3 = toolCall.input;
1907
+ if (this.options.preHook) {
1908
+ try {
1909
+ const hookResult = await this.options.preHook(toolCall.name, input3);
1910
+ if (!hookResult.proceed) {
1911
+ return {
1912
+ toolUseId: toolCall.id,
1913
+ toolName: toolCall.name,
1914
+ result: makeToolError(`Tool execution blocked by hook for: ${toolCall.name}`)
1915
+ };
1916
+ }
1917
+ if (hookResult.updatedInput) {
1918
+ input3 = hookResult.updatedInput;
1919
+ }
1920
+ } catch (err) {
1921
+ console.error(chalk8.yellow(`Hook error for ${toolCall.name}: ${err}`));
1922
+ }
1923
+ }
1924
+ if (this.options.showToolCalls) {
1925
+ console.log(renderToolCall(toolCall.name, input3));
1926
+ }
1927
+ let result;
1928
+ const execStart = Date.now();
1929
+ let elapsedTimer = null;
1930
+ if (this.options.showToolCalls) {
1931
+ elapsedTimer = setInterval(() => {
1932
+ const elapsed = ((Date.now() - execStart) / 1e3).toFixed(1);
1933
+ updateSpinner(`${toolCall.name} (${elapsed}s...)`);
1934
+ }, 200);
1935
+ startSpinner(`${toolCall.name}...`);
1936
+ if (toolCall.name === "bash") {
1937
+ setBashOutputCallback((_chunk) => {
1938
+ const elapsed = ((Date.now() - execStart) / 1e3).toFixed(1);
1939
+ updateSpinner(`${toolCall.name} (${elapsed}s...) \u25B8 \uCD9C\uB825 \uC218\uC2E0 \uC911`);
1940
+ });
1941
+ }
1942
+ }
1943
+ try {
1944
+ result = await tool.execute(input3);
1945
+ } catch (err) {
1946
+ result = makeToolError(
1947
+ `Tool '${toolCall.name}' threw an error: ${err instanceof Error ? err.message : String(err)}`
1948
+ );
1949
+ } finally {
1950
+ if (elapsedTimer) {
1951
+ clearInterval(elapsedTimer);
1952
+ }
1953
+ if (toolCall.name === "bash") {
1954
+ setBashOutputCallback(null);
1955
+ }
1956
+ stopSpinner();
1957
+ }
1958
+ const execDuration = Date.now() - execStart;
1959
+ logger.debug("\uB3C4\uAD6C \uC2E4\uD589 \uC644\uB8CC", {
1960
+ tool: toolCall.name,
1961
+ durationMs: execDuration,
1962
+ success: result.success
1963
+ });
1964
+ if (this.options.showToolCalls) {
1965
+ console.log(renderToolResult(toolCall.name, result.output, !result.success, execDuration));
1966
+ }
1967
+ if (this.options.postHook) {
1968
+ try {
1969
+ await this.options.postHook(toolCall.name, input3, result);
1970
+ } catch {
1971
+ }
1972
+ }
1973
+ return {
1974
+ toolUseId: toolCall.id,
1975
+ toolName: toolCall.name,
1976
+ result
1977
+ };
1978
+ }
1979
+ async executeMany(toolCalls) {
1980
+ const safeCalls = [];
1981
+ const dangerousCalls = [];
1982
+ for (const tc of toolCalls) {
1983
+ const tool = this.registry.get(tc.name);
1984
+ if (tool?.dangerous) {
1985
+ dangerousCalls.push(tc);
1986
+ } else {
1987
+ safeCalls.push(tc);
1988
+ }
1989
+ }
1990
+ const safePromises = safeCalls.map((tc) => this.executeOne(tc));
1991
+ const dangerousResults = [];
1992
+ for (const tc of dangerousCalls) {
1993
+ const result = await this.executeOne(tc);
1994
+ dangerousResults.push(result);
1995
+ }
1996
+ const safeResults = await Promise.allSettled(safePromises);
1997
+ const results = [];
1998
+ for (let i = 0; i < safeResults.length; i++) {
1999
+ const r = safeResults[i];
2000
+ if (r.status === "fulfilled") {
2001
+ results.push(r.value);
2002
+ } else {
2003
+ results.push({
2004
+ toolUseId: safeCalls[i].id,
2005
+ toolName: safeCalls[i].name,
2006
+ result: makeToolError(`Tool execution failed: ${r.reason}`)
2007
+ });
2008
+ }
2009
+ }
2010
+ results.push(...dangerousResults);
2011
+ const orderMap = new Map(toolCalls.map((tc, i) => [tc.id, i]));
2012
+ results.sort((a, b) => (orderMap.get(a.toolUseId) ?? 0) - (orderMap.get(b.toolUseId) ?? 0));
2013
+ return results;
2014
+ }
2015
+ setOptions(options) {
2016
+ Object.assign(this.options, options);
2017
+ }
2018
+ };
2019
+
2020
+ // src/agent/token-tracker.ts
2021
+ init_esm_shims();
2022
+
2023
+ // src/llm/types.ts
2024
+ init_esm_shims();
1281
2025
  var MODEL_COSTS = {
2026
+ // Claude models
1282
2027
  "claude-sonnet-4-20250514": { input: 3e-3, output: 0.015 },
1283
2028
  "claude-opus-4-20250514": { input: 0.015, output: 0.075 },
1284
2029
  "claude-haiku-3-5-20241022": { input: 8e-4, output: 4e-3 },
2030
+ // GPT models
1285
2031
  "gpt-4o": { input: 25e-4, output: 0.01 },
1286
2032
  "gpt-4o-mini": { input: 15e-5, output: 6e-4 },
1287
2033
  "gpt-4.1": { input: 2e-3, output: 8e-3 },
1288
2034
  "gpt-4.1-mini": { input: 4e-4, output: 16e-4 },
1289
- "gpt-4.1-nano": { input: 1e-4, output: 4e-4 }
2035
+ "gpt-4.1-nano": { input: 1e-4, output: 4e-4 },
2036
+ "o1": { input: 0.015, output: 0.06 },
2037
+ "o1-mini": { input: 3e-3, output: 0.012 },
2038
+ "o3-mini": { input: 11e-4, output: 44e-4 },
2039
+ // Gemini models
2040
+ "gemini-2.5-flash": { input: 15e-5, output: 6e-4 },
2041
+ "gemini-2.5-pro": { input: 125e-5, output: 0.01 },
2042
+ "gemini-2.0-flash": { input: 1e-4, output: 4e-4 },
2043
+ "gemini-1.5-flash": { input: 75e-6, output: 3e-4 },
2044
+ "gemini-1.5-pro": { input: 125e-5, output: 5e-3 }
1290
2045
  };
1291
2046
  function getModelCost(model) {
2047
+ if (model.startsWith("ollama:") || model.startsWith("ollama/")) {
2048
+ return { input: 0, output: 0 };
2049
+ }
1292
2050
  return MODEL_COSTS[model] ?? { input: 0, output: 0 };
1293
2051
  }
1294
2052
 
1295
2053
  // src/agent/token-tracker.ts
1296
2054
  var TokenTracker = class {
2055
+ // Global (accumulated) counters
1297
2056
  inputTokens = 0;
1298
2057
  outputTokens = 0;
1299
2058
  requests = 0;
2059
+ // Per-session counters
2060
+ sessionInputTokens = 0;
2061
+ sessionOutputTokens = 0;
2062
+ sessionRequests = 0;
2063
+ lastRequestCost = null;
1300
2064
  model = "";
1301
2065
  setModel(model) {
1302
2066
  this.model = model;
@@ -1305,6 +2069,17 @@ var TokenTracker = class {
1305
2069
  this.inputTokens += usage.input_tokens;
1306
2070
  this.outputTokens += usage.output_tokens;
1307
2071
  this.requests++;
2072
+ this.sessionInputTokens += usage.input_tokens;
2073
+ this.sessionOutputTokens += usage.output_tokens;
2074
+ this.sessionRequests++;
2075
+ const costs = getModelCost(this.model);
2076
+ const reqCost = usage.input_tokens / 1e3 * costs.input + usage.output_tokens / 1e3 * costs.output;
2077
+ this.lastRequestCost = {
2078
+ inputTokens: usage.input_tokens,
2079
+ outputTokens: usage.output_tokens,
2080
+ cost: reqCost,
2081
+ timestamp: Date.now()
2082
+ };
1308
2083
  }
1309
2084
  getStats() {
1310
2085
  const costs = getModelCost(this.model);
@@ -1317,6 +2092,25 @@ var TokenTracker = class {
1317
2092
  requests: this.requests
1318
2093
  };
1319
2094
  }
2095
+ getSessionStats() {
2096
+ const costs = getModelCost(this.model);
2097
+ const cost = this.sessionInputTokens / 1e3 * costs.input + this.sessionOutputTokens / 1e3 * costs.output;
2098
+ return {
2099
+ inputTokens: this.sessionInputTokens,
2100
+ outputTokens: this.sessionOutputTokens,
2101
+ totalTokens: this.sessionInputTokens + this.sessionOutputTokens,
2102
+ cost,
2103
+ requests: this.sessionRequests,
2104
+ lastRequestCost: this.lastRequestCost,
2105
+ avgCostPerRequest: this.sessionRequests > 0 ? cost / this.sessionRequests : 0
2106
+ };
2107
+ }
2108
+ resetSession() {
2109
+ this.sessionInputTokens = 0;
2110
+ this.sessionOutputTokens = 0;
2111
+ this.sessionRequests = 0;
2112
+ this.lastRequestCost = null;
2113
+ }
1320
2114
  getCost() {
1321
2115
  return this.getStats().cost;
1322
2116
  }
@@ -1346,39 +2140,9 @@ var TokenTracker = class {
1346
2140
  };
1347
2141
  var tokenTracker = new TokenTracker();
1348
2142
 
1349
- // src/ui/spinner.ts
1350
- init_esm_shims();
1351
- import ora from "ora";
1352
- import chalk6 from "chalk";
1353
- var currentSpinner = null;
1354
- function startSpinner(text) {
1355
- stopSpinner();
1356
- currentSpinner = ora({
1357
- text: chalk6.dim(text),
1358
- spinner: "dots",
1359
- color: "cyan"
1360
- }).start();
1361
- return currentSpinner;
1362
- }
1363
- function updateSpinner(text) {
1364
- if (currentSpinner) {
1365
- currentSpinner.text = chalk6.dim(text);
1366
- }
1367
- }
1368
- function stopSpinner(symbol) {
1369
- if (currentSpinner) {
1370
- if (symbol) {
1371
- currentSpinner.stopAndPersist({ symbol });
1372
- } else {
1373
- currentSpinner.stop();
1374
- }
1375
- currentSpinner = null;
1376
- }
1377
- }
1378
-
1379
2143
  // src/agent/agent-loop.ts
1380
2144
  init_renderer();
1381
- import chalk7 from "chalk";
2145
+ import chalk9 from "chalk";
1382
2146
  var MAX_RETRIES = 3;
1383
2147
  var RETRY_DELAYS = [1e3, 2e3, 4e3];
1384
2148
  async function agentLoop(userMessage, options) {
@@ -1412,8 +2176,9 @@ async function agentLoop(userMessage, options) {
1412
2176
  } catch (err) {
1413
2177
  stopSpinner();
1414
2178
  const errMsg = err instanceof Error ? err.message : String(err);
2179
+ logger.error("LLM \uD638\uCD9C \uC2E4\uD328", { model: provider.model }, err instanceof Error ? err : new Error(errMsg));
1415
2180
  if (showOutput) {
1416
- console.error(chalk7.red(`
2181
+ console.error(chalk9.red(`
1417
2182
  LLM Error: ${errMsg}`));
1418
2183
  }
1419
2184
  return `Error communicating with LLM: ${errMsg}`;
@@ -1427,6 +2192,13 @@ LLM Error: ${errMsg}`));
1427
2192
  outputTokens: stats.outputTokens,
1428
2193
  cost: stats.cost
1429
2194
  });
2195
+ logger.debug("LLM \uC751\uB2F5 \uC218\uC2E0", {
2196
+ model: provider.model,
2197
+ inputTokens: response.usage.input_tokens,
2198
+ outputTokens: response.usage.output_tokens,
2199
+ stopReason: response.stopReason,
2200
+ toolCalls: response.toolCalls?.length ?? 0
2201
+ });
1430
2202
  }
1431
2203
  conversation.addAssistantMessage(response.content);
1432
2204
  const wasStreamed = response._streamed === true;
@@ -1447,7 +2219,7 @@ LLM Error: ${errMsg}`));
1447
2219
  }
1448
2220
  if (response.stopReason === "max_tokens") {
1449
2221
  if (showOutput) {
1450
- console.log(chalk7.yellow("\n\u26A0 Response truncated (max tokens reached)"));
2222
+ console.log(chalk9.yellow("\n\u26A0 Response truncated (max tokens reached)"));
1451
2223
  }
1452
2224
  }
1453
2225
  if (response.toolCalls && response.toolCalls.length > 0) {
@@ -1485,7 +2257,7 @@ LLM Error: ${errMsg}`));
1485
2257
  }
1486
2258
  if (iterations >= maxIterations) {
1487
2259
  if (showOutput) {
1488
- console.log(chalk7.yellow(`
2260
+ console.log(chalk9.yellow(`
1489
2261
  \u26A0 Agent loop reached maximum iterations (${maxIterations})`));
1490
2262
  }
1491
2263
  }
@@ -1539,12 +2311,12 @@ async function callLlmWithRetry(provider, conversation, registry, stream, option
1539
2311
  throw lastError || new Error("Max retries exceeded");
1540
2312
  }
1541
2313
  function sleep(ms) {
1542
- return new Promise((resolve10) => setTimeout(resolve10, ms));
2314
+ return new Promise((resolve11) => setTimeout(resolve11, ms));
1543
2315
  }
1544
2316
 
1545
2317
  // src/agent/system-prompt.ts
1546
2318
  init_esm_shims();
1547
- import * as os4 from "os";
2319
+ import * as os6 from "os";
1548
2320
  import { execSync as execSync2 } from "child_process";
1549
2321
  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.
1550
2322
 
@@ -1604,7 +2376,8 @@ var TOOL_HIERARCHY = `# Tool Usage Rules
1604
2376
  - Use grep instead of bash grep/rg for content search
1605
2377
  - Reserve bash for system commands that have no dedicated tool
1606
2378
  - Use sub_agent for complex multi-step exploration tasks
1607
- - Call multiple tools in parallel when they are independent`;
2379
+ - Call multiple tools in parallel when they are independent
2380
+ - Use update_memory to persist important information (architecture, user preferences, patterns, decisions) across conversations. Proactively save useful context when you discover it.`;
1608
2381
  var WINDOWS_RULES = `# Windows Shell Rules
1609
2382
  You are running on Windows. The shell is PowerShell. Follow these rules:
1610
2383
  - Use PowerShell syntax, NOT bash/sh syntax
@@ -1656,7 +2429,7 @@ function buildSystemPrompt(context) {
1656
2429
  fragments.push(buildEnvironmentInfo(context));
1657
2430
  fragments.push(CONVERSATION_RULES);
1658
2431
  fragments.push(TOOL_HIERARCHY);
1659
- if (os4.platform() === "win32") {
2432
+ if (os6.platform() === "win32") {
1660
2433
  fragments.push(WINDOWS_RULES);
1661
2434
  }
1662
2435
  fragments.push(CODE_RULES);
@@ -1687,8 +2460,8 @@ function buildEnvironmentInfo(context) {
1687
2460
  const lines = [
1688
2461
  "# Environment",
1689
2462
  `- Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
1690
- `- OS: ${os4.platform()} ${os4.release()}`,
1691
- `- Shell: ${os4.platform() === "win32" ? "PowerShell" : process.env["SHELL"] || "/bin/bash"}`,
2463
+ `- OS: ${os6.platform()} ${os6.release()}`,
2464
+ `- Shell: ${os6.platform() === "win32" ? "PowerShell" : process.env["SHELL"] || "/bin/bash"}`,
1692
2465
  `- Working Directory: ${context.cwd}`,
1693
2466
  `- Model: ${context.model}`,
1694
2467
  `- Provider: ${context.provider}`
@@ -1720,7 +2493,7 @@ var ContextCompressor = class {
1720
2493
  this.options = { ...DEFAULT_OPTIONS, ...options };
1721
2494
  }
1722
2495
  shouldCompress(conversation) {
1723
- const estimatedTokens = conversation.estimateTokens();
2496
+ const estimatedTokens = conversation.estimateTokens(this.options.model);
1724
2497
  return estimatedTokens > this.options.maxContextTokens * this.options.threshold;
1725
2498
  }
1726
2499
  async compress(conversation, provider, focusHint) {
@@ -1758,58 +2531,58 @@ ${summaryContent}`;
1758
2531
 
1759
2532
  // src/agent/memory.ts
1760
2533
  init_esm_shims();
1761
- import * as fs4 from "fs";
1762
- import * as os5 from "os";
1763
- import * as path6 from "path";
2534
+ import * as fs6 from "fs";
2535
+ import * as os7 from "os";
2536
+ import * as path7 from "path";
1764
2537
  import * as crypto from "crypto";
1765
2538
  var MemoryManager = class {
1766
2539
  memoryDir;
1767
2540
  constructor() {
1768
- const home = process.env["HOME"] || process.env["USERPROFILE"] || os5.homedir();
2541
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || os7.homedir();
1769
2542
  const projectHash = crypto.createHash("md5").update(process.cwd()).digest("hex").slice(0, 8);
1770
- const projectName = path6.basename(process.cwd());
1771
- this.memoryDir = path6.join(home, ".codi", "projects", `${projectName}-${projectHash}`, "memory");
2543
+ const projectName = path7.basename(process.cwd());
2544
+ this.memoryDir = path7.join(home, ".codi", "projects", `${projectName}-${projectHash}`, "memory");
1772
2545
  }
1773
2546
  ensureDir() {
1774
- if (!fs4.existsSync(this.memoryDir)) {
1775
- fs4.mkdirSync(this.memoryDir, { recursive: true });
2547
+ if (!fs6.existsSync(this.memoryDir)) {
2548
+ fs6.mkdirSync(this.memoryDir, { recursive: true });
1776
2549
  }
1777
2550
  }
1778
2551
  getMemoryDir() {
1779
2552
  return this.memoryDir;
1780
2553
  }
1781
2554
  loadIndex() {
1782
- const indexPath = path6.join(this.memoryDir, "MEMORY.md");
1783
- if (!fs4.existsSync(indexPath)) return "";
1784
- const content = fs4.readFileSync(indexPath, "utf-8");
2555
+ const indexPath = path7.join(this.memoryDir, "MEMORY.md");
2556
+ if (!fs6.existsSync(indexPath)) return "";
2557
+ const content = fs6.readFileSync(indexPath, "utf-8");
1785
2558
  const lines = content.split("\n");
1786
2559
  return lines.slice(0, 200).join("\n");
1787
2560
  }
1788
2561
  saveIndex(content) {
1789
2562
  this.ensureDir();
1790
- const indexPath = path6.join(this.memoryDir, "MEMORY.md");
1791
- fs4.writeFileSync(indexPath, content, "utf-8");
2563
+ const indexPath = path7.join(this.memoryDir, "MEMORY.md");
2564
+ fs6.writeFileSync(indexPath, content, "utf-8");
1792
2565
  }
1793
2566
  loadTopic(name) {
1794
- const topicPath = path6.join(this.memoryDir, `${name}.md`);
1795
- if (!fs4.existsSync(topicPath)) return null;
1796
- return fs4.readFileSync(topicPath, "utf-8");
2567
+ const topicPath = path7.join(this.memoryDir, `${name}.md`);
2568
+ if (!fs6.existsSync(topicPath)) return null;
2569
+ return fs6.readFileSync(topicPath, "utf-8");
1797
2570
  }
1798
2571
  saveTopic(name, content) {
1799
2572
  this.ensureDir();
1800
- const topicPath = path6.join(this.memoryDir, `${name}.md`);
1801
- fs4.writeFileSync(topicPath, content, "utf-8");
2573
+ const topicPath = path7.join(this.memoryDir, `${name}.md`);
2574
+ fs6.writeFileSync(topicPath, content, "utf-8");
1802
2575
  }
1803
2576
  listTopics() {
1804
- if (!fs4.existsSync(this.memoryDir)) return [];
1805
- return fs4.readdirSync(this.memoryDir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").map((f) => f.replace(".md", ""));
2577
+ if (!fs6.existsSync(this.memoryDir)) return [];
2578
+ return fs6.readdirSync(this.memoryDir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").map((f) => f.replace(".md", ""));
1806
2579
  }
1807
2580
  buildMemoryPrompt() {
1808
2581
  const index = this.loadIndex();
1809
2582
  if (!index) return "";
1810
2583
  const lines = [
1811
2584
  `You have a persistent memory directory at ${this.memoryDir}.`,
1812
- "Use write_file/edit_file to update memory files as you learn patterns.",
2585
+ "Use the update_memory tool to save, delete, or list memory topics as you learn patterns.",
1813
2586
  "",
1814
2587
  "Current MEMORY.md:",
1815
2588
  index
@@ -1821,25 +2594,25 @@ var memoryManager = new MemoryManager();
1821
2594
 
1822
2595
  // src/agent/session.ts
1823
2596
  init_esm_shims();
1824
- import * as fs5 from "fs";
1825
- import * as os6 from "os";
1826
- import * as path7 from "path";
2597
+ import * as fs7 from "fs";
2598
+ import * as os8 from "os";
2599
+ import * as path8 from "path";
1827
2600
  import * as crypto2 from "crypto";
1828
2601
  var SessionManager = class {
1829
2602
  sessionsDir;
1830
2603
  constructor() {
1831
- const home = process.env["HOME"] || process.env["USERPROFILE"] || os6.homedir();
1832
- this.sessionsDir = path7.join(home, ".codi", "sessions");
2604
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || os8.homedir();
2605
+ this.sessionsDir = path8.join(home, ".codi", "sessions");
1833
2606
  }
1834
2607
  ensureDir() {
1835
- if (!fs5.existsSync(this.sessionsDir)) {
1836
- fs5.mkdirSync(this.sessionsDir, { recursive: true });
2608
+ if (!fs7.existsSync(this.sessionsDir)) {
2609
+ fs7.mkdirSync(this.sessionsDir, { recursive: true });
1837
2610
  }
1838
2611
  }
1839
2612
  save(conversation, name, model) {
1840
2613
  this.ensureDir();
1841
2614
  const id = name || crypto2.randomUUID().slice(0, 8);
1842
- const filePath = path7.join(this.sessionsDir, `${id}.jsonl`);
2615
+ const filePath = path8.join(this.sessionsDir, `${id}.jsonl`);
1843
2616
  const data = conversation.serialize();
1844
2617
  const meta = {
1845
2618
  id,
@@ -1855,13 +2628,13 @@ var SessionManager = class {
1855
2628
  JSON.stringify({ type: "system", content: data.systemPrompt }),
1856
2629
  ...data.messages.map((m) => JSON.stringify({ type: "message", ...m }))
1857
2630
  ];
1858
- fs5.writeFileSync(filePath, lines.join("\n") + "\n", "utf-8");
2631
+ fs7.writeFileSync(filePath, lines.join("\n") + "\n", "utf-8");
1859
2632
  return id;
1860
2633
  }
1861
2634
  load(id) {
1862
- const filePath = path7.join(this.sessionsDir, `${id}.jsonl`);
1863
- if (!fs5.existsSync(filePath)) return null;
1864
- const content = fs5.readFileSync(filePath, "utf-8");
2635
+ const filePath = path8.join(this.sessionsDir, `${id}.jsonl`);
2636
+ if (!fs7.existsSync(filePath)) return null;
2637
+ const content = fs7.readFileSync(filePath, "utf-8");
1865
2638
  const lines = content.trim().split("\n").filter(Boolean);
1866
2639
  let meta = null;
1867
2640
  let systemPrompt = "";
@@ -1886,12 +2659,12 @@ var SessionManager = class {
1886
2659
  }
1887
2660
  list() {
1888
2661
  this.ensureDir();
1889
- const files = fs5.readdirSync(this.sessionsDir).filter((f) => f.endsWith(".jsonl"));
2662
+ const files = fs7.readdirSync(this.sessionsDir).filter((f) => f.endsWith(".jsonl"));
1890
2663
  const sessions = [];
1891
2664
  for (const file of files) {
1892
- const filePath = path7.join(this.sessionsDir, file);
2665
+ const filePath = path8.join(this.sessionsDir, file);
1893
2666
  try {
1894
- const firstLine = fs5.readFileSync(filePath, "utf-8").split("\n")[0];
2667
+ const firstLine = fs7.readFileSync(filePath, "utf-8").split("\n")[0];
1895
2668
  if (firstLine) {
1896
2669
  const meta = JSON.parse(firstLine);
1897
2670
  if (meta.type === "meta") {
@@ -1909,9 +2682,9 @@ var SessionManager = class {
1909
2682
  return sessions[0] ?? null;
1910
2683
  }
1911
2684
  delete(id) {
1912
- const filePath = path7.join(this.sessionsDir, `${id}.jsonl`);
1913
- if (fs5.existsSync(filePath)) {
1914
- fs5.unlinkSync(filePath);
2685
+ const filePath = path8.join(this.sessionsDir, `${id}.jsonl`);
2686
+ if (fs7.existsSync(filePath)) {
2687
+ fs7.unlinkSync(filePath);
1915
2688
  return true;
1916
2689
  }
1917
2690
  return false;
@@ -1933,25 +2706,79 @@ var sessionManager = new SessionManager();
1933
2706
 
1934
2707
  // src/agent/checkpoint.ts
1935
2708
  init_esm_shims();
2709
+ import * as fs8 from "fs";
2710
+ import * as os9 from "os";
2711
+ import * as path9 from "path";
2712
+ import * as crypto3 from "crypto";
1936
2713
  import { execSync as execSync3 } from "child_process";
2714
+ var MAX_CHECKPOINTS = 20;
1937
2715
  var CheckpointManager = class {
1938
2716
  checkpoints = [];
1939
2717
  nextId = 1;
1940
2718
  isGitRepo;
1941
- constructor() {
2719
+ sessionId;
2720
+ checkpointDir;
2721
+ constructor(sessionId) {
2722
+ this.sessionId = sessionId || crypto3.randomUUID().slice(0, 8);
2723
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || os9.homedir();
2724
+ this.checkpointDir = path9.join(home, ".codi", "checkpoints", this.sessionId);
1942
2725
  try {
1943
2726
  execSync3("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
1944
2727
  this.isGitRepo = true;
1945
2728
  } catch {
1946
2729
  this.isGitRepo = false;
1947
2730
  }
2731
+ this.loadFromDisk();
2732
+ }
2733
+ getSessionId() {
2734
+ return this.sessionId;
2735
+ }
2736
+ ensureDir() {
2737
+ if (!fs8.existsSync(this.checkpointDir)) {
2738
+ fs8.mkdirSync(this.checkpointDir, { recursive: true });
2739
+ }
2740
+ }
2741
+ checkpointPath(id) {
2742
+ return path9.join(this.checkpointDir, `checkpoint-${id}.json`);
2743
+ }
2744
+ saveToDisk(checkpoint) {
2745
+ this.ensureDir();
2746
+ const filePath = this.checkpointPath(checkpoint.id);
2747
+ fs8.writeFileSync(filePath, JSON.stringify(checkpoint), "utf-8");
2748
+ }
2749
+ deleteFromDisk(id) {
2750
+ const filePath = this.checkpointPath(id);
2751
+ if (fs8.existsSync(filePath)) {
2752
+ fs8.unlinkSync(filePath);
2753
+ }
2754
+ }
2755
+ loadFromDisk() {
2756
+ if (!fs8.existsSync(this.checkpointDir)) return;
2757
+ const files = fs8.readdirSync(this.checkpointDir).filter((f) => f.startsWith("checkpoint-") && f.endsWith(".json")).sort((a, b) => {
2758
+ const idA = parseInt(a.replace("checkpoint-", "").replace(".json", ""), 10);
2759
+ const idB = parseInt(b.replace("checkpoint-", "").replace(".json", ""), 10);
2760
+ return idA - idB;
2761
+ });
2762
+ for (const file of files) {
2763
+ try {
2764
+ const content = fs8.readFileSync(path9.join(this.checkpointDir, file), "utf-8");
2765
+ const checkpoint = JSON.parse(content);
2766
+ this.checkpoints.push(checkpoint);
2767
+ if (checkpoint.id >= this.nextId) {
2768
+ this.nextId = checkpoint.id + 1;
2769
+ }
2770
+ } catch {
2771
+ continue;
2772
+ }
2773
+ }
1948
2774
  }
1949
2775
  create(conversation, description) {
1950
2776
  const checkpoint = {
1951
2777
  id: this.nextId++,
1952
2778
  timestamp: Date.now(),
1953
2779
  conversation: conversation.serialize(),
1954
- description
2780
+ description,
2781
+ messageCount: conversation.getMessageCount()
1955
2782
  };
1956
2783
  if (this.isGitRepo) {
1957
2784
  try {
@@ -1973,8 +2800,12 @@ var CheckpointManager = class {
1973
2800
  }
1974
2801
  }
1975
2802
  this.checkpoints.push(checkpoint);
1976
- if (this.checkpoints.length > 20) {
1977
- this.checkpoints = this.checkpoints.slice(-20);
2803
+ this.saveToDisk(checkpoint);
2804
+ if (this.checkpoints.length > MAX_CHECKPOINTS) {
2805
+ const removed = this.checkpoints.splice(0, this.checkpoints.length - MAX_CHECKPOINTS);
2806
+ for (const cp of removed) {
2807
+ this.deleteFromDisk(cp.id);
2808
+ }
1978
2809
  }
1979
2810
  return checkpoint.id;
1980
2811
  }
@@ -1994,7 +2825,10 @@ var CheckpointManager = class {
1994
2825
  }
1995
2826
  const idx = this.checkpoints.indexOf(checkpoint);
1996
2827
  if (idx >= 0) {
1997
- this.checkpoints = this.checkpoints.slice(0, idx + 1);
2828
+ const removed = this.checkpoints.splice(idx + 1);
2829
+ for (const cp of removed) {
2830
+ this.deleteFromDisk(cp.id);
2831
+ }
1998
2832
  }
1999
2833
  return {
2000
2834
  conversation: Conversation.deserialize(checkpoint.conversation),
@@ -2005,54 +2839,64 @@ var CheckpointManager = class {
2005
2839
  return this.checkpoints.map((cp) => ({
2006
2840
  id: cp.id,
2007
2841
  timestamp: cp.timestamp,
2008
- description: cp.description
2842
+ description: cp.description,
2843
+ messageCount: cp.messageCount
2009
2844
  }));
2010
2845
  }
2846
+ /**
2847
+ * 세션 종료 시 체크포인트 파일 정리
2848
+ */
2849
+ cleanup() {
2850
+ if (fs8.existsSync(this.checkpointDir)) {
2851
+ fs8.rmSync(this.checkpointDir, { recursive: true, force: true });
2852
+ }
2853
+ this.checkpoints = [];
2854
+ }
2011
2855
  };
2012
2856
  var checkpointManager = new CheckpointManager();
2013
2857
 
2014
2858
  // src/agent/codi-md.ts
2015
2859
  init_esm_shims();
2016
- import * as fs6 from "fs";
2017
- import * as path8 from "path";
2860
+ import * as fs9 from "fs";
2861
+ import * as path10 from "path";
2018
2862
  function loadCodiMd() {
2019
2863
  const fragments = [];
2020
2864
  let dir = process.cwd();
2021
- const root = path8.parse(dir).root;
2865
+ const root = path10.parse(dir).root;
2022
2866
  while (dir !== root) {
2023
2867
  loadFromDir(dir, fragments);
2024
- const parent = path8.dirname(dir);
2868
+ const parent = path10.dirname(dir);
2025
2869
  if (parent === dir) break;
2026
2870
  dir = parent;
2027
2871
  }
2028
2872
  return fragments.join("\n\n---\n\n");
2029
2873
  }
2030
2874
  function loadFromDir(dir, fragments) {
2031
- const codiPath = path8.join(dir, "CODI.md");
2032
- if (fs6.existsSync(codiPath)) {
2875
+ const codiPath = path10.join(dir, "CODI.md");
2876
+ if (fs9.existsSync(codiPath)) {
2033
2877
  try {
2034
- let content = fs6.readFileSync(codiPath, "utf-8");
2878
+ let content = fs9.readFileSync(codiPath, "utf-8");
2035
2879
  content = processImports(content, dir);
2036
2880
  fragments.push(`[CODI.md from ${dir}]
2037
2881
  ${content}`);
2038
2882
  } catch {
2039
2883
  }
2040
2884
  }
2041
- const localPath = path8.join(dir, "CODI.local.md");
2042
- if (fs6.existsSync(localPath)) {
2885
+ const localPath = path10.join(dir, "CODI.local.md");
2886
+ if (fs9.existsSync(localPath)) {
2043
2887
  try {
2044
- const content = fs6.readFileSync(localPath, "utf-8");
2888
+ const content = fs9.readFileSync(localPath, "utf-8");
2045
2889
  fragments.push(`[CODI.local.md from ${dir}]
2046
2890
  ${content}`);
2047
2891
  } catch {
2048
2892
  }
2049
2893
  }
2050
- const rulesDir = path8.join(dir, ".codi", "rules");
2051
- if (fs6.existsSync(rulesDir)) {
2894
+ const rulesDir = path10.join(dir, ".codi", "rules");
2895
+ if (fs9.existsSync(rulesDir)) {
2052
2896
  try {
2053
- const files = fs6.readdirSync(rulesDir).filter((f) => f.endsWith(".md")).sort();
2897
+ const files = fs9.readdirSync(rulesDir).filter((f) => f.endsWith(".md")).sort();
2054
2898
  for (const file of files) {
2055
- const content = fs6.readFileSync(path8.join(rulesDir, file), "utf-8");
2899
+ const content = fs9.readFileSync(path10.join(rulesDir, file), "utf-8");
2056
2900
  fragments.push(`[Rule: ${file}]
2057
2901
  ${content}`);
2058
2902
  }
@@ -2062,10 +2906,10 @@ ${content}`);
2062
2906
  }
2063
2907
  function processImports(content, baseDir) {
2064
2908
  return content.replace(/@([\w./-]+)/g, (match, importPath) => {
2065
- const resolved = path8.resolve(baseDir, importPath);
2066
- if (fs6.existsSync(resolved)) {
2909
+ const resolved = path10.resolve(baseDir, importPath);
2910
+ if (fs9.existsSync(resolved)) {
2067
2911
  try {
2068
- return fs6.readFileSync(resolved, "utf-8");
2912
+ return fs9.readFileSync(resolved, "utf-8");
2069
2913
  } catch {
2070
2914
  return match;
2071
2915
  }
@@ -2076,12 +2920,12 @@ function processImports(content, baseDir) {
2076
2920
 
2077
2921
  // src/agent/mode-manager.ts
2078
2922
  init_esm_shims();
2079
- var currentMode = "execute";
2923
+ var currentMode2 = "execute";
2080
2924
  function getMode() {
2081
- return currentMode;
2925
+ return currentMode2;
2082
2926
  }
2083
2927
  function setMode(mode) {
2084
- currentMode = mode;
2928
+ currentMode2 = mode;
2085
2929
  }
2086
2930
 
2087
2931
  // src/tools/registry.ts
@@ -2143,117 +2987,12 @@ var ToolRegistry = class _ToolRegistry {
2143
2987
  }
2144
2988
  };
2145
2989
 
2146
- // src/security/permission-manager.ts
2147
- init_esm_shims();
2148
- import * as readline3 from "readline/promises";
2149
- import chalk8 from "chalk";
2150
-
2151
- // src/config/permissions.ts
2152
- init_esm_shims();
2153
- function parseRule(rule) {
2154
- const match = rule.match(/^(\w+)(?:\((.+)\))?$/);
2155
- if (match) {
2156
- return { tool: match[1], pattern: match[2] };
2157
- }
2158
- return { tool: rule };
2159
- }
2160
- function matchesRule(rule, toolName, input3) {
2161
- if (rule.tool !== toolName) return false;
2162
- if (!rule.pattern) return true;
2163
- const pattern = new RegExp(rule.pattern.replace(/\*/g, ".*"));
2164
- for (const value of Object.values(input3)) {
2165
- if (typeof value === "string" && pattern.test(value)) {
2166
- return true;
2167
- }
2168
- }
2169
- return false;
2170
- }
2171
- function evaluatePermission(toolName, input3, rules) {
2172
- for (const rule of rules.deny) {
2173
- if (matchesRule(parseRule(rule), toolName, input3)) {
2174
- return "deny";
2175
- }
2176
- }
2177
- for (const rule of rules.ask) {
2178
- if (matchesRule(parseRule(rule), toolName, input3)) {
2179
- return "ask";
2180
- }
2181
- }
2182
- for (const rule of rules.allow) {
2183
- if (matchesRule(parseRule(rule), toolName, input3)) {
2184
- return "allow";
2185
- }
2186
- }
2187
- return "ask";
2188
- }
2189
-
2190
- // src/security/permission-manager.ts
2191
- var sessionAllowed = /* @__PURE__ */ new Set();
2192
- var sessionDenied = /* @__PURE__ */ new Set();
2193
- var currentMode2 = "default";
2194
- function setPermissionMode(mode) {
2195
- currentMode2 = mode;
2196
- }
2197
- async function checkPermission(tool, input3) {
2198
- if (!tool.dangerous) return true;
2199
- if (currentMode2 === "yolo") return true;
2200
- if (currentMode2 === "acceptEdits" && ["write_file", "edit_file", "multi_edit"].includes(tool.name)) {
2201
- return true;
2202
- }
2203
- if (currentMode2 === "plan" && !tool.readOnly) {
2204
- return false;
2205
- }
2206
- const config = configManager.get();
2207
- const decision = evaluatePermission(tool.name, input3, config.permissions);
2208
- if (decision === "allow") return true;
2209
- if (decision === "deny") {
2210
- console.log(chalk8.red(`\u2717 Permission denied for ${tool.name} (denied by rule)`));
2211
- return false;
2212
- }
2213
- const key = `${tool.name}:${JSON.stringify(input3)}`;
2214
- if (sessionAllowed.has(tool.name)) return true;
2215
- if (sessionDenied.has(tool.name)) return false;
2216
- return promptUser(tool, input3);
2217
- }
2218
- async function promptUser(tool, input3) {
2219
- console.log("");
2220
- console.log(chalk8.yellow.bold(`\u26A0 Permission Required: ${tool.name}`));
2221
- const relevantParams = Object.entries(input3).filter(([, v]) => v !== void 0);
2222
- for (const [key, value] of relevantParams) {
2223
- const displayValue = typeof value === "string" && value.length > 200 ? value.slice(0, 200) + "..." : String(value);
2224
- console.log(chalk8.dim(` ${key}: `) + displayValue);
2225
- }
2226
- console.log("");
2227
- const rl = readline3.createInterface({
2228
- input: process.stdin,
2229
- output: process.stdout
2230
- });
2231
- try {
2232
- const answer = await rl.question(
2233
- chalk8.yellow(`Allow? [${chalk8.bold("Y")}es / ${chalk8.bold("n")}o / ${chalk8.bold("a")}lways for this tool] `)
2234
- );
2235
- rl.close();
2236
- const choice = answer.trim().toLowerCase();
2237
- if (choice === "a" || choice === "always") {
2238
- sessionAllowed.add(tool.name);
2239
- return true;
2240
- }
2241
- if (choice === "n" || choice === "no") {
2242
- return false;
2243
- }
2244
- return true;
2245
- } catch {
2246
- rl.close();
2247
- return false;
2248
- }
2249
- }
2250
-
2251
2990
  // src/hooks/hook-manager.ts
2252
2991
  init_esm_shims();
2253
2992
  import { exec } from "child_process";
2254
- import * as os7 from "os";
2255
- function getDefaultShell() {
2256
- if (os7.platform() === "win32") {
2993
+ import * as os10 from "os";
2994
+ function getDefaultShell2() {
2995
+ if (os10.platform() === "win32") {
2257
2996
  return "powershell.exe";
2258
2997
  }
2259
2998
  return void 0;
@@ -2283,41 +3022,41 @@ var HookManager = class {
2283
3022
  return { proceed: true };
2284
3023
  }
2285
3024
  async runCommandHook(command, context, timeout) {
2286
- return new Promise((resolve10) => {
3025
+ return new Promise((resolve11) => {
2287
3026
  const stdinData = JSON.stringify({
2288
3027
  tool: context["tool"],
2289
3028
  args: context["args"],
2290
3029
  session_id: context["sessionId"],
2291
3030
  cwd: context["cwd"] || process.cwd()
2292
3031
  });
2293
- const isWin = os7.platform() === "win32";
3032
+ const isWin = os10.platform() === "win32";
2294
3033
  const finalCommand = isWin ? `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ${command}` : command;
2295
3034
  const proc = exec(finalCommand, {
2296
3035
  timeout: timeout || 5e3,
2297
3036
  cwd: process.cwd(),
2298
3037
  env: { ...process.env },
2299
- shell: getDefaultShell()
3038
+ shell: getDefaultShell2()
2300
3039
  }, (err, stdout, stderr) => {
2301
3040
  if (err) {
2302
3041
  if (err.code === 2) {
2303
- resolve10({
3042
+ resolve11({
2304
3043
  proceed: false,
2305
3044
  reason: stderr || stdout || "Blocked by hook"
2306
3045
  });
2307
3046
  return;
2308
3047
  }
2309
- resolve10({ proceed: true });
3048
+ resolve11({ proceed: true });
2310
3049
  return;
2311
3050
  }
2312
3051
  try {
2313
3052
  const output3 = JSON.parse(stdout);
2314
- resolve10({
3053
+ resolve11({
2315
3054
  proceed: output3.decision !== "block",
2316
3055
  reason: output3.reason,
2317
3056
  updatedInput: output3.updatedInput
2318
3057
  });
2319
3058
  } catch {
2320
- resolve10({ proceed: true });
3059
+ resolve11({ proceed: true });
2321
3060
  }
2322
3061
  });
2323
3062
  if (proc.stdin) {
@@ -2332,12 +3071,12 @@ var hookManager = new HookManager();
2332
3071
  // src/mcp/mcp-manager.ts
2333
3072
  init_esm_shims();
2334
3073
  init_tool();
2335
- import * as fs7 from "fs";
2336
- import * as os8 from "os";
2337
- import * as path9 from "path";
3074
+ import * as fs10 from "fs";
3075
+ import * as os11 from "os";
3076
+ import * as path11 from "path";
2338
3077
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2339
3078
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
2340
- import chalk9 from "chalk";
3079
+ import chalk10 from "chalk";
2341
3080
  var McpManager = class {
2342
3081
  servers = /* @__PURE__ */ new Map();
2343
3082
  async initialize(registry) {
@@ -2348,21 +3087,21 @@ var McpManager = class {
2348
3087
  try {
2349
3088
  await this.connectServer(name, serverConfig, registry);
2350
3089
  } catch (err) {
2351
- console.error(chalk9.yellow(` \u26A0 Failed to connect MCP server '${name}': ${err instanceof Error ? err.message : String(err)}`));
3090
+ console.error(chalk10.yellow(` \u26A0 Failed to connect MCP server '${name}': ${err instanceof Error ? err.message : String(err)}`));
2352
3091
  }
2353
3092
  }
2354
3093
  }
2355
3094
  loadMcpConfigs() {
2356
3095
  const configs = {};
2357
- const home = process.env["HOME"] || process.env["USERPROFILE"] || os8.homedir();
3096
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || os11.homedir();
2358
3097
  const paths = [
2359
- path9.join(home, ".codi", "mcp.json"),
2360
- path9.join(process.cwd(), ".codi", "mcp.json")
3098
+ path11.join(home, ".codi", "mcp.json"),
3099
+ path11.join(process.cwd(), ".codi", "mcp.json")
2361
3100
  ];
2362
3101
  for (const p of paths) {
2363
3102
  try {
2364
- if (fs7.existsSync(p)) {
2365
- const content = JSON.parse(fs7.readFileSync(p, "utf-8"));
3103
+ if (fs10.existsSync(p)) {
3104
+ const content = JSON.parse(fs10.readFileSync(p, "utf-8"));
2366
3105
  if (content.mcpServers) {
2367
3106
  Object.assign(configs, content.mcpServers);
2368
3107
  }
@@ -2414,7 +3153,7 @@ var McpManager = class {
2414
3153
  registry.register(tool);
2415
3154
  }
2416
3155
  this.servers.set(name, { name, client, transport, tools: toolNames });
2417
- console.log(chalk9.dim(` \u2713 MCP server '${name}' connected (${toolNames.length} tools)`));
3156
+ console.log(chalk10.dim(` \u2713 MCP server '${name}' connected (${toolNames.length} tools)`));
2418
3157
  }
2419
3158
  async disconnect(name) {
2420
3159
  const server = this.servers.get(name);
@@ -2445,7 +3184,7 @@ var mcpManager = new McpManager();
2445
3184
  // src/agent/sub-agent.ts
2446
3185
  init_esm_shims();
2447
3186
  init_tool();
2448
- import chalk10 from "chalk";
3187
+ import chalk11 from "chalk";
2449
3188
  var AGENT_PRESETS = {
2450
3189
  explore: {
2451
3190
  tools: ["read_file", "glob", "grep", "list_dir"],
@@ -2484,7 +3223,7 @@ function createSubAgentHandler(provider, mainRegistry) {
2484
3223
  }
2485
3224
  const conversation = new Conversation();
2486
3225
  const maxIterations = input3["maxIterations"] || preset.maxIterations;
2487
- console.log(chalk10.dim(`
3226
+ console.log(chalk11.dim(`
2488
3227
  \u25B8 Launching ${type} agent: ${task.slice(0, 80)}...`));
2489
3228
  const runAgent = async () => {
2490
3229
  const result = await agentLoop(task, {
@@ -2513,7 +3252,7 @@ function createSubAgentHandler(provider, mainRegistry) {
2513
3252
  }
2514
3253
  try {
2515
3254
  const result = await runAgent();
2516
- console.log(chalk10.dim(` \u25B8 ${type} agent completed.`));
3255
+ console.log(chalk11.dim(` \u25B8 ${type} agent completed.`));
2517
3256
  return makeToolResult(result);
2518
3257
  } catch (err) {
2519
3258
  return makeToolError(`Sub-agent failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -2557,10 +3296,10 @@ var subAgentTool = {
2557
3296
 
2558
3297
  // src/config/slash-commands.ts
2559
3298
  init_esm_shims();
2560
- import * as fs8 from "fs";
2561
- import * as os9 from "os";
2562
- import * as path10 from "path";
2563
- import chalk11 from "chalk";
3299
+ import * as fs12 from "fs";
3300
+ import * as os13 from "os";
3301
+ import * as path13 from "path";
3302
+ import chalk12 from "chalk";
2564
3303
  function createBuiltinCommands() {
2565
3304
  return [
2566
3305
  {
@@ -2568,14 +3307,14 @@ function createBuiltinCommands() {
2568
3307
  description: "Show available commands",
2569
3308
  handler: async () => {
2570
3309
  const commands = createBuiltinCommands();
2571
- console.log(chalk11.bold("\nAvailable Commands:\n"));
3310
+ console.log(chalk12.bold("\nAvailable Commands:\n"));
2572
3311
  for (const cmd of commands) {
2573
- const aliases = cmd.aliases ? chalk11.dim(` (${cmd.aliases.join(", ")})`) : "";
2574
- console.log(` ${chalk11.cyan(cmd.name)}${aliases} - ${cmd.description}`);
3312
+ const aliases = cmd.aliases ? chalk12.dim(` (${cmd.aliases.join(", ")})`) : "";
3313
+ console.log(` ${chalk12.cyan(cmd.name)}${aliases} - ${cmd.description}`);
2575
3314
  }
2576
3315
  console.log("");
2577
- console.log(chalk11.dim(" Prefixes: ! (bash), @ (file reference)"));
2578
- console.log(chalk11.dim(" Use \\ at end of line for multiline input"));
3316
+ console.log(chalk12.dim(" Prefixes: ! (bash), @ (file reference)"));
3317
+ console.log(chalk12.dim(" Use \\ at end of line for multiline input"));
2579
3318
  console.log("");
2580
3319
  return true;
2581
3320
  }
@@ -2588,7 +3327,7 @@ function createBuiltinCommands() {
2588
3327
  if (ctx.exitFn) {
2589
3328
  await ctx.exitFn();
2590
3329
  }
2591
- console.log(chalk11.dim("\nGoodbye!\n"));
3330
+ console.log(chalk12.dim("\nGoodbye!\n"));
2592
3331
  process.exit(0);
2593
3332
  }
2594
3333
  },
@@ -2599,7 +3338,7 @@ function createBuiltinCommands() {
2599
3338
  handler: async (_args, ctx) => {
2600
3339
  ctx.conversation.clear();
2601
3340
  ctx.reloadSystemPrompt();
2602
- console.log(chalk11.green("\u2713 Conversation cleared"));
3341
+ console.log(chalk12.green("\u2713 Conversation cleared"));
2603
3342
  return true;
2604
3343
  }
2605
3344
  },
@@ -2609,7 +3348,7 @@ function createBuiltinCommands() {
2609
3348
  handler: async (args, ctx) => {
2610
3349
  if (!args) {
2611
3350
  const info = statusLine.getInfo();
2612
- console.log(chalk11.cyan(`Current model: ${info.model} (${info.provider})`));
3351
+ console.log(chalk12.cyan(`Current model: ${info.model} (${info.provider})`));
2613
3352
  return true;
2614
3353
  }
2615
3354
  if (args.includes(":")) {
@@ -2618,7 +3357,7 @@ function createBuiltinCommands() {
2618
3357
  } else {
2619
3358
  ctx.setProvider("", args);
2620
3359
  }
2621
- console.log(chalk11.green(`\u2713 Model switched to: ${args}`));
3360
+ console.log(chalk12.green(`\u2713 Model switched to: ${args}`));
2622
3361
  return true;
2623
3362
  }
2624
3363
  },
@@ -2626,10 +3365,10 @@ function createBuiltinCommands() {
2626
3365
  name: "/compact",
2627
3366
  description: "Compress conversation history (optional: focus hint)",
2628
3367
  handler: async (args, ctx) => {
2629
- console.log(chalk11.dim("Compressing conversation..."));
3368
+ console.log(chalk12.dim("Compressing conversation..."));
2630
3369
  await ctx.compressor.compress(ctx.conversation, ctx.provider, args || void 0);
2631
3370
  ctx.reloadSystemPrompt();
2632
- console.log(chalk11.green("\u2713 Conversation compressed"));
3371
+ console.log(chalk12.green("\u2713 Conversation compressed"));
2633
3372
  return true;
2634
3373
  }
2635
3374
  },
@@ -2637,9 +3376,31 @@ function createBuiltinCommands() {
2637
3376
  name: "/cost",
2638
3377
  description: "Show token usage and cost",
2639
3378
  handler: async () => {
2640
- console.log(chalk11.cyan(`
2641
- ${tokenTracker.format()}
2642
- `));
3379
+ const session = tokenTracker.getSessionStats();
3380
+ const total = tokenTracker.getStats();
3381
+ console.log(chalk12.bold("\nToken Usage & Cost:\n"));
3382
+ console.log(chalk12.cyan(" [Current Session]"));
3383
+ console.log(` Requests: ${session.requests}`);
3384
+ console.log(` Input: ${formatTokens(session.inputTokens)}`);
3385
+ console.log(` Output: ${formatTokens(session.outputTokens)}`);
3386
+ console.log(` Total: ${formatTokens(session.totalTokens)}`);
3387
+ console.log(` Cost: $${session.cost.toFixed(4)}`);
3388
+ if (session.requests > 0) {
3389
+ console.log(` Avg/Request: $${session.avgCostPerRequest.toFixed(4)}`);
3390
+ }
3391
+ if (session.lastRequestCost) {
3392
+ console.log(chalk12.dim(` Last Request: $${session.lastRequestCost.cost.toFixed(4)} (${formatTokens(session.lastRequestCost.inputTokens)} in / ${formatTokens(session.lastRequestCost.outputTokens)} out)`));
3393
+ }
3394
+ if (total.requests !== session.requests) {
3395
+ console.log("");
3396
+ console.log(chalk12.cyan(" [Total Accumulated]"));
3397
+ console.log(` Requests: ${total.requests}`);
3398
+ console.log(` Input: ${formatTokens(total.inputTokens)}`);
3399
+ console.log(` Output: ${formatTokens(total.outputTokens)}`);
3400
+ console.log(` Total: ${formatTokens(total.totalTokens)}`);
3401
+ console.log(` Cost: $${total.cost.toFixed(4)}`);
3402
+ }
3403
+ console.log("");
2643
3404
  return true;
2644
3405
  }
2645
3406
  },
@@ -2648,9 +3409,9 @@ ${tokenTracker.format()}
2648
3409
  description: "Show current configuration",
2649
3410
  handler: async () => {
2650
3411
  const config = configManager.get();
2651
- console.log(chalk11.bold("\nConfiguration:\n"));
2652
- console.log(chalk11.dim(JSON.stringify(config, null, 2)));
2653
- console.log(chalk11.dim(`
3412
+ console.log(chalk12.bold("\nConfiguration:\n"));
3413
+ console.log(chalk12.dim(JSON.stringify(config, null, 2)));
3414
+ console.log(chalk12.dim(`
2654
3415
  Config files: ${configManager.getConfigPaths().join(", ") || "(none)"}`));
2655
3416
  console.log("");
2656
3417
  return true;
@@ -2661,10 +3422,10 @@ Config files: ${configManager.getConfigPaths().join(", ") || "(none)"}`));
2661
3422
  description: "Show permission rules",
2662
3423
  handler: async () => {
2663
3424
  const config = configManager.get();
2664
- console.log(chalk11.bold("\nPermission Rules:\n"));
2665
- console.log(chalk11.green(" Allow: ") + config.permissions.allow.join(", "));
2666
- console.log(chalk11.red(" Deny: ") + (config.permissions.deny.join(", ") || "(none)"));
2667
- console.log(chalk11.yellow(" Ask: ") + config.permissions.ask.join(", "));
3425
+ console.log(chalk12.bold("\nPermission Rules:\n"));
3426
+ console.log(chalk12.green(" Allow: ") + config.permissions.allow.join(", "));
3427
+ console.log(chalk12.red(" Deny: ") + (config.permissions.deny.join(", ") || "(none)"));
3428
+ console.log(chalk12.yellow(" Ask: ") + config.permissions.ask.join(", "));
2668
3429
  console.log("");
2669
3430
  return true;
2670
3431
  }
@@ -2675,7 +3436,7 @@ Config files: ${configManager.getConfigPaths().join(", ") || "(none)"}`));
2675
3436
  handler: async (args, ctx) => {
2676
3437
  const name = args || void 0;
2677
3438
  const id = sessionManager.save(ctx.conversation, name, statusLine.getInfo().model);
2678
- console.log(chalk11.green(`\u2713 Session saved: ${id}`));
3439
+ console.log(chalk12.green(`\u2713 Session saved: ${id}`));
2679
3440
  return true;
2680
3441
  }
2681
3442
  },
@@ -2686,12 +3447,12 @@ Config files: ${configManager.getConfigPaths().join(", ") || "(none)"}`));
2686
3447
  handler: async (args, ctx) => {
2687
3448
  const id = args || sessionManager.getLatest()?.id;
2688
3449
  if (!id) {
2689
- console.log(chalk11.yellow("No sessions found. Use /save to save a session first."));
3450
+ console.log(chalk12.yellow("No sessions found. Use /save to save a session first."));
2690
3451
  return true;
2691
3452
  }
2692
3453
  const session = sessionManager.load(id);
2693
3454
  if (!session) {
2694
- console.log(chalk11.red(`Session not found: ${id}`));
3455
+ console.log(chalk12.red(`Session not found: ${id}`));
2695
3456
  return true;
2696
3457
  }
2697
3458
  const data = session.conversation.serialize();
@@ -2701,7 +3462,7 @@ Config files: ${configManager.getConfigPaths().join(", ") || "(none)"}`));
2701
3462
  if (msg.role === "user") ctx.conversation.addUserMessage(msg.content);
2702
3463
  else if (msg.role === "assistant") ctx.conversation.addAssistantMessage(msg.content);
2703
3464
  }
2704
- console.log(chalk11.green(`\u2713 Resumed session: ${id} (${session.meta.messageCount} messages)`));
3465
+ console.log(chalk12.green(`\u2713 Resumed session: ${id} (${session.meta.messageCount} messages)`));
2705
3466
  return true;
2706
3467
  }
2707
3468
  },
@@ -2711,7 +3472,7 @@ Config files: ${configManager.getConfigPaths().join(", ") || "(none)"}`));
2711
3472
  handler: async (args, ctx) => {
2712
3473
  const name = args || `fork-${Date.now()}`;
2713
3474
  const id = sessionManager.save(ctx.conversation, name, statusLine.getInfo().model);
2714
- console.log(chalk11.green(`\u2713 Conversation forked: ${id}`));
3475
+ console.log(chalk12.green(`\u2713 Conversation forked: ${id}`));
2715
3476
  return true;
2716
3477
  }
2717
3478
  },
@@ -2723,7 +3484,7 @@ Config files: ${configManager.getConfigPaths().join(", ") || "(none)"}`));
2723
3484
  const newMode = current === "plan" ? "execute" : "plan";
2724
3485
  setMode(newMode);
2725
3486
  statusLine.update({ mode: newMode });
2726
- console.log(chalk11.cyan(`Mode: ${newMode === "plan" ? "PLAN (read-only)" : "EXECUTE"}`));
3487
+ console.log(chalk12.cyan(`Mode: ${newMode === "plan" ? "PLAN (read-only)" : "EXECUTE"}`));
2727
3488
  return true;
2728
3489
  }
2729
3490
  },
@@ -2733,16 +3494,16 @@ Config files: ${configManager.getConfigPaths().join(", ") || "(none)"}`));
2733
3494
  handler: async () => {
2734
3495
  const index = memoryManager.loadIndex();
2735
3496
  const topics = memoryManager.listTopics();
2736
- console.log(chalk11.bold("\nAuto Memory:\n"));
2737
- console.log(chalk11.dim(`Directory: ${memoryManager.getMemoryDir()}`));
3497
+ console.log(chalk12.bold("\nAuto Memory:\n"));
3498
+ console.log(chalk12.dim(`Directory: ${memoryManager.getMemoryDir()}`));
2738
3499
  if (index) {
2739
- console.log(chalk11.dim("\nMEMORY.md:"));
3500
+ console.log(chalk12.dim("\nMEMORY.md:"));
2740
3501
  console.log(index);
2741
3502
  } else {
2742
- console.log(chalk11.dim("\nNo memory saved yet."));
3503
+ console.log(chalk12.dim("\nNo memory saved yet."));
2743
3504
  }
2744
3505
  if (topics.length > 0) {
2745
- console.log(chalk11.dim(`
3506
+ console.log(chalk12.dim(`
2746
3507
  Topics: ${topics.join(", ")}`));
2747
3508
  }
2748
3509
  console.log("");
@@ -2753,12 +3514,12 @@ Topics: ${topics.join(", ")}`));
2753
3514
  name: "/init",
2754
3515
  description: "Initialize CODI.md in the current project",
2755
3516
  handler: async () => {
2756
- const codiPath = path10.join(process.cwd(), "CODI.md");
2757
- if (fs8.existsSync(codiPath)) {
2758
- console.log(chalk11.yellow("CODI.md already exists"));
3517
+ const codiPath = path13.join(process.cwd(), "CODI.md");
3518
+ if (fs12.existsSync(codiPath)) {
3519
+ console.log(chalk12.yellow("CODI.md already exists"));
2759
3520
  return true;
2760
3521
  }
2761
- const content = `# Project: ${path10.basename(process.cwd())}
3522
+ const content = `# Project: ${path13.basename(process.cwd())}
2762
3523
 
2763
3524
  ## Overview
2764
3525
  <!-- Describe your project here -->
@@ -2772,8 +3533,8 @@ Topics: ${topics.join(", ")}`));
2772
3533
  ## Conventions
2773
3534
  <!-- Code style, naming conventions, etc. -->
2774
3535
  `;
2775
- fs8.writeFileSync(codiPath, content, "utf-8");
2776
- console.log(chalk11.green("\u2713 Created CODI.md"));
3536
+ fs12.writeFileSync(codiPath, content, "utf-8");
3537
+ console.log(chalk12.green("\u2713 Created CODI.md"));
2777
3538
  return true;
2778
3539
  }
2779
3540
  },
@@ -2801,8 +3562,8 @@ ${content}
2801
3562
 
2802
3563
  `;
2803
3564
  }
2804
- fs8.writeFileSync(filePath, md, "utf-8");
2805
- console.log(chalk11.green(`\u2713 Exported to ${filePath}`));
3565
+ fs12.writeFileSync(filePath, md, "utf-8");
3566
+ console.log(chalk12.green(`\u2713 Exported to ${filePath}`));
2806
3567
  return true;
2807
3568
  }
2808
3569
  },
@@ -2813,12 +3574,12 @@ ${content}
2813
3574
  const { taskManager: taskManager2 } = await Promise.resolve().then(() => (init_task_tools(), task_tools_exports));
2814
3575
  const tasks = taskManager2.list();
2815
3576
  if (tasks.length === 0) {
2816
- console.log(chalk11.dim("\nNo tasks.\n"));
3577
+ console.log(chalk12.dim("\nNo tasks.\n"));
2817
3578
  return true;
2818
3579
  }
2819
- console.log(chalk11.bold("\nTasks:\n"));
3580
+ console.log(chalk12.bold("\nTasks:\n"));
2820
3581
  for (const task of tasks) {
2821
- const statusIcon = task.status === "completed" ? chalk11.green("\u2713") : task.status === "in_progress" ? chalk11.yellow("\u27F3") : chalk11.dim("\u25CB");
3582
+ const statusIcon = task.status === "completed" ? chalk12.green("\u2713") : task.status === "in_progress" ? chalk12.yellow("\u27F3") : chalk12.dim("\u25CB");
2822
3583
  console.log(` ${statusIcon} #${task.id} ${task.subject} [${task.status}]`);
2823
3584
  }
2824
3585
  console.log("");
@@ -2833,7 +3594,7 @@ ${content}
2833
3594
  const info = statusLine.getInfo();
2834
3595
  const stats = tokenTracker.getStats();
2835
3596
  const mcpServers = mcpManager.listServers();
2836
- console.log(chalk11.bold("\nCodi Status:\n"));
3597
+ console.log(chalk12.bold("\nCodi Status:\n"));
2837
3598
  console.log(` Version: 0.1.0`);
2838
3599
  console.log(` Model: ${info.model}`);
2839
3600
  console.log(` Provider: ${config.provider}`);
@@ -2855,7 +3616,7 @@ ${content}
2855
3616
  const max = 2e5;
2856
3617
  const pct = Math.round(estimated / max * 100);
2857
3618
  const bar = "\u2588".repeat(Math.round(pct / 5)) + "\u2591".repeat(20 - Math.round(pct / 5));
2858
- console.log(chalk11.bold("\nContext Window:\n"));
3619
+ console.log(chalk12.bold("\nContext Window:\n"));
2859
3620
  console.log(` ${bar} ${pct}%`);
2860
3621
  console.log(` ~${estimated.toLocaleString()} / ${max.toLocaleString()} tokens`);
2861
3622
  console.log(` Messages: ${ctx.conversation.getMessageCount()}`);
@@ -2869,12 +3630,12 @@ ${content}
2869
3630
  handler: async (_args, ctx) => {
2870
3631
  const checkpoints = checkpointManager.list();
2871
3632
  if (checkpoints.length === 0) {
2872
- console.log(chalk11.yellow("No checkpoints available."));
3633
+ console.log(chalk12.yellow("No checkpoints available."));
2873
3634
  return true;
2874
3635
  }
2875
3636
  const result = checkpointManager.rewind();
2876
3637
  if (!result) {
2877
- console.log(chalk11.yellow("No checkpoint to rewind to."));
3638
+ console.log(chalk12.yellow("No checkpoint to rewind to."));
2878
3639
  return true;
2879
3640
  }
2880
3641
  const data = result.conversation.serialize();
@@ -2884,7 +3645,7 @@ ${content}
2884
3645
  if (msg.role === "user") ctx.conversation.addUserMessage(msg.content);
2885
3646
  else if (msg.role === "assistant") ctx.conversation.addAssistantMessage(msg.content);
2886
3647
  }
2887
- console.log(chalk11.green(`\u2713 Rewound to checkpoint${result.description ? `: ${result.description}` : ""}`));
3648
+ console.log(chalk12.green(`\u2713 Rewound to checkpoint${result.description ? `: ${result.description}` : ""}`));
2888
3649
  return true;
2889
3650
  }
2890
3651
  },
@@ -2896,13 +3657,13 @@ ${content}
2896
3657
  const { execSync: execSync5 } = await import("child_process");
2897
3658
  const diff = execSync5("git diff", { encoding: "utf-8", cwd: process.cwd() });
2898
3659
  if (!diff.trim()) {
2899
- console.log(chalk11.dim("\nNo changes.\n"));
3660
+ console.log(chalk12.dim("\nNo changes.\n"));
2900
3661
  } else {
2901
3662
  const { renderDiff: renderDiff2 } = await Promise.resolve().then(() => (init_renderer(), renderer_exports));
2902
3663
  console.log("\n" + renderDiff2("", "", diff) + "\n");
2903
3664
  }
2904
3665
  } catch {
2905
- console.log(chalk11.yellow("Not a git repository or git not available."));
3666
+ console.log(chalk12.yellow("Not a git repository or git not available."));
2906
3667
  }
2907
3668
  return true;
2908
3669
  }
@@ -2915,21 +3676,64 @@ ${content}
2915
3676
  try {
2916
3677
  const staged = execSync5("git diff --cached", { encoding: "utf-8", cwd: process.cwd() });
2917
3678
  const unstaged = execSync5("git diff", { encoding: "utf-8", cwd: process.cwd() });
2918
- const diff = staged + unstaged;
2919
- if (!diff.trim()) {
2920
- console.log(chalk11.dim("\nNo changes to commit.\n"));
3679
+ if (!staged.trim() && unstaged.trim()) {
3680
+ const untracked = execSync5("git ls-files --others --exclude-standard", {
3681
+ encoding: "utf-8",
3682
+ cwd: process.cwd()
3683
+ }).trim();
3684
+ if (untracked) {
3685
+ console.log(chalk12.yellow(`
3686
+ \uC8FC\uC758: \uCD94\uC801\uB418\uC9C0 \uC54A\uB294 \uD30C\uC77C\uC774 \uC788\uC2B5\uB2C8\uB2E4 (\uC790\uB3D9 \uC2A4\uD14C\uC774\uC9D5 \uC548 \uB428):`));
3687
+ for (const f of untracked.split("\n").slice(0, 10)) {
3688
+ console.log(chalk12.dim(` ${f}`));
3689
+ }
3690
+ if (untracked.split("\n").length > 10) {
3691
+ console.log(chalk12.dim(` ... \uC678 ${untracked.split("\n").length - 10}\uAC1C`));
3692
+ }
3693
+ console.log("");
3694
+ }
3695
+ console.log(chalk12.dim("\uC218\uC815\uB41C \uD30C\uC77C\uC744 \uC790\uB3D9 \uC2A4\uD14C\uC774\uC9D5\uD569\uB2C8\uB2E4..."));
3696
+ execSync5("git add -u", { encoding: "utf-8", cwd: process.cwd() });
3697
+ }
3698
+ const finalDiff = execSync5("git diff --cached", { encoding: "utf-8", cwd: process.cwd() });
3699
+ if (!finalDiff.trim()) {
3700
+ console.log(chalk12.dim("\nNo changes to commit.\n"));
2921
3701
  return true;
2922
3702
  }
3703
+ let conventionHint = "";
3704
+ try {
3705
+ const recentLog = execSync5("git log --oneline -10", {
3706
+ encoding: "utf-8",
3707
+ cwd: process.cwd()
3708
+ }).trim();
3709
+ if (recentLog) {
3710
+ const conventionalPattern = /^[a-f0-9]+\s+(feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(\(.+\))?[!]?:/;
3711
+ const lines = recentLog.split("\n");
3712
+ const conventionalCount = lines.filter((l) => conventionalPattern.test(l)).length;
3713
+ if (conventionalCount >= 3) {
3714
+ conventionHint = `
3715
+
3716
+ \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.`;
3717
+ }
3718
+ conventionHint += `
3719
+
3720
+ \uCD5C\uADFC \uCEE4\uBC0B \uCC38\uACE0:
3721
+ \`\`\`
3722
+ ${recentLog}
3723
+ \`\`\``;
3724
+ }
3725
+ } catch {
3726
+ }
2923
3727
  ctx.conversation.addUserMessage(
2924
- `\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.
3728
+ `\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}
2925
3729
 
2926
3730
  \`\`\`diff
2927
- ${diff}
3731
+ ${finalDiff}
2928
3732
  \`\`\``
2929
3733
  );
2930
3734
  return false;
2931
3735
  } catch {
2932
- console.log(chalk11.yellow("Not a git repository or git not available."));
3736
+ console.log(chalk12.yellow("Not a git repository or git not available."));
2933
3737
  return true;
2934
3738
  }
2935
3739
  }
@@ -2944,7 +3748,7 @@ ${diff}
2944
3748
  const unstaged = execSync5("git diff", { encoding: "utf-8", cwd: process.cwd() });
2945
3749
  const diff = staged + unstaged;
2946
3750
  if (!diff.trim()) {
2947
- console.log(chalk11.dim("\nNo changes to review.\n"));
3751
+ console.log(chalk12.dim("\nNo changes to review.\n"));
2948
3752
  return true;
2949
3753
  }
2950
3754
  ctx.conversation.addUserMessage(
@@ -2956,7 +3760,7 @@ ${diff}
2956
3760
  );
2957
3761
  return false;
2958
3762
  } catch {
2959
- console.log(chalk11.yellow("Not a git repository or git not available."));
3763
+ console.log(chalk12.yellow("Not a git repository or git not available."));
2960
3764
  return true;
2961
3765
  }
2962
3766
  }
@@ -2966,27 +3770,27 @@ ${diff}
2966
3770
  description: "Search past conversation sessions",
2967
3771
  handler: async (args) => {
2968
3772
  if (!args) {
2969
- console.log(chalk11.yellow("Usage: /search <keyword>"));
3773
+ console.log(chalk12.yellow("Usage: /search <keyword>"));
2970
3774
  return true;
2971
3775
  }
2972
- const home = process.env["HOME"] || process.env["USERPROFILE"] || os9.homedir();
2973
- const sessionsDir = path10.join(home, ".codi", "sessions");
2974
- if (!fs8.existsSync(sessionsDir)) {
2975
- console.log(chalk11.dim("\nNo sessions found.\n"));
3776
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || os13.homedir();
3777
+ const sessionsDir = path13.join(home, ".codi", "sessions");
3778
+ if (!fs12.existsSync(sessionsDir)) {
3779
+ console.log(chalk12.dim("\nNo sessions found.\n"));
2976
3780
  return true;
2977
3781
  }
2978
- const files = fs8.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
3782
+ const files = fs12.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
2979
3783
  const results = [];
2980
3784
  const keyword = args.toLowerCase();
2981
3785
  for (const file of files) {
2982
3786
  if (results.length >= 10) break;
2983
- const filePath = path10.join(sessionsDir, file);
2984
- const lines = fs8.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
3787
+ const filePath = path13.join(sessionsDir, file);
3788
+ const lines = fs12.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
2985
3789
  for (const line of lines) {
2986
3790
  if (results.length >= 10) break;
2987
3791
  if (line.toLowerCase().includes(keyword)) {
2988
3792
  const sessionId = file.replace(".jsonl", "");
2989
- const stat = fs8.statSync(filePath);
3793
+ const stat = fs12.statSync(filePath);
2990
3794
  const date = stat.mtime.toISOString().split("T")[0];
2991
3795
  const preview = line.length > 100 ? line.slice(0, 100) + "..." : line;
2992
3796
  results.push({ sessionId, date, preview });
@@ -2995,16 +3799,16 @@ ${diff}
2995
3799
  }
2996
3800
  }
2997
3801
  if (results.length === 0) {
2998
- console.log(chalk11.dim(`
3802
+ console.log(chalk12.dim(`
2999
3803
  No results for "${args}".
3000
3804
  `));
3001
3805
  } else {
3002
- console.log(chalk11.bold(`
3806
+ console.log(chalk12.bold(`
3003
3807
  Search results for "${args}":
3004
3808
  `));
3005
3809
  for (const r of results) {
3006
- console.log(` ${chalk11.cyan(r.sessionId)} ${chalk11.dim(r.date)}`);
3007
- console.log(` ${chalk11.dim(r.preview)}`);
3810
+ console.log(` ${chalk12.cyan(r.sessionId)} ${chalk12.dim(r.date)}`);
3811
+ console.log(` ${chalk12.dim(r.preview)}`);
3008
3812
  }
3009
3813
  console.log("");
3010
3814
  }
@@ -3016,24 +3820,24 @@ Search results for "${args}":
3016
3820
  description: "Run a command and auto-fix errors (e.g., /fix npm run build)",
3017
3821
  handler: async (args, ctx) => {
3018
3822
  if (!args) {
3019
- console.log(chalk11.yellow("Usage: /fix <command>"));
3823
+ console.log(chalk12.yellow("Usage: /fix <command>"));
3020
3824
  return true;
3021
3825
  }
3022
3826
  const { execSync: execSync5 } = await import("child_process");
3023
3827
  try {
3024
- const isWin = os9.platform() === "win32";
3828
+ const isWin = os13.platform() === "win32";
3025
3829
  const shell = isWin ? "powershell.exe" : void 0;
3026
3830
  const fixCmd = isWin ? `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ${args}` : args;
3027
3831
  const output3 = execSync5(fixCmd, { encoding: "utf-8", cwd: process.cwd(), stdio: "pipe", shell });
3028
- console.log(chalk11.green(`
3832
+ console.log(chalk12.green(`
3029
3833
  \u2713 Command succeeded. No errors to fix.
3030
3834
  `));
3031
- if (output3.trim()) console.log(chalk11.dim(output3));
3835
+ if (output3.trim()) console.log(chalk12.dim(output3));
3032
3836
  return true;
3033
3837
  } catch (err) {
3034
3838
  const error = err;
3035
3839
  const errorOutput = (error.stderr || "") + (error.stdout || "");
3036
- console.log(chalk11.red(`
3840
+ console.log(chalk12.red(`
3037
3841
  Command failed: ${args}
3038
3842
  `));
3039
3843
  ctx.conversation.addUserMessage(
@@ -3049,21 +3853,236 @@ ${errorOutput}
3049
3853
  }
3050
3854
  }
3051
3855
  },
3856
+ {
3857
+ name: "/undo",
3858
+ description: "Undo the most recent file edit (rollback from backup)",
3859
+ handler: async (args) => {
3860
+ const { getBackupHistory: getBackupHistory2, undoLast: undoLast2 } = await Promise.resolve().then(() => (init_file_backup(), file_backup_exports));
3861
+ if (args === "list") {
3862
+ const history = getBackupHistory2();
3863
+ if (history.length === 0) {
3864
+ console.log(chalk12.dim("\n\uB418\uB3CC\uB9B4 \uD30C\uC77C \uBCC0\uACBD \uC774\uB825\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n"));
3865
+ return true;
3866
+ }
3867
+ console.log(chalk12.bold(`
3868
+ \uD30C\uC77C \uBCC0\uACBD \uC774\uB825 (\uCD5C\uADFC ${Math.min(history.length, 20)}\uAC1C):
3869
+ `));
3870
+ const recent = history.slice(-20).reverse();
3871
+ for (let i = 0; i < recent.length; i++) {
3872
+ const entry2 = recent[i];
3873
+ const time = new Date(entry2.timestamp).toLocaleTimeString();
3874
+ const tag = entry2.wasNew ? chalk12.yellow("[\uC0C8 \uD30C\uC77C]") : chalk12.cyan("[\uC218\uC815]");
3875
+ console.log(` ${i + 1}. ${tag} ${entry2.originalPath} ${chalk12.dim(time)}`);
3876
+ }
3877
+ console.log("");
3878
+ return true;
3879
+ }
3880
+ const entry = undoLast2();
3881
+ if (!entry) {
3882
+ console.log(chalk12.yellow("\n\uB418\uB3CC\uB9B4 \uBCC0\uACBD \uC0AC\uD56D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n"));
3883
+ return true;
3884
+ }
3885
+ const action = entry.wasNew ? "\uC0AD\uC81C\uB428 (\uC0C8\uB85C \uC0DD\uC131\uB41C \uD30C\uC77C)" : "\uC774\uC804 \uC0C1\uD0DC\uB85C \uBCF5\uC6D0\uB428";
3886
+ console.log(chalk12.green(`
3887
+ \u2713 \uB418\uB3CC\uB9AC\uAE30 \uC644\uB8CC: ${entry.originalPath}`));
3888
+ console.log(chalk12.dim(` ${action}`));
3889
+ console.log("");
3890
+ return true;
3891
+ }
3892
+ },
3893
+ {
3894
+ name: "/branch",
3895
+ description: "Create and switch to a new branch, or show current branch",
3896
+ handler: async (args) => {
3897
+ const { execSync: execSync5 } = await import("child_process");
3898
+ try {
3899
+ if (!args) {
3900
+ const current = execSync5("git branch --show-current", {
3901
+ encoding: "utf-8",
3902
+ cwd: process.cwd()
3903
+ }).trim();
3904
+ const branches = execSync5("git branch -a", {
3905
+ encoding: "utf-8",
3906
+ cwd: process.cwd()
3907
+ }).trim();
3908
+ console.log(chalk12.bold(`
3909
+ Current branch: ${chalk12.green(current || "(detached HEAD)")}
3910
+ `));
3911
+ console.log(branches);
3912
+ console.log("");
3913
+ } else {
3914
+ const name = args.trim();
3915
+ execSync5(`git checkout -b ${name}`, {
3916
+ encoding: "utf-8",
3917
+ cwd: process.cwd()
3918
+ });
3919
+ console.log(chalk12.green(`
3920
+ \u2713 Created and switched to branch: ${name}
3921
+ `));
3922
+ }
3923
+ } catch (err) {
3924
+ const error = err;
3925
+ const msg = error.stderr || error.message || "Unknown error";
3926
+ console.log(chalk12.red(`
3927
+ Branch operation failed: ${msg.trim()}
3928
+ `));
3929
+ }
3930
+ return true;
3931
+ }
3932
+ },
3933
+ {
3934
+ name: "/stash",
3935
+ description: "Git stash management (pop, list, drop, or save)",
3936
+ handler: async (args) => {
3937
+ const { execSync: execSync5 } = await import("child_process");
3938
+ const sub = args.trim().split(/\s+/);
3939
+ const action = sub[0] || "push";
3940
+ const allowed = ["push", "pop", "list", "drop", "show", "apply", "clear"];
3941
+ if (!allowed.includes(action)) {
3942
+ console.log(chalk12.yellow(`Usage: /stash [${allowed.join("|")}]`));
3943
+ return true;
3944
+ }
3945
+ try {
3946
+ if (action === "clear") {
3947
+ console.log(chalk12.yellow("\u26A0 This will drop all stashes permanently."));
3948
+ }
3949
+ const cmd = `git stash ${args.trim() || "push"}`;
3950
+ const output3 = execSync5(cmd, {
3951
+ encoding: "utf-8",
3952
+ cwd: process.cwd()
3953
+ });
3954
+ console.log(output3.trim() ? `
3955
+ ${output3.trim()}
3956
+ ` : chalk12.dim("\n(no output)\n"));
3957
+ } catch (err) {
3958
+ const error = err;
3959
+ const msg = error.stderr || error.stdout || error.message || "Unknown error";
3960
+ console.log(chalk12.red(`
3961
+ Stash operation failed: ${msg.trim()}
3962
+ `));
3963
+ }
3964
+ return true;
3965
+ }
3966
+ },
3967
+ {
3968
+ name: "/pr",
3969
+ description: "Generate a pull request description from current branch diff",
3970
+ handler: async (_args, ctx) => {
3971
+ const { execSync: execSync5 } = await import("child_process");
3972
+ try {
3973
+ const currentBranch = execSync5("git branch --show-current", {
3974
+ encoding: "utf-8",
3975
+ cwd: process.cwd()
3976
+ }).trim();
3977
+ if (!currentBranch) {
3978
+ console.log(chalk12.yellow("Not on a branch (detached HEAD)."));
3979
+ return true;
3980
+ }
3981
+ let baseBranch = "main";
3982
+ try {
3983
+ execSync5("git rev-parse --verify main", {
3984
+ encoding: "utf-8",
3985
+ cwd: process.cwd(),
3986
+ stdio: "pipe"
3987
+ });
3988
+ } catch {
3989
+ try {
3990
+ execSync5("git rev-parse --verify master", {
3991
+ encoding: "utf-8",
3992
+ cwd: process.cwd(),
3993
+ stdio: "pipe"
3994
+ });
3995
+ baseBranch = "master";
3996
+ } catch {
3997
+ console.log(chalk12.yellow("Cannot find base branch (main or master)."));
3998
+ return true;
3999
+ }
4000
+ }
4001
+ if (currentBranch === baseBranch) {
4002
+ console.log(chalk12.yellow(`Already on ${baseBranch}. Switch to a feature branch first.`));
4003
+ return true;
4004
+ }
4005
+ let commitLog = "";
4006
+ try {
4007
+ commitLog = execSync5(`git log ${baseBranch}..HEAD --oneline`, {
4008
+ encoding: "utf-8",
4009
+ cwd: process.cwd()
4010
+ }).trim();
4011
+ } catch {
4012
+ }
4013
+ if (!commitLog) {
4014
+ console.log(chalk12.yellow(`No commits ahead of ${baseBranch}.`));
4015
+ return true;
4016
+ }
4017
+ let diffStat = "";
4018
+ try {
4019
+ diffStat = execSync5(`git diff ${baseBranch}...HEAD --stat`, {
4020
+ encoding: "utf-8",
4021
+ cwd: process.cwd()
4022
+ }).trim();
4023
+ } catch {
4024
+ }
4025
+ let diff = "";
4026
+ try {
4027
+ diff = execSync5(`git diff ${baseBranch}...HEAD`, {
4028
+ encoding: "utf-8",
4029
+ cwd: process.cwd(),
4030
+ maxBuffer: 10 * 1024 * 1024
4031
+ });
4032
+ if (diff.length > 5e4) {
4033
+ diff = diff.slice(0, 5e4) + "\n\n... (diff truncated, too large)";
4034
+ }
4035
+ } catch {
4036
+ }
4037
+ console.log(chalk12.dim(`
4038
+ Analyzing ${commitLog.split("\n").length} commit(s) from ${currentBranch}...
4039
+ `));
4040
+ ctx.conversation.addUserMessage(
4041
+ `\uD604\uC7AC \uBE0C\uB79C\uCE58 \`${currentBranch}\`\uC5D0\uC11C \`${baseBranch}\`\uB85C \uBCF4\uB0BC Pull Request \uC124\uBA85\uC744 \uC0DD\uC131\uD574\uC918.
4042
+
4043
+ \uB2E4\uC74C \uD615\uC2DD\uC758 \uB9C8\uD06C\uB2E4\uC6B4\uC73C\uB85C \uCD9C\uB825\uD574\uC918:
4044
+ - **Title**: PR \uC81C\uBAA9 (70\uC790 \uC774\uB0B4, \uC601\uBB38)
4045
+ - **## Summary**: \uBCC0\uACBD \uC0AC\uD56D \uC694\uC57D (1-3 bullet points)
4046
+ - **## Changes**: \uC8FC\uC694 \uBCC0\uACBD \uD30C\uC77C \uBC0F \uB0B4\uC6A9
4047
+ - **## Test Plan**: \uD14C\uC2A4\uD2B8 \uACC4\uD68D \uCCB4\uD06C\uB9AC\uC2A4\uD2B8
4048
+
4049
+ ### Commits:
4050
+ \`\`\`
4051
+ ${commitLog}
4052
+ \`\`\`
4053
+
4054
+ ### Diff stat:
4055
+ \`\`\`
4056
+ ${diffStat}
4057
+ \`\`\`
4058
+
4059
+ ### Full diff:
4060
+ \`\`\`diff
4061
+ ${diff}
4062
+ \`\`\``
4063
+ );
4064
+ return false;
4065
+ } catch {
4066
+ console.log(chalk12.yellow("Not a git repository or git not available."));
4067
+ return true;
4068
+ }
4069
+ }
4070
+ },
3052
4071
  {
3053
4072
  name: "/mcp",
3054
4073
  description: "Show MCP server status",
3055
4074
  handler: async () => {
3056
4075
  const servers = mcpManager.listServers();
3057
4076
  if (servers.length === 0) {
3058
- console.log(chalk11.dim("\nNo MCP servers connected.\n"));
3059
- console.log(chalk11.dim("Add servers in .codi/mcp.json or ~/.codi/mcp.json"));
4077
+ console.log(chalk12.dim("\nNo MCP servers connected.\n"));
4078
+ console.log(chalk12.dim("Add servers in .codi/mcp.json or ~/.codi/mcp.json"));
3060
4079
  return true;
3061
4080
  }
3062
- console.log(chalk11.bold("\nMCP Servers:\n"));
4081
+ console.log(chalk12.bold("\nMCP Servers:\n"));
3063
4082
  for (const s of servers) {
3064
- console.log(` ${chalk11.green("\u25CF")} ${s.name}`);
4083
+ console.log(` ${chalk12.green("\u25CF")} ${s.name}`);
3065
4084
  for (const t of s.tools) {
3066
- console.log(chalk11.dim(` - ${t}`));
4085
+ console.log(chalk12.dim(` - ${t}`));
3067
4086
  }
3068
4087
  }
3069
4088
  console.log("");
@@ -3072,24 +4091,29 @@ ${errorOutput}
3072
4091
  }
3073
4092
  ];
3074
4093
  }
4094
+ function formatTokens(n) {
4095
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
4096
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
4097
+ return String(n);
4098
+ }
3075
4099
  function loadCustomCommands() {
3076
4100
  const commands = [];
3077
- const home = process.env["HOME"] || process.env["USERPROFILE"] || os9.homedir();
4101
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || os13.homedir();
3078
4102
  const dirs = [
3079
- path10.join(home, ".codi", "commands"),
3080
- path10.join(process.cwd(), ".codi", "commands")
4103
+ path13.join(home, ".codi", "commands"),
4104
+ path13.join(process.cwd(), ".codi", "commands")
3081
4105
  ];
3082
4106
  for (const dir of dirs) {
3083
- if (!fs8.existsSync(dir)) continue;
3084
- const files = fs8.readdirSync(dir).filter((f) => f.endsWith(".md"));
4107
+ if (!fs12.existsSync(dir)) continue;
4108
+ const files = fs12.readdirSync(dir).filter((f) => f.endsWith(".md"));
3085
4109
  for (const file of files) {
3086
4110
  const name = "/" + file.replace(".md", "");
3087
- const filePath = path10.join(dir, file);
4111
+ const filePath = path13.join(dir, file);
3088
4112
  commands.push({
3089
4113
  name,
3090
- description: `Custom command from ${path10.relative(process.cwd(), filePath)}`,
4114
+ description: `Custom command from ${path13.relative(process.cwd(), filePath)}`,
3091
4115
  handler: async (_args, ctx) => {
3092
- let content = fs8.readFileSync(filePath, "utf-8");
4116
+ let content = fs12.readFileSync(filePath, "utf-8");
3093
4117
  content = content.replace(/\{\{cwd\}\}/g, process.cwd()).replace(/\{\{date\}\}/g, (/* @__PURE__ */ new Date()).toISOString().split("T")[0]).replace(/\{\{file_path\}\}/g, _args || "");
3094
4118
  await ctx.conversation.addUserMessage(content);
3095
4119
  return false;
@@ -3103,8 +4127,8 @@ function loadCustomCommands() {
3103
4127
  // src/tools/file-read.ts
3104
4128
  init_esm_shims();
3105
4129
  init_tool();
3106
- import * as fs9 from "fs";
3107
- import * as path11 from "path";
4130
+ import * as fs13 from "fs";
4131
+ import * as path14 from "path";
3108
4132
  var fileReadTool = {
3109
4133
  name: "read_file",
3110
4134
  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.`,
@@ -3124,17 +4148,17 @@ var fileReadTool = {
3124
4148
  const filePath = String(input3["file_path"]);
3125
4149
  const offset = input3["offset"];
3126
4150
  const limit = input3["limit"];
3127
- const resolved = path11.resolve(filePath);
3128
- if (!fs9.existsSync(resolved)) {
4151
+ const resolved = path14.resolve(filePath);
4152
+ if (!fs13.existsSync(resolved)) {
3129
4153
  return makeToolError(`File not found: ${resolved}`);
3130
4154
  }
3131
- const stat = fs9.statSync(resolved);
4155
+ const stat = fs13.statSync(resolved);
3132
4156
  if (stat.isDirectory()) {
3133
4157
  return makeToolError(`Path is a directory, not a file: ${resolved}. Use list_dir instead.`);
3134
4158
  }
3135
- const ext = path11.extname(resolved).toLowerCase();
4159
+ const ext = path14.extname(resolved).toLowerCase();
3136
4160
  if ([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"].includes(ext)) {
3137
- const data = fs9.readFileSync(resolved);
4161
+ const data = fs13.readFileSync(resolved);
3138
4162
  const base64 = data.toString("base64");
3139
4163
  const mimeMap = {
3140
4164
  ".png": "image/png",
@@ -3145,7 +4169,7 @@ var fileReadTool = {
3145
4169
  ".bmp": "image/bmp",
3146
4170
  ".svg": "image/svg+xml"
3147
4171
  };
3148
- return makeToolResult(`[Image: ${path11.basename(resolved)}]`, {
4172
+ return makeToolResult(`[Image: ${path14.basename(resolved)}]`, {
3149
4173
  filePath: resolved,
3150
4174
  isImage: true,
3151
4175
  imageData: base64,
@@ -3156,7 +4180,7 @@ var fileReadTool = {
3156
4180
  try {
3157
4181
  const pdfModule = await import("pdf-parse");
3158
4182
  const pdfParse = pdfModule.default || pdfModule;
3159
- const buffer = fs9.readFileSync(resolved);
4183
+ const buffer = fs13.readFileSync(resolved);
3160
4184
  const data = await pdfParse(buffer);
3161
4185
  const pages = input3["pages"];
3162
4186
  let text = data.text;
@@ -3174,7 +4198,7 @@ var fileReadTool = {
3174
4198
  }
3175
4199
  if (ext === ".ipynb") {
3176
4200
  try {
3177
- const content = fs9.readFileSync(resolved, "utf-8");
4201
+ const content = fs13.readFileSync(resolved, "utf-8");
3178
4202
  const nb = JSON.parse(content);
3179
4203
  const output3 = [];
3180
4204
  for (let i = 0; i < (nb.cells || []).length; i++) {
@@ -3202,7 +4226,7 @@ var fileReadTool = {
3202
4226
  }
3203
4227
  }
3204
4228
  try {
3205
- const raw = fs9.readFileSync(resolved, "utf-8");
4229
+ const raw = fs13.readFileSync(resolved, "utf-8");
3206
4230
  const content = raw.replace(/\r\n/g, "\n");
3207
4231
  const lines = content.split("\n");
3208
4232
  const totalLines = lines.length;
@@ -3230,8 +4254,9 @@ var fileReadTool = {
3230
4254
  // src/tools/file-write.ts
3231
4255
  init_esm_shims();
3232
4256
  init_tool();
3233
- import * as fs10 from "fs";
3234
- import * as path12 from "path";
4257
+ init_file_backup();
4258
+ import * as fs14 from "fs";
4259
+ import * as path15 from "path";
3235
4260
  var fileWriteTool = {
3236
4261
  name: "write_file",
3237
4262
  description: `Create a new file or overwrite an existing file. Creates parent directories if needed. For modifying existing files, prefer edit_file instead.`,
@@ -3248,14 +4273,15 @@ var fileWriteTool = {
3248
4273
  async execute(input3) {
3249
4274
  const filePath = String(input3["file_path"]);
3250
4275
  const content = String(input3["content"]);
3251
- const resolved = path12.resolve(filePath);
4276
+ const resolved = path15.resolve(filePath);
3252
4277
  try {
3253
- const dir = path12.dirname(resolved);
3254
- if (!fs10.existsSync(dir)) {
3255
- fs10.mkdirSync(dir, { recursive: true });
4278
+ const dir = path15.dirname(resolved);
4279
+ if (!fs14.existsSync(dir)) {
4280
+ fs14.mkdirSync(dir, { recursive: true });
3256
4281
  }
3257
- const existed = fs10.existsSync(resolved);
3258
- fs10.writeFileSync(resolved, content, "utf-8");
4282
+ const existed = fs14.existsSync(resolved);
4283
+ backupFile(resolved);
4284
+ fs14.writeFileSync(resolved, content, "utf-8");
3259
4285
  const lines = content.split("\n").length;
3260
4286
  const action = existed ? "Overwrote" : "Created";
3261
4287
  return makeToolResult(`${action} ${resolved} (${lines} lines)`, {
@@ -3271,8 +4297,9 @@ var fileWriteTool = {
3271
4297
  // src/tools/file-edit.ts
3272
4298
  init_esm_shims();
3273
4299
  init_tool();
3274
- import * as fs11 from "fs";
3275
- import * as path13 from "path";
4300
+ init_file_backup();
4301
+ import * as fs15 from "fs";
4302
+ import * as path16 from "path";
3276
4303
  var fileEditTool = {
3277
4304
  name: "edit_file",
3278
4305
  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.`,
@@ -3293,12 +4320,12 @@ var fileEditTool = {
3293
4320
  const oldString = String(input3["old_string"]);
3294
4321
  const newString = String(input3["new_string"]);
3295
4322
  const replaceAll = input3["replace_all"] === true;
3296
- const resolved = path13.resolve(filePath);
3297
- if (!fs11.existsSync(resolved)) {
4323
+ const resolved = path16.resolve(filePath);
4324
+ if (!fs15.existsSync(resolved)) {
3298
4325
  return makeToolError(`File not found: ${resolved}`);
3299
4326
  }
3300
4327
  try {
3301
- const raw = fs11.readFileSync(resolved, "utf-8");
4328
+ const raw = fs15.readFileSync(resolved, "utf-8");
3302
4329
  const hasCrlf = raw.includes("\r\n");
3303
4330
  let content = hasCrlf ? raw.replace(/\r\n/g, "\n") : raw;
3304
4331
  if (oldString === newString) {
@@ -3332,8 +4359,9 @@ Did you mean this line?
3332
4359
  const idx = content.indexOf(oldString);
3333
4360
  content = content.slice(0, idx) + newString + content.slice(idx + oldString.length);
3334
4361
  }
4362
+ backupFile(resolved);
3335
4363
  const output3 = hasCrlf ? content.replace(/\n/g, "\r\n") : content;
3336
- fs11.writeFileSync(resolved, output3, "utf-8");
4364
+ fs15.writeFileSync(resolved, output3, "utf-8");
3337
4365
  const linesChanged = Math.max(
3338
4366
  oldString.split("\n").length,
3339
4367
  newString.split("\n").length
@@ -3351,8 +4379,9 @@ Did you mean this line?
3351
4379
  // src/tools/file-multi-edit.ts
3352
4380
  init_esm_shims();
3353
4381
  init_tool();
3354
- import * as fs12 from "fs";
3355
- import * as path14 from "path";
4382
+ init_file_backup();
4383
+ import * as fs16 from "fs";
4384
+ import * as path17 from "path";
3356
4385
  var fileMultiEditTool = {
3357
4386
  name: "multi_edit",
3358
4387
  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.`,
@@ -3380,15 +4409,15 @@ var fileMultiEditTool = {
3380
4409
  async execute(input3) {
3381
4410
  const filePath = String(input3["file_path"]);
3382
4411
  const edits = input3["edits"];
3383
- const resolved = path14.resolve(filePath);
3384
- if (!fs12.existsSync(resolved)) {
4412
+ const resolved = path17.resolve(filePath);
4413
+ if (!fs16.existsSync(resolved)) {
3385
4414
  return makeToolError(`File not found: ${resolved}`);
3386
4415
  }
3387
4416
  if (!Array.isArray(edits) || edits.length === 0) {
3388
4417
  return makeToolError("edits must be a non-empty array of {old_string, new_string} objects");
3389
4418
  }
3390
4419
  try {
3391
- const raw = fs12.readFileSync(resolved, "utf-8");
4420
+ const raw = fs16.readFileSync(resolved, "utf-8");
3392
4421
  const hasCrlf = raw.includes("\r\n");
3393
4422
  let content = hasCrlf ? raw.replace(/\r\n/g, "\n") : raw;
3394
4423
  for (let i = 0; i < edits.length; i++) {
@@ -3412,8 +4441,9 @@ Searching for: ${edit2.old_string.slice(0, 100)}...`
3412
4441
  edit2.new_string.split("\n").length
3413
4442
  );
3414
4443
  }
4444
+ backupFile(resolved);
3415
4445
  const output3 = hasCrlf ? content.replace(/\n/g, "\r\n") : content;
3416
- fs12.writeFileSync(resolved, output3, "utf-8");
4446
+ fs16.writeFileSync(resolved, output3, "utf-8");
3417
4447
  return makeToolResult(`Applied ${edits.length} edits to ${resolved}`, {
3418
4448
  filePath: resolved,
3419
4449
  linesChanged: totalLinesChanged
@@ -3427,8 +4457,8 @@ Searching for: ${edit2.old_string.slice(0, 100)}...`
3427
4457
  // src/tools/glob.ts
3428
4458
  init_esm_shims();
3429
4459
  init_tool();
3430
- import * as fs13 from "fs";
3431
- import * as path15 from "path";
4460
+ import * as fs17 from "fs";
4461
+ import * as path18 from "path";
3432
4462
  import { globby } from "globby";
3433
4463
  var globTool = {
3434
4464
  name: "glob",
@@ -3446,7 +4476,7 @@ var globTool = {
3446
4476
  async execute(input3) {
3447
4477
  const pattern = String(input3["pattern"]);
3448
4478
  const searchPath = input3["path"] ? String(input3["path"]) : process.cwd();
3449
- const resolved = path15.resolve(searchPath);
4479
+ const resolved = path18.resolve(searchPath);
3450
4480
  try {
3451
4481
  const files = await globby(pattern, {
3452
4482
  cwd: resolved,
@@ -3457,7 +4487,7 @@ var globTool = {
3457
4487
  });
3458
4488
  const withStats = files.map((f) => {
3459
4489
  try {
3460
- const stat = fs13.statSync(f);
4490
+ const stat = fs17.statSync(f);
3461
4491
  return { path: f, mtime: stat.mtimeMs };
3462
4492
  } catch {
3463
4493
  return { path: f, mtime: 0 };
@@ -3482,8 +4512,8 @@ ${result.join("\n")}`
3482
4512
  init_esm_shims();
3483
4513
  init_tool();
3484
4514
  import { execFile } from "child_process";
3485
- import * as fs14 from "fs";
3486
- import * as path16 from "path";
4515
+ import * as fs18 from "fs";
4516
+ import * as path19 from "path";
3487
4517
  var grepTool = {
3488
4518
  name: "grep",
3489
4519
  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.`,
@@ -3513,7 +4543,7 @@ var grepTool = {
3513
4543
  readOnly: true,
3514
4544
  async execute(input3) {
3515
4545
  const pattern = String(input3["pattern"]);
3516
- const searchPath = path16.resolve(input3["path"] ? String(input3["path"]) : process.cwd());
4546
+ const searchPath = path19.resolve(input3["path"] ? String(input3["path"]) : process.cwd());
3517
4547
  const outputMode = input3["output_mode"] || "files_with_matches";
3518
4548
  const headLimit = input3["head_limit"] || 0;
3519
4549
  const hasRg = await hasCommand("rg");
@@ -3578,19 +4608,19 @@ var grepTool = {
3578
4608
  };
3579
4609
  function hasCommand(cmd) {
3580
4610
  const checkCmd = process.platform === "win32" ? "where" : "which";
3581
- return new Promise((resolve10) => {
3582
- execFile(checkCmd, [cmd], (err) => resolve10(!err));
4611
+ return new Promise((resolve11) => {
4612
+ execFile(checkCmd, [cmd], (err) => resolve11(!err));
3583
4613
  });
3584
4614
  }
3585
4615
  function runCommand(cmd, args) {
3586
- return new Promise((resolve10, reject) => {
4616
+ return new Promise((resolve11, reject) => {
3587
4617
  execFile(cmd, args, { maxBuffer: 10 * 1024 * 1024, timeout: 3e4 }, (err, stdout, stderr) => {
3588
4618
  if (err) {
3589
4619
  err.code = err.code;
3590
4620
  reject(err);
3591
4621
  return;
3592
4622
  }
3593
- resolve10(stdout);
4623
+ resolve11(stdout);
3594
4624
  });
3595
4625
  });
3596
4626
  }
@@ -3633,7 +4663,7 @@ async function builtinSearch(pattern, searchPath, input3, outputMode, headLimit)
3633
4663
  if (headLimit > 0 && entryCount >= headLimit) break;
3634
4664
  let content;
3635
4665
  try {
3636
- content = fs14.readFileSync(filePath, "utf-8");
4666
+ content = fs18.readFileSync(filePath, "utf-8");
3637
4667
  } catch {
3638
4668
  continue;
3639
4669
  }
@@ -3697,18 +4727,18 @@ function collectFiles(dirPath, typeFilter, globFilter) {
3697
4727
  function walk(dir) {
3698
4728
  let entries;
3699
4729
  try {
3700
- entries = fs14.readdirSync(dir, { withFileTypes: true });
4730
+ entries = fs18.readdirSync(dir, { withFileTypes: true });
3701
4731
  } catch {
3702
4732
  return;
3703
4733
  }
3704
4734
  for (const entry of entries) {
3705
4735
  if (IGNORE_DIRS.has(entry.name)) continue;
3706
4736
  if (entry.name.startsWith(".") && entry.name !== ".") continue;
3707
- const fullPath = path16.join(dir, entry.name);
4737
+ const fullPath = path19.join(dir, entry.name);
3708
4738
  if (entry.isDirectory()) {
3709
4739
  walk(fullPath);
3710
4740
  } else if (entry.isFile()) {
3711
- const ext = path16.extname(entry.name).toLowerCase();
4741
+ const ext = path19.extname(entry.name).toLowerCase();
3712
4742
  if (BINARY_EXTENSIONS.has(ext)) continue;
3713
4743
  if (allowedExtensions && !allowedExtensions.has(ext)) continue;
3714
4744
  if (globRegex && !globRegex.test(entry.name)) continue;
@@ -3717,7 +4747,7 @@ function collectFiles(dirPath, typeFilter, globFilter) {
3717
4747
  }
3718
4748
  }
3719
4749
  try {
3720
- const stat = fs14.statSync(dirPath);
4750
+ const stat = fs18.statSync(dirPath);
3721
4751
  if (stat.isFile()) {
3722
4752
  return [dirPath];
3723
4753
  }
@@ -3728,116 +4758,11 @@ function collectFiles(dirPath, typeFilter, globFilter) {
3728
4758
  return files;
3729
4759
  }
3730
4760
 
3731
- // src/tools/bash.ts
3732
- init_esm_shims();
3733
- init_tool();
3734
- import { exec as exec2, spawn } from "child_process";
3735
- import * as os10 from "os";
3736
- function getDefaultShell2() {
3737
- if (os10.platform() === "win32") {
3738
- return "powershell.exe";
3739
- }
3740
- return process.env["SHELL"] || "/bin/bash";
3741
- }
3742
- var backgroundTasks = /* @__PURE__ */ new Map();
3743
- var taskCounter = 0;
3744
- var bashTool = {
3745
- name: "bash",
3746
- 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).`,
3747
- inputSchema: {
3748
- type: "object",
3749
- properties: {
3750
- command: { type: "string", description: "The bash command to execute" },
3751
- description: { type: "string", description: "Brief description of what the command does" },
3752
- timeout: { type: "number", description: "Timeout in milliseconds (max 600000, default 120000)" },
3753
- run_in_background: { type: "boolean", description: "Run in background and return a task ID" }
3754
- },
3755
- required: ["command"]
3756
- },
3757
- dangerous: true,
3758
- readOnly: false,
3759
- async execute(input3) {
3760
- const command = String(input3["command"]);
3761
- const timeout = Math.min(Number(input3["timeout"]) || 12e4, 6e5);
3762
- const runInBackground = input3["run_in_background"] === true;
3763
- if (!command.trim()) {
3764
- return makeToolError("Command cannot be empty");
3765
- }
3766
- if (runInBackground) {
3767
- return runBackgroundTask(command);
3768
- }
3769
- return new Promise((resolve10) => {
3770
- const finalCommand = os10.platform() === "win32" ? `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ${command}` : command;
3771
- exec2(finalCommand, {
3772
- timeout,
3773
- maxBuffer: 10 * 1024 * 1024,
3774
- shell: getDefaultShell2(),
3775
- cwd: process.cwd(),
3776
- env: { ...process.env }
3777
- }, (err, stdout, stderr) => {
3778
- if (err) {
3779
- const exitCode = err.code;
3780
- const output4 = [
3781
- stdout ? `stdout:
3782
- ${stdout}` : "",
3783
- stderr ? `stderr:
3784
- ${stderr}` : "",
3785
- `Exit code: ${exitCode}`
3786
- ].filter(Boolean).join("\n\n");
3787
- if (err.killed) {
3788
- resolve10(makeToolError(`Command timed out after ${timeout / 1e3}s
3789
- ${output4}`));
3790
- } else {
3791
- resolve10(makeToolResult(output4 || `Command failed with exit code ${exitCode}`));
3792
- }
3793
- return;
3794
- }
3795
- const output3 = [
3796
- stdout ? stdout : "",
3797
- stderr ? `stderr:
3798
- ${stderr}` : ""
3799
- ].filter(Boolean).join("\n");
3800
- resolve10(makeToolResult(output3 || "(no output)"));
3801
- });
3802
- });
3803
- }
3804
- };
3805
- function runBackgroundTask(command) {
3806
- const taskId = `bg_${++taskCounter}`;
3807
- const bgCommand = os10.platform() === "win32" ? `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ${command}` : command;
3808
- const proc = spawn(bgCommand, {
3809
- shell: getDefaultShell2(),
3810
- cwd: process.cwd(),
3811
- env: { ...process.env },
3812
- stdio: ["ignore", "pipe", "pipe"],
3813
- detached: false
3814
- });
3815
- const task = { process: proc, output: "", status: "running", exitCode: void 0 };
3816
- backgroundTasks.set(taskId, task);
3817
- proc.stdout?.on("data", (data) => {
3818
- task.output += data.toString();
3819
- });
3820
- proc.stderr?.on("data", (data) => {
3821
- task.output += data.toString();
3822
- });
3823
- proc.on("close", (code) => {
3824
- task.status = code === 0 ? "done" : "error";
3825
- task.exitCode = code ?? 1;
3826
- });
3827
- proc.on("error", (err) => {
3828
- task.status = "error";
3829
- task.output += `
3830
- Process error: ${err.message}`;
3831
- });
3832
- return makeToolResult(`Background task started with ID: ${taskId}
3833
- Use task_output tool to check results.`);
3834
- }
3835
-
3836
4761
  // src/tools/list-dir.ts
3837
4762
  init_esm_shims();
3838
4763
  init_tool();
3839
- import * as fs15 from "fs";
3840
- import * as path17 from "path";
4764
+ import * as fs19 from "fs";
4765
+ import * as path20 from "path";
3841
4766
  var listDirTool = {
3842
4767
  name: "list_dir",
3843
4768
  description: `List directory contents with file/folder distinction and basic metadata.`,
@@ -3851,16 +4776,16 @@ var listDirTool = {
3851
4776
  dangerous: false,
3852
4777
  readOnly: true,
3853
4778
  async execute(input3) {
3854
- const dirPath = path17.resolve(input3["path"] ? String(input3["path"]) : process.cwd());
3855
- if (!fs15.existsSync(dirPath)) {
4779
+ const dirPath = path20.resolve(input3["path"] ? String(input3["path"]) : process.cwd());
4780
+ if (!fs19.existsSync(dirPath)) {
3856
4781
  return makeToolError(`Directory not found: ${dirPath}`);
3857
4782
  }
3858
- const stat = fs15.statSync(dirPath);
4783
+ const stat = fs19.statSync(dirPath);
3859
4784
  if (!stat.isDirectory()) {
3860
4785
  return makeToolError(`Not a directory: ${dirPath}`);
3861
4786
  }
3862
4787
  try {
3863
- const entries = fs15.readdirSync(dirPath, { withFileTypes: true });
4788
+ const entries = fs19.readdirSync(dirPath, { withFileTypes: true });
3864
4789
  const IGNORE = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store", "__pycache__", ".next", "dist", "build"]);
3865
4790
  const lines = [];
3866
4791
  const dirs = [];
@@ -3871,7 +4796,7 @@ var listDirTool = {
3871
4796
  dirs.push(`${entry.name}/`);
3872
4797
  } else if (entry.isSymbolicLink()) {
3873
4798
  try {
3874
- const target = fs15.readlinkSync(path17.join(dirPath, entry.name));
4799
+ const target = fs19.readlinkSync(path20.join(dirPath, entry.name));
3875
4800
  files.push(`${entry.name} -> ${target}`);
3876
4801
  } catch {
3877
4802
  files.push(`${entry.name} -> (broken link)`);
@@ -3898,9 +4823,95 @@ ${lines.join("\n")}`);
3898
4823
  init_esm_shims();
3899
4824
  init_tool();
3900
4825
  import { execSync as execSync4 } from "child_process";
4826
+ function detectConflictFiles(cwd) {
4827
+ try {
4828
+ const output3 = execSync4("git diff --name-only --diff-filter=U", {
4829
+ encoding: "utf-8",
4830
+ cwd,
4831
+ timeout: 1e4
4832
+ });
4833
+ return output3.trim().split("\n").filter(Boolean);
4834
+ } catch {
4835
+ return [];
4836
+ }
4837
+ }
4838
+ function parseConflictMarkers(fileContent, filePath) {
4839
+ const sections = [];
4840
+ const lines = fileContent.split("\n");
4841
+ let inConflict = false;
4842
+ let startLine = 0;
4843
+ let oursLines = [];
4844
+ let theirsLines = [];
4845
+ let inTheirs = false;
4846
+ for (let i = 0; i < lines.length; i++) {
4847
+ const line = lines[i];
4848
+ if (line.startsWith("<<<<<<<")) {
4849
+ inConflict = true;
4850
+ inTheirs = false;
4851
+ startLine = i + 1;
4852
+ oursLines = [];
4853
+ theirsLines = [];
4854
+ } else if (line.startsWith("=======") && inConflict) {
4855
+ inTheirs = true;
4856
+ } else if (line.startsWith(">>>>>>>") && inConflict) {
4857
+ sections.push({
4858
+ file: filePath,
4859
+ startLine,
4860
+ ours: oursLines.join("\n"),
4861
+ theirs: theirsLines.join("\n")
4862
+ });
4863
+ inConflict = false;
4864
+ inTheirs = false;
4865
+ } else if (inConflict) {
4866
+ if (inTheirs) {
4867
+ theirsLines.push(line);
4868
+ } else {
4869
+ oursLines.push(line);
4870
+ }
4871
+ }
4872
+ }
4873
+ return sections;
4874
+ }
4875
+ function formatConflictReport(cwd) {
4876
+ const conflictFiles = detectConflictFiles(cwd);
4877
+ if (conflictFiles.length === 0) return null;
4878
+ const fs22 = __require("fs");
4879
+ const path23 = __require("path");
4880
+ const lines = [
4881
+ `\u26A0 Merge conflicts detected in ${conflictFiles.length} file(s):`,
4882
+ ""
4883
+ ];
4884
+ for (const file of conflictFiles) {
4885
+ const fullPath = path23.join(cwd, file);
4886
+ let content;
4887
+ try {
4888
+ content = fs22.readFileSync(fullPath, "utf-8");
4889
+ } catch {
4890
+ lines.push(` - ${file} (cannot read)`);
4891
+ continue;
4892
+ }
4893
+ const sections = parseConflictMarkers(content, file);
4894
+ lines.push(` - ${file} (${sections.length} conflict(s))`);
4895
+ for (let i = 0; i < sections.length; i++) {
4896
+ const s = sections[i];
4897
+ lines.push(` [Conflict ${i + 1} at line ${s.startLine}]`);
4898
+ lines.push(` OURS:`);
4899
+ for (const l of s.ours.split("\n").slice(0, 5)) {
4900
+ lines.push(` ${l}`);
4901
+ }
4902
+ if (s.ours.split("\n").length > 5) lines.push(` ... (${s.ours.split("\n").length} lines)`);
4903
+ lines.push(` THEIRS:`);
4904
+ for (const l of s.theirs.split("\n").slice(0, 5)) {
4905
+ lines.push(` ${l}`);
4906
+ }
4907
+ if (s.theirs.split("\n").length > 5) lines.push(` ... (${s.theirs.split("\n").length} lines)`);
4908
+ }
4909
+ }
4910
+ return lines.join("\n");
4911
+ }
3901
4912
  var gitTool = {
3902
4913
  name: "git",
3903
- 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.`,
4914
+ 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.`,
3904
4915
  inputSchema: {
3905
4916
  type: "object",
3906
4917
  properties: {
@@ -3929,7 +4940,9 @@ var gitTool = {
3929
4940
  }
3930
4941
  }
3931
4942
  const readOnlyPrefixes = ["status", "diff", "log", "show", "branch", "tag", "remote", "stash list", "ls-files"];
3932
- const isReadOnly = readOnlyPrefixes.some((p) => command.startsWith(p));
4943
+ const _isReadOnly = readOnlyPrefixes.some((p) => command.startsWith(p));
4944
+ const mergeCommands = /^(merge|pull|rebase|cherry-pick)\b/;
4945
+ const isMergeCommand = mergeCommands.test(command);
3933
4946
  try {
3934
4947
  const result = execSync4(`git ${command}`, {
3935
4948
  encoding: "utf-8",
@@ -3937,9 +4950,26 @@ var gitTool = {
3937
4950
  timeout: 3e4,
3938
4951
  cwd: process.cwd()
3939
4952
  });
4953
+ if (isMergeCommand) {
4954
+ const conflictReport = formatConflictReport(process.cwd());
4955
+ if (conflictReport) {
4956
+ return makeToolResult(`${result}
4957
+
4958
+ ${conflictReport}`);
4959
+ }
4960
+ }
3940
4961
  return makeToolResult(result || "(no output)");
3941
4962
  } catch (err) {
3942
4963
  const output3 = [err.stdout, err.stderr].filter(Boolean).join("\n");
4964
+ if (isMergeCommand) {
4965
+ const conflictReport = formatConflictReport(process.cwd());
4966
+ if (conflictReport) {
4967
+ return makeToolError(`git ${command} failed with conflicts:
4968
+ ${output3}
4969
+
4970
+ ${conflictReport}`);
4971
+ }
4972
+ }
3943
4973
  return makeToolError(`git ${command} failed:
3944
4974
  ${output3 || err.message}`);
3945
4975
  }
@@ -3950,15 +4980,92 @@ ${output3 || err.message}`);
3950
4980
  init_esm_shims();
3951
4981
  init_tool();
3952
4982
  var cache = /* @__PURE__ */ new Map();
3953
- var CACHE_TTL = 15 * 60 * 1e3;
4983
+ var DEFAULT_CACHE_TTL = 15 * 60 * 1e3;
4984
+ var MAX_RESPONSE_SIZE = 5 * 1024 * 1024;
4985
+ var MAX_TEXT_LEN = 5e4;
4986
+ var USER_AGENT = "Mozilla/5.0 (compatible; Codi/0.1; +https://github.com/gemdoq/codi)";
4987
+ function extractCharset(contentType) {
4988
+ const match = contentType.match(/charset=([^\s;]+)/i);
4989
+ return match && match[1] ? match[1].replace(/['"]/g, "") : null;
4990
+ }
4991
+ function parseCacheMaxAge(headers) {
4992
+ const cc = headers.get("cache-control");
4993
+ if (!cc) return null;
4994
+ const match = cc.match(/max-age=(\d+)/);
4995
+ if (!match || !match[1]) return null;
4996
+ const seconds = parseInt(match[1], 10);
4997
+ if (isNaN(seconds) || seconds <= 0) return null;
4998
+ const clamped = Math.max(60, Math.min(seconds, 3600));
4999
+ return clamped * 1e3;
5000
+ }
5001
+ function isPdf(url, contentType) {
5002
+ return contentType.includes("application/pdf") || /\.pdf(\?|#|$)/i.test(url);
5003
+ }
5004
+ function isJson(contentType) {
5005
+ return contentType.includes("application/json") || contentType.includes("+json");
5006
+ }
5007
+ async function extractHtmlText(html) {
5008
+ const { load } = await import("cheerio");
5009
+ const $ = load(html);
5010
+ $('script, style, nav, header, footer, iframe, noscript, svg, [role="navigation"], [role="banner"], .sidebar, .ad, .ads, .advertisement').remove();
5011
+ const main2 = $('main, article, .content, #content, .main, [role="main"]').first();
5012
+ let text = main2.length ? main2.text() : $("body").text();
5013
+ 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();
5014
+ return text;
5015
+ }
5016
+ async function extractPdfText(buffer) {
5017
+ const { PDFParse } = await import("pdf-parse");
5018
+ const parser = new PDFParse({ data: new Uint8Array(buffer) });
5019
+ try {
5020
+ const textResult = await parser.getText();
5021
+ const text = textResult.text?.trim() || "";
5022
+ const totalPages = textResult.total ?? "unknown";
5023
+ let infoStr = `Pages: ${totalPages}`;
5024
+ try {
5025
+ const info = await parser.getInfo();
5026
+ if (info.info?.Title) infoStr += ` | Title: ${info.info.Title}`;
5027
+ if (info.info?.Author) infoStr += ` | Author: ${info.info.Author}`;
5028
+ } catch {
5029
+ }
5030
+ return `[PDF] ${infoStr}
5031
+
5032
+ ${text}`;
5033
+ } finally {
5034
+ await parser.destroy().catch(() => {
5035
+ });
5036
+ }
5037
+ }
5038
+ function formatJson(raw) {
5039
+ try {
5040
+ const parsed = JSON.parse(raw);
5041
+ return `[JSON Response]
5042
+ ${JSON.stringify(parsed, null, 2)}`;
5043
+ } catch {
5044
+ return `[JSON Response - parse error]
5045
+ ${raw}`;
5046
+ }
5047
+ }
5048
+ async function decodeResponse(response, contentType) {
5049
+ const charset = extractCharset(contentType);
5050
+ if (charset && charset.toLowerCase() !== "utf-8" && charset.toLowerCase() !== "utf8") {
5051
+ const buffer = await response.arrayBuffer();
5052
+ const decoder = new TextDecoder(charset);
5053
+ return decoder.decode(buffer);
5054
+ }
5055
+ return response.text();
5056
+ }
3954
5057
  var webFetchTool = {
3955
5058
  name: "web_fetch",
3956
- 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.`,
5059
+ 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.`,
3957
5060
  inputSchema: {
3958
5061
  type: "object",
3959
5062
  properties: {
3960
5063
  url: { type: "string", description: "URL to fetch" },
3961
- prompt: { type: "string", description: "What information to extract from the page" }
5064
+ prompt: { type: "string", description: "What information to extract from the page" },
5065
+ cache_ttl: {
5066
+ type: "number",
5067
+ description: "Cache TTL in seconds (default: 900, i.e. 15 minutes). Set to 0 to bypass cache."
5068
+ }
3962
5069
  },
3963
5070
  required: ["url", "prompt"]
3964
5071
  },
@@ -3967,50 +5074,103 @@ var webFetchTool = {
3967
5074
  async execute(input3) {
3968
5075
  let url = String(input3["url"]);
3969
5076
  const prompt = String(input3["prompt"] || "");
5077
+ const cacheTtlInput = input3["cache_ttl"];
5078
+ const requestTtl = typeof cacheTtlInput === "number" ? cacheTtlInput * 1e3 : null;
5079
+ const bypassCache = requestTtl === 0;
3970
5080
  if (url.startsWith("http://")) {
3971
5081
  url = url.replace("http://", "https://");
3972
5082
  }
3973
- const cached = cache.get(url);
3974
- if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
3975
- return makeToolResult(`[Cached] ${prompt ? `Query: ${prompt}
5083
+ if (!bypassCache) {
5084
+ const cached = cache.get(url);
5085
+ if (cached && Date.now() - cached.timestamp < cached.ttl) {
5086
+ return makeToolResult(
5087
+ `[Cached] ${prompt ? `Query: ${prompt}
3976
5088
 
3977
- ` : ""}${cached.content}`);
5089
+ ` : ""}${cached.content}`
5090
+ );
5091
+ }
3978
5092
  }
3979
5093
  try {
3980
5094
  const response = await fetch(url, {
3981
5095
  headers: {
3982
- "User-Agent": "Codi/0.1 (AI Code Agent)",
3983
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
5096
+ "User-Agent": USER_AGENT,
5097
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,application/json;q=0.8,application/pdf;q=0.7,*/*;q=0.5"
3984
5098
  },
3985
5099
  redirect: "follow",
3986
5100
  signal: AbortSignal.timeout(3e4)
3987
5101
  });
3988
5102
  if (!response.ok) {
3989
- return makeToolError(`HTTP ${response.status}: ${response.statusText}`);
5103
+ const status = response.status;
5104
+ const statusText = response.statusText || "Unknown";
5105
+ let detail = `HTTP ${status} ${statusText}`;
5106
+ if (status === 403 || status === 401) {
5107
+ 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.";
5108
+ } else if (status === 404) {
5109
+ detail += " - \uD398\uC774\uC9C0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.";
5110
+ } else if (status === 429) {
5111
+ detail += " - \uC694\uCCAD\uC774 \uB108\uBB34 \uB9CE\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.";
5112
+ } else if (status >= 500) {
5113
+ detail += " - \uC11C\uBC84 \uC624\uB958\uC785\uB2C8\uB2E4.";
5114
+ }
5115
+ if (response.redirected) {
5116
+ detail += `
5117
+ Redirected to: ${response.url}`;
5118
+ }
5119
+ return makeToolError(detail);
5120
+ }
5121
+ const contentLength = response.headers.get("content-length");
5122
+ if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) {
5123
+ return makeToolError(
5124
+ `\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.`
5125
+ );
3990
5126
  }
3991
5127
  const contentType = response.headers.get("content-type") || "";
3992
5128
  let text;
3993
- if (contentType.includes("text/html") || contentType.includes("application/xhtml")) {
3994
- const html = await response.text();
3995
- const { load } = await import("cheerio");
3996
- const $ = load(html);
3997
- $("script, style, nav, header, footer, iframe, noscript").remove();
3998
- const main2 = $("main, article, .content, #content, .main").first();
3999
- text = (main2.length ? main2.text() : $("body").text()).replace(/\s+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
5129
+ if (isPdf(url, contentType)) {
5130
+ const buffer = await response.arrayBuffer();
5131
+ if (buffer.byteLength > MAX_RESPONSE_SIZE) {
5132
+ return makeToolError(
5133
+ `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.`
5134
+ );
5135
+ }
5136
+ text = await extractPdfText(buffer);
5137
+ } else if (isJson(contentType)) {
5138
+ const raw = await decodeResponse(response, contentType);
5139
+ text = formatJson(raw);
5140
+ } else if (contentType.includes("text/html") || contentType.includes("application/xhtml")) {
5141
+ const html = await decodeResponse(response, contentType);
5142
+ text = await extractHtmlText(html);
4000
5143
  } else {
4001
- text = await response.text();
5144
+ text = await decodeResponse(response, contentType);
5145
+ }
5146
+ if (text.length > MAX_TEXT_LEN) {
5147
+ text = text.slice(0, MAX_TEXT_LEN) + "\n\n... (truncated)";
5148
+ }
5149
+ const effectiveTtl = requestTtl ?? parseCacheMaxAge(response.headers) ?? DEFAULT_CACHE_TTL;
5150
+ if (!bypassCache) {
5151
+ cache.set(url, { content: text, timestamp: Date.now(), ttl: effectiveTtl });
4002
5152
  }
4003
- const MAX_LEN = 5e4;
4004
- if (text.length > MAX_LEN) {
4005
- text = text.slice(0, MAX_LEN) + "\n\n... (truncated)";
5153
+ let prefix = `URL: ${url}`;
5154
+ if (response.redirected && response.url !== url) {
5155
+ prefix += `
5156
+ Redirected to: ${response.url}`;
4006
5157
  }
4007
- cache.set(url, { content: text, timestamp: Date.now() });
4008
- return makeToolResult(prompt ? `URL: ${url}
4009
- Query: ${prompt}
5158
+ if (prompt) {
5159
+ prefix += `
5160
+ Query: ${prompt}`;
5161
+ }
5162
+ return makeToolResult(`${prefix}
4010
5163
 
4011
- ${text}` : text);
5164
+ ${text}`);
4012
5165
  } catch (err) {
4013
- return makeToolError(`Failed to fetch URL: ${err instanceof Error ? err.message : String(err)}`);
5166
+ const message = err instanceof Error ? err.message : String(err);
5167
+ if (message.includes("TimeoutError") || message.includes("aborted")) {
5168
+ return makeToolError(`\uC694\uCCAD \uC2DC\uAC04 \uCD08\uACFC (30\uCD08): ${url}`);
5169
+ }
5170
+ if (message.includes("ENOTFOUND") || message.includes("getaddrinfo")) {
5171
+ return makeToolError(`\uB3C4\uBA54\uC778\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${url}`);
5172
+ }
5173
+ return makeToolError(`URL \uAC00\uC838\uC624\uAE30 \uC2E4\uD328: ${message}`);
4014
5174
  }
4015
5175
  }
4016
5176
  };
@@ -4018,73 +5178,181 @@ ${text}` : text);
4018
5178
  // src/tools/web-search.ts
4019
5179
  init_esm_shims();
4020
5180
  init_tool();
5181
+ var searchCache = /* @__PURE__ */ new Map();
5182
+ var SEARCH_CACHE_TTL = 10 * 60 * 1e3;
5183
+ 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";
5184
+ function extractRealUrl(href) {
5185
+ try {
5186
+ const urlObj = new URL(href, "https://duckduckgo.com");
5187
+ return urlObj.searchParams.get("uddg") || href;
5188
+ } catch {
5189
+ return href;
5190
+ }
5191
+ }
5192
+ function normalizeUrl(url) {
5193
+ try {
5194
+ const u = new URL(url);
5195
+ const trackingParams = ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term", "ref", "fbclid", "gclid"];
5196
+ for (const param of trackingParams) {
5197
+ u.searchParams.delete(param);
5198
+ }
5199
+ let path23 = u.pathname.replace(/\/+$/, "") || "/";
5200
+ return `${u.hostname}${path23}${u.search}`;
5201
+ } catch {
5202
+ return url;
5203
+ }
5204
+ }
5205
+ function deduplicateResults(results) {
5206
+ const seen = /* @__PURE__ */ new Set();
5207
+ const deduped = [];
5208
+ for (const result of results) {
5209
+ const normalized = normalizeUrl(result.url);
5210
+ if (!seen.has(normalized) && result.title.length > 0) {
5211
+ seen.add(normalized);
5212
+ deduped.push(result);
5213
+ }
5214
+ }
5215
+ return deduped;
5216
+ }
5217
+ async function searchDuckDuckGo(query, maxResults) {
5218
+ const encoded = encodeURIComponent(query);
5219
+ const url = `https://html.duckduckgo.com/html/?q=${encoded}`;
5220
+ const response = await fetch(url, {
5221
+ headers: {
5222
+ "User-Agent": USER_AGENT2,
5223
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
5224
+ "Accept-Language": "en-US,en;q=0.9"
5225
+ },
5226
+ signal: AbortSignal.timeout(15e3)
5227
+ });
5228
+ if (!response.ok) {
5229
+ throw new Error(`DuckDuckGo HTTP ${response.status} ${response.statusText}`);
5230
+ }
5231
+ const html = await response.text();
5232
+ if (html.includes("If this error persists") || html.includes("blocked")) {
5233
+ throw new Error("DuckDuckGo\uC5D0\uC11C \uC694\uCCAD\uC774 \uCC28\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4");
5234
+ }
5235
+ const { load } = await import("cheerio");
5236
+ const $ = load(html);
5237
+ const results = [];
5238
+ $(".result").each((_i, el) => {
5239
+ const $el = $(el);
5240
+ if ($el.hasClass("result--ad") || $el.find(".badge--ad").length > 0) {
5241
+ return;
5242
+ }
5243
+ const titleEl = $el.find(".result__title a, .result__a");
5244
+ const title = titleEl.text().trim();
5245
+ const href = titleEl.attr("href") || "";
5246
+ const snippet = $el.find(".result__snippet").text().trim();
5247
+ if (title && href) {
5248
+ const actualUrl = extractRealUrl(href);
5249
+ if (actualUrl.startsWith("http://") || actualUrl.startsWith("https://")) {
5250
+ results.push({ title, url: actualUrl, snippet });
5251
+ }
5252
+ }
5253
+ });
5254
+ return deduplicateResults(results).slice(0, maxResults);
5255
+ }
5256
+ async function searchDuckDuckGoLite(query, maxResults) {
5257
+ const encoded = encodeURIComponent(query);
5258
+ const url = `https://lite.duckduckgo.com/lite/?q=${encoded}`;
5259
+ const response = await fetch(url, {
5260
+ headers: {
5261
+ "User-Agent": USER_AGENT2,
5262
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
5263
+ },
5264
+ signal: AbortSignal.timeout(15e3)
5265
+ });
5266
+ if (!response.ok) {
5267
+ throw new Error(`DuckDuckGo Lite HTTP ${response.status}`);
5268
+ }
5269
+ const html = await response.text();
5270
+ const { load } = await import("cheerio");
5271
+ const $ = load(html);
5272
+ const results = [];
5273
+ $("a.result-link").each((_i, el) => {
5274
+ const $a = $(el);
5275
+ const title = $a.text().trim();
5276
+ const href = $a.attr("href") || "";
5277
+ if (title && href) {
5278
+ const actualUrl = extractRealUrl(href);
5279
+ const $row = $a.closest("tr");
5280
+ const snippet = $row.next("tr").find(".result-snippet").text().trim() || $row.next("tr").find("td").last().text().trim();
5281
+ if (actualUrl.startsWith("http://") || actualUrl.startsWith("https://")) {
5282
+ results.push({ title, url: actualUrl, snippet });
5283
+ }
5284
+ }
5285
+ });
5286
+ return deduplicateResults(results).slice(0, maxResults);
5287
+ }
4021
5288
  var webSearchTool = {
4022
5289
  name: "web_search",
4023
- description: `Search the web for information. Returns search results with titles, URLs, and snippets. Uses DuckDuckGo as the search engine.`,
5290
+ 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.`,
4024
5291
  inputSchema: {
4025
5292
  type: "object",
4026
5293
  properties: {
4027
- query: { type: "string", description: "Search query" }
5294
+ query: { type: "string", description: "Search query" },
5295
+ max_results: {
5296
+ type: "number",
5297
+ description: "Maximum number of results (default: 10, max: 20)"
5298
+ }
4028
5299
  },
4029
5300
  required: ["query"]
4030
5301
  },
4031
5302
  dangerous: true,
4032
5303
  readOnly: true,
4033
5304
  async execute(input3) {
4034
- const query = String(input3["query"]);
5305
+ const query = String(input3["query"]).trim();
5306
+ if (!query) {
5307
+ return makeToolError("\uAC80\uC0C9\uC5B4\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4.");
5308
+ }
5309
+ const rawMax = typeof input3["max_results"] === "number" ? input3["max_results"] : 10;
5310
+ const maxResults = Math.max(1, Math.min(20, Math.round(rawMax)));
5311
+ const cacheKey = `${query}|${maxResults}`;
5312
+ const cached = searchCache.get(cacheKey);
5313
+ if (cached && Date.now() - cached.timestamp < SEARCH_CACHE_TTL) {
5314
+ return makeToolResult(`[Cached] ${cached.results}`);
5315
+ }
5316
+ let results = [];
5317
+ let fallbackUsed = false;
4035
5318
  try {
4036
- const encoded = encodeURIComponent(query);
4037
- const url = `https://html.duckduckgo.com/html/?q=${encoded}`;
4038
- const response = await fetch(url, {
4039
- headers: {
4040
- "User-Agent": "Mozilla/5.0 (compatible; Codi/0.1)"
4041
- },
4042
- signal: AbortSignal.timeout(15e3)
4043
- });
4044
- if (!response.ok) {
4045
- return makeToolError(`Search failed: HTTP ${response.status}`);
4046
- }
4047
- const html = await response.text();
4048
- const { load } = await import("cheerio");
4049
- const $ = load(html);
4050
- const results = [];
4051
- $(".result").each((i, el) => {
4052
- if (i >= 10) return false;
4053
- const $el = $(el);
4054
- const title = $el.find(".result__title a").text().trim();
4055
- const href = $el.find(".result__title a").attr("href") || "";
4056
- const snippet = $el.find(".result__snippet").text().trim();
4057
- if (title && href) {
4058
- let actualUrl = href;
4059
- try {
4060
- const urlObj = new URL(href, "https://duckduckgo.com");
4061
- actualUrl = urlObj.searchParams.get("uddg") || href;
4062
- } catch {
4063
- actualUrl = href;
4064
- }
4065
- results.push({ title, url: actualUrl, snippet });
4066
- }
4067
- });
4068
- if (results.length === 0) {
4069
- return makeToolResult(`No results found for: ${query}`);
5319
+ results = await searchDuckDuckGo(query, maxResults);
5320
+ } catch (primaryErr) {
5321
+ try {
5322
+ results = await searchDuckDuckGoLite(query, maxResults);
5323
+ fallbackUsed = true;
5324
+ } catch (fallbackErr) {
5325
+ const primaryMsg = primaryErr instanceof Error ? primaryErr.message : String(primaryErr);
5326
+ const fallbackMsg = fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr);
5327
+ return makeToolError(
5328
+ `\uAC80\uC0C9 \uC2E4\uD328:
5329
+ Primary: ${primaryMsg}
5330
+ Fallback: ${fallbackMsg}`
5331
+ );
4070
5332
  }
4071
- const formatted = results.map((r, i) => `${i + 1}. ${r.title}
4072
- ${r.url}
4073
- ${r.snippet}`).join("\n\n");
4074
- return makeToolResult(`Search results for: ${query}
4075
-
4076
- ${formatted}`);
4077
- } catch (err) {
4078
- return makeToolError(`Search failed: ${err instanceof Error ? err.message : String(err)}`);
4079
5333
  }
5334
+ if (results.length === 0) {
5335
+ return makeToolResult(`"${query}"\uC5D0 \uB300\uD55C \uAC80\uC0C9 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`);
5336
+ }
5337
+ const formatted = results.map(
5338
+ (r, i) => `${i + 1}. ${r.title}
5339
+ ${r.url}${r.snippet ? `
5340
+ ${r.snippet}` : ""}`
5341
+ ).join("\n\n");
5342
+ const header = fallbackUsed ? `Search results for: ${query} (fallback engine used)` : `Search results for: ${query}`;
5343
+ const output3 = `${header}
5344
+
5345
+ ${formatted}`;
5346
+ searchCache.set(cacheKey, { results: output3, timestamp: Date.now() });
5347
+ return makeToolResult(output3);
4080
5348
  }
4081
5349
  };
4082
5350
 
4083
5351
  // src/tools/notebook-edit.ts
4084
5352
  init_esm_shims();
4085
5353
  init_tool();
4086
- import * as fs16 from "fs";
4087
- import * as path18 from "path";
5354
+ import * as fs20 from "fs";
5355
+ import * as path21 from "path";
4088
5356
  var notebookEditTool = {
4089
5357
  name: "notebook_edit",
4090
5358
  description: `Edit Jupyter notebook (.ipynb) cells. Supports replacing, inserting, and deleting cells.`,
@@ -4102,16 +5370,16 @@ var notebookEditTool = {
4102
5370
  dangerous: true,
4103
5371
  readOnly: false,
4104
5372
  async execute(input3) {
4105
- const nbPath = path18.resolve(String(input3["notebook_path"]));
5373
+ const nbPath = path21.resolve(String(input3["notebook_path"]));
4106
5374
  const cellNumber = input3["cell_number"];
4107
5375
  const newSource = String(input3["new_source"]);
4108
5376
  const cellType = input3["cell_type"] || "code";
4109
5377
  const editMode = input3["edit_mode"] || "replace";
4110
- if (!fs16.existsSync(nbPath)) {
5378
+ if (!fs20.existsSync(nbPath)) {
4111
5379
  return makeToolError(`Notebook not found: ${nbPath}`);
4112
5380
  }
4113
5381
  try {
4114
- const content = fs16.readFileSync(nbPath, "utf-8");
5382
+ const content = fs20.readFileSync(nbPath, "utf-8");
4115
5383
  const nb = JSON.parse(content);
4116
5384
  if (!nb.cells || !Array.isArray(nb.cells)) {
4117
5385
  return makeToolError("Invalid notebook format: no cells array");
@@ -4156,7 +5424,7 @@ var notebookEditTool = {
4156
5424
  default:
4157
5425
  return makeToolError(`Unknown edit_mode: ${editMode}`);
4158
5426
  }
4159
- fs16.writeFileSync(nbPath, JSON.stringify(nb, null, 1), "utf-8");
5427
+ fs20.writeFileSync(nbPath, JSON.stringify(nb, null, 1), "utf-8");
4160
5428
  return makeToolResult(`Notebook ${editMode}d cell in ${nbPath} (${nb.cells.length} cells total)`);
4161
5429
  } catch (err) {
4162
5430
  return makeToolError(`Notebook edit failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -4170,8 +5438,7 @@ init_task_tools();
4170
5438
  // src/tools/ask-user.ts
4171
5439
  init_esm_shims();
4172
5440
  init_tool();
4173
- import * as readline4 from "readline/promises";
4174
- import chalk12 from "chalk";
5441
+ import chalk13 from "chalk";
4175
5442
  var askUserTool = {
4176
5443
  name: "ask_user",
4177
5444
  description: `Ask the user a question with optional choices. Use to gather preferences, clarify requirements, or get decisions.`,
@@ -4202,23 +5469,18 @@ var askUserTool = {
4202
5469
  const options = input3["options"];
4203
5470
  const multiSelect = input3["multiSelect"] === true;
4204
5471
  console.log("");
4205
- console.log(chalk12.cyan.bold("? ") + chalk12.bold(question));
5472
+ console.log(chalk13.cyan.bold("? ") + chalk13.bold(question));
4206
5473
  if (options && options.length > 0) {
4207
5474
  console.log("");
4208
5475
  for (let i = 0; i < options.length; i++) {
4209
5476
  const opt = options[i];
4210
- console.log(chalk12.cyan(` ${i + 1}.`) + ` ${opt.label}${opt.description ? chalk12.dim(` - ${opt.description}`) : ""}`);
5477
+ console.log(chalk13.cyan(` ${i + 1}.`) + ` ${opt.label}${opt.description ? chalk13.dim(` - ${opt.description}`) : ""}`);
4211
5478
  }
4212
- console.log(chalk12.dim(` ${options.length + 1}. Other (type custom response)`));
5479
+ console.log(chalk13.dim(` ${options.length + 1}. Other (type custom response)`));
4213
5480
  console.log("");
4214
- const rl2 = readline4.createInterface({
4215
- input: process.stdin,
4216
- output: process.stdout
4217
- });
4218
5481
  try {
4219
- const prompt = multiSelect ? chalk12.dim("Enter numbers separated by commas: ") : chalk12.dim("Enter number or type response: ");
4220
- const answer = await rl2.question(prompt);
4221
- rl2.close();
5482
+ const prompt = multiSelect ? chalk13.dim("Enter numbers separated by commas: ") : chalk13.dim("Enter number or type response: ");
5483
+ const answer = await sharedPrompt(prompt);
4222
5484
  if (multiSelect) {
4223
5485
  const indices = answer.split(",").map((s) => parseInt(s.trim()) - 1);
4224
5486
  const selected = indices.filter((i) => i >= 0 && i < options.length).map((i) => options[i].label);
@@ -4233,25 +5495,145 @@ var askUserTool = {
4233
5495
  }
4234
5496
  return makeToolResult(`User response: ${answer}`);
4235
5497
  } catch {
4236
- rl2.close();
4237
5498
  return makeToolError("Failed to get user input");
4238
5499
  }
4239
5500
  }
4240
- const rl = readline4.createInterface({
4241
- input: process.stdin,
4242
- output: process.stdout
4243
- });
4244
5501
  try {
4245
- const answer = await rl.question(chalk12.dim("> "));
4246
- rl.close();
5502
+ const answer = await sharedPrompt(chalk13.dim("> "));
4247
5503
  return makeToolResult(`User response: ${answer}`);
4248
5504
  } catch {
4249
- rl.close();
4250
5505
  return makeToolError("Failed to get user input");
4251
5506
  }
4252
5507
  }
4253
5508
  };
4254
5509
 
5510
+ // src/tools/memory-tool.ts
5511
+ init_esm_shims();
5512
+ init_tool();
5513
+ import * as fs21 from "fs";
5514
+ import * as path22 from "path";
5515
+ function buildFrontmatter(topic, description) {
5516
+ return `---
5517
+ name: ${topic}
5518
+ description: ${description}
5519
+ type: project
5520
+ ---
5521
+ `;
5522
+ }
5523
+ function parseFrontmatter(content) {
5524
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
5525
+ if (!match) return { body: content };
5526
+ const meta = {};
5527
+ for (const line of match[1].split("\n")) {
5528
+ const idx = line.indexOf(":");
5529
+ if (idx !== -1) {
5530
+ meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
5531
+ }
5532
+ }
5533
+ return { name: meta["name"], description: meta["description"], body: match[2] };
5534
+ }
5535
+ function updateIndex(memoryDir) {
5536
+ const indexPath = path22.join(memoryDir, "MEMORY.md");
5537
+ const files = fs21.readdirSync(memoryDir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort();
5538
+ const lines = ["# Project Memory", ""];
5539
+ if (files.length === 0) {
5540
+ lines.push("No topics saved yet.");
5541
+ } else {
5542
+ lines.push("| Topic | Description |");
5543
+ lines.push("|-------|-------------|");
5544
+ for (const file of files) {
5545
+ const content = fs21.readFileSync(path22.join(memoryDir, file), "utf-8");
5546
+ const parsed = parseFrontmatter(content);
5547
+ const topic = file.replace(".md", "");
5548
+ const desc = parsed.description || "";
5549
+ lines.push(`| [${topic}](${file}) | ${desc} |`);
5550
+ }
5551
+ }
5552
+ lines.push("");
5553
+ fs21.writeFileSync(indexPath, lines.join("\n"), "utf-8");
5554
+ }
5555
+ var updateMemoryTool = {
5556
+ name: "update_memory",
5557
+ description: `Save, delete, or list project memory topics. Use this to persist important information (architecture decisions, user preferences, patterns, etc.) across conversations.
5558
+ - save: Create or update a memory topic file with content
5559
+ - delete: Remove a memory topic file
5560
+ - list: List all existing memory topics with descriptions`,
5561
+ inputSchema: {
5562
+ type: "object",
5563
+ properties: {
5564
+ action: {
5565
+ type: "string",
5566
+ enum: ["save", "delete", "list"],
5567
+ description: "Action to perform"
5568
+ },
5569
+ topic: {
5570
+ type: "string",
5571
+ description: 'Topic name (used as filename, e.g. "architecture" -> architecture.md). Required for save/delete.'
5572
+ },
5573
+ content: {
5574
+ type: "string",
5575
+ description: "Content to save. First line is used as description. Required for save."
5576
+ }
5577
+ },
5578
+ required: ["action", "topic"]
5579
+ },
5580
+ dangerous: false,
5581
+ readOnly: false,
5582
+ async execute(input3) {
5583
+ const action = String(input3["action"]);
5584
+ const topic = String(input3["topic"] || "");
5585
+ const content = input3["content"] != null ? String(input3["content"]) : void 0;
5586
+ const memoryDir = memoryManager.getMemoryDir();
5587
+ switch (action) {
5588
+ case "save": {
5589
+ if (!topic) return makeToolError("topic is required for save action");
5590
+ if (!content) return makeToolError("content is required for save action");
5591
+ memoryManager.ensureDir();
5592
+ const firstLine = content.split("\n")[0].trim();
5593
+ const description = firstLine.length > 100 ? firstLine.slice(0, 100) + "..." : firstLine;
5594
+ const fileContent = buildFrontmatter(topic, description) + content;
5595
+ const topicPath = path22.join(memoryDir, `${topic}.md`);
5596
+ fs21.writeFileSync(topicPath, fileContent, "utf-8");
5597
+ updateIndex(memoryDir);
5598
+ return makeToolResult(`Memory topic "${topic}" saved to ${topicPath}`);
5599
+ }
5600
+ case "delete": {
5601
+ if (!topic) return makeToolError("topic is required for delete action");
5602
+ const topicPath = path22.join(memoryDir, `${topic}.md`);
5603
+ if (!fs21.existsSync(topicPath)) {
5604
+ return makeToolError(`Memory topic "${topic}" not found`);
5605
+ }
5606
+ fs21.unlinkSync(topicPath);
5607
+ memoryManager.ensureDir();
5608
+ updateIndex(memoryDir);
5609
+ return makeToolResult(`Memory topic "${topic}" deleted`);
5610
+ }
5611
+ case "list": {
5612
+ const topics = memoryManager.listTopics();
5613
+ if (topics.length === 0) {
5614
+ return makeToolResult("No memory topics saved yet.");
5615
+ }
5616
+ const lines = [];
5617
+ for (const t of topics) {
5618
+ const raw = memoryManager.loadTopic(t);
5619
+ if (raw) {
5620
+ const parsed = parseFrontmatter(raw);
5621
+ lines.push(`- ${t}: ${parsed.description || "(no description)"}`);
5622
+ } else {
5623
+ lines.push(`- ${t}: (no description)`);
5624
+ }
5625
+ }
5626
+ return makeToolResult(`Memory topics (${topics.length}):
5627
+ ${lines.join("\n")}
5628
+
5629
+ Memory dir: ${memoryDir}`);
5630
+ }
5631
+ default:
5632
+ return makeToolError(`Unknown action: ${action}. Use save, delete, or list.`);
5633
+ }
5634
+ }
5635
+ };
5636
+
4255
5637
  // src/llm/anthropic.ts
4256
5638
  init_esm_shims();
4257
5639
  import Anthropic from "@anthropic-ai/sdk";
@@ -4907,12 +6289,12 @@ function parseArgs(argv) {
4907
6289
  }
4908
6290
  function printHelp() {
4909
6291
  console.log(`
4910
- ${chalk13.cyan.bold("Codi (\uCF54\uB514)")} - AI Code Agent for Terminal
6292
+ ${chalk14.cyan.bold("Codi (\uCF54\uB514)")} - AI Code Agent for Terminal
4911
6293
 
4912
- ${chalk13.bold("Usage:")}
6294
+ ${chalk14.bold("Usage:")}
4913
6295
  codi [options] [prompt]
4914
6296
 
4915
- ${chalk13.bold("Options:")}
6297
+ ${chalk14.bold("Options:")}
4916
6298
  -m, --model <model> Set the model (default: gemini-2.5-flash)
4917
6299
  --provider <name> Set the provider (openai, anthropic, ollama)
4918
6300
  -p <prompt> Run a single prompt and exit
@@ -4923,12 +6305,12 @@ ${chalk13.bold("Options:")}
4923
6305
  -h, --help Show this help
4924
6306
  -v, --version Show version
4925
6307
 
4926
- ${chalk13.bold("Environment:")}
6308
+ ${chalk14.bold("Environment:")}
4927
6309
  GEMINI_API_KEY Google Gemini API key (default provider)
4928
6310
  OPENAI_API_KEY OpenAI API key
4929
6311
  ANTHROPIC_API_KEY Anthropic API key
4930
6312
 
4931
- ${chalk13.bold("Examples:")}
6313
+ ${chalk14.bold("Examples:")}
4932
6314
  codi # Start interactive session
4933
6315
  codi -p "explain main.ts" # Single prompt
4934
6316
  codi --provider anthropic # Use Anthropic Claude
@@ -4944,11 +6326,11 @@ async function main() {
4944
6326
  }
4945
6327
  if (args.version) {
4946
6328
  try {
4947
- const { readFileSync: readFileSync14 } = await import("fs");
6329
+ const { readFileSync: readFileSync17 } = await import("fs");
4948
6330
  const { fileURLToPath: fileURLToPath3 } = await import("url");
4949
6331
  const p = await import("path");
4950
6332
  const dir = p.dirname(fileURLToPath3(import.meta.url));
4951
- const pkg = JSON.parse(readFileSync14(p.join(dir, "..", "package.json"), "utf-8"));
6333
+ const pkg = JSON.parse(readFileSync17(p.join(dir, "..", "package.json"), "utf-8"));
4952
6334
  console.log(`codi v${pkg.version}`);
4953
6335
  } catch {
4954
6336
  console.log("codi v0.1.8");
@@ -5012,7 +6394,8 @@ async function main() {
5012
6394
  taskUpdateTool,
5013
6395
  taskListTool,
5014
6396
  taskGetTool,
5015
- askUserTool
6397
+ askUserTool,
6398
+ updateMemoryTool
5016
6399
  ]);
5017
6400
  const subAgentHandler2 = createSubAgentHandler(provider, registry);
5018
6401
  setSubAgentHandler(subAgentHandler2);
@@ -5055,10 +6438,11 @@ async function main() {
5055
6438
  if (msg.role === "user") conversation.addUserMessage(msg.content);
5056
6439
  else if (msg.role === "assistant") conversation.addAssistantMessage(msg.content);
5057
6440
  }
5058
- console.log(chalk13.dim(`Resumed session: ${id}`));
6441
+ console.log(chalk14.dim(`Resumed session: ${id}`));
5059
6442
  }
5060
6443
  }
5061
6444
  }
6445
+ logger.info("\uC138\uC158 \uC2DC\uC791", { provider: providerName, model: modelName, cwd: process.cwd(), planMode: getMode() === "plan", yolo: !!args.yolo });
5062
6446
  await hookManager.runHooks("SessionStart", { cwd: process.cwd() });
5063
6447
  if (args.prompt) {
5064
6448
  await agentLoop(args.prompt, {
@@ -5073,6 +6457,7 @@ async function main() {
5073
6457
  },
5074
6458
  planMode: getMode() === "plan"
5075
6459
  });
6460
+ logger.info("\uC138\uC158 \uC885\uB8CC (single prompt)");
5076
6461
  await hookManager.runHooks("SessionEnd", {});
5077
6462
  await mcpManager.disconnectAll();
5078
6463
  process.exit(0);
@@ -5084,7 +6469,7 @@ async function main() {
5084
6469
  compressor,
5085
6470
  exitFn: async () => {
5086
6471
  stopSpinner();
5087
- console.log(chalk13.dim("\nSaving session..."));
6472
+ console.log(chalk14.dim("\nSaving session..."));
5088
6473
  sessionManager.save(conversation, void 0, provider.model);
5089
6474
  await hookManager.runHooks("SessionEnd", {});
5090
6475
  await mcpManager.disconnectAll();
@@ -5114,7 +6499,7 @@ async function main() {
5114
6499
  const preview = typeof message === "string" ? message.slice(0, 50) : message.find((b) => b.type === "text")?.text?.slice(0, 50) || "image";
5115
6500
  checkpointManager.create(conversation, preview);
5116
6501
  if (compressor.shouldCompress(conversation)) {
5117
- console.log(chalk13.dim("Auto-compacting conversation..."));
6502
+ console.log(chalk14.dim("Auto-compacting conversation..."));
5118
6503
  await compressor.compress(conversation, provider);
5119
6504
  conversation.setSystemPrompt(buildPrompt());
5120
6505
  }
@@ -5143,14 +6528,17 @@ async function main() {
5143
6528
  },
5144
6529
  onExit: async () => {
5145
6530
  stopSpinner();
5146
- console.log(chalk13.dim("\nSaving session..."));
6531
+ logger.info("\uC138\uC158 \uC885\uB8CC (REPL exit)");
6532
+ console.log(chalk14.dim("\nSaving session..."));
5147
6533
  sessionManager.save(conversation, void 0, provider.model);
6534
+ checkpointManager.cleanup();
5148
6535
  await hookManager.runHooks("SessionEnd", {});
5149
6536
  await mcpManager.disconnectAll();
5150
6537
  }
5151
6538
  });
5152
6539
  process.on("SIGTERM", async () => {
5153
6540
  stopSpinner();
6541
+ logger.info("\uC138\uC158 \uC885\uB8CC (SIGTERM)");
5154
6542
  sessionManager.save(conversation, void 0, provider.model);
5155
6543
  await hookManager.runHooks("SessionEnd", {});
5156
6544
  await mcpManager.disconnectAll();
@@ -5159,7 +6547,8 @@ async function main() {
5159
6547
  await repl.start();
5160
6548
  }
5161
6549
  main().catch((err) => {
5162
- console.error(chalk13.red(`Fatal error: ${err.message}`));
6550
+ logger.error("\uCE58\uBA85\uC801 \uC624\uB958", {}, err instanceof Error ? err : new Error(String(err)));
6551
+ console.error(chalk14.red(`Fatal error: ${err.message}`));
5163
6552
  console.error(err.stack);
5164
6553
  process.exit(1);
5165
6554
  });