@gemdoq/codi 0.1.9 → 0.2.0

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