jinzd-ai-cli 0.4.154 → 0.4.155

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/{batch-W57MV5OT.js → batch-LS3IJVBK.js} +2 -2
  2. package/dist/{chat-index-LUQWWLKO.js → chat-index-IF4EINLQ.js} +2 -1
  3. package/dist/{chunk-UE26B3RO.js → chunk-B3LFGPU2.js} +1 -1
  4. package/dist/{chunk-ZAYDVWY4.js → chunk-CIZQZ7CC.js} +23 -787
  5. package/dist/{chunk-SH7NTECG.js → chunk-D6GJTJQH.js} +1 -1
  6. package/dist/{chunk-XWYWASPT.js → chunk-E5ICQT3P.js} +4 -4
  7. package/dist/{chunk-OSTMMSOV.js → chunk-IBBYW6PM.js} +1 -1
  8. package/dist/{chunk-NP7WOVIH.js → chunk-JOJRBV2K.js} +1 -1
  9. package/dist/{chunk-2IODI5TI.js → chunk-JXSWY54M.js} +1 -1
  10. package/dist/{chunk-HVNEBTSF.js → chunk-NFRTSL3N.js} +1 -1
  11. package/dist/chunk-SLSWPBK3.js +120 -0
  12. package/dist/chunk-TOTEUETI.js +768 -0
  13. package/dist/{chunk-RXM76HB7.js → chunk-U5MY24UZ.js} +3 -117
  14. package/dist/{ci-JYZGZSMP.js → ci-34ZQH43L.js} +2 -2
  15. package/dist/{constants-S4Y6A25E.js → constants-DQ5VJOGS.js} +1 -1
  16. package/dist/{doctor-cli-FMTMDO2Z.js → doctor-cli-TSCI4ORL.js} +4 -4
  17. package/dist/electron-server.js +2 -2
  18. package/dist/{hub-OP7EWTQQ.js → hub-ZILVZWI2.js} +19 -3
  19. package/dist/index.js +28 -22
  20. package/dist/persist-3EBOLHFZ.js +52 -0
  21. package/dist/{run-tests-4XNY7QB4.js → run-tests-5CJRMOMI.js} +1 -1
  22. package/dist/{run-tests-3QAZGHP2.js → run-tests-5KWCHBQS.js} +2 -2
  23. package/dist/{server-UL42EXOA.js → server-35OQV62B.js} +16 -13
  24. package/dist/{server-W4TBZN6I.js → server-DVIP7NLW.js} +6 -5
  25. package/dist/{task-orchestrator-RLAZK5EB.js → task-orchestrator-AXSS7ROD.js} +6 -5
  26. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  truncateForPersist
4
- } from "./chunk-XWYWASPT.js";
4
+ } from "./chunk-E5ICQT3P.js";
5
5
  import {
6
6
  APP_NAME,
7
7
  CONFIG_DIR_NAME,
@@ -11,769 +11,7 @@ import {
11
11
  MCP_PROTOCOL_VERSION,
12
12
  MCP_TOOL_PREFIX,
13
13
  VERSION
14
- } from "./chunk-UE26B3RO.js";
15
- import {
16
- redactJson
17
- } from "./chunk-RXM76HB7.js";
18
-
19
- // src/session/session-manager.ts
20
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync, renameSync, openSync, readSync, closeSync } from "fs";
21
- import { join } from "path";
22
- import { v4 as uuidv4 } from "uuid";
23
-
24
- // src/core/types.ts
25
- function getContentText(content) {
26
- if (typeof content === "string") return content;
27
- return content.filter((p) => p.type === "text").map((p) => p.text).join("");
28
- }
29
-
30
- // src/session/session.ts
31
- function makeBranchId() {
32
- return Math.random().toString(16).slice(2, 8);
33
- }
34
- function messagesEqual(a, b) {
35
- if (a.role !== b.role) return false;
36
- if (JSON.stringify(a.content) !== JSON.stringify(b.content)) return false;
37
- if (JSON.stringify(a.toolCalls ?? null) !== JSON.stringify(b.toolCalls ?? null)) return false;
38
- return true;
39
- }
40
- var Session = class _Session {
41
- id;
42
- provider;
43
- model;
44
- created;
45
- updated;
46
- messages = [];
47
- title;
48
- /**
49
- * 标题是否已由 AI 生成过短摘要(v0.4.126+)。
50
- * - false / undefined:title 仍是 addMessage 截取的首条用户消息前 50 字(heuristic)
51
- * - true:已由 maybeGenerateSessionTitle 替换为 AI 摘要,不再二次生成
52
- *
53
- * 持久化在 toJSON / fromJSON 中,重启后不会重复请求。
54
- */
55
- titleAiGenerated;
56
- tokenUsage = {
57
- inputTokens: 0,
58
- outputTokens: 0,
59
- cacheCreationTokens: 0,
60
- cacheReadTokens: 0
61
- };
62
- checkpoints = [];
63
- // ── B2 Branches (v0.4.74+) ──────────────────────────────────────
64
- /**
65
- * All branches in this session. The 'main' branch is auto-created and
66
- * represents the linear conversation for pre-B2 sessions.
67
- */
68
- branches = [];
69
- /** Currently active branch — its messages live in `this.messages`. */
70
- activeBranchId = "main";
71
- /**
72
- * Stashed message arrays for INACTIVE branches. The active branch's
73
- * messages are always in `this.messages`, never duplicated here.
74
- */
75
- _inactiveBranchMessages = /* @__PURE__ */ new Map();
76
- constructor(id, provider, model) {
77
- this.id = id;
78
- this.provider = provider;
79
- this.model = model;
80
- this.created = /* @__PURE__ */ new Date();
81
- this.updated = /* @__PURE__ */ new Date();
82
- this.branches.push({
83
- id: "main",
84
- title: "main",
85
- parentBranchId: null,
86
- parentMessageIndex: 0,
87
- created: this.created
88
- });
89
- }
90
- /**
91
- * 更新 session 关联的 provider 和 model(在 /provider 或 /model 切换时调用)。
92
- * 保留对话历史和所有状态,仅更新元数据。
93
- */
94
- updateProvider(provider, model) {
95
- this.provider = provider;
96
- this.model = model;
97
- this.updated = /* @__PURE__ */ new Date();
98
- }
99
- addMessage(message) {
100
- this.messages.push(message);
101
- this.updated = /* @__PURE__ */ new Date();
102
- if (!this.title && message.role === "user") {
103
- this.title = getContentText(message.content).slice(0, 50).replace(/\n/g, " ");
104
- }
105
- }
106
- addTokenUsage(usage) {
107
- this.tokenUsage.inputTokens += usage.inputTokens;
108
- this.tokenUsage.outputTokens += usage.outputTokens;
109
- this.tokenUsage.cacheCreationTokens += usage.cacheCreationTokens ?? 0;
110
- this.tokenUsage.cacheReadTokens += usage.cacheReadTokens ?? 0;
111
- }
112
- clear() {
113
- this.messages = [];
114
- this.title = void 0;
115
- this.titleAiGenerated = false;
116
- this.tokenUsage = {
117
- inputTokens: 0,
118
- outputTokens: 0,
119
- cacheCreationTokens: 0,
120
- cacheReadTokens: 0
121
- };
122
- this.updated = /* @__PURE__ */ new Date();
123
- }
124
- /**
125
- * 上下文压缩:用摘要消息替换旧消息,保留最近 keepLast 条。
126
- *
127
- * Tool-history-aware: if the cut point lands inside a tool round
128
- * (assistant+toolCalls followed by tool results), expand to keep the
129
- * entire round intact. This prevents orphaned tool results.
130
- *
131
- * 压缩后消息结构:
132
- * [summaryMsg(user), ackMsg(assistant), ...最近 N 条原始消息]
133
- *
134
- * @returns 被删除的消息条数
135
- */
136
- compact(summaryMsg, ackMsg, keepLast) {
137
- let cutIndex = this.messages.length - keepLast;
138
- if (cutIndex <= 0) {
139
- return 0;
140
- }
141
- while (cutIndex > 0 && this.messages[cutIndex]?.role === "tool") {
142
- cutIndex--;
143
- }
144
- const preserved = this.messages.slice(cutIndex);
145
- const removedCount = cutIndex;
146
- this.messages = [summaryMsg, ackMsg, ...preserved];
147
- this.updated = /* @__PURE__ */ new Date();
148
- return removedCount;
149
- }
150
- /** 在当前消息位置创建检查点 */
151
- createCheckpoint(name) {
152
- this.checkpoints = this.checkpoints.filter((c) => c.name !== name);
153
- this.checkpoints.push({
154
- name,
155
- messageIndex: this.messages.length,
156
- timestamp: /* @__PURE__ */ new Date()
157
- });
158
- }
159
- /** 恢复到指定检查点:截断消息到检查点位置,移除后续检查点 */
160
- restoreCheckpoint(name) {
161
- const cp = this.checkpoints.find((c) => c.name === name);
162
- if (!cp) return false;
163
- this.messages = this.messages.slice(0, cp.messageIndex);
164
- this.checkpoints = this.checkpoints.filter((c) => c.messageIndex <= cp.messageIndex);
165
- this.updated = /* @__PURE__ */ new Date();
166
- return true;
167
- }
168
- listCheckpoints() {
169
- return [...this.checkpoints];
170
- }
171
- deleteCheckpoint(name) {
172
- const len = this.checkpoints.length;
173
- this.checkpoints = this.checkpoints.filter((c) => c.name !== name);
174
- return this.checkpoints.length < len;
175
- }
176
- // ── B2 Branch operations ────────────────────────────────────────
177
- /** Deep-clone a messages array (matches `Session.fork` semantics). */
178
- static cloneMessages(msgs) {
179
- return msgs.map((m) => {
180
- const cloned = { ...m };
181
- if (Array.isArray(cloned.content)) {
182
- cloned.content = cloned.content.map(
183
- (part) => typeof part === "object" && part !== null ? { ...part } : part
184
- );
185
- }
186
- if (cloned.toolCalls) {
187
- cloned.toolCalls = cloned.toolCalls.map((tc) => ({ ...tc }));
188
- }
189
- return cloned;
190
- });
191
- }
192
- /** List all branches (metadata only). */
193
- listBranches() {
194
- return this.branches.map((b) => ({ ...b }));
195
- }
196
- /** Current active branch metadata. */
197
- getActiveBranch() {
198
- const b = this.branches.find((b2) => b2.id === this.activeBranchId);
199
- if (!b) {
200
- this.activeBranchId = this.branches[0]?.id ?? "main";
201
- return this.branches[0] ?? { id: "main", title: "main", parentBranchId: null, parentMessageIndex: 0, created: /* @__PURE__ */ new Date() };
202
- }
203
- return b;
204
- }
205
- /**
206
- * Create a new branch by forking the active branch at message index
207
- * `fromIndex`. Copies `messages[0..fromIndex]` into the new branch
208
- * and switches to it. The original active branch is preserved intact
209
- * in the stash.
210
- *
211
- * @returns new branch id
212
- * @throws if fromIndex is out of range
213
- */
214
- createBranch(fromIndex, title) {
215
- if (fromIndex < 0 || fromIndex > this.messages.length) {
216
- throw new Error(
217
- `createBranch: fromIndex ${fromIndex} out of range [0, ${this.messages.length}]`
218
- );
219
- }
220
- this._inactiveBranchMessages.set(this.activeBranchId, this.messages);
221
- const id = makeBranchId();
222
- const meta = {
223
- id,
224
- title: title || `branch-${this.branches.length + 1}`,
225
- parentBranchId: this.activeBranchId,
226
- parentMessageIndex: fromIndex,
227
- created: /* @__PURE__ */ new Date()
228
- };
229
- this.branches.push(meta);
230
- this.messages = _Session.cloneMessages(this.messages.slice(0, fromIndex));
231
- this.activeBranchId = id;
232
- this.updated = /* @__PURE__ */ new Date();
233
- return id;
234
- }
235
- /**
236
- * Switch the active branch. Stashes current messages under the old
237
- * active id and loads the target branch's messages into `this.messages`.
238
- *
239
- * @returns true if switched, false if id not found or already active
240
- */
241
- switchBranch(id) {
242
- if (id === this.activeBranchId) return false;
243
- if (!this.branches.some((b) => b.id === id)) return false;
244
- this._inactiveBranchMessages.set(this.activeBranchId, this.messages);
245
- const target = this._inactiveBranchMessages.get(id) ?? [];
246
- this._inactiveBranchMessages.delete(id);
247
- this.messages = target;
248
- this.activeBranchId = id;
249
- this.updated = /* @__PURE__ */ new Date();
250
- return true;
251
- }
252
- /**
253
- * Delete a branch by id. Cannot delete the active branch or the last
254
- * remaining branch. If other branches list this one as parent, their
255
- * parent pointer is retargeted to this branch's parent (transparent
256
- * to callers — branches still form a valid forest).
257
- *
258
- * @returns true if deleted
259
- */
260
- deleteBranch(id) {
261
- if (id === this.activeBranchId) return false;
262
- if (this.branches.length <= 1) return false;
263
- const idx = this.branches.findIndex((b) => b.id === id);
264
- if (idx === -1) return false;
265
- const deleted = this.branches[idx];
266
- for (const b of this.branches) {
267
- if (b.parentBranchId === id) {
268
- b.parentBranchId = deleted.parentBranchId;
269
- }
270
- }
271
- this.branches.splice(idx, 1);
272
- this._inactiveBranchMessages.delete(id);
273
- this.updated = /* @__PURE__ */ new Date();
274
- return true;
275
- }
276
- /** Rename a branch (affects only display title). */
277
- renameBranch(id, newTitle) {
278
- const b = this.branches.find((b2) => b2.id === id);
279
- if (!b) return false;
280
- b.title = newTitle;
281
- this.updated = /* @__PURE__ */ new Date();
282
- return true;
283
- }
284
- /**
285
- * Resolve a user-supplied branch reference (v0.4.81+).
286
- *
287
- * Accepts the 6-hex branch id, the branch's display title, or an unambiguous
288
- * id prefix (≥2 chars). This exists because users naturally type the title
289
- * they chose at `/branch new`, not the auto-generated id — every other
290
- * `/branch <sub>` used to error on that with "not found".
291
- *
292
- * Resolution order:
293
- * 1. Exact id match
294
- * 2. Exact title match (unique)
295
- * 3. Id prefix match (unique, length ≥ 2)
296
- *
297
- * @returns `{ ok: true, id }` on success; on failure `{ ok: false, reason, matches }`
298
- * where `matches` lists candidates for ambiguous input so callers
299
- * can render a helpful error.
300
- */
301
- resolveBranchRef(ref) {
302
- if (!ref) return { ok: false, reason: "not-found", matches: [] };
303
- const byId = this.branches.find((b) => b.id === ref);
304
- if (byId) return { ok: true, id: byId.id };
305
- const byTitle = this.branches.filter((b) => b.title === ref);
306
- if (byTitle.length === 1) return { ok: true, id: byTitle[0].id };
307
- if (byTitle.length > 1) return { ok: false, reason: "ambiguous", matches: byTitle };
308
- if (ref.length >= 2) {
309
- const byPrefix = this.branches.filter((b) => b.id.startsWith(ref));
310
- if (byPrefix.length === 1) return { ok: true, id: byPrefix[0].id };
311
- if (byPrefix.length > 1) return { ok: false, reason: "ambiguous", matches: byPrefix };
312
- }
313
- return { ok: false, reason: "not-found", matches: [] };
314
- }
315
- /** Messages of any branch (active or inactive) — read-only copy. */
316
- getBranchMessages(id) {
317
- if (id === this.activeBranchId) return this.messages.slice();
318
- const m = this._inactiveBranchMessages.get(id);
319
- return m ? m.slice() : null;
320
- }
321
- // ── B3 Branch diff + cherry-pick (v0.4.80+) ────────────────────────
322
- /**
323
- * Compare messages between two branches. Finds the longest common prefix
324
- * by message equality (role + content + toolCalls shape) — NOT by fork
325
- * point metadata, so user-edited histories still diff cleanly.
326
- *
327
- * @param sourceId Branch to compare
328
- * @param targetId Branch to compare against (defaults to active branch)
329
- * @returns BranchDiff, or null if either branch id is unknown
330
- */
331
- diffBranches(sourceId, targetId) {
332
- const tgt = targetId ?? this.activeBranchId;
333
- const src = this.getBranchMessages(sourceId);
334
- const dst = this.getBranchMessages(tgt);
335
- if (!src || !dst) return null;
336
- let i = 0;
337
- while (i < src.length && i < dst.length && messagesEqual(src[i], dst[i])) i++;
338
- return {
339
- sourceId,
340
- targetId: tgt,
341
- commonPrefix: i,
342
- sourceOnly: src.slice(i),
343
- targetOnly: dst.slice(i)
344
- };
345
- }
346
- /**
347
- * Copy a single message from another branch into the active branch
348
- * (appended at the end). Timestamp is reset to now so it's clear when
349
- * the cherry-pick happened.
350
- *
351
- * @returns the cloned message, or null if sourceId / msgIndex is invalid
352
- */
353
- cherryPickMessage(sourceId, msgIndex) {
354
- const src = this.getBranchMessages(sourceId);
355
- if (!src) return null;
356
- if (msgIndex < 0 || msgIndex >= src.length) return null;
357
- const orig = src[msgIndex];
358
- const copy = {
359
- ...orig,
360
- timestamp: /* @__PURE__ */ new Date()
361
- };
362
- this.messages.push(copy);
363
- this.updated = /* @__PURE__ */ new Date();
364
- return copy;
365
- }
366
- getMeta() {
367
- return {
368
- id: this.id,
369
- provider: this.provider,
370
- model: this.model,
371
- messageCount: this.messages.length,
372
- created: this.created,
373
- updated: this.updated,
374
- title: this.title
375
- };
376
- }
377
- toJSON() {
378
- const serializeMessages = (msgs) => msgs.map((m) => {
379
- const out = {
380
- role: m.role,
381
- content: m.content,
382
- timestamp: m.timestamp.toISOString()
383
- };
384
- if (m.toolCalls) out.toolCalls = m.toolCalls;
385
- if (m.reasoningContent !== void 0) out.reasoningContent = m.reasoningContent;
386
- if (m.toolCallId) out.toolCallId = m.toolCallId;
387
- if (m.toolName) out.toolName = m.toolName;
388
- if (m.isError !== void 0) out.isError = m.isError;
389
- return out;
390
- });
391
- const branchMessages = {};
392
- for (const [id, msgs] of this._inactiveBranchMessages.entries()) {
393
- branchMessages[id] = serializeMessages(msgs);
394
- }
395
- return {
396
- id: this.id,
397
- provider: this.provider,
398
- model: this.model,
399
- created: this.created.toISOString(),
400
- updated: this.updated.toISOString(),
401
- title: this.title,
402
- titleAiGenerated: this.titleAiGenerated ?? false,
403
- tokenUsage: { ...this.tokenUsage },
404
- checkpoints: this.checkpoints.map((c) => ({
405
- name: c.name,
406
- messageIndex: c.messageIndex,
407
- timestamp: c.timestamp.toISOString()
408
- })),
409
- // B2 Branches (v0.4.74+). Omitted for sessions with only the default
410
- // 'main' branch and no stashed messages (keeps file size identical
411
- // to pre-B2 for the common case).
412
- ...this.branches.length > 1 || this._inactiveBranchMessages.size > 0 ? {
413
- activeBranchId: this.activeBranchId,
414
- branches: this.branches.map((b) => ({
415
- id: b.id,
416
- title: b.title,
417
- parentBranchId: b.parentBranchId,
418
- parentMessageIndex: b.parentMessageIndex,
419
- created: b.created.toISOString()
420
- })),
421
- branchMessages
422
- } : {},
423
- messages: serializeMessages(this.messages)
424
- };
425
- }
426
- /**
427
- * 从现有 session 分叉创建新 session。
428
- *
429
- * 复制 messages[0..messageCount],保留范围内的 checkpoints。
430
- * 新 session 拥有独立 UUID,title 默认 "Fork of <原标题>"。
431
- *
432
- * @param original 原始 session
433
- * @param newId 新 session 的 UUID
434
- * @param messageCount 复制的消息数量(0 = 空 session,> messages.length 则取全部)
435
- * @param newTitle 可选的新标题
436
- */
437
- static fork(original, newId, messageCount, newTitle) {
438
- const forked = new _Session(newId, original.provider, original.model);
439
- forked.title = newTitle ?? (original.title ? `Fork of ${original.title}` : void 0);
440
- const clampedCount = Math.min(Math.max(messageCount, 0), original.messages.length);
441
- forked.messages = original.messages.slice(0, clampedCount).map((m) => {
442
- const cloned = { ...m };
443
- if (Array.isArray(cloned.content)) {
444
- cloned.content = cloned.content.map(
445
- (part) => typeof part === "object" && part !== null ? { ...part } : part
446
- );
447
- }
448
- return cloned;
449
- });
450
- forked.checkpoints = original.checkpoints.filter((c) => c.messageIndex <= clampedCount).map((c) => ({ ...c, timestamp: new Date(c.timestamp.getTime()) }));
451
- forked.updated = /* @__PURE__ */ new Date();
452
- return forked;
453
- }
454
- /**
455
- * 从磁盘 JSON 数据恢复 Session 实例。
456
- * 添加运行时校验:损坏或不兼容的历史文件会抛出明确错误,而非 TypeError 崩溃。
457
- */
458
- static fromJSON(data) {
459
- const d = data;
460
- if (!d || typeof d !== "object") {
461
- throw new Error("Invalid session data: expected an object");
462
- }
463
- if (typeof d.id !== "string" || typeof d.provider !== "string" || typeof d.model !== "string") {
464
- throw new Error("Invalid session data: missing or invalid id/provider/model fields");
465
- }
466
- if (!Array.isArray(d.messages)) {
467
- throw new Error("Invalid session data: messages is not an array");
468
- }
469
- const session = new _Session(d.id, d.provider, d.model);
470
- session.title = typeof d.title === "string" ? d.title : void 0;
471
- session.titleAiGenerated = d.titleAiGenerated === true;
472
- const created = new Date(d.created);
473
- const updated = new Date(d.updated);
474
- session.created = isNaN(created.getTime()) ? /* @__PURE__ */ new Date() : created;
475
- session.updated = isNaN(updated.getTime()) ? /* @__PURE__ */ new Date() : updated;
476
- const tu = d.tokenUsage;
477
- if (tu && typeof tu === "object") {
478
- session.tokenUsage = {
479
- inputTokens: typeof tu.inputTokens === "number" ? tu.inputTokens : 0,
480
- outputTokens: typeof tu.outputTokens === "number" ? tu.outputTokens : 0,
481
- cacheCreationTokens: typeof tu.cacheCreationTokens === "number" ? tu.cacheCreationTokens : 0,
482
- cacheReadTokens: typeof tu.cacheReadTokens === "number" ? tu.cacheReadTokens : 0
483
- };
484
- }
485
- if (Array.isArray(d.checkpoints)) {
486
- session.checkpoints = d.checkpoints.map((c) => ({
487
- name: String(c.name ?? ""),
488
- messageIndex: typeof c.messageIndex === "number" ? c.messageIndex : 0,
489
- timestamp: new Date(c.timestamp)
490
- }));
491
- }
492
- const deserializeMessages = (arr) => arr.map((m) => {
493
- const ts = new Date(m.timestamp);
494
- const msg = {
495
- role: m.role ?? "user",
496
- content: Array.isArray(m.content) ? m.content : String(m.content ?? ""),
497
- timestamp: isNaN(ts.getTime()) ? /* @__PURE__ */ new Date() : ts
498
- };
499
- if (Array.isArray(m.toolCalls)) msg.toolCalls = m.toolCalls;
500
- if (typeof m.reasoningContent === "string") msg.reasoningContent = m.reasoningContent;
501
- if (typeof m.toolCallId === "string") msg.toolCallId = m.toolCallId;
502
- if (typeof m.toolName === "string") msg.toolName = m.toolName;
503
- if (typeof m.isError === "boolean") msg.isError = m.isError;
504
- return msg;
505
- });
506
- session.messages = deserializeMessages(d.messages);
507
- if (Array.isArray(d.branches) && d.branches.length > 0) {
508
- session.branches = d.branches.map((b) => {
509
- const ts = new Date(b.created);
510
- return {
511
- id: String(b.id ?? "main"),
512
- title: String(b.title ?? b.id ?? "main"),
513
- parentBranchId: typeof b.parentBranchId === "string" ? b.parentBranchId : null,
514
- parentMessageIndex: typeof b.parentMessageIndex === "number" ? b.parentMessageIndex : 0,
515
- created: isNaN(ts.getTime()) ? /* @__PURE__ */ new Date() : ts
516
- };
517
- });
518
- session.activeBranchId = typeof d.activeBranchId === "string" ? d.activeBranchId : session.branches[0]?.id ?? "main";
519
- const bm = d.branchMessages;
520
- if (bm && typeof bm === "object") {
521
- for (const [id, arr] of Object.entries(bm)) {
522
- if (id === session.activeBranchId) continue;
523
- if (!Array.isArray(arr)) continue;
524
- session._inactiveBranchMessages.set(
525
- id,
526
- deserializeMessages(arr)
527
- );
528
- }
529
- }
530
- }
531
- return session;
532
- }
533
- };
534
-
535
- // src/session/session-manager.ts
536
- function safeDate(value) {
537
- const d = new Date(value);
538
- return isNaN(d.getTime()) ? /* @__PURE__ */ new Date(0) : d;
539
- }
540
- function extractJsonField(header, field) {
541
- const re = new RegExp(`"${field}"\\s*:\\s*"([^"]*)"`, "i");
542
- const m = header.match(re);
543
- return m ? m[1] : void 0;
544
- }
545
- var SessionManager = class {
546
- _current = null;
547
- historyDir;
548
- config;
549
- /** Last save's redaction hit count — exposed for /security status reporting */
550
- lastRedactionHits = 0;
551
- constructor(config) {
552
- this.config = config;
553
- this.historyDir = config.getHistoryDir();
554
- }
555
- /**
556
- * Build redaction options from config. Returns `{ enabled: false }` when
557
- * `security.redactOnSave` is off or `security.mode` is 'off'.
558
- */
559
- redactOptionsForSave() {
560
- const security = this.config.get("security");
561
- if (!security || !security.redactOnSave || security.mode === "off") {
562
- return { enabled: false };
563
- }
564
- return {
565
- enabled: true,
566
- customRegexes: security.customPatterns ?? []
567
- };
568
- }
569
- get current() {
570
- return this._current;
571
- }
572
- createSession(provider, model) {
573
- const session = new Session(uuidv4(), provider, model);
574
- this._current = session;
575
- return session;
576
- }
577
- /**
578
- * 直接设置当前会话(用于从内存缓存恢复未保存的会话)。
579
- * 与 `loadSession` 不同,此方法不读取磁盘,也不抛出错误。
580
- * Web 多 Tab 场景下,SessionHandler 会维护一份未保存会话的内存缓存,
581
- * 切换 Tab 时通过此方法将缓存中的会话设为当前会话,避免"Session not found"。
582
- */
583
- setCurrent(session) {
584
- this._current = session;
585
- }
586
- /** 清除当前会话引用(下次访问将触发 lazy 创建)。 */
587
- clearCurrent() {
588
- this._current = null;
589
- }
590
- async save() {
591
- if (!this._current) return;
592
- mkdirSync(this.historyDir, { recursive: true });
593
- const filePath = join(this.historyDir, `${this._current.id}.json`);
594
- const raw = this._current.toJSON();
595
- const opts = this.redactOptionsForSave();
596
- const { value: payload, hits } = redactJson(raw, opts);
597
- this.lastRedactionHits = hits.length;
598
- const tmpPath = filePath + ".tmp";
599
- writeFileSync(tmpPath, JSON.stringify(payload, null, 2), "utf-8");
600
- renameSync(tmpPath, filePath);
601
- }
602
- loadSession(id) {
603
- const filePath = join(this.historyDir, `${id}.json`);
604
- if (!existsSync(filePath)) {
605
- throw new Error(`Session ${id} not found`);
606
- }
607
- let data;
608
- try {
609
- data = JSON.parse(readFileSync(filePath, "utf-8"));
610
- } catch (err) {
611
- throw new Error(`Session ${id} is corrupted: ${err instanceof Error ? err.message : String(err)}`);
612
- }
613
- const session = Session.fromJSON(data);
614
- this._current = session;
615
- return session;
616
- }
617
- listSessions() {
618
- if (!existsSync(this.historyDir)) return [];
619
- const files = readdirSync(this.historyDir).filter((f) => f.endsWith(".json"));
620
- const metas = [];
621
- for (const file of files) {
622
- try {
623
- const meta = this.readSessionMeta(join(this.historyDir, file));
624
- if (meta) metas.push(meta);
625
- } catch (err) {
626
- process.stderr.write(
627
- `[Warning] Skipping corrupted session file "${file}": ${err instanceof Error ? err.message : String(err)}
628
- `
629
- );
630
- }
631
- }
632
- return metas.sort((a, b) => b.updated.getTime() - a.updated.getTime());
633
- }
634
- /**
635
- * P1-B: Read only the first ~1KB of a session file to extract metadata fields.
636
- * Session JSON format puts id/provider/model/created/updated/title before the
637
- * large "messages" array, so a small header read suffices for metadata extraction.
638
- * Falls back to full file read if header parsing fails.
639
- */
640
- readSessionMeta(filePath) {
641
- const HEADER_SIZE = 1024;
642
- let header;
643
- try {
644
- const fd = openSync(filePath, "r");
645
- const buf = Buffer.alloc(HEADER_SIZE);
646
- const bytesRead = readSync(fd, buf, 0, HEADER_SIZE, 0);
647
- closeSync(fd);
648
- header = buf.toString("utf-8", 0, bytesRead);
649
- } catch {
650
- return null;
651
- }
652
- const id = extractJsonField(header, "id");
653
- const provider = extractJsonField(header, "provider");
654
- const model = extractJsonField(header, "model");
655
- const created = extractJsonField(header, "created");
656
- const updated = extractJsonField(header, "updated");
657
- const title = extractJsonField(header, "title");
658
- if (id && provider && model) {
659
- let messageCount = 0;
660
- try {
661
- const full = readFileSync(filePath, "utf-8");
662
- const matches = full.match(/"role"\s*:/g);
663
- messageCount = matches ? matches.length : 0;
664
- } catch {
665
- }
666
- return {
667
- id,
668
- provider,
669
- model,
670
- messageCount,
671
- created: safeDate(created),
672
- updated: safeDate(updated),
673
- title: title || void 0
674
- };
675
- }
676
- const data = JSON.parse(readFileSync(filePath, "utf-8"));
677
- return {
678
- id: data.id,
679
- provider: data.provider,
680
- model: data.model,
681
- messageCount: data.messages?.length ?? 0,
682
- created: safeDate(data.created),
683
- updated: safeDate(data.updated),
684
- title: data.title
685
- };
686
- }
687
- deleteSession(id) {
688
- const filePath = join(this.historyDir, `${id}.json`);
689
- if (!existsSync(filePath)) return false;
690
- try {
691
- unlinkSync(filePath);
692
- if (this._current && this._current.id === id) {
693
- this._current = null;
694
- }
695
- return true;
696
- } catch {
697
- return false;
698
- }
699
- }
700
- /**
701
- * 从当前 session 分叉创建新 session。
702
- *
703
- * 先保存原始 session(保留完整历史),然后创建分叉(截取到 messageCount),
704
- * 将分叉设为当前 session 并保存。
705
- *
706
- * @param messageCount 复制的消息数量
707
- * @param title 可选的新标题
708
- * @returns 新的分叉 session
709
- */
710
- async forkSession(messageCount, title) {
711
- if (!this._current) {
712
- throw new Error("No active session to fork");
713
- }
714
- await this.save();
715
- const forked = Session.fork(this._current, uuidv4(), messageCount, title);
716
- this._current = forked;
717
- await this.save();
718
- return forked;
719
- }
720
- /**
721
- * 跨 session 全文搜索。
722
- * 遍历所有历史 JSON 文件,逐条匹配消息内容(不区分大小写),
723
- * 每个 session 最多返回 3 条匹配片段,全局最多 maxResults 个 session。
724
- */
725
- searchMessages(query, maxResults = 20) {
726
- if (!existsSync(this.historyDir)) return [];
727
- const q = query.toLowerCase();
728
- const files = readdirSync(this.historyDir).filter((f) => f.endsWith(".json")).map((f) => join(this.historyDir, f));
729
- const results = [];
730
- for (const filePath of files) {
731
- if (results.length >= maxResults) break;
732
- try {
733
- const data = JSON.parse(readFileSync(filePath, "utf-8"));
734
- const messages = data.messages ?? [];
735
- const matches = [];
736
- for (const msg of messages) {
737
- if (matches.length >= 3) break;
738
- let text = "";
739
- if (typeof msg.content === "string") {
740
- text = msg.content;
741
- } else if (Array.isArray(msg.content)) {
742
- text = msg.content.filter((p) => p.type === "text").map((p) => p.text ?? "").join("");
743
- }
744
- const lowerText = text.toLowerCase();
745
- const idx = lowerText.indexOf(q);
746
- if (idx !== -1) {
747
- const start = Math.max(0, idx - 30);
748
- const end = Math.min(text.length, idx + query.length + 60);
749
- const snippet = (start > 0 ? "\u2026" : "") + text.slice(start, end).replace(/\n/g, " ") + (end < text.length ? "\u2026" : "");
750
- matches.push({ role: msg.role, snippet });
751
- }
752
- }
753
- if (matches.length > 0) {
754
- results.push({
755
- sessionMeta: {
756
- id: data.id,
757
- provider: data.provider,
758
- model: data.model,
759
- messageCount: messages.length,
760
- created: safeDate(data.created),
761
- updated: safeDate(data.updated),
762
- title: data.title
763
- },
764
- matches
765
- });
766
- }
767
- } catch (err) {
768
- process.stderr.write(
769
- `[Warning] Skipping corrupted session file "${filePath}": ${err instanceof Error ? err.message : String(err)}
770
- `
771
- );
772
- }
773
- }
774
- return results.sort((a, b) => b.sessionMeta.updated.getTime() - a.sessionMeta.updated.getTime());
775
- }
776
- };
14
+ } from "./chunk-B3LFGPU2.js";
777
15
 
778
16
  // src/mcp/client.ts
779
17
  import { spawn } from "child_process";
@@ -1294,11 +532,11 @@ var McpManager = class {
1294
532
  };
1295
533
 
1296
534
  // src/skills/manager.ts
1297
- import { existsSync as existsSync2, readdirSync as readdirSync2, mkdirSync as mkdirSync2, statSync } from "fs";
1298
- import { join as join2 } from "path";
535
+ import { existsSync, readdirSync, mkdirSync, statSync } from "fs";
536
+ import { join } from "path";
1299
537
 
1300
538
  // src/skills/types.ts
1301
- import { readFileSync as readFileSync2 } from "fs";
539
+ import { readFileSync } from "fs";
1302
540
  import { basename } from "path";
1303
541
  function parseSimpleYaml(yaml) {
1304
542
  const result = {};
@@ -1320,7 +558,7 @@ function parseYamlArray(value) {
1320
558
  function parseSkillFile(filePath) {
1321
559
  let raw;
1322
560
  try {
1323
- raw = readFileSync2(filePath, "utf-8");
561
+ raw = readFileSync(filePath, "utf-8");
1324
562
  } catch {
1325
563
  return null;
1326
564
  }
@@ -1363,29 +601,29 @@ var SkillManager = class {
1363
601
  /** 发现并加载 skillsDir 下所有 .md 文件,返回加载数量 */
1364
602
  loadSkills() {
1365
603
  this.skills.clear();
1366
- if (!existsSync2(this.skillsDir)) {
604
+ if (!existsSync(this.skillsDir)) {
1367
605
  try {
1368
- mkdirSync2(this.skillsDir, { recursive: true });
606
+ mkdirSync(this.skillsDir, { recursive: true });
1369
607
  } catch {
1370
608
  }
1371
609
  return 0;
1372
610
  }
1373
611
  let entries;
1374
612
  try {
1375
- entries = readdirSync2(this.skillsDir);
613
+ entries = readdirSync(this.skillsDir);
1376
614
  } catch {
1377
615
  return 0;
1378
616
  }
1379
617
  for (const entry of entries) {
1380
618
  let filePath;
1381
- const fullPath = join2(this.skillsDir, entry);
619
+ const fullPath = join(this.skillsDir, entry);
1382
620
  if (entry.endsWith(".md")) {
1383
621
  filePath = fullPath;
1384
622
  } else {
1385
623
  try {
1386
624
  if (statSync(fullPath).isDirectory()) {
1387
- const skillMd = join2(fullPath, "SKILL.md");
1388
- if (existsSync2(skillMd)) {
625
+ const skillMd = join(fullPath, "SKILL.md");
626
+ if (existsSync(skillMd)) {
1389
627
  filePath = skillMd;
1390
628
  } else {
1391
629
  continue;
@@ -1639,8 +877,8 @@ function autoTrimSessionIfNeeded(session, sizeLimit = SESSION_SIZE_LIMIT) {
1639
877
  }
1640
878
 
1641
879
  // src/repl/dev-state.ts
1642
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3 } from "fs";
1643
- import { join as join3 } from "path";
880
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
881
+ import { join as join2 } from "path";
1644
882
  import { homedir } from "os";
1645
883
  var DEV_STATE_MAX_CHARS = 6e3;
1646
884
  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.
@@ -1694,12 +932,12 @@ function sessionHasMeaningfulContent(messages) {
1694
932
  return hasUser && hasAssistant;
1695
933
  }
1696
934
  function getDevStatePath() {
1697
- return join3(homedir(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
935
+ return join2(homedir(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
1698
936
  }
1699
937
  function saveDevState(content) {
1700
- const configDir = join3(homedir(), CONFIG_DIR_NAME);
1701
- if (!existsSync3(configDir)) {
1702
- mkdirSync3(configDir, { recursive: true });
938
+ const configDir = join2(homedir(), CONFIG_DIR_NAME);
939
+ if (!existsSync2(configDir)) {
940
+ mkdirSync2(configDir, { recursive: true });
1703
941
  }
1704
942
  let trimmed = content.trim();
1705
943
  if (trimmed.length > DEV_STATE_MAX_CHARS) {
@@ -1710,27 +948,25 @@ function saveDevState(content) {
1710
948
  }
1711
949
  trimmed += "\n\n[...truncated]";
1712
950
  }
1713
- writeFileSync2(getDevStatePath(), trimmed, "utf-8");
951
+ writeFileSync(getDevStatePath(), trimmed, "utf-8");
1714
952
  }
1715
953
  function loadDevState() {
1716
954
  const path = getDevStatePath();
1717
- if (!existsSync3(path)) return null;
1718
- const content = readFileSync3(path, "utf-8").trim();
955
+ if (!existsSync2(path)) return null;
956
+ const content = readFileSync2(path, "utf-8").trim();
1719
957
  return content || null;
1720
958
  }
1721
959
  function clearDevState() {
1722
960
  const path = getDevStatePath();
1723
- if (existsSync3(path)) {
961
+ if (existsSync2(path)) {
1724
962
  try {
1725
- unlinkSync2(path);
963
+ unlinkSync(path);
1726
964
  } catch {
1727
965
  }
1728
966
  }
1729
967
  }
1730
968
 
1731
969
  export {
1732
- getContentText,
1733
- SessionManager,
1734
970
  getPricing,
1735
971
  computeCost,
1736
972
  formatCost,