dskcode 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -772,7 +772,7 @@ async function getAllSkills(cwd) {
772
772
 
773
773
  // src/cli/index.tsx
774
774
  import { readFile as readFile11 } from "fs/promises";
775
- import { join as join10 } from "path";
775
+ import { join as join12 } from "path";
776
776
 
777
777
  // src/ui/RenderScope.tsx
778
778
  import { render } from "ink";
@@ -846,7 +846,15 @@ import { Box as Box5, Text as Text6 } from "ink";
846
846
  // src/provider/cost-tracker.ts
847
847
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
848
848
  import { join as join3 } from "path";
849
- var CostTracker = class {
849
+ var CostTracker = class _CostTracker {
850
+ /** 全局活跃实例表,用于进程退出时统一 flush。 */
851
+ static #instances = /* @__PURE__ */ new Set();
852
+ /** 进程退出兜底:统一 flush 所有活跃 CostTracker。 */
853
+ static async flushAll() {
854
+ await Promise.allSettled(
855
+ [..._CostTracker.#instances].map((t) => t.flush())
856
+ );
857
+ }
850
858
  #costDir;
851
859
  #budgetLimit;
852
860
  #tokenBudgetLimit;
@@ -872,6 +880,7 @@ var CostTracker = class {
872
880
  const today = getTodayStr();
873
881
  this.#todayDate = today;
874
882
  this.#todaySummary = createEmptyDailySummary(today);
883
+ _CostTracker.#instances.add(this);
875
884
  }
876
885
  // -----------------------------------------------------------------------
877
886
  // 核心方法
@@ -879,6 +888,7 @@ var CostTracker = class {
879
888
  /**
880
889
  * 记录一次 API 调用的 Token 使用量和成本。
881
890
  * 自动计算费用,累加到会话级和日级统计。
891
+ * 标记脏数据(dirty),由上层在合适的时机调用 flush() 持久化到磁盘。
882
892
  */
883
893
  record(usage, model) {
884
894
  const cost = calculateCost(usage, model);
@@ -987,8 +997,10 @@ var CostTracker = class {
987
997
  /**
988
998
  * 从磁盘加载历史成本数据。
989
999
  * 启动时调用,用于恢复今日和历史数据。
1000
+ * 副作用:确保成本目录存在(首次运行时创建 ~/.dskcode/costs)。
990
1001
  */
991
1002
  async load() {
1003
+ await this.#ensureCostDir();
992
1004
  const store = await this.#loadStore();
993
1005
  const today = getTodayStr();
994
1006
  if (store.daily[today]) {
@@ -998,14 +1010,19 @@ var CostTracker = class {
998
1010
  }
999
1011
  /**
1000
1012
  * 将脏数据持久化到磁盘。
1001
- * 通常在每次 record() 后异步调用,或在会话结束时主动调用。
1013
+ * 会话结束/进程退出时主动调用一次即可。需要每日数据精确时也可在每次
1014
+ * record() 后调用。并发安全:靠 #flushInProgress 互斥。
1002
1015
  */
1003
1016
  async flush() {
1004
1017
  if (!this.#dirty || this.#flushInProgress) return;
1005
1018
  this.#flushInProgress = true;
1006
1019
  this.#dirty = false;
1007
1020
  try {
1021
+ await this.#ensureCostDir();
1008
1022
  await this.#saveStore();
1023
+ } catch (err) {
1024
+ this.#dirty = true;
1025
+ console.error("[CostTracker] flush \u5931\u8D25:", err);
1009
1026
  } finally {
1010
1027
  this.#flushInProgress = false;
1011
1028
  }
@@ -1033,6 +1050,14 @@ var CostTracker = class {
1033
1050
  this.#sessionStartedAt = (/* @__PURE__ */ new Date()).toISOString();
1034
1051
  this.#sessionRecords.length = 0;
1035
1052
  }
1053
+ /**
1054
+ * 销毁实例:从全局实例表移除并 flush。
1055
+ * 一般用不到,仅在测试或显式生命周期管理时使用。
1056
+ */
1057
+ async dispose() {
1058
+ _CostTracker.#instances.delete(this);
1059
+ await this.flush();
1060
+ }
1036
1061
  // -----------------------------------------------------------------------
1037
1062
  // 内部方法
1038
1063
  // -----------------------------------------------------------------------
@@ -1085,6 +1110,10 @@ var CostTracker = class {
1085
1110
  return { version: 1, daily: {} };
1086
1111
  }
1087
1112
  }
1113
+ /** 确保成本目录存在(首次运行时创建 ~/.dskcode/costs) */
1114
+ async #ensureCostDir() {
1115
+ await mkdir3(this.#costDir, { recursive: true });
1116
+ }
1088
1117
  /** 保存成本存储到磁盘 */
1089
1118
  async #saveStore() {
1090
1119
  const store = await this.#loadStore();
@@ -1096,7 +1125,6 @@ var CostTracker = class {
1096
1125
  for (const date of datesToRemove) {
1097
1126
  delete store.daily[date];
1098
1127
  }
1099
- await mkdir3(this.#costDir, { recursive: true });
1100
1128
  const filePath = join3(this.#costDir, "history.json");
1101
1129
  await writeFile2(filePath, JSON.stringify(store, null, 2), "utf-8");
1102
1130
  }
@@ -2249,6 +2277,153 @@ function isENOENT(err) {
2249
2277
  return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
2250
2278
  }
2251
2279
 
2280
+ // src/logger/logger.ts
2281
+ import { mkdir as mkdir5, appendFile } from "fs/promises";
2282
+ import { join as join5, basename } from "path";
2283
+ var MAX_DATA_LEN = 2e3;
2284
+ var MAX_QUEUE = 500;
2285
+ function defaultLogsDir() {
2286
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "~";
2287
+ return join5(home, ".dskcode", "logs");
2288
+ }
2289
+ function projectNameFromCwd(cwd) {
2290
+ const name = basename(cwd);
2291
+ return name || "_root";
2292
+ }
2293
+ var ConversationLogger = class _ConversationLogger {
2294
+ /** 所有活跃的 logger 实例(用于退出时统一 flush) */
2295
+ static #instances = /* @__PURE__ */ new Set();
2296
+ /**
2297
+ * 刷新所有活跃 logger 实例的缓冲,确保事件落盘。
2298
+ * 进程退出前调用。
2299
+ */
2300
+ static async flushAll() {
2301
+ const promises = [];
2302
+ for (const logger of _ConversationLogger.#instances) {
2303
+ promises.push(logger.flush());
2304
+ }
2305
+ await Promise.allSettled(promises);
2306
+ }
2307
+ #logPath;
2308
+ #enabled;
2309
+ #queue = Promise.resolve();
2310
+ #queueLen = 0;
2311
+ #closed = false;
2312
+ constructor(sessionId, cwd, options) {
2313
+ this.#enabled = options?.enabled ?? true;
2314
+ if (!this.#enabled) {
2315
+ this.#logPath = "";
2316
+ return;
2317
+ }
2318
+ const baseDir = options?.logsDir ?? defaultLogsDir();
2319
+ const projectName = projectNameFromCwd(cwd);
2320
+ this.#logPath = join5(baseDir, projectName, `${sessionId}.jsonl`);
2321
+ _ConversationLogger.#instances.add(this);
2322
+ }
2323
+ /** 日志文件完整路径(禁用时返回空字符串) */
2324
+ get logPath() {
2325
+ return this.#logPath;
2326
+ }
2327
+ /** 是否启用日志记录 */
2328
+ get enabled() {
2329
+ return this.#enabled;
2330
+ }
2331
+ /**
2332
+ * 记录一个事件。
2333
+ *
2334
+ * 内部将事件序列化为 JSON 并追加到日志文件。
2335
+ * 写入操作串行排队,保证事件顺序。调用方无需 await。
2336
+ */
2337
+ log(event) {
2338
+ if (!this.#enabled || this.#closed) return;
2339
+ if (this.#queueLen >= MAX_QUEUE) return;
2340
+ this.#queueLen++;
2341
+ this.#queue = this.#queue.then(() => this.#writeLine(event)).catch(() => {
2342
+ }).finally(() => this.#queueLen--);
2343
+ }
2344
+ /**
2345
+ * 关闭日志记录器。
2346
+ *
2347
+ * 标记关闭,不再接受新事件,但会等待队列中已有事件写入完成。
2348
+ * 返回的 Promise resolve 后保证所有已 log 的事件都已落盘。
2349
+ */
2350
+ async flush() {
2351
+ this.#closed = true;
2352
+ await this.#queue.catch(() => {
2353
+ });
2354
+ _ConversationLogger.#instances.delete(this);
2355
+ }
2356
+ /** 将一行 JSON 写入日志文件 */
2357
+ async #writeLine(event) {
2358
+ const dir = join5(this.#logPath, "..");
2359
+ await mkdir5(dir, { recursive: true });
2360
+ const line = JSON.stringify(event) + "\n";
2361
+ await appendFile(this.#logPath, line, "utf-8");
2362
+ }
2363
+ // -----------------------------------------------------------------------
2364
+ // 便捷方法 — 封装常见事件类型,减少调用方样板代码
2365
+ // -----------------------------------------------------------------------
2366
+ /** 记录会话开始 */
2367
+ logSessionStart(sessionId, cwd, model, mode) {
2368
+ this.log({ ts: Date.now(), type: "session_start", sessionId, cwd, model, mode });
2369
+ }
2370
+ /** 记录用户消息 */
2371
+ logUserMessage(content) {
2372
+ this.log({ ts: Date.now(), type: "user_message", content });
2373
+ }
2374
+ /** 记录助手文本(一轮中模型输出的完整文本) */
2375
+ logAssistantText(content, round) {
2376
+ this.log({ ts: Date.now(), type: "assistant_text", content, round });
2377
+ }
2378
+ /** 记录工具调用 */
2379
+ logToolCall(name, callId, args, round) {
2380
+ this.log({ ts: Date.now(), type: "tool_call", name, callId, arguments: args, round });
2381
+ }
2382
+ /** 记录工具结果 */
2383
+ logToolResult(name, callId, success, data, error, elapsed, round) {
2384
+ this.log({
2385
+ ts: Date.now(),
2386
+ type: "tool_result",
2387
+ name,
2388
+ callId,
2389
+ success,
2390
+ data: truncate(data),
2391
+ ...error ? { error } : {},
2392
+ ...elapsed !== void 0 ? { elapsed } : {},
2393
+ round
2394
+ });
2395
+ }
2396
+ /** 记录 Token 用量与费用 */
2397
+ logUsage(model, promptTokens, completionTokens, cachedPromptTokens, cost, round) {
2398
+ this.log({
2399
+ ts: Date.now(),
2400
+ type: "usage",
2401
+ model,
2402
+ promptTokens,
2403
+ completionTokens,
2404
+ ...cachedPromptTokens !== void 0 ? { cachedPromptTokens } : {},
2405
+ cost,
2406
+ round
2407
+ });
2408
+ }
2409
+ /** 记录错误 */
2410
+ logError(message, stack) {
2411
+ this.log({ ts: Date.now(), type: "error", message, ...stack ? { stack } : {} });
2412
+ }
2413
+ /** 记录一轮对话完成 */
2414
+ logTurnDone(elapsed, toolRounds) {
2415
+ this.log({ ts: Date.now(), type: "turn_done", elapsed, toolRounds });
2416
+ }
2417
+ /** 记录会话结束 */
2418
+ logSessionEnd(elapsed) {
2419
+ this.log({ ts: Date.now(), type: "session_end", elapsed });
2420
+ }
2421
+ };
2422
+ function truncate(text) {
2423
+ if (text.length <= MAX_DATA_LEN) return text;
2424
+ return text.slice(0, MAX_DATA_LEN) + `...[\u5DF2\u622A\u65AD\uFF0C\u539F\u59CB\u957F\u5EA6 ${text.length}]`;
2425
+ }
2426
+
2252
2427
  // src/agent/index.ts
2253
2428
  var Session = class _Session {
2254
2429
  #messages = [];
@@ -2264,6 +2439,7 @@ var Session = class _Session {
2264
2439
  #checkpoints = /* @__PURE__ */ new Map();
2265
2440
  #stormRecords = [];
2266
2441
  #mode = "code";
2442
+ #logger;
2267
2443
  constructor(provider, tools = [], costTracker, options) {
2268
2444
  this.#provider = provider;
2269
2445
  if (tools instanceof ToolRegistry) {
@@ -2281,11 +2457,16 @@ var Session = class _Session {
2281
2457
  projectContext: options?.projectContext,
2282
2458
  gate: options?.gate ?? new AlwaysAllowGate(),
2283
2459
  writeRoots: options?.writeRoots ?? [options?.cwd ?? process.cwd()],
2284
- enableCheckpoint: options?.enableCheckpoint ?? true
2460
+ enableCheckpoint: options?.enableCheckpoint ?? true,
2461
+ enableLog: options?.enableLog ?? true
2285
2462
  };
2286
2463
  this.#sessionId = options?.sessionId ?? SessionStore.newId();
2287
2464
  this.#store = options?.store === false ? null : options?.store ?? new SessionStore();
2288
2465
  this.#createdAt = Date.now();
2466
+ this.#logger = new ConversationLogger(this.#sessionId, this.#options.cwd, {
2467
+ enabled: this.#options.enableLog
2468
+ });
2469
+ this.#logger.logSessionStart(this.#sessionId, this.#options.cwd, this.#provider.model(), this.#mode);
2289
2470
  }
2290
2471
  get messages() {
2291
2472
  return this.#messages;
@@ -2320,6 +2501,7 @@ var Session = class _Session {
2320
2501
  }
2321
2502
  async *chat(userInput, opts) {
2322
2503
  this.#messages.push({ role: "user", content: userInput });
2504
+ this.#logger.logUserMessage(userInput);
2323
2505
  const userMsgIndex = this.#messages.length - 1;
2324
2506
  if (this.#options.enableCheckpoint) {
2325
2507
  try {
@@ -2368,13 +2550,26 @@ var Session = class _Session {
2368
2550
  if (lastUsage) {
2369
2551
  const modelId = this.#provider.model();
2370
2552
  this.#costTracker.record(lastUsage, modelId);
2553
+ const cost = calculateCost(lastUsage, modelId);
2554
+ this.#logger.logUsage(
2555
+ modelId,
2556
+ lastUsage.promptTokens,
2557
+ lastUsage.completionTokens,
2558
+ lastUsage.cachedPromptTokens,
2559
+ cost.totalCost,
2560
+ toolRounds
2561
+ );
2371
2562
  yield { type: "usage", usage: lastUsage, model: modelId };
2372
2563
  }
2373
2564
  const assistantMsg = { role: "assistant", content: accumulatedText };
2374
2565
  if (lastToolCalls && lastToolCalls.length > 0) assistantMsg.toolCalls = lastToolCalls;
2375
2566
  this.#messages.push(assistantMsg);
2567
+ this.#logger.logAssistantText(accumulatedText, toolRounds);
2376
2568
  if (lastToolCalls && lastToolCalls.length > 0) {
2377
2569
  yield { type: "tool_calls", calls: lastToolCalls };
2570
+ for (const tc of lastToolCalls) {
2571
+ this.#logger.logToolCall(tc.name, tc.id, tc.arguments, toolRounds);
2572
+ }
2378
2573
  const stormBroken = this.#checkStormBreak(lastToolCalls);
2379
2574
  if (stormBroken) {
2380
2575
  const stormMsg = "\n\u26A0\uFE0F \u540C\u4E00\u5DE5\u5177\u91CD\u590D\u51FA\u9519\uFF0C\u5DF2\u5F3A\u5236\u5207\u6362\u7B56\u7565\n";
@@ -2389,6 +2584,15 @@ var Session = class _Session {
2389
2584
  this.#stormRecords = results.records;
2390
2585
  for (const item of results.items) {
2391
2586
  yield { type: "tool_result", name: item.name, result: item.result };
2587
+ this.#logger.logToolResult(
2588
+ item.name,
2589
+ item.callId,
2590
+ item.result.success,
2591
+ item.result.data,
2592
+ item.result.error,
2593
+ void 0,
2594
+ toolRounds
2595
+ );
2392
2596
  let toolContent = item.result.data;
2393
2597
  if (item.result.diff && item.result.diff.patch) toolContent += `
2394
2598
 
@@ -2408,11 +2612,18 @@ ${item.result.diff.patch}`;
2408
2612
  } catch (err) {
2409
2613
  if (err instanceof DOMException && err.name === "AbortError") return;
2410
2614
  if (err instanceof Error && err.name === "AbortError") return;
2615
+ this.#logger.logError(
2616
+ err instanceof Error ? err.message : String(err),
2617
+ err instanceof Error ? err.stack : void 0
2618
+ );
2411
2619
  yield { type: "error", error: err instanceof Error ? err : new Error(String(err)) };
2412
2620
  return;
2413
2621
  }
2414
2622
  const elapsed = Date.now() - startTime;
2623
+ this.#logger.logTurnDone(elapsed, toolRounds);
2415
2624
  void this.#persist();
2625
+ await this.#costTracker.flush().catch(() => {
2626
+ });
2416
2627
  yield { type: "done", elapsed };
2417
2628
  }
2418
2629
  async #executeBatch(calls) {
@@ -2514,6 +2725,10 @@ ${item.result.diff.patch}`;
2514
2725
  this.#persistTimer = null;
2515
2726
  }
2516
2727
  }
2728
+ /** 刷新日志缓冲,确保所有已记录的事件落盘 */
2729
+ async flushLog() {
2730
+ await this.#logger.flush();
2731
+ }
2517
2732
  reset() {
2518
2733
  this.#messages.length = 0;
2519
2734
  this.#costTracker.resetSession();
@@ -2655,6 +2870,8 @@ ${item.result.diff.patch}`;
2655
2870
  void discardCheckpoint(cp2);
2656
2871
  }
2657
2872
  this.#checkpoints.clear();
2873
+ this.#logger.logSessionEnd(Date.now() - this.#createdAt);
2874
+ await this.#logger.flush();
2658
2875
  }
2659
2876
  // -------------------------------------------------------------------------
2660
2877
  // 内部方法
@@ -2750,6 +2967,10 @@ async function execCommand(command, args, cwd, timeoutMs = DEFAULT_TIMEOUT_MS, s
2750
2967
  spawnCmd = command;
2751
2968
  spawnArgs = args;
2752
2969
  useShell = false;
2970
+ if (isWindows && spawnCmd === "cmd" && spawnArgs[0] === "/c") {
2971
+ const prefixed = `chcp 65001 >nul && ${spawnArgs[1] ?? ""}`;
2972
+ spawnArgs = ["/c", prefixed];
2973
+ }
2753
2974
  } else {
2754
2975
  useShell = !isWindows;
2755
2976
  spawnCmd = isWindows ? "cmd" : command;
@@ -2764,10 +2985,10 @@ async function execCommand(command, args, cwd, timeoutMs = DEFAULT_TIMEOUT_MS, s
2764
2985
  let stdout = "";
2765
2986
  let stderr = "";
2766
2987
  child.stdout.on("data", (data) => {
2767
- stdout += data.toString();
2988
+ stdout += data.toString("utf8");
2768
2989
  });
2769
2990
  child.stderr.on("data", (data) => {
2770
- stderr += data.toString();
2991
+ stderr += data.toString("utf8");
2771
2992
  });
2772
2993
  const timeout = setTimeout(() => {
2773
2994
  child.kill("SIGTERM");
@@ -3213,8 +3434,8 @@ var readFileTool = {
3213
3434
  };
3214
3435
 
3215
3436
  // src/tool/builtins/write-file.ts
3216
- import { mkdir as mkdir5, readFile as readFile6 } from "fs/promises";
3217
- import { dirname, relative as relative3, basename } from "path";
3437
+ import { mkdir as mkdir6, readFile as readFile6 } from "fs/promises";
3438
+ import { dirname, relative as relative3, basename as basename2 } from "path";
3218
3439
  var writeFileTool = {
3219
3440
  name: "write_file",
3220
3441
  kind: "edit" /* Edit */,
@@ -3256,7 +3477,7 @@ var writeFileTool = {
3256
3477
  existedBefore = true;
3257
3478
  } catch {
3258
3479
  }
3259
- await mkdir5(dirname(filePath), { recursive: true });
3480
+ await mkdir6(dirname(filePath), { recursive: true });
3260
3481
  const content = args.content;
3261
3482
  await writeFileWithEol(filePath, oldContent, content);
3262
3483
  const diff = computeFileDiff(oldContent, content, filePath);
@@ -3265,7 +3486,7 @@ var writeFileTool = {
3265
3486
  const byteSize = Buffer.byteLength(content, "utf-8");
3266
3487
  const action = existedBefore ? "\u5DF2\u4FEE\u6539" : "\u5DF2\u521B\u5EFA";
3267
3488
  const diffSummary = existedBefore ? `\uFF0C+${diff.additions} -${diff.deletions}` : `\uFF0C+${diff.additions} \u884C\uFF08\u65B0\u5EFA\uFF09`;
3268
- const fileName = basename(filePath);
3489
+ const fileName = basename2(filePath);
3269
3490
  const summary = existedBefore ? `\u{1F4DD} \u4FEE\u6539: ${fileName} (+${diff.additions} -${diff.deletions})` : `\u{1F4DD} \u65B0\u5EFA: ${fileName} (+${diff.additions} \u884C)`;
3270
3491
  return {
3271
3492
  success: true,
@@ -3286,7 +3507,7 @@ var writeFileTool = {
3286
3507
 
3287
3508
  // src/tool/builtins/edit-file.ts
3288
3509
  import { readFile as readFile7 } from "fs/promises";
3289
- import { basename as basename2 } from "path";
3510
+ import { basename as basename3 } from "path";
3290
3511
  var editFileTool = {
3291
3512
  name: "edit_file",
3292
3513
  kind: "edit" /* Edit */,
@@ -3354,7 +3575,7 @@ var editFileTool = {
3354
3575
  const oldLines = args.old_text.split("\n").length;
3355
3576
  const newLines = args.new_text.split("\n").length;
3356
3577
  const diffSummary = `+${diff.additions} -${diff.deletions}`;
3357
- const summary = `\u{1F4DD} \u4FEE\u6539: ${basename2(filePath)} (+${diff.additions} -${diff.deletions})`;
3578
+ const summary = `\u{1F4DD} \u4FEE\u6539: ${basename3(filePath)} (+${diff.additions} -${diff.deletions})`;
3358
3579
  return {
3359
3580
  success: true,
3360
3581
  data: `\u6587\u4EF6\u5DF2\u7F16\u8F91\uFF1A${filePath}
@@ -3377,7 +3598,7 @@ ${oldLines} \u884C \u2192 ${newLines} \u884C
3377
3598
 
3378
3599
  // src/tool/builtins/multi-edit.ts
3379
3600
  import { readFile as readFile8 } from "fs/promises";
3380
- import { basename as basename3 } from "path";
3601
+ import { basename as basename4 } from "path";
3381
3602
  var multiEditTool = {
3382
3603
  name: "multi_edit",
3383
3604
  kind: "edit" /* Edit */,
@@ -3475,7 +3696,7 @@ var multiEditTool = {
3475
3696
  data: `\u6587\u4EF6\u5DF2\u7F16\u8F91\uFF1A${filePath}
3476
3697
  \u5171\u6267\u884C ${args.edits.length} \u6B65\u66FF\u6362
3477
3698
  \u53D8\u66F4\uFF1A+${diff.additions} -${diff.deletions}`,
3478
- summary: `\u{1F4DD} \u4FEE\u6539: ${basename3(filePath)} (${args.edits.length} \u6B65, +${diff.additions} -${diff.deletions})`,
3699
+ summary: `\u{1F4DD} \u4FEE\u6539: ${basename4(filePath)} (${args.edits.length} \u6B65, +${diff.additions} -${diff.deletions})`,
3479
3700
  diff
3480
3701
  };
3481
3702
  } catch (err) {
@@ -3491,7 +3712,7 @@ var multiEditTool = {
3491
3712
 
3492
3713
  // src/tool/builtins/delete-range.ts
3493
3714
  import { readFile as readFile9 } from "fs/promises";
3494
- import { basename as basename4 } from "path";
3715
+ import { basename as basename5 } from "path";
3495
3716
  function findUniqueLine(lines, anchor, label) {
3496
3717
  const matches = [];
3497
3718
  for (let i = 0; i < lines.length; i++) {
@@ -3587,7 +3808,7 @@ var deleteRangeTool = {
3587
3808
  const diff = computeFileDiff(content, newContent, filePath);
3588
3809
  diff.existedBefore = true;
3589
3810
  const deletedLines = rangeEnd - rangeStart + 1;
3590
- const summary = `\u{1F4DD} \u4FEE\u6539: ${basename4(filePath)} (\u5220 ${deletedLines} \u884C, +${diff.additions} -${diff.deletions})`;
3811
+ const summary = `\u{1F4DD} \u4FEE\u6539: ${basename5(filePath)} (\u5220 ${deletedLines} \u884C, +${diff.additions} -${diff.deletions})`;
3591
3812
  return {
3592
3813
  success: true,
3593
3814
  data: `\u6587\u4EF6\u5DF2\u7F16\u8F91\uFF1A${filePath}
@@ -3609,17 +3830,62 @@ var deleteRangeTool = {
3609
3830
 
3610
3831
  // src/tool/builtins/bash.ts
3611
3832
  import process3 from "process";
3833
+ import { existsSync as existsSync3 } from "fs";
3834
+ import { join as join6 } from "path";
3612
3835
  var isWindows2 = process3.platform === "win32";
3836
+ function detectShell() {
3837
+ if (!isWindows2) {
3838
+ return {
3839
+ bin: "sh",
3840
+ args: ["-c"],
3841
+ label: "sh",
3842
+ hint: "\u5F53\u524D shell \u662F bash \u517C\u5BB9\u8BED\u6CD5\uFF0C\u652F\u6301 pwd/ls/&&/\u7BA1\u9053\u7B49\u6807\u51C6 Unix \u5DE5\u5177\u3002"
3843
+ };
3844
+ }
3845
+ const envBash = process3.env.DSKCODE_BASH;
3846
+ if (envBash && existsSync3(envBash)) {
3847
+ return { bin: envBash, args: ["-c"], label: "Git Bash", hint: SHELL_HINT_BASH };
3848
+ }
3849
+ const roots = [
3850
+ process3.env.ProgramFiles,
3851
+ process3.env["ProgramFiles(x86)"],
3852
+ process3.env.LOCALAPPDATA ? join6(process3.env.LOCALAPPDATA, "Programs") : null,
3853
+ // 非 C 盘安装(你机器就是 D 盘),枚举几个常见盘符根
3854
+ "D:\\Program Files",
3855
+ "E:\\Program Files"
3856
+ ].filter((v) => Boolean(v));
3857
+ for (const root of roots) {
3858
+ for (const sub of ["Git\\bin\\bash.exe", "Git\\usr\\bin\\bash.exe"]) {
3859
+ const candidate = join6(root, sub);
3860
+ if (existsSync3(candidate)) {
3861
+ return { bin: candidate, args: ["-c"], label: "Git Bash", hint: SHELL_HINT_BASH };
3862
+ }
3863
+ }
3864
+ }
3865
+ return {
3866
+ bin: "cmd",
3867
+ args: ["/c"],
3868
+ label: "cmd.exe",
3869
+ hint: "\u5F53\u524D shell \u662F Windows cmd.exe\u3002\u8BF7\u4F7F\u7528 cmd \u8BED\u6CD5\uFF1A\u8DEF\u5F84\u7528\u53CD\u659C\u6760\u6216\u6B63\u659C\u6760\u5747\u53EF\u3001\u547D\u4EE4\u94FE\u7528 & \u6216\u6362\u884C\uFF0C\u907F\u514D\u4F7F\u7528 pwd/ls/&& \u7B49 Unix \u547D\u4EE4\u3002"
3870
+ };
3871
+ }
3872
+ var SHELL_HINT_BASH = "\u5F53\u524D shell \u662F bash \u517C\u5BB9\u8BED\u6CD5\uFF0C\u652F\u6301 pwd/ls/&&/\u7BA1\u9053\u7B49\u6807\u51C6 Unix \u5DE5\u5177\u3002";
3873
+ var SHELL_INFO = detectShell();
3613
3874
  var bashTool = {
3614
3875
  name: "bash",
3615
3876
  kind: "other" /* Other */,
3616
- description: "\u5728 shell \u4E2D\u6267\u884C\u547D\u4EE4\u3002\u8FD4\u56DE\u6807\u51C6\u8F93\u51FA\u3001\u6807\u51C6\u9519\u8BEF\u548C\u9000\u51FA\u7801\u3002\u652F\u6301\u8D85\u65F6\u63A7\u5236\u548C\u4FE1\u53F7\u4E2D\u6B62\u3002\u9002\u7528\u4E8E\u8FD0\u884C\u6784\u5EFA\u3001\u6D4B\u8BD5\u3001Git \u64CD\u4F5C\u7B49\u547D\u4EE4\u3002",
3877
+ description: [
3878
+ "\u5728 shell \u4E2D\u6267\u884C\u547D\u4EE4\uFF0C\u8FD4\u56DE stdout / stderr / \u9000\u51FA\u7801\u3002\u652F\u6301\u8D85\u65F6\u63A7\u5236\u548C\u4FE1\u53F7\u4E2D\u6B62\u3002",
3879
+ `\u5F53\u524D shell\uFF1A${SHELL_INFO.label}\u3002${SHELL_INFO.hint}`,
3880
+ "\u8BFB\u53D6\u6587\u4EF6\u8BF7\u4F18\u5148\u4F7F\u7528 read_file \u5DE5\u5177\uFF0C\u4E0D\u8981\u7528 cat/type/Get-Content\uFF1B",
3881
+ "\u641C\u7D22\u4EE3\u7801\u7528 grep / glob\uFF0C\u5217\u76EE\u5F55\u7528 ls\u2014\u2014\u8FD9\u4E9B\u4E13\u7528\u5DE5\u5177\u5728\u6240\u6709\u5E73\u53F0\u90FD\u53EF\u7528\u4E14\u65E0\u9700\u5173\u5FC3 shell \u8BED\u6CD5\u3002"
3882
+ ].join(""),
3617
3883
  parameters: {
3618
3884
  type: "object",
3619
3885
  properties: {
3620
3886
  command: {
3621
3887
  type: "string",
3622
- description: "\u8981\u6267\u884C\u7684 shell \u547D\u4EE4"
3888
+ description: `\u8981\u6267\u884C\u7684 shell \u547D\u4EE4\uFF08\u5F53\u524D shell\uFF1A${SHELL_INFO.label}\uFF09`
3623
3889
  },
3624
3890
  timeout: {
3625
3891
  type: "number",
@@ -3635,8 +3901,8 @@ var bashTool = {
3635
3901
  }
3636
3902
  const timeout = args.timeout ?? ctx.timeout ?? getDefaultTimeout();
3637
3903
  try {
3638
- const shellCommand = isWindows2 ? "cmd" : "sh";
3639
- const shellArgs = isWindows2 ? ["/c", args.command] : ["-c", args.command];
3904
+ const shellCommand = SHELL_INFO.bin ?? "cmd";
3905
+ const shellArgs = [...SHELL_INFO.args, args.command];
3640
3906
  const result = await execCommand(
3641
3907
  shellCommand,
3642
3908
  shellArgs,
@@ -3677,7 +3943,7 @@ ${truncateOutput(result.stderr)}`);
3677
3943
 
3678
3944
  // src/tool/builtins/glob.ts
3679
3945
  import { readdir as readdir3, stat as stat2 } from "fs/promises";
3680
- import { join as join5, relative as relative4, isAbsolute as isAbsolute2 } from "path";
3946
+ import { join as join7, relative as relative4, isAbsolute as isAbsolute2 } from "path";
3681
3947
  function globToRegex(pattern) {
3682
3948
  let regexStr = pattern;
3683
3949
  regexStr = regexStr.replace(/\*\*\//g, "<<GLOBSTAR_SLASH>>");
@@ -3702,7 +3968,7 @@ async function walkDir(dir, baseDir) {
3702
3968
  if (entry.isDirectory() && (entry.name === "node_modules" || entry.name === ".git")) {
3703
3969
  continue;
3704
3970
  }
3705
- const fullPath = join5(dir, entry.name);
3971
+ const fullPath = join7(dir, entry.name);
3706
3972
  const relPath = relative4(baseDir, fullPath);
3707
3973
  if (entry.isDirectory()) {
3708
3974
  results.push(...await walkDir(fullPath, baseDir));
@@ -3735,7 +4001,7 @@ var globTool = {
3735
4001
  if (!args?.pattern || typeof args.pattern !== "string") {
3736
4002
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 pattern", error: "INVALID_ARGS" };
3737
4003
  }
3738
- const searchDir = args.directory ? isAbsolute2(args.directory) ? args.directory : join5(ctx.cwd, args.directory) : ctx.cwd;
4004
+ const searchDir = args.directory ? isAbsolute2(args.directory) ? args.directory : join7(ctx.cwd, args.directory) : ctx.cwd;
3739
4005
  const regex = globToRegex(args.pattern);
3740
4006
  try {
3741
4007
  const dirStat = await stat2(searchDir);
@@ -3775,7 +4041,7 @@ var globTool = {
3775
4041
 
3776
4042
  // src/tool/builtins/grep.ts
3777
4043
  import { readdir as readdir4, readFile as readFile10, stat as stat3 } from "fs/promises";
3778
- import { join as join6, relative as relative5, isAbsolute as isAbsolute3 } from "path";
4044
+ import { join as join8, relative as relative5, isAbsolute as isAbsolute3 } from "path";
3779
4045
  async function collectFiles(dir, extension, maxFiles = 200) {
3780
4046
  const results = [];
3781
4047
  let entries;
@@ -3789,7 +4055,7 @@ async function collectFiles(dir, extension, maxFiles = 200) {
3789
4055
  if (entry.isDirectory() && (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist")) {
3790
4056
  continue;
3791
4057
  }
3792
- const fullPath = join6(dir, entry.name);
4058
+ const fullPath = join8(dir, entry.name);
3793
4059
  if (entry.isDirectory()) {
3794
4060
  results.push(...await collectFiles(fullPath, extension, maxFiles - results.length));
3795
4061
  } else {
@@ -3836,7 +4102,7 @@ var grepTool = {
3836
4102
  if (!args?.pattern || typeof args.pattern !== "string") {
3837
4103
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 pattern", error: "INVALID_ARGS" };
3838
4104
  }
3839
- const searchDir = args.directory ? isAbsolute3(args.directory) ? args.directory : join6(ctx.cwd, args.directory) : ctx.cwd;
4105
+ const searchDir = args.directory ? isAbsolute3(args.directory) ? args.directory : join8(ctx.cwd, args.directory) : ctx.cwd;
3840
4106
  const maxFiles = args.max_files ?? 200;
3841
4107
  try {
3842
4108
  const flags = args.case_sensitive ? "g" : "gi";
@@ -3896,7 +4162,7 @@ var grepTool = {
3896
4162
 
3897
4163
  // src/tool/builtins/ls.ts
3898
4164
  import { readdir as readdir5, stat as stat4 } from "fs/promises";
3899
- import { join as join7, relative as relative6 } from "path";
4165
+ import { join as join9, relative as relative6 } from "path";
3900
4166
  var lsTool = {
3901
4167
  name: "ls",
3902
4168
  kind: "read" /* Read */,
@@ -3934,7 +4200,7 @@ var lsTool = {
3934
4200
  let sizeStr = "";
3935
4201
  if (typeMark === "FILE") {
3936
4202
  try {
3937
- const fileStat = await stat4(join7(dirPath, entry.name));
4203
+ const fileStat = await stat4(join9(dirPath, entry.name));
3938
4204
  if (fileStat.size < 1024) {
3939
4205
  sizeStr = `${fileStat.size}B`;
3940
4206
  } else if (fileStat.size < 1024 * 1024) {
@@ -4373,7 +4639,12 @@ function ChatSession({
4373
4639
  }
4374
4640
  }, []);
4375
4641
  const { doubleCtrlC, handleCtrlC } = useDoubleCtrlC(() => {
4376
- process.exit(0);
4642
+ const s = sessionRef.current;
4643
+ if (s) {
4644
+ void s.flushLog().finally(() => process.exit(0));
4645
+ } else {
4646
+ process.exit(0);
4647
+ }
4377
4648
  });
4378
4649
  useInput(
4379
4650
  useCallback2(
@@ -4630,10 +4901,11 @@ function ChatSession({
4630
4901
  const cmdLower = trimmed.toLowerCase();
4631
4902
  if (cmdLower === "/rewind" || cmdLower.startsWith("/rewind ")) {
4632
4903
  if (isStreaming || rewinding) {
4904
+ const reason = rewinding ? "\u56DE\u9000\u4E2D" : "\u751F\u6210\u4E2D";
4633
4905
  setDisplayMessages((prev) => [
4634
4906
  ...prev,
4635
4907
  { role: "user", content: trimmed },
4636
- { role: "assistant", content: "\u26A0 \u6B63\u5728\u751F\u6210\u6216\u56DE\u9000\u4E2D\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5 /rewind\u3002" }
4908
+ { role: "assistant", content: `\u26A0 \u6B63\u5728${reason}\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5 /rewind\u3002` }
4637
4909
  ]);
4638
4910
  setInput("");
4639
4911
  return;
@@ -4772,6 +5044,7 @@ function ChatSession({
4772
5044
  const result = cmd.handler();
4773
5045
  switch (result.kind) {
4774
5046
  case "exit":
5047
+ await sessionRef.current?.flushLog();
4775
5048
  process.exit(0);
4776
5049
  return;
4777
5050
  case "clear":
@@ -4957,7 +5230,7 @@ function ChatSession({
4957
5230
  }, 2e3);
4958
5231
  }
4959
5232
  }
4960
- }, [onLaunchGame, onLaunchStock, currentContent, currentToolCalls, skills, skillSelectIndex, getFilteredSkills, thinkingEnabled, thinkingEffort, responseFormat, toolChoice, activeModel, sessionMode]);
5233
+ }, [onLaunchGame, onLaunchStock, currentContent, currentToolCalls, skills, skillSelectIndex, getFilteredSkills, thinkingEnabled, thinkingEffort, responseFormat, toolChoice, activeModel, sessionMode, isStreaming, rewinding]);
4961
5234
  useEffect3(() => {
4962
5235
  if (!isStreaming && externalCostTracker) {
4963
5236
  setTodayCost(externalCostTracker.todayTotalCost);
@@ -6418,9 +6691,9 @@ import { Box as Box13, Text as Text14, useInput as useInput5 } from "ink";
6418
6691
  import { useState as useState7, useCallback as useCallback6, useEffect as useEffect6, useMemo as useMemo2 } from "react";
6419
6692
  import asciichart from "asciichart";
6420
6693
  import os from "os";
6421
- import { join as join8 } from "path";
6694
+ import { join as join10 } from "path";
6422
6695
  import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
6423
- var SETTINGS_PATH = join8(os.homedir(), ".dskcode", "settings.json");
6696
+ var SETTINGS_PATH = join10(os.homedir(), ".dskcode", "settings.json");
6424
6697
  function toFileUrl(p) {
6425
6698
  const norm = p.replace(/\\/g, "/");
6426
6699
  if (process.platform === "win32") {
@@ -6755,7 +7028,7 @@ function renderDetail(stock, _onBack, prices, countdown = 10, currentTime) {
6755
7028
 
6756
7029
  // src/utils/scan-files.ts
6757
7030
  import { readdir as readdir6 } from "fs/promises";
6758
- import { join as join9, relative as relative7 } from "path";
7031
+ import { join as join11, relative as relative7 } from "path";
6759
7032
  var IGNORE_DIRS = /* @__PURE__ */ new Set([
6760
7033
  "node_modules",
6761
7034
  ".git",
@@ -6820,12 +7093,12 @@ async function scanProjectFiles(baseDir, dir) {
6820
7093
  } else if (entry.isFile()) {
6821
7094
  const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
6822
7095
  if (SOURCE_EXTS.has(ext)) {
6823
- files.push(relative7(baseDir, join9(currentDir, name)));
7096
+ files.push(relative7(baseDir, join11(currentDir, name)));
6824
7097
  }
6825
7098
  }
6826
7099
  }
6827
7100
  const nested = await Promise.all(
6828
- dirs.map((d) => scanProjectFiles(baseDir, join9(currentDir, d)))
7101
+ dirs.map((d) => scanProjectFiles(baseDir, join11(currentDir, d)))
6829
7102
  );
6830
7103
  for (const n of nested) {
6831
7104
  files.push(...n);
@@ -6913,7 +7186,7 @@ compdef _dskcode_completion dskcode`);
6913
7186
  program2.command("stock").description("\u67E5\u770B\u81EA\u9009\u80A1\u5B9E\u65F6\u884C\u60C5").argument("[codes...]", "\u80A1\u7968\u4EE3\u7801\uFF08\u7A7A\u683C\u5206\u9694\uFF09\uFF0C\u5982 513090 600519").action(async function(codes) {
6914
7187
  const _ctx = this.dskcodeCtx;
6915
7188
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "~";
6916
- const globalConfigPath = join10(home, ".dskcode", "settings.json");
7189
+ const globalConfigPath = join12(home, ".dskcode", "settings.json");
6917
7190
  let globalConfigHasStock = false;
6918
7191
  try {
6919
7192
  const raw = await readFile11(globalConfigPath, "utf-8");
@@ -7081,10 +7354,21 @@ var ExitCode = {
7081
7354
  // src/index.ts
7082
7355
  var sigintCount = 0;
7083
7356
  var sigintTimer = null;
7357
+ async function gracefulExit(code) {
7358
+ try {
7359
+ await Promise.all([
7360
+ CostTracker.flushAll(),
7361
+ ConversationLogger.flushAll()
7362
+ ]);
7363
+ } catch {
7364
+ }
7365
+ process.exit(code);
7366
+ }
7084
7367
  process.on("SIGINT", () => {
7085
7368
  sigintCount++;
7086
7369
  if (sigintCount >= 2) {
7087
- process.exit(ExitCode.SIGINT);
7370
+ void gracefulExit(ExitCode.SIGINT);
7371
+ return;
7088
7372
  }
7089
7373
  process.stdout.write("\n \u26A0 \u518D\u6309\u4E00\u6B21 Ctrl+C \u9000\u51FA dskcode\n");
7090
7374
  if (sigintTimer) clearTimeout(sigintTimer);
@@ -7092,18 +7376,25 @@ process.on("SIGINT", () => {
7092
7376
  sigintCount = 0;
7093
7377
  }, 1500);
7094
7378
  });
7379
+ process.on("SIGTERM", () => {
7380
+ void gracefulExit(ExitCode.SIGINT);
7381
+ });
7095
7382
  var program = createCli();
7096
7383
  try {
7097
7384
  await program.parseAsync(process.argv);
7385
+ await Promise.all([CostTracker.flushAll(), ConversationLogger.flushAll()]);
7098
7386
  } catch (err) {
7099
7387
  const error = err;
7100
7388
  if (error.code === "commander.helpDisplayed" || error.code === "commander.version") {
7389
+ await Promise.all([CostTracker.flushAll(), ConversationLogger.flushAll()]);
7101
7390
  process.exit(error.exitCode ?? ExitCode.SUCCESS);
7102
7391
  }
7103
7392
  if (typeof error.exitCode === "number") {
7393
+ await Promise.all([CostTracker.flushAll(), ConversationLogger.flushAll()]);
7104
7394
  process.exit(error.exitCode);
7105
7395
  }
7106
7396
  console.error(String(err));
7397
+ await Promise.all([CostTracker.flushAll(), ConversationLogger.flushAll()]);
7107
7398
  process.exit(ExitCode.GENERAL_ERROR);
7108
7399
  }
7109
7400
  //# sourceMappingURL=index.js.map