jinzd-ai-cli 0.4.72 → 0.4.74

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,330 +1,24 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ schemaToJsonSchema,
4
+ truncateForPersist
5
+ } from "./chunk-D5ZDVEJJ.js";
2
6
  import {
3
7
  AuthError,
4
- ConfigError,
5
- EnvLoader,
6
8
  ProviderError,
7
9
  ProviderNotFoundError,
8
- RateLimitError,
9
- schemaToJsonSchema,
10
- truncateForPersist
11
- } from "./chunk-XH65H3BT.js";
10
+ RateLimitError
11
+ } from "./chunk-2ZD3YTVM.js";
12
12
  import {
13
13
  APP_NAME,
14
14
  CONFIG_DIR_NAME,
15
- CONFIG_FILE_NAME,
16
15
  DEV_STATE_FILE_NAME,
17
- HISTORY_DIR_NAME,
18
16
  MCP_CALL_TIMEOUT,
19
17
  MCP_CONNECT_TIMEOUT,
20
18
  MCP_PROTOCOL_VERSION,
21
19
  MCP_TOOL_PREFIX,
22
- PLUGINS_DIR_NAME,
23
20
  VERSION
24
- } from "./chunk-73UI5AH7.js";
25
-
26
- // src/config/config-manager.ts
27
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
28
- import { join } from "path";
29
- import { homedir } from "os";
30
-
31
- // src/config/schema.ts
32
- import { z } from "zod";
33
- var CustomModelSchema = z.object({
34
- id: z.string(),
35
- displayName: z.string().optional(),
36
- contextWindow: z.number().optional()
37
- });
38
- var CustomProviderConfigSchema = z.object({
39
- id: z.string(),
40
- // 唯一 ID,不能与内置 provider 重名
41
- displayName: z.string(),
42
- // 显示名称
43
- apiKey: z.string().optional(),
44
- // 可选:直接在此写 key(也可通过 apiKeys 字段或环境变量提供)
45
- baseUrl: z.string(),
46
- // OpenAI 兼容 API 的 base URL(必填)
47
- defaultModel: z.string(),
48
- // 默认使用的模型 ID
49
- models: z.array(CustomModelSchema).default([]),
50
- timeout: z.number().optional()
51
- // 请求超时(ms),覆盖全局默认值
52
- });
53
- var ModelParamsSchema = z.object({
54
- temperature: z.number().min(0).max(2).optional(),
55
- maxTokens: z.number().int().positive().optional(),
56
- timeout: z.number().int().positive().optional(),
57
- /** 是否启用深度思考(thinking)模式,Claude Sonnet/Opus、GLM-5 等 */
58
- thinking: z.boolean().optional(),
59
- /** thinking 模式的 token 预算(最小 1024,仅 Claude Extended Thinking 使用) */
60
- thinkingBudget: z.number().int().min(1024).optional()
61
- });
62
- var UserProfileSchema = z.object({
63
- /** 真实姓名(如 "Jin Zhengdong") */
64
- name: z.string().optional(),
65
- /** 昵称/称呼偏好(如 "东叔"),AI 会以此称呼你 */
66
- nickname: z.string().optional(),
67
- /** 职业角色(如 "Full-stack developer"、"Data scientist") */
68
- role: z.string().optional(),
69
- /** 个人简介 / 专长描述(1-3 句话) */
70
- bio: z.string().optional(),
71
- /** 兴趣领域或技术栈(如 ["TypeScript", "Rust", "AI/ML"]) */
72
- interests: z.array(z.string()).default([]),
73
- /** 语言偏好(如 "zh-CN"、"en"),AI 将据此选择交流语言 */
74
- locale: z.string().optional(),
75
- /** 自定义人设补充(自由格式,直接注入 system prompt) */
76
- extra: z.string().optional()
77
- }).default({});
78
- var ConfigSchema = z.object({
79
- version: z.string().default("1.0.0"),
80
- // 用户身份档案 — 跨 Provider 的 "灵魂"
81
- userProfile: UserProfileSchema,
82
- defaultProvider: z.string().default("claude"),
83
- // 每个 provider 的默认模型(key 为 provider ID)
84
- defaultModels: z.record(z.string()).default({}),
85
- // API Keys:放宽为任意 record,支持自定义 provider ID
86
- apiKeys: z.record(z.string()).default({}),
87
- customBaseUrls: z.record(z.string()).default({}),
88
- // Per-provider timeout in ms (e.g. { deepseek: 60000 })
89
- // ⚠️ Timeout 优先级说明(L5):
90
- // 1. modelParams[modelId].timeout — 最高优先级,精确到具体模型(如 deepseek-reasoner 需要更长超时)
91
- // 2. timeouts[providerId] — Provider 级默认,覆盖内置默认值
92
- // 3. 内置 Provider 的硬编码默认值 — 最低兜底(如 OpenAI 兼容系列默认 60000ms)
93
- // 实现位置:src/providers/registry.ts initialize(),将 timeouts[id] 注入 provider
94
- timeouts: z.record(z.number()).default({}),
95
- // HTTP/HTTPS 代理地址(Node.js 不自动使用系统代理,需显式配置)
96
- // 例:http://127.0.0.1:10809
97
- // 也可通过环境变量 HTTPS_PROXY / HTTP_PROXY 覆盖
98
- proxy: z.string().optional(),
99
- // 自定义 Provider 列表(OpenAI 兼容接口,无需改代码)
100
- customProviders: z.array(CustomProviderConfigSchema).default([]),
101
- // 按模型 ID 存储的推理参数(key 为模型 ID,如 "deepseek-chat")
102
- modelParams: z.record(ModelParamsSchema).default({}),
103
- ui: z.object({
104
- streaming: z.boolean().default(true),
105
- markdownRendering: z.boolean().default(true),
106
- showTokenCount: z.boolean().default(true),
107
- /** 桌面通知阈值(毫秒):AI 任务耗时超过此值时发送系统通知。0 = 禁用。默认 10000 (10s) */
108
- notificationThreshold: z.number().default(1e4),
109
- /** 终端输出折行宽度。0 = 自动(使用终端宽度),>0 = 固定列宽。默认 0 */
110
- wordWrap: z.number().int().min(0).default(0),
111
- /** 颜色主题:'dark'(默认)| 'light' | 'custom' */
112
- theme: z.enum(["dark", "light", "custom"]).default("dark"),
113
- /** 自定义颜色覆盖(仅在 theme='custom' 时生效)。值为 chalk 颜色名或 '#hex',支持 'bold.cyan' 组合 */
114
- colors: z.object({
115
- prompt: z.string().optional(),
116
- info: z.string().optional(),
117
- warning: z.string().optional(),
118
- error: z.string().optional(),
119
- success: z.string().optional(),
120
- dim: z.string().optional(),
121
- accent: z.string().optional(),
122
- toolCall: z.string().optional(),
123
- toolResult: z.string().optional(),
124
- heading: z.string().optional()
125
- }).optional()
126
- }).default({}),
127
- session: z.object({
128
- autoSave: z.boolean().default(true),
129
- maxHistoryDays: z.number().default(30),
130
- systemPrompt: z.string().optional()
131
- }).default({}),
132
- // 项目上下文文件配置
133
- // 启动时自动读取并注入 system prompt,类似 Claude Code 的 CLAUDE.md 机制
134
- // 默认按顺序查找:AICLI.md → CLAUDE.md → .aicli/context.md
135
- // 设为 false 可禁用此功能
136
- contextFile: z.union([z.string(), z.literal(false)]).default("auto"),
137
- // Google Custom Search API 的 Search Engine ID (cx 参数)
138
- // API Key 通过 apiKeys['google-search'] 或 AICLI_API_KEY_GOOGLESEARCH 环境变量配置
139
- // CX 也可通过 AICLI_GOOGLE_CX 环境变量覆盖
140
- googleSearchEngineId: z.string().optional(),
141
- // MCP (Model Context Protocol) 服务器配置
142
- // 声明外部 MCP 服务器,启动时自动连接、发现工具并注册
143
- // 配置格式兼容 Claude Desktop(command + args + env)
144
- // 示例:{ "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"] } }
145
- mcpServers: z.record(z.object({
146
- command: z.string(),
147
- args: z.array(z.string()).default([]),
148
- env: z.record(z.string()).optional(),
149
- timeout: z.number().default(3e4)
150
- })).default({}),
151
- // 工具执行钩子(shell 命令,模板变量:{tool} {dangerLevel} {args} {status})
152
- hooks: z.object({
153
- preToolExecution: z.string().optional(),
154
- postToolExecution: z.string().optional()
155
- }).optional(),
156
- // 工具权限规则(按顺序匹配第一个生效)
157
- permissionRules: z.array(z.object({
158
- tool: z.string(),
159
- action: z.enum(["auto-approve", "deny", "confirm"]),
160
- when: z.object({
161
- dangerLevel: z.enum(["safe", "write", "destructive"]).optional(),
162
- pathPattern: z.string().optional()
163
- }).optional()
164
- })).default([]),
165
- // 无规则匹配时的默认权限动作
166
- defaultPermission: z.enum(["auto-approve", "deny", "confirm"]).default("confirm"),
167
- // 自动上下文压缩开关
168
- // 当对话估算 token 数超过模型 contextWindow 的 80% 时,自动触发 compact 压缩旧消息
169
- // 默认开启。设为 false 则仅在手动 /compact 时压缩
170
- autoCompact: z.boolean().default(true),
171
- // Agentic 工具调用循环单次对话最大轮次(默认 200)。
172
- // 超过此值后 AI 被强制停止调用工具并生成总结。
173
- // 建议范围:25(保守)~ 1000(宽松,接近 Claude Code)。
174
- // CLI `--max-tool-rounds <n>` 可覆盖此值。
175
- maxToolRounds: z.number().int().min(1).max(1e4).default(200),
176
- // Auto-pause checkpoint:Agentic 循环每隔多少轮暂停一次,让用户确认方向或中途介入。
177
- // 默认 50;设为 0 禁用(完全自动执行到完成或 maxToolRounds 用尽)。
178
- // CLI 与 Web UI 行为一致:CLI 用 readline question 提示,Web UI 弹出对话框。
179
- autoPauseInterval: z.number().int().min(0).max(1e4).default(50),
180
- // 单次工具输出(如 read_file、bash、grep_files)返回给 AI 的最大字符数上限。
181
- // 默认 500_000 (~500K chars ≈ 6000-8000 行代码)。
182
- // 实际上限还会受模型 contextWindow 动态约束(取 contextWindow/4 作为下限)。
183
- // 设置为 0 或未配置时使用默认值;不建议设为小于 12_000 或大于模型 contextWindow/2。
184
- maxToolOutputChars: z.number().int().min(0).default(5e5),
185
- // 月度成本预算(USD)。
186
- // 设置后,每次 AI 回复后会跟踪成本,接近或超过预算时在 /status 和 /cost 中显示警告。
187
- // 默认 0 = 不限制。例:50 表示每月最多花 $50。
188
- monthlyBudget: z.number().min(0).default(0),
189
- // 插件加载开关(安全控制)
190
- // 默认 false:不自动加载 ~/.aicli/plugins/ 中的插件文件。
191
- // 插件以完整 Node.js 权限在主进程中执行(可读写文件、访问网络、执行命令),
192
- // 必须确认插件来源可信后,再设为 true 启用。
193
- // 可通过 /config 命令或直接编辑 ~/.aicli/config.json 开启。
194
- allowPlugins: z.boolean().default(false),
195
- // 智能模型路由(v0.4.68+)
196
- // 按用户每轮输入的内容/标签/长度动态选择模型,在同一 provider 内切换,
197
- // 例:短问题走 haiku(省钱),planning 走 opus(质量)。
198
- // enabled=false 时永远返回当前模型。rules 按顺序匹配,首个命中的规则生效。
199
- // 每个 rule 的 match 必须至少有一个条件(tag/contains/maxLength/minLength)。
200
- // 详见 src/core/model-router.ts。
201
- routing: z.object({
202
- enabled: z.boolean().default(false),
203
- rules: z.array(z.object({
204
- match: z.object({
205
- contains: z.array(z.string()).optional(),
206
- maxLength: z.number().int().positive().optional(),
207
- minLength: z.number().int().positive().optional(),
208
- tag: z.string().optional()
209
- }),
210
- model: z.string(),
211
- name: z.string().optional()
212
- })).default([]),
213
- fallback: z.string().optional()
214
- }).default({ enabled: false, rules: [] })
215
- });
216
-
217
- // src/config/config-manager.ts
218
- var ConfigManager = class {
219
- configDir;
220
- configPath;
221
- config;
222
- constructor(configDir) {
223
- this.configDir = configDir ?? join(homedir(), CONFIG_DIR_NAME);
224
- this.configPath = join(this.configDir, CONFIG_FILE_NAME);
225
- this.config = this.load();
226
- }
227
- load() {
228
- if (!existsSync(this.configPath)) {
229
- return ConfigSchema.parse({});
230
- }
231
- try {
232
- const raw = JSON.parse(readFileSync(this.configPath, "utf-8"));
233
- return ConfigSchema.parse(raw);
234
- } catch (err) {
235
- throw new ConfigError(
236
- `Config file at ${this.configPath} is invalid. Delete it and run 'ai-cli config' to recreate.
237
- ${err}`
238
- );
239
- }
240
- }
241
- save() {
242
- mkdirSync(this.configDir, { recursive: true });
243
- writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), "utf-8");
244
- }
245
- getApiKey(providerId) {
246
- const envKey = EnvLoader.getApiKey(providerId);
247
- if (envKey) return envKey;
248
- return this.config.apiKeys[providerId];
249
- }
250
- setApiKey(providerId, key) {
251
- this.config.apiKeys[providerId] = key;
252
- this.save();
253
- }
254
- get(key) {
255
- return this.config[key];
256
- }
257
- set(key, value) {
258
- this.config[key] = value;
259
- this.save();
260
- }
261
- /**
262
- * 仅修改内存中的配置值,不持久化到磁盘。
263
- * 用于 CLI 命令行参数覆盖(-p, -m, --no-stream),使其只对当前进程生效。
264
- */
265
- setTransient(key, value) {
266
- this.config[key] = value;
267
- }
268
- isFirstRun() {
269
- return !existsSync(this.configPath);
270
- }
271
- getConfigDir() {
272
- return this.configDir;
273
- }
274
- getHistoryDir() {
275
- return join(this.configDir, HISTORY_DIR_NAME);
276
- }
277
- getPluginsDir() {
278
- return join(this.configDir, PLUGINS_DIR_NAME);
279
- }
280
- getDefaultProvider() {
281
- return EnvLoader.getDefaultProvider() ?? this.config.defaultProvider;
282
- }
283
- /** 点分路径读取配置值,如 `ui.theme` → config.ui.theme */
284
- getByPath(path) {
285
- const keys = path.split(".");
286
- let current = this.config;
287
- for (const key of keys) {
288
- if (current == null || typeof current !== "object") return void 0;
289
- current = current[key];
290
- }
291
- return current;
292
- }
293
- /** 点分路径写入配置值,自动类型转换(boolean/number/string)并持久化 */
294
- setByPath(path, rawValue) {
295
- const keys = path.split(".");
296
- if (keys.length === 0) return;
297
- let value = rawValue;
298
- if (rawValue === "true") value = true;
299
- else if (rawValue === "false") value = false;
300
- else if (rawValue !== "" && !isNaN(Number(rawValue))) value = Number(rawValue);
301
- const draft = JSON.parse(JSON.stringify(this.config));
302
- let current = draft;
303
- for (let i = 0; i < keys.length - 1; i++) {
304
- const key = keys[i];
305
- if (current[key] == null || typeof current[key] !== "object") {
306
- current[key] = {};
307
- }
308
- current = current[key];
309
- }
310
- current[keys[keys.length - 1]] = value;
311
- const result = ConfigSchema.safeParse(draft);
312
- if (!result.success) {
313
- const firstErr = result.error.errors[0];
314
- throw new ConfigError(`Invalid config value for "${path}": ${firstErr?.message ?? "validation failed"}`);
315
- }
316
- this.config = result.data;
317
- this.save();
318
- }
319
- /** 获取完整配置对象的格式化 JSON 字符串(用于 /config show 等展示) */
320
- toFormattedJSON() {
321
- return JSON.stringify(this.config, null, 2);
322
- }
323
- /** 获取完整配置对象(JSON 兼容的原始对象) */
324
- toJSON() {
325
- return structuredClone(this.config);
326
- }
327
- };
21
+ } from "./chunk-BT2TCINO.js";
328
22
 
329
23
  // src/providers/claude.ts
330
24
  import Anthropic from "@anthropic-ai/sdk";
@@ -2427,8 +2121,8 @@ var ProviderRegistry = class {
2427
2121
  };
2428
2122
 
2429
2123
  // src/session/session-manager.ts
2430
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, unlinkSync, renameSync, openSync, readSync, closeSync } from "fs";
2431
- import { join as join2 } from "path";
2124
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync, renameSync, openSync, readSync, closeSync } from "fs";
2125
+ import { join } from "path";
2432
2126
  import { v4 as uuidv4 } from "uuid";
2433
2127
 
2434
2128
  // src/core/types.ts
@@ -2438,6 +2132,9 @@ function getContentText(content) {
2438
2132
  }
2439
2133
 
2440
2134
  // src/session/session.ts
2135
+ function makeBranchId() {
2136
+ return Math.random().toString(16).slice(2, 8);
2137
+ }
2441
2138
  var Session = class _Session {
2442
2139
  id;
2443
2140
  provider;
@@ -2453,12 +2150,32 @@ var Session = class _Session {
2453
2150
  cacheReadTokens: 0
2454
2151
  };
2455
2152
  checkpoints = [];
2153
+ // ── B2 Branches (v0.4.74+) ──────────────────────────────────────
2154
+ /**
2155
+ * All branches in this session. The 'main' branch is auto-created and
2156
+ * represents the linear conversation for pre-B2 sessions.
2157
+ */
2158
+ branches = [];
2159
+ /** Currently active branch — its messages live in `this.messages`. */
2160
+ activeBranchId = "main";
2161
+ /**
2162
+ * Stashed message arrays for INACTIVE branches. The active branch's
2163
+ * messages are always in `this.messages`, never duplicated here.
2164
+ */
2165
+ _inactiveBranchMessages = /* @__PURE__ */ new Map();
2456
2166
  constructor(id, provider, model) {
2457
2167
  this.id = id;
2458
2168
  this.provider = provider;
2459
2169
  this.model = model;
2460
2170
  this.created = /* @__PURE__ */ new Date();
2461
2171
  this.updated = /* @__PURE__ */ new Date();
2172
+ this.branches.push({
2173
+ id: "main",
2174
+ title: "main",
2175
+ parentBranchId: null,
2176
+ parentMessageIndex: 0,
2177
+ created: this.created
2178
+ });
2462
2179
  }
2463
2180
  /**
2464
2181
  * 更新 session 关联的 provider 和 model(在 /provider 或 /model 切换时调用)。
@@ -2545,6 +2262,120 @@ var Session = class _Session {
2545
2262
  this.checkpoints = this.checkpoints.filter((c) => c.name !== name);
2546
2263
  return this.checkpoints.length < len;
2547
2264
  }
2265
+ // ── B2 Branch operations ────────────────────────────────────────
2266
+ /** Deep-clone a messages array (matches `Session.fork` semantics). */
2267
+ static cloneMessages(msgs) {
2268
+ return msgs.map((m) => {
2269
+ const cloned = { ...m };
2270
+ if (Array.isArray(cloned.content)) {
2271
+ cloned.content = cloned.content.map(
2272
+ (part) => typeof part === "object" && part !== null ? { ...part } : part
2273
+ );
2274
+ }
2275
+ if (cloned.toolCalls) {
2276
+ cloned.toolCalls = cloned.toolCalls.map((tc) => ({ ...tc }));
2277
+ }
2278
+ return cloned;
2279
+ });
2280
+ }
2281
+ /** List all branches (metadata only). */
2282
+ listBranches() {
2283
+ return this.branches.map((b) => ({ ...b }));
2284
+ }
2285
+ /** Current active branch metadata. */
2286
+ getActiveBranch() {
2287
+ const b = this.branches.find((b2) => b2.id === this.activeBranchId);
2288
+ if (!b) {
2289
+ this.activeBranchId = this.branches[0]?.id ?? "main";
2290
+ return this.branches[0] ?? { id: "main", title: "main", parentBranchId: null, parentMessageIndex: 0, created: /* @__PURE__ */ new Date() };
2291
+ }
2292
+ return b;
2293
+ }
2294
+ /**
2295
+ * Create a new branch by forking the active branch at message index
2296
+ * `fromIndex`. Copies `messages[0..fromIndex]` into the new branch
2297
+ * and switches to it. The original active branch is preserved intact
2298
+ * in the stash.
2299
+ *
2300
+ * @returns new branch id
2301
+ * @throws if fromIndex is out of range
2302
+ */
2303
+ createBranch(fromIndex, title) {
2304
+ if (fromIndex < 0 || fromIndex > this.messages.length) {
2305
+ throw new Error(
2306
+ `createBranch: fromIndex ${fromIndex} out of range [0, ${this.messages.length}]`
2307
+ );
2308
+ }
2309
+ this._inactiveBranchMessages.set(this.activeBranchId, this.messages);
2310
+ const id = makeBranchId();
2311
+ const meta = {
2312
+ id,
2313
+ title: title || `branch-${this.branches.length + 1}`,
2314
+ parentBranchId: this.activeBranchId,
2315
+ parentMessageIndex: fromIndex,
2316
+ created: /* @__PURE__ */ new Date()
2317
+ };
2318
+ this.branches.push(meta);
2319
+ this.messages = _Session.cloneMessages(this.messages.slice(0, fromIndex));
2320
+ this.activeBranchId = id;
2321
+ this.updated = /* @__PURE__ */ new Date();
2322
+ return id;
2323
+ }
2324
+ /**
2325
+ * Switch the active branch. Stashes current messages under the old
2326
+ * active id and loads the target branch's messages into `this.messages`.
2327
+ *
2328
+ * @returns true if switched, false if id not found or already active
2329
+ */
2330
+ switchBranch(id) {
2331
+ if (id === this.activeBranchId) return false;
2332
+ if (!this.branches.some((b) => b.id === id)) return false;
2333
+ this._inactiveBranchMessages.set(this.activeBranchId, this.messages);
2334
+ const target = this._inactiveBranchMessages.get(id) ?? [];
2335
+ this._inactiveBranchMessages.delete(id);
2336
+ this.messages = target;
2337
+ this.activeBranchId = id;
2338
+ this.updated = /* @__PURE__ */ new Date();
2339
+ return true;
2340
+ }
2341
+ /**
2342
+ * Delete a branch by id. Cannot delete the active branch or the last
2343
+ * remaining branch. If other branches list this one as parent, their
2344
+ * parent pointer is retargeted to this branch's parent (transparent
2345
+ * to callers — branches still form a valid forest).
2346
+ *
2347
+ * @returns true if deleted
2348
+ */
2349
+ deleteBranch(id) {
2350
+ if (id === this.activeBranchId) return false;
2351
+ if (this.branches.length <= 1) return false;
2352
+ const idx = this.branches.findIndex((b) => b.id === id);
2353
+ if (idx === -1) return false;
2354
+ const deleted = this.branches[idx];
2355
+ for (const b of this.branches) {
2356
+ if (b.parentBranchId === id) {
2357
+ b.parentBranchId = deleted.parentBranchId;
2358
+ }
2359
+ }
2360
+ this.branches.splice(idx, 1);
2361
+ this._inactiveBranchMessages.delete(id);
2362
+ this.updated = /* @__PURE__ */ new Date();
2363
+ return true;
2364
+ }
2365
+ /** Rename a branch (affects only display title). */
2366
+ renameBranch(id, newTitle) {
2367
+ const b = this.branches.find((b2) => b2.id === id);
2368
+ if (!b) return false;
2369
+ b.title = newTitle;
2370
+ this.updated = /* @__PURE__ */ new Date();
2371
+ return true;
2372
+ }
2373
+ /** Messages of any branch (active or inactive) — read-only copy. */
2374
+ getBranchMessages(id) {
2375
+ if (id === this.activeBranchId) return this.messages.slice();
2376
+ const m = this._inactiveBranchMessages.get(id);
2377
+ return m ? m.slice() : null;
2378
+ }
2548
2379
  getMeta() {
2549
2380
  return {
2550
2381
  id: this.id,
@@ -2557,6 +2388,23 @@ var Session = class _Session {
2557
2388
  };
2558
2389
  }
2559
2390
  toJSON() {
2391
+ const serializeMessages = (msgs) => msgs.map((m) => {
2392
+ const out = {
2393
+ role: m.role,
2394
+ content: m.content,
2395
+ timestamp: m.timestamp.toISOString()
2396
+ };
2397
+ if (m.toolCalls) out.toolCalls = m.toolCalls;
2398
+ if (m.reasoningContent !== void 0) out.reasoningContent = m.reasoningContent;
2399
+ if (m.toolCallId) out.toolCallId = m.toolCallId;
2400
+ if (m.toolName) out.toolName = m.toolName;
2401
+ if (m.isError !== void 0) out.isError = m.isError;
2402
+ return out;
2403
+ });
2404
+ const branchMessages = {};
2405
+ for (const [id, msgs] of this._inactiveBranchMessages.entries()) {
2406
+ branchMessages[id] = serializeMessages(msgs);
2407
+ }
2560
2408
  return {
2561
2409
  id: this.id,
2562
2410
  provider: this.provider,
@@ -2570,19 +2418,21 @@ var Session = class _Session {
2570
2418
  messageIndex: c.messageIndex,
2571
2419
  timestamp: c.timestamp.toISOString()
2572
2420
  })),
2573
- messages: this.messages.map((m) => {
2574
- const out = {
2575
- role: m.role,
2576
- content: m.content,
2577
- timestamp: m.timestamp.toISOString()
2578
- };
2579
- if (m.toolCalls) out.toolCalls = m.toolCalls;
2580
- if (m.reasoningContent !== void 0) out.reasoningContent = m.reasoningContent;
2581
- if (m.toolCallId) out.toolCallId = m.toolCallId;
2582
- if (m.toolName) out.toolName = m.toolName;
2583
- if (m.isError !== void 0) out.isError = m.isError;
2584
- return out;
2585
- })
2421
+ // B2 Branches (v0.4.74+). Omitted for sessions with only the default
2422
+ // 'main' branch and no stashed messages (keeps file size identical
2423
+ // to pre-B2 for the common case).
2424
+ ...this.branches.length > 1 || this._inactiveBranchMessages.size > 0 ? {
2425
+ activeBranchId: this.activeBranchId,
2426
+ branches: this.branches.map((b) => ({
2427
+ id: b.id,
2428
+ title: b.title,
2429
+ parentBranchId: b.parentBranchId,
2430
+ parentMessageIndex: b.parentMessageIndex,
2431
+ created: b.created.toISOString()
2432
+ })),
2433
+ branchMessages
2434
+ } : {},
2435
+ messages: serializeMessages(this.messages)
2586
2436
  };
2587
2437
  }
2588
2438
  /**
@@ -2650,7 +2500,7 @@ var Session = class _Session {
2650
2500
  timestamp: new Date(c.timestamp)
2651
2501
  }));
2652
2502
  }
2653
- session.messages = d.messages.map((m) => {
2503
+ const deserializeMessages = (arr) => arr.map((m) => {
2654
2504
  const ts = new Date(m.timestamp);
2655
2505
  const msg = {
2656
2506
  role: m.role ?? "user",
@@ -2664,6 +2514,31 @@ var Session = class _Session {
2664
2514
  if (typeof m.isError === "boolean") msg.isError = m.isError;
2665
2515
  return msg;
2666
2516
  });
2517
+ session.messages = deserializeMessages(d.messages);
2518
+ if (Array.isArray(d.branches) && d.branches.length > 0) {
2519
+ session.branches = d.branches.map((b) => {
2520
+ const ts = new Date(b.created);
2521
+ return {
2522
+ id: String(b.id ?? "main"),
2523
+ title: String(b.title ?? b.id ?? "main"),
2524
+ parentBranchId: typeof b.parentBranchId === "string" ? b.parentBranchId : null,
2525
+ parentMessageIndex: typeof b.parentMessageIndex === "number" ? b.parentMessageIndex : 0,
2526
+ created: isNaN(ts.getTime()) ? /* @__PURE__ */ new Date() : ts
2527
+ };
2528
+ });
2529
+ session.activeBranchId = typeof d.activeBranchId === "string" ? d.activeBranchId : session.branches[0]?.id ?? "main";
2530
+ const bm = d.branchMessages;
2531
+ if (bm && typeof bm === "object") {
2532
+ for (const [id, arr] of Object.entries(bm)) {
2533
+ if (id === session.activeBranchId) continue;
2534
+ if (!Array.isArray(arr)) continue;
2535
+ session._inactiveBranchMessages.set(
2536
+ id,
2537
+ deserializeMessages(arr)
2538
+ );
2539
+ }
2540
+ }
2541
+ }
2667
2542
  return session;
2668
2543
  }
2669
2544
  };
@@ -2707,20 +2582,20 @@ var SessionManager = class {
2707
2582
  }
2708
2583
  async save() {
2709
2584
  if (!this._current) return;
2710
- mkdirSync2(this.historyDir, { recursive: true });
2711
- const filePath = join2(this.historyDir, `${this._current.id}.json`);
2585
+ mkdirSync(this.historyDir, { recursive: true });
2586
+ const filePath = join(this.historyDir, `${this._current.id}.json`);
2712
2587
  const tmpPath = filePath + ".tmp";
2713
- writeFileSync2(tmpPath, JSON.stringify(this._current.toJSON(), null, 2), "utf-8");
2588
+ writeFileSync(tmpPath, JSON.stringify(this._current.toJSON(), null, 2), "utf-8");
2714
2589
  renameSync(tmpPath, filePath);
2715
2590
  }
2716
2591
  loadSession(id) {
2717
- const filePath = join2(this.historyDir, `${id}.json`);
2718
- if (!existsSync2(filePath)) {
2592
+ const filePath = join(this.historyDir, `${id}.json`);
2593
+ if (!existsSync(filePath)) {
2719
2594
  throw new Error(`Session ${id} not found`);
2720
2595
  }
2721
2596
  let data;
2722
2597
  try {
2723
- data = JSON.parse(readFileSync2(filePath, "utf-8"));
2598
+ data = JSON.parse(readFileSync(filePath, "utf-8"));
2724
2599
  } catch (err) {
2725
2600
  throw new Error(`Session ${id} is corrupted: ${err instanceof Error ? err.message : String(err)}`);
2726
2601
  }
@@ -2729,12 +2604,12 @@ var SessionManager = class {
2729
2604
  return session;
2730
2605
  }
2731
2606
  listSessions() {
2732
- if (!existsSync2(this.historyDir)) return [];
2607
+ if (!existsSync(this.historyDir)) return [];
2733
2608
  const files = readdirSync(this.historyDir).filter((f) => f.endsWith(".json"));
2734
2609
  const metas = [];
2735
2610
  for (const file of files) {
2736
2611
  try {
2737
- const meta = this.readSessionMeta(join2(this.historyDir, file));
2612
+ const meta = this.readSessionMeta(join(this.historyDir, file));
2738
2613
  if (meta) metas.push(meta);
2739
2614
  } catch (err) {
2740
2615
  process.stderr.write(
@@ -2772,7 +2647,7 @@ var SessionManager = class {
2772
2647
  if (id && provider && model) {
2773
2648
  let messageCount = 0;
2774
2649
  try {
2775
- const full = readFileSync2(filePath, "utf-8");
2650
+ const full = readFileSync(filePath, "utf-8");
2776
2651
  const matches = full.match(/"role"\s*:/g);
2777
2652
  messageCount = matches ? matches.length : 0;
2778
2653
  } catch {
@@ -2787,7 +2662,7 @@ var SessionManager = class {
2787
2662
  title: title || void 0
2788
2663
  };
2789
2664
  }
2790
- const data = JSON.parse(readFileSync2(filePath, "utf-8"));
2665
+ const data = JSON.parse(readFileSync(filePath, "utf-8"));
2791
2666
  return {
2792
2667
  id: data.id,
2793
2668
  provider: data.provider,
@@ -2799,8 +2674,8 @@ var SessionManager = class {
2799
2674
  };
2800
2675
  }
2801
2676
  deleteSession(id) {
2802
- const filePath = join2(this.historyDir, `${id}.json`);
2803
- if (!existsSync2(filePath)) return false;
2677
+ const filePath = join(this.historyDir, `${id}.json`);
2678
+ if (!existsSync(filePath)) return false;
2804
2679
  try {
2805
2680
  unlinkSync(filePath);
2806
2681
  if (this._current && this._current.id === id) {
@@ -2837,14 +2712,14 @@ var SessionManager = class {
2837
2712
  * 每个 session 最多返回 3 条匹配片段,全局最多 maxResults 个 session。
2838
2713
  */
2839
2714
  searchMessages(query, maxResults = 20) {
2840
- if (!existsSync2(this.historyDir)) return [];
2715
+ if (!existsSync(this.historyDir)) return [];
2841
2716
  const q = query.toLowerCase();
2842
- const files = readdirSync(this.historyDir).filter((f) => f.endsWith(".json")).map((f) => join2(this.historyDir, f));
2717
+ const files = readdirSync(this.historyDir).filter((f) => f.endsWith(".json")).map((f) => join(this.historyDir, f));
2843
2718
  const results = [];
2844
2719
  for (const filePath of files) {
2845
2720
  if (results.length >= maxResults) break;
2846
2721
  try {
2847
- const data = JSON.parse(readFileSync2(filePath, "utf-8"));
2722
+ const data = JSON.parse(readFileSync(filePath, "utf-8"));
2848
2723
  const messages = data.messages ?? [];
2849
2724
  const matches = [];
2850
2725
  for (const msg of messages) {
@@ -2891,8 +2766,8 @@ var SessionManager = class {
2891
2766
 
2892
2767
  // src/tools/git-context.ts
2893
2768
  import { execSync } from "child_process";
2894
- import { existsSync as existsSync3 } from "fs";
2895
- import { join as join3 } from "path";
2769
+ import { existsSync as existsSync2 } from "fs";
2770
+ import { join as join2 } from "path";
2896
2771
  function runGit(cmd, cwd) {
2897
2772
  try {
2898
2773
  return execSync(`git ${cmd}`, {
@@ -2909,7 +2784,7 @@ function getGitRoot(cwd = process.cwd()) {
2909
2784
  return runGit("rev-parse --show-toplevel", cwd);
2910
2785
  }
2911
2786
  function getGitContext(cwd = process.cwd()) {
2912
- if (!existsSync3(join3(cwd, ".git"))) {
2787
+ if (!existsSync2(join2(cwd, ".git"))) {
2913
2788
  const result = runGit("rev-parse --git-dir", cwd);
2914
2789
  if (!result) return null;
2915
2790
  }
@@ -3489,11 +3364,11 @@ var McpManager = class {
3489
3364
  };
3490
3365
 
3491
3366
  // src/skills/manager.ts
3492
- import { existsSync as existsSync4, readdirSync as readdirSync2, mkdirSync as mkdirSync3, statSync } from "fs";
3493
- import { join as join4 } from "path";
3367
+ import { existsSync as existsSync3, readdirSync as readdirSync2, mkdirSync as mkdirSync2, statSync } from "fs";
3368
+ import { join as join3 } from "path";
3494
3369
 
3495
3370
  // src/skills/types.ts
3496
- import { readFileSync as readFileSync3 } from "fs";
3371
+ import { readFileSync as readFileSync2 } from "fs";
3497
3372
  import { basename } from "path";
3498
3373
  function parseSimpleYaml(yaml) {
3499
3374
  const result = {};
@@ -3515,7 +3390,7 @@ function parseYamlArray(value) {
3515
3390
  function parseSkillFile(filePath) {
3516
3391
  let raw;
3517
3392
  try {
3518
- raw = readFileSync3(filePath, "utf-8");
3393
+ raw = readFileSync2(filePath, "utf-8");
3519
3394
  } catch {
3520
3395
  return null;
3521
3396
  }
@@ -3555,9 +3430,9 @@ var SkillManager = class {
3555
3430
  /** 发现并加载 skillsDir 下所有 .md 文件,返回加载数量 */
3556
3431
  loadSkills() {
3557
3432
  this.skills.clear();
3558
- if (!existsSync4(this.skillsDir)) {
3433
+ if (!existsSync3(this.skillsDir)) {
3559
3434
  try {
3560
- mkdirSync3(this.skillsDir, { recursive: true });
3435
+ mkdirSync2(this.skillsDir, { recursive: true });
3561
3436
  } catch {
3562
3437
  }
3563
3438
  return 0;
@@ -3570,14 +3445,14 @@ var SkillManager = class {
3570
3445
  }
3571
3446
  for (const entry of entries) {
3572
3447
  let filePath;
3573
- const fullPath = join4(this.skillsDir, entry);
3448
+ const fullPath = join3(this.skillsDir, entry);
3574
3449
  if (entry.endsWith(".md")) {
3575
3450
  filePath = fullPath;
3576
3451
  } else {
3577
3452
  try {
3578
3453
  if (statSync(fullPath).isDirectory()) {
3579
- const skillMd = join4(fullPath, "SKILL.md");
3580
- if (existsSync4(skillMd)) {
3454
+ const skillMd = join3(fullPath, "SKILL.md");
3455
+ if (existsSync3(skillMd)) {
3581
3456
  filePath = skillMd;
3582
3457
  } else {
3583
3458
  continue;
@@ -3880,9 +3755,9 @@ function autoTrimSessionIfNeeded(session, sizeLimit = SESSION_SIZE_LIMIT) {
3880
3755
  }
3881
3756
 
3882
3757
  // src/repl/dev-state.ts
3883
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "fs";
3884
- import { join as join5 } from "path";
3885
- import { homedir as homedir2 } from "os";
3758
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3 } from "fs";
3759
+ import { join as join4 } from "path";
3760
+ import { homedir } from "os";
3886
3761
  var DEV_STATE_MAX_CHARS = 6e3;
3887
3762
  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.
3888
3763
 
@@ -3935,12 +3810,12 @@ function sessionHasMeaningfulContent(messages) {
3935
3810
  return hasUser && hasAssistant;
3936
3811
  }
3937
3812
  function getDevStatePath() {
3938
- return join5(homedir2(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
3813
+ return join4(homedir(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
3939
3814
  }
3940
3815
  function saveDevState(content) {
3941
- const configDir = join5(homedir2(), CONFIG_DIR_NAME);
3942
- if (!existsSync5(configDir)) {
3943
- mkdirSync4(configDir, { recursive: true });
3816
+ const configDir = join4(homedir(), CONFIG_DIR_NAME);
3817
+ if (!existsSync4(configDir)) {
3818
+ mkdirSync3(configDir, { recursive: true });
3944
3819
  }
3945
3820
  let trimmed = content.trim();
3946
3821
  if (trimmed.length > DEV_STATE_MAX_CHARS) {
@@ -3951,17 +3826,17 @@ function saveDevState(content) {
3951
3826
  }
3952
3827
  trimmed += "\n\n[...truncated]";
3953
3828
  }
3954
- writeFileSync3(getDevStatePath(), trimmed, "utf-8");
3829
+ writeFileSync2(getDevStatePath(), trimmed, "utf-8");
3955
3830
  }
3956
3831
  function loadDevState() {
3957
3832
  const path = getDevStatePath();
3958
- if (!existsSync5(path)) return null;
3959
- const content = readFileSync4(path, "utf-8").trim();
3833
+ if (!existsSync4(path)) return null;
3834
+ const content = readFileSync3(path, "utf-8").trim();
3960
3835
  return content || null;
3961
3836
  }
3962
3837
  function clearDevState() {
3963
3838
  const path = getDevStatePath();
3964
- if (existsSync5(path)) {
3839
+ if (existsSync4(path)) {
3965
3840
  try {
3966
3841
  unlinkSync2(path);
3967
3842
  } catch {
@@ -3970,7 +3845,6 @@ function clearDevState() {
3970
3845
  }
3971
3846
 
3972
3847
  export {
3973
- ConfigManager,
3974
3848
  detectsHallucinatedFileOp,
3975
3849
  hadPreviousWriteToolCalls,
3976
3850
  TOOL_CALL_REMINDER,