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.
- package/dist/{batch-W57MV5OT.js → batch-LS3IJVBK.js} +2 -2
- package/dist/{chat-index-LUQWWLKO.js → chat-index-IF4EINLQ.js} +2 -1
- package/dist/{chunk-UE26B3RO.js → chunk-B3LFGPU2.js} +1 -1
- package/dist/{chunk-ZAYDVWY4.js → chunk-CIZQZ7CC.js} +23 -787
- package/dist/{chunk-SH7NTECG.js → chunk-D6GJTJQH.js} +1 -1
- package/dist/{chunk-XWYWASPT.js → chunk-E5ICQT3P.js} +4 -4
- package/dist/{chunk-OSTMMSOV.js → chunk-IBBYW6PM.js} +1 -1
- package/dist/{chunk-NP7WOVIH.js → chunk-JOJRBV2K.js} +1 -1
- package/dist/{chunk-2IODI5TI.js → chunk-JXSWY54M.js} +1 -1
- package/dist/{chunk-HVNEBTSF.js → chunk-NFRTSL3N.js} +1 -1
- package/dist/chunk-SLSWPBK3.js +120 -0
- package/dist/chunk-TOTEUETI.js +768 -0
- package/dist/{chunk-RXM76HB7.js → chunk-U5MY24UZ.js} +3 -117
- package/dist/{ci-JYZGZSMP.js → ci-34ZQH43L.js} +2 -2
- package/dist/{constants-S4Y6A25E.js → constants-DQ5VJOGS.js} +1 -1
- package/dist/{doctor-cli-FMTMDO2Z.js → doctor-cli-TSCI4ORL.js} +4 -4
- package/dist/electron-server.js +2 -2
- package/dist/{hub-OP7EWTQQ.js → hub-ZILVZWI2.js} +19 -3
- package/dist/index.js +28 -22
- package/dist/persist-3EBOLHFZ.js +52 -0
- package/dist/{run-tests-4XNY7QB4.js → run-tests-5CJRMOMI.js} +1 -1
- package/dist/{run-tests-3QAZGHP2.js → run-tests-5KWCHBQS.js} +2 -2
- package/dist/{server-UL42EXOA.js → server-35OQV62B.js} +16 -13
- package/dist/{server-W4TBZN6I.js → server-DVIP7NLW.js} +6 -5
- package/dist/{task-orchestrator-RLAZK5EB.js → task-orchestrator-AXSS7ROD.js} +6 -5
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
truncateForPersist
|
|
4
|
-
} from "./chunk-
|
|
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-
|
|
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
|
|
1298
|
-
import { join
|
|
535
|
+
import { existsSync, readdirSync, mkdirSync, statSync } from "fs";
|
|
536
|
+
import { join } from "path";
|
|
1299
537
|
|
|
1300
538
|
// src/skills/types.ts
|
|
1301
|
-
import { readFileSync
|
|
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 =
|
|
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 (!
|
|
604
|
+
if (!existsSync(this.skillsDir)) {
|
|
1367
605
|
try {
|
|
1368
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1388
|
-
if (
|
|
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
|
|
1643
|
-
import { join as
|
|
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
|
|
935
|
+
return join2(homedir(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
|
|
1698
936
|
}
|
|
1699
937
|
function saveDevState(content) {
|
|
1700
|
-
const configDir =
|
|
1701
|
-
if (!
|
|
1702
|
-
|
|
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
|
-
|
|
951
|
+
writeFileSync(getDevStatePath(), trimmed, "utf-8");
|
|
1714
952
|
}
|
|
1715
953
|
function loadDevState() {
|
|
1716
954
|
const path = getDevStatePath();
|
|
1717
|
-
if (!
|
|
1718
|
-
const content =
|
|
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 (
|
|
961
|
+
if (existsSync2(path)) {
|
|
1724
962
|
try {
|
|
1725
|
-
|
|
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,
|