jinzd-ai-cli 0.4.266 → 0.4.268

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 (33) hide show
  1. package/dist/{batch-FQU6HK3Z.js → batch-2N2YMRX2.js} +2 -2
  2. package/dist/{chunk-QW76RX6G.js → chunk-2FQGMR34.js} +1 -1
  3. package/dist/{chunk-Y2X24VYB.js → chunk-74BK22BM.js} +4 -4
  4. package/dist/{chunk-TJNBSR3I.js → chunk-7US7K3CD.js} +1 -1
  5. package/dist/{chunk-FA3HGXXM.js → chunk-EPVJS4F6.js} +3 -1
  6. package/dist/{chunk-V4D7YD24.js → chunk-R7GERZDA.js} +145 -4
  7. package/dist/{chunk-GWOZET2E.js → chunk-RRQZRSXW.js} +1 -1
  8. package/dist/{chunk-LGJ7EX2F.js → chunk-UT2WAJJG.js} +1 -1
  9. package/dist/{chunk-CEJUB4YV.js → chunk-VGVKJTHF.js} +1 -1
  10. package/dist/{chunk-UCHND2K6.js → chunk-VNCBY4KY.js} +193 -2
  11. package/dist/{chunk-3O4VLKD4.js → chunk-WNG3YVZX.js} +1 -1
  12. package/dist/{chunk-N2E6GFLZ.js → chunk-WWLXW4SY.js} +1 -1
  13. package/dist/{chunk-LHY7ST6X.js → chunk-XDITAGE5.js} +1 -1
  14. package/dist/{chunk-FIDEG2PG.js → chunk-YLSM7RF6.js} +3 -3
  15. package/dist/{chunk-XEVURCAM.js → chunk-Z7CZ72RP.js} +3 -3
  16. package/dist/{ci-GZH3QMME.js → ci-3RWVJSZ3.js} +4 -4
  17. package/dist/{ci-format-VKP2MGYJ.js → ci-format-M6HVCXCG.js} +2 -2
  18. package/dist/{constants-ICAF6I3J.js → constants-G6VN7SJL.js} +1 -1
  19. package/dist/{doctor-cli-CO6BJWRW.js → doctor-cli-Y3SVVJBO.js} +4 -4
  20. package/dist/electron-server.js +915 -410
  21. package/dist/{hub-4APIRQ73.js → hub-5T5HKOAR.js} +1 -1
  22. package/dist/index.js +87 -134
  23. package/dist/{persistent-memory-YD3SHSUH.js → persistent-memory-XVSSR277.js} +2 -2
  24. package/dist/{persistent-memory-YPO25MOP.js → persistent-memory-YBWIZWQP.js} +2 -2
  25. package/dist/{pr-CDOPCAPK.js → pr-NF5IWLH3.js} +4 -4
  26. package/dist/{run-tests-QNNP2HCD.js → run-tests-33X3D7PV.js} +2 -2
  27. package/dist/{run-tests-4XRHKHN5.js → run-tests-Y44SCLIO.js} +2 -2
  28. package/dist/{server-B4A7LHZ5.js → server-3LERX42X.js} +5 -5
  29. package/dist/{server-53GM3CRH.js → server-NJ3ZNPQW.js} +114 -38
  30. package/dist/{task-orchestrator-WNV6PT6Z.js → task-orchestrator-JN2WFZSF.js} +5 -5
  31. package/dist/{usage-E3FEYSBU.js → usage-IFTNXTWC.js} +4 -6
  32. package/package.json +1 -1
  33. package/dist/chunk-YRD6M5XI.js +0 -149
@@ -34,14 +34,14 @@ import {
34
34
  touchMemoryReferences,
35
35
  updateMemoryApproval,
36
36
  updateMemoryEntry
37
- } from "./chunk-GWOZET2E.js";
37
+ } from "./chunk-RRQZRSXW.js";
38
38
  import {
39
39
  redactJson,
40
40
  scanString
41
41
  } from "./chunk-YVTASHS5.js";
42
42
  import {
43
43
  runTestsTool
44
- } from "./chunk-LHY7ST6X.js";
44
+ } from "./chunk-XDITAGE5.js";
45
45
  import {
46
46
  AGENTIC_BEHAVIOR_GUIDELINE,
47
47
  APP_NAME,
@@ -51,6 +51,7 @@ import {
51
51
  CONFIG_FILE_NAME,
52
52
  CONTEXT_FILE_CANDIDATES,
53
53
  CONTEXT_FILE_MAX_BYTES,
54
+ CONTEXT_PRESSURE_THRESHOLD,
54
55
  CUSTOM_COMMANDS_DIR_NAME,
55
56
  DEFAULT_MAX_TOKENS,
56
57
  DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP,
@@ -73,7 +74,7 @@ import {
73
74
  SUBAGENT_MAX_ROUNDS_LIMIT,
74
75
  VERSION,
75
76
  buildUserIdentityPrompt
76
- } from "./chunk-FA3HGXXM.js";
77
+ } from "./chunk-EPVJS4F6.js";
77
78
  import {
78
79
  hasSemanticIndex,
79
80
  semanticSearch
@@ -91,8 +92,8 @@ import {
91
92
  import express from "express";
92
93
  import { createServer } from "http";
93
94
  import { WebSocketServer } from "ws";
94
- import { join as join25, dirname as dirname10, resolve as resolve10, relative as relative6, sep as sep4 } from "path";
95
- import { existsSync as existsSync30, readFileSync as readFileSync24, readdirSync as readdirSync15, statSync as statSync14, realpathSync as realpathSync3 } from "fs";
95
+ import { join as join26, dirname as dirname10, resolve as resolve10, relative as relative6, sep as sep4 } from "path";
96
+ import { existsSync as existsSync31, readFileSync as readFileSync25, readdirSync as readdirSync15, statSync as statSync14, realpathSync as realpathSync3 } from "fs";
96
97
  import { networkInterfaces } from "os";
97
98
 
98
99
  // src/config/config-manager.ts
@@ -709,6 +710,403 @@ ${err}`
709
710
  }
710
711
  };
711
712
 
713
+ // src/core/cost-tracker.ts
714
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
715
+ import { join as join2 } from "path";
716
+
717
+ // src/core/pricing.ts
718
+ var DEEPSEEK_CNY_PER_USD = 6.9;
719
+ var DEEPSEEK_TIME_PRICING_EFFECTIVE_AT = /* @__PURE__ */ new Date("2026-08-16T16:00:00.000Z");
720
+ function deepSeekCnyPricing(cacheRead, input, output) {
721
+ return {
722
+ cacheRead: cacheRead / DEEPSEEK_CNY_PER_USD,
723
+ input: input / DEEPSEEK_CNY_PER_USD,
724
+ output: output / DEEPSEEK_CNY_PER_USD
725
+ };
726
+ }
727
+ function getDeepSeekV4Pricing(model, at) {
728
+ const isPro = model.startsWith("deepseek-v4-pro");
729
+ const isFlash = model.startsWith("deepseek-v4-flash");
730
+ if (!isPro && !isFlash) return null;
731
+ if (at < DEEPSEEK_TIME_PRICING_EFFECTIVE_AT) {
732
+ return isPro ? deepSeekCnyPricing(0.025, 3, 6) : deepSeekCnyPricing(0.02, 1, 2);
733
+ }
734
+ const beijingHour = (at.getUTCHours() + 8) % 24;
735
+ const isPeak = beijingHour >= 9 && beijingHour < 12 || beijingHour >= 14 && beijingHour < 18;
736
+ if (isPro) {
737
+ return isPeak ? deepSeekCnyPricing(0.3, 9, 27) : deepSeekCnyPricing(0.15, 4.5, 13.5);
738
+ }
739
+ return isPeak ? deepSeekCnyPricing(0.1, 3, 9) : deepSeekCnyPricing(0.05, 1.5, 4.5);
740
+ }
741
+ var PRICING_TABLE = {
742
+ // ── Anthropic Claude ──────────────────────────────────────────
743
+ "claude-opus-4-8": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
744
+ "claude-opus-4-7": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
745
+ "claude-opus-4-6": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
746
+ "claude-opus-4-5": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
747
+ "claude-sonnet-4-6": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
748
+ "claude-sonnet-4-5-20250929": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
749
+ "claude-haiku-4-5-20251001": { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 },
750
+ "claude-haiku-4-5": { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 },
751
+ // Legacy Claude 3.x families (prefix fallback handles minor date suffixes)
752
+ "claude-3-5-sonnet": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
753
+ "claude-3-5-haiku": { input: 0.8, output: 4, cacheWrite: 1, cacheRead: 0.08 },
754
+ "claude-3-opus": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
755
+ // ── OpenAI ────────────────────────────────────────────────────
756
+ // GAP (v0.4.184): the GPT-5 family (gpt-5, gpt-5-mini/nano, gpt-5.4, gpt-5.4-pro)
757
+ // and o4-mini are in the provider's model list but have NO verified pricing yet —
758
+ // left unpriced on purpose so `aicli usage` shows "—" (unknown) rather than a
759
+ // deceptive $0.00. Add real rates when confirmed from the official pricing page.
760
+ "gpt-4o": { input: 2.5, output: 10, cacheRead: 1.25 },
761
+ "gpt-4o-mini": { input: 0.15, output: 0.6, cacheRead: 0.075 },
762
+ "gpt-4-turbo": { input: 10, output: 30 },
763
+ "gpt-4": { input: 30, output: 60 },
764
+ "gpt-4.1": { input: 2, output: 8, cacheRead: 0.5 },
765
+ "gpt-4.1-mini": { input: 0.4, output: 1.6, cacheRead: 0.1 },
766
+ "gpt-4.1-nano": { input: 0.1, output: 0.4, cacheRead: 0.025 },
767
+ "o1": { input: 15, output: 60, cacheRead: 7.5 },
768
+ "o1-mini": { input: 3, output: 12, cacheRead: 1.5 },
769
+ "o3": { input: 10, output: 40, cacheRead: 2.5 },
770
+ "o3-mini": { input: 1.1, output: 4.4, cacheRead: 0.55 },
771
+ // ── Google Gemini ─────────────────────────────────────────────
772
+ "gemini-2.5-pro": { input: 1.25, output: 10 },
773
+ "gemini-2.5-flash": { input: 0.3, output: 2.5 },
774
+ "gemini-2.0-flash": { input: 0.1, output: 0.4 },
775
+ "gemini-1.5-pro": { input: 1.25, output: 5 },
776
+ "gemini-1.5-flash": { input: 0.075, output: 0.3 },
777
+ // ── DeepSeek ──────────────────────────────────────────────────
778
+ // V4 family (2026-04-23+):1M context, Thinking / Non-Thinking dual-mode, 384K max output
779
+ // 费率 2026-08-01 对 api-docs.deepseek.com/quick_start/pricing 官方 USD 表逐项核对。
780
+ // 修正前 pro 三项整齐高 4×(官方降过价未跟)、flash cacheRead 高 10×(像是按 input 的
781
+ // 20% 拍脑袋而非抄文档)——`aicli usage` 因此系统性高报 DeepSeek 成本。
782
+ // 待办:官方已公告峰谷定价(北京时间 9-12 点、14-18 点为平时 2 倍),本表仍是单一费率,
783
+ // 届时高峰时段会低报;等该策略实际生效后再决定是否引入时段维度。
784
+ // Legacy aliases:retires 2026-07-24 UTC 15:59,官方 route 到 V4 Flash。
785
+ // 这几条只服务历史 session 的成本回算,保留当时的费率口径,不随现价改动。
786
+ "deepseek-chat": { input: 0.14, output: 0.28, cacheRead: 0.028 },
787
+ "deepseek-reasoner": { input: 0.14, output: 0.28, cacheRead: 0.028 },
788
+ "deepseek-v3": { input: 0.27, output: 1.1, cacheRead: 0.07 },
789
+ // ── Moonshot Kimi ─────────────────────────────────────────────
790
+ "moonshot-v1-8k": { input: 0.17, output: 0.17 },
791
+ "moonshot-v1-32k": { input: 0.33, output: 0.33 },
792
+ "moonshot-v1-128k": { input: 0.83, output: 0.83 },
793
+ "kimi-k2": { input: 0.6, output: 2.5 },
794
+ "kimi-latest": { input: 0.6, output: 2.5 },
795
+ // Kimi K3(2026-07 旗舰,1M 上下文)官方定价 platform.kimi.ai/pricing/chat-k3:
796
+ // 输入 cache-miss $3.00 / cache-hit $0.30,输出 $15.00(/MTok)。
797
+ "kimi-k3": { input: 3, output: 15, cacheRead: 0.3 },
798
+ // ── Zhipu GLM ─────────────────────────────────────────────────
799
+ "glm-4-plus": { input: 0.7, output: 0.7 },
800
+ "glm-4": { input: 0.14, output: 0.14 },
801
+ "glm-4-flash": { input: 0, output: 0 },
802
+ "glm-4.5": { input: 0.29, output: 1.14 },
803
+ "glm-4.6": { input: 0.6, output: 2.2 },
804
+ "glm-4.6v": { input: 0.6, output: 2.2 },
805
+ "glm-5": { input: 0.85, output: 2.85 },
806
+ // GLM-5.2(2026-06)官方直连 API 定价 $1.40 / $4.40。
807
+ "glm-5.2": { input: 1.4, output: 4.4 },
808
+ // GLM-5V-Turbo(2026-04 视觉编程旗舰)官方直连 API 定价 $1.20 / $4.00。
809
+ "glm-5v-turbo": { input: 1.2, output: 4 },
810
+ "glm-5.1": { input: 0.95, output: 3.15 },
811
+ "glm-5.1-reasoning": { input: 1.4, output: 4.4 },
812
+ "glm-5.1-air": { input: 0.4, output: 1.2 },
813
+ "glm-z1": { input: 0.5, output: 1.5 },
814
+ "glm-z1-air": { input: 0.2, output: 0.6 },
815
+ "glm-z1-flash": { input: 0, output: 0 }
816
+ // ── MiniMax (海螺) ────────────────────────────────────────────
817
+ // GAP (v0.4.184): MiniMax-M3 / M2.x are unpriced — no verified rate anchor yet.
818
+ // Unpriced → `aicli usage` shows "—" (unknown), not a false $0.00. Add when confirmed.
819
+ // ── OpenRouter (pass-through — actual cost depends on underlying model) ──
820
+ // Left empty by design: model IDs are "vendor/model" (e.g. anthropic/claude-sonnet-4)
821
+ // and the real price is set by the underlying model + OpenRouter markup, not statically
822
+ // resolvable here. Shows "—" (unknown) in `aicli usage`.
823
+ // ── Ollama (local, zero cost) ─────────────────────────────────
824
+ // Handled via provider check below (FREE_PROVIDERS → genuine $0.00, not "unknown").
825
+ };
826
+ var FREE_PROVIDERS = /* @__PURE__ */ new Set(["ollama"]);
827
+ function getPricing(provider, model, at = /* @__PURE__ */ new Date()) {
828
+ if (FREE_PROVIDERS.has(provider.toLowerCase())) {
829
+ return { input: 0, output: 0 };
830
+ }
831
+ const key = model.toLowerCase();
832
+ if (provider.toLowerCase() === "deepseek") {
833
+ const dynamicPricing = getDeepSeekV4Pricing(key, at);
834
+ if (dynamicPricing) return dynamicPricing;
835
+ }
836
+ if (PRICING_TABLE[key]) return PRICING_TABLE[key];
837
+ const keys = Object.keys(PRICING_TABLE).sort((a, b) => b.length - a.length);
838
+ for (const k of keys) {
839
+ if (key.startsWith(k)) return PRICING_TABLE[k];
840
+ }
841
+ return null;
842
+ }
843
+ function computeCost(provider, model, usage, at = /* @__PURE__ */ new Date()) {
844
+ const p = getPricing(provider, model, at);
845
+ if (!p) return null;
846
+ const input = usage.inputTokens * p.input;
847
+ const output = usage.outputTokens * p.output;
848
+ const cacheWrite = (usage.cacheCreationTokens ?? 0) * (p.cacheWrite ?? p.input);
849
+ const cacheRead = (usage.cacheReadTokens ?? 0) * (p.cacheRead ?? p.input);
850
+ return (input + output + cacheWrite + cacheRead) / 1e6;
851
+ }
852
+ function formatCost(amount) {
853
+ if (amount === 0) return "$0.0000";
854
+ if (amount < 0.01) return `$${amount.toFixed(4)}`;
855
+ if (amount < 1) return `$${amount.toFixed(3)}`;
856
+ return `$${amount.toFixed(2)}`;
857
+ }
858
+
859
+ // src/core/cost-tracker.ts
860
+ var CostTracker = class _CostTracker {
861
+ filePath;
862
+ records = [];
863
+ dirty = false;
864
+ /** P2-PERF-04: debounce timer to batch rapid addCost() calls into a single save. */
865
+ _saveTimer;
866
+ constructor(configDir) {
867
+ this.filePath = join2(configDir, "cost-history.json");
868
+ this.load();
869
+ }
870
+ /** P2-PERF-04: debounce interval (ms) — coalesces multiple rapid addCost calls. */
871
+ static SAVE_DEBOUNCE_MS = 2e3;
872
+ load() {
873
+ try {
874
+ if (existsSync2(this.filePath)) {
875
+ const data = JSON.parse(readFileSync2(this.filePath, "utf-8"));
876
+ if (data.version === 1 && Array.isArray(data.records)) {
877
+ this.records = [...data.records].sort((a, b) => a.date.localeCompare(b.date));
878
+ }
879
+ }
880
+ } catch {
881
+ this.records = [];
882
+ }
883
+ }
884
+ /**
885
+ * Save to disk (atomic write).
886
+ *
887
+ * H2: Snapshot records before writing and only clear the dirty flag after
888
+ * the write succeeds. If writeFileSync/renameSync throws, dirty remains
889
+ * true so the next save() retries — prevents silent data loss on transient
890
+ * disk errors. writeFileSync is sync in Node.js so there's no interleaving
891
+ * risk within a single save() call; the snapshot mainly protects against
892
+ * future refactors to async I/O.
893
+ */
894
+ save() {
895
+ if (!this.dirty) return;
896
+ if (this._saveTimer) {
897
+ clearTimeout(this._saveTimer);
898
+ this._saveTimer = void 0;
899
+ }
900
+ const snapshot = {
901
+ version: 1,
902
+ records: [...this.records]
903
+ // shallow copy — records are plain data
904
+ };
905
+ try {
906
+ atomicWriteFileSync(this.filePath, JSON.stringify(snapshot, null, 2));
907
+ this.dirty = false;
908
+ } catch (err) {
909
+ console.error("[cost-tracker] Failed to persist cost history:", err);
910
+ }
911
+ }
912
+ /**
913
+ * Record cost from a completed session/interaction.
914
+ *
915
+ * 返回本次记账的美元金额(定价未知时返回 null)。调用方据此累计**按调用时刻**
916
+ * 算出的会话成本——2026-08-14 审计 P3-01:DeepSeek V4 自 8/17 起峰谷计价,
917
+ * 展示层若拿聚合用量在「查看时刻」现算,闲时跑的会话在峰时看会被按峰价显示,
918
+ * 跨时段会话更是整段按一个费率算。记账时刻的值才是真实花费。
919
+ */
920
+ addCost(provider, model, usage) {
921
+ const priced = computeCost(provider, model, usage);
922
+ const cost = priced ?? 0;
923
+ const inTok = usage.inputTokens || 0;
924
+ const outTok = usage.outputTokens || 0;
925
+ if (cost === 0 && inTok === 0 && outTok === 0) return priced;
926
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
927
+ let record = this.records.find((r) => r.date === today);
928
+ if (!record) {
929
+ record = { date: today, cost: 0, sessions: 0, inputTokens: 0, outputTokens: 0 };
930
+ this.records.push(record);
931
+ }
932
+ record.cost += cost;
933
+ record.sessions += 1;
934
+ record.inputTokens += inTok;
935
+ record.outputTokens += outTok;
936
+ const key = `${provider}/${model}`;
937
+ record.byModel ??= {};
938
+ const m = record.byModel[key] ??= { cost: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, calls: 0 };
939
+ m.cost += cost;
940
+ m.inputTokens += inTok;
941
+ m.outputTokens += outTok;
942
+ m.cacheReadTokens += usage.cacheReadTokens ?? 0;
943
+ m.calls += 1;
944
+ this.dirty = true;
945
+ if (!this._saveTimer) {
946
+ this._saveTimer = setTimeout(() => {
947
+ this._saveTimer = void 0;
948
+ this.save();
949
+ }, _CostTracker.SAVE_DEBOUNCE_MS);
950
+ if (this._saveTimer && typeof this._saveTimer === "object" && "unref" in this._saveTimer) {
951
+ this._saveTimer.unref();
952
+ }
953
+ }
954
+ if (this.records.length > 90) {
955
+ this.records = this.records.slice(-90);
956
+ }
957
+ return priced;
958
+ }
959
+ /** Get total cost for a given month ("2026-04"). */
960
+ getMonthlyCost(yearMonth) {
961
+ const prefix = yearMonth ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
962
+ return this.records.filter((r) => r.date.startsWith(prefix)).reduce((sum, r) => sum + r.cost, 0);
963
+ }
964
+ /** Get today's cost. */
965
+ getTodayCost() {
966
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
967
+ return this.records.find((r) => r.date === today)?.cost ?? 0;
968
+ }
969
+ /** Get total cost for last N days. */
970
+ getRecentCost(days) {
971
+ const cutoff = /* @__PURE__ */ new Date();
972
+ cutoff.setDate(cutoff.getDate() - days);
973
+ const cutoffStr = cutoff.toISOString().slice(0, 10);
974
+ return this.records.filter((r) => r.date >= cutoffStr).reduce((sum, r) => sum + r.cost, 0);
975
+ }
976
+ /** Get all records for display. */
977
+ getRecords() {
978
+ return [...this.records];
979
+ }
980
+ /**
981
+ * Aggregate usage across a date window, grouped by provider/model (v0.4.184+).
982
+ *
983
+ * Window selection (precedence): `month` ("2026-06") > `sinceDays` (N days back,
984
+ * inclusive of today) > all tracked history. Legacy records without a byModel
985
+ * breakdown fold into a single "(unattributed)" row so totals still reconcile
986
+ * with `getMonthlyCost()` etc.
987
+ *
988
+ * @returns rows sorted by cost desc then total tokens desc, plus grand totals
989
+ * and the actual covered date range.
990
+ */
991
+ getUsageBreakdown(opts) {
992
+ let filtered;
993
+ if (opts?.month) {
994
+ filtered = this.records.filter((r) => r.date.startsWith(opts.month));
995
+ } else if (opts?.sinceDays && opts.sinceDays > 0) {
996
+ const cutoff = /* @__PURE__ */ new Date();
997
+ cutoff.setDate(cutoff.getDate() - (opts.sinceDays - 1));
998
+ const cutoffStr = cutoff.toISOString().slice(0, 10);
999
+ filtered = this.records.filter((r) => r.date >= cutoffStr);
1000
+ } else {
1001
+ filtered = [...this.records];
1002
+ }
1003
+ const acc = /* @__PURE__ */ new Map();
1004
+ const bump = (key, provider, model, v) => {
1005
+ const row = acc.get(key) ?? { key, provider, model, cost: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, calls: 0 };
1006
+ row.cost += v.cost;
1007
+ row.inputTokens += v.inputTokens;
1008
+ row.outputTokens += v.outputTokens;
1009
+ row.cacheReadTokens += v.cacheReadTokens;
1010
+ row.calls += v.calls;
1011
+ acc.set(key, row);
1012
+ };
1013
+ for (const rec of filtered) {
1014
+ if (rec.byModel && Object.keys(rec.byModel).length > 0) {
1015
+ for (const [key, m] of Object.entries(rec.byModel)) {
1016
+ const slash = key.indexOf("/");
1017
+ const provider = slash >= 0 ? key.slice(0, slash) : key;
1018
+ const model = slash >= 0 ? key.slice(slash + 1) : "";
1019
+ bump(key, provider, model, m);
1020
+ }
1021
+ } else {
1022
+ bump("(unattributed)", "(unattributed)", "", {
1023
+ cost: rec.cost,
1024
+ inputTokens: rec.inputTokens,
1025
+ outputTokens: rec.outputTokens,
1026
+ cacheReadTokens: 0,
1027
+ calls: rec.sessions
1028
+ });
1029
+ }
1030
+ }
1031
+ const rows = [...acc.values()].sort(
1032
+ (a, b) => b.cost - a.cost || b.inputTokens + b.outputTokens - (a.inputTokens + a.outputTokens)
1033
+ );
1034
+ const totals = rows.reduce(
1035
+ (t, r) => {
1036
+ t.cost += r.cost;
1037
+ t.inputTokens += r.inputTokens;
1038
+ t.outputTokens += r.outputTokens;
1039
+ t.cacheReadTokens += r.cacheReadTokens;
1040
+ t.calls += r.calls;
1041
+ return t;
1042
+ },
1043
+ { cost: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, calls: 0 }
1044
+ );
1045
+ return {
1046
+ rows,
1047
+ totals,
1048
+ fromDate: filtered.length > 0 ? filtered[0].date : null,
1049
+ toDate: filtered.length > 0 ? filtered[filtered.length - 1].date : null,
1050
+ dayCount: filtered.length
1051
+ };
1052
+ }
1053
+ /**
1054
+ * Check if monthly cost exceeds budget, return warning message or null.
1055
+ */
1056
+ checkBudget(monthlyBudget) {
1057
+ if (!monthlyBudget || monthlyBudget <= 0) return null;
1058
+ const monthlyCost = this.getMonthlyCost();
1059
+ const ratio = monthlyCost / monthlyBudget;
1060
+ if (ratio >= 1) {
1061
+ return `\u{1F6A8} Monthly budget exceeded: ${formatCost(monthlyCost)} / ${formatCost(monthlyBudget)} (${Math.round(ratio * 100)}%)`;
1062
+ }
1063
+ if (ratio >= 0.8) {
1064
+ return `\u26A0 Monthly budget warning: ${formatCost(monthlyCost)} / ${formatCost(monthlyBudget)} (${Math.round(ratio * 100)}%)`;
1065
+ }
1066
+ return null;
1067
+ }
1068
+ /**
1069
+ * Format a cost summary for display.
1070
+ */
1071
+ formatSummary(monthlyBudget) {
1072
+ const today = this.getTodayCost();
1073
+ const monthly = this.getMonthlyCost();
1074
+ const yearMonth = (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
1075
+ const lines = [
1076
+ `Today: ${formatCost(today)}`,
1077
+ `This month: ${formatCost(monthly)} (${yearMonth})`
1078
+ ];
1079
+ if (monthlyBudget && monthlyBudget > 0) {
1080
+ const ratio = monthly / monthlyBudget;
1081
+ const bar = "\u2588".repeat(Math.min(20, Math.round(ratio * 20))) + "\u2591".repeat(Math.max(0, 20 - Math.round(ratio * 20)));
1082
+ lines.push(`Budget: ${formatCost(monthlyBudget)} [${bar}] ${Math.round(ratio * 100)}%`);
1083
+ }
1084
+ const last7 = [];
1085
+ for (let i = 6; i >= 0; i--) {
1086
+ const d = /* @__PURE__ */ new Date();
1087
+ d.setDate(d.getDate() - i);
1088
+ const dateStr = d.toISOString().slice(0, 10);
1089
+ const record = this.records.find((r) => r.date === dateStr);
1090
+ const dayLabel = dateStr.slice(5);
1091
+ last7.push(`${dayLabel}: ${record ? formatCost(record.cost) : "$0.00"}`);
1092
+ }
1093
+ lines.push(`Last 7 days: ${last7.join(" ")}`);
1094
+ return lines.join("\n");
1095
+ }
1096
+ /**
1097
+ * P2-PERF-04: flush pending cost records to disk immediately.
1098
+ * Call this before process exit or before reading cost data for display
1099
+ * to ensure the in-memory buffer is reflected on disk.
1100
+ */
1101
+ flush() {
1102
+ if (this._saveTimer) {
1103
+ clearTimeout(this._saveTimer);
1104
+ this._saveTimer = void 0;
1105
+ }
1106
+ this.save();
1107
+ }
1108
+ };
1109
+
712
1110
  // src/providers/claude.ts
713
1111
  import Anthropic from "@anthropic-ai/sdk";
714
1112
 
@@ -3905,18 +4303,18 @@ var CustomProvider = class extends OpenAICompatibleProvider {
3905
4303
  };
3906
4304
 
3907
4305
  // src/providers/model-cache.ts
3908
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync } from "fs";
3909
- import { dirname, join as join2 } from "path";
4306
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync } from "fs";
4307
+ import { dirname, join as join3 } from "path";
3910
4308
  var CACHE_FILE_NAME = "model-cache.json";
3911
4309
  var LIVE_DEFAULT_CONTEXT = 128e3;
3912
4310
  function modelCachePath(configDir) {
3913
- return join2(configDir, CACHE_FILE_NAME);
4311
+ return join3(configDir, CACHE_FILE_NAME);
3914
4312
  }
3915
4313
  function loadModelCache(configDir) {
3916
4314
  const path3 = modelCachePath(configDir);
3917
- if (!existsSync2(path3)) return { version: 1, providers: {} };
4315
+ if (!existsSync3(path3)) return { version: 1, providers: {} };
3918
4316
  try {
3919
- const parsed = JSON.parse(readFileSync2(path3, "utf-8"));
4317
+ const parsed = JSON.parse(readFileSync3(path3, "utf-8"));
3920
4318
  if (parsed.version !== 1 || !parsed.providers || typeof parsed.providers !== "object") {
3921
4319
  return { version: 1, providers: {} };
3922
4320
  }
@@ -3961,7 +4359,7 @@ function setCachedModels(configDir, providerId, models, baseUrl) {
3961
4359
  function clearModelCache(configDir, providerId) {
3962
4360
  const path3 = modelCachePath(configDir);
3963
4361
  if (!providerId) {
3964
- if (existsSync2(path3)) unlinkSync(path3);
4362
+ if (existsSync3(path3)) unlinkSync(path3);
3965
4363
  return;
3966
4364
  }
3967
4365
  const cache = loadModelCache(configDir);
@@ -4152,8 +4550,8 @@ var ProviderRegistry = class {
4152
4550
  };
4153
4551
 
4154
4552
  // src/session/session-manager.ts
4155
- import { readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync, unlinkSync as unlinkSync2, statSync } from "fs";
4156
- import { join as join3 } from "path";
4553
+ import { readFileSync as readFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync, unlinkSync as unlinkSync2, statSync } from "fs";
4554
+ import { join as join4 } from "path";
4157
4555
  import { v4 as uuidv4 } from "uuid";
4158
4556
 
4159
4557
  // src/core/types.ts
@@ -4738,7 +5136,7 @@ var SessionManager = class {
4738
5136
  if (!this._current) return;
4739
5137
  if (!force && !this._current.dirty) return;
4740
5138
  mkdirSync3(this.historyDir, { recursive: true });
4741
- const filePath = join3(this.historyDir, `${this._current.id}.json`);
5139
+ const filePath = join4(this.historyDir, `${this._current.id}.json`);
4742
5140
  const raw = this._current.toJSON();
4743
5141
  const opts = this.redactOptionsForSave();
4744
5142
  const { value: payload, hits } = redactJson(raw, opts);
@@ -4759,13 +5157,13 @@ var SessionManager = class {
4759
5157
  }
4760
5158
  }
4761
5159
  loadSession(id) {
4762
- const filePath = join3(this.historyDir, `${id}.json`);
4763
- if (!existsSync3(filePath)) {
5160
+ const filePath = join4(this.historyDir, `${id}.json`);
5161
+ if (!existsSync4(filePath)) {
4764
5162
  throw new Error(`Session ${id} not found`);
4765
5163
  }
4766
5164
  let data;
4767
5165
  try {
4768
- data = JSON.parse(readFileSync3(filePath, "utf-8"));
5166
+ data = JSON.parse(readFileSync4(filePath, "utf-8"));
4769
5167
  } catch (err) {
4770
5168
  throw new Error(`Session ${id} is corrupted: ${err instanceof Error ? err.message : String(err)}`);
4771
5169
  }
@@ -4774,7 +5172,7 @@ var SessionManager = class {
4774
5172
  return session;
4775
5173
  }
4776
5174
  listSessions(opts) {
4777
- if (!existsSync3(this.historyDir)) return [];
5175
+ if (!existsSync4(this.historyDir)) return [];
4778
5176
  try {
4779
5177
  return listFromIndex(this.historyDir, opts);
4780
5178
  } catch {
@@ -4783,7 +5181,7 @@ var SessionManager = class {
4783
5181
  const metas = [];
4784
5182
  for (const file of files) {
4785
5183
  try {
4786
- const filePath = join3(this.historyDir, file);
5184
+ const filePath = join4(this.historyDir, file);
4787
5185
  const meta = readSessionMeta(filePath);
4788
5186
  if (!meta) continue;
4789
5187
  if (opts?.includeFileSize) {
@@ -4803,8 +5201,8 @@ var SessionManager = class {
4803
5201
  return metas.sort((a, b) => b.updated.getTime() - a.updated.getTime());
4804
5202
  }
4805
5203
  deleteSession(id) {
4806
- const filePath = join3(this.historyDir, `${id}.json`);
4807
- if (!existsSync3(filePath)) return false;
5204
+ const filePath = join4(this.historyDir, `${id}.json`);
5205
+ if (!existsSync4(filePath)) return false;
4808
5206
  try {
4809
5207
  unlinkSync2(filePath);
4810
5208
  if (this._current && this._current.id === id) {
@@ -4845,20 +5243,20 @@ var SessionManager = class {
4845
5243
  * 每个 session 最多返回 3 条匹配片段,全局最多 maxResults 个 session。
4846
5244
  */
4847
5245
  searchMessages(query, maxResults = 20) {
4848
- if (!existsSync3(this.historyDir)) return [];
5246
+ if (!existsSync4(this.historyDir)) return [];
4849
5247
  const q = query.toLowerCase();
4850
5248
  const candidateIds = searchCandidates(this.historyDir, q);
4851
5249
  let filesToSearch;
4852
5250
  if (candidateIds !== null) {
4853
- filesToSearch = candidateIds.map((id) => join3(this.historyDir, `${id}.json`)).filter((fp) => existsSync3(fp));
5251
+ filesToSearch = candidateIds.map((id) => join4(this.historyDir, `${id}.json`)).filter((fp) => existsSync4(fp));
4854
5252
  } else {
4855
- filesToSearch = readdirSync(this.historyDir).filter((f) => f.endsWith(".json")).map((f) => join3(this.historyDir, f));
5253
+ filesToSearch = readdirSync(this.historyDir).filter((f) => f.endsWith(".json")).map((f) => join4(this.historyDir, f));
4856
5254
  }
4857
5255
  const results = [];
4858
5256
  for (const filePath of filesToSearch) {
4859
5257
  if (results.length >= maxResults) break;
4860
5258
  try {
4861
- const data = JSON.parse(readFileSync3(filePath, "utf-8"));
5259
+ const data = JSON.parse(readFileSync4(filePath, "utf-8"));
4862
5260
  const messages = data.messages ?? [];
4863
5261
  const matches = [];
4864
5262
  for (const msg of messages) {
@@ -4905,12 +5303,12 @@ var SessionManager = class {
4905
5303
 
4906
5304
  // src/tools/builtin/bash.ts
4907
5305
  import { spawn as spawn3 } from "child_process";
4908
- import { existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
5306
+ import { existsSync as existsSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
4909
5307
  import { platform as platform3 } from "os";
4910
5308
  import { resolve } from "path";
4911
5309
 
4912
5310
  // src/tools/undo-stack.ts
4913
- import { readFileSync as readFileSync4, writeFileSync, unlinkSync as unlinkSync3, rmdirSync, existsSync as existsSync4 } from "fs";
5311
+ import { readFileSync as readFileSync5, writeFileSync, unlinkSync as unlinkSync3, rmdirSync, existsSync as existsSync5 } from "fs";
4914
5312
  var MAX_UNDO_DEPTH = 20;
4915
5313
  var UndoStack = class {
4916
5314
  stack = [];
@@ -4921,9 +5319,9 @@ var UndoStack = class {
4921
5319
  */
4922
5320
  push(filePath, description) {
4923
5321
  let previousContent = null;
4924
- if (existsSync4(filePath)) {
5322
+ if (existsSync5(filePath)) {
4925
5323
  try {
4926
- previousContent = readFileSync4(filePath, "utf-8");
5324
+ previousContent = readFileSync5(filePath, "utf-8");
4927
5325
  } catch {
4928
5326
  return;
4929
5327
  }
@@ -4977,7 +5375,7 @@ var UndoStack = class {
4977
5375
  try {
4978
5376
  if (entry.previousContent === null) {
4979
5377
  if (entry.isDirectory) {
4980
- if (existsSync4(entry.filePath)) {
5378
+ if (existsSync5(entry.filePath)) {
4981
5379
  try {
4982
5380
  rmdirSync(entry.filePath);
4983
5381
  return { entry, result: `Removed newly created directory: ${entry.filePath}` };
@@ -4987,7 +5385,7 @@ var UndoStack = class {
4987
5385
  }
4988
5386
  return { entry, result: `Directory already removed: ${entry.filePath}` };
4989
5387
  } else {
4990
- if (existsSync4(entry.filePath)) {
5388
+ if (existsSync5(entry.filePath)) {
4991
5389
  unlinkSync3(entry.filePath);
4992
5390
  }
4993
5391
  return { entry, result: `Deleted newly created file: ${entry.filePath}` };
@@ -5378,7 +5776,7 @@ Important rules:
5378
5776
  throw new ToolError("bash", blockingHint);
5379
5777
  }
5380
5778
  let currentCwd = getCwd();
5381
- if (!existsSync5(currentCwd)) {
5779
+ if (!existsSync6(currentCwd)) {
5382
5780
  const fallback = process.cwd();
5383
5781
  process.stderr.write(
5384
5782
  `[bash] Previous cwd "${currentCwd}" no longer exists, reset to "${fallback}"
@@ -5390,7 +5788,7 @@ Important rules:
5390
5788
  let effectiveCwd = currentCwd;
5391
5789
  if (cwdArg) {
5392
5790
  const resolved = resolve(currentCwd, cwdArg);
5393
- if (!existsSync5(resolved)) {
5791
+ if (!existsSync6(resolved)) {
5394
5792
  throw new ToolError(
5395
5793
  "bash",
5396
5794
  `cwd directory does not exist: "${resolved}". Create it first (e.g. mkdir -p "${resolved}") before specifying it as cwd.`
@@ -5410,7 +5808,7 @@ Important rules:
5410
5808
  const parsedTargets = parseCreationTargets(command, effectiveCwd);
5411
5809
  const parsedTargetsBefore = /* @__PURE__ */ new Map();
5412
5810
  for (const t of parsedTargets) {
5413
- parsedTargetsBefore.set(t, existsSync5(t));
5811
+ parsedTargetsBefore.set(t, existsSync6(t));
5414
5812
  }
5415
5813
  let poolResult = null;
5416
5814
  try {
@@ -5707,7 +6105,7 @@ function pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, cwd) {
5707
6105
  }
5708
6106
  }
5709
6107
  for (const [target, existedBefore] of parsedTargetsBefore) {
5710
- if (!existedBefore && !tracked.has(target) && existsSync5(target)) {
6108
+ if (!existedBefore && !tracked.has(target) && existsSync6(target)) {
5711
6109
  try {
5712
6110
  const st = statSync2(target);
5713
6111
  if (st.isDirectory()) {
@@ -5794,7 +6192,7 @@ function updateCwdFromCommand(command, baseCwd) {
5794
6192
  if (!target || target.startsWith("$") || target === "~") return;
5795
6193
  try {
5796
6194
  const newDir = resolve(baseCwd, target);
5797
- if (existsSync5(newDir)) {
6195
+ if (existsSync6(newDir)) {
5798
6196
  setCwd(newDir);
5799
6197
  }
5800
6198
  } catch {
@@ -5802,7 +6200,7 @@ function updateCwdFromCommand(command, baseCwd) {
5802
6200
  }
5803
6201
 
5804
6202
  // src/tools/builtin/read-file.ts
5805
- import { readFileSync as readFileSync5, existsSync as existsSync6, statSync as statSync3, readdirSync as readdirSync3 } from "fs";
6203
+ import { readFileSync as readFileSync6, existsSync as existsSync7, statSync as statSync3, readdirSync as readdirSync3 } from "fs";
5806
6204
  import { execFileSync } from "child_process";
5807
6205
  import { extname, resolve as resolve2, basename, sep, dirname as dirname2 } from "path";
5808
6206
  import { homedir as homedir2 } from "os";
@@ -6079,7 +6477,7 @@ var readFileTool = {
6079
6477
  const normalizedPath = resolve2(filePath);
6080
6478
  const refusal = getHardRefusal(normalizedPath);
6081
6479
  if (refusal) return refusal;
6082
- if (!existsSync6(normalizedPath)) {
6480
+ if (!existsSync7(normalizedPath)) {
6083
6481
  const suggestions = findSimilarFiles(filePath);
6084
6482
  if (suggestions.length > 0) {
6085
6483
  throw new ToolError(
@@ -6130,7 +6528,7 @@ ${pdfText}`;
6130
6528
  }
6131
6529
  const dir = dirname2(normalizedPath);
6132
6530
  const nameNoExt = basename(normalizedPath, ext);
6133
- const textAlts = [".md", ".txt", ".html"].map((e) => resolve2(dir, nameNoExt + e)).filter(existsSync6);
6531
+ const textAlts = [".md", ".txt", ".html"].map((e) => resolve2(dir, nameNoExt + e)).filter(existsSync7);
6134
6532
  if (textAlts.length > 0) {
6135
6533
  return `[PDF file: ${filePath}]
6136
6534
  Cannot extract text from this PDF, but found alternative text versions:
@@ -6143,7 +6541,7 @@ Suggestion: read existing text versions (.md / .txt) in the project, or install
6143
6541
  }
6144
6542
  if (ext === ".ipynb") {
6145
6543
  try {
6146
- const raw = readFileSync5(normalizedPath, "utf-8");
6544
+ const raw = readFileSync6(normalizedPath, "utf-8");
6147
6545
  const nb = parseNotebook(raw);
6148
6546
  return renderNotebookAsText(nb, filePath);
6149
6547
  } catch (err) {
@@ -6156,7 +6554,7 @@ ${err.message}`;
6156
6554
  This is a binary file and cannot be read as text. If there is a text version (.md / .txt) in the project, please read that instead.`;
6157
6555
  }
6158
6556
  const { readFile: readFile2 } = await import("fs/promises");
6159
- const buf = size > 1048576 ? await readFile2(normalizedPath) : readFileSync5(normalizedPath);
6557
+ const buf = size > 1048576 ? await readFile2(normalizedPath) : readFileSync6(normalizedPath);
6160
6558
  if (encoding === "base64") {
6161
6559
  return `[File: ${filePath} | base64]
6162
6560
 
@@ -6194,12 +6592,12 @@ ${content}`;
6194
6592
  };
6195
6593
 
6196
6594
  // src/tools/builtin/write-file.ts
6197
- import { appendFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync10, statSync as statSync4 } from "fs";
6595
+ import { appendFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync11, statSync as statSync4 } from "fs";
6198
6596
  import { dirname as dirname5 } from "path";
6199
6597
 
6200
6598
  // src/tools/executor.ts
6201
6599
  import chalk3 from "chalk";
6202
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
6600
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
6203
6601
  import { tmpdir } from "os";
6204
6602
 
6205
6603
  // src/core/readline-internal.ts
@@ -6439,8 +6837,8 @@ function simpleDiff(oldLines, newLines) {
6439
6837
  // src/tools/hooks.ts
6440
6838
  import { execSync } from "child_process";
6441
6839
  import { createHash } from "crypto";
6442
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6 } from "fs";
6443
- import { join as join4 } from "path";
6840
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7 } from "fs";
6841
+ import { join as join5 } from "path";
6444
6842
  var TRUST_FILE = "hooks-trust.json";
6445
6843
  var DEFAULT_TIMEOUT_MS = 5e3;
6446
6844
  function asArray(entry) {
@@ -6449,13 +6847,13 @@ function asArray(entry) {
6449
6847
  return raw.map((item) => typeof item === "string" ? { command: item } : item);
6450
6848
  }
6451
6849
  function hookTrustPath(configDir) {
6452
- return join4(configDir, TRUST_FILE);
6850
+ return join5(configDir, TRUST_FILE);
6453
6851
  }
6454
6852
  function loadHookTrustStore(configDir) {
6455
6853
  const file = hookTrustPath(configDir);
6456
- if (!existsSync7(file)) return { records: [] };
6854
+ if (!existsSync8(file)) return { records: [] };
6457
6855
  try {
6458
- const parsed = JSON.parse(readFileSync6(file, "utf-8"));
6856
+ const parsed = JSON.parse(readFileSync7(file, "utf-8"));
6459
6857
  return { records: Array.isArray(parsed.records) ? parsed.records : [] };
6460
6858
  } catch {
6461
6859
  return { records: [] };
@@ -6599,7 +6997,7 @@ function runHook(template, vars) {
6599
6997
  }
6600
6998
 
6601
6999
  // src/tools/permissions.ts
6602
- import { existsSync as existsSync8, realpathSync } from "fs";
7000
+ import { existsSync as existsSync9, realpathSync } from "fs";
6603
7001
  import { basename as basename2, dirname as dirname3, isAbsolute, resolve as resolve3 } from "path";
6604
7002
  function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "confirm") {
6605
7003
  for (const rule of rules) {
@@ -6648,7 +7046,7 @@ function normalizePathForPermission(value) {
6648
7046
  function canonicalizeWithNearestParent(value) {
6649
7047
  const missing = [];
6650
7048
  let cursor = resolve3(value);
6651
- while (!existsSync8(cursor)) {
7049
+ while (!existsSync9(cursor)) {
6652
7050
  const parent = dirname3(cursor);
6653
7051
  if (parent === cursor) return null;
6654
7052
  missing.unshift(basename2(cursor));
@@ -7013,8 +7411,8 @@ var theme = new Proxy(DARK_THEME, {
7013
7411
  });
7014
7412
 
7015
7413
  // src/diagnostics/tool-stats.ts
7016
- import { existsSync as existsSync9, readFileSync as readFileSync7, mkdirSync as mkdirSync5 } from "fs";
7017
- import { join as join5, dirname as dirname4 } from "path";
7414
+ import { existsSync as existsSync10, readFileSync as readFileSync8, mkdirSync as mkdirSync5 } from "fs";
7415
+ import { join as join6, dirname as dirname4 } from "path";
7018
7416
  import { homedir as homedir3 } from "os";
7019
7417
  var STATS_FILE_NAME = "tool-stats.json";
7020
7418
  var STATS_VERSION = 1;
@@ -7024,16 +7422,16 @@ var dirty = false;
7024
7422
  var pendingWrites = 0;
7025
7423
  var configDirOverride;
7026
7424
  function statsFilePath() {
7027
- const base = configDirOverride ?? join5(homedir3(), CONFIG_DIR_NAME);
7028
- return join5(base, STATS_FILE_NAME);
7425
+ const base = configDirOverride ?? join6(homedir3(), CONFIG_DIR_NAME);
7426
+ return join6(base, STATS_FILE_NAME);
7029
7427
  }
7030
7428
  function load() {
7031
7429
  const path3 = statsFilePath();
7032
- if (!existsSync9(path3)) {
7430
+ if (!existsSync10(path3)) {
7033
7431
  return { version: STATS_VERSION, startedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: {} };
7034
7432
  }
7035
7433
  try {
7036
- const raw = JSON.parse(readFileSync7(path3, "utf-8"));
7434
+ const raw = JSON.parse(readFileSync8(path3, "utf-8"));
7037
7435
  if (raw.version !== STATS_VERSION || !raw.entries) {
7038
7436
  return { version: STATS_VERSION, startedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: {} };
7039
7437
  }
@@ -7115,8 +7513,8 @@ function getTopUsedTools(limit = 5) {
7115
7513
  }
7116
7514
 
7117
7515
  // src/tools/action-classifier.ts
7118
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
7119
- import { isAbsolute as isAbsolute2, join as join6, resolve as resolve4 } from "path";
7516
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
7517
+ import { isAbsolute as isAbsolute2, join as join7, resolve as resolve4 } from "path";
7120
7518
  var RECENT_DENIED_LIMIT = 50;
7121
7519
  var recentlyDenied = [];
7122
7520
  function recordRecentlyDeniedAutoAction(call, classification) {
@@ -7157,10 +7555,10 @@ function splitShellWords(command) {
7157
7555
  return out.filter(Boolean);
7158
7556
  }
7159
7557
  function readPackageManifest(root) {
7160
- const p = join6(root, "package.json");
7161
- if (!existsSync10(p)) return null;
7558
+ const p = join7(root, "package.json");
7559
+ if (!existsSync11(p)) return null;
7162
7560
  try {
7163
- return JSON.parse(readFileSync8(p, "utf-8"));
7561
+ return JSON.parse(readFileSync9(p, "utf-8"));
7164
7562
  } catch {
7165
7563
  return null;
7166
7564
  }
@@ -7176,10 +7574,10 @@ function manifestHasDependency(root, name) {
7176
7574
  }
7177
7575
  function lockfileMentions(root, name) {
7178
7576
  for (const file of ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"]) {
7179
- const p = join6(root, file);
7180
- if (!existsSync10(p)) continue;
7577
+ const p = join7(root, file);
7578
+ if (!existsSync11(p)) continue;
7181
7579
  try {
7182
- if (readFileSync8(p, "utf-8").includes(name)) return true;
7580
+ if (readFileSync9(p, "utf-8").includes(name)) return true;
7183
7581
  } catch {
7184
7582
  }
7185
7583
  }
@@ -7753,10 +8151,10 @@ var ToolExecutor = class {
7753
8151
  if (call.name === "write_file") {
7754
8152
  const { path: filePath, content: chunk, append } = resolveWriteFileArgs(call.arguments);
7755
8153
  if (!filePath) return;
7756
- if (existsSync11(filePath)) {
8154
+ if (existsSync12(filePath)) {
7757
8155
  let oldContent;
7758
8156
  try {
7759
- oldContent = readFileSync9(filePath, "utf-8");
8157
+ oldContent = readFileSync10(filePath, "utf-8");
7760
8158
  } catch {
7761
8159
  return;
7762
8160
  }
@@ -7780,7 +8178,7 @@ var ToolExecutor = class {
7780
8178
  }
7781
8179
  } else if (call.name === "edit_file") {
7782
8180
  const filePath = String(call.arguments["path"] ?? "");
7783
- if (!filePath || !existsSync11(filePath)) return;
8181
+ if (!filePath || !existsSync12(filePath)) return;
7784
8182
  const oldStr = call.arguments["old_str"];
7785
8183
  const newStr = call.arguments["new_str"];
7786
8184
  if (oldStr !== void 0) {
@@ -7807,7 +8205,7 @@ var ToolExecutor = class {
7807
8205
  const to = Number(call.arguments["delete_to_line"] ?? from);
7808
8206
  let fileContent;
7809
8207
  try {
7810
- fileContent = readFileSync9(filePath, "utf-8");
8208
+ fileContent = readFileSync10(filePath, "utf-8");
7811
8209
  } catch {
7812
8210
  return;
7813
8211
  }
@@ -7993,7 +8391,7 @@ var subAgentGuard = {
7993
8391
 
7994
8392
  // src/tools/builtin/write-file.ts
7995
8393
  function countFileLines(filePath) {
7996
- return readFileSync10(filePath, "utf-8").split("\n").length;
8394
+ return readFileSync11(filePath, "utf-8").split("\n").length;
7997
8395
  }
7998
8396
  var writeFileTool = {
7999
8397
  definition: {
@@ -8076,7 +8474,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
8076
8474
  };
8077
8475
 
8078
8476
  // src/tools/builtin/edit-file.ts
8079
- import { readFileSync as readFileSync11, existsSync as existsSync12 } from "fs";
8477
+ import { readFileSync as readFileSync12, existsSync as existsSync13 } from "fs";
8080
8478
 
8081
8479
  // src/tools/builtin/patch-apply.ts
8082
8480
  function parseUnifiedDiff(patch) {
@@ -8444,7 +8842,7 @@ Note: Path can be absolute or relative to cwd.`,
8444
8842
  const filePath = String(args["path"] ?? args["file"] ?? "");
8445
8843
  const encoding = args["encoding"] ?? "utf-8";
8446
8844
  if (!filePath) throw new ToolError("edit_file", "path is required");
8447
- if (!existsSync12(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
8845
+ if (!existsSync13(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
8448
8846
  const verdict = classifyWritePath(filePath);
8449
8847
  if (verdict.sensitive && subAgentGuard.active) {
8450
8848
  throw new ToolError(
@@ -8452,7 +8850,7 @@ Note: Path can be absolute or relative to cwd.`,
8452
8850
  `Refused: sub-agents cannot edit sensitive paths \u2014 ${verdict.reason}. If this is genuinely needed, ask the parent agent to perform the edit so the user can approve it.`
8453
8851
  );
8454
8852
  }
8455
- const original = readFileSync11(filePath, encoding);
8853
+ const original = readFileSync12(filePath, encoding);
8456
8854
  if (args["patch"] !== void 0) {
8457
8855
  const patchText = String(args["patch"] ?? "");
8458
8856
  const stopOnError = args["stop_on_error"] !== false;
@@ -8623,8 +9021,8 @@ function truncatePreview(str, maxLen = 80) {
8623
9021
  }
8624
9022
 
8625
9023
  // src/tools/builtin/list-dir.ts
8626
- import { readdirSync as readdirSync4, statSync as statSync5, existsSync as existsSync13 } from "fs";
8627
- import { join as join7, basename as basename4 } from "path";
9024
+ import { readdirSync as readdirSync4, statSync as statSync5, existsSync as existsSync14 } from "fs";
9025
+ import { join as join8, basename as basename4 } from "path";
8628
9026
  var listDirTool = {
8629
9027
  definition: {
8630
9028
  name: "list_dir",
@@ -8646,7 +9044,7 @@ var listDirTool = {
8646
9044
  async execute(args) {
8647
9045
  const dirPath = String(args["path"] ?? process.cwd());
8648
9046
  const recursive = Boolean(args["recursive"] ?? false);
8649
- if (!existsSync13(dirPath)) {
9047
+ if (!existsSync14(dirPath)) {
8650
9048
  const targetName = basename4(dirPath).toLowerCase();
8651
9049
  const cwd = process.cwd();
8652
9050
  const suggestions = [];
@@ -8704,11 +9102,11 @@ function listRecursive(basePath, indent, recursive, lines) {
8704
9102
  if (entry.isDirectory()) {
8705
9103
  lines.push(`${indent}\u{1F4C1} ${entry.name}/`);
8706
9104
  if (recursive) {
8707
- listRecursive(join7(basePath, entry.name), indent + " ", true, lines);
9105
+ listRecursive(join8(basePath, entry.name), indent + " ", true, lines);
8708
9106
  }
8709
9107
  } else {
8710
9108
  try {
8711
- const stat = statSync5(join7(basePath, entry.name));
9109
+ const stat = statSync5(join8(basePath, entry.name));
8712
9110
  const size = formatSize(stat.size);
8713
9111
  lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
8714
9112
  } catch {
@@ -8724,9 +9122,9 @@ function formatSize(bytes) {
8724
9122
  }
8725
9123
 
8726
9124
  // src/tools/builtin/grep-files.ts
8727
- import { readdirSync as readdirSync5, readFileSync as readFileSync12, statSync as statSync6, existsSync as existsSync14 } from "fs";
9125
+ import { readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync6, existsSync as existsSync15 } from "fs";
8728
9126
  import { readFile } from "fs/promises";
8729
- import { join as join8, relative } from "path";
9127
+ import { join as join9, relative } from "path";
8730
9128
  var grepFilesTool = {
8731
9129
  definition: {
8732
9130
  name: "grep_files",
@@ -8777,7 +9175,7 @@ Supports regex. Automatically skips node_modules, dist, .git directories.`,
8777
9175
  const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
8778
9176
  const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
8779
9177
  if (!pattern) throw new ToolError("grep_files", "pattern is required");
8780
- if (!existsSync14(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
9178
+ if (!existsSync15(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
8781
9179
  const MAX_PATTERN_LENGTH = 1e3;
8782
9180
  if (pattern.length > MAX_PATTERN_LENGTH) {
8783
9181
  throw new ToolError("grep_files", `Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
@@ -8871,11 +9269,11 @@ function collectFilePaths(rootPath, filePattern, maxFiles) {
8871
9269
  if (paths.length >= maxFiles) return;
8872
9270
  if (entry.isDirectory()) {
8873
9271
  if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
8874
- walk(join8(dirPath, entry.name));
9272
+ walk(join9(dirPath, entry.name));
8875
9273
  } else if (entry.isFile()) {
8876
9274
  if (isBinary(entry.name)) continue;
8877
9275
  if (filePattern && !matchesFilePattern(entry.name, filePattern)) continue;
8878
- const fullPath = join8(dirPath, entry.name);
9276
+ const fullPath = join9(dirPath, entry.name);
8879
9277
  try {
8880
9278
  if (statSync6(fullPath).size > 1e6) continue;
8881
9279
  } catch {
@@ -8930,7 +9328,7 @@ function searchInFile(fullPath, displayPath2, regex, contextLines, maxResults, r
8930
9328
  }
8931
9329
  let content;
8932
9330
  try {
8933
- content = readFileSync12(fullPath, "utf-8");
9331
+ content = readFileSync13(fullPath, "utf-8");
8934
9332
  } catch {
8935
9333
  return;
8936
9334
  }
@@ -8961,8 +9359,8 @@ function searchInFile(fullPath, displayPath2, regex, contextLines, maxResults, r
8961
9359
  }
8962
9360
 
8963
9361
  // src/tools/builtin/glob-files.ts
8964
- import { readdirSync as readdirSync6, statSync as statSync7, existsSync as existsSync15 } from "fs";
8965
- import { join as join9, relative as relative2, basename as basename5 } from "path";
9362
+ import { readdirSync as readdirSync6, statSync as statSync7, existsSync as existsSync16 } from "fs";
9363
+ import { join as join10, relative as relative2, basename as basename5 } from "path";
8966
9364
  var globFilesTool = {
8967
9365
  definition: {
8968
9366
  name: "glob_files",
@@ -8995,7 +9393,7 @@ Results sorted by most recent modification time. Automatically skips node_module
8995
9393
  const rootPath = String(args["path"] ?? process.cwd());
8996
9394
  const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
8997
9395
  if (!pattern) throw new ToolError("glob_files", "pattern is required");
8998
- if (!existsSync15(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
9396
+ if (!existsSync16(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
8999
9397
  const regex = globToRegex(pattern);
9000
9398
  const matches = [];
9001
9399
  collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
@@ -9072,7 +9470,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
9072
9470
  }
9073
9471
  for (const entry of entries) {
9074
9472
  if (results.length >= maxResults) break;
9075
- const fullPath = join9(dirPath, entry.name);
9473
+ const fullPath = join10(dirPath, entry.name);
9076
9474
  if (entry.isDirectory()) {
9077
9475
  if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
9078
9476
  collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
@@ -9914,10 +10312,10 @@ Any of these triggers means use save_last_response, NOT write_file:
9914
10312
  };
9915
10313
 
9916
10314
  // src/tools/builtin/save-memory.ts
9917
- import { join as join10 } from "path";
10315
+ import { join as join11 } from "path";
9918
10316
  import { homedir as homedir5 } from "os";
9919
10317
  function getConfigDir() {
9920
- return join10(homedir5(), CONFIG_DIR_NAME);
10318
+ return join11(homedir5(), CONFIG_DIR_NAME);
9921
10319
  }
9922
10320
  function parseSensitivity(value) {
9923
10321
  return value === "low" || value === "medium" || value === "high" ? value : void 0;
@@ -10266,13 +10664,13 @@ function formatResults2(query, data, _requested) {
10266
10664
  }
10267
10665
 
10268
10666
  // src/agents/agent-config.ts
10269
- import { existsSync as existsSync17, readdirSync as readdirSync8, readFileSync as readFileSync14 } from "fs";
10270
- import { join as join12 } from "path";
10667
+ import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync15 } from "fs";
10668
+ import { join as join13 } from "path";
10271
10669
  import { homedir as homedir6 } from "os";
10272
10670
 
10273
10671
  // src/plugins/plugin-manager.ts
10274
- import { existsSync as existsSync16, mkdirSync as mkdirSync8, readdirSync as readdirSync7, readFileSync as readFileSync13, realpathSync as realpathSync2, cpSync, rmSync, statSync as statSync8 } from "fs";
10275
- import { basename as basename6, dirname as dirname7, join as join11, relative as relative3, resolve as resolve6, sep as sep3 } from "path";
10672
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, readdirSync as readdirSync7, readFileSync as readFileSync14, realpathSync as realpathSync2, cpSync, rmSync, statSync as statSync8 } from "fs";
10673
+ import { basename as basename6, dirname as dirname7, join as join12, relative as relative3, resolve as resolve6, sep as sep3 } from "path";
10276
10674
  import { createHash as createHash2 } from "crypto";
10277
10675
  import { z as z2 } from "zod";
10278
10676
  var MANIFEST_RELATIVE = ".aicli-plugin/plugin.json";
@@ -10332,19 +10730,19 @@ var PluginManifestSchema = z2.object({
10332
10730
  permissionHints: z2.array(z2.string()).default([])
10333
10731
  }).strict();
10334
10732
  function pluginRoot(configDir) {
10335
- return join11(configDir, PLUGINS_DIR_NAME);
10733
+ return join12(configDir, PLUGINS_DIR_NAME);
10336
10734
  }
10337
10735
  function pluginManifestPath(pluginDir) {
10338
- return join11(pluginDir, MANIFEST_RELATIVE);
10736
+ return join12(pluginDir, MANIFEST_RELATIVE);
10339
10737
  }
10340
10738
  function statePath(configDir) {
10341
- return join11(pluginRoot(configDir), STATE_FILE);
10739
+ return join12(pluginRoot(configDir), STATE_FILE);
10342
10740
  }
10343
10741
  function loadState(configDir) {
10344
10742
  const file = statePath(configDir);
10345
- if (!existsSync16(file)) return { version: 2, plugins: [] };
10743
+ if (!existsSync17(file)) return { version: 2, plugins: [] };
10346
10744
  try {
10347
- const parsed = JSON.parse(readFileSync13(file, "utf-8"));
10745
+ const parsed = JSON.parse(readFileSync14(file, "utf-8"));
10348
10746
  const rawPlugins = Array.isArray(parsed.plugins) ? parsed.plugins : [];
10349
10747
  return { version: 2, plugins: rawPlugins.map((p) => migrateEntry(p)) };
10350
10748
  } catch {
@@ -10402,7 +10800,7 @@ function extractCommandPaths(command, pluginDir) {
10402
10800
  try {
10403
10801
  const abs = resolve6(resolvedDir, ref);
10404
10802
  if (!abs.startsWith(resolvedDir + sep3)) continue;
10405
- if (existsSync16(abs)) {
10803
+ if (existsSync17(abs)) {
10406
10804
  const real = realpathSync2(abs);
10407
10805
  if (!real.startsWith(resolvedDir + sep3)) continue;
10408
10806
  valid.push(relative3(resolvedDir, real));
@@ -10434,8 +10832,8 @@ function hashPluginContent(pluginDir, manifest) {
10434
10832
  }
10435
10833
  const sorted = [...assetRels].sort();
10436
10834
  const hasher = createHash2("sha256");
10437
- const manifestPath = join11(resolvedDir, MANIFEST_RELATIVE);
10438
- hasher.update(normalizeForHash(readFileSync13(manifestPath)));
10835
+ const manifestPath = join12(resolvedDir, MANIFEST_RELATIVE);
10836
+ hasher.update(normalizeForHash(readFileSync14(manifestPath)));
10439
10837
  for (const rel of sorted) {
10440
10838
  const abs = resolve6(resolvedDir, rel);
10441
10839
  if (!abs.startsWith(resolvedDir + sep3)) {
@@ -10455,7 +10853,7 @@ function hashPluginContent(pluginDir, manifest) {
10455
10853
  if (!realPath.startsWith(resolvedDir + sep3)) {
10456
10854
  throw new Error(`plugin asset symlink escapes plugin dir: ${rel} \u2192 ${realPath}`);
10457
10855
  }
10458
- hasher.update(normalizeForHash(readFileSync13(abs)));
10856
+ hasher.update(normalizeForHash(readFileSync14(abs)));
10459
10857
  }
10460
10858
  return hasher.digest("hex");
10461
10859
  }
@@ -10463,22 +10861,22 @@ function effectiveHash(entry) {
10463
10861
  return entry.contentHash ?? entry.hash ?? "";
10464
10862
  }
10465
10863
  function readPluginManifest(manifestPath) {
10466
- return PluginManifestSchema.parse(JSON.parse(readFileSync13(manifestPath, "utf-8")));
10864
+ return PluginManifestSchema.parse(JSON.parse(readFileSync14(manifestPath, "utf-8")));
10467
10865
  }
10468
10866
  function findPluginSourceDir(inputPath) {
10469
10867
  const abs = resolve6(inputPath);
10470
10868
  const direct = statSync8(abs).isDirectory() ? abs : dirname7(abs);
10471
- if (existsSync16(pluginManifestPath(direct))) return direct;
10472
- const nested = join11(direct, ".aicli-plugin");
10473
- if (existsSync16(join11(nested, "plugin.json"))) return direct;
10869
+ if (existsSync17(pluginManifestPath(direct))) return direct;
10870
+ const nested = join12(direct, ".aicli-plugin");
10871
+ if (existsSync17(join12(nested, "plugin.json"))) return direct;
10474
10872
  throw new Error(`plugin manifest not found: ${MANIFEST_RELATIVE}`);
10475
10873
  }
10476
10874
  function installPlugin(configDir, inputPath) {
10477
10875
  const sourceDir = findPluginSourceDir(inputPath);
10478
10876
  const manifest = readPluginManifest(pluginManifestPath(sourceDir));
10479
- const targetDir = join11(pluginRoot(configDir), manifest.name);
10877
+ const targetDir = join12(pluginRoot(configDir), manifest.name);
10480
10878
  mkdirSync8(pluginRoot(configDir), { recursive: true });
10481
- if (existsSync16(targetDir)) rmSync(targetDir, { recursive: true, force: true });
10879
+ if (existsSync17(targetDir)) rmSync(targetDir, { recursive: true, force: true });
10482
10880
  cpSync(sourceDir, targetDir, { recursive: true });
10483
10881
  const manifestPath = pluginManifestPath(targetDir);
10484
10882
  const contentHash = hashPluginContent(targetDir, manifest);
@@ -10536,18 +10934,18 @@ function getInstalledPlugin(configDir, name) {
10536
10934
  }
10537
10935
  function listInstalledPlugins(configDir) {
10538
10936
  const root = pluginRoot(configDir);
10539
- if (!existsSync16(root)) return [];
10937
+ if (!existsSync17(root)) return [];
10540
10938
  const state2 = loadState(configDir);
10541
10939
  const out = [];
10542
10940
  for (const entry of readdirSync7(root)) {
10543
- const dir = join11(root, entry);
10941
+ const dir = join12(root, entry);
10544
10942
  try {
10545
10943
  if (!statSync8(dir).isDirectory()) continue;
10546
10944
  } catch {
10547
10945
  continue;
10548
10946
  }
10549
10947
  const manifestPath = pluginManifestPath(dir);
10550
- if (!existsSync16(manifestPath)) continue;
10948
+ if (!existsSync17(manifestPath)) continue;
10551
10949
  try {
10552
10950
  const manifest = readPluginManifest(manifestPath);
10553
10951
  const contentHash = hashPluginContent(dir, manifest);
@@ -10587,7 +10985,7 @@ function uniqueDirs(plugin, rels) {
10587
10985
  for (const rel of rels) {
10588
10986
  const abs = resolve6(plugin.dir, rel);
10589
10987
  if (!abs.startsWith(resolve6(plugin.dir))) continue;
10590
- if (existsSync16(abs)) dirs.add(statSync8(abs).isDirectory() ? abs : dirname7(abs));
10988
+ if (existsSync17(abs)) dirs.add(statSync8(abs).isDirectory() ? abs : dirname7(abs));
10591
10989
  }
10592
10990
  return [...dirs];
10593
10991
  }
@@ -10750,13 +11148,13 @@ function parseAgentConfig(raw, source, path3) {
10750
11148
  return cfg;
10751
11149
  }
10752
11150
  function loadAgentDir(dir, source) {
10753
- if (!existsSync17(dir)) return [];
11151
+ if (!existsSync18(dir)) return [];
10754
11152
  const out = [];
10755
11153
  for (const file of readdirSync8(dir)) {
10756
11154
  if (!file.endsWith(".json")) continue;
10757
- const path3 = join12(dir, file);
11155
+ const path3 = join13(dir, file);
10758
11156
  try {
10759
- const parsed = JSON.parse(readFileSync14(path3, "utf-8"));
11157
+ const parsed = JSON.parse(readFileSync15(path3, "utf-8"));
10760
11158
  const cfg = parseAgentConfig(parsed, source, path3);
10761
11159
  if (cfg) out.push(cfg);
10762
11160
  } catch {
@@ -10766,8 +11164,8 @@ function loadAgentDir(dir, source) {
10766
11164
  }
10767
11165
  function getAgentDirs(configDir, cwd = process.cwd()) {
10768
11166
  return {
10769
- user: join12(configDir ?? join12(homedir6(), CONFIG_DIR_NAME), "agents"),
10770
- project: join12(cwd, CONFIG_DIR_NAME, "agents")
11167
+ user: join13(configDir ?? join13(homedir6(), CONFIG_DIR_NAME), "agents"),
11168
+ project: join13(cwd, CONFIG_DIR_NAME, "agents")
10771
11169
  };
10772
11170
  }
10773
11171
  function listAgentConfigs(configDir, cwd = process.cwd()) {
@@ -11485,14 +11883,14 @@ var taskStopTool = {
11485
11883
 
11486
11884
  // src/tools/builtin/git-tools.ts
11487
11885
  import { execFileSync as execFileSync2 } from "child_process";
11488
- import { existsSync as existsSync18 } from "fs";
11489
- import { join as join13 } from "path";
11886
+ import { existsSync as existsSync19 } from "fs";
11887
+ import { join as join14 } from "path";
11490
11888
  function assertGitRepo(cwd) {
11491
11889
  let dir = cwd;
11492
11890
  const root = dir.split(/[\\/]/)[0] + (dir.includes("\\") ? "\\" : "/");
11493
11891
  while (dir && dir !== root) {
11494
- if (existsSync18(join13(dir, ".git"))) return;
11495
- const parent = join13(dir, "..");
11892
+ if (existsSync19(join14(dir, ".git"))) return;
11893
+ const parent = join14(dir, "..");
11496
11894
  if (parent === dir) break;
11497
11895
  dir = parent;
11498
11896
  }
@@ -11757,7 +12155,7 @@ ${commitOutput.trim()}`;
11757
12155
  };
11758
12156
 
11759
12157
  // src/tools/builtin/notebook-edit.ts
11760
- import { readFileSync as readFileSync15, existsSync as existsSync19 } from "fs";
12158
+ import { readFileSync as readFileSync16, existsSync as existsSync20 } from "fs";
11761
12159
  import { writeFile } from "fs/promises";
11762
12160
  import { resolve as resolve7, extname as extname2 } from "path";
11763
12161
  var notebookEditTool = {
@@ -11812,10 +12210,10 @@ var notebookEditTool = {
11812
12210
  if (extname2(absPath).toLowerCase() !== ".ipynb") {
11813
12211
  throw new ToolError("notebook_edit", "path must point to a .ipynb file");
11814
12212
  }
11815
- if (!existsSync19(absPath)) {
12213
+ if (!existsSync20(absPath)) {
11816
12214
  throw new ToolError("notebook_edit", `Notebook not found: ${filePath}`);
11817
12215
  }
11818
- const raw = readFileSync15(absPath, "utf-8");
12216
+ const raw = readFileSync16(absPath, "utf-8");
11819
12217
  const nb = parseNotebook(raw);
11820
12218
  const cellIdx0 = cellIndexRaw - 1;
11821
12219
  undoStack.push(absPath, `notebook_edit (${action}): ${filePath}`);
@@ -12141,7 +12539,7 @@ ${lines.join("\n")}`;
12141
12539
  };
12142
12540
 
12143
12541
  // src/tools/builtin/recall-memory.ts
12144
- import { join as join14 } from "path";
12542
+ import { join as join15 } from "path";
12145
12543
  import { homedir as homedir7 } from "os";
12146
12544
  function formatHit(h, i) {
12147
12545
  const ts = h.chunk.timestamp.slice(0, 16).replace("T", " ");
@@ -12192,7 +12590,7 @@ var recallMemoryTool = {
12192
12590
  const excludeCurrent = Boolean(args["excludeCurrentSession"]);
12193
12591
  const currentId = args["currentSessionId"] ? String(args["currentSessionId"]) : void 0;
12194
12592
  const minScore = args["minScore"] !== void 0 ? Number(args["minScore"]) : 0.25;
12195
- const configDir = join14(homedir7(), CONFIG_DIR_NAME);
12593
+ const configDir = join15(homedir7(), CONFIG_DIR_NAME);
12196
12594
  let persistentSection = "";
12197
12595
  try {
12198
12596
  const persistentHits = searchPersistentMemories(configDir, query, process.cwd(), 3);
@@ -12246,8 +12644,8 @@ function estimateToolDefinitionTokens(def) {
12246
12644
 
12247
12645
  // src/tools/registry.ts
12248
12646
  import { pathToFileURL } from "url";
12249
- import { existsSync as existsSync20, mkdirSync as mkdirSync9, readdirSync as readdirSync9 } from "fs";
12250
- import { join as join15 } from "path";
12647
+ import { existsSync as existsSync21, mkdirSync as mkdirSync9, readdirSync as readdirSync9 } from "fs";
12648
+ import { join as join16 } from "path";
12251
12649
  var ToolRegistry = class {
12252
12650
  tools = /* @__PURE__ */ new Map();
12253
12651
  pluginToolNames = /* @__PURE__ */ new Set();
@@ -12408,7 +12806,7 @@ var ToolRegistry = class {
12408
12806
  * Returns the number of successfully loaded plugins.
12409
12807
  */
12410
12808
  async loadPlugins(pluginsDir, allowPlugins = false) {
12411
- if (!existsSync20(pluginsDir)) {
12809
+ if (!existsSync21(pluginsDir)) {
12412
12810
  try {
12413
12811
  mkdirSync9(pluginsDir, { recursive: true });
12414
12812
  } catch {
@@ -12434,12 +12832,12 @@ var ToolRegistry = class {
12434
12832
  process.stderr.write(
12435
12833
  `
12436
12834
  [plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
12437
- ` + files.map((f) => ` + ${join15(pluginsDir, f)}`).join("\n") + "\n\n"
12835
+ ` + files.map((f) => ` + ${join16(pluginsDir, f)}`).join("\n") + "\n\n"
12438
12836
  );
12439
12837
  let loaded = 0;
12440
12838
  for (const file of files) {
12441
12839
  try {
12442
- const fileUrl = pathToFileURL(join15(pluginsDir, file)).href;
12840
+ const fileUrl = pathToFileURL(join16(pluginsDir, file)).href;
12443
12841
  const mod = await import(fileUrl);
12444
12842
  const tool = mod.tool ?? mod.default?.tool ?? mod.default;
12445
12843
  if (!tool || typeof tool.execute !== "function" || !tool.definition?.name) {
@@ -12984,11 +13382,11 @@ var McpManager = class {
12984
13382
  };
12985
13383
 
12986
13384
  // src/skills/manager.ts
12987
- import { existsSync as existsSync21, readdirSync as readdirSync10, mkdirSync as mkdirSync10, statSync as statSync9 } from "fs";
12988
- import { join as join16 } from "path";
13385
+ import { existsSync as existsSync22, readdirSync as readdirSync10, mkdirSync as mkdirSync10, statSync as statSync9 } from "fs";
13386
+ import { join as join17 } from "path";
12989
13387
 
12990
13388
  // src/skills/types.ts
12991
- import { readFileSync as readFileSync16 } from "fs";
13389
+ import { readFileSync as readFileSync17 } from "fs";
12992
13390
  import { basename as basename7 } from "path";
12993
13391
  function parseSimpleYaml(yaml) {
12994
13392
  const result = {};
@@ -13010,7 +13408,7 @@ function parseYamlArray(value) {
13010
13408
  function parseSkillFile(filePath) {
13011
13409
  let raw;
13012
13410
  try {
13013
- raw = readFileSync16(filePath, "utf-8");
13411
+ raw = readFileSync17(filePath, "utf-8");
13014
13412
  } catch {
13015
13413
  return null;
13016
13414
  }
@@ -13058,7 +13456,7 @@ var SkillManager = class {
13058
13456
  return this.skills.size;
13059
13457
  }
13060
13458
  loadSkillDir(dir, createIfMissing) {
13061
- if (!existsSync21(dir)) {
13459
+ if (!existsSync22(dir)) {
13062
13460
  if (createIfMissing) {
13063
13461
  try {
13064
13462
  mkdirSync10(dir, { recursive: true });
@@ -13075,14 +13473,14 @@ var SkillManager = class {
13075
13473
  }
13076
13474
  for (const entry of entries) {
13077
13475
  let filePath;
13078
- const fullPath = join16(dir, entry);
13476
+ const fullPath = join17(dir, entry);
13079
13477
  if (entry.endsWith(".md")) {
13080
13478
  filePath = fullPath;
13081
13479
  } else {
13082
13480
  try {
13083
13481
  if (statSync9(fullPath).isDirectory()) {
13084
- const skillMd = join16(fullPath, "SKILL.md");
13085
- if (existsSync21(skillMd)) filePath = skillMd;
13482
+ const skillMd = join17(fullPath, "SKILL.md");
13483
+ if (existsSync22(skillMd)) filePath = skillMd;
13086
13484
  else continue;
13087
13485
  } else {
13088
13486
  continue;
@@ -13141,7 +13539,7 @@ var SkillManager = class {
13141
13539
  // src/web/tool-executor-web.ts
13142
13540
  import { randomUUID as randomUUID2 } from "crypto";
13143
13541
  import { tmpdir as tmpdir2 } from "os";
13144
- import { existsSync as existsSync22, readFileSync as readFileSync17 } from "fs";
13542
+ import { existsSync as existsSync23, readFileSync as readFileSync18 } from "fs";
13145
13543
  var ToolExecutorWeb = class _ToolExecutorWeb {
13146
13544
  constructor(registry, ws) {
13147
13545
  this.registry = registry;
@@ -13281,9 +13679,9 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
13281
13679
  getDiffPreview(call) {
13282
13680
  if (call.name === "write_file") {
13283
13681
  const { path: filePath, content: chunk, append } = resolveWriteFileArgs(call.arguments);
13284
- if (filePath && existsSync22(filePath)) {
13682
+ if (filePath && existsSync23(filePath)) {
13285
13683
  try {
13286
- const old = readFileSync17(filePath, "utf-8");
13684
+ const old = readFileSync18(filePath, "utf-8");
13287
13685
  return renderDiff(old, append ? old + chunk : chunk, { filePath });
13288
13686
  } catch {
13289
13687
  }
@@ -13596,16 +13994,16 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
13596
13994
  };
13597
13995
 
13598
13996
  // src/repl/dev-state.ts
13599
- import { existsSync as existsSync23, readFileSync as readFileSync18, unlinkSync as unlinkSync5, mkdirSync as mkdirSync11 } from "fs";
13600
- import { join as join17 } from "path";
13997
+ import { existsSync as existsSync24, readFileSync as readFileSync19, unlinkSync as unlinkSync5, mkdirSync as mkdirSync11 } from "fs";
13998
+ import { join as join18 } from "path";
13601
13999
  import { homedir as homedir8 } from "os";
13602
14000
  function getDevStatePath() {
13603
- return join17(homedir8(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
14001
+ return join18(homedir8(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
13604
14002
  }
13605
14003
  function loadDevState() {
13606
14004
  const path3 = getDevStatePath();
13607
- if (!existsSync23(path3)) return null;
13608
- const content = readFileSync18(path3, "utf-8").trim();
14005
+ if (!existsSync24(path3)) return null;
14006
+ const content = readFileSync19(path3, "utf-8").trim();
13609
14007
  return content || null;
13610
14008
  }
13611
14009
 
@@ -13666,148 +14064,6 @@ function loadMemoryContent(configDir, maxChars) {
13666
14064
  return formatMemoryForPrompt(configDir, process.cwd(), maxChars);
13667
14065
  }
13668
14066
 
13669
- // src/core/pricing.ts
13670
- var DEEPSEEK_CNY_PER_USD = 6.9;
13671
- var DEEPSEEK_TIME_PRICING_EFFECTIVE_AT = /* @__PURE__ */ new Date("2026-08-16T16:00:00.000Z");
13672
- function deepSeekCnyPricing(cacheRead, input, output) {
13673
- return {
13674
- cacheRead: cacheRead / DEEPSEEK_CNY_PER_USD,
13675
- input: input / DEEPSEEK_CNY_PER_USD,
13676
- output: output / DEEPSEEK_CNY_PER_USD
13677
- };
13678
- }
13679
- function getDeepSeekV4Pricing(model, at) {
13680
- const isPro = model.startsWith("deepseek-v4-pro");
13681
- const isFlash = model.startsWith("deepseek-v4-flash");
13682
- if (!isPro && !isFlash) return null;
13683
- if (at < DEEPSEEK_TIME_PRICING_EFFECTIVE_AT) {
13684
- return isPro ? deepSeekCnyPricing(0.025, 3, 6) : deepSeekCnyPricing(0.02, 1, 2);
13685
- }
13686
- const beijingHour = (at.getUTCHours() + 8) % 24;
13687
- const isPeak = beijingHour >= 9 && beijingHour < 12 || beijingHour >= 14 && beijingHour < 18;
13688
- if (isPro) {
13689
- return isPeak ? deepSeekCnyPricing(0.3, 9, 27) : deepSeekCnyPricing(0.15, 4.5, 13.5);
13690
- }
13691
- return isPeak ? deepSeekCnyPricing(0.1, 3, 9) : deepSeekCnyPricing(0.05, 1.5, 4.5);
13692
- }
13693
- var PRICING_TABLE = {
13694
- // ── Anthropic Claude ──────────────────────────────────────────
13695
- "claude-opus-4-8": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
13696
- "claude-opus-4-7": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
13697
- "claude-opus-4-6": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
13698
- "claude-opus-4-5": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
13699
- "claude-sonnet-4-6": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
13700
- "claude-sonnet-4-5-20250929": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
13701
- "claude-haiku-4-5-20251001": { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 },
13702
- "claude-haiku-4-5": { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 },
13703
- // Legacy Claude 3.x families (prefix fallback handles minor date suffixes)
13704
- "claude-3-5-sonnet": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
13705
- "claude-3-5-haiku": { input: 0.8, output: 4, cacheWrite: 1, cacheRead: 0.08 },
13706
- "claude-3-opus": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
13707
- // ── OpenAI ────────────────────────────────────────────────────
13708
- // GAP (v0.4.184): the GPT-5 family (gpt-5, gpt-5-mini/nano, gpt-5.4, gpt-5.4-pro)
13709
- // and o4-mini are in the provider's model list but have NO verified pricing yet —
13710
- // left unpriced on purpose so `aicli usage` shows "—" (unknown) rather than a
13711
- // deceptive $0.00. Add real rates when confirmed from the official pricing page.
13712
- "gpt-4o": { input: 2.5, output: 10, cacheRead: 1.25 },
13713
- "gpt-4o-mini": { input: 0.15, output: 0.6, cacheRead: 0.075 },
13714
- "gpt-4-turbo": { input: 10, output: 30 },
13715
- "gpt-4": { input: 30, output: 60 },
13716
- "gpt-4.1": { input: 2, output: 8, cacheRead: 0.5 },
13717
- "gpt-4.1-mini": { input: 0.4, output: 1.6, cacheRead: 0.1 },
13718
- "gpt-4.1-nano": { input: 0.1, output: 0.4, cacheRead: 0.025 },
13719
- "o1": { input: 15, output: 60, cacheRead: 7.5 },
13720
- "o1-mini": { input: 3, output: 12, cacheRead: 1.5 },
13721
- "o3": { input: 10, output: 40, cacheRead: 2.5 },
13722
- "o3-mini": { input: 1.1, output: 4.4, cacheRead: 0.55 },
13723
- // ── Google Gemini ─────────────────────────────────────────────
13724
- "gemini-2.5-pro": { input: 1.25, output: 10 },
13725
- "gemini-2.5-flash": { input: 0.3, output: 2.5 },
13726
- "gemini-2.0-flash": { input: 0.1, output: 0.4 },
13727
- "gemini-1.5-pro": { input: 1.25, output: 5 },
13728
- "gemini-1.5-flash": { input: 0.075, output: 0.3 },
13729
- // ── DeepSeek ──────────────────────────────────────────────────
13730
- // V4 family (2026-04-23+):1M context, Thinking / Non-Thinking dual-mode, 384K max output
13731
- // 费率 2026-08-01 对 api-docs.deepseek.com/quick_start/pricing 官方 USD 表逐项核对。
13732
- // 修正前 pro 三项整齐高 4×(官方降过价未跟)、flash cacheRead 高 10×(像是按 input 的
13733
- // 20% 拍脑袋而非抄文档)——`aicli usage` 因此系统性高报 DeepSeek 成本。
13734
- // 待办:官方已公告峰谷定价(北京时间 9-12 点、14-18 点为平时 2 倍),本表仍是单一费率,
13735
- // 届时高峰时段会低报;等该策略实际生效后再决定是否引入时段维度。
13736
- // Legacy aliases:retires 2026-07-24 UTC 15:59,官方 route 到 V4 Flash。
13737
- // 这几条只服务历史 session 的成本回算,保留当时的费率口径,不随现价改动。
13738
- "deepseek-chat": { input: 0.14, output: 0.28, cacheRead: 0.028 },
13739
- "deepseek-reasoner": { input: 0.14, output: 0.28, cacheRead: 0.028 },
13740
- "deepseek-v3": { input: 0.27, output: 1.1, cacheRead: 0.07 },
13741
- // ── Moonshot Kimi ─────────────────────────────────────────────
13742
- "moonshot-v1-8k": { input: 0.17, output: 0.17 },
13743
- "moonshot-v1-32k": { input: 0.33, output: 0.33 },
13744
- "moonshot-v1-128k": { input: 0.83, output: 0.83 },
13745
- "kimi-k2": { input: 0.6, output: 2.5 },
13746
- "kimi-latest": { input: 0.6, output: 2.5 },
13747
- // Kimi K3(2026-07 旗舰,1M 上下文)官方定价 platform.kimi.ai/pricing/chat-k3:
13748
- // 输入 cache-miss $3.00 / cache-hit $0.30,输出 $15.00(/MTok)。
13749
- "kimi-k3": { input: 3, output: 15, cacheRead: 0.3 },
13750
- // ── Zhipu GLM ─────────────────────────────────────────────────
13751
- "glm-4-plus": { input: 0.7, output: 0.7 },
13752
- "glm-4": { input: 0.14, output: 0.14 },
13753
- "glm-4-flash": { input: 0, output: 0 },
13754
- "glm-4.5": { input: 0.29, output: 1.14 },
13755
- "glm-4.6": { input: 0.6, output: 2.2 },
13756
- "glm-4.6v": { input: 0.6, output: 2.2 },
13757
- "glm-5": { input: 0.85, output: 2.85 },
13758
- // GLM-5.2(2026-06)官方直连 API 定价 $1.40 / $4.40。
13759
- "glm-5.2": { input: 1.4, output: 4.4 },
13760
- // GLM-5V-Turbo(2026-04 视觉编程旗舰)官方直连 API 定价 $1.20 / $4.00。
13761
- "glm-5v-turbo": { input: 1.2, output: 4 },
13762
- "glm-5.1": { input: 0.95, output: 3.15 },
13763
- "glm-5.1-reasoning": { input: 1.4, output: 4.4 },
13764
- "glm-5.1-air": { input: 0.4, output: 1.2 },
13765
- "glm-z1": { input: 0.5, output: 1.5 },
13766
- "glm-z1-air": { input: 0.2, output: 0.6 },
13767
- "glm-z1-flash": { input: 0, output: 0 }
13768
- // ── MiniMax (海螺) ────────────────────────────────────────────
13769
- // GAP (v0.4.184): MiniMax-M3 / M2.x are unpriced — no verified rate anchor yet.
13770
- // Unpriced → `aicli usage` shows "—" (unknown), not a false $0.00. Add when confirmed.
13771
- // ── OpenRouter (pass-through — actual cost depends on underlying model) ──
13772
- // Left empty by design: model IDs are "vendor/model" (e.g. anthropic/claude-sonnet-4)
13773
- // and the real price is set by the underlying model + OpenRouter markup, not statically
13774
- // resolvable here. Shows "—" (unknown) in `aicli usage`.
13775
- // ── Ollama (local, zero cost) ─────────────────────────────────
13776
- // Handled via provider check below (FREE_PROVIDERS → genuine $0.00, not "unknown").
13777
- };
13778
- var FREE_PROVIDERS = /* @__PURE__ */ new Set(["ollama"]);
13779
- function getPricing(provider, model, at = /* @__PURE__ */ new Date()) {
13780
- if (FREE_PROVIDERS.has(provider.toLowerCase())) {
13781
- return { input: 0, output: 0 };
13782
- }
13783
- const key = model.toLowerCase();
13784
- if (provider.toLowerCase() === "deepseek") {
13785
- const dynamicPricing = getDeepSeekV4Pricing(key, at);
13786
- if (dynamicPricing) return dynamicPricing;
13787
- }
13788
- if (PRICING_TABLE[key]) return PRICING_TABLE[key];
13789
- const keys = Object.keys(PRICING_TABLE).sort((a, b) => b.length - a.length);
13790
- for (const k of keys) {
13791
- if (key.startsWith(k)) return PRICING_TABLE[k];
13792
- }
13793
- return null;
13794
- }
13795
- function computeCost(provider, model, usage, at = /* @__PURE__ */ new Date()) {
13796
- const p = getPricing(provider, model, at);
13797
- if (!p) return null;
13798
- const input = usage.inputTokens * p.input;
13799
- const output = usage.outputTokens * p.output;
13800
- const cacheWrite = (usage.cacheCreationTokens ?? 0) * (p.cacheWrite ?? p.input);
13801
- const cacheRead = (usage.cacheReadTokens ?? 0) * (p.cacheRead ?? p.input);
13802
- return (input + output + cacheWrite + cacheRead) / 1e6;
13803
- }
13804
- function formatCost(amount) {
13805
- if (amount === 0) return "$0.0000";
13806
- if (amount < 0.01) return `$${amount.toFixed(4)}`;
13807
- if (amount < 1) return `$${amount.toFixed(3)}`;
13808
- return `$${amount.toFixed(2)}`;
13809
- }
13810
-
13811
14067
  // src/core/chat-params.ts
13812
14068
  function thinkingParamsFor(allModelParams, model) {
13813
14069
  const params = allModelParams?.[model];
@@ -13906,9 +14162,41 @@ function autoTrimSessionIfNeeded(session, sizeLimit = SESSION_SIZE_LIMIT) {
13906
14162
  return committedSize < originalSize;
13907
14163
  }
13908
14164
 
14165
+ // src/core/context-pressure.ts
14166
+ function estimateConversationTokens(input) {
14167
+ let total = 0;
14168
+ if (input.systemPrompt) total += estimateTokens(input.systemPrompt);
14169
+ for (const msg of input.messages) {
14170
+ if (typeof msg.content === "string") {
14171
+ total += estimateTokens(msg.content);
14172
+ } else if (Array.isArray(msg.content)) {
14173
+ for (const part of msg.content) {
14174
+ if (part.type === "text" && part.text) total += estimateTokens(part.text);
14175
+ }
14176
+ }
14177
+ if (msg.toolCalls) {
14178
+ for (const tc of msg.toolCalls) {
14179
+ total += estimateTokens(JSON.stringify(tc.arguments));
14180
+ }
14181
+ }
14182
+ }
14183
+ return total;
14184
+ }
14185
+ function evaluateContextPressure(input) {
14186
+ if (input.contextWindow <= 0) return { shouldCompact: false, ratio: null, percent: null };
14187
+ const ratio = input.estimatedTokens / input.contextWindow;
14188
+ const threshold = input.threshold ?? CONTEXT_PRESSURE_THRESHOLD;
14189
+ return {
14190
+ shouldCompact: input.autoCompactEnabled && ratio >= threshold,
14191
+ ratio,
14192
+ percent: Math.round(ratio * 100)
14193
+ };
14194
+ }
14195
+ var AUTO_COMPACT_INSTRUCTION = "Preserve key context, compress old content, ensure seamless continuation";
14196
+
13909
14197
  // src/core/context-files.ts
13910
- import { existsSync as existsSync24, readFileSync as readFileSync19 } from "fs";
13911
- import { join as join18, relative as relative4, resolve as resolve8 } from "path";
14198
+ import { existsSync as existsSync25, readFileSync as readFileSync20 } from "fs";
14199
+ import { join as join19, relative as relative4, resolve as resolve8 } from "path";
13912
14200
  function uniqueNonEmpty(values) {
13913
14201
  const seen = /* @__PURE__ */ new Set();
13914
14202
  const out = [];
@@ -13938,7 +14226,7 @@ function readContextFile(level, filePath, cwd, maxBytes) {
13938
14226
  const name = filePath.split(/[\\/]/).pop() ?? filePath;
13939
14227
  const shown = displayPath(filePath, cwd);
13940
14228
  try {
13941
- const raw = readFileSync19(filePath);
14229
+ const raw = readFileSync20(filePath);
13942
14230
  const byteCount = raw.byteLength;
13943
14231
  if (byteCount === 0) {
13944
14232
  return {
@@ -13985,8 +14273,8 @@ function readContextFile(level, filePath, cwd, maxBytes) {
13985
14273
  function findFirstContextFile(level, dir, cwd, candidates, maxBytes) {
13986
14274
  const skipped = [];
13987
14275
  for (const candidate of candidates) {
13988
- const filePath = join18(dir, candidate);
13989
- if (!existsSync24(filePath)) continue;
14276
+ const filePath = join19(dir, candidate);
14277
+ if (!existsSync25(filePath)) continue;
13990
14278
  const result = readContextFile(level, filePath, cwd, maxBytes);
13991
14279
  if (result.skipped) skipped.push(result.skipped);
13992
14280
  if (result.layer) return { layer: result.layer, skipped };
@@ -14013,7 +14301,7 @@ function loadContextFiles(options) {
14013
14301
  });
14014
14302
  return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
14015
14303
  }
14016
- if (!existsSync24(filePath)) {
14304
+ if (!existsSync25(filePath)) {
14017
14305
  skipped.push({
14018
14306
  level: "single",
14019
14307
  filePath,
@@ -14068,8 +14356,8 @@ import { mkdirSync as mkdirSync14, createWriteStream as createWriteStream2 } fro
14068
14356
  import { dirname as dirname9 } from "path";
14069
14357
 
14070
14358
  // src/repl/commands/project-init.ts
14071
- import { existsSync as existsSync25, readFileSync as readFileSync20, readdirSync as readdirSync11, statSync as statSync10 } from "fs";
14072
- import { join as join19 } from "path";
14359
+ import { existsSync as existsSync26, readFileSync as readFileSync21, readdirSync as readdirSync11, statSync as statSync10 } from "fs";
14360
+ import { join as join20 } from "path";
14073
14361
  var SCAN_SKIP_DIRS = /* @__PURE__ */ new Set([
14074
14362
  "node_modules",
14075
14363
  ".git",
@@ -14113,11 +14401,11 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
14113
14401
  const sorted = filtered.sort((a, b) => {
14114
14402
  let aIsDir = false, bIsDir = false;
14115
14403
  try {
14116
- aIsDir = statSync10(join19(d, a)).isDirectory();
14404
+ aIsDir = statSync10(join20(d, a)).isDirectory();
14117
14405
  } catch {
14118
14406
  }
14119
14407
  try {
14120
- bIsDir = statSync10(join19(d, b)).isDirectory();
14408
+ bIsDir = statSync10(join20(d, b)).isDirectory();
14121
14409
  } catch {
14122
14410
  }
14123
14411
  if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
@@ -14125,7 +14413,7 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
14125
14413
  });
14126
14414
  for (let i = 0; i < sorted.length && count < maxEntries; i++) {
14127
14415
  const name = sorted[i];
14128
- const fullPath = join19(d, name);
14416
+ const fullPath = join20(d, name);
14129
14417
  const isLast = i === sorted.length - 1;
14130
14418
  const connector = isLast ? "+-- " : "|-- ";
14131
14419
  let isDir;
@@ -14152,7 +14440,7 @@ function scanProject(cwd) {
14152
14440
  configFiles: [],
14153
14441
  directoryStructure: ""
14154
14442
  };
14155
- const check = (file) => existsSync25(join19(cwd, file));
14443
+ const check = (file) => existsSync26(join20(cwd, file));
14156
14444
  const configCandidates = [
14157
14445
  "package.json",
14158
14446
  "tsconfig.json",
@@ -14179,7 +14467,7 @@ function scanProject(cwd) {
14179
14467
  info.type = "node";
14180
14468
  info.language = check("tsconfig.json") ? "TypeScript" : "JavaScript";
14181
14469
  try {
14182
- const pkg = JSON.parse(readFileSync20(join19(cwd, "package.json"), "utf-8"));
14470
+ const pkg = JSON.parse(readFileSync21(join20(cwd, "package.json"), "utf-8"));
14183
14471
  const scripts = pkg.scripts ?? {};
14184
14472
  info.buildCommand = scripts.build ? "npm run build" : void 0;
14185
14473
  info.testCommand = scripts.test ? "npm test" : void 0;
@@ -14840,7 +15128,7 @@ function resolveRoleProviders(roles, lookup, defaultProvider, defaultModel, avai
14840
15128
  }
14841
15129
 
14842
15130
  // src/hub/persist.ts
14843
- import { join as join20 } from "path";
15131
+ import { join as join21 } from "path";
14844
15132
  function discussionToMessages(state2) {
14845
15133
  const out = [];
14846
15134
  const t0 = state2.messages[0]?.timestamp ?? /* @__PURE__ */ new Date();
@@ -14878,7 +15166,7 @@ async function persistDiscussion(state2, config, defaultProvider, defaultModel)
14878
15166
  session.title = `[Hub] ${state2.topic.slice(0, 48)}`.replace(/\n/g, " ");
14879
15167
  session.titleAiGenerated = true;
14880
15168
  await sm.save();
14881
- return { id: session.id, path: join20(config.getHistoryDir(), `${session.id}.json`) };
15169
+ return { id: session.id, path: join21(config.getHistoryDir(), `${session.id}.json`) };
14882
15170
  }
14883
15171
 
14884
15172
  // src/core/think-command.ts
@@ -15032,6 +15320,12 @@ async function handlePlan(args, ctx) {
15032
15320
  } else if (sub === "execute") {
15033
15321
  ctx.planMode = false;
15034
15322
  ctx.send({ type: "info", message: "Plan mode OFF. Executing with all tools enabled." });
15323
+ } else if (sub === "status" || sub === "show") {
15324
+ ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
15325
+ return;
15326
+ } else if (sub !== void 0) {
15327
+ ctx.send({ type: "error", message: `Unknown /plan option "${sub}" \u2014 expected execute | exit | status` });
15328
+ return;
15035
15329
  } else {
15036
15330
  ctx.planMode = !ctx.planMode;
15037
15331
  ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
@@ -15039,9 +15333,44 @@ async function handlePlan(args, ctx) {
15039
15333
  ctx.sendStatus();
15040
15334
  }
15041
15335
 
15336
+ // src/core/session-cost.ts
15337
+ function buildSessionCostReport(input) {
15338
+ const { usage } = input;
15339
+ const totalTokens = usage.inputTokens + usage.outputTokens + usage.cacheCreationTokens + usage.cacheReadTokens;
15340
+ if (totalTokens === 0) {
15341
+ return { hasUsage: false, totalTokens: 0, cacheHitPct: null, cost: null, costSource: "unknown", savings: null };
15342
+ }
15343
+ const liveCost = computeCost(input.provider, input.model, usage);
15344
+ const useAccrual = input.accruedUsd > 0;
15345
+ const cost = useAccrual ? input.accruedUsd : liveCost;
15346
+ const costSource = useAccrual ? input.accrualIncomplete ? "accrued-partial" : "accrued" : liveCost != null ? "estimated" : "unknown";
15347
+ let savings = null;
15348
+ if (liveCost != null && usage.cacheReadTokens > 0) {
15349
+ const withoutCache = computeCost(input.provider, input.model, {
15350
+ inputTokens: usage.inputTokens + usage.cacheReadTokens,
15351
+ outputTokens: usage.outputTokens,
15352
+ cacheCreationTokens: usage.cacheCreationTokens,
15353
+ cacheReadTokens: 0
15354
+ });
15355
+ if (withoutCache != null) savings = withoutCache - liveCost;
15356
+ }
15357
+ const cacheHitPct = usage.cacheReadTokens > 0 ? Math.round(usage.cacheReadTokens / (usage.cacheReadTokens + usage.inputTokens) * 100) : null;
15358
+ return { hasUsage: true, totalTokens, cacheHitPct, cost, costSource, savings };
15359
+ }
15360
+ function costSourceNote(source) {
15361
+ switch (source) {
15362
+ case "accrued-partial":
15363
+ return "(partial \u2014 some turns had unknown pricing)";
15364
+ case "estimated":
15365
+ return "(estimated at current rates)";
15366
+ default:
15367
+ return null;
15368
+ }
15369
+ }
15370
+
15042
15371
  // src/web/commands/_shared.ts
15043
- import { existsSync as existsSync28, readFileSync as readFileSync22, writeFileSync as writeFileSync3, mkdirSync as mkdirSync13, readdirSync as readdirSync13, statSync as statSync13, createWriteStream } from "fs";
15044
- import { join as join23, resolve as resolve9, dirname as dirname8 } from "path";
15372
+ import { existsSync as existsSync29, readFileSync as readFileSync23, writeFileSync as writeFileSync3, mkdirSync as mkdirSync13, readdirSync as readdirSync13, statSync as statSync13, createWriteStream } from "fs";
15373
+ import { join as join24, resolve as resolve9, dirname as dirname8 } from "path";
15045
15374
 
15046
15375
  // src/cli/review-prompts.ts
15047
15376
  function buildReviewPrompt(diff, gitContextStr, detailed) {
@@ -15113,34 +15442,34 @@ function readGitDiff(options = {}) {
15113
15442
  }
15114
15443
 
15115
15444
  // src/diagnostics/doctor-report.ts
15116
- import { existsSync as existsSync27, statSync as statSync12 } from "fs";
15117
- import { join as join22 } from "path";
15445
+ import { existsSync as existsSync28, statSync as statSync12 } from "fs";
15446
+ import { join as join23 } from "path";
15118
15447
  import { arch as arch2, platform as platform7, release as release2 } from "os";
15119
15448
 
15120
15449
  // src/diagnostics/crash-log.ts
15121
15450
  import {
15122
- existsSync as existsSync26,
15451
+ existsSync as existsSync27,
15123
15452
  mkdirSync as mkdirSync12,
15124
15453
  readdirSync as readdirSync12,
15125
- readFileSync as readFileSync21,
15454
+ readFileSync as readFileSync22,
15126
15455
  statSync as statSync11,
15127
15456
  unlinkSync as unlinkSync6,
15128
15457
  writeFileSync as writeFileSync2
15129
15458
  } from "fs";
15130
- import { join as join21 } from "path";
15459
+ import { join as join22 } from "path";
15131
15460
  import { homedir as homedir9, platform as platform6, release, arch } from "os";
15132
15461
  var LOGS_DIR_NAME = "logs";
15133
15462
  var CRASH_LOG_PREFIX = "crash-";
15134
15463
  function getLogsDir(configDir) {
15135
- const base = configDir ?? join21(homedir9(), CONFIG_DIR_NAME);
15136
- return join21(base, LOGS_DIR_NAME);
15464
+ const base = configDir ?? join22(homedir9(), CONFIG_DIR_NAME);
15465
+ return join22(base, LOGS_DIR_NAME);
15137
15466
  }
15138
15467
  function truncate(s, n) {
15139
15468
  return s.length <= n ? s : s.slice(0, n) + "\u2026";
15140
15469
  }
15141
15470
  function listRecentCrashes(limit = 10, configDir) {
15142
15471
  const dir = getLogsDir(configDir);
15143
- if (!existsSync26(dir)) return [];
15472
+ if (!existsSync27(dir)) return [];
15144
15473
  const files = [];
15145
15474
  let names;
15146
15475
  try {
@@ -15150,10 +15479,10 @@ function listRecentCrashes(limit = 10, configDir) {
15150
15479
  }
15151
15480
  for (const name of names) {
15152
15481
  if (!name.startsWith(CRASH_LOG_PREFIX) || !name.endsWith(".log")) continue;
15153
- const full = join21(dir, name);
15482
+ const full = join22(dir, name);
15154
15483
  try {
15155
15484
  const st = statSync11(full);
15156
- const head = readFileSync21(full, "utf-8").split("\n").slice(0, 20);
15485
+ const head = readFileSync22(full, "utf-8").split("\n").slice(0, 20);
15157
15486
  const tsLine = head.find((l) => l.startsWith("timestamp:"));
15158
15487
  const kindLine = head.find((l) => l.startsWith("kind:"));
15159
15488
  const msgIdx = head.findIndex((l) => l.trim() === "## message");
@@ -15172,8 +15501,8 @@ function listRecentCrashes(limit = 10, configDir) {
15172
15501
  return files.slice(0, limit);
15173
15502
  }
15174
15503
  function getConfigDirUsage(configDir) {
15175
- const base = configDir ?? join21(homedir9(), CONFIG_DIR_NAME);
15176
- if (!existsSync26(base)) return { totalBytes: 0, entries: [] };
15504
+ const base = configDir ?? join22(homedir9(), CONFIG_DIR_NAME);
15505
+ if (!existsSync27(base)) return { totalBytes: 0, entries: [] };
15177
15506
  const entries = [];
15178
15507
  let total = 0;
15179
15508
  let names;
@@ -15183,7 +15512,7 @@ function getConfigDirUsage(configDir) {
15183
15512
  return { totalBytes: 0, entries: [] };
15184
15513
  }
15185
15514
  for (const name of names) {
15186
- const full = join21(base, name);
15515
+ const full = join22(base, name);
15187
15516
  const bytes = dirSize(full);
15188
15517
  entries.push({ name, bytes });
15189
15518
  total += bytes;
@@ -15198,7 +15527,7 @@ function dirSize(path3) {
15198
15527
  if (!st.isDirectory()) return 0;
15199
15528
  let sum = 0;
15200
15529
  for (const name of readdirSync12(path3)) {
15201
- sum += dirSize(join21(path3, name));
15530
+ sum += dirSize(join22(path3, name));
15202
15531
  }
15203
15532
  return sum;
15204
15533
  } catch {
@@ -15208,7 +15537,7 @@ function dirSize(path3) {
15208
15537
 
15209
15538
  // src/diagnostics/doctor-report.ts
15210
15539
  function checkFile(label, path3) {
15211
- const exists = existsSync27(path3);
15540
+ const exists = existsSync28(path3);
15212
15541
  let sizeBytes = null;
15213
15542
  if (exists) {
15214
15543
  try {
@@ -15251,12 +15580,12 @@ function buildDoctorReport(options) {
15251
15580
  configDir,
15252
15581
  providers: options.providers.listAll().map((p) => ({ id: p.id, displayName: p.displayName, configured: p.configured })),
15253
15582
  files: [
15254
- checkFile("config.json", join22(configDir, "config.json")),
15255
- checkFile("memory.md", join22(configDir, MEMORY_FILE_NAME)),
15256
- checkFile("memory.jsonl", join22(configDir, "memory.jsonl")),
15257
- checkFile("dev-state.md", join22(configDir, DEV_STATE_FILE_NAME)),
15258
- checkFile("hooks-trust.json", join22(configDir, "hooks-trust.json")),
15259
- checkFile("plugin-state.json", join22(configDir, "plugins", "plugin-state.json"))
15583
+ checkFile("config.json", join23(configDir, "config.json")),
15584
+ checkFile("memory.md", join23(configDir, MEMORY_FILE_NAME)),
15585
+ checkFile("memory.jsonl", join23(configDir, "memory.jsonl")),
15586
+ checkFile("dev-state.md", join23(configDir, DEV_STATE_FILE_NAME)),
15587
+ checkFile("hooks-trust.json", join23(configDir, "hooks-trust.json")),
15588
+ checkFile("plugin-state.json", join23(configDir, "plugins", "plugin-state.json"))
15260
15589
  ],
15261
15590
  context: {
15262
15591
  enabled: contextSetting !== false,
@@ -15291,7 +15620,7 @@ function buildDoctorReport(options) {
15291
15620
  disabled: hooks.filter((h) => h.disabled).length
15292
15621
  },
15293
15622
  plugins: {
15294
- root: join22(configDir, "plugins"),
15623
+ root: join23(configDir, "plugins"),
15295
15624
  installed: plugins.length,
15296
15625
  active: activeAssets.plugins.length,
15297
15626
  invalid: plugins.filter((p) => !p.valid).length,
@@ -15306,7 +15635,7 @@ function buildDoctorReport(options) {
15306
15635
  mcp: {
15307
15636
  enabled: config.get("mcpEnabled"),
15308
15637
  configured: Object.keys(mcpServers).length,
15309
- projectConfigExists: existsSync27(join22(projectRoot, MCP_PROJECT_CONFIG_NAME)),
15638
+ projectConfigExists: existsSync28(join23(projectRoot, MCP_PROJECT_CONFIG_NAME)),
15310
15639
  statuses: options.mcpStatuses ?? []
15311
15640
  },
15312
15641
  recentCrashes: listRecentCrashes(5, configDir),
@@ -15534,7 +15863,7 @@ Cache: write=${cacheCreate} read=${cacheRead}` : "";
15534
15863
  Cost: ${formatCost(cost)}` : "";
15535
15864
  let memoryLine = "";
15536
15865
  try {
15537
- const { countPendingMemories } = await import("./persistent-memory-YPO25MOP.js");
15866
+ const { countPendingMemories } = await import("./persistent-memory-YBWIZWQP.js");
15538
15867
  const pending = countPendingMemories(ctx.config.getConfigDir());
15539
15868
  if (pending > 0) memoryLine = `
15540
15869
  Memory: \u26A0 ${pending} pending approval \u2014 see the Memory panel or /memory`;
@@ -15742,18 +16071,16 @@ async function handleExport(args, ctx) {
15742
16071
  }
15743
16072
  async function handleCost(_args, ctx) {
15744
16073
  const u = ctx.sessionTokenUsage;
15745
- const total = u.inputTokens + u.outputTokens + u.cacheCreationTokens + u.cacheReadTokens;
15746
- const cost = computeCost(ctx.currentProvider, ctx.currentModel, u);
15747
- let savings = null;
15748
- if (cost != null && u.cacheReadTokens > 0) {
15749
- const withoutCache = computeCost(ctx.currentProvider, ctx.currentModel, {
15750
- inputTokens: u.inputTokens + u.cacheReadTokens,
15751
- outputTokens: u.outputTokens,
15752
- cacheCreationTokens: u.cacheCreationTokens,
15753
- cacheReadTokens: 0
15754
- });
15755
- if (withoutCache != null) savings = withoutCache - cost;
15756
- }
16074
+ const report = buildSessionCostReport({
16075
+ provider: ctx.currentProvider,
16076
+ model: ctx.currentModel,
16077
+ usage: u,
16078
+ accruedUsd: ctx.sessionAccruedCostUsd,
16079
+ accrualIncomplete: ctx.sessionCostIncomplete
16080
+ });
16081
+ const total = report.totalTokens;
16082
+ const cost = report.cost;
16083
+ const savings = report.savings;
15757
16084
  const lines = [
15758
16085
  "\u{1F4B0} Session Cost & Token Usage",
15759
16086
  ` Provider : ${ctx.currentProvider}`,
@@ -15762,13 +16089,13 @@ async function handleCost(_args, ctx) {
15762
16089
  ` Output : ${u.outputTokens.toLocaleString()}`
15763
16090
  ];
15764
16091
  if (u.cacheCreationTokens > 0) lines.push(` Cache write : ${u.cacheCreationTokens.toLocaleString()}`);
15765
- if (u.cacheReadTokens > 0) {
15766
- const pct = Math.round(u.cacheReadTokens / (u.cacheReadTokens + u.inputTokens) * 100);
15767
- lines.push(` Cache read : ${u.cacheReadTokens.toLocaleString()} (${pct}% hit rate)`);
16092
+ if (report.cacheHitPct != null) {
16093
+ lines.push(` Cache read : ${u.cacheReadTokens.toLocaleString()} (${report.cacheHitPct}% hit rate)`);
15768
16094
  }
15769
16095
  lines.push(` Total tokens : ${total.toLocaleString()}`);
15770
16096
  if (cost != null) {
15771
- lines.push(` Cost : ${formatCost(cost)}`);
16097
+ const note = costSourceNote(report.costSource);
16098
+ lines.push(` Cost : ${formatCost(cost)}${note ? ` ${note}` : ""}`);
15772
16099
  if (savings != null && savings > 0) {
15773
16100
  lines.push(` Cache savings : -${formatCost(savings)} (vs no cache)`);
15774
16101
  }
@@ -16118,7 +16445,7 @@ async function handlePlugin(args, ctx) {
16118
16445
  lines.push("");
16119
16446
  }
16120
16447
  if (pluginTools.length > 0) {
16121
- const legacyDir = join23(configDir, PLUGINS_DIR_NAME);
16448
+ const legacyDir = join24(configDir, PLUGINS_DIR_NAME);
16122
16449
  lines.push(`**Legacy JS tools** (${pluginTools.length}):`);
16123
16450
  lines.push(`Dir: ${legacyDir}`);
16124
16451
  for (const t of pluginTools) lines.push(` \u2022 ${t.definition.name} \u2014 ${t.definition.description}`);
@@ -16242,8 +16569,8 @@ ${activated.meta.description || ""}` });
16242
16569
  }
16243
16570
  async function handleCommands(_args, ctx) {
16244
16571
  const configDir = ctx.config.getConfigDir();
16245
- const commandsDir = join23(configDir, CUSTOM_COMMANDS_DIR_NAME);
16246
- if (!existsSync28(commandsDir)) {
16572
+ const commandsDir = join24(configDir, CUSTOM_COMMANDS_DIR_NAME);
16573
+ if (!existsSync29(commandsDir)) {
16247
16574
  ctx.send({ type: "info", message: `No custom commands directory.
16248
16575
  Create: ${commandsDir}/ with .md files.` });
16249
16576
  return;
@@ -16349,7 +16676,7 @@ async function handleTest(args, ctx) {
16349
16676
  const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
16350
16677
  testArgs = isCommand ? { command: argStr } : { filter: argStr };
16351
16678
  }
16352
- const runTests = ctx.runTests ?? (await import("./run-tests-4XRHKHN5.js")).executeTests;
16679
+ const runTests = ctx.runTests ?? (await import("./run-tests-Y44SCLIO.js")).executeTests;
16353
16680
  const report = await runTests(testArgs);
16354
16681
  ctx.send({ type: "info", message: report });
16355
16682
  } catch (err) {
@@ -16373,9 +16700,9 @@ async function handleDiff(args, ctx) {
16373
16700
  let modifiedFiles = 0;
16374
16701
  const diffLines2 = [];
16375
16702
  for (const [filePath, { earliest }] of fileMap) {
16376
- const currentContent = existsSync28(filePath) ? (() => {
16703
+ const currentContent = existsSync29(filePath) ? (() => {
16377
16704
  try {
16378
- return readFileSync22(filePath, "utf-8");
16705
+ return readFileSync23(filePath, "utf-8");
16379
16706
  } catch {
16380
16707
  return null;
16381
16708
  }
@@ -16410,9 +16737,9 @@ async function handleDiff(args, ctx) {
16410
16737
  }
16411
16738
  async function handleInit(args, ctx) {
16412
16739
  const cwd = process.cwd();
16413
- const targetPath = join23(cwd, "AICLI.md");
16740
+ const targetPath = join24(cwd, "AICLI.md");
16414
16741
  const force = args.includes("--force");
16415
- if (existsSync28(targetPath) && !force) {
16742
+ if (existsSync29(targetPath) && !force) {
16416
16743
  ctx.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
16417
16744
  Use /init --force to overwrite.` });
16418
16745
  return;
@@ -16607,7 +16934,7 @@ async function handleMemory(args, ctx) {
16607
16934
  ctx.handleMemoryManage(sub, args[1], sub === "expire" ? args[2] : void 0);
16608
16935
  } else if (sub === "export") {
16609
16936
  if (args[1] === "md") {
16610
- const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-YPO25MOP.js");
16937
+ const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-YBWIZWQP.js");
16611
16938
  ctx.send({
16612
16939
  type: "export_data",
16613
16940
  format: "md",
@@ -16645,7 +16972,7 @@ Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path>
16645
16972
  return;
16646
16973
  }
16647
16974
  const dirPath = resolve9(sub);
16648
- if (!existsSync28(dirPath)) {
16975
+ if (!existsSync29(dirPath)) {
16649
16976
  ctx.send({ type: "error", message: `Directory not found: ${dirPath}` });
16650
16977
  return;
16651
16978
  }
@@ -16658,6 +16985,119 @@ Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path>
16658
16985
  It will be included in AI context for subsequent messages.` });
16659
16986
  }
16660
16987
 
16988
+ // src/core/profile-command.ts
16989
+ var PROFILE_FIELDS = ["name", "nickname", "role", "bio", "interests", "locale", "extra"];
16990
+ function stripSurroundingQuotes(value) {
16991
+ const pairs = [['"', '"'], ["'", "'"], ["\u201C", "\u201D"], ["\u2018", "\u2019"], ["\u300C", "\u300D"]];
16992
+ for (const [open, close] of pairs) {
16993
+ if (value.length >= 2 && value.startsWith(open) && value.endsWith(close)) {
16994
+ return value.slice(open.length, value.length - close.length).trim();
16995
+ }
16996
+ }
16997
+ return value;
16998
+ }
16999
+ function showRows(profile) {
17000
+ const rows = [];
17001
+ if (profile.name) rows.push(["Name", profile.name]);
17002
+ if (profile.nickname) rows.push(["Nickname", profile.nickname]);
17003
+ if (profile.role) rows.push(["Role", profile.role]);
17004
+ if (profile.bio) rows.push(["Bio", profile.bio]);
17005
+ if (profile.interests?.length) rows.push(["Interests", profile.interests.join(", ")]);
17006
+ if (profile.locale) rows.push(["Locale", profile.locale]);
17007
+ if (profile.extra) {
17008
+ rows.push(["Extra", profile.extra.length > 80 ? `${profile.extra.slice(0, 80)}\u2026` : profile.extra]);
17009
+ }
17010
+ return rows;
17011
+ }
17012
+ function applyProfileCommand(args, profile) {
17013
+ const sub = (args[0] ?? "show").toLowerCase();
17014
+ if (sub === "show" || sub === "view") {
17015
+ const rows = showRows(profile);
17016
+ if (rows.length === 0) {
17017
+ return {
17018
+ summary: "No profile configured yet.",
17019
+ rows: [],
17020
+ details: ["Set one with /profile set <field> <value> \u2014 it is injected into every conversation."],
17021
+ changed: false,
17022
+ isError: false
17023
+ };
17024
+ }
17025
+ return {
17026
+ summary: "\u{1F464} Your Profile",
17027
+ rows,
17028
+ details: ["This identity is injected into every AI conversation."],
17029
+ changed: false,
17030
+ isError: false
17031
+ };
17032
+ }
17033
+ if (sub === "clear") {
17034
+ return {
17035
+ profile: {},
17036
+ summary: "Profile cleared. The AI will no longer know who you are.",
17037
+ rows: [],
17038
+ details: [],
17039
+ changed: true,
17040
+ isError: false
17041
+ };
17042
+ }
17043
+ if (sub === "set") {
17044
+ const field = args[1];
17045
+ const value = args.slice(2).join(" ").trim();
17046
+ if (!field || !PROFILE_FIELDS.includes(field)) {
17047
+ return {
17048
+ summary: "Usage: /profile set <field> <value>",
17049
+ rows: [],
17050
+ details: [`Fields: ${PROFILE_FIELDS.join(", ")}`],
17051
+ changed: false,
17052
+ isError: true
17053
+ };
17054
+ }
17055
+ if (!value) {
17056
+ return {
17057
+ summary: `Usage: /profile set ${field} <value>`,
17058
+ rows: [],
17059
+ details: [],
17060
+ changed: false,
17061
+ isError: true
17062
+ };
17063
+ }
17064
+ const updated = { ...profile };
17065
+ if (field === "interests") {
17066
+ updated.interests = value.split(",").map((s) => stripSurroundingQuotes(s.trim())).filter(Boolean);
17067
+ } else {
17068
+ updated[field] = stripSurroundingQuotes(value);
17069
+ }
17070
+ const shown = field === "interests" ? (updated.interests ?? []).join(", ") : stripSurroundingQuotes(value);
17071
+ return {
17072
+ profile: updated,
17073
+ summary: `Profile updated: ${field} = ${shown}`,
17074
+ rows: [],
17075
+ details: [],
17076
+ changed: true,
17077
+ isError: false
17078
+ };
17079
+ }
17080
+ if (sub === "edit") {
17081
+ return {
17082
+ summary: "Interactive editing lives in the setup wizard.",
17083
+ rows: [],
17084
+ details: [
17085
+ "Run `aicli config` in a terminal, or use /profile set <field> <value> here.",
17086
+ `Fields: ${PROFILE_FIELDS.join(", ")}`
17087
+ ],
17088
+ changed: false,
17089
+ isError: false
17090
+ };
17091
+ }
17092
+ return {
17093
+ summary: `Unknown /profile option "${args[0]}" \u2014 expected show | set | clear | edit`,
17094
+ rows: [],
17095
+ details: [],
17096
+ changed: false,
17097
+ isError: true
17098
+ };
17099
+ }
17100
+
16661
17101
  // src/web/commands/security-commands.ts
16662
17102
  async function handleConfig(args, ctx) {
16663
17103
  const sub = args[0];
@@ -16719,6 +17159,18 @@ async function handleBug(_args, ctx) {
16719
17159
  ];
16720
17160
  ctx.send({ type: "info", message: lines.join("\n") });
16721
17161
  }
17162
+ async function handleProfile(args, ctx) {
17163
+ const outcome = applyProfileCommand(args, ctx.config.get("userProfile") ?? {});
17164
+ if (outcome.profile) {
17165
+ ctx.config.set("userProfile", outcome.profile);
17166
+ ctx.config.save();
17167
+ }
17168
+ const lines = [outcome.summary];
17169
+ for (const [label, value] of outcome.rows) lines.push(` ${label}: ${value}`);
17170
+ for (const line of outcome.details) lines.push(line);
17171
+ ctx.send({ type: outcome.isError ? "error" : "info", message: lines.join("\n") });
17172
+ if (outcome.changed) ctx.sendStatus();
17173
+ }
16722
17174
 
16723
17175
  // src/web/commands/agent-commands.ts
16724
17176
  async function handleAuto(args, ctx) {
@@ -16762,6 +17214,17 @@ async function handlePermissions(args, ctx) {
16762
17214
  }
16763
17215
  async function handleYolo(args, ctx) {
16764
17216
  const sub = args[0]?.toLowerCase();
17217
+ if (sub === "status") {
17218
+ ctx.send({
17219
+ type: "info",
17220
+ message: `YOLO mode: ${ctx.toolExecutor.sessionAutoApprove ? "ON (write tools auto-approved)" : "OFF"}`
17221
+ });
17222
+ return;
17223
+ }
17224
+ if (sub !== void 0 && sub !== "on" && sub !== "off") {
17225
+ ctx.send({ type: "error", message: `Unknown /yolo option "${sub}" \u2014 expected on | off | status` });
17226
+ return;
17227
+ }
16765
17228
  if (sub === "off") {
16766
17229
  ctx.toolExecutor.sessionAutoApprove = false;
16767
17230
  ctx.send({ type: "info", message: "\u{1F512} Auto-approve disabled \u2014 confirmations restored for this session." });
@@ -16973,14 +17436,17 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
16973
17436
  }
16974
17437
 
16975
17438
  // src/web/session-handler.ts
16976
- var CLI_ONLY_WEB_COMMANDS = /* @__PURE__ */ new Set([
16977
- "copy",
16978
- "paste",
16979
- "exit",
16980
- "quit",
16981
- "snapshot",
16982
- "hooks",
16983
- "hook"
17439
+ var CLI_ONLY_WEB_COMMANDS = /* @__PURE__ */ new Map([
17440
+ ["copy", "It copies to the terminal clipboard; use your browser/OS copy instead."],
17441
+ ["paste", "It reads the terminal clipboard; paste directly into the composer instead."],
17442
+ ["exit", "Close the browser tab to end this Web session."],
17443
+ ["quit", "Close the browser tab to end this Web session."],
17444
+ ["snapshot", "Dev-state snapshots are written when the terminal REPL exits."],
17445
+ ["hooks", "Hook trust is granted per machine from the terminal."],
17446
+ ["hook", "Hook trust is granted per machine from the terminal."],
17447
+ // 以下两条并非「终端才有意义」,而是 Web 侧尚未实现——如实说明,别假装是设计。
17448
+ ["route", "Smart model routing is not applied to Web sessions yet (config.routing is REPL-only)."],
17449
+ ["security", "Redaction status and toggles are terminal-only for now; /status shows the current profile."]
16984
17450
  ]);
16985
17451
  var webCommandHandlers = {
16986
17452
  provider: handleProvider,
@@ -17016,6 +17482,7 @@ var webCommandHandlers = {
17016
17482
  config: handleConfig,
17017
17483
  doctor: handleDoctor,
17018
17484
  bug: handleBug,
17485
+ profile: handleProfile,
17019
17486
  auto: handleAuto,
17020
17487
  permissions: handlePermissions,
17021
17488
  yolo: handleYolo,
@@ -17028,6 +17495,7 @@ var webCommandHandlers = {
17028
17495
  var SessionHandler = class {
17029
17496
  ws;
17030
17497
  config;
17498
+ costTracker;
17031
17499
  providers;
17032
17500
  sessions;
17033
17501
  toolRegistry;
@@ -17041,12 +17509,22 @@ var SessionHandler = class {
17041
17509
  runtimeThinkingEffort = null;
17042
17510
  sessionTokenUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
17043
17511
  unknownPricingWarnings = /* @__PURE__ */ new Set();
17512
+ /**
17513
+ * 本会话按**调用时刻**费率累计的花费(美元)。
17514
+ * 审计 P2-02 第二批:Web 端此前根本不给 CostTracker 记账,aicli usage 与
17515
+ * monthlyBudget 完全看不到 Web UI 的开销;顺带补上 P3-01 的记账口径。
17516
+ */
17517
+ sessionAccruedCostUsd = 0;
17518
+ sessionCostIncomplete = false;
17044
17519
  /** Accumulate a TokenUsage (with optional cache fields) into sessionTokenUsage. */
17045
17520
  addWebSessionUsage(u) {
17046
17521
  this.sessionTokenUsage.inputTokens += u.inputTokens;
17047
17522
  this.sessionTokenUsage.outputTokens += u.outputTokens;
17048
17523
  this.sessionTokenUsage.cacheCreationTokens += u.cacheCreationTokens ?? 0;
17049
17524
  this.sessionTokenUsage.cacheReadTokens += u.cacheReadTokens ?? 0;
17525
+ const priced = this.costTracker.addCost(this.currentProvider, this.currentModel, u);
17526
+ if (priced === null) this.sessionCostIncomplete = true;
17527
+ else this.sessionAccruedCostUsd += priced;
17050
17528
  const totalTokens = u.inputTokens + u.outputTokens + (u.cacheCreationTokens ?? 0) + (u.cacheReadTokens ?? 0);
17051
17529
  if (totalTokens > 0 && !getPricing(this.currentProvider, this.currentModel)) {
17052
17530
  const key = `${this.currentProvider}/${this.currentModel}`;
@@ -17058,6 +17536,8 @@ var SessionHandler = class {
17058
17536
  }
17059
17537
  resetWebSessionUsage() {
17060
17538
  this.sessionTokenUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
17539
+ this.sessionAccruedCostUsd = 0;
17540
+ this.sessionCostIncomplete = false;
17061
17541
  }
17062
17542
  abortController = null;
17063
17543
  userInterjection = null;
@@ -17091,6 +17571,7 @@ var SessionHandler = class {
17091
17571
  constructor(ws, shared) {
17092
17572
  this.ws = ws;
17093
17573
  this.config = shared.config;
17574
+ this.costTracker = shared.costTracker;
17094
17575
  this.providers = shared.providers;
17095
17576
  this.sessions = shared.sessions;
17096
17577
  this.toolRegistry = shared.toolRegistry;
@@ -17509,6 +17990,7 @@ var SessionHandler = class {
17509
17990
  } else {
17510
17991
  await this.handleChatSimple(provider, session.messages);
17511
17992
  }
17993
+ await this.checkContextPressure();
17512
17994
  } catch (err) {
17513
17995
  const parts = [];
17514
17996
  let current = err;
@@ -18027,10 +18509,11 @@ ${summaryContent}`,
18027
18509
  }
18028
18510
  // ── Commands ─────────────────────────────────────────────────────
18029
18511
  async handleCommand(name, args) {
18030
- if (CLI_ONLY_WEB_COMMANDS.has(name)) {
18512
+ const cliOnlyReason = CLI_ONLY_WEB_COMMANDS.get(name);
18513
+ if (cliOnlyReason) {
18031
18514
  this.send({
18032
18515
  type: "error",
18033
- message: `/${name} is only available in the terminal REPL. Run \`aicli\` in a terminal and use /${name} there.`
18516
+ message: `/${name} is not available in the Web UI. ${cliOnlyReason}`
18034
18517
  });
18035
18518
  return;
18036
18519
  }
@@ -18041,6 +18524,25 @@ ${summaryContent}`,
18041
18524
  this.send({ type: "error", message: `Unknown command: /${name}. Type /help for available commands.` });
18042
18525
  }
18043
18526
  }
18527
+ /**
18528
+ * 到达上下文压力阈值时自动压缩(与 REPL 同一判据,见 core/context-pressure.ts)。
18529
+ * 上下文窗口未知时不动——不基于猜测改用户的对话历史。
18530
+ */
18531
+ async checkContextPressure() {
18532
+ const contextWindow = this.getContextWindowSize();
18533
+ const session = this.sessions.current;
18534
+ const pressure = evaluateContextPressure({
18535
+ autoCompactEnabled: this.config.get("autoCompact") === true,
18536
+ estimatedTokens: contextWindow > 0 && session ? estimateConversationTokens({ systemPrompt: this.activeSystemPrompt, messages: session.messages }) : 0,
18537
+ contextWindow
18538
+ });
18539
+ if (!pressure.shouldCompact) return;
18540
+ this.send({
18541
+ type: "info",
18542
+ message: `\u26A0 Context pressure: ~${pressure.percent}% of the ${contextWindow.toLocaleString()}-token window used. Auto-compacting\u2026`
18543
+ });
18544
+ await this.compactSession(AUTO_COMPACT_INSTRUCTION);
18545
+ }
18044
18546
  async compactSession(instruction) {
18045
18547
  const session = this.sessions.current;
18046
18548
  if (!session || session.messages.length < 6) {
@@ -18527,8 +19029,8 @@ async function setupProxy(configProxy) {
18527
19029
  }
18528
19030
 
18529
19031
  // src/web/auth.ts
18530
- import { existsSync as existsSync29, readFileSync as readFileSync23, writeFileSync as writeFileSync4, mkdirSync as mkdirSync15, readdirSync as readdirSync14, copyFileSync } from "fs";
18531
- import { isAbsolute as isAbsolute3, join as join24, relative as relative5 } from "path";
19032
+ import { existsSync as existsSync30, readFileSync as readFileSync24, writeFileSync as writeFileSync4, mkdirSync as mkdirSync15, readdirSync as readdirSync14, copyFileSync } from "fs";
19033
+ import { isAbsolute as isAbsolute3, join as join25, relative as relative5 } from "path";
18532
19034
  import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
18533
19035
  var USERS_FILE = "users.json";
18534
19036
  var TOKEN_EXPIRY_HOURS = 24;
@@ -18543,7 +19045,7 @@ var AuthManager = class {
18543
19045
  db;
18544
19046
  constructor(baseDir) {
18545
19047
  this.baseDir = baseDir;
18546
- this.usersFile = join24(baseDir, USERS_FILE);
19048
+ this.usersFile = join25(baseDir, USERS_FILE);
18547
19049
  this.db = this.loadOrCreate();
18548
19050
  }
18549
19051
  // ── Public API ─────────────────────────────────────────────────
@@ -18572,9 +19074,9 @@ var AuthManager = class {
18572
19074
  }
18573
19075
  const salt = randomBytes(16).toString("hex");
18574
19076
  const passwordHash = this.hashPassword(password, salt);
18575
- const dataDir = join24(USERS_DIR, username);
18576
- const fullDataDir = join24(this.baseDir, dataDir);
18577
- mkdirSync15(join24(fullDataDir, "history"), { recursive: true });
19077
+ const dataDir = join25(USERS_DIR, username);
19078
+ const fullDataDir = join25(this.baseDir, dataDir);
19079
+ mkdirSync15(join25(fullDataDir, "history"), { recursive: true });
18578
19080
  const user = {
18579
19081
  username,
18580
19082
  passwordHash,
@@ -18689,7 +19191,7 @@ var AuthManager = class {
18689
19191
  getUserDataDir(username) {
18690
19192
  const user = this.db.users.find((u) => u.username === username);
18691
19193
  if (!user) throw new Error(`User not found: ${username}`);
18692
- return join24(this.baseDir, user.dataDir);
19194
+ return join25(this.baseDir, user.dataDir);
18693
19195
  }
18694
19196
  /** List all usernames */
18695
19197
  listUsers() {
@@ -18725,30 +19227,30 @@ var AuthManager = class {
18725
19227
  const err = this.register(username, password);
18726
19228
  if (err) return err;
18727
19229
  const userDir = this.getUserDataDir(username);
18728
- const globalConfig = join24(this.baseDir, "config.json");
18729
- if (existsSync29(globalConfig)) {
19230
+ const globalConfig = join25(this.baseDir, "config.json");
19231
+ if (existsSync30(globalConfig)) {
18730
19232
  try {
18731
- const content = readFileSync23(globalConfig, "utf-8");
18732
- writeFileSync4(join24(userDir, "config.json"), content, "utf-8");
19233
+ const content = readFileSync24(globalConfig, "utf-8");
19234
+ writeFileSync4(join25(userDir, "config.json"), content, "utf-8");
18733
19235
  } catch {
18734
19236
  }
18735
19237
  }
18736
- const globalMemory = join24(this.baseDir, "memory.md");
18737
- if (existsSync29(globalMemory)) {
19238
+ const globalMemory = join25(this.baseDir, "memory.md");
19239
+ if (existsSync30(globalMemory)) {
18738
19240
  try {
18739
- const content = readFileSync23(globalMemory, "utf-8");
18740
- writeFileSync4(join24(userDir, "memory.md"), content, "utf-8");
19241
+ const content = readFileSync24(globalMemory, "utf-8");
19242
+ writeFileSync4(join25(userDir, "memory.md"), content, "utf-8");
18741
19243
  } catch {
18742
19244
  }
18743
19245
  }
18744
- const globalHistory = join24(this.baseDir, "history");
18745
- if (existsSync29(globalHistory)) {
19246
+ const globalHistory = join25(this.baseDir, "history");
19247
+ if (existsSync30(globalHistory)) {
18746
19248
  try {
18747
19249
  const files = readdirSync14(globalHistory).filter((f) => f.endsWith(".json"));
18748
- const userHistory = join24(userDir, "history");
19250
+ const userHistory = join25(userDir, "history");
18749
19251
  for (const f of files) {
18750
19252
  try {
18751
- copyFileSync(join24(globalHistory, f), join24(userHistory, f));
19253
+ copyFileSync(join25(globalHistory, f), join25(userHistory, f));
18752
19254
  } catch {
18753
19255
  }
18754
19256
  }
@@ -18759,9 +19261,9 @@ var AuthManager = class {
18759
19261
  }
18760
19262
  // ── Private methods ────────────────────────────────────────────
18761
19263
  loadOrCreate() {
18762
- if (existsSync29(this.usersFile)) {
19264
+ if (existsSync30(this.usersFile)) {
18763
19265
  try {
18764
- const parsed = JSON.parse(readFileSync23(this.usersFile, "utf-8"));
19266
+ const parsed = JSON.parse(readFileSync24(this.usersFile, "utf-8"));
18765
19267
  if (!this.isValidUsersDB(parsed)) {
18766
19268
  throw new Error("database structure is invalid");
18767
19269
  }
@@ -18795,7 +19297,7 @@ var AuthManager = class {
18795
19297
  if (typeof record.username !== "string" || !/^[a-z0-9_-]{2,32}$/.test(record.username) || usernames.has(record.username) || typeof record.passwordHash !== "string" || record.passwordHash.length === 0 || typeof record.salt !== "string" || record.salt.length === 0 || typeof record.createdAt !== "string" || typeof record.dataDir !== "string" || record.dataDir.length === 0 || isAbsolute3(record.dataDir)) {
18796
19298
  return false;
18797
19299
  }
18798
- const dataDirFromBase = relative5(this.baseDir, join24(this.baseDir, record.dataDir));
19300
+ const dataDirFromBase = relative5(this.baseDir, join25(this.baseDir, record.dataDir));
18799
19301
  if (dataDirFromBase.startsWith("..") || isAbsolute3(dataDirFromBase)) return false;
18800
19302
  if (record.hashVersion !== void 0 && record.hashVersion !== 1 && record.hashVersion !== 2) return false;
18801
19303
  if (record.tokensRevokedBefore !== void 0 && !Number.isFinite(record.tokensRevokedBefore)) return false;
@@ -18911,8 +19413,8 @@ async function startWebServer(options = {}) {
18911
19413
  }
18912
19414
  }
18913
19415
  let skillManager = null;
18914
- const skillsDir = join25(config.getConfigDir(), SKILLS_DIR_NAME);
18915
- if (existsSync30(skillsDir)) {
19416
+ const skillsDir = join26(config.getConfigDir(), SKILLS_DIR_NAME);
19417
+ if (existsSync31(skillsDir)) {
18916
19418
  skillManager = new SkillManager(skillsDir, config.get("ui").skillSizeWarn);
18917
19419
  skillManager.loadSkills(pluginAssets.skillDirs);
18918
19420
  const count = skillManager.listSkills().length;
@@ -18922,6 +19424,7 @@ async function startWebServer(options = {}) {
18922
19424
  }
18923
19425
  const shared = {
18924
19426
  config,
19427
+ costTracker: new CostTracker(config.getConfigDir()),
18925
19428
  providers,
18926
19429
  sessions,
18927
19430
  toolRegistry,
@@ -18987,18 +19490,18 @@ async function startWebServer(options = {}) {
18987
19490
  next();
18988
19491
  };
18989
19492
  const moduleDir = getModuleDir();
18990
- let clientDir = join25(moduleDir, "web", "client");
18991
- if (!existsSync30(clientDir)) {
18992
- clientDir = join25(moduleDir, "client");
19493
+ let clientDir = join26(moduleDir, "web", "client");
19494
+ if (!existsSync31(clientDir)) {
19495
+ clientDir = join26(moduleDir, "client");
18993
19496
  }
18994
- if (!existsSync30(clientDir)) {
18995
- clientDir = join25(moduleDir, "..", "..", "src", "web", "client");
19497
+ if (!existsSync31(clientDir)) {
19498
+ clientDir = join26(moduleDir, "..", "..", "src", "web", "client");
18996
19499
  }
18997
- if (!existsSync30(clientDir)) {
18998
- clientDir = join25(process.cwd(), "src", "web", "client");
19500
+ if (!existsSync31(clientDir)) {
19501
+ clientDir = join26(process.cwd(), "src", "web", "client");
18999
19502
  }
19000
19503
  console.log(` Static files: ${clientDir}`);
19001
- app.use("/vendor", express.static(join25(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
19504
+ app.use("/vendor", express.static(join26(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
19002
19505
  app.use(express.static(clientDir));
19003
19506
  app.get("/api/status", (_req, res) => {
19004
19507
  res.json({
@@ -19065,7 +19568,7 @@ async function startWebServer(options = {}) {
19065
19568
  app.get("/api/files", requireAuth, (req, res) => {
19066
19569
  const cwd = process.cwd();
19067
19570
  const prefix = req.query.prefix || "";
19068
- const targetDir = join25(cwd, prefix);
19571
+ const targetDir = join26(cwd, prefix);
19069
19572
  try {
19070
19573
  const canonicalTarget = realpathSync3(resolve10(targetDir));
19071
19574
  const canonicalCwd = realpathSync3(resolve10(cwd));
@@ -19082,7 +19585,7 @@ async function startWebServer(options = {}) {
19082
19585
  const entries = readdirSync15(targetDir, { withFileTypes: true });
19083
19586
  const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
19084
19587
  name: e.name,
19085
- path: relative6(cwd, join25(targetDir, e.name)).replace(/\\/g, "/"),
19588
+ path: relative6(cwd, join26(targetDir, e.name)).replace(/\\/g, "/"),
19086
19589
  isDir: e.isDirectory()
19087
19590
  }));
19088
19591
  res.json({ files });
@@ -19122,8 +19625,8 @@ async function startWebServer(options = {}) {
19122
19625
  try {
19123
19626
  const authUser = req._authUser;
19124
19627
  const histDir = authUser ? getUserShared(authUser).config.getHistoryDir() : config.getHistoryDir();
19125
- const filePath = join25(histDir, `${id}.json`);
19126
- if (!existsSync30(filePath)) {
19628
+ const filePath = join26(histDir, `${id}.json`);
19629
+ if (!existsSync31(filePath)) {
19127
19630
  res.status(404).json({ error: "Session not found" });
19128
19631
  return;
19129
19632
  }
@@ -19138,7 +19641,7 @@ async function startWebServer(options = {}) {
19138
19641
  res.status(404).json({ error: "Session not found" });
19139
19642
  return;
19140
19643
  }
19141
- const data = JSON.parse(readFileSync24(filePath, "utf-8"));
19644
+ const data = JSON.parse(readFileSync25(filePath, "utf-8"));
19142
19645
  res.json({ session: data });
19143
19646
  } catch (err) {
19144
19647
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
@@ -19151,7 +19654,7 @@ async function startWebServer(options = {}) {
19151
19654
  return;
19152
19655
  }
19153
19656
  const cwd = process.cwd();
19154
- const fullPath = resolve10(join25(cwd, filePath));
19657
+ const fullPath = resolve10(join26(cwd, filePath));
19155
19658
  try {
19156
19659
  const canonicalFull = realpathSync3(fullPath);
19157
19660
  const canonicalCwd = realpathSync3(resolve10(cwd));
@@ -19169,7 +19672,7 @@ async function startWebServer(options = {}) {
19169
19672
  res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
19170
19673
  return;
19171
19674
  }
19172
- const content = readFileSync24(fullPath, "utf-8");
19675
+ const content = readFileSync25(fullPath, "utf-8");
19173
19676
  res.json({ content, size: stat.size });
19174
19677
  } catch {
19175
19678
  res.json({ error: "Cannot read file" });
@@ -19188,6 +19691,8 @@ async function startWebServer(options = {}) {
19188
19691
  const userSessions = new SessionManager(userConfig);
19189
19692
  const userShared = {
19190
19693
  config: userConfig,
19694
+ // 每用户各自的 config 目录 → 各自的 cost-history.json
19695
+ costTracker: new CostTracker(userConfig.getConfigDir()),
19191
19696
  providers,
19192
19697
  // Shared — providers re-read API keys per call
19193
19698
  sessions: userSessions,
@@ -19424,17 +19929,17 @@ function resolveProjectMcpPath() {
19424
19929
  const cwd = process.cwd();
19425
19930
  const gitRoot = getGitRoot(cwd);
19426
19931
  const projectRoot = gitRoot ?? cwd;
19427
- const configPath = join25(projectRoot, MCP_PROJECT_CONFIG_NAME);
19428
- return existsSync30(configPath) ? configPath : null;
19932
+ const configPath = join26(projectRoot, MCP_PROJECT_CONFIG_NAME);
19933
+ return existsSync31(configPath) ? configPath : null;
19429
19934
  }
19430
19935
  function loadProjectMcpConfig() {
19431
19936
  const cwd = process.cwd();
19432
19937
  const gitRoot = getGitRoot(cwd);
19433
19938
  const projectRoot = gitRoot ?? cwd;
19434
- const configPath = join25(projectRoot, MCP_PROJECT_CONFIG_NAME);
19435
- if (!existsSync30(configPath)) return null;
19939
+ const configPath = join26(projectRoot, MCP_PROJECT_CONFIG_NAME);
19940
+ if (!existsSync31(configPath)) return null;
19436
19941
  try {
19437
- const raw = JSON.parse(readFileSync24(configPath, "utf-8"));
19942
+ const raw = JSON.parse(readFileSync25(configPath, "utf-8"));
19438
19943
  return raw.mcpServers ?? raw;
19439
19944
  } catch {
19440
19945
  return null;