jinzd-ai-cli 0.4.118 → 0.4.120

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.
@@ -1,27 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- schemaToJsonSchema,
4
- truncateForPersist
5
- } from "./chunk-7QZOLJ2R.js";
3
+ schemaToJsonSchema
4
+ } from "./chunk-UEXBJEVV.js";
6
5
  import {
7
6
  AuthError,
8
7
  ProviderError,
9
8
  ProviderNotFoundError,
10
9
  RateLimitError
11
10
  } from "./chunk-2ZD3YTVM.js";
12
- import {
13
- APP_NAME,
14
- CONFIG_DIR_NAME,
15
- DEV_STATE_FILE_NAME,
16
- MCP_CALL_TIMEOUT,
17
- MCP_CONNECT_TIMEOUT,
18
- MCP_PROTOCOL_VERSION,
19
- MCP_TOOL_PREFIX,
20
- VERSION
21
- } from "./chunk-LO4MSGGO.js";
22
- import {
23
- redactJson
24
- } from "./chunk-7ZJN4KLV.js";
25
11
 
26
12
  // src/providers/claude.ts
27
13
  import Anthropic from "@anthropic-ai/sdk";
@@ -1768,12 +1754,12 @@ function findPhantomClaims(content, extraMessages) {
1768
1754
  const claimed = extractClaimedFilePaths(content);
1769
1755
  if (claimed.length === 0) return [];
1770
1756
  const normalize = (p) => p.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
1771
- const basename2 = (p) => {
1757
+ const basename = (p) => {
1772
1758
  const parts = normalize(p).split("/");
1773
1759
  return parts[parts.length - 1] ?? "";
1774
1760
  };
1775
1761
  const written = extractWrittenFilePaths(extraMessages).map(normalize);
1776
- const writtenBases = new Set(written.map(basename2));
1762
+ const writtenBases = new Set(written.map(basename));
1777
1763
  const writtenFull = new Set(written);
1778
1764
  return claimed.filter((raw) => {
1779
1765
  const norm = normalize(raw);
@@ -1781,7 +1767,7 @@ function findPhantomClaims(content, extraMessages) {
1781
1767
  for (const w of writtenFull) {
1782
1768
  if (w.endsWith("/" + norm) || norm.endsWith("/" + w)) return false;
1783
1769
  }
1784
- if (writtenBases.has(basename2(norm))) return false;
1770
+ if (writtenBases.has(basename(norm))) return false;
1785
1771
  return true;
1786
1772
  });
1787
1773
  }
@@ -2466,6 +2452,14 @@ var ProviderRegistry = class {
2466
2452
  this.providers.set(cfg.id, provider);
2467
2453
  }
2468
2454
  }
2455
+ /**
2456
+ * Register a provider instance under the given id (overwrites any existing).
2457
+ * Primary uses: integration tests injecting MockProvider, and future hot-swap
2458
+ * scenarios. The provider is assumed already initialized.
2459
+ */
2460
+ register(id, provider) {
2461
+ this.providers.set(id, provider);
2462
+ }
2469
2463
  get(id) {
2470
2464
  const provider = this.providers.get(id);
2471
2465
  if (!provider) {
@@ -2504,1698 +2498,6 @@ var ProviderRegistry = class {
2504
2498
  }
2505
2499
  };
2506
2500
 
2507
- // src/session/session-manager.ts
2508
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync, renameSync, openSync, readSync, closeSync } from "fs";
2509
- import { join } from "path";
2510
- import { v4 as uuidv4 } from "uuid";
2511
-
2512
- // src/core/types.ts
2513
- function getContentText(content) {
2514
- if (typeof content === "string") return content;
2515
- return content.filter((p) => p.type === "text").map((p) => p.text).join("");
2516
- }
2517
-
2518
- // src/session/session.ts
2519
- function makeBranchId() {
2520
- return Math.random().toString(16).slice(2, 8);
2521
- }
2522
- function messagesEqual(a, b) {
2523
- if (a.role !== b.role) return false;
2524
- if (JSON.stringify(a.content) !== JSON.stringify(b.content)) return false;
2525
- if (JSON.stringify(a.toolCalls ?? null) !== JSON.stringify(b.toolCalls ?? null)) return false;
2526
- return true;
2527
- }
2528
- var Session = class _Session {
2529
- id;
2530
- provider;
2531
- model;
2532
- created;
2533
- updated;
2534
- messages = [];
2535
- title;
2536
- tokenUsage = {
2537
- inputTokens: 0,
2538
- outputTokens: 0,
2539
- cacheCreationTokens: 0,
2540
- cacheReadTokens: 0
2541
- };
2542
- checkpoints = [];
2543
- // ── B2 Branches (v0.4.74+) ──────────────────────────────────────
2544
- /**
2545
- * All branches in this session. The 'main' branch is auto-created and
2546
- * represents the linear conversation for pre-B2 sessions.
2547
- */
2548
- branches = [];
2549
- /** Currently active branch — its messages live in `this.messages`. */
2550
- activeBranchId = "main";
2551
- /**
2552
- * Stashed message arrays for INACTIVE branches. The active branch's
2553
- * messages are always in `this.messages`, never duplicated here.
2554
- */
2555
- _inactiveBranchMessages = /* @__PURE__ */ new Map();
2556
- constructor(id, provider, model) {
2557
- this.id = id;
2558
- this.provider = provider;
2559
- this.model = model;
2560
- this.created = /* @__PURE__ */ new Date();
2561
- this.updated = /* @__PURE__ */ new Date();
2562
- this.branches.push({
2563
- id: "main",
2564
- title: "main",
2565
- parentBranchId: null,
2566
- parentMessageIndex: 0,
2567
- created: this.created
2568
- });
2569
- }
2570
- /**
2571
- * 更新 session 关联的 provider 和 model(在 /provider 或 /model 切换时调用)。
2572
- * 保留对话历史和所有状态,仅更新元数据。
2573
- */
2574
- updateProvider(provider, model) {
2575
- this.provider = provider;
2576
- this.model = model;
2577
- this.updated = /* @__PURE__ */ new Date();
2578
- }
2579
- addMessage(message) {
2580
- this.messages.push(message);
2581
- this.updated = /* @__PURE__ */ new Date();
2582
- if (!this.title && message.role === "user") {
2583
- this.title = getContentText(message.content).slice(0, 50).replace(/\n/g, " ");
2584
- }
2585
- }
2586
- addTokenUsage(usage) {
2587
- this.tokenUsage.inputTokens += usage.inputTokens;
2588
- this.tokenUsage.outputTokens += usage.outputTokens;
2589
- this.tokenUsage.cacheCreationTokens += usage.cacheCreationTokens ?? 0;
2590
- this.tokenUsage.cacheReadTokens += usage.cacheReadTokens ?? 0;
2591
- }
2592
- clear() {
2593
- this.messages = [];
2594
- this.title = void 0;
2595
- this.tokenUsage = {
2596
- inputTokens: 0,
2597
- outputTokens: 0,
2598
- cacheCreationTokens: 0,
2599
- cacheReadTokens: 0
2600
- };
2601
- this.updated = /* @__PURE__ */ new Date();
2602
- }
2603
- /**
2604
- * 上下文压缩:用摘要消息替换旧消息,保留最近 keepLast 条。
2605
- *
2606
- * Tool-history-aware: if the cut point lands inside a tool round
2607
- * (assistant+toolCalls followed by tool results), expand to keep the
2608
- * entire round intact. This prevents orphaned tool results.
2609
- *
2610
- * 压缩后消息结构:
2611
- * [summaryMsg(user), ackMsg(assistant), ...最近 N 条原始消息]
2612
- *
2613
- * @returns 被删除的消息条数
2614
- */
2615
- compact(summaryMsg, ackMsg, keepLast) {
2616
- let cutIndex = this.messages.length - keepLast;
2617
- if (cutIndex <= 0) {
2618
- return 0;
2619
- }
2620
- while (cutIndex > 0 && this.messages[cutIndex]?.role === "tool") {
2621
- cutIndex--;
2622
- }
2623
- const preserved = this.messages.slice(cutIndex);
2624
- const removedCount = cutIndex;
2625
- this.messages = [summaryMsg, ackMsg, ...preserved];
2626
- this.updated = /* @__PURE__ */ new Date();
2627
- return removedCount;
2628
- }
2629
- /** 在当前消息位置创建检查点 */
2630
- createCheckpoint(name) {
2631
- this.checkpoints = this.checkpoints.filter((c) => c.name !== name);
2632
- this.checkpoints.push({
2633
- name,
2634
- messageIndex: this.messages.length,
2635
- timestamp: /* @__PURE__ */ new Date()
2636
- });
2637
- }
2638
- /** 恢复到指定检查点:截断消息到检查点位置,移除后续检查点 */
2639
- restoreCheckpoint(name) {
2640
- const cp = this.checkpoints.find((c) => c.name === name);
2641
- if (!cp) return false;
2642
- this.messages = this.messages.slice(0, cp.messageIndex);
2643
- this.checkpoints = this.checkpoints.filter((c) => c.messageIndex <= cp.messageIndex);
2644
- this.updated = /* @__PURE__ */ new Date();
2645
- return true;
2646
- }
2647
- listCheckpoints() {
2648
- return [...this.checkpoints];
2649
- }
2650
- deleteCheckpoint(name) {
2651
- const len = this.checkpoints.length;
2652
- this.checkpoints = this.checkpoints.filter((c) => c.name !== name);
2653
- return this.checkpoints.length < len;
2654
- }
2655
- // ── B2 Branch operations ────────────────────────────────────────
2656
- /** Deep-clone a messages array (matches `Session.fork` semantics). */
2657
- static cloneMessages(msgs) {
2658
- return msgs.map((m) => {
2659
- const cloned = { ...m };
2660
- if (Array.isArray(cloned.content)) {
2661
- cloned.content = cloned.content.map(
2662
- (part) => typeof part === "object" && part !== null ? { ...part } : part
2663
- );
2664
- }
2665
- if (cloned.toolCalls) {
2666
- cloned.toolCalls = cloned.toolCalls.map((tc) => ({ ...tc }));
2667
- }
2668
- return cloned;
2669
- });
2670
- }
2671
- /** List all branches (metadata only). */
2672
- listBranches() {
2673
- return this.branches.map((b) => ({ ...b }));
2674
- }
2675
- /** Current active branch metadata. */
2676
- getActiveBranch() {
2677
- const b = this.branches.find((b2) => b2.id === this.activeBranchId);
2678
- if (!b) {
2679
- this.activeBranchId = this.branches[0]?.id ?? "main";
2680
- return this.branches[0] ?? { id: "main", title: "main", parentBranchId: null, parentMessageIndex: 0, created: /* @__PURE__ */ new Date() };
2681
- }
2682
- return b;
2683
- }
2684
- /**
2685
- * Create a new branch by forking the active branch at message index
2686
- * `fromIndex`. Copies `messages[0..fromIndex]` into the new branch
2687
- * and switches to it. The original active branch is preserved intact
2688
- * in the stash.
2689
- *
2690
- * @returns new branch id
2691
- * @throws if fromIndex is out of range
2692
- */
2693
- createBranch(fromIndex, title) {
2694
- if (fromIndex < 0 || fromIndex > this.messages.length) {
2695
- throw new Error(
2696
- `createBranch: fromIndex ${fromIndex} out of range [0, ${this.messages.length}]`
2697
- );
2698
- }
2699
- this._inactiveBranchMessages.set(this.activeBranchId, this.messages);
2700
- const id = makeBranchId();
2701
- const meta = {
2702
- id,
2703
- title: title || `branch-${this.branches.length + 1}`,
2704
- parentBranchId: this.activeBranchId,
2705
- parentMessageIndex: fromIndex,
2706
- created: /* @__PURE__ */ new Date()
2707
- };
2708
- this.branches.push(meta);
2709
- this.messages = _Session.cloneMessages(this.messages.slice(0, fromIndex));
2710
- this.activeBranchId = id;
2711
- this.updated = /* @__PURE__ */ new Date();
2712
- return id;
2713
- }
2714
- /**
2715
- * Switch the active branch. Stashes current messages under the old
2716
- * active id and loads the target branch's messages into `this.messages`.
2717
- *
2718
- * @returns true if switched, false if id not found or already active
2719
- */
2720
- switchBranch(id) {
2721
- if (id === this.activeBranchId) return false;
2722
- if (!this.branches.some((b) => b.id === id)) return false;
2723
- this._inactiveBranchMessages.set(this.activeBranchId, this.messages);
2724
- const target = this._inactiveBranchMessages.get(id) ?? [];
2725
- this._inactiveBranchMessages.delete(id);
2726
- this.messages = target;
2727
- this.activeBranchId = id;
2728
- this.updated = /* @__PURE__ */ new Date();
2729
- return true;
2730
- }
2731
- /**
2732
- * Delete a branch by id. Cannot delete the active branch or the last
2733
- * remaining branch. If other branches list this one as parent, their
2734
- * parent pointer is retargeted to this branch's parent (transparent
2735
- * to callers — branches still form a valid forest).
2736
- *
2737
- * @returns true if deleted
2738
- */
2739
- deleteBranch(id) {
2740
- if (id === this.activeBranchId) return false;
2741
- if (this.branches.length <= 1) return false;
2742
- const idx = this.branches.findIndex((b) => b.id === id);
2743
- if (idx === -1) return false;
2744
- const deleted = this.branches[idx];
2745
- for (const b of this.branches) {
2746
- if (b.parentBranchId === id) {
2747
- b.parentBranchId = deleted.parentBranchId;
2748
- }
2749
- }
2750
- this.branches.splice(idx, 1);
2751
- this._inactiveBranchMessages.delete(id);
2752
- this.updated = /* @__PURE__ */ new Date();
2753
- return true;
2754
- }
2755
- /** Rename a branch (affects only display title). */
2756
- renameBranch(id, newTitle) {
2757
- const b = this.branches.find((b2) => b2.id === id);
2758
- if (!b) return false;
2759
- b.title = newTitle;
2760
- this.updated = /* @__PURE__ */ new Date();
2761
- return true;
2762
- }
2763
- /**
2764
- * Resolve a user-supplied branch reference (v0.4.81+).
2765
- *
2766
- * Accepts the 6-hex branch id, the branch's display title, or an unambiguous
2767
- * id prefix (≥2 chars). This exists because users naturally type the title
2768
- * they chose at `/branch new`, not the auto-generated id — every other
2769
- * `/branch <sub>` used to error on that with "not found".
2770
- *
2771
- * Resolution order:
2772
- * 1. Exact id match
2773
- * 2. Exact title match (unique)
2774
- * 3. Id prefix match (unique, length ≥ 2)
2775
- *
2776
- * @returns `{ ok: true, id }` on success; on failure `{ ok: false, reason, matches }`
2777
- * where `matches` lists candidates for ambiguous input so callers
2778
- * can render a helpful error.
2779
- */
2780
- resolveBranchRef(ref) {
2781
- if (!ref) return { ok: false, reason: "not-found", matches: [] };
2782
- const byId = this.branches.find((b) => b.id === ref);
2783
- if (byId) return { ok: true, id: byId.id };
2784
- const byTitle = this.branches.filter((b) => b.title === ref);
2785
- if (byTitle.length === 1) return { ok: true, id: byTitle[0].id };
2786
- if (byTitle.length > 1) return { ok: false, reason: "ambiguous", matches: byTitle };
2787
- if (ref.length >= 2) {
2788
- const byPrefix = this.branches.filter((b) => b.id.startsWith(ref));
2789
- if (byPrefix.length === 1) return { ok: true, id: byPrefix[0].id };
2790
- if (byPrefix.length > 1) return { ok: false, reason: "ambiguous", matches: byPrefix };
2791
- }
2792
- return { ok: false, reason: "not-found", matches: [] };
2793
- }
2794
- /** Messages of any branch (active or inactive) — read-only copy. */
2795
- getBranchMessages(id) {
2796
- if (id === this.activeBranchId) return this.messages.slice();
2797
- const m = this._inactiveBranchMessages.get(id);
2798
- return m ? m.slice() : null;
2799
- }
2800
- // ── B3 Branch diff + cherry-pick (v0.4.80+) ────────────────────────
2801
- /**
2802
- * Compare messages between two branches. Finds the longest common prefix
2803
- * by message equality (role + content + toolCalls shape) — NOT by fork
2804
- * point metadata, so user-edited histories still diff cleanly.
2805
- *
2806
- * @param sourceId Branch to compare
2807
- * @param targetId Branch to compare against (defaults to active branch)
2808
- * @returns BranchDiff, or null if either branch id is unknown
2809
- */
2810
- diffBranches(sourceId, targetId) {
2811
- const tgt = targetId ?? this.activeBranchId;
2812
- const src = this.getBranchMessages(sourceId);
2813
- const dst = this.getBranchMessages(tgt);
2814
- if (!src || !dst) return null;
2815
- let i = 0;
2816
- while (i < src.length && i < dst.length && messagesEqual(src[i], dst[i])) i++;
2817
- return {
2818
- sourceId,
2819
- targetId: tgt,
2820
- commonPrefix: i,
2821
- sourceOnly: src.slice(i),
2822
- targetOnly: dst.slice(i)
2823
- };
2824
- }
2825
- /**
2826
- * Copy a single message from another branch into the active branch
2827
- * (appended at the end). Timestamp is reset to now so it's clear when
2828
- * the cherry-pick happened.
2829
- *
2830
- * @returns the cloned message, or null if sourceId / msgIndex is invalid
2831
- */
2832
- cherryPickMessage(sourceId, msgIndex) {
2833
- const src = this.getBranchMessages(sourceId);
2834
- if (!src) return null;
2835
- if (msgIndex < 0 || msgIndex >= src.length) return null;
2836
- const orig = src[msgIndex];
2837
- const copy = {
2838
- ...orig,
2839
- timestamp: /* @__PURE__ */ new Date()
2840
- };
2841
- this.messages.push(copy);
2842
- this.updated = /* @__PURE__ */ new Date();
2843
- return copy;
2844
- }
2845
- getMeta() {
2846
- return {
2847
- id: this.id,
2848
- provider: this.provider,
2849
- model: this.model,
2850
- messageCount: this.messages.length,
2851
- created: this.created,
2852
- updated: this.updated,
2853
- title: this.title
2854
- };
2855
- }
2856
- toJSON() {
2857
- const serializeMessages = (msgs) => msgs.map((m) => {
2858
- const out = {
2859
- role: m.role,
2860
- content: m.content,
2861
- timestamp: m.timestamp.toISOString()
2862
- };
2863
- if (m.toolCalls) out.toolCalls = m.toolCalls;
2864
- if (m.reasoningContent !== void 0) out.reasoningContent = m.reasoningContent;
2865
- if (m.toolCallId) out.toolCallId = m.toolCallId;
2866
- if (m.toolName) out.toolName = m.toolName;
2867
- if (m.isError !== void 0) out.isError = m.isError;
2868
- return out;
2869
- });
2870
- const branchMessages = {};
2871
- for (const [id, msgs] of this._inactiveBranchMessages.entries()) {
2872
- branchMessages[id] = serializeMessages(msgs);
2873
- }
2874
- return {
2875
- id: this.id,
2876
- provider: this.provider,
2877
- model: this.model,
2878
- created: this.created.toISOString(),
2879
- updated: this.updated.toISOString(),
2880
- title: this.title,
2881
- tokenUsage: { ...this.tokenUsage },
2882
- checkpoints: this.checkpoints.map((c) => ({
2883
- name: c.name,
2884
- messageIndex: c.messageIndex,
2885
- timestamp: c.timestamp.toISOString()
2886
- })),
2887
- // B2 Branches (v0.4.74+). Omitted for sessions with only the default
2888
- // 'main' branch and no stashed messages (keeps file size identical
2889
- // to pre-B2 for the common case).
2890
- ...this.branches.length > 1 || this._inactiveBranchMessages.size > 0 ? {
2891
- activeBranchId: this.activeBranchId,
2892
- branches: this.branches.map((b) => ({
2893
- id: b.id,
2894
- title: b.title,
2895
- parentBranchId: b.parentBranchId,
2896
- parentMessageIndex: b.parentMessageIndex,
2897
- created: b.created.toISOString()
2898
- })),
2899
- branchMessages
2900
- } : {},
2901
- messages: serializeMessages(this.messages)
2902
- };
2903
- }
2904
- /**
2905
- * 从现有 session 分叉创建新 session。
2906
- *
2907
- * 复制 messages[0..messageCount],保留范围内的 checkpoints。
2908
- * 新 session 拥有独立 UUID,title 默认 "Fork of <原标题>"。
2909
- *
2910
- * @param original 原始 session
2911
- * @param newId 新 session 的 UUID
2912
- * @param messageCount 复制的消息数量(0 = 空 session,> messages.length 则取全部)
2913
- * @param newTitle 可选的新标题
2914
- */
2915
- static fork(original, newId, messageCount, newTitle) {
2916
- const forked = new _Session(newId, original.provider, original.model);
2917
- forked.title = newTitle ?? (original.title ? `Fork of ${original.title}` : void 0);
2918
- const clampedCount = Math.min(Math.max(messageCount, 0), original.messages.length);
2919
- forked.messages = original.messages.slice(0, clampedCount).map((m) => {
2920
- const cloned = { ...m };
2921
- if (Array.isArray(cloned.content)) {
2922
- cloned.content = cloned.content.map(
2923
- (part) => typeof part === "object" && part !== null ? { ...part } : part
2924
- );
2925
- }
2926
- return cloned;
2927
- });
2928
- forked.checkpoints = original.checkpoints.filter((c) => c.messageIndex <= clampedCount).map((c) => ({ ...c, timestamp: new Date(c.timestamp.getTime()) }));
2929
- forked.updated = /* @__PURE__ */ new Date();
2930
- return forked;
2931
- }
2932
- /**
2933
- * 从磁盘 JSON 数据恢复 Session 实例。
2934
- * 添加运行时校验:损坏或不兼容的历史文件会抛出明确错误,而非 TypeError 崩溃。
2935
- */
2936
- static fromJSON(data) {
2937
- const d = data;
2938
- if (!d || typeof d !== "object") {
2939
- throw new Error("Invalid session data: expected an object");
2940
- }
2941
- if (typeof d.id !== "string" || typeof d.provider !== "string" || typeof d.model !== "string") {
2942
- throw new Error("Invalid session data: missing or invalid id/provider/model fields");
2943
- }
2944
- if (!Array.isArray(d.messages)) {
2945
- throw new Error("Invalid session data: messages is not an array");
2946
- }
2947
- const session = new _Session(d.id, d.provider, d.model);
2948
- session.title = typeof d.title === "string" ? d.title : void 0;
2949
- const created = new Date(d.created);
2950
- const updated = new Date(d.updated);
2951
- session.created = isNaN(created.getTime()) ? /* @__PURE__ */ new Date() : created;
2952
- session.updated = isNaN(updated.getTime()) ? /* @__PURE__ */ new Date() : updated;
2953
- const tu = d.tokenUsage;
2954
- if (tu && typeof tu === "object") {
2955
- session.tokenUsage = {
2956
- inputTokens: typeof tu.inputTokens === "number" ? tu.inputTokens : 0,
2957
- outputTokens: typeof tu.outputTokens === "number" ? tu.outputTokens : 0,
2958
- cacheCreationTokens: typeof tu.cacheCreationTokens === "number" ? tu.cacheCreationTokens : 0,
2959
- cacheReadTokens: typeof tu.cacheReadTokens === "number" ? tu.cacheReadTokens : 0
2960
- };
2961
- }
2962
- if (Array.isArray(d.checkpoints)) {
2963
- session.checkpoints = d.checkpoints.map((c) => ({
2964
- name: String(c.name ?? ""),
2965
- messageIndex: typeof c.messageIndex === "number" ? c.messageIndex : 0,
2966
- timestamp: new Date(c.timestamp)
2967
- }));
2968
- }
2969
- const deserializeMessages = (arr) => arr.map((m) => {
2970
- const ts = new Date(m.timestamp);
2971
- const msg = {
2972
- role: m.role ?? "user",
2973
- content: Array.isArray(m.content) ? m.content : String(m.content ?? ""),
2974
- timestamp: isNaN(ts.getTime()) ? /* @__PURE__ */ new Date() : ts
2975
- };
2976
- if (Array.isArray(m.toolCalls)) msg.toolCalls = m.toolCalls;
2977
- if (typeof m.reasoningContent === "string") msg.reasoningContent = m.reasoningContent;
2978
- if (typeof m.toolCallId === "string") msg.toolCallId = m.toolCallId;
2979
- if (typeof m.toolName === "string") msg.toolName = m.toolName;
2980
- if (typeof m.isError === "boolean") msg.isError = m.isError;
2981
- return msg;
2982
- });
2983
- session.messages = deserializeMessages(d.messages);
2984
- if (Array.isArray(d.branches) && d.branches.length > 0) {
2985
- session.branches = d.branches.map((b) => {
2986
- const ts = new Date(b.created);
2987
- return {
2988
- id: String(b.id ?? "main"),
2989
- title: String(b.title ?? b.id ?? "main"),
2990
- parentBranchId: typeof b.parentBranchId === "string" ? b.parentBranchId : null,
2991
- parentMessageIndex: typeof b.parentMessageIndex === "number" ? b.parentMessageIndex : 0,
2992
- created: isNaN(ts.getTime()) ? /* @__PURE__ */ new Date() : ts
2993
- };
2994
- });
2995
- session.activeBranchId = typeof d.activeBranchId === "string" ? d.activeBranchId : session.branches[0]?.id ?? "main";
2996
- const bm = d.branchMessages;
2997
- if (bm && typeof bm === "object") {
2998
- for (const [id, arr] of Object.entries(bm)) {
2999
- if (id === session.activeBranchId) continue;
3000
- if (!Array.isArray(arr)) continue;
3001
- session._inactiveBranchMessages.set(
3002
- id,
3003
- deserializeMessages(arr)
3004
- );
3005
- }
3006
- }
3007
- }
3008
- return session;
3009
- }
3010
- };
3011
-
3012
- // src/session/session-manager.ts
3013
- function safeDate(value) {
3014
- const d = new Date(value);
3015
- return isNaN(d.getTime()) ? /* @__PURE__ */ new Date(0) : d;
3016
- }
3017
- function extractJsonField(header, field) {
3018
- const re = new RegExp(`"${field}"\\s*:\\s*"([^"]*)"`, "i");
3019
- const m = header.match(re);
3020
- return m ? m[1] : void 0;
3021
- }
3022
- var SessionManager = class {
3023
- _current = null;
3024
- historyDir;
3025
- config;
3026
- /** Last save's redaction hit count — exposed for /security status reporting */
3027
- lastRedactionHits = 0;
3028
- constructor(config) {
3029
- this.config = config;
3030
- this.historyDir = config.getHistoryDir();
3031
- }
3032
- /**
3033
- * Build redaction options from config. Returns `{ enabled: false }` when
3034
- * `security.redactOnSave` is off or `security.mode` is 'off'.
3035
- */
3036
- redactOptionsForSave() {
3037
- const security = this.config.get("security");
3038
- if (!security || !security.redactOnSave || security.mode === "off") {
3039
- return { enabled: false };
3040
- }
3041
- return {
3042
- enabled: true,
3043
- customRegexes: security.customPatterns ?? []
3044
- };
3045
- }
3046
- get current() {
3047
- return this._current;
3048
- }
3049
- createSession(provider, model) {
3050
- const session = new Session(uuidv4(), provider, model);
3051
- this._current = session;
3052
- return session;
3053
- }
3054
- /**
3055
- * 直接设置当前会话(用于从内存缓存恢复未保存的会话)。
3056
- * 与 `loadSession` 不同,此方法不读取磁盘,也不抛出错误。
3057
- * Web 多 Tab 场景下,SessionHandler 会维护一份未保存会话的内存缓存,
3058
- * 切换 Tab 时通过此方法将缓存中的会话设为当前会话,避免"Session not found"。
3059
- */
3060
- setCurrent(session) {
3061
- this._current = session;
3062
- }
3063
- /** 清除当前会话引用(下次访问将触发 lazy 创建)。 */
3064
- clearCurrent() {
3065
- this._current = null;
3066
- }
3067
- async save() {
3068
- if (!this._current) return;
3069
- mkdirSync(this.historyDir, { recursive: true });
3070
- const filePath = join(this.historyDir, `${this._current.id}.json`);
3071
- const raw = this._current.toJSON();
3072
- const opts = this.redactOptionsForSave();
3073
- const { value: payload, hits } = redactJson(raw, opts);
3074
- this.lastRedactionHits = hits.length;
3075
- const tmpPath = filePath + ".tmp";
3076
- writeFileSync(tmpPath, JSON.stringify(payload, null, 2), "utf-8");
3077
- renameSync(tmpPath, filePath);
3078
- }
3079
- loadSession(id) {
3080
- const filePath = join(this.historyDir, `${id}.json`);
3081
- if (!existsSync(filePath)) {
3082
- throw new Error(`Session ${id} not found`);
3083
- }
3084
- let data;
3085
- try {
3086
- data = JSON.parse(readFileSync(filePath, "utf-8"));
3087
- } catch (err) {
3088
- throw new Error(`Session ${id} is corrupted: ${err instanceof Error ? err.message : String(err)}`);
3089
- }
3090
- const session = Session.fromJSON(data);
3091
- this._current = session;
3092
- return session;
3093
- }
3094
- listSessions() {
3095
- if (!existsSync(this.historyDir)) return [];
3096
- const files = readdirSync(this.historyDir).filter((f) => f.endsWith(".json"));
3097
- const metas = [];
3098
- for (const file of files) {
3099
- try {
3100
- const meta = this.readSessionMeta(join(this.historyDir, file));
3101
- if (meta) metas.push(meta);
3102
- } catch (err) {
3103
- process.stderr.write(
3104
- `[Warning] Skipping corrupted session file "${file}": ${err instanceof Error ? err.message : String(err)}
3105
- `
3106
- );
3107
- }
3108
- }
3109
- return metas.sort((a, b) => b.updated.getTime() - a.updated.getTime());
3110
- }
3111
- /**
3112
- * P1-B: Read only the first ~1KB of a session file to extract metadata fields.
3113
- * Session JSON format puts id/provider/model/created/updated/title before the
3114
- * large "messages" array, so a small header read suffices for metadata extraction.
3115
- * Falls back to full file read if header parsing fails.
3116
- */
3117
- readSessionMeta(filePath) {
3118
- const HEADER_SIZE = 1024;
3119
- let header;
3120
- try {
3121
- const fd = openSync(filePath, "r");
3122
- const buf = Buffer.alloc(HEADER_SIZE);
3123
- const bytesRead = readSync(fd, buf, 0, HEADER_SIZE, 0);
3124
- closeSync(fd);
3125
- header = buf.toString("utf-8", 0, bytesRead);
3126
- } catch {
3127
- return null;
3128
- }
3129
- const id = extractJsonField(header, "id");
3130
- const provider = extractJsonField(header, "provider");
3131
- const model = extractJsonField(header, "model");
3132
- const created = extractJsonField(header, "created");
3133
- const updated = extractJsonField(header, "updated");
3134
- const title = extractJsonField(header, "title");
3135
- if (id && provider && model) {
3136
- let messageCount = 0;
3137
- try {
3138
- const full = readFileSync(filePath, "utf-8");
3139
- const matches = full.match(/"role"\s*:/g);
3140
- messageCount = matches ? matches.length : 0;
3141
- } catch {
3142
- }
3143
- return {
3144
- id,
3145
- provider,
3146
- model,
3147
- messageCount,
3148
- created: safeDate(created),
3149
- updated: safeDate(updated),
3150
- title: title || void 0
3151
- };
3152
- }
3153
- const data = JSON.parse(readFileSync(filePath, "utf-8"));
3154
- return {
3155
- id: data.id,
3156
- provider: data.provider,
3157
- model: data.model,
3158
- messageCount: data.messages?.length ?? 0,
3159
- created: safeDate(data.created),
3160
- updated: safeDate(data.updated),
3161
- title: data.title
3162
- };
3163
- }
3164
- deleteSession(id) {
3165
- const filePath = join(this.historyDir, `${id}.json`);
3166
- if (!existsSync(filePath)) return false;
3167
- try {
3168
- unlinkSync(filePath);
3169
- if (this._current && this._current.id === id) {
3170
- this._current = null;
3171
- }
3172
- return true;
3173
- } catch {
3174
- return false;
3175
- }
3176
- }
3177
- /**
3178
- * 从当前 session 分叉创建新 session。
3179
- *
3180
- * 先保存原始 session(保留完整历史),然后创建分叉(截取到 messageCount),
3181
- * 将分叉设为当前 session 并保存。
3182
- *
3183
- * @param messageCount 复制的消息数量
3184
- * @param title 可选的新标题
3185
- * @returns 新的分叉 session
3186
- */
3187
- async forkSession(messageCount, title) {
3188
- if (!this._current) {
3189
- throw new Error("No active session to fork");
3190
- }
3191
- await this.save();
3192
- const forked = Session.fork(this._current, uuidv4(), messageCount, title);
3193
- this._current = forked;
3194
- await this.save();
3195
- return forked;
3196
- }
3197
- /**
3198
- * 跨 session 全文搜索。
3199
- * 遍历所有历史 JSON 文件,逐条匹配消息内容(不区分大小写),
3200
- * 每个 session 最多返回 3 条匹配片段,全局最多 maxResults 个 session。
3201
- */
3202
- searchMessages(query, maxResults = 20) {
3203
- if (!existsSync(this.historyDir)) return [];
3204
- const q = query.toLowerCase();
3205
- const files = readdirSync(this.historyDir).filter((f) => f.endsWith(".json")).map((f) => join(this.historyDir, f));
3206
- const results = [];
3207
- for (const filePath of files) {
3208
- if (results.length >= maxResults) break;
3209
- try {
3210
- const data = JSON.parse(readFileSync(filePath, "utf-8"));
3211
- const messages = data.messages ?? [];
3212
- const matches = [];
3213
- for (const msg of messages) {
3214
- if (matches.length >= 3) break;
3215
- let text = "";
3216
- if (typeof msg.content === "string") {
3217
- text = msg.content;
3218
- } else if (Array.isArray(msg.content)) {
3219
- text = msg.content.filter((p) => p.type === "text").map((p) => p.text ?? "").join("");
3220
- }
3221
- const lowerText = text.toLowerCase();
3222
- const idx = lowerText.indexOf(q);
3223
- if (idx !== -1) {
3224
- const start = Math.max(0, idx - 30);
3225
- const end = Math.min(text.length, idx + query.length + 60);
3226
- const snippet = (start > 0 ? "\u2026" : "") + text.slice(start, end).replace(/\n/g, " ") + (end < text.length ? "\u2026" : "");
3227
- matches.push({ role: msg.role, snippet });
3228
- }
3229
- }
3230
- if (matches.length > 0) {
3231
- results.push({
3232
- sessionMeta: {
3233
- id: data.id,
3234
- provider: data.provider,
3235
- model: data.model,
3236
- messageCount: messages.length,
3237
- created: safeDate(data.created),
3238
- updated: safeDate(data.updated),
3239
- title: data.title
3240
- },
3241
- matches
3242
- });
3243
- }
3244
- } catch (err) {
3245
- process.stderr.write(
3246
- `[Warning] Skipping corrupted session file "${filePath}": ${err instanceof Error ? err.message : String(err)}
3247
- `
3248
- );
3249
- }
3250
- }
3251
- return results.sort((a, b) => b.sessionMeta.updated.getTime() - a.sessionMeta.updated.getTime());
3252
- }
3253
- };
3254
-
3255
- // src/mcp/client.ts
3256
- import { spawn } from "child_process";
3257
- var McpClient = class {
3258
- serverId;
3259
- config;
3260
- process = null;
3261
- nextId = 1;
3262
- // M8: wraps at MAX_SAFE_INTEGER via getNextId()
3263
- connected = false;
3264
- serverInfo = null;
3265
- /** stderr 收集(最多保留最后 2KB,用于错误报告) */
3266
- stderrBuffer = "";
3267
- /** 缓存已发现的工具列表 */
3268
- cachedTools = [];
3269
- /** 错误信息(连接失败时设置) */
3270
- errorMessage = null;
3271
- // ── JSON-RPC 请求/响应匹配 ──────────────────────────────────────
3272
- pendingRequests = /* @__PURE__ */ new Map();
3273
- /** stdout 残余缓冲区(处理不完整的 JSON 行) */
3274
- stdoutBuffer = "";
3275
- constructor(serverId, config) {
3276
- this.serverId = serverId;
3277
- this.config = config;
3278
- }
3279
- get isConnected() {
3280
- return this.connected;
3281
- }
3282
- get serverName() {
3283
- return this.serverInfo?.name ?? this.serverId;
3284
- }
3285
- get tools() {
3286
- return this.cachedTools;
3287
- }
3288
- // ══════════════════════════════════════════════════════════════════
3289
- // 连接与初始化
3290
- // ══════════════════════════════════════════════════════════════════
3291
- async connect() {
3292
- const timeout = this.config.timeout ?? MCP_CONNECT_TIMEOUT;
3293
- try {
3294
- this.process = spawn(this.config.command, this.config.args ?? [], {
3295
- stdio: ["pipe", "pipe", "pipe"],
3296
- env: { ...process.env, ...this.config.env },
3297
- // Windows 上 npx 等是 .cmd 脚本,需要 shell 模式
3298
- shell: process.platform === "win32",
3299
- // 不让子进程阻止父进程退出
3300
- detached: false
3301
- });
3302
- this.process.on("error", (err) => {
3303
- this.errorMessage = err.message;
3304
- this.connected = false;
3305
- this.rejectAllPending(new Error(`MCP server [${this.serverId}] process error: ${err.message}`));
3306
- });
3307
- this.process.on("exit", (code, signal) => {
3308
- this.connected = false;
3309
- const reason = signal ? `signal ${signal}` : `code ${code}`;
3310
- this.rejectAllPending(new Error(`MCP server [${this.serverId}] exited: ${reason}`));
3311
- });
3312
- this.process.stdout.setEncoding("utf-8");
3313
- this.process.stdout.on("data", (chunk) => this.handleStdoutData(chunk));
3314
- this.process.stderr.setEncoding("utf-8");
3315
- this.process.stderr.on("data", (chunk) => {
3316
- this.stderrBuffer += chunk;
3317
- if (this.stderrBuffer.length > 2048) {
3318
- this.stderrBuffer = this.stderrBuffer.slice(-2048);
3319
- }
3320
- });
3321
- const initResult = await this.withTimeout(
3322
- this.sendRequest("initialize", {
3323
- protocolVersion: MCP_PROTOCOL_VERSION,
3324
- capabilities: {},
3325
- clientInfo: { name: APP_NAME, version: VERSION }
3326
- }),
3327
- timeout,
3328
- "initialize handshake"
3329
- );
3330
- this.serverInfo = initResult.serverInfo;
3331
- this.sendNotification("notifications/initialized");
3332
- this.connected = true;
3333
- await this.refreshTools();
3334
- } catch (err) {
3335
- this.errorMessage = err instanceof Error ? err.message : String(err);
3336
- this.connected = false;
3337
- this.killProcess();
3338
- throw err;
3339
- }
3340
- }
3341
- // ══════════════════════════════════════════════════════════════════
3342
- // 工具操作
3343
- // ══════════════════════════════════════════════════════════════════
3344
- /** 刷新工具列表(tools/list) */
3345
- async refreshTools() {
3346
- this.ensureConnected();
3347
- const result = await this.withTimeout(
3348
- this.sendRequest("tools/list", {}),
3349
- MCP_CALL_TIMEOUT,
3350
- "tools/list"
3351
- );
3352
- this.cachedTools = result.tools ?? [];
3353
- return this.cachedTools;
3354
- }
3355
- /** 调用工具(tools/call) */
3356
- async callTool(name, args) {
3357
- this.ensureConnected();
3358
- return this.withTimeout(
3359
- this.sendRequest("tools/call", { name, arguments: args }),
3360
- MCP_CALL_TIMEOUT,
3361
- `tools/call(${name})`
3362
- );
3363
- }
3364
- // ══════════════════════════════════════════════════════════════════
3365
- // 关闭连接
3366
- // ══════════════════════════════════════════════════════════════════
3367
- async close() {
3368
- this.connected = false;
3369
- this.rejectAllPending(new Error("Client closing"));
3370
- this.killProcess();
3371
- }
3372
- /**
3373
- * 断线重连:清理旧状态后重新执行完整的 connect() 流程。
3374
- * 用于 MCP 服务器子进程意外退出后的自动或手动恢复。
3375
- */
3376
- async reconnect() {
3377
- this.connected = false;
3378
- this.rejectAllPending(new Error("Reconnecting"));
3379
- this.killProcess();
3380
- this.errorMessage = null;
3381
- this.stderrBuffer = "";
3382
- this.stdoutBuffer = "";
3383
- this.cachedTools = [];
3384
- await this.connect();
3385
- }
3386
- // ══════════════════════════════════════════════════════════════════
3387
- // 内部方法:JSON-RPC 通信
3388
- // ══════════════════════════════════════════════════════════════════
3389
- sendRequest(method, params) {
3390
- return new Promise((resolve, reject) => {
3391
- if (!this.process?.stdin?.writable) {
3392
- return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
3393
- }
3394
- const id = this.nextId++;
3395
- if (this.nextId > Number.MAX_SAFE_INTEGER - 1) this.nextId = 1;
3396
- const request = {
3397
- jsonrpc: "2.0",
3398
- id,
3399
- method,
3400
- ...params !== void 0 ? { params } : {}
3401
- };
3402
- let timer;
3403
- const cleanup = () => {
3404
- this.pendingRequests.delete(id);
3405
- clearTimeout(timer);
3406
- };
3407
- timer = setTimeout(() => {
3408
- cleanup();
3409
- reject(new Error(`MCP request [${method}] timed out (internal)`));
3410
- }, MCP_CALL_TIMEOUT * 2);
3411
- this.pendingRequests.set(id, {
3412
- resolve: (result) => {
3413
- cleanup();
3414
- resolve(result);
3415
- },
3416
- reject: (error) => {
3417
- cleanup();
3418
- reject(error);
3419
- },
3420
- timer
3421
- });
3422
- const json = JSON.stringify(request) + "\n";
3423
- this.process.stdin.write(json, (err) => {
3424
- if (err) {
3425
- cleanup();
3426
- reject(new Error(`MCP write error: ${err.message}`));
3427
- }
3428
- });
3429
- });
3430
- }
3431
- sendNotification(method, params) {
3432
- if (!this.process?.stdin?.writable) return;
3433
- const notification = {
3434
- jsonrpc: "2.0",
3435
- method,
3436
- ...params !== void 0 ? { params } : {}
3437
- };
3438
- this.process.stdin.write(JSON.stringify(notification) + "\n");
3439
- }
3440
- /**
3441
- * 处理 stdout 数据:按行分割,每行解析为 JSON-RPC 响应。
3442
- * 处理不完整行:残余数据保留在 stdoutBuffer 中。
3443
- */
3444
- handleStdoutData(chunk) {
3445
- this.stdoutBuffer += chunk;
3446
- if (this.stdoutBuffer.length > 1048576) {
3447
- process.stderr.write(`[mcp] stdout buffer exceeded 1MB \u2014 clearing
3448
- `);
3449
- this.stdoutBuffer = "";
3450
- return;
3451
- }
3452
- const lines = this.stdoutBuffer.split("\n");
3453
- this.stdoutBuffer = lines.pop() ?? "";
3454
- for (const line of lines) {
3455
- const trimmed = line.trim();
3456
- if (!trimmed) continue;
3457
- try {
3458
- const msg = JSON.parse(trimmed);
3459
- this.handleMessage(msg);
3460
- } catch {
3461
- }
3462
- }
3463
- }
3464
- /** 处理收到的 JSON-RPC 消息 */
3465
- handleMessage(msg) {
3466
- if ("id" in msg && typeof msg.id === "number") {
3467
- const pending = this.pendingRequests.get(msg.id);
3468
- if (!pending) return;
3469
- const response = msg;
3470
- if (response.error) {
3471
- pending.reject(new Error(
3472
- `MCP error [${response.error.code}]: ${response.error.message}`
3473
- ));
3474
- } else {
3475
- pending.resolve(response.result);
3476
- }
3477
- }
3478
- }
3479
- // ══════════════════════════════════════════════════════════════════
3480
- // 辅助方法
3481
- // ══════════════════════════════════════════════════════════════════
3482
- ensureConnected() {
3483
- if (!this.connected) {
3484
- throw new Error(
3485
- `MCP server [${this.serverId}] is not connected` + (this.errorMessage ? `: ${this.errorMessage}` : "")
3486
- );
3487
- }
3488
- }
3489
- /** Promise 超时包装 */
3490
- withTimeout(promise, ms, label) {
3491
- return new Promise((resolve, reject) => {
3492
- const timer = setTimeout(() => {
3493
- reject(new Error(`MCP [${this.serverId}] ${label} timed out after ${ms}ms`));
3494
- }, ms);
3495
- promise.then((val) => {
3496
- clearTimeout(timer);
3497
- resolve(val);
3498
- }).catch((err) => {
3499
- clearTimeout(timer);
3500
- reject(err);
3501
- });
3502
- });
3503
- }
3504
- /** 拒绝所有挂起的请求。pending.reject() 内部的 cleanup() 已包含 clearTimeout。 */
3505
- rejectAllPending(error) {
3506
- for (const [, pending] of this.pendingRequests) {
3507
- pending.reject(error);
3508
- }
3509
- this.pendingRequests.clear();
3510
- }
3511
- /** 杀掉子进程,并移除所有事件监听器防止僵尸引用 */
3512
- killProcess() {
3513
- if (this.process) {
3514
- this.process.removeAllListeners("error");
3515
- this.process.removeAllListeners("exit");
3516
- this.process.stdout?.removeAllListeners("data");
3517
- this.process.stderr?.removeAllListeners("data");
3518
- try {
3519
- this.process.stdin?.end();
3520
- this.process.kill();
3521
- } catch {
3522
- }
3523
- this.process = null;
3524
- }
3525
- }
3526
- };
3527
-
3528
- // src/mcp/manager.ts
3529
- var McpManager = class {
3530
- clients = /* @__PURE__ */ new Map();
3531
- /**
3532
- * 连接所有配置的 MCP 服务器(并发连接,单个失败不阻塞其他)。
3533
- * 连接结果通过 getStatus() 查看。
3534
- */
3535
- async connectAll(servers) {
3536
- const entries = Object.entries(servers);
3537
- if (entries.length === 0) return;
3538
- const promises = entries.map(async ([serverId, config]) => {
3539
- const client = new McpClient(serverId, config);
3540
- this.clients.set(serverId, client);
3541
- try {
3542
- await client.connect();
3543
- process.stderr.write(`[mcp] \u2713 ${serverId}: connected (${client.serverName}, ${client.tools.length} tools)
3544
- `);
3545
- } catch (err) {
3546
- const msg = err instanceof Error ? err.message : String(err);
3547
- process.stderr.write(`[mcp] \u2717 ${serverId}: ${msg}
3548
- `);
3549
- }
3550
- });
3551
- await Promise.allSettled(promises);
3552
- }
3553
- /**
3554
- * 获取所有已连接服务器的工具,转换为 ai-cli 的 Tool 接口。
3555
- * 工具名格式:mcp__<serverId>__<toolName>
3556
- */
3557
- getAllTools() {
3558
- const tools = [];
3559
- for (const [serverId, client] of this.clients) {
3560
- if (!client.isConnected) continue;
3561
- for (const mcpTool of client.tools) {
3562
- tools.push(this.wrapMcpTool(serverId, client, mcpTool));
3563
- }
3564
- }
3565
- return tools;
3566
- }
3567
- /**
3568
- * 获取连接状态摘要。
3569
- */
3570
- getStatus() {
3571
- const statuses = [];
3572
- for (const [serverId, client] of this.clients) {
3573
- statuses.push({
3574
- serverId,
3575
- serverName: client.serverName,
3576
- toolCount: client.tools.length,
3577
- connected: client.isConnected,
3578
- error: client.errorMessage ?? void 0
3579
- });
3580
- }
3581
- return statuses;
3582
- }
3583
- /**
3584
- * 获取已连接服务器数量。
3585
- */
3586
- getConnectedCount() {
3587
- let count = 0;
3588
- for (const client of this.clients.values()) {
3589
- if (client.isConnected) count++;
3590
- }
3591
- return count;
3592
- }
3593
- /**
3594
- * 获取所有 MCP 工具的总数。
3595
- */
3596
- getTotalToolCount() {
3597
- let count = 0;
3598
- for (const client of this.clients.values()) {
3599
- if (client.isConnected) count += client.tools.length;
3600
- }
3601
- return count;
3602
- }
3603
- /**
3604
- * 重连指定 MCP 服务器。
3605
- * @returns 重连是否成功
3606
- */
3607
- async reconnectServer(serverId) {
3608
- const client = this.clients.get(serverId);
3609
- if (!client) return false;
3610
- try {
3611
- await client.reconnect();
3612
- process.stderr.write(`[mcp] \u2713 ${serverId}: reconnected (${client.serverName}, ${client.tools.length} tools)
3613
- `);
3614
- return true;
3615
- } catch (err) {
3616
- const msg = err instanceof Error ? err.message : String(err);
3617
- process.stderr.write(`[mcp] \u2717 ${serverId}: reconnect failed: ${msg}
3618
- `);
3619
- return false;
3620
- }
3621
- }
3622
- /**
3623
- * 重连所有已断开的 MCP 服务器。
3624
- * @returns 成功重连的服务器数量
3625
- */
3626
- async reconnectAll() {
3627
- let reconnected = 0;
3628
- for (const [serverId, client] of this.clients) {
3629
- if (!client.isConnected) {
3630
- const ok = await this.reconnectServer(serverId);
3631
- if (ok) reconnected++;
3632
- }
3633
- }
3634
- return reconnected;
3635
- }
3636
- /**
3637
- * 关闭所有 MCP 服务器连接。
3638
- */
3639
- async closeAll() {
3640
- const promises = [...this.clients.entries()].map(
3641
- ([id, c]) => c.close().catch((err) => {
3642
- process.stderr.write(`[mcp] Failed to close ${id}: ${err instanceof Error ? err.message : err}
3643
- `);
3644
- })
3645
- );
3646
- await Promise.allSettled(promises);
3647
- this.clients.clear();
3648
- }
3649
- // ══════════════════════════════════════════════════════════════════
3650
- // 内部方法
3651
- // ══════════════════════════════════════════════════════════════════
3652
- /**
3653
- * 将 MCP 工具包装为 ai-cli 的 Tool 接口。
3654
- * execute() 方法委托给对应 McpClient.callTool()。
3655
- */
3656
- wrapMcpTool(serverId, client, mcpTool) {
3657
- const toolName = `${MCP_TOOL_PREFIX}${serverId}__${mcpTool.name}`;
3658
- const definition = this.convertDefinition(toolName, serverId, mcpTool);
3659
- return {
3660
- definition,
3661
- execute: async (args) => {
3662
- if (!client.isConnected) {
3663
- process.stderr.write(`[mcp] ${serverId}: disconnected, attempting reconnect...
3664
- `);
3665
- try {
3666
- await client.reconnect();
3667
- process.stderr.write(`[mcp] \u2713 ${serverId}: reconnected (${client.tools.length} tools)
3668
- `);
3669
- } catch {
3670
- throw new Error(
3671
- `MCP server [${serverId}] is disconnected and reconnect failed` + (client.errorMessage ? `: ${client.errorMessage}` : "") + `. Use /mcp reconnect to retry, or restart ai-cli.`
3672
- );
3673
- }
3674
- }
3675
- try {
3676
- const result = await client.callTool(mcpTool.name, args);
3677
- if (result.isError) {
3678
- const errorText = this.extractText(result.content);
3679
- throw new Error(errorText || "MCP tool returned error with no message");
3680
- }
3681
- return this.extractText(result.content) || "(no output)";
3682
- } catch (err) {
3683
- throw new Error(`MCP [${serverId}/${mcpTool.name}]: ${err instanceof Error ? err.message : String(err)}`);
3684
- }
3685
- }
3686
- };
3687
- }
3688
- /**
3689
- * 转换 MCP 工具定义为 ai-cli 的 ToolDefinition。
3690
- * MCP 使用 JSON Schema 的 inputSchema,ai-cli 使用 Record<string, ToolParameterSchema>。
3691
- * M12 修复:递归转换嵌套 object/array schema,保留完整结构供 AI 正确传参。
3692
- */
3693
- convertDefinition(toolName, serverId, mcpTool) {
3694
- const parameters = {};
3695
- const props = mcpTool.inputSchema?.properties ?? {};
3696
- const required = new Set(mcpTool.inputSchema?.required ?? []);
3697
- for (const [key, schema] of Object.entries(props)) {
3698
- parameters[key] = {
3699
- ...this.convertSchema(schema),
3700
- ...required.has(key) ? { required: true } : {}
3701
- };
3702
- }
3703
- return {
3704
- name: toolName,
3705
- description: (mcpTool.description ?? mcpTool.name) + ` [MCP: ${serverId}]`,
3706
- parameters
3707
- };
3708
- }
3709
- /**
3710
- * 递归转换单个 JSON Schema 属性为 ToolParameterSchema。
3711
- * 支持嵌套 object(含 properties)和 array(含 items)。
3712
- * 递归深度受 MCP 工具实际 schema 复杂度约束(通常 2-3 层)。
3713
- */
3714
- convertSchema(schema) {
3715
- const type = this.normalizeType(String(schema["type"] ?? "string"));
3716
- const result = {
3717
- type,
3718
- description: String(schema["description"] ?? ""),
3719
- ...schema["enum"] ? { enum: schema["enum"] } : {}
3720
- };
3721
- if (type === "object" && schema["properties"]) {
3722
- const props = schema["properties"];
3723
- result.properties = {};
3724
- for (const [k, v] of Object.entries(props)) {
3725
- result.properties[k] = this.convertSchema(v);
3726
- }
3727
- }
3728
- if (type === "array" && schema["items"]) {
3729
- const items = schema["items"];
3730
- result.items = this.convertSchema(items);
3731
- }
3732
- return result;
3733
- }
3734
- /**
3735
- * 将 JSON Schema 类型映射到 ai-cli 的 ToolParameterType。
3736
- * integer → number(ai-cli 不区分整数/浮点)
3737
- */
3738
- normalizeType(jsonSchemaType) {
3739
- switch (jsonSchemaType) {
3740
- case "string":
3741
- return "string";
3742
- case "number":
3743
- case "integer":
3744
- return "number";
3745
- case "boolean":
3746
- return "boolean";
3747
- case "array":
3748
- return "array";
3749
- case "object":
3750
- return "object";
3751
- default:
3752
- return "string";
3753
- }
3754
- }
3755
- /**
3756
- * 从 MCP 响应的 content blocks 中提取纯文本。
3757
- * 只处理 type="text" 的块,其他类型(image 等)跳过。
3758
- */
3759
- extractText(content) {
3760
- return content.filter((block) => block.type === "text" && block.text).map((block) => block.text).join("\n");
3761
- }
3762
- };
3763
-
3764
- // src/skills/manager.ts
3765
- import { existsSync as existsSync2, readdirSync as readdirSync2, mkdirSync as mkdirSync2, statSync } from "fs";
3766
- import { join as join2 } from "path";
3767
-
3768
- // src/skills/types.ts
3769
- import { readFileSync as readFileSync2 } from "fs";
3770
- import { basename } from "path";
3771
- function parseSimpleYaml(yaml) {
3772
- const result = {};
3773
- for (const line of yaml.split("\n")) {
3774
- const match = line.replace(/\r$/, "").match(/^(\w+)\s*:\s*(.+)$/);
3775
- if (match) {
3776
- result[match[1]] = match[2].trim().replace(/^['"]|['"]$/g, "");
3777
- }
3778
- }
3779
- return result;
3780
- }
3781
- function parseYamlArray(value) {
3782
- const bracketMatch = value.match(/^\[(.+)]$/);
3783
- if (bracketMatch) {
3784
- return bracketMatch[1].split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, ""));
3785
- }
3786
- return [value.trim()];
3787
- }
3788
- function parseSkillFile(filePath) {
3789
- let raw;
3790
- try {
3791
- raw = readFileSync2(filePath, "utf-8");
3792
- } catch {
3793
- return null;
3794
- }
3795
- const frontmatterMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
3796
- if (!frontmatterMatch) {
3797
- return {
3798
- meta: {
3799
- name: basename(filePath, ".md"),
3800
- description: ""
3801
- },
3802
- content: raw.trim(),
3803
- filePath
3804
- };
3805
- }
3806
- const [, yaml, content] = frontmatterMatch;
3807
- const parsed = parseSimpleYaml(yaml);
3808
- return {
3809
- meta: {
3810
- name: parsed["name"] ?? basename(filePath, ".md"),
3811
- description: parsed["description"] ?? "",
3812
- tools: parsed["tools"] ? parseYamlArray(parsed["tools"]) : void 0
3813
- },
3814
- content: content.trim(),
3815
- filePath
3816
- };
3817
- }
3818
-
3819
- // src/skills/manager.ts
3820
- var SKILL_CONTENT_WARN_CHARS_DEFAULT = 1e4;
3821
- var SkillManager = class {
3822
- skills = /* @__PURE__ */ new Map();
3823
- activeSkill = null;
3824
- skillsDir;
3825
- /** 超大技能文件警告阈值,由调用方从 config 传入;0 = 静默,undefined = 用默认 10000 */
3826
- warnThreshold;
3827
- constructor(skillsDir, warnThreshold) {
3828
- this.skillsDir = skillsDir;
3829
- this.warnThreshold = warnThreshold ?? SKILL_CONTENT_WARN_CHARS_DEFAULT;
3830
- }
3831
- /** 发现并加载 skillsDir 下所有 .md 文件,返回加载数量 */
3832
- loadSkills() {
3833
- this.skills.clear();
3834
- if (!existsSync2(this.skillsDir)) {
3835
- try {
3836
- mkdirSync2(this.skillsDir, { recursive: true });
3837
- } catch {
3838
- }
3839
- return 0;
3840
- }
3841
- let entries;
3842
- try {
3843
- entries = readdirSync2(this.skillsDir);
3844
- } catch {
3845
- return 0;
3846
- }
3847
- for (const entry of entries) {
3848
- let filePath;
3849
- const fullPath = join2(this.skillsDir, entry);
3850
- if (entry.endsWith(".md")) {
3851
- filePath = fullPath;
3852
- } else {
3853
- try {
3854
- if (statSync(fullPath).isDirectory()) {
3855
- const skillMd = join2(fullPath, "SKILL.md");
3856
- if (existsSync2(skillMd)) {
3857
- filePath = skillMd;
3858
- } else {
3859
- continue;
3860
- }
3861
- } else {
3862
- continue;
3863
- }
3864
- } catch {
3865
- continue;
3866
- }
3867
- }
3868
- const skill = parseSkillFile(filePath);
3869
- if (skill) {
3870
- if (skill.meta.name === "SKILL" && !entry.endsWith(".md")) {
3871
- skill.meta.name = entry;
3872
- }
3873
- this.skills.set(skill.meta.name, skill);
3874
- if (this.warnThreshold > 0 && skill.content.length > this.warnThreshold) {
3875
- process.stderr.write(
3876
- `\u26A0 Skill '${skill.meta.name}' is ${skill.content.length} chars (>${this.warnThreshold}). Only consumed when activated via /skill. Adjust with: /config set ui.skillSizeWarn <n|0>
3877
- `
3878
- );
3879
- }
3880
- }
3881
- }
3882
- return this.skills.size;
3883
- }
3884
- /** 列出所有已加载的技能 */
3885
- listSkills() {
3886
- return [...this.skills.values()];
3887
- }
3888
- /** 按 name 激活技能。返回被激活的 Skill,未找到返回 null。 */
3889
- activate(name) {
3890
- const skill = this.skills.get(name);
3891
- if (!skill) return null;
3892
- this.activeSkill = skill;
3893
- return skill;
3894
- }
3895
- /** 停用当前技能 */
3896
- deactivate() {
3897
- this.activeSkill = null;
3898
- }
3899
- /** 获取当前激活的技能(null 表示无激活技能) */
3900
- getActive() {
3901
- return this.activeSkill;
3902
- }
3903
- /** 获取激活技能的 prompt 内容(null 表示无激活技能) */
3904
- getActivePromptContent() {
3905
- return this.activeSkill?.content ?? null;
3906
- }
3907
- /** 获取激活技能的工具白名单 Set(null 表示无限制) */
3908
- getActiveToolFilter() {
3909
- if (!this.activeSkill?.meta.tools) return null;
3910
- return new Set(this.activeSkill.meta.tools);
3911
- }
3912
- /** 获取技能目录路径 */
3913
- getSkillsDir() {
3914
- return this.skillsDir;
3915
- }
3916
- };
3917
-
3918
- // src/core/proxy.ts
3919
- async function setupProxy(configProxy) {
3920
- const proxyUrl = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY ?? process.env.https_proxy ?? process.env.http_proxy ?? configProxy;
3921
- if (!proxyUrl) return;
3922
- try {
3923
- const { ProxyAgent, setGlobalDispatcher } = await import("undici");
3924
- setGlobalDispatcher(new ProxyAgent({ uri: proxyUrl }));
3925
- } catch {
3926
- }
3927
- }
3928
-
3929
- // src/core/pricing.ts
3930
- var PRICING_TABLE = {
3931
- // ── Anthropic Claude ──────────────────────────────────────────
3932
- "claude-opus-4-6": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
3933
- "claude-opus-4-5": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
3934
- "claude-sonnet-4-6": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
3935
- "claude-sonnet-4-5-20250929": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
3936
- "claude-haiku-4-5-20251001": { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 },
3937
- "claude-haiku-4-5": { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 },
3938
- // Legacy Claude 3.x families (prefix fallback handles minor date suffixes)
3939
- "claude-3-5-sonnet": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
3940
- "claude-3-5-haiku": { input: 0.8, output: 4, cacheWrite: 1, cacheRead: 0.08 },
3941
- "claude-3-opus": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
3942
- // ── OpenAI ────────────────────────────────────────────────────
3943
- "gpt-4o": { input: 2.5, output: 10, cacheRead: 1.25 },
3944
- "gpt-4o-mini": { input: 0.15, output: 0.6, cacheRead: 0.075 },
3945
- "gpt-4-turbo": { input: 10, output: 30 },
3946
- "gpt-4": { input: 30, output: 60 },
3947
- "gpt-4.1": { input: 2, output: 8, cacheRead: 0.5 },
3948
- "gpt-4.1-mini": { input: 0.4, output: 1.6, cacheRead: 0.1 },
3949
- "gpt-4.1-nano": { input: 0.1, output: 0.4, cacheRead: 0.025 },
3950
- "o1": { input: 15, output: 60, cacheRead: 7.5 },
3951
- "o1-mini": { input: 3, output: 12, cacheRead: 1.5 },
3952
- "o3": { input: 10, output: 40, cacheRead: 2.5 },
3953
- "o3-mini": { input: 1.1, output: 4.4, cacheRead: 0.55 },
3954
- // ── Google Gemini ─────────────────────────────────────────────
3955
- "gemini-2.5-pro": { input: 1.25, output: 10 },
3956
- "gemini-2.5-flash": { input: 0.3, output: 2.5 },
3957
- "gemini-2.0-flash": { input: 0.1, output: 0.4 },
3958
- "gemini-1.5-pro": { input: 1.25, output: 5 },
3959
- "gemini-1.5-flash": { input: 0.075, output: 0.3 },
3960
- // ── DeepSeek ──────────────────────────────────────────────────
3961
- // V4 family (2026-04-23+):1M context, Thinking / Non-Thinking dual-mode, 384K max output
3962
- "deepseek-v4-pro": { input: 1.74, output: 3.48, cacheRead: 0.145 },
3963
- "deepseek-v4-flash": { input: 0.14, output: 0.28, cacheRead: 0.028 },
3964
- // Legacy aliases:retires 2026-07-24 UTC 15:59,官方 route 到 V4 Flash
3965
- "deepseek-chat": { input: 0.14, output: 0.28, cacheRead: 0.028 },
3966
- "deepseek-reasoner": { input: 0.14, output: 0.28, cacheRead: 0.028 },
3967
- "deepseek-v3": { input: 0.27, output: 1.1, cacheRead: 0.07 },
3968
- // ── Moonshot Kimi ─────────────────────────────────────────────
3969
- "moonshot-v1-8k": { input: 0.17, output: 0.17 },
3970
- "moonshot-v1-32k": { input: 0.33, output: 0.33 },
3971
- "moonshot-v1-128k": { input: 0.83, output: 0.83 },
3972
- "kimi-k2": { input: 0.6, output: 2.5 },
3973
- "kimi-latest": { input: 0.6, output: 2.5 },
3974
- // ── Zhipu GLM ─────────────────────────────────────────────────
3975
- "glm-4-plus": { input: 0.7, output: 0.7 },
3976
- "glm-4": { input: 0.14, output: 0.14 },
3977
- "glm-4-flash": { input: 0, output: 0 },
3978
- "glm-4.5": { input: 0.29, output: 1.14 },
3979
- "glm-4.6": { input: 0.6, output: 2.2 },
3980
- "glm-4.6v": { input: 0.6, output: 2.2 },
3981
- "glm-5": { input: 0.85, output: 2.85 },
3982
- "glm-5.1": { input: 0.95, output: 3.15 },
3983
- "glm-5.1-reasoning": { input: 1.4, output: 4.4 },
3984
- "glm-5.1-air": { input: 0.4, output: 1.2 },
3985
- "glm-z1": { input: 0.5, output: 1.5 },
3986
- "glm-z1-air": { input: 0.2, output: 0.6 },
3987
- "glm-z1-flash": { input: 0, output: 0 }
3988
- // ── OpenRouter (pass-through — actual cost depends on underlying model) ──
3989
- // Left empty; callers should resolve via underlying model ID.
3990
- // ── Ollama (local, zero cost) ─────────────────────────────────
3991
- // Handled via provider check below.
3992
- };
3993
- var FREE_PROVIDERS = /* @__PURE__ */ new Set(["ollama"]);
3994
- function getPricing(provider, model) {
3995
- if (FREE_PROVIDERS.has(provider.toLowerCase())) {
3996
- return { input: 0, output: 0 };
3997
- }
3998
- const key = model.toLowerCase();
3999
- if (PRICING_TABLE[key]) return PRICING_TABLE[key];
4000
- const keys = Object.keys(PRICING_TABLE).sort((a, b) => b.length - a.length);
4001
- for (const k of keys) {
4002
- if (key.startsWith(k)) return PRICING_TABLE[k];
4003
- }
4004
- return null;
4005
- }
4006
- function computeCost(provider, model, usage) {
4007
- const p = getPricing(provider, model);
4008
- if (!p) return null;
4009
- const input = usage.inputTokens * p.input;
4010
- const output = usage.outputTokens * p.output;
4011
- const cacheWrite = (usage.cacheCreationTokens ?? 0) * (p.cacheWrite ?? p.input);
4012
- const cacheRead = (usage.cacheReadTokens ?? 0) * (p.cacheRead ?? p.input);
4013
- return (input + output + cacheWrite + cacheRead) / 1e6;
4014
- }
4015
- function formatCost(amount) {
4016
- if (amount === 0) return "$0.0000";
4017
- if (amount < 0.01) return `$${amount.toFixed(4)}`;
4018
- if (amount < 1) return `$${amount.toFixed(3)}`;
4019
- return `$${amount.toFixed(2)}`;
4020
- }
4021
-
4022
- // src/session/tool-history.ts
4023
- var SESSION_SIZE_LIMIT = 2 * 1024 * 1024;
4024
- function persistToolRound(session, toolCalls, toolResults, opts) {
4025
- session.addMessage({
4026
- role: "assistant",
4027
- content: opts?.assistantContent ?? "",
4028
- toolCalls,
4029
- reasoningContent: opts?.reasoningContent,
4030
- timestamp: /* @__PURE__ */ new Date()
4031
- });
4032
- for (let i = 0; i < toolCalls.length; i++) {
4033
- const tc = toolCalls[i];
4034
- const tr = toolResults[i];
4035
- if (!tr) continue;
4036
- session.addMessage({
4037
- role: "tool",
4038
- content: truncateForPersist(tr.content),
4039
- toolCallId: tr.callId,
4040
- toolName: tc.name,
4041
- isError: tr.isError,
4042
- timestamp: /* @__PURE__ */ new Date()
4043
- });
4044
- }
4045
- }
4046
- function trimOldToolOutput(messages, keepRecentRounds = 10) {
4047
- const roundStarts = [];
4048
- for (let i = 0; i < messages.length; i++) {
4049
- if (messages[i].role === "assistant" && messages[i].toolCalls?.length) {
4050
- roundStarts.push(i);
4051
- }
4052
- }
4053
- if (roundStarts.length <= keepRecentRounds) return 0;
4054
- const cutoffRoundIdx = roundStarts.length - keepRecentRounds;
4055
- let trimmed = 0;
4056
- for (let r = 0; r < cutoffRoundIdx; r++) {
4057
- const start = roundStarts[r];
4058
- const end = r + 1 < roundStarts.length ? roundStarts[r + 1] : messages.length;
4059
- const assistantMsg = messages[start];
4060
- if (typeof assistantMsg.content === "string" && assistantMsg.content.length > 200) {
4061
- assistantMsg.content = assistantMsg.content.slice(0, 100) + "\u2026 [trimmed for size]";
4062
- trimmed++;
4063
- }
4064
- for (let j = start + 1; j < end; j++) {
4065
- const msg = messages[j];
4066
- if (msg.role === "tool") {
4067
- const status = msg.isError ? "\u2717 error" : "\u2713 ok";
4068
- const name = msg.toolName ?? "unknown";
4069
- const currentContent = typeof msg.content === "string" ? msg.content : "";
4070
- if (currentContent.length > 200) {
4071
- msg.content = `[${name}: ${status}] (output trimmed for size \u2014 ${currentContent.length} chars)`;
4072
- trimmed++;
4073
- }
4074
- }
4075
- }
4076
- }
4077
- return trimmed;
4078
- }
4079
- function cloneMessages(messages) {
4080
- return messages.map((m) => ({ ...m }));
4081
- }
4082
- function autoTrimSessionIfNeeded(session, sizeLimit = SESSION_SIZE_LIMIT) {
4083
- const originalSize = JSON.stringify(session.toJSON()).length;
4084
- if (originalSize <= sizeLimit) return false;
4085
- const snapshotLen = session.messages.length;
4086
- let working = cloneMessages(session.messages.slice(0, snapshotLen));
4087
- let committedSize = originalSize;
4088
- let anyTrim = false;
4089
- let keepRecent = 10;
4090
- while (keepRecent >= 2) {
4091
- const trimmedCount = trimOldToolOutput(working, keepRecent);
4092
- if (trimmedCount === 0) break;
4093
- anyTrim = true;
4094
- const originalMessages = session.messages;
4095
- session.messages = [...working, ...originalMessages.slice(snapshotLen)];
4096
- const newSize = JSON.stringify(session.toJSON()).length;
4097
- session.messages = originalMessages;
4098
- committedSize = newSize;
4099
- if (newSize <= sizeLimit) break;
4100
- keepRecent = Math.max(2, Math.floor(keepRecent / 2));
4101
- if (keepRecent < 2) break;
4102
- }
4103
- if (!anyTrim) return false;
4104
- const appended = session.messages.slice(snapshotLen);
4105
- session.messages.splice(0, session.messages.length, ...working, ...appended);
4106
- return committedSize < originalSize;
4107
- }
4108
-
4109
- // src/repl/dev-state.ts
4110
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3 } from "fs";
4111
- import { join as join3 } from "path";
4112
- import { homedir } from "os";
4113
- var DEV_STATE_MAX_CHARS = 6e3;
4114
- var SNAPSHOT_PROMPT = `You are about to be replaced by a different AI model. Please generate a structured development state snapshot so the next model can continue seamlessly.
4115
-
4116
- CRITICAL: Be SPECIFIC and DETAILED. Include exact values, file paths, format requirements, and constraints \u2014 vague summaries are useless to the next model.
4117
-
4118
- Output ONLY the snapshot in the following exact format (no preamble, no explanation):
4119
-
4120
- ## Development State Snapshot
4121
-
4122
- ### Current Task
4123
- [1-2 sentence summary of the primary task/goal the user is working on]
4124
-
4125
- ### Key Parameters & Constraints
4126
- - [List ALL specific parameters, values, format requirements discovered during this conversation]
4127
- - [Include exact numbers, dimensions, scoring rules, naming conventions, etc.]
4128
- - [Example: "Exam duration 90 minutes, total score 200, 40 single-choice x2 pts + 20 multi-choice x4 pts + 20 true/false x2 pts"]
4129
- - [Example: "File naming format YYYYMMDD-NN-mock-difficulty.md, saved to exam_papers/ directory"]
4130
-
4131
- ### Completed Steps
4132
- - [List each completed step with specific details (file paths, key outcomes)]
4133
-
4134
- ### In-Progress Work
4135
- - [List any work that was started but not finished]
4136
-
4137
- ### Critical Reference Files
4138
- - [List file paths that the next model MUST read before doing any work]
4139
- - [Include brief description of each file's purpose]
4140
- - [Example: "Exam2025.md \u2014 reference format for official past exams (90min/200pts/question type distribution)"]
4141
- - [Example: "style-guide.md \u2014 style guidelines (character setup/scenario building/current events)"]
4142
-
4143
- ### Modified/Created Files
4144
- - [List any files that were created or modified, with brief notes on content]
4145
-
4146
- ### Key Decisions & Context
4147
- - [Important decisions, user preferences, or constraints established]
4148
-
4149
- ### Next Steps
4150
- - [What should be done next to continue this work]
4151
- - [Include specific instructions the next model should follow]
4152
-
4153
- ### Important Notes
4154
- - [Any warnings, caveats, or critical context the next model needs to know]
4155
- - [Things that went wrong or should be avoided]
4156
-
4157
- If any section has no content, write "(none)" for that section. Be thorough \u2014 the next model may have access to our conversation messages, but the detailed tool call results (file contents, command outputs) are NOT preserved. This snapshot is the primary source of specific details and context.`;
4158
- function sessionHasMeaningfulContent(messages) {
4159
- if (messages.length < 2) return false;
4160
- const hasUser = messages.some((m) => m.role === "user");
4161
- const hasAssistant = messages.some((m) => m.role === "assistant");
4162
- return hasUser && hasAssistant;
4163
- }
4164
- function getDevStatePath() {
4165
- return join3(homedir(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
4166
- }
4167
- function saveDevState(content) {
4168
- const configDir = join3(homedir(), CONFIG_DIR_NAME);
4169
- if (!existsSync3(configDir)) {
4170
- mkdirSync3(configDir, { recursive: true });
4171
- }
4172
- let trimmed = content.trim();
4173
- if (trimmed.length > DEV_STATE_MAX_CHARS) {
4174
- trimmed = trimmed.slice(0, DEV_STATE_MAX_CHARS);
4175
- const lastNewline = trimmed.lastIndexOf("\n");
4176
- if (lastNewline > 0) {
4177
- trimmed = trimmed.slice(0, lastNewline);
4178
- }
4179
- trimmed += "\n\n[...truncated]";
4180
- }
4181
- writeFileSync2(getDevStatePath(), trimmed, "utf-8");
4182
- }
4183
- function loadDevState() {
4184
- const path = getDevStatePath();
4185
- if (!existsSync3(path)) return null;
4186
- const content = readFileSync3(path, "utf-8").trim();
4187
- return content || null;
4188
- }
4189
- function clearDevState() {
4190
- const path = getDevStatePath();
4191
- if (existsSync3(path)) {
4192
- try {
4193
- unlinkSync2(path);
4194
- } catch {
4195
- }
4196
- }
4197
- }
4198
-
4199
2501
  export {
4200
2502
  detectsHallucinatedFileOp,
4201
2503
  hadPreviousWriteToolCalls,
@@ -4212,21 +2514,5 @@ export {
4212
2514
  stripToolCallReminder,
4213
2515
  TEE_FINAL_USER_NUDGE,
4214
2516
  CONTENT_ONLY_STREAM_REMINDER,
4215
- ProviderRegistry,
4216
- getContentText,
4217
- SessionManager,
4218
- getPricing,
4219
- computeCost,
4220
- formatCost,
4221
- parseSimpleYaml,
4222
- persistToolRound,
4223
- autoTrimSessionIfNeeded,
4224
- SNAPSHOT_PROMPT,
4225
- sessionHasMeaningfulContent,
4226
- saveDevState,
4227
- loadDevState,
4228
- clearDevState,
4229
- McpManager,
4230
- SkillManager,
4231
- setupProxy
2517
+ ProviderRegistry
4232
2518
  };