ccjk 4.0.0-beta.1 → 4.0.0-beta.2
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/LICENSE +50 -1
- package/dist/chunks/claude-wrapper.mjs +3588 -393
- package/dist/chunks/codex.mjs +1 -1
- package/dist/chunks/context.mjs +5 -7
- package/dist/chunks/init.mjs +18 -2
- package/dist/chunks/menu.mjs +1 -1
- package/dist/chunks/package.mjs +1 -1
- package/dist/chunks/platform.mjs +1 -50
- package/dist/cli.mjs +4 -4
- package/dist/i18n/locales/en/errors.json +14 -1
- package/dist/i18n/locales/zh-CN/errors.json +14 -1
- package/dist/index.mjs +1 -1
- package/package.json +28 -24
- package/templates/claude-code/en/workflow/essential/commands/feat.md +6 -0
- package/templates/claude-code/zh-CN/workflow/essential/commands/feat.md +6 -0
|
@@ -1,462 +1,3657 @@
|
|
|
1
1
|
import process__default from 'node:process';
|
|
2
|
+
import { existsSync, createReadStream, readFileSync, statSync, watch, mkdirSync } from 'node:fs';
|
|
3
|
+
import { homedir, tmpdir } from 'node:os';
|
|
4
|
+
import { join, dirname, normalize } from 'pathe';
|
|
2
5
|
import { exec } from 'tinyexec';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return "zsh";
|
|
20
|
-
}
|
|
21
|
-
if (shell.includes("fish")) {
|
|
22
|
-
return "fish";
|
|
23
|
-
}
|
|
24
|
-
return "unknown";
|
|
6
|
+
import { EventEmitter } from 'node:events';
|
|
7
|
+
import { readFile, mkdir, writeFile, readdir, rename, unlink, stat } from 'node:fs/promises';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
10
|
+
import { createInterface } from 'node:readline';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CONFIG = {
|
|
13
|
+
toolResultThreshold: 500,
|
|
14
|
+
// 超过 500 tokens 的工具结果会被省略
|
|
15
|
+
preserveErrors: true,
|
|
16
|
+
preserveKeyInfo: true,
|
|
17
|
+
keyTools: ["Read", "Grep", "Glob", "Bash", "WebFetch"],
|
|
18
|
+
placeholderTemplate: "<Tool result omitted to save tokens. The assistant's response below contains the key findings.>"
|
|
19
|
+
};
|
|
20
|
+
function estimateTokens$1(text) {
|
|
21
|
+
return Math.ceil(text.length / 4);
|
|
25
22
|
}
|
|
26
|
-
function
|
|
27
|
-
const
|
|
28
|
-
switch (
|
|
29
|
-
case "
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
case "
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
function extractKeyInfo(toolName, result) {
|
|
24
|
+
const lines = result.split("\n").filter((l) => l.trim());
|
|
25
|
+
switch (toolName) {
|
|
26
|
+
case "Read": {
|
|
27
|
+
const pathMatch = result.match(/Reading file: (.+)/);
|
|
28
|
+
const lineCount = lines.length;
|
|
29
|
+
return pathMatch ? `[Read ${pathMatch[1]}, ${lineCount} lines]` : `[Read file, ${lineCount} lines]`;
|
|
30
|
+
}
|
|
31
|
+
case "Grep": {
|
|
32
|
+
const matchCount = lines.length;
|
|
33
|
+
return `[Grep found ${matchCount} matches]`;
|
|
34
|
+
}
|
|
35
|
+
case "Glob": {
|
|
36
|
+
const fileCount = lines.length;
|
|
37
|
+
return `[Glob found ${fileCount} files]`;
|
|
38
|
+
}
|
|
39
|
+
case "Bash": {
|
|
40
|
+
const exitMatch = result.match(/exit code (\d+)/);
|
|
41
|
+
const exitCode = exitMatch ? exitMatch[1] : "0";
|
|
42
|
+
return `[Bash completed, exit code ${exitCode}]`;
|
|
43
|
+
}
|
|
44
|
+
case "WebFetch": {
|
|
45
|
+
const urlMatch = result.match(/https?:\/\/\S+/);
|
|
46
|
+
return urlMatch ? `[WebFetch from ${urlMatch[0].substring(0, 50)}...]` : "[WebFetch completed]";
|
|
47
|
+
}
|
|
38
48
|
default:
|
|
39
|
-
return
|
|
49
|
+
return `[${toolName} completed]`;
|
|
40
50
|
}
|
|
41
51
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
52
|
+
class MiroThinkerCompressor {
|
|
53
|
+
config;
|
|
54
|
+
constructor(config = {}) {
|
|
55
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 压缩对话历史
|
|
59
|
+
*
|
|
60
|
+
* @param messages - 原始对话消息
|
|
61
|
+
* @returns 压缩后的对话
|
|
62
|
+
*/
|
|
63
|
+
compress(messages) {
|
|
64
|
+
let originalTokens = 0;
|
|
65
|
+
let compressedTokens = 0;
|
|
66
|
+
let omittedToolResults = 0;
|
|
67
|
+
const compressedMessages = [];
|
|
68
|
+
for (let i = 0; i < messages.length; i++) {
|
|
69
|
+
const msg = messages[i];
|
|
70
|
+
const msgTokens = estimateTokens$1(msg.content);
|
|
71
|
+
originalTokens += msgTokens;
|
|
72
|
+
if (msg.role === "tool_result") {
|
|
73
|
+
const shouldCompress = this.shouldCompressToolResult(msg, msgTokens);
|
|
74
|
+
if (shouldCompress) {
|
|
75
|
+
const compressedContent = this.compressToolResult(msg);
|
|
76
|
+
const compressedMsg = {
|
|
77
|
+
...msg,
|
|
78
|
+
content: compressedContent,
|
|
79
|
+
originalTokens: msgTokens,
|
|
80
|
+
compressed: true
|
|
81
|
+
};
|
|
82
|
+
compressedMessages.push(compressedMsg);
|
|
83
|
+
compressedTokens += estimateTokens$1(compressedContent);
|
|
84
|
+
omittedToolResults++;
|
|
85
|
+
} else {
|
|
86
|
+
compressedMessages.push({ ...msg, originalTokens: msgTokens });
|
|
87
|
+
compressedTokens += msgTokens;
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
compressedMessages.push({ ...msg, originalTokens: msgTokens });
|
|
91
|
+
compressedTokens += msgTokens;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
messages: compressedMessages,
|
|
96
|
+
originalTokens,
|
|
97
|
+
compressedTokens,
|
|
98
|
+
compressionRatio: originalTokens > 0 ? compressedTokens / originalTokens : 1,
|
|
99
|
+
omittedToolResults
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 判断是否应该压缩工具结果
|
|
104
|
+
*/
|
|
105
|
+
shouldCompressToolResult(msg, tokens) {
|
|
106
|
+
if (tokens < this.config.toolResultThreshold) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
if (this.config.preserveErrors && this.isErrorResult(msg.content)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 检查是否是错误结果
|
|
116
|
+
*/
|
|
117
|
+
isErrorResult(content) {
|
|
118
|
+
const errorPatterns = [
|
|
119
|
+
/error/i,
|
|
120
|
+
/failed/i,
|
|
121
|
+
/exception/i,
|
|
122
|
+
/not found/i,
|
|
123
|
+
/permission denied/i
|
|
124
|
+
];
|
|
125
|
+
return errorPatterns.some((p) => p.test(content));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 压缩工具结果
|
|
129
|
+
*/
|
|
130
|
+
compressToolResult(msg) {
|
|
131
|
+
const parts = [];
|
|
132
|
+
parts.push(this.config.placeholderTemplate);
|
|
133
|
+
if (this.config.preserveKeyInfo && msg.toolName) {
|
|
134
|
+
const keyInfo = extractKeyInfo(msg.toolName, msg.content);
|
|
135
|
+
parts.push(keyInfo);
|
|
136
|
+
}
|
|
137
|
+
return parts.join("\n");
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* 从 FC 摘要生成压缩消息
|
|
141
|
+
*/
|
|
142
|
+
compressFromFCSummaries(summaries) {
|
|
143
|
+
return summaries.map((summary) => ({
|
|
144
|
+
role: "tool_result",
|
|
145
|
+
content: `<Tool result omitted> ${summary.summary}`,
|
|
146
|
+
toolCallId: summary.fcId,
|
|
147
|
+
toolName: summary.fcName,
|
|
148
|
+
originalTokens: summary.tokens,
|
|
149
|
+
compressed: true
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 生成摘要提示词
|
|
154
|
+
*
|
|
155
|
+
* 用于指导 AI 理解压缩后的上下文
|
|
156
|
+
*/
|
|
157
|
+
generateSummaryPrompt() {
|
|
158
|
+
return `
|
|
159
|
+
\u3010\u4E0A\u4E0B\u6587\u538B\u7F29\u8BF4\u660E - MiroThinker \u7B56\u7565\u3011
|
|
160
|
+
|
|
161
|
+
\u672C\u5BF9\u8BDD\u5386\u53F2\u5DF2\u5E94\u7528"\u53BB\u8089\u7559\u9AA8"\u538B\u7F29\u7B56\u7565\uFF1A
|
|
162
|
+
- "\u8089"\uFF08\u539F\u59CB\u6570\u636E\uFF09\uFF1A\u5DE5\u5177\u8FD4\u56DE\u7684\u539F\u59CB\u5185\u5BB9\u5DF2\u7701\u7565\uFF0C\u7528\u5360\u4F4D\u7B26\u66FF\u4EE3
|
|
163
|
+
- "\u9AA8"\uFF08\u4FE1\u606F\u5207\u7247\uFF09\uFF1AAI \u7684\u601D\u8003\u548C\u7ED3\u8BBA\u5B8C\u6574\u4FDD\u7559
|
|
164
|
+
|
|
165
|
+
\u539F\u7406\uFF1AAI \u7684\u56DE\u590D\u5DF2\u7ECF\u662F\u5BF9\u539F\u59CB\u6570\u636E\u7684\u9AD8\u4FDD\u771F\u63D0\u70BC\u3002
|
|
166
|
+
\u5F53\u4F60\u770B\u5230 "<Tool result omitted>" \u65F6\uFF0C\u8BF7\u53C2\u8003\u7D27\u968F\u5176\u540E\u7684 Assistant \u56DE\u590D\uFF0C
|
|
167
|
+
\u90A3\u91CC\u5305\u542B\u4E86\u5DE5\u5177\u7ED3\u679C\u7684\u5173\u952E\u53D1\u73B0\u548C\u7ED3\u8BBA\u3002
|
|
168
|
+
|
|
169
|
+
\u3010\u6458\u8981\u8981\u6C42\u3011
|
|
170
|
+
1. \u4FDD\u7559\u6240\u6709\u5173\u952E\u51B3\u7B56\u548C\u91CD\u8981\u7ED3\u8BBA\uFF08\u8FD9\u662F"\u9AA8"\uFF09
|
|
171
|
+
2. \u4FDD\u7559\u9879\u76EE\u80CC\u666F\u548C\u5F53\u524D\u4EFB\u52A1
|
|
172
|
+
3. \u4FDD\u7559 AI \u7684\u601D\u8003\u94FE\u548C\u63A8\u7406\u8FC7\u7A0B
|
|
173
|
+
4. \u4E0D\u9700\u8981\u6062\u590D\u88AB\u7701\u7565\u7684\u539F\u59CB\u6570\u636E
|
|
174
|
+
`.trim();
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* 更新配置
|
|
178
|
+
*/
|
|
179
|
+
updateConfig(config) {
|
|
180
|
+
this.config = { ...this.config, ...config };
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 获取当前配置
|
|
184
|
+
*/
|
|
185
|
+
getConfig() {
|
|
186
|
+
return { ...this.config };
|
|
187
|
+
}
|
|
84
188
|
}
|
|
85
|
-
|
|
189
|
+
|
|
190
|
+
class AutoSummarizeManager extends EventEmitter {
|
|
191
|
+
contextManager;
|
|
192
|
+
compressor;
|
|
193
|
+
options;
|
|
194
|
+
lastSummarizeTime = 0;
|
|
195
|
+
pendingSummarize = false;
|
|
196
|
+
summarizeCount = 0;
|
|
197
|
+
constructor(contextManager, options = {}) {
|
|
198
|
+
super();
|
|
199
|
+
this.contextManager = contextManager;
|
|
200
|
+
this.compressor = new MiroThinkerCompressor({
|
|
201
|
+
toolResultThreshold: 500,
|
|
202
|
+
preserveErrors: true,
|
|
203
|
+
preserveKeyInfo: true
|
|
204
|
+
});
|
|
205
|
+
this.options = {
|
|
206
|
+
enabled: options.enabled ?? true,
|
|
207
|
+
minInterval: options.minInterval ?? 6e5,
|
|
208
|
+
// 10 minutes default
|
|
209
|
+
tokenThreshold: options.tokenThreshold ?? 1e5,
|
|
210
|
+
// 100K tokens
|
|
211
|
+
strategy: options.strategy ?? "miro-thinker",
|
|
212
|
+
compressionTarget: options.compressionTarget ?? 0.7,
|
|
213
|
+
// 70% compression target
|
|
214
|
+
debug: options.debug ?? false
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Check if auto-summarization should trigger
|
|
219
|
+
*/
|
|
220
|
+
shouldSummarize(currentTokens) {
|
|
221
|
+
if (!this.options.enabled) {
|
|
222
|
+
this.log("Auto-summarize disabled");
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (this.pendingSummarize) {
|
|
226
|
+
this.log("Summarization already in progress");
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
const timeSinceLastSummarize = now - this.lastSummarizeTime;
|
|
231
|
+
if (timeSinceLastSummarize < this.options.minInterval) {
|
|
232
|
+
const remainingTime = Math.round((this.options.minInterval - timeSinceLastSummarize) / 1e3);
|
|
233
|
+
this.log(`Rate limited: ${remainingTime}s remaining (${timeSinceLastSummarize}ms / ${this.options.minInterval}ms)`);
|
|
234
|
+
this.emit("summarize:rate_limited", {
|
|
235
|
+
timeSinceLastSummarize,
|
|
236
|
+
remainingTime,
|
|
237
|
+
minInterval: this.options.minInterval
|
|
238
|
+
});
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
if (currentTokens < this.options.tokenThreshold) {
|
|
242
|
+
this.log(`Threshold not met: ${currentTokens} / ${this.options.tokenThreshold} tokens`);
|
|
243
|
+
this.emit("summarize:threshold_not_met", {
|
|
244
|
+
currentTokens,
|
|
245
|
+
threshold: this.options.tokenThreshold
|
|
246
|
+
});
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
this.log(`Should summarize: ${currentTokens} tokens, ${Math.round(timeSinceLastSummarize / 1e3)}s since last`);
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Perform auto-summarization
|
|
254
|
+
*/
|
|
255
|
+
async summarize() {
|
|
256
|
+
if (!this.options.enabled) {
|
|
257
|
+
return {
|
|
258
|
+
performed: false,
|
|
259
|
+
reason: "disabled",
|
|
260
|
+
summary: this.createEmptySummary()
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (this.pendingSummarize) {
|
|
264
|
+
this.log("Summarization already in progress, skipping");
|
|
265
|
+
return {
|
|
266
|
+
performed: false,
|
|
267
|
+
reason: "rate_limited",
|
|
268
|
+
summary: this.createEmptySummary()
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
this.pendingSummarize = true;
|
|
272
|
+
this.emit("summarize:started");
|
|
273
|
+
try {
|
|
274
|
+
this.log("\u{1F916} Auto-summarize triggered...");
|
|
275
|
+
const messages = await this.contextManager.getMessages();
|
|
276
|
+
const conversationMessages = messages.map((msg) => ({
|
|
277
|
+
role: msg.role === "system" ? "assistant" : msg.role,
|
|
278
|
+
content: msg.content,
|
|
279
|
+
originalTokens: msg.metadata?.tokens,
|
|
280
|
+
compressed: false
|
|
281
|
+
}));
|
|
282
|
+
const compressed = this.compressor.compress(conversationMessages);
|
|
283
|
+
const summaryContent = compressed.messages.filter((m) => m.role === "assistant").map((m) => m.content).join("\n\n---\n\n");
|
|
284
|
+
const summary = {
|
|
285
|
+
content: summaryContent,
|
|
286
|
+
originalTokens: compressed.originalTokens,
|
|
287
|
+
compressedTokens: compressed.compressedTokens,
|
|
288
|
+
compressionRatio: compressed.compressionRatio,
|
|
289
|
+
fcCount: 0,
|
|
290
|
+
// Not tracking function calls in auto-summarize
|
|
291
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
292
|
+
};
|
|
293
|
+
await this.contextManager.storeSummary(summary);
|
|
294
|
+
this.lastSummarizeTime = Date.now();
|
|
295
|
+
this.summarizeCount++;
|
|
296
|
+
this.log(`\u2705 Auto-summarize complete: ${summary.originalTokens} \u2192 ${summary.compressedTokens} tokens (${Math.round(summary.compressionRatio * 100)}% compression)`);
|
|
297
|
+
this.emit("summarize:completed", summary);
|
|
298
|
+
return {
|
|
299
|
+
performed: true,
|
|
300
|
+
summary
|
|
301
|
+
};
|
|
302
|
+
} catch (error) {
|
|
303
|
+
this.log(`\u274C Auto-summarize error: ${error}`);
|
|
304
|
+
this.emit("summarize:error", error);
|
|
305
|
+
return {
|
|
306
|
+
performed: false,
|
|
307
|
+
reason: "error",
|
|
308
|
+
summary: this.createEmptySummary()
|
|
309
|
+
};
|
|
310
|
+
} finally {
|
|
311
|
+
this.pendingSummarize = false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Check if summarization is allowed (not rate limited)
|
|
316
|
+
*/
|
|
317
|
+
canSummarize() {
|
|
318
|
+
if (!this.options.enabled)
|
|
319
|
+
return false;
|
|
320
|
+
if (this.pendingSummarize)
|
|
321
|
+
return false;
|
|
322
|
+
const now = Date.now();
|
|
323
|
+
const timeSinceLastSummarize = now - this.lastSummarizeTime;
|
|
324
|
+
return timeSinceLastSummarize >= this.options.minInterval;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Get time until next summarization allowed (ms)
|
|
328
|
+
*/
|
|
329
|
+
getTimeUntilNextSummarize() {
|
|
330
|
+
const now = Date.now();
|
|
331
|
+
const timeSinceLastSummarize = now - this.lastSummarizeTime;
|
|
332
|
+
const remaining = this.options.minInterval - timeSinceLastSummarize;
|
|
333
|
+
return Math.max(0, remaining);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Get statistics
|
|
337
|
+
*/
|
|
338
|
+
getStats() {
|
|
339
|
+
const now = Date.now();
|
|
340
|
+
const timeSinceLastSummarize = now - this.lastSummarizeTime;
|
|
341
|
+
return {
|
|
342
|
+
enabled: this.options.enabled,
|
|
343
|
+
lastSummarizeTime: this.lastSummarizeTime,
|
|
344
|
+
timeSinceLastSummarize,
|
|
345
|
+
canSummarize: this.canSummarize(),
|
|
346
|
+
timeUntilNextSummarize: this.getTimeUntilNextSummarize(),
|
|
347
|
+
minInterval: this.options.minInterval,
|
|
348
|
+
tokenThreshold: this.options.tokenThreshold,
|
|
349
|
+
summarizeCount: this.summarizeCount,
|
|
350
|
+
isPending: this.pendingSummarize
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Enable auto-summarization
|
|
355
|
+
*/
|
|
356
|
+
enable() {
|
|
357
|
+
this.options.enabled = true;
|
|
358
|
+
this.log("Auto-summarize enabled");
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Disable auto-summarization
|
|
362
|
+
*/
|
|
363
|
+
disable() {
|
|
364
|
+
this.options.enabled = false;
|
|
365
|
+
this.log("Auto-summarize disabled");
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Update configuration
|
|
369
|
+
*/
|
|
370
|
+
updateConfig(options) {
|
|
371
|
+
if (options.enabled !== void 0)
|
|
372
|
+
this.options.enabled = options.enabled;
|
|
373
|
+
if (options.minInterval !== void 0)
|
|
374
|
+
this.options.minInterval = options.minInterval;
|
|
375
|
+
if (options.tokenThreshold !== void 0)
|
|
376
|
+
this.options.tokenThreshold = options.tokenThreshold;
|
|
377
|
+
if (options.strategy !== void 0)
|
|
378
|
+
this.options.strategy = options.strategy;
|
|
379
|
+
if (options.compressionTarget !== void 0)
|
|
380
|
+
this.options.compressionTarget = options.compressionTarget;
|
|
381
|
+
if (options.debug !== void 0)
|
|
382
|
+
this.options.debug = options.debug;
|
|
383
|
+
this.log("Configuration updated");
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Force reset rate limiting (for testing or manual override)
|
|
387
|
+
*/
|
|
388
|
+
resetRateLimit() {
|
|
389
|
+
this.lastSummarizeTime = 0;
|
|
390
|
+
this.log("Rate limit reset");
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Reset all statistics
|
|
394
|
+
*/
|
|
395
|
+
resetStats() {
|
|
396
|
+
this.lastSummarizeTime = 0;
|
|
397
|
+
this.summarizeCount = 0;
|
|
398
|
+
this.pendingSummarize = false;
|
|
399
|
+
this.log("Statistics reset");
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Create empty summary for error cases
|
|
403
|
+
*/
|
|
404
|
+
createEmptySummary() {
|
|
405
|
+
return {
|
|
406
|
+
content: "",
|
|
407
|
+
originalTokens: 0,
|
|
408
|
+
compressedTokens: 0,
|
|
409
|
+
compressionRatio: 0,
|
|
410
|
+
fcCount: 0,
|
|
411
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Debug logging
|
|
416
|
+
*/
|
|
417
|
+
log(message) {
|
|
418
|
+
if (this.options.debug) {
|
|
419
|
+
console.log(`[AutoSummarizeManager] ${message}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const DEFAULT_CONTEXT_CONFIG = {
|
|
425
|
+
enabled: true,
|
|
426
|
+
autoSummarize: true,
|
|
427
|
+
contextThreshold: 1e5,
|
|
428
|
+
// 100K tokens
|
|
429
|
+
maxContextTokens: 15e4,
|
|
430
|
+
// 150K tokens
|
|
431
|
+
summaryModel: "haiku",
|
|
432
|
+
cloudSync: {
|
|
433
|
+
enabled: false,
|
|
434
|
+
apiKey: void 0,
|
|
435
|
+
endpoint: void 0
|
|
436
|
+
},
|
|
437
|
+
cleanup: {
|
|
438
|
+
maxSessionAge: 30,
|
|
439
|
+
// 30 days
|
|
440
|
+
maxStorageSize: 500,
|
|
441
|
+
// 500 MB
|
|
442
|
+
autoCleanup: true
|
|
443
|
+
},
|
|
444
|
+
storage: {
|
|
445
|
+
baseDir: join(homedir(), ".ccjk", "context"),
|
|
446
|
+
sessionsDir: "sessions",
|
|
447
|
+
syncQueueDir: "sync-queue"
|
|
448
|
+
},
|
|
449
|
+
// Phase 1: New configuration options
|
|
450
|
+
threadBased: {
|
|
451
|
+
enabled: false,
|
|
452
|
+
// Opt-in feature
|
|
453
|
+
maxThreadTokens: 5e3,
|
|
454
|
+
autoCloseOnThreshold: true,
|
|
455
|
+
preserveThreadChain: true
|
|
456
|
+
},
|
|
457
|
+
planAcceptance: {
|
|
458
|
+
enabled: false,
|
|
459
|
+
// Opt-in feature
|
|
460
|
+
autoCompress: true,
|
|
461
|
+
clearContext: true,
|
|
462
|
+
injectSummary: true,
|
|
463
|
+
minInterval: 6e5
|
|
464
|
+
// 10 minutes
|
|
465
|
+
},
|
|
466
|
+
autoSummarizeEnhanced: {
|
|
467
|
+
enabled: false,
|
|
468
|
+
// Opt-in feature
|
|
469
|
+
minInterval: 6e5,
|
|
470
|
+
// 10 minutes
|
|
471
|
+
tokenThreshold: 1e5,
|
|
472
|
+
// 100K tokens
|
|
473
|
+
strategy: "miro-thinker",
|
|
474
|
+
compressionTarget: 0.7
|
|
475
|
+
// 70% compression target
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
class ConfigManager {
|
|
479
|
+
config;
|
|
480
|
+
configPath;
|
|
481
|
+
loaded = false;
|
|
482
|
+
constructor(configPath) {
|
|
483
|
+
this.configPath = configPath || join(homedir(), ".ccjk", "context", "config.json");
|
|
484
|
+
this.config = { ...DEFAULT_CONTEXT_CONFIG };
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Load configuration from disk
|
|
488
|
+
* Creates default config if not exists
|
|
489
|
+
*/
|
|
490
|
+
async load() {
|
|
491
|
+
try {
|
|
492
|
+
if (existsSync(this.configPath)) {
|
|
493
|
+
const content = await readFile(this.configPath, "utf-8");
|
|
494
|
+
const loadedConfig = JSON.parse(content);
|
|
495
|
+
this.config = this.mergeWithDefaults(loadedConfig);
|
|
496
|
+
} else {
|
|
497
|
+
await this.save();
|
|
498
|
+
}
|
|
499
|
+
this.loaded = true;
|
|
500
|
+
return this.config;
|
|
501
|
+
} catch (error) {
|
|
502
|
+
throw new Error(`Failed to load context config: ${error instanceof Error ? error.message : String(error)}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Save configuration to disk
|
|
507
|
+
*/
|
|
508
|
+
async save() {
|
|
509
|
+
try {
|
|
510
|
+
const dir = dirname(this.configPath);
|
|
511
|
+
if (!existsSync(dir)) {
|
|
512
|
+
await mkdir(dir, { recursive: true });
|
|
513
|
+
}
|
|
514
|
+
const content = JSON.stringify(this.config, null, 2);
|
|
515
|
+
await writeFile(this.configPath, content, "utf-8");
|
|
516
|
+
} catch (error) {
|
|
517
|
+
throw new Error(`Failed to save context config: ${error instanceof Error ? error.message : String(error)}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Get current configuration
|
|
522
|
+
* Loads from disk if not already loaded
|
|
523
|
+
*/
|
|
524
|
+
async get() {
|
|
525
|
+
if (!this.loaded) {
|
|
526
|
+
await this.load();
|
|
527
|
+
}
|
|
528
|
+
return { ...this.config };
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Update configuration
|
|
532
|
+
* Merges partial updates with existing config
|
|
533
|
+
*/
|
|
534
|
+
async update(updates) {
|
|
535
|
+
if (!this.loaded) {
|
|
536
|
+
await this.load();
|
|
537
|
+
}
|
|
538
|
+
this.config = this.deepMerge(this.config, updates);
|
|
539
|
+
this.validate(this.config);
|
|
540
|
+
await this.save();
|
|
541
|
+
return { ...this.config };
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Reset configuration to defaults
|
|
545
|
+
*/
|
|
546
|
+
async reset() {
|
|
547
|
+
this.config = { ...DEFAULT_CONTEXT_CONFIG };
|
|
548
|
+
await this.save();
|
|
549
|
+
return { ...this.config };
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Get specific configuration value
|
|
553
|
+
*/
|
|
554
|
+
async getValue(key) {
|
|
555
|
+
if (!this.loaded) {
|
|
556
|
+
await this.load();
|
|
557
|
+
}
|
|
558
|
+
return this.config[key];
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Set specific configuration value
|
|
562
|
+
*/
|
|
563
|
+
async setValue(key, value) {
|
|
564
|
+
if (!this.loaded) {
|
|
565
|
+
await this.load();
|
|
566
|
+
}
|
|
567
|
+
this.config[key] = value;
|
|
568
|
+
this.validate(this.config);
|
|
569
|
+
await this.save();
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Check if context system is enabled
|
|
573
|
+
*/
|
|
574
|
+
async isEnabled() {
|
|
575
|
+
return this.getValue("enabled");
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Enable or disable context system
|
|
579
|
+
*/
|
|
580
|
+
async setEnabled(enabled) {
|
|
581
|
+
await this.setValue("enabled", enabled);
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Get storage paths
|
|
585
|
+
*/
|
|
586
|
+
async getStoragePaths() {
|
|
587
|
+
const config = await this.get();
|
|
588
|
+
const { baseDir, sessionsDir, syncQueueDir } = config.storage;
|
|
589
|
+
return {
|
|
590
|
+
baseDir,
|
|
591
|
+
sessionsDir,
|
|
592
|
+
syncQueueDir,
|
|
593
|
+
absoluteSessionsDir: join(baseDir, sessionsDir),
|
|
594
|
+
absoluteSyncQueueDir: join(baseDir, syncQueueDir)
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Validate configuration
|
|
599
|
+
* Throws error if invalid
|
|
600
|
+
*/
|
|
601
|
+
validate(config) {
|
|
602
|
+
if (config.contextThreshold <= 0) {
|
|
603
|
+
throw new Error("contextThreshold must be positive");
|
|
604
|
+
}
|
|
605
|
+
if (config.maxContextTokens <= 0) {
|
|
606
|
+
throw new Error("maxContextTokens must be positive");
|
|
607
|
+
}
|
|
608
|
+
if (config.contextThreshold >= config.maxContextTokens) {
|
|
609
|
+
throw new Error("contextThreshold must be less than maxContextTokens");
|
|
610
|
+
}
|
|
611
|
+
if (config.cleanup.maxSessionAge <= 0) {
|
|
612
|
+
throw new Error("cleanup.maxSessionAge must be positive");
|
|
613
|
+
}
|
|
614
|
+
if (config.cleanup.maxStorageSize <= 0) {
|
|
615
|
+
throw new Error("cleanup.maxStorageSize must be positive");
|
|
616
|
+
}
|
|
617
|
+
if (config.cloudSync.enabled) {
|
|
618
|
+
if (!config.cloudSync.apiKey || config.cloudSync.apiKey.trim() === "") {
|
|
619
|
+
throw new Error("cloudSync.apiKey is required when cloudSync is enabled");
|
|
620
|
+
}
|
|
621
|
+
if (!config.cloudSync.endpoint || config.cloudSync.endpoint.trim() === "") {
|
|
622
|
+
throw new Error("cloudSync.endpoint is required when cloudSync is enabled");
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (!config.storage.baseDir) {
|
|
626
|
+
throw new Error("storage.baseDir is required");
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Merge partial config with defaults
|
|
631
|
+
*/
|
|
632
|
+
mergeWithDefaults(partial) {
|
|
633
|
+
return this.deepMerge(DEFAULT_CONTEXT_CONFIG, partial);
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Deep merge two objects
|
|
637
|
+
*/
|
|
638
|
+
deepMerge(target, source) {
|
|
639
|
+
const result = { ...target };
|
|
640
|
+
for (const key in source) {
|
|
641
|
+
const sourceValue = source[key];
|
|
642
|
+
const targetValue = result[key];
|
|
643
|
+
if (sourceValue === void 0) {
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
if (typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue) && typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue)) {
|
|
647
|
+
result[key] = this.deepMerge(targetValue, sourceValue);
|
|
648
|
+
} else {
|
|
649
|
+
result[key] = sourceValue;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return result;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
class PlanAcceptanceManager extends EventEmitter {
|
|
657
|
+
contextManager;
|
|
658
|
+
threadManager;
|
|
659
|
+
compressor;
|
|
660
|
+
options;
|
|
661
|
+
lastSummarizeTime = 0;
|
|
662
|
+
constructor(contextManager, options = {}) {
|
|
663
|
+
super();
|
|
664
|
+
this.contextManager = contextManager;
|
|
665
|
+
this.compressor = new MiroThinkerCompressor({
|
|
666
|
+
toolResultThreshold: 500,
|
|
667
|
+
preserveErrors: true,
|
|
668
|
+
preserveKeyInfo: true
|
|
669
|
+
});
|
|
670
|
+
this.options = {
|
|
671
|
+
minSummarizeInterval: options.minSummarizeInterval ?? 6e5,
|
|
672
|
+
// 10 minutes
|
|
673
|
+
autoCompress: options.autoCompress ?? true,
|
|
674
|
+
clearContext: options.clearContext ?? true,
|
|
675
|
+
injectSummary: options.injectSummary ?? true,
|
|
676
|
+
debug: options.debug ?? false
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Set thread manager for thread-aware compression
|
|
681
|
+
*/
|
|
682
|
+
setThreadManager(threadManager) {
|
|
683
|
+
this.threadManager = threadManager;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Handle plan acceptance event
|
|
687
|
+
*/
|
|
688
|
+
async onPlanAccepted(plan) {
|
|
689
|
+
this.log("\u{1F4CB} Plan accepted, preparing context refresh...");
|
|
690
|
+
this.emit("plan:accepted", plan);
|
|
691
|
+
const now = Date.now();
|
|
692
|
+
const timeSinceLastSummarize = now - this.lastSummarizeTime;
|
|
693
|
+
if (timeSinceLastSummarize < this.options.minSummarizeInterval) {
|
|
694
|
+
const remainingTime = Math.round((this.options.minSummarizeInterval - timeSinceLastSummarize) / 1e3);
|
|
695
|
+
this.log(`\u23F3 Rate limited: ${remainingTime}s remaining until next summarization allowed`);
|
|
696
|
+
this.emit("plan:rate_limited", {
|
|
697
|
+
timeSinceLastSummarize,
|
|
698
|
+
remainingTime
|
|
699
|
+
});
|
|
700
|
+
if (this.options.clearContext) {
|
|
701
|
+
await this.clearContextWithoutSummarize(plan);
|
|
702
|
+
}
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (this.options.autoCompress) {
|
|
706
|
+
this.log("\u{1F5DC}\uFE0F Compressing context (MiroThinker strategy)...");
|
|
707
|
+
this.emit("plan:compression_started");
|
|
708
|
+
const summary = await this.compressCurrentContext();
|
|
709
|
+
this.log(`\u2705 Compression complete: ${summary.originalTokens} \u2192 ${summary.compressedTokens} tokens (${Math.round(summary.compressionRatio * 100)}% compression)`);
|
|
710
|
+
this.emit("plan:compression_completed", summary);
|
|
711
|
+
this.log("\u{1F4BE} Storing summary...");
|
|
712
|
+
await this.storeSummary(summary);
|
|
713
|
+
if (this.options.clearContext) {
|
|
714
|
+
this.log("\u{1F9F9} Clearing context for fresh start...");
|
|
715
|
+
await this.clearContext();
|
|
716
|
+
this.emit("plan:context_cleared");
|
|
717
|
+
}
|
|
718
|
+
if (this.options.injectSummary) {
|
|
719
|
+
this.log("\u{1F4DD} Injecting plan and summary into fresh context...");
|
|
720
|
+
await this.injectPlanContext(plan, summary);
|
|
721
|
+
this.emit("plan:summary_injected", { plan, summary });
|
|
722
|
+
}
|
|
723
|
+
this.lastSummarizeTime = now;
|
|
724
|
+
this.log("\u2705 Context refreshed successfully!");
|
|
725
|
+
} else {
|
|
726
|
+
if (this.options.clearContext) {
|
|
727
|
+
await this.clearContext();
|
|
728
|
+
}
|
|
729
|
+
await this.injectPlanContext(plan);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Compress current context using MiroThinker
|
|
734
|
+
*/
|
|
735
|
+
async compressCurrentContext() {
|
|
736
|
+
const messages = await this.contextManager.getMessages();
|
|
737
|
+
const conversationMessages = messages.map((msg) => ({
|
|
738
|
+
role: msg.role === "system" ? "assistant" : msg.role,
|
|
739
|
+
content: msg.content,
|
|
740
|
+
originalTokens: msg.metadata?.tokens,
|
|
741
|
+
compressed: false
|
|
742
|
+
}));
|
|
743
|
+
const compressed = this.compressor.compress(conversationMessages);
|
|
744
|
+
const summaryContent = compressed.messages.filter((m) => m.role === "assistant").map((m) => m.content).join("\n\n---\n\n");
|
|
745
|
+
return {
|
|
746
|
+
content: summaryContent,
|
|
747
|
+
originalTokens: compressed.originalTokens,
|
|
748
|
+
compressedTokens: compressed.compressedTokens,
|
|
749
|
+
compressionRatio: compressed.compressionRatio,
|
|
750
|
+
timestamp: Date.now(),
|
|
751
|
+
strategy: "miro-thinker"
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Store summary in context manager storage
|
|
756
|
+
*/
|
|
757
|
+
async storeSummary(summary) {
|
|
758
|
+
await this.contextManager.storeSummary({
|
|
759
|
+
content: summary.content,
|
|
760
|
+
originalTokens: summary.originalTokens,
|
|
761
|
+
compressedTokens: summary.compressedTokens,
|
|
762
|
+
compressionRatio: summary.compressionRatio,
|
|
763
|
+
fcCount: 0,
|
|
764
|
+
// Not applicable for plan acceptance
|
|
765
|
+
timestamp: new Date(summary.timestamp)
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Clear context (integrates with Claude Code's native clearing)
|
|
770
|
+
*/
|
|
771
|
+
async clearContext() {
|
|
772
|
+
await this.contextManager.reset();
|
|
773
|
+
if (this.threadManager) {
|
|
774
|
+
const currentThread = this.threadManager.getCurrentThread();
|
|
775
|
+
if (currentThread && currentThread.status === "active") {
|
|
776
|
+
await this.threadManager.closeThread();
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Clear context without summarization (when rate limited)
|
|
782
|
+
*/
|
|
783
|
+
async clearContextWithoutSummarize(plan) {
|
|
784
|
+
this.log("\u{1F9F9} Clearing context without summarization (rate limited)...");
|
|
785
|
+
await this.clearContext();
|
|
786
|
+
await this.injectPlanContext(plan);
|
|
787
|
+
this.log("\u2705 Context cleared and plan injected (no compression)");
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Inject plan + summary into fresh context
|
|
791
|
+
*/
|
|
792
|
+
async injectPlanContext(plan, summary) {
|
|
793
|
+
let contextPrompt = `# \u{1F4CB} Accepted Plan
|
|
794
|
+
|
|
795
|
+
${plan.content}
|
|
796
|
+
|
|
797
|
+
---
|
|
798
|
+
|
|
86
799
|
`;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
set real_claude (command -v claude 2>/dev/null)
|
|
95
|
-
|
|
96
|
-
# Handle /plugin command - use CCJK's plugin marketplace
|
|
97
|
-
if test "$argv[1]" = "/plugin"
|
|
98
|
-
set -e argv[1] # Remove /plugin
|
|
99
|
-
${ccjkPath} plugin $argv
|
|
100
|
-
return $status
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Other native slash commands (/doctor, /config, etc.) - pass through directly
|
|
104
|
-
if string match -q '/*' -- $argv[1]
|
|
105
|
-
$real_claude $argv
|
|
106
|
-
return $status
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Check for recursion - if already in wrapper, call real claude directly
|
|
110
|
-
if set -q CCJK_WRAPPER_ACTIVE
|
|
111
|
-
$real_claude $argv
|
|
112
|
-
return $status
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
set -x CCJK_WRAPPER_ACTIVE 1
|
|
116
|
-
${ccjkPath} claude $argv
|
|
117
|
-
set exit_code $status
|
|
118
|
-
set -e CCJK_WRAPPER_ACTIVE
|
|
119
|
-
return $exit_code
|
|
120
|
-
end
|
|
121
|
-
# END CCJK Context Compression Hook
|
|
800
|
+
if (summary) {
|
|
801
|
+
contextPrompt += `# \u{1F9E0} Previous Context Summary (Compressed)
|
|
802
|
+
|
|
803
|
+
${summary.content}
|
|
804
|
+
|
|
805
|
+
---
|
|
806
|
+
|
|
122
807
|
`;
|
|
123
|
-
|
|
124
|
-
|
|
808
|
+
contextPrompt += `**Compression Stats**: ${summary.originalTokens} tokens \u2192 ${summary.compressedTokens} tokens (${Math.round(summary.compressionRatio * 100)}% compression)
|
|
809
|
+
|
|
810
|
+
`;
|
|
811
|
+
contextPrompt += `**Strategy**: MiroThinker "\u53BB\u8089\u7559\u9AA8" (Keep AI thoughts, remove raw data)
|
|
812
|
+
|
|
813
|
+
---
|
|
814
|
+
|
|
815
|
+
`;
|
|
816
|
+
}
|
|
817
|
+
contextPrompt += `**Note**: You now have a fresh context window. The above summary contains key insights from the previous conversation. Let's execute this plan with full attention and focus.
|
|
818
|
+
`;
|
|
819
|
+
await this.contextManager.addMessage({
|
|
820
|
+
role: "system",
|
|
821
|
+
content: contextPrompt,
|
|
822
|
+
timestamp: Date.now()
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Check if summarization is allowed (not rate limited)
|
|
827
|
+
*/
|
|
828
|
+
canSummarize() {
|
|
829
|
+
const now = Date.now();
|
|
830
|
+
const timeSinceLastSummarize = now - this.lastSummarizeTime;
|
|
831
|
+
return timeSinceLastSummarize >= this.options.minSummarizeInterval;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Get time until next summarization allowed
|
|
835
|
+
*/
|
|
836
|
+
getTimeUntilNextSummarize() {
|
|
837
|
+
const now = Date.now();
|
|
838
|
+
const timeSinceLastSummarize = now - this.lastSummarizeTime;
|
|
839
|
+
const remaining = this.options.minSummarizeInterval - timeSinceLastSummarize;
|
|
840
|
+
return Math.max(0, remaining);
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Get statistics
|
|
844
|
+
*/
|
|
845
|
+
getStats() {
|
|
846
|
+
const now = Date.now();
|
|
847
|
+
const timeSinceLastSummarize = now - this.lastSummarizeTime;
|
|
848
|
+
return {
|
|
849
|
+
lastSummarizeTime: this.lastSummarizeTime,
|
|
850
|
+
timeSinceLastSummarize,
|
|
851
|
+
canSummarize: this.canSummarize(),
|
|
852
|
+
timeUntilNextSummarize: this.getTimeUntilNextSummarize(),
|
|
853
|
+
minInterval: this.options.minSummarizeInterval
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Force reset rate limiting (for testing or manual override)
|
|
858
|
+
*/
|
|
859
|
+
resetRateLimit() {
|
|
860
|
+
this.lastSummarizeTime = 0;
|
|
861
|
+
this.log("Rate limit reset");
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Debug logging
|
|
865
|
+
*/
|
|
866
|
+
log(message) {
|
|
867
|
+
if (this.options.debug) {
|
|
868
|
+
console.log(`[PlanAcceptanceManager] ${message}`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
874
|
+
maxRetries: 3,
|
|
875
|
+
initialDelay: 1e3,
|
|
876
|
+
maxDelay: 1e4,
|
|
877
|
+
backoffMultiplier: 2
|
|
878
|
+
};
|
|
879
|
+
class AnthropicApiClient {
|
|
880
|
+
client;
|
|
881
|
+
config;
|
|
882
|
+
constructor(config = {}) {
|
|
883
|
+
this.client = new Anthropic({
|
|
884
|
+
apiKey: config.apiKey || process__default.env.ANTHROPIC_API_KEY
|
|
885
|
+
});
|
|
886
|
+
this.config = {
|
|
887
|
+
apiKey: config.apiKey || process__default.env.ANTHROPIC_API_KEY || "",
|
|
888
|
+
model: config.model || "claude-3-5-haiku-20241022",
|
|
889
|
+
maxTokens: config.maxTokens || 1024,
|
|
890
|
+
temperature: config.temperature || 0.3,
|
|
891
|
+
retry: { ...DEFAULT_RETRY_CONFIG, ...config.retry }
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Send message to Claude with retry logic
|
|
896
|
+
*/
|
|
897
|
+
async sendMessage(prompt, options = {}) {
|
|
898
|
+
const model = options.model || this.config.model;
|
|
899
|
+
const maxTokens = options.maxTokens || this.config.maxTokens;
|
|
900
|
+
const temperature = options.temperature || this.config.temperature;
|
|
901
|
+
return this.withRetry(async () => {
|
|
902
|
+
const response = await this.client.messages.create({
|
|
903
|
+
model,
|
|
904
|
+
max_tokens: maxTokens,
|
|
905
|
+
temperature,
|
|
906
|
+
messages: [
|
|
907
|
+
{
|
|
908
|
+
role: "user",
|
|
909
|
+
content: prompt
|
|
910
|
+
}
|
|
911
|
+
]
|
|
912
|
+
});
|
|
913
|
+
const content = response.content[0];
|
|
914
|
+
if (content.type === "text") {
|
|
915
|
+
return content.text;
|
|
916
|
+
}
|
|
917
|
+
throw new Error("Unexpected response type from Claude API");
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Execute function with exponential backoff retry
|
|
922
|
+
*/
|
|
923
|
+
async withRetry(fn, attempt = 1) {
|
|
924
|
+
try {
|
|
925
|
+
return await fn();
|
|
926
|
+
} catch (error) {
|
|
927
|
+
if (attempt >= this.config.retry.maxRetries) {
|
|
928
|
+
throw error;
|
|
929
|
+
}
|
|
930
|
+
if (!this.isRetryableError(error)) {
|
|
931
|
+
throw error;
|
|
932
|
+
}
|
|
933
|
+
const delay = Math.min(
|
|
934
|
+
this.config.retry.initialDelay * this.config.retry.backoffMultiplier ** (attempt - 1),
|
|
935
|
+
this.config.retry.maxDelay
|
|
936
|
+
);
|
|
937
|
+
await this.sleep(delay);
|
|
938
|
+
return this.withRetry(fn, attempt + 1);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Check if error is retryable
|
|
943
|
+
*/
|
|
944
|
+
isRetryableError(error) {
|
|
945
|
+
if (error.code === "ECONNRESET" || error.code === "ETIMEDOUT") {
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
if (error.status === 429) {
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
if (error.status >= 500 && error.status < 600) {
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Sleep for specified milliseconds
|
|
958
|
+
*/
|
|
959
|
+
sleep(ms) {
|
|
960
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Update client configuration
|
|
964
|
+
*/
|
|
965
|
+
updateConfig(config) {
|
|
966
|
+
if (config.apiKey) {
|
|
967
|
+
this.client = new Anthropic({ apiKey: config.apiKey });
|
|
968
|
+
this.config.apiKey = config.apiKey;
|
|
969
|
+
}
|
|
970
|
+
if (config.model)
|
|
971
|
+
this.config.model = config.model;
|
|
972
|
+
if (config.maxTokens)
|
|
973
|
+
this.config.maxTokens = config.maxTokens;
|
|
974
|
+
if (config.temperature)
|
|
975
|
+
this.config.temperature = config.temperature;
|
|
976
|
+
if (config.retry)
|
|
977
|
+
this.config.retry = { ...this.config.retry, ...config.retry };
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Get current configuration
|
|
981
|
+
*/
|
|
982
|
+
getConfig() {
|
|
983
|
+
return { ...this.config };
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
function createApiClient(config) {
|
|
987
|
+
return new AnthropicApiClient(config);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function estimateTokens(text) {
|
|
991
|
+
const estimation = estimateTokensDetailed(text);
|
|
992
|
+
return estimation.total;
|
|
993
|
+
}
|
|
994
|
+
function estimateTokensDetailed(text) {
|
|
995
|
+
const chineseChars = (text.match(/[\u4E00-\u9FA5]/g) || []).length;
|
|
996
|
+
const otherChars = text.length - chineseChars;
|
|
997
|
+
const chineseTokens = Math.ceil(chineseChars / 1.5);
|
|
998
|
+
const otherTokens = Math.ceil(otherChars / 4);
|
|
999
|
+
const total = chineseTokens + otherTokens;
|
|
1000
|
+
return {
|
|
1001
|
+
total,
|
|
1002
|
+
chineseChars,
|
|
1003
|
+
otherChars
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
function calculateContextUsage(currentTokens, maxTokens) {
|
|
1007
|
+
return currentTokens / maxTokens * 100;
|
|
1008
|
+
}
|
|
1009
|
+
function isThresholdExceeded(currentTokens, maxTokens, threshold) {
|
|
1010
|
+
const usage = calculateContextUsage(currentTokens, maxTokens);
|
|
1011
|
+
return usage >= threshold * 100;
|
|
1012
|
+
}
|
|
1013
|
+
function getRemainingTokens(currentTokens, maxTokens) {
|
|
1014
|
+
return Math.max(0, maxTokens - currentTokens);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const SUMMARIZE_PROMPT = `You are a context compression assistant. Summarize the following function call result concisely.
|
|
1018
|
+
|
|
1019
|
+
Function: {fc_name}
|
|
1020
|
+
Arguments: {fc_args}
|
|
1021
|
+
Result: {fc_result}
|
|
1022
|
+
|
|
1023
|
+
Provide a one-line summary (max 100 chars) capturing:
|
|
1024
|
+
1. What action was performed
|
|
1025
|
+
2. Key outcome or finding
|
|
1026
|
+
3. Any important details for future reference
|
|
1027
|
+
|
|
1028
|
+
Summary:`;
|
|
1029
|
+
class Summarizer {
|
|
1030
|
+
apiClient;
|
|
1031
|
+
config;
|
|
1032
|
+
queue = [];
|
|
1033
|
+
processing = false;
|
|
1034
|
+
constructor(config = {}) {
|
|
1035
|
+
this.config = {
|
|
1036
|
+
model: config.model || "haiku",
|
|
1037
|
+
apiKey: config.apiKey || process__default.env.ANTHROPIC_API_KEY || "",
|
|
1038
|
+
batchSize: config.batchSize || 5,
|
|
1039
|
+
maxConcurrent: config.maxConcurrent || 3
|
|
1040
|
+
};
|
|
1041
|
+
const modelName = this.config.model === "haiku" ? "claude-3-5-haiku-20241022" : void 0;
|
|
1042
|
+
this.apiClient = createApiClient({
|
|
1043
|
+
apiKey: this.config.apiKey,
|
|
1044
|
+
model: modelName,
|
|
1045
|
+
maxTokens: 150,
|
|
1046
|
+
// Short summaries
|
|
1047
|
+
temperature: 0.3
|
|
1048
|
+
// Consistent output
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Summarize a single function call
|
|
1053
|
+
*/
|
|
1054
|
+
async summarize(request) {
|
|
1055
|
+
try {
|
|
1056
|
+
const prompt = this.buildPrompt(request);
|
|
1057
|
+
const summary = await this.apiClient.sendMessage(prompt);
|
|
1058
|
+
const cleanedSummary = this.cleanSummary(summary);
|
|
1059
|
+
const tokens = estimateTokens(cleanedSummary);
|
|
1060
|
+
return {
|
|
1061
|
+
fcId: request.fcId,
|
|
1062
|
+
fcName: request.fcName,
|
|
1063
|
+
summary: cleanedSummary,
|
|
1064
|
+
tokens,
|
|
1065
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1066
|
+
};
|
|
1067
|
+
} catch {
|
|
1068
|
+
return this.createFallbackSummary(request);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Add request to queue for batch processing
|
|
1073
|
+
*/
|
|
1074
|
+
async queueSummarization(request) {
|
|
1075
|
+
this.queue.push(request);
|
|
1076
|
+
if (!this.processing) {
|
|
1077
|
+
this.processBatch();
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Process batch of summarization requests
|
|
1082
|
+
*/
|
|
1083
|
+
async processBatch() {
|
|
1084
|
+
if (this.processing || this.queue.length === 0) {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
this.processing = true;
|
|
1088
|
+
try {
|
|
1089
|
+
while (this.queue.length > 0) {
|
|
1090
|
+
const batch = this.queue.splice(0, this.config.batchSize);
|
|
1091
|
+
await this.processConcurrent(batch);
|
|
1092
|
+
}
|
|
1093
|
+
} finally {
|
|
1094
|
+
this.processing = false;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Process requests concurrently with limit
|
|
1099
|
+
*/
|
|
1100
|
+
async processConcurrent(requests) {
|
|
1101
|
+
const results = [];
|
|
1102
|
+
const chunks = this.chunkArray(requests, this.config.maxConcurrent);
|
|
1103
|
+
for (const chunk of chunks) {
|
|
1104
|
+
const promises = chunk.map((req) => this.summarize(req));
|
|
1105
|
+
const chunkResults = await Promise.all(promises);
|
|
1106
|
+
results.push(...chunkResults);
|
|
1107
|
+
}
|
|
1108
|
+
return results;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Summarize multiple function calls
|
|
1112
|
+
*/
|
|
1113
|
+
async summarizeBatch(requests) {
|
|
1114
|
+
const summaries = await this.processConcurrent(requests);
|
|
1115
|
+
return summaries.map((summary) => ({
|
|
1116
|
+
fcId: summary.fcId,
|
|
1117
|
+
summary: summary.summary,
|
|
1118
|
+
tokens: summary.tokens
|
|
1119
|
+
}));
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Build summarization prompt
|
|
1123
|
+
*/
|
|
1124
|
+
buildPrompt(request) {
|
|
1125
|
+
const argsStr = JSON.stringify(request.fcArgs, null, 2);
|
|
1126
|
+
const resultStr = this.truncateResult(request.fcResult);
|
|
1127
|
+
return SUMMARIZE_PROMPT.replace("{fc_name}", request.fcName).replace("{fc_args}", argsStr).replace("{fc_result}", resultStr);
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Truncate result to reasonable length
|
|
1131
|
+
*/
|
|
1132
|
+
truncateResult(result, maxLength = 2e3) {
|
|
1133
|
+
if (result.length <= maxLength) {
|
|
1134
|
+
return result;
|
|
1135
|
+
}
|
|
1136
|
+
return `${result.substring(0, maxLength)}... [truncated]`;
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Clean up summary text
|
|
1140
|
+
*/
|
|
1141
|
+
cleanSummary(summary) {
|
|
1142
|
+
let cleaned = summary.trim();
|
|
1143
|
+
cleaned = cleaned.replace(/^Summary:\s*/i, "");
|
|
1144
|
+
if (cleaned.length > 100) {
|
|
1145
|
+
cleaned = `${cleaned.substring(0, 97)}...`;
|
|
1146
|
+
}
|
|
1147
|
+
return cleaned;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Create fallback summary when API fails
|
|
1151
|
+
*/
|
|
1152
|
+
createFallbackSummary(request) {
|
|
1153
|
+
const summary = `${request.fcName} executed`;
|
|
1154
|
+
const tokens = estimateTokens(summary);
|
|
1155
|
+
return {
|
|
1156
|
+
fcId: request.fcId,
|
|
1157
|
+
fcName: request.fcName,
|
|
1158
|
+
summary,
|
|
1159
|
+
tokens,
|
|
1160
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Chunk array into smaller arrays
|
|
1165
|
+
*/
|
|
1166
|
+
chunkArray(array, size) {
|
|
1167
|
+
const chunks = [];
|
|
1168
|
+
for (let i = 0; i < array.length; i += size) {
|
|
1169
|
+
chunks.push(array.slice(i, i + size));
|
|
1170
|
+
}
|
|
1171
|
+
return chunks;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Update configuration
|
|
1175
|
+
*/
|
|
1176
|
+
updateConfig(config) {
|
|
1177
|
+
if (config.model)
|
|
1178
|
+
this.config.model = config.model;
|
|
1179
|
+
if (config.apiKey)
|
|
1180
|
+
this.config.apiKey = config.apiKey;
|
|
1181
|
+
if (config.batchSize)
|
|
1182
|
+
this.config.batchSize = config.batchSize;
|
|
1183
|
+
if (config.maxConcurrent)
|
|
1184
|
+
this.config.maxConcurrent = config.maxConcurrent;
|
|
1185
|
+
if (config.apiKey || config.model) {
|
|
1186
|
+
const modelName = this.config.model === "haiku" ? "claude-3-5-haiku-20241022" : void 0;
|
|
1187
|
+
this.apiClient.updateConfig({
|
|
1188
|
+
apiKey: this.config.apiKey,
|
|
1189
|
+
model: modelName
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Get current configuration
|
|
1195
|
+
*/
|
|
1196
|
+
getConfig() {
|
|
1197
|
+
return { ...this.config };
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Get queue length
|
|
1201
|
+
*/
|
|
1202
|
+
getQueueLength() {
|
|
1203
|
+
return this.queue.length;
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Check if processing
|
|
1207
|
+
*/
|
|
1208
|
+
isProcessing() {
|
|
1209
|
+
return this.processing;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
function createSummarizer(config) {
|
|
1213
|
+
return new Summarizer(config);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const DEFAULT_SESSION_CONFIG = {
|
|
1217
|
+
contextThreshold: 0.8,
|
|
1218
|
+
// 80%
|
|
1219
|
+
maxContextTokens: 2e5,
|
|
1220
|
+
summaryModel: "haiku",
|
|
1221
|
+
autoSummarize: true
|
|
1222
|
+
};
|
|
1223
|
+
class SessionManager extends EventEmitter {
|
|
1224
|
+
currentSession = null;
|
|
1225
|
+
config;
|
|
1226
|
+
summarizer;
|
|
1227
|
+
sessionHistory = [];
|
|
1228
|
+
constructor(config = {}) {
|
|
1229
|
+
super();
|
|
1230
|
+
this.config = { ...DEFAULT_SESSION_CONFIG, ...config };
|
|
1231
|
+
this.summarizer = createSummarizer({
|
|
1232
|
+
model: this.config.summaryModel
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Create new session
|
|
1237
|
+
*/
|
|
1238
|
+
createSession(projectPath) {
|
|
1239
|
+
if (this.currentSession) {
|
|
1240
|
+
this.completeSession();
|
|
1241
|
+
}
|
|
1242
|
+
const projectHash = this.generateProjectHash(projectPath);
|
|
1243
|
+
const session = {
|
|
1244
|
+
id: this.generateSessionId(),
|
|
1245
|
+
projectPath,
|
|
1246
|
+
projectHash,
|
|
1247
|
+
startTime: /* @__PURE__ */ new Date(),
|
|
1248
|
+
status: "active",
|
|
1249
|
+
tokenCount: 0,
|
|
1250
|
+
fcCount: 0,
|
|
1251
|
+
summaries: []
|
|
1252
|
+
};
|
|
1253
|
+
this.currentSession = session;
|
|
1254
|
+
this.emitEvent("session_created", session.id, { session });
|
|
1255
|
+
return session;
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Get current session
|
|
1259
|
+
*/
|
|
1260
|
+
getCurrentSession() {
|
|
1261
|
+
return this.currentSession;
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Add function call summary to current session
|
|
1265
|
+
*/
|
|
1266
|
+
async addFunctionCall(fcName, fcArgs, fcResult) {
|
|
1267
|
+
if (!this.currentSession) {
|
|
1268
|
+
throw new Error("No active session");
|
|
1269
|
+
}
|
|
1270
|
+
const fcId = this.generateFcId(fcName, fcArgs);
|
|
1271
|
+
let summary = null;
|
|
1272
|
+
if (this.config.autoSummarize) {
|
|
1273
|
+
summary = await this.summarizer.summarize({
|
|
1274
|
+
fcId,
|
|
1275
|
+
fcName,
|
|
1276
|
+
fcArgs,
|
|
1277
|
+
fcResult
|
|
1278
|
+
});
|
|
1279
|
+
this.currentSession.summaries.push(summary);
|
|
1280
|
+
this.currentSession.tokenCount += summary.tokens;
|
|
1281
|
+
} else {
|
|
1282
|
+
const tokens = estimateTokens(fcResult);
|
|
1283
|
+
this.currentSession.tokenCount += tokens;
|
|
1284
|
+
}
|
|
1285
|
+
this.currentSession.fcCount++;
|
|
1286
|
+
this.checkThresholds();
|
|
1287
|
+
if (summary) {
|
|
1288
|
+
this.emitEvent("fc_summarized", this.currentSession.id, { summary });
|
|
1289
|
+
}
|
|
1290
|
+
return summary;
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Check context thresholds
|
|
1294
|
+
*/
|
|
1295
|
+
checkThresholds() {
|
|
1296
|
+
if (!this.currentSession)
|
|
1297
|
+
return;
|
|
1298
|
+
const level = this.getThresholdLevel();
|
|
1299
|
+
if (level === "warning") {
|
|
1300
|
+
this.emitEvent("threshold_warning", this.currentSession.id, {
|
|
1301
|
+
usage: this.getContextUsage(),
|
|
1302
|
+
remaining: this.getRemainingTokens()
|
|
1303
|
+
});
|
|
1304
|
+
} else if (level === "critical") {
|
|
1305
|
+
this.emitEvent("threshold_critical", this.currentSession.id, {
|
|
1306
|
+
usage: this.getContextUsage(),
|
|
1307
|
+
remaining: this.getRemainingTokens(),
|
|
1308
|
+
sessionSummary: this.generateSessionSummary()
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Get threshold level
|
|
1314
|
+
*/
|
|
1315
|
+
getThresholdLevel() {
|
|
1316
|
+
if (!this.currentSession)
|
|
1317
|
+
return "normal";
|
|
1318
|
+
const usage = this.getContextUsage();
|
|
1319
|
+
if (usage >= this.config.contextThreshold * 100) {
|
|
1320
|
+
return "critical";
|
|
1321
|
+
}
|
|
1322
|
+
if (usage >= (this.config.contextThreshold - 0.1) * 100) {
|
|
1323
|
+
return "warning";
|
|
1324
|
+
}
|
|
1325
|
+
return "normal";
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Get context usage percentage
|
|
1329
|
+
*/
|
|
1330
|
+
getContextUsage() {
|
|
1331
|
+
if (!this.currentSession)
|
|
1332
|
+
return 0;
|
|
1333
|
+
return calculateContextUsage(
|
|
1334
|
+
this.currentSession.tokenCount,
|
|
1335
|
+
this.config.maxContextTokens
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Get remaining tokens
|
|
1340
|
+
*/
|
|
1341
|
+
getRemainingTokens() {
|
|
1342
|
+
if (!this.currentSession)
|
|
1343
|
+
return this.config.maxContextTokens;
|
|
1344
|
+
return getRemainingTokens(
|
|
1345
|
+
this.currentSession.tokenCount,
|
|
1346
|
+
this.config.maxContextTokens
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Check if threshold is exceeded
|
|
1351
|
+
*/
|
|
1352
|
+
isThresholdExceeded() {
|
|
1353
|
+
if (!this.currentSession)
|
|
1354
|
+
return false;
|
|
1355
|
+
return isThresholdExceeded(
|
|
1356
|
+
this.currentSession.tokenCount,
|
|
1357
|
+
this.config.maxContextTokens,
|
|
1358
|
+
this.config.contextThreshold
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Generate session summary for continuation
|
|
1363
|
+
*/
|
|
1364
|
+
generateSessionSummary() {
|
|
1365
|
+
if (!this.currentSession) {
|
|
1366
|
+
return "No active session";
|
|
1367
|
+
}
|
|
1368
|
+
const session = this.currentSession;
|
|
1369
|
+
const lines = [];
|
|
1370
|
+
lines.push("# Session Summary");
|
|
1371
|
+
lines.push("");
|
|
1372
|
+
lines.push(`Project: ${session.projectPath}`);
|
|
1373
|
+
lines.push(`Session ID: ${session.id}`);
|
|
1374
|
+
lines.push(`Duration: ${this.getSessionDuration()}`);
|
|
1375
|
+
lines.push(`Function Calls: ${session.fcCount}`);
|
|
1376
|
+
lines.push(`Token Usage: ${session.tokenCount} / ${this.config.maxContextTokens} (${this.getContextUsage().toFixed(1)}%)`);
|
|
1377
|
+
lines.push("");
|
|
1378
|
+
if (session.summaries.length > 0) {
|
|
1379
|
+
lines.push("## Function Call Summaries");
|
|
1380
|
+
lines.push("");
|
|
1381
|
+
for (const summary of session.summaries) {
|
|
1382
|
+
lines.push(`- **${summary.fcName}**: ${summary.summary}`);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return lines.join("\n");
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Get session duration
|
|
1389
|
+
*/
|
|
1390
|
+
getSessionDuration() {
|
|
1391
|
+
if (!this.currentSession)
|
|
1392
|
+
return "0s";
|
|
1393
|
+
const start = this.currentSession.startTime.getTime();
|
|
1394
|
+
const end = this.currentSession.endTime?.getTime() || Date.now();
|
|
1395
|
+
const duration = Math.floor((end - start) / 1e3);
|
|
1396
|
+
if (duration < 60)
|
|
1397
|
+
return `${duration}s`;
|
|
1398
|
+
if (duration < 3600)
|
|
1399
|
+
return `${Math.floor(duration / 60)}m ${duration % 60}s`;
|
|
1400
|
+
const hours = Math.floor(duration / 3600);
|
|
1401
|
+
const minutes = Math.floor(duration % 3600 / 60);
|
|
1402
|
+
return `${hours}h ${minutes}m`;
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Complete current session
|
|
1406
|
+
*/
|
|
1407
|
+
completeSession() {
|
|
1408
|
+
if (!this.currentSession)
|
|
1409
|
+
return null;
|
|
1410
|
+
this.currentSession.status = "completed";
|
|
1411
|
+
this.currentSession.endTime = /* @__PURE__ */ new Date();
|
|
1412
|
+
this.sessionHistory.push(this.currentSession);
|
|
1413
|
+
this.emitEvent("session_completed", this.currentSession.id, {
|
|
1414
|
+
session: this.currentSession,
|
|
1415
|
+
summary: this.generateSessionSummary()
|
|
1416
|
+
});
|
|
1417
|
+
const completedSession = this.currentSession;
|
|
1418
|
+
this.currentSession = null;
|
|
1419
|
+
return completedSession;
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Archive session
|
|
1423
|
+
*/
|
|
1424
|
+
archiveSession(sessionId) {
|
|
1425
|
+
const session = this.sessionHistory.find((s) => s.id === sessionId);
|
|
1426
|
+
if (!session)
|
|
1427
|
+
return false;
|
|
1428
|
+
session.status = "archived";
|
|
1429
|
+
this.emitEvent("session_archived", sessionId, { session });
|
|
1430
|
+
return true;
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Get session by ID
|
|
1434
|
+
*/
|
|
1435
|
+
getSession(sessionId) {
|
|
1436
|
+
if (this.currentSession?.id === sessionId) {
|
|
1437
|
+
return this.currentSession;
|
|
1438
|
+
}
|
|
1439
|
+
return this.sessionHistory.find((s) => s.id === sessionId) || null;
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Get all sessions
|
|
1443
|
+
*/
|
|
1444
|
+
getAllSessions() {
|
|
1445
|
+
const sessions = [...this.sessionHistory];
|
|
1446
|
+
if (this.currentSession) {
|
|
1447
|
+
sessions.push(this.currentSession);
|
|
1448
|
+
}
|
|
1449
|
+
return sessions;
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Get sessions by project
|
|
1453
|
+
*/
|
|
1454
|
+
getSessionsByProject(projectPath) {
|
|
1455
|
+
const projectHash = this.generateProjectHash(projectPath);
|
|
1456
|
+
return this.getAllSessions().filter((s) => s.projectHash === projectHash);
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Clear session history
|
|
1460
|
+
*/
|
|
1461
|
+
clearHistory() {
|
|
1462
|
+
this.sessionHistory = [];
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Update configuration
|
|
1466
|
+
*/
|
|
1467
|
+
updateConfig(config) {
|
|
1468
|
+
this.config = { ...this.config, ...config };
|
|
1469
|
+
if (config.summaryModel) {
|
|
1470
|
+
this.summarizer.updateConfig({ model: config.summaryModel });
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Get configuration
|
|
1475
|
+
*/
|
|
1476
|
+
getConfig() {
|
|
1477
|
+
return { ...this.config };
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Generate session ID
|
|
1481
|
+
*/
|
|
1482
|
+
generateSessionId() {
|
|
1483
|
+
return `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Generate project hash
|
|
1487
|
+
*/
|
|
1488
|
+
generateProjectHash(projectPath) {
|
|
1489
|
+
return createHash("md5").update(projectPath).digest("hex").substring(0, 8);
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Generate function call ID
|
|
1493
|
+
*/
|
|
1494
|
+
generateFcId(fcName, fcArgs) {
|
|
1495
|
+
const data = `${fcName}_${JSON.stringify(fcArgs)}_${Date.now()}`;
|
|
1496
|
+
return createHash("md5").update(data).digest("hex").substring(0, 12);
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Emit session event
|
|
1500
|
+
*/
|
|
1501
|
+
emitEvent(type, sessionId, data) {
|
|
1502
|
+
const event = {
|
|
1503
|
+
type,
|
|
1504
|
+
sessionId,
|
|
1505
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1506
|
+
data
|
|
1507
|
+
};
|
|
1508
|
+
this.emit("session_event", event);
|
|
1509
|
+
this.emit(type, event);
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Get summarizer instance
|
|
1513
|
+
*/
|
|
1514
|
+
getSummarizer() {
|
|
1515
|
+
return this.summarizer;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
async function generateProjectHash(projectPath) {
|
|
1520
|
+
let normalizedPath = normalize(projectPath);
|
|
1521
|
+
normalizedPath = normalizedPath.replace(/[/\\]+$/, "");
|
|
1522
|
+
const gitInfo = await getGitInfo(normalizedPath);
|
|
1523
|
+
const hashInput = [
|
|
1524
|
+
normalizedPath,
|
|
1525
|
+
gitInfo.remote || "",
|
|
1526
|
+
gitInfo.branch || ""
|
|
1527
|
+
].join("|");
|
|
1528
|
+
const hash = createHash("sha256").update(hashInput).digest("hex").substring(0, 16);
|
|
1529
|
+
return {
|
|
1530
|
+
path: normalizedPath,
|
|
1531
|
+
gitRemote: gitInfo.remote,
|
|
1532
|
+
gitBranch: gitInfo.branch,
|
|
1533
|
+
hash
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
async function getGitInfo(projectPath) {
|
|
1537
|
+
try {
|
|
1538
|
+
const gitDir = join(projectPath, ".git");
|
|
1539
|
+
if (!existsSync(gitDir)) {
|
|
1540
|
+
return {};
|
|
1541
|
+
}
|
|
1542
|
+
let remote;
|
|
1543
|
+
try {
|
|
1544
|
+
const remoteResult = await exec("git", ["remote", "get-url", "origin"], {
|
|
1545
|
+
nodeOptions: { cwd: projectPath }
|
|
1546
|
+
});
|
|
1547
|
+
remote = remoteResult.stdout?.trim();
|
|
1548
|
+
} catch {
|
|
1549
|
+
}
|
|
1550
|
+
let branch;
|
|
1551
|
+
try {
|
|
1552
|
+
const branchResult = await exec("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
1553
|
+
nodeOptions: { cwd: projectPath }
|
|
1554
|
+
});
|
|
1555
|
+
branch = branchResult.stdout?.trim();
|
|
1556
|
+
} catch {
|
|
1557
|
+
}
|
|
1558
|
+
return { remote, branch };
|
|
1559
|
+
} catch {
|
|
1560
|
+
return {};
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
class ProjectHashCache {
|
|
1564
|
+
cache = /* @__PURE__ */ new Map();
|
|
1565
|
+
cacheTimeout = 5 * 60 * 1e3;
|
|
1566
|
+
// 5 minutes
|
|
1567
|
+
timestamps = /* @__PURE__ */ new Map();
|
|
1568
|
+
/**
|
|
1569
|
+
* Get or generate project identity
|
|
1570
|
+
*
|
|
1571
|
+
* @param projectPath - Project directory path
|
|
1572
|
+
* @param forceRefresh - Force cache refresh
|
|
1573
|
+
* @returns Project identity
|
|
1574
|
+
*/
|
|
1575
|
+
async get(projectPath, forceRefresh = false) {
|
|
1576
|
+
let normalizedPath = normalize(projectPath);
|
|
1577
|
+
normalizedPath = normalizedPath.replace(/[/\\]+$/, "");
|
|
1578
|
+
const now = Date.now();
|
|
1579
|
+
const timestamp = this.timestamps.get(normalizedPath);
|
|
1580
|
+
if (!forceRefresh && timestamp && now - timestamp < this.cacheTimeout) {
|
|
1581
|
+
const cached = this.cache.get(normalizedPath);
|
|
1582
|
+
if (cached) {
|
|
1583
|
+
return cached;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
const identity = await generateProjectHash(normalizedPath);
|
|
1587
|
+
this.cache.set(normalizedPath, identity);
|
|
1588
|
+
this.timestamps.set(normalizedPath, now);
|
|
1589
|
+
return identity;
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Clear cache for specific project or all projects
|
|
1593
|
+
*
|
|
1594
|
+
* @param projectPath - Optional project path to clear
|
|
1595
|
+
*/
|
|
1596
|
+
clear(projectPath) {
|
|
1597
|
+
if (projectPath) {
|
|
1598
|
+
let normalizedPath = normalize(projectPath);
|
|
1599
|
+
normalizedPath = normalizedPath.replace(/[/\\]+$/, "");
|
|
1600
|
+
this.cache.delete(normalizedPath);
|
|
1601
|
+
this.timestamps.delete(normalizedPath);
|
|
1602
|
+
} else {
|
|
1603
|
+
this.cache.clear();
|
|
1604
|
+
this.timestamps.clear();
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Get cache statistics
|
|
1609
|
+
*/
|
|
1610
|
+
getStats() {
|
|
1611
|
+
const timestamps = Array.from(this.timestamps.values());
|
|
1612
|
+
return {
|
|
1613
|
+
size: this.cache.size,
|
|
1614
|
+
oldestEntry: timestamps.length > 0 ? Math.min(...timestamps) : void 0
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
const projectHashCache = new ProjectHashCache();
|
|
1619
|
+
async function getProjectIdentity(projectPath, forceRefresh = false) {
|
|
1620
|
+
return projectHashCache.get(projectPath, forceRefresh);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
class StorageManager {
|
|
1624
|
+
baseDir;
|
|
1625
|
+
sessionsDir;
|
|
1626
|
+
initialized = false;
|
|
1627
|
+
constructor(baseDir) {
|
|
1628
|
+
this.baseDir = baseDir || join(homedir(), ".ccjk", "context");
|
|
1629
|
+
this.sessionsDir = join(this.baseDir, "sessions");
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Initialize storage directories
|
|
1633
|
+
*/
|
|
1634
|
+
async initialize() {
|
|
1635
|
+
if (this.initialized) {
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
await mkdir(this.baseDir, { recursive: true });
|
|
1639
|
+
await mkdir(this.sessionsDir, { recursive: true });
|
|
1640
|
+
this.initialized = true;
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Create a new session
|
|
1644
|
+
*
|
|
1645
|
+
* @param projectPath - Absolute path to project directory
|
|
1646
|
+
* @param description - Optional session description
|
|
1647
|
+
* @returns Created session
|
|
1648
|
+
*/
|
|
1649
|
+
async createSession(projectPath, description) {
|
|
1650
|
+
await this.initialize();
|
|
1651
|
+
const identity = await getProjectIdentity(projectPath);
|
|
1652
|
+
const sessionId = this.generateSessionId();
|
|
1653
|
+
const projectDir = join(this.sessionsDir, identity.hash);
|
|
1654
|
+
const sessionDir = join(projectDir, sessionId);
|
|
1655
|
+
await mkdir(sessionDir, { recursive: true });
|
|
1656
|
+
const meta = {
|
|
1657
|
+
id: sessionId,
|
|
1658
|
+
projectPath: identity.path,
|
|
1659
|
+
projectHash: identity.hash,
|
|
1660
|
+
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1661
|
+
status: "active",
|
|
1662
|
+
tokenCount: 0,
|
|
1663
|
+
summaryTokens: 0,
|
|
1664
|
+
fcCount: 0,
|
|
1665
|
+
version: this.getCcjkVersion(),
|
|
1666
|
+
description,
|
|
1667
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1668
|
+
};
|
|
1669
|
+
const metaPath = join(sessionDir, "meta.json");
|
|
1670
|
+
await this.writeJsonAtomic(metaPath, meta);
|
|
1671
|
+
const fcLogPath = join(sessionDir, "fc-log.jsonl");
|
|
1672
|
+
await writeFile(fcLogPath, "", "utf-8");
|
|
1673
|
+
await this.setCurrentSession(identity.hash, sessionId);
|
|
1674
|
+
const session = {
|
|
1675
|
+
meta,
|
|
1676
|
+
path: sessionDir,
|
|
1677
|
+
fcLogPath,
|
|
1678
|
+
summaryPath: join(sessionDir, "summary.md")
|
|
1679
|
+
};
|
|
1680
|
+
return session;
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Get session by ID
|
|
1684
|
+
*
|
|
1685
|
+
* @param sessionId - Session identifier
|
|
1686
|
+
* @param projectHash - Optional project hash for faster lookup
|
|
1687
|
+
* @returns Session or null if not found
|
|
1688
|
+
*/
|
|
1689
|
+
async getSession(sessionId, projectHash) {
|
|
1690
|
+
await this.initialize();
|
|
1691
|
+
try {
|
|
1692
|
+
let sessionDir;
|
|
1693
|
+
if (projectHash) {
|
|
1694
|
+
sessionDir = join(this.sessionsDir, projectHash, sessionId);
|
|
1695
|
+
} else {
|
|
1696
|
+
const projectDirs = await readdir(this.sessionsDir);
|
|
1697
|
+
for (const dir of projectDirs) {
|
|
1698
|
+
const candidateDir = join(this.sessionsDir, dir, sessionId);
|
|
1699
|
+
if (existsSync(candidateDir)) {
|
|
1700
|
+
sessionDir = candidateDir;
|
|
1701
|
+
break;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
if (!sessionDir) {
|
|
1705
|
+
return null;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
if (!existsSync(sessionDir)) {
|
|
1709
|
+
return null;
|
|
1710
|
+
}
|
|
1711
|
+
const metaPath = join(sessionDir, "meta.json");
|
|
1712
|
+
const meta = await this.readJson(metaPath);
|
|
1713
|
+
if (!meta) {
|
|
1714
|
+
return null;
|
|
1715
|
+
}
|
|
1716
|
+
return {
|
|
1717
|
+
meta,
|
|
1718
|
+
path: sessionDir,
|
|
1719
|
+
fcLogPath: join(sessionDir, "fc-log.jsonl"),
|
|
1720
|
+
summaryPath: join(sessionDir, "summary.md")
|
|
1721
|
+
};
|
|
1722
|
+
} catch {
|
|
1723
|
+
return null;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Update session metadata
|
|
1728
|
+
*
|
|
1729
|
+
* @param session - Session with updated metadata
|
|
1730
|
+
*/
|
|
1731
|
+
async updateSession(session) {
|
|
1732
|
+
await this.initialize();
|
|
1733
|
+
session.meta.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1734
|
+
const metaPath = join(session.path, "meta.json");
|
|
1735
|
+
await this.writeJsonAtomic(metaPath, session.meta);
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Complete a session
|
|
1739
|
+
*
|
|
1740
|
+
* @param sessionId - Session identifier
|
|
1741
|
+
* @param projectHash - Optional project hash
|
|
1742
|
+
*/
|
|
1743
|
+
async completeSession(sessionId, projectHash) {
|
|
1744
|
+
const session = await this.getSession(sessionId, projectHash);
|
|
1745
|
+
if (!session) {
|
|
1746
|
+
return false;
|
|
1747
|
+
}
|
|
1748
|
+
session.meta.status = "completed";
|
|
1749
|
+
session.meta.endTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
1750
|
+
await this.updateSession(session);
|
|
1751
|
+
return true;
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Archive a session
|
|
1755
|
+
*
|
|
1756
|
+
* @param sessionId - Session identifier
|
|
1757
|
+
* @param projectHash - Optional project hash
|
|
1758
|
+
*/
|
|
1759
|
+
async archiveSession(sessionId, projectHash) {
|
|
1760
|
+
const session = await this.getSession(sessionId, projectHash);
|
|
1761
|
+
if (!session) {
|
|
1762
|
+
return false;
|
|
1763
|
+
}
|
|
1764
|
+
session.meta.status = "archived";
|
|
1765
|
+
await this.updateSession(session);
|
|
1766
|
+
return true;
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* List sessions with optional filtering
|
|
1770
|
+
*
|
|
1771
|
+
* @param options - Query options
|
|
1772
|
+
* @returns Array of session metadata
|
|
1773
|
+
*/
|
|
1774
|
+
async listSessions(options) {
|
|
1775
|
+
await this.initialize();
|
|
1776
|
+
const sessions = [];
|
|
1777
|
+
try {
|
|
1778
|
+
const projectDirs = options?.projectHash ? [options.projectHash] : await readdir(this.sessionsDir);
|
|
1779
|
+
for (const projectDir of projectDirs) {
|
|
1780
|
+
const projectPath = join(this.sessionsDir, projectDir);
|
|
1781
|
+
if (!existsSync(projectPath)) {
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
const sessionDirs = await readdir(projectPath);
|
|
1785
|
+
for (const sessionDir of sessionDirs) {
|
|
1786
|
+
if (sessionDir === "current.json") {
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
const metaPath = join(projectPath, sessionDir, "meta.json");
|
|
1790
|
+
if (!existsSync(metaPath)) {
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
const meta = await this.readJson(metaPath);
|
|
1794
|
+
if (!meta) {
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
if (options?.status && meta.status !== options.status) {
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
sessions.push(meta);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (options?.sortBy) {
|
|
1804
|
+
const sortKey = options.sortBy;
|
|
1805
|
+
const order = options.sortOrder || "desc";
|
|
1806
|
+
sessions.sort((a, b) => {
|
|
1807
|
+
const aVal = a[sortKey];
|
|
1808
|
+
const bVal = b[sortKey];
|
|
1809
|
+
if (typeof aVal === "string" && typeof bVal === "string") {
|
|
1810
|
+
return order === "asc" ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
|
|
1811
|
+
}
|
|
1812
|
+
if (typeof aVal === "number" && typeof bVal === "number") {
|
|
1813
|
+
return order === "asc" ? aVal - bVal : bVal - aVal;
|
|
1814
|
+
}
|
|
1815
|
+
return 0;
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
if (options?.limit && options.limit > 0) {
|
|
1819
|
+
return sessions.slice(0, options.limit);
|
|
1820
|
+
}
|
|
1821
|
+
return sessions;
|
|
1822
|
+
} catch {
|
|
1823
|
+
return [];
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Append function call log entry
|
|
1828
|
+
*
|
|
1829
|
+
* @param sessionId - Session identifier
|
|
1830
|
+
* @param entry - FC log entry
|
|
1831
|
+
* @param projectHash - Optional project hash
|
|
1832
|
+
*/
|
|
1833
|
+
async appendFCLog(sessionId, entry, projectHash) {
|
|
1834
|
+
await this.initialize();
|
|
1835
|
+
const session = await this.getSession(sessionId, projectHash);
|
|
1836
|
+
if (!session) {
|
|
1837
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1838
|
+
}
|
|
1839
|
+
const line = `${JSON.stringify(entry)}
|
|
1840
|
+
`;
|
|
1841
|
+
await writeFile(session.fcLogPath, line, { flag: "a", encoding: "utf-8" });
|
|
1842
|
+
session.meta.fcCount++;
|
|
1843
|
+
session.meta.tokenCount += entry.tokens;
|
|
1844
|
+
await this.updateSession(session);
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Get function call logs as async generator
|
|
1848
|
+
* Efficiently streams large log files
|
|
1849
|
+
*
|
|
1850
|
+
* @param sessionId - Session identifier
|
|
1851
|
+
* @param options - Query options
|
|
1852
|
+
* @param projectHash - Optional project hash
|
|
1853
|
+
*/
|
|
1854
|
+
async *getFCLogs(sessionId, options, projectHash) {
|
|
1855
|
+
await this.initialize();
|
|
1856
|
+
const session = await this.getSession(sessionId, projectHash);
|
|
1857
|
+
if (!session || !existsSync(session.fcLogPath)) {
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
const fileStream = createReadStream(session.fcLogPath, { encoding: "utf-8" });
|
|
1861
|
+
const rl = createInterface({
|
|
1862
|
+
input: fileStream,
|
|
1863
|
+
crlfDelay: Infinity
|
|
1864
|
+
});
|
|
1865
|
+
let count = 0;
|
|
1866
|
+
for await (const line of rl) {
|
|
1867
|
+
if (!line.trim()) {
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
try {
|
|
1871
|
+
const entry = JSON.parse(line);
|
|
1872
|
+
if (options?.startTime && entry.ts < options.startTime) {
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
if (options?.endTime && entry.ts > options.endTime) {
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
if (options?.functionName && entry.fc !== options.functionName) {
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
if (options?.status && entry.status !== options.status) {
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
yield entry;
|
|
1885
|
+
count++;
|
|
1886
|
+
if (options?.limit && count >= options.limit) {
|
|
1887
|
+
break;
|
|
1888
|
+
}
|
|
1889
|
+
} catch {
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Get all FC logs as array
|
|
1896
|
+
* Use getFCLogs() generator for large files
|
|
1897
|
+
*
|
|
1898
|
+
* @param sessionId - Session identifier
|
|
1899
|
+
* @param options - Query options
|
|
1900
|
+
* @param projectHash - Optional project hash
|
|
1901
|
+
*/
|
|
1902
|
+
async getFCLogsArray(sessionId, options, projectHash) {
|
|
1903
|
+
const logs = [];
|
|
1904
|
+
for await (const entry of this.getFCLogs(sessionId, options, projectHash)) {
|
|
1905
|
+
logs.push(entry);
|
|
1906
|
+
}
|
|
1907
|
+
return logs;
|
|
1908
|
+
}
|
|
1909
|
+
/**
|
|
1910
|
+
* Save session summary
|
|
1911
|
+
*
|
|
1912
|
+
* @param sessionId - Session identifier
|
|
1913
|
+
* @param summary - Markdown summary content
|
|
1914
|
+
* @param projectHash - Optional project hash
|
|
1915
|
+
*/
|
|
1916
|
+
async saveSummary(sessionId, summary, projectHash) {
|
|
1917
|
+
await this.initialize();
|
|
1918
|
+
const session = await this.getSession(sessionId, projectHash);
|
|
1919
|
+
if (!session) {
|
|
1920
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1921
|
+
}
|
|
1922
|
+
await this.writeFileAtomic(session.summaryPath, summary);
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Get session summary
|
|
1926
|
+
*
|
|
1927
|
+
* @param sessionId - Session identifier
|
|
1928
|
+
* @param projectHash - Optional project hash
|
|
1929
|
+
* @returns Summary content or null if not found
|
|
1930
|
+
*/
|
|
1931
|
+
async getSummary(sessionId, projectHash) {
|
|
1932
|
+
await this.initialize();
|
|
1933
|
+
const session = await this.getSession(sessionId, projectHash);
|
|
1934
|
+
if (!session || !existsSync(session.summaryPath)) {
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1937
|
+
try {
|
|
1938
|
+
return await readFile(session.summaryPath, "utf-8");
|
|
1939
|
+
} catch {
|
|
1940
|
+
return null;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Get current session for a project
|
|
1945
|
+
*
|
|
1946
|
+
* @param projectHash - Project hash identifier
|
|
1947
|
+
* @returns Current session or null
|
|
1948
|
+
*/
|
|
1949
|
+
async getCurrentSession(projectHash) {
|
|
1950
|
+
await this.initialize();
|
|
1951
|
+
const pointerPath = join(this.sessionsDir, projectHash, "current.json");
|
|
1952
|
+
if (!existsSync(pointerPath)) {
|
|
1953
|
+
return null;
|
|
1954
|
+
}
|
|
1955
|
+
try {
|
|
1956
|
+
const pointer = await this.readJson(pointerPath);
|
|
1957
|
+
if (!pointer) {
|
|
1958
|
+
return null;
|
|
1959
|
+
}
|
|
1960
|
+
return this.getSession(pointer.sessionId, projectHash);
|
|
1961
|
+
} catch {
|
|
1962
|
+
return null;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Set current session for a project
|
|
1967
|
+
*
|
|
1968
|
+
* @param projectHash - Project hash identifier
|
|
1969
|
+
* @param sessionId - Session identifier
|
|
1970
|
+
*/
|
|
1971
|
+
async setCurrentSession(projectHash, sessionId) {
|
|
1972
|
+
await this.initialize();
|
|
1973
|
+
const projectDir = join(this.sessionsDir, projectHash);
|
|
1974
|
+
await mkdir(projectDir, { recursive: true });
|
|
1975
|
+
const pointer = {
|
|
1976
|
+
sessionId,
|
|
1977
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1978
|
+
};
|
|
1979
|
+
const pointerPath = join(projectDir, "current.json");
|
|
1980
|
+
await this.writeJsonAtomic(pointerPath, pointer);
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Delete a session
|
|
1984
|
+
*
|
|
1985
|
+
* @param sessionId - Session identifier
|
|
1986
|
+
* @param projectHash - Optional project hash
|
|
1987
|
+
* @returns True if deleted successfully
|
|
1988
|
+
*/
|
|
1989
|
+
async deleteSession(sessionId, projectHash) {
|
|
1990
|
+
await this.initialize();
|
|
1991
|
+
const session = await this.getSession(sessionId, projectHash);
|
|
1992
|
+
if (!session) {
|
|
1993
|
+
return false;
|
|
1994
|
+
}
|
|
1995
|
+
try {
|
|
1996
|
+
await this.deleteDirectory(session.path);
|
|
1997
|
+
return true;
|
|
1998
|
+
} catch {
|
|
1999
|
+
return false;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Clean up old sessions
|
|
2004
|
+
*
|
|
2005
|
+
* @param maxAge - Maximum age in milliseconds
|
|
2006
|
+
* @returns Cleanup result
|
|
2007
|
+
*/
|
|
2008
|
+
async cleanOldSessions(maxAge) {
|
|
2009
|
+
await this.initialize();
|
|
2010
|
+
const startTime = Date.now();
|
|
2011
|
+
const cutoffTime = new Date(Date.now() - maxAge).toISOString();
|
|
2012
|
+
const allSessions = await this.listSessions();
|
|
2013
|
+
const removedSessionIds = [];
|
|
2014
|
+
let bytesFreed = 0;
|
|
2015
|
+
for (const meta of allSessions) {
|
|
2016
|
+
if (meta.status === "active") {
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
const sessionTime = meta.endTime || meta.lastUpdated;
|
|
2020
|
+
if (sessionTime >= cutoffTime) {
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
const session = await this.getSession(meta.id, meta.projectHash);
|
|
2024
|
+
if (session) {
|
|
2025
|
+
const size = await this.getDirectorySize(session.path);
|
|
2026
|
+
bytesFreed += size;
|
|
2027
|
+
const deleted = await this.deleteSession(meta.id, meta.projectHash);
|
|
2028
|
+
if (deleted) {
|
|
2029
|
+
removedSessionIds.push(meta.id);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
return {
|
|
2034
|
+
sessionsRemoved: removedSessionIds.length,
|
|
2035
|
+
bytesFreed,
|
|
2036
|
+
removedSessionIds,
|
|
2037
|
+
duration: Date.now() - startTime
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
/**
|
|
2041
|
+
* Get storage statistics
|
|
2042
|
+
*/
|
|
2043
|
+
async getStorageStats() {
|
|
2044
|
+
await this.initialize();
|
|
2045
|
+
const allSessions = await this.listSessions();
|
|
2046
|
+
const stats = {
|
|
2047
|
+
totalSessions: allSessions.length,
|
|
2048
|
+
activeSessions: 0,
|
|
2049
|
+
completedSessions: 0,
|
|
2050
|
+
archivedSessions: 0,
|
|
2051
|
+
totalSize: 0,
|
|
2052
|
+
totalTokens: 0,
|
|
2053
|
+
totalFCs: 0,
|
|
2054
|
+
pendingSyncItems: 0
|
|
2055
|
+
};
|
|
2056
|
+
let oldestTime;
|
|
2057
|
+
let newestTime;
|
|
2058
|
+
for (const meta of allSessions) {
|
|
2059
|
+
if (meta.status === "active")
|
|
2060
|
+
stats.activeSessions++;
|
|
2061
|
+
else if (meta.status === "completed")
|
|
2062
|
+
stats.completedSessions++;
|
|
2063
|
+
else if (meta.status === "archived")
|
|
2064
|
+
stats.archivedSessions++;
|
|
2065
|
+
stats.totalTokens += meta.tokenCount;
|
|
2066
|
+
stats.totalFCs += meta.fcCount;
|
|
2067
|
+
if (!oldestTime || meta.startTime < oldestTime) {
|
|
2068
|
+
oldestTime = meta.startTime;
|
|
2069
|
+
}
|
|
2070
|
+
if (!newestTime || meta.startTime > newestTime) {
|
|
2071
|
+
newestTime = meta.startTime;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
stats.oldestSession = oldestTime;
|
|
2075
|
+
stats.newestSession = newestTime;
|
|
2076
|
+
try {
|
|
2077
|
+
stats.totalSize = await this.getDirectorySize(this.baseDir);
|
|
2078
|
+
} catch {
|
|
2079
|
+
stats.totalSize = 0;
|
|
2080
|
+
}
|
|
2081
|
+
return stats;
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* Write JSON file atomically
|
|
2085
|
+
* Writes to temp file first, then renames
|
|
2086
|
+
*/
|
|
2087
|
+
async writeJsonAtomic(filePath, data) {
|
|
2088
|
+
const content = JSON.stringify(data, null, 2);
|
|
2089
|
+
await this.writeFileAtomic(filePath, content);
|
|
2090
|
+
}
|
|
2091
|
+
/**
|
|
2092
|
+
* Write file atomically
|
|
2093
|
+
* Writes to temp file first, then renames
|
|
2094
|
+
*/
|
|
2095
|
+
async writeFileAtomic(filePath, content) {
|
|
2096
|
+
const dir = dirname(filePath);
|
|
2097
|
+
const tempPath = join(tmpdir(), `ccjk-${Date.now()}-${Math.random().toString(36).substring(2)}.tmp`);
|
|
2098
|
+
try {
|
|
2099
|
+
await writeFile(tempPath, content, "utf-8");
|
|
2100
|
+
await mkdir(dir, { recursive: true });
|
|
2101
|
+
await rename(tempPath, filePath);
|
|
2102
|
+
} catch (error) {
|
|
2103
|
+
try {
|
|
2104
|
+
await unlink(tempPath);
|
|
2105
|
+
} catch {
|
|
2106
|
+
}
|
|
2107
|
+
throw error;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Read JSON file safely
|
|
2112
|
+
*/
|
|
2113
|
+
async readJson(filePath) {
|
|
2114
|
+
try {
|
|
2115
|
+
const content = await readFile(filePath, "utf-8");
|
|
2116
|
+
return JSON.parse(content);
|
|
2117
|
+
} catch {
|
|
2118
|
+
return null;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
/**
|
|
2122
|
+
* Generate unique session ID
|
|
2123
|
+
*/
|
|
2124
|
+
generateSessionId() {
|
|
2125
|
+
const timestamp = Date.now();
|
|
2126
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
2127
|
+
return `session-${timestamp}-${random}`;
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Get CCJK version
|
|
2131
|
+
*/
|
|
2132
|
+
getCcjkVersion() {
|
|
2133
|
+
try {
|
|
2134
|
+
const pkgPath = join(__dirname, "../../../package.json");
|
|
2135
|
+
if (existsSync(pkgPath)) {
|
|
2136
|
+
const pkgContent = readFileSync(pkgPath, "utf-8");
|
|
2137
|
+
const pkg = JSON.parse(pkgContent);
|
|
2138
|
+
return pkg.version || "unknown";
|
|
2139
|
+
}
|
|
2140
|
+
} catch {
|
|
2141
|
+
}
|
|
2142
|
+
return "unknown";
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Get directory size recursively
|
|
2146
|
+
*/
|
|
2147
|
+
async getDirectorySize(dirPath) {
|
|
2148
|
+
let totalSize = 0;
|
|
2149
|
+
try {
|
|
2150
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
2151
|
+
for (const entry of entries) {
|
|
2152
|
+
const fullPath = join(dirPath, entry.name);
|
|
2153
|
+
if (entry.isDirectory()) {
|
|
2154
|
+
totalSize += await this.getDirectorySize(fullPath);
|
|
2155
|
+
} else if (entry.isFile()) {
|
|
2156
|
+
const stats = await stat(fullPath);
|
|
2157
|
+
totalSize += stats.size;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
} catch {
|
|
2161
|
+
}
|
|
2162
|
+
return totalSize;
|
|
2163
|
+
}
|
|
2164
|
+
/**
|
|
2165
|
+
* Delete directory recursively
|
|
2166
|
+
*/
|
|
2167
|
+
async deleteDirectory(dirPath) {
|
|
2168
|
+
if (!existsSync(dirPath)) {
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
2172
|
+
for (const entry of entries) {
|
|
2173
|
+
const fullPath = join(dirPath, entry.name);
|
|
2174
|
+
if (entry.isDirectory()) {
|
|
2175
|
+
await this.deleteDirectory(fullPath);
|
|
2176
|
+
} else {
|
|
2177
|
+
await unlink(fullPath);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
const fsp = await import('node:fs/promises');
|
|
2181
|
+
await fsp.rm(dirPath, { recursive: true });
|
|
125
2182
|
}
|
|
126
2183
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
2184
|
+
|
|
2185
|
+
class ThreadManager extends EventEmitter {
|
|
2186
|
+
threads = /* @__PURE__ */ new Map();
|
|
2187
|
+
currentThread = null;
|
|
2188
|
+
compressor;
|
|
2189
|
+
options;
|
|
2190
|
+
constructor(options = {}) {
|
|
2191
|
+
super();
|
|
2192
|
+
this.options = {
|
|
2193
|
+
defaultMaxTokens: options.defaultMaxTokens ?? 5e3,
|
|
2194
|
+
autoClose: options.autoClose ?? true,
|
|
2195
|
+
compressionStrategy: options.compressionStrategy ?? "miro-thinker",
|
|
2196
|
+
debug: options.debug ?? false
|
|
2197
|
+
};
|
|
2198
|
+
this.compressor = new MiroThinkerCompressor({
|
|
2199
|
+
toolResultThreshold: 500,
|
|
2200
|
+
preserveErrors: true,
|
|
2201
|
+
preserveKeyInfo: true
|
|
2202
|
+
});
|
|
130
2203
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
2204
|
+
/**
|
|
2205
|
+
* Create a new thread
|
|
2206
|
+
*/
|
|
2207
|
+
createThread(config) {
|
|
2208
|
+
if (this.currentThread && this.options.autoClose) {
|
|
2209
|
+
if (this.shouldCloseThread()) {
|
|
2210
|
+
this.log("Auto-closing previous thread before creating new one");
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
const thread = {
|
|
2214
|
+
id: this.generateThreadId(),
|
|
2215
|
+
parentId: config.parentId || this.currentThread?.id,
|
|
2216
|
+
topic: config.topic,
|
|
2217
|
+
messages: [],
|
|
2218
|
+
status: "active",
|
|
2219
|
+
createdAt: Date.now(),
|
|
2220
|
+
tokenCount: 0,
|
|
2221
|
+
maxTokens: config.maxTokens ?? this.options.defaultMaxTokens,
|
|
2222
|
+
metadata: config.metadata
|
|
143
2223
|
};
|
|
2224
|
+
this.threads.set(thread.id, thread);
|
|
2225
|
+
this.currentThread = thread;
|
|
2226
|
+
this.log(`Thread created: ${thread.id} (${thread.topic})`);
|
|
2227
|
+
this.emit("thread:created", thread);
|
|
2228
|
+
return thread;
|
|
144
2229
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
2230
|
+
/**
|
|
2231
|
+
* Get current active thread
|
|
2232
|
+
*/
|
|
2233
|
+
getCurrentThread() {
|
|
2234
|
+
return this.currentThread;
|
|
2235
|
+
}
|
|
2236
|
+
/**
|
|
2237
|
+
* Get thread by ID
|
|
2238
|
+
*/
|
|
2239
|
+
getThread(id) {
|
|
2240
|
+
return this.threads.get(id);
|
|
2241
|
+
}
|
|
2242
|
+
/**
|
|
2243
|
+
* Get all threads
|
|
2244
|
+
*/
|
|
2245
|
+
getAllThreads() {
|
|
2246
|
+
return Array.from(this.threads.values());
|
|
2247
|
+
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Add message to current thread
|
|
2250
|
+
*/
|
|
2251
|
+
addMessage(message) {
|
|
2252
|
+
if (!this.currentThread) {
|
|
2253
|
+
throw new Error("No active thread. Create a thread first.");
|
|
2254
|
+
}
|
|
2255
|
+
if (this.currentThread.status !== "active") {
|
|
2256
|
+
throw new Error(`Cannot add message to ${this.currentThread.status} thread`);
|
|
2257
|
+
}
|
|
2258
|
+
const tokens = estimateTokens(message.content);
|
|
2259
|
+
const fullMessage = {
|
|
2260
|
+
...message,
|
|
2261
|
+
timestamp: Date.now(),
|
|
2262
|
+
tokens
|
|
153
2263
|
};
|
|
2264
|
+
this.currentThread.messages.push(fullMessage);
|
|
2265
|
+
this.currentThread.tokenCount += tokens;
|
|
2266
|
+
this.log(`Message added: ${tokens} tokens (total: ${this.currentThread.tokenCount}/${this.currentThread.maxTokens})`);
|
|
2267
|
+
this.emit("thread:message_added", { thread: this.currentThread, message: fullMessage });
|
|
2268
|
+
if (this.shouldCloseThread()) {
|
|
2269
|
+
this.log("Thread threshold reached");
|
|
2270
|
+
this.emit("thread:threshold_reached", this.currentThread);
|
|
2271
|
+
if (this.options.autoClose) {
|
|
2272
|
+
this.log("Auto-closing thread due to threshold");
|
|
2273
|
+
this.closeThread().catch((err) => {
|
|
2274
|
+
this.log(`Error auto-closing thread: ${err}`);
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Check if current thread should be closed
|
|
2281
|
+
*/
|
|
2282
|
+
shouldCloseThread() {
|
|
2283
|
+
if (!this.currentThread)
|
|
2284
|
+
return false;
|
|
2285
|
+
if (this.currentThread.status !== "active")
|
|
2286
|
+
return false;
|
|
2287
|
+
return this.currentThread.tokenCount >= this.currentThread.maxTokens;
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Close current thread and compress
|
|
2291
|
+
*/
|
|
2292
|
+
async closeThread() {
|
|
2293
|
+
if (!this.currentThread) {
|
|
2294
|
+
throw new Error("No active thread to close");
|
|
2295
|
+
}
|
|
2296
|
+
if (this.currentThread.status !== "active") {
|
|
2297
|
+
throw new Error(`Thread is already ${this.currentThread.status}`);
|
|
2298
|
+
}
|
|
2299
|
+
this.log(`Closing thread: ${this.currentThread.id}`);
|
|
2300
|
+
const summary = await this.compressThread(this.currentThread);
|
|
2301
|
+
this.currentThread.status = "completed";
|
|
2302
|
+
this.currentThread.completedAt = Date.now();
|
|
2303
|
+
this.currentThread.summary = summary;
|
|
2304
|
+
this.log(`Thread closed and compressed: ${summary.compressionRatio.toFixed(2)}x compression`);
|
|
2305
|
+
this.emit("thread:closed", this.currentThread);
|
|
2306
|
+
this.emit("thread:compressed", { thread: this.currentThread, summary });
|
|
2307
|
+
return summary;
|
|
154
2308
|
}
|
|
155
|
-
|
|
2309
|
+
/**
|
|
2310
|
+
* Compress thread using MiroThinker strategy
|
|
2311
|
+
*/
|
|
2312
|
+
async compressThread(thread) {
|
|
2313
|
+
const messages = thread.messages.map((msg) => ({
|
|
2314
|
+
role: msg.role === "system" ? "assistant" : msg.role,
|
|
2315
|
+
content: msg.content,
|
|
2316
|
+
originalTokens: msg.tokens,
|
|
2317
|
+
compressed: false
|
|
2318
|
+
}));
|
|
2319
|
+
const compressed = this.compressor.compress(messages);
|
|
2320
|
+
const summaryContent = compressed.messages.filter((m) => m.role === "assistant").map((m) => m.content).join("\n\n---\n\n");
|
|
156
2321
|
return {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
2322
|
+
content: summaryContent,
|
|
2323
|
+
originalTokens: compressed.originalTokens,
|
|
2324
|
+
compressedTokens: compressed.compressedTokens,
|
|
2325
|
+
compressionRatio: compressed.compressionRatio,
|
|
2326
|
+
timestamp: Date.now()
|
|
161
2327
|
};
|
|
162
2328
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
2329
|
+
/**
|
|
2330
|
+
* Get thread chain (current thread + all ancestors)
|
|
2331
|
+
*/
|
|
2332
|
+
getThreadChain(threadId) {
|
|
2333
|
+
const chain = [];
|
|
2334
|
+
let thread = threadId ? this.threads.get(threadId) : this.currentThread;
|
|
2335
|
+
while (thread) {
|
|
2336
|
+
chain.unshift(thread);
|
|
2337
|
+
thread = thread.parentId ? this.threads.get(thread.parentId) : void 0;
|
|
2338
|
+
}
|
|
2339
|
+
return chain;
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Get combined context from thread chain
|
|
2343
|
+
* Returns compressed summaries from ancestor threads + current thread messages
|
|
2344
|
+
*/
|
|
2345
|
+
getCombinedContext(includeCurrentMessages = true) {
|
|
2346
|
+
const chain = this.getThreadChain();
|
|
2347
|
+
const parts = [];
|
|
2348
|
+
for (let i = 0; i < chain.length - 1; i++) {
|
|
2349
|
+
const thread = chain[i];
|
|
2350
|
+
if (thread.summary) {
|
|
2351
|
+
parts.push(`## Thread ${i + 1}: ${thread.topic}
|
|
2352
|
+
|
|
2353
|
+
${thread.summary.content}`);
|
|
169
2354
|
}
|
|
170
|
-
fs.writeFileSync(rcFile, "", "utf-8");
|
|
171
2355
|
}
|
|
172
|
-
|
|
173
|
-
${
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
2356
|
+
if (includeCurrentMessages && this.currentThread) {
|
|
2357
|
+
const currentMessages = this.currentThread.messages.map((m) => `**${m.role}**: ${m.content}`).join("\n\n");
|
|
2358
|
+
parts.push(`## Current Thread: ${this.currentThread.topic}
|
|
2359
|
+
|
|
2360
|
+
${currentMessages}`);
|
|
2361
|
+
}
|
|
2362
|
+
return parts.join("\n\n---\n\n");
|
|
2363
|
+
}
|
|
2364
|
+
/**
|
|
2365
|
+
* Archive old threads
|
|
2366
|
+
*/
|
|
2367
|
+
archiveThread(threadId) {
|
|
2368
|
+
const thread = this.threads.get(threadId);
|
|
2369
|
+
if (!thread) {
|
|
2370
|
+
throw new Error(`Thread not found: ${threadId}`);
|
|
2371
|
+
}
|
|
2372
|
+
if (thread.status === "active") {
|
|
2373
|
+
throw new Error("Cannot archive active thread. Close it first.");
|
|
2374
|
+
}
|
|
2375
|
+
thread.status = "archived";
|
|
2376
|
+
this.log(`Thread archived: ${threadId}`);
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* Get statistics
|
|
2380
|
+
*/
|
|
2381
|
+
getStats() {
|
|
2382
|
+
const threads = Array.from(this.threads.values());
|
|
2383
|
+
const completedWithSummary = threads.filter((t) => t.summary);
|
|
182
2384
|
return {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
2385
|
+
totalThreads: threads.length,
|
|
2386
|
+
activeThreads: threads.filter((t) => t.status === "active").length,
|
|
2387
|
+
completedThreads: threads.filter((t) => t.status === "completed").length,
|
|
2388
|
+
archivedThreads: threads.filter((t) => t.status === "archived").length,
|
|
2389
|
+
totalTokens: threads.reduce((sum, t) => sum + t.tokenCount, 0),
|
|
2390
|
+
averageCompressionRatio: completedWithSummary.length > 0 ? completedWithSummary.reduce((sum, t) => sum + (t.summary?.compressionRatio || 0), 0) / completedWithSummary.length : 0
|
|
188
2391
|
};
|
|
189
2392
|
}
|
|
2393
|
+
/**
|
|
2394
|
+
* Generate unique thread ID
|
|
2395
|
+
*/
|
|
2396
|
+
generateThreadId() {
|
|
2397
|
+
return `thread_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
2398
|
+
}
|
|
2399
|
+
/**
|
|
2400
|
+
* Debug logging
|
|
2401
|
+
*/
|
|
2402
|
+
log(message) {
|
|
2403
|
+
if (this.options.debug) {
|
|
2404
|
+
console.log(`[ThreadManager] ${message}`);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
190
2407
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
2408
|
+
|
|
2409
|
+
class ContextManager extends EventEmitter {
|
|
2410
|
+
sessionManager;
|
|
2411
|
+
summarizer;
|
|
2412
|
+
configManager;
|
|
2413
|
+
storageManager;
|
|
2414
|
+
// New managers for Phase 1
|
|
2415
|
+
threadManager;
|
|
2416
|
+
planAcceptanceManager;
|
|
2417
|
+
autoSummarizeManager;
|
|
2418
|
+
options;
|
|
2419
|
+
initialized = false;
|
|
2420
|
+
currentStorageSession = null;
|
|
2421
|
+
messageHistory = [];
|
|
2422
|
+
totalMessages = 0;
|
|
2423
|
+
lastCompressionTime = null;
|
|
2424
|
+
compressedTokens = 0;
|
|
2425
|
+
/**
|
|
2426
|
+
* Create a new Context Manager instance
|
|
2427
|
+
*
|
|
2428
|
+
* @param options - Configuration options
|
|
2429
|
+
*/
|
|
2430
|
+
constructor(options = {}) {
|
|
2431
|
+
super();
|
|
2432
|
+
this.options = {
|
|
2433
|
+
configPath: options.configPath,
|
|
2434
|
+
autoCompress: options.autoCompress ?? true,
|
|
2435
|
+
compressionThreshold: options.compressionThreshold ?? 0.8,
|
|
2436
|
+
maxHistoryLength: options.maxHistoryLength ?? 100,
|
|
2437
|
+
storageBaseDir: options.storageBaseDir,
|
|
2438
|
+
debug: options.debug ?? false
|
|
200
2439
|
};
|
|
2440
|
+
this.configManager = new ConfigManager(this.options.configPath);
|
|
2441
|
+
this.storageManager = new StorageManager(this.options.storageBaseDir);
|
|
2442
|
+
this.sessionManager = new SessionManager();
|
|
2443
|
+
this.summarizer = new Summarizer();
|
|
2444
|
+
this.setupEventForwarding();
|
|
201
2445
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
2446
|
+
/**
|
|
2447
|
+
* Initialize all subsystems
|
|
2448
|
+
* Must be called before using the manager
|
|
2449
|
+
*/
|
|
2450
|
+
async initialize() {
|
|
2451
|
+
if (this.initialized) {
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
try {
|
|
2455
|
+
this.debug("Initializing Context Manager...");
|
|
2456
|
+
const config = await this.configManager.load();
|
|
2457
|
+
this.debug("Configuration loaded:", config);
|
|
2458
|
+
await this.storageManager.initialize();
|
|
2459
|
+
this.debug("Storage initialized");
|
|
2460
|
+
this.sessionManager.updateConfig({
|
|
2461
|
+
contextThreshold: config.contextThreshold / config.maxContextTokens,
|
|
2462
|
+
maxContextTokens: config.maxContextTokens,
|
|
2463
|
+
summaryModel: config.summaryModel,
|
|
2464
|
+
autoSummarize: config.autoSummarize
|
|
2465
|
+
});
|
|
2466
|
+
this.summarizer.updateConfig({
|
|
2467
|
+
model: config.summaryModel
|
|
2468
|
+
});
|
|
2469
|
+
this.initialized = true;
|
|
2470
|
+
this.debug("Context Manager initialized successfully");
|
|
2471
|
+
} catch (error) {
|
|
2472
|
+
const errorMsg = `Failed to initialize Context Manager: ${error instanceof Error ? error.message : String(error)}`;
|
|
2473
|
+
this.emit("error", new Error(errorMsg));
|
|
2474
|
+
throw new Error(errorMsg);
|
|
2475
|
+
}
|
|
211
2476
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
2477
|
+
/**
|
|
2478
|
+
* Start a new session or resume existing session
|
|
2479
|
+
*
|
|
2480
|
+
* @param projectPath - Absolute path to project directory
|
|
2481
|
+
* @returns Session information
|
|
2482
|
+
*/
|
|
2483
|
+
async startSession(projectPath) {
|
|
2484
|
+
this.ensureInitialized();
|
|
2485
|
+
try {
|
|
2486
|
+
const nodeProcess = await import('node:process');
|
|
2487
|
+
const path = projectPath || nodeProcess.cwd();
|
|
2488
|
+
this.debug(`Starting session for project: ${path}`);
|
|
2489
|
+
const sessionManagerSession = this.sessionManager.createSession(path);
|
|
2490
|
+
this.currentStorageSession = await this.storageManager.createSession(
|
|
2491
|
+
path,
|
|
2492
|
+
"Context compression session"
|
|
2493
|
+
);
|
|
2494
|
+
this.messageHistory = [];
|
|
2495
|
+
this.emit("session:start", {
|
|
2496
|
+
sessionId: sessionManagerSession.id,
|
|
2497
|
+
projectPath: path
|
|
2498
|
+
});
|
|
2499
|
+
this.debug(`Session started: ${sessionManagerSession.id}`);
|
|
2500
|
+
return sessionManagerSession;
|
|
2501
|
+
} catch (error) {
|
|
2502
|
+
const errorMsg = `Failed to start session: ${error instanceof Error ? error.message : String(error)}`;
|
|
2503
|
+
this.emit("error", new Error(errorMsg));
|
|
2504
|
+
throw new Error(errorMsg);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
/**
|
|
2508
|
+
* Add a message to the current session
|
|
2509
|
+
*
|
|
2510
|
+
* @param message - Message to add
|
|
2511
|
+
*/
|
|
2512
|
+
async addMessage(message) {
|
|
2513
|
+
this.ensureInitialized();
|
|
2514
|
+
const currentSession = this.sessionManager.getCurrentSession();
|
|
2515
|
+
if (!currentSession) {
|
|
2516
|
+
throw new Error("No active session. Call startSession() first.");
|
|
2517
|
+
}
|
|
2518
|
+
try {
|
|
2519
|
+
const timestampedMessage = {
|
|
2520
|
+
...message,
|
|
2521
|
+
timestamp: message.timestamp || Date.now()
|
|
2522
|
+
};
|
|
2523
|
+
this.messageHistory.push(timestampedMessage);
|
|
2524
|
+
this.totalMessages++;
|
|
2525
|
+
const tokens = estimateTokens(message.content);
|
|
2526
|
+
if (message.metadata?.isFunctionCall) {
|
|
2527
|
+
await this.sessionManager.addFunctionCall(
|
|
2528
|
+
message.metadata.functionName,
|
|
2529
|
+
message.metadata.arguments,
|
|
2530
|
+
message.content
|
|
2531
|
+
);
|
|
2532
|
+
}
|
|
2533
|
+
this.emit("message:added", {
|
|
2534
|
+
sessionId: currentSession.id,
|
|
2535
|
+
message: timestampedMessage,
|
|
2536
|
+
tokens
|
|
2537
|
+
});
|
|
2538
|
+
if (this.options.autoCompress && this.shouldCompress()) {
|
|
2539
|
+
this.debug("Auto-compression threshold reached");
|
|
2540
|
+
await this.compress();
|
|
2541
|
+
}
|
|
2542
|
+
if (this.messageHistory.length > this.options.maxHistoryLength) {
|
|
2543
|
+
const removed = this.messageHistory.splice(
|
|
2544
|
+
0,
|
|
2545
|
+
this.messageHistory.length - this.options.maxHistoryLength
|
|
2546
|
+
);
|
|
2547
|
+
this.debug(`Trimmed ${removed.length} messages from history`);
|
|
2548
|
+
}
|
|
2549
|
+
} catch (error) {
|
|
2550
|
+
const errorMsg = `Failed to add message: ${error instanceof Error ? error.message : String(error)}`;
|
|
2551
|
+
this.emit("error", new Error(errorMsg));
|
|
2552
|
+
throw new Error(errorMsg);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
/**
|
|
2556
|
+
* Check if compression should be triggered
|
|
2557
|
+
*
|
|
2558
|
+
* @returns True if compression is recommended
|
|
2559
|
+
*/
|
|
2560
|
+
shouldCompress() {
|
|
2561
|
+
this.ensureInitialized();
|
|
2562
|
+
const currentSession = this.sessionManager.getCurrentSession();
|
|
2563
|
+
if (!currentSession) {
|
|
2564
|
+
return false;
|
|
2565
|
+
}
|
|
2566
|
+
const isExceeded = this.sessionManager.isThresholdExceeded();
|
|
2567
|
+
if (isExceeded) {
|
|
2568
|
+
this.emit("threshold:reached", {
|
|
2569
|
+
sessionId: currentSession.id,
|
|
2570
|
+
usage: this.sessionManager.getContextUsage(),
|
|
2571
|
+
remaining: this.sessionManager.getRemainingTokens()
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
return isExceeded;
|
|
2575
|
+
}
|
|
2576
|
+
/**
|
|
2577
|
+
* Execute compression on current session
|
|
2578
|
+
*
|
|
2579
|
+
* @returns Summary of compression
|
|
2580
|
+
*/
|
|
2581
|
+
async compress() {
|
|
2582
|
+
this.ensureInitialized();
|
|
2583
|
+
const currentSession = this.sessionManager.getCurrentSession();
|
|
2584
|
+
if (!currentSession) {
|
|
2585
|
+
throw new Error("No active session to compress");
|
|
2586
|
+
}
|
|
2587
|
+
try {
|
|
2588
|
+
this.debug("Starting compression...");
|
|
2589
|
+
this.emit("compression:start", {
|
|
2590
|
+
sessionId: currentSession.id,
|
|
2591
|
+
tokenCount: currentSession.tokenCount
|
|
2592
|
+
});
|
|
2593
|
+
const summaryContent = this.sessionManager.generateSessionSummary();
|
|
2594
|
+
const originalTokens = currentSession.tokenCount;
|
|
2595
|
+
const compressedTokens = estimateTokens(summaryContent);
|
|
2596
|
+
const compressionRatio = compressedTokens / originalTokens;
|
|
2597
|
+
if (this.currentStorageSession) {
|
|
2598
|
+
await this.storageManager.saveSummary(
|
|
2599
|
+
this.currentStorageSession.meta.id,
|
|
2600
|
+
summaryContent,
|
|
2601
|
+
this.currentStorageSession.meta.projectHash
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
this.compressedTokens += originalTokens - compressedTokens;
|
|
2605
|
+
this.lastCompressionTime = Date.now();
|
|
2606
|
+
const summary = {
|
|
2607
|
+
content: summaryContent,
|
|
2608
|
+
originalTokens,
|
|
2609
|
+
compressedTokens,
|
|
2610
|
+
compressionRatio,
|
|
2611
|
+
fcCount: currentSession.fcCount,
|
|
2612
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
2613
|
+
};
|
|
2614
|
+
this.emit("compression:complete", {
|
|
2615
|
+
sessionId: currentSession.id,
|
|
2616
|
+
summary
|
|
2617
|
+
});
|
|
2618
|
+
this.debug(`Compression complete. Ratio: ${(compressionRatio * 100).toFixed(1)}%`);
|
|
2619
|
+
return summary;
|
|
2620
|
+
} catch (error) {
|
|
2621
|
+
const errorMsg = `Failed to compress context: ${error instanceof Error ? error.message : String(error)}`;
|
|
2622
|
+
this.emit("error", new Error(errorMsg));
|
|
2623
|
+
throw new Error(errorMsg);
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
/**
|
|
2627
|
+
* Get optimized context for new conversation
|
|
2628
|
+
* Returns compressed summary if available, otherwise full context
|
|
2629
|
+
*
|
|
2630
|
+
* @returns Optimized context string
|
|
2631
|
+
*/
|
|
2632
|
+
async getOptimizedContext() {
|
|
2633
|
+
this.ensureInitialized();
|
|
2634
|
+
const currentSession = this.sessionManager.getCurrentSession();
|
|
2635
|
+
if (!currentSession) {
|
|
2636
|
+
return "";
|
|
2637
|
+
}
|
|
2638
|
+
try {
|
|
2639
|
+
if (this.currentStorageSession) {
|
|
2640
|
+
const summary = await this.storageManager.getSummary(
|
|
2641
|
+
this.currentStorageSession.meta.id,
|
|
2642
|
+
this.currentStorageSession.meta.projectHash
|
|
2643
|
+
);
|
|
2644
|
+
if (summary) {
|
|
2645
|
+
this.debug("Using saved summary for context");
|
|
2646
|
+
return summary;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
this.debug("Generating fresh summary for context");
|
|
2650
|
+
return this.sessionManager.generateSessionSummary();
|
|
2651
|
+
} catch (error) {
|
|
2652
|
+
this.debug(`Failed to get optimized context: ${error}`);
|
|
2653
|
+
return this.sessionManager.generateSessionSummary();
|
|
2654
|
+
}
|
|
219
2655
|
}
|
|
220
|
-
|
|
2656
|
+
/**
|
|
2657
|
+
* Get current statistics
|
|
2658
|
+
*
|
|
2659
|
+
* @returns Context statistics
|
|
2660
|
+
*/
|
|
2661
|
+
getStats() {
|
|
2662
|
+
this.ensureInitialized();
|
|
2663
|
+
const currentSession = this.sessionManager.getCurrentSession();
|
|
2664
|
+
const allSessions = this.sessionManager.getAllSessions();
|
|
221
2665
|
return {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
2666
|
+
currentTokens: currentSession?.tokenCount || 0,
|
|
2667
|
+
compressedTokens: this.compressedTokens,
|
|
2668
|
+
compressionRatio: currentSession ? this.compressedTokens / (currentSession.tokenCount + this.compressedTokens) : 0,
|
|
2669
|
+
sessionCount: allSessions.length,
|
|
2670
|
+
totalMessages: this.totalMessages,
|
|
2671
|
+
lastCompression: this.lastCompressionTime,
|
|
2672
|
+
thresholdLevel: this.sessionManager.getThresholdLevel(),
|
|
2673
|
+
contextUsage: this.sessionManager.getContextUsage()
|
|
226
2674
|
};
|
|
227
2675
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
2676
|
+
/**
|
|
2677
|
+
* End current session
|
|
2678
|
+
*
|
|
2679
|
+
* @returns Completed session or null
|
|
2680
|
+
*/
|
|
2681
|
+
async endSession() {
|
|
2682
|
+
this.ensureInitialized();
|
|
2683
|
+
const currentSession = this.sessionManager.getCurrentSession();
|
|
2684
|
+
if (!currentSession) {
|
|
2685
|
+
return null;
|
|
2686
|
+
}
|
|
2687
|
+
try {
|
|
2688
|
+
this.debug(`Ending session: ${currentSession.id}`);
|
|
2689
|
+
const completedSession = this.sessionManager.completeSession();
|
|
2690
|
+
if (this.currentStorageSession) {
|
|
2691
|
+
await this.storageManager.completeSession(
|
|
2692
|
+
this.currentStorageSession.meta.id,
|
|
2693
|
+
this.currentStorageSession.meta.projectHash
|
|
2694
|
+
);
|
|
2695
|
+
}
|
|
2696
|
+
this.emit("session:end", {
|
|
2697
|
+
sessionId: currentSession.id,
|
|
2698
|
+
summary: this.sessionManager.generateSessionSummary()
|
|
2699
|
+
});
|
|
2700
|
+
this.currentStorageSession = null;
|
|
2701
|
+
this.debug("Session ended successfully");
|
|
2702
|
+
return completedSession;
|
|
2703
|
+
} catch (error) {
|
|
2704
|
+
const errorMsg = `Failed to end session: ${error instanceof Error ? error.message : String(error)}`;
|
|
2705
|
+
this.emit("error", new Error(errorMsg));
|
|
2706
|
+
throw new Error(errorMsg);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Get all sessions for a project
|
|
2711
|
+
*
|
|
2712
|
+
* @param projectPath - Project path
|
|
2713
|
+
* @returns Array of session metadata
|
|
2714
|
+
*/
|
|
2715
|
+
async getProjectSessions(projectPath) {
|
|
2716
|
+
this.ensureInitialized();
|
|
2717
|
+
try {
|
|
2718
|
+
const sessions = await this.storageManager.listSessions({ projectHash: void 0 });
|
|
2719
|
+
return sessions.filter((s) => s.projectPath === projectPath);
|
|
2720
|
+
} catch (error) {
|
|
2721
|
+
this.debug(`Failed to get project sessions: ${error}`);
|
|
2722
|
+
return [];
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
/**
|
|
2726
|
+
* Clean up old sessions
|
|
2727
|
+
*
|
|
2728
|
+
* @param maxAgeDays - Maximum age in days
|
|
2729
|
+
* @returns Cleanup result
|
|
2730
|
+
*/
|
|
2731
|
+
async cleanupOldSessions(maxAgeDays = 30) {
|
|
2732
|
+
this.ensureInitialized();
|
|
2733
|
+
try {
|
|
2734
|
+
const maxAge = maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
2735
|
+
const result = await this.storageManager.cleanOldSessions(maxAge);
|
|
2736
|
+
this.debug(`Cleaned up ${result.sessionsRemoved} sessions, freed ${result.bytesFreed} bytes`);
|
|
2737
|
+
return {
|
|
2738
|
+
sessionsRemoved: result.sessionsRemoved,
|
|
2739
|
+
bytesFreed: result.bytesFreed
|
|
2740
|
+
};
|
|
2741
|
+
} catch (error) {
|
|
2742
|
+
this.debug(`Failed to cleanup sessions: ${error}`);
|
|
2743
|
+
return { sessionsRemoved: 0, bytesFreed: 0 };
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Update configuration
|
|
2748
|
+
*
|
|
2749
|
+
* @param updates - Partial configuration updates
|
|
2750
|
+
*/
|
|
2751
|
+
async updateConfig(updates) {
|
|
2752
|
+
this.ensureInitialized();
|
|
2753
|
+
try {
|
|
2754
|
+
Object.assign(this.options, updates);
|
|
2755
|
+
if (updates.compressionThreshold !== void 0) {
|
|
2756
|
+
const config = await this.configManager.get();
|
|
2757
|
+
await this.configManager.update({
|
|
2758
|
+
contextThreshold: updates.compressionThreshold * config.maxContextTokens
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
this.debug("Configuration updated");
|
|
2762
|
+
} catch (error) {
|
|
2763
|
+
const errorMsg = `Failed to update config: ${error instanceof Error ? error.message : String(error)}`;
|
|
2764
|
+
this.emit("error", new Error(errorMsg));
|
|
2765
|
+
throw new Error(errorMsg);
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
/**
|
|
2769
|
+
* Clean up resources
|
|
2770
|
+
* Should be called when shutting down
|
|
2771
|
+
*/
|
|
2772
|
+
async cleanup() {
|
|
2773
|
+
try {
|
|
2774
|
+
this.debug("Cleaning up Context Manager...");
|
|
2775
|
+
const currentSession = this.sessionManager.getCurrentSession();
|
|
2776
|
+
if (currentSession) {
|
|
2777
|
+
await this.endSession();
|
|
2778
|
+
}
|
|
2779
|
+
this.messageHistory = [];
|
|
2780
|
+
this.removeAllListeners();
|
|
2781
|
+
this.initialized = false;
|
|
2782
|
+
this.debug("Context Manager cleaned up");
|
|
2783
|
+
} catch (error) {
|
|
2784
|
+
this.debug(`Cleanup error: ${error}`);
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
/**
|
|
2788
|
+
* Setup event forwarding from session manager
|
|
2789
|
+
*/
|
|
2790
|
+
setupEventForwarding() {
|
|
2791
|
+
this.sessionManager.on("session_event", (event) => {
|
|
2792
|
+
this.debug(`Session event: ${event.type}`);
|
|
2793
|
+
const eventMap = {
|
|
2794
|
+
session_created: "session:start",
|
|
2795
|
+
session_completed: "session:end",
|
|
2796
|
+
threshold_warning: "threshold:reached",
|
|
2797
|
+
threshold_critical: "threshold:reached",
|
|
2798
|
+
fc_summarized: null,
|
|
2799
|
+
// Internal event, don't forward
|
|
2800
|
+
session_archived: null
|
|
2801
|
+
// Internal event, don't forward
|
|
2802
|
+
};
|
|
2803
|
+
const contextEvent = eventMap[event.type];
|
|
2804
|
+
if (contextEvent) {
|
|
2805
|
+
this.emit(contextEvent, event.data);
|
|
2806
|
+
}
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* Ensure manager is initialized
|
|
2811
|
+
* @throws Error if not initialized
|
|
2812
|
+
*/
|
|
2813
|
+
ensureInitialized() {
|
|
2814
|
+
if (!this.initialized) {
|
|
2815
|
+
throw new Error("Context Manager not initialized. Call initialize() first.");
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
/**
|
|
2819
|
+
* Debug logging helper
|
|
2820
|
+
*/
|
|
2821
|
+
debug(...args) {
|
|
2822
|
+
if (this.options.debug) {
|
|
2823
|
+
console.log("[ContextManager]", ...args);
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
/**
|
|
2827
|
+
* Get current session (for testing/debugging)
|
|
2828
|
+
*/
|
|
2829
|
+
getCurrentSession() {
|
|
2830
|
+
return this.sessionManager.getCurrentSession();
|
|
2831
|
+
}
|
|
2832
|
+
/**
|
|
2833
|
+
* Get storage manager (for advanced usage)
|
|
2834
|
+
*/
|
|
2835
|
+
getStorageManager() {
|
|
2836
|
+
return this.storageManager;
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* Get session manager (for advanced usage)
|
|
2840
|
+
*/
|
|
2841
|
+
getSessionManager() {
|
|
2842
|
+
return this.sessionManager;
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* Get summarizer (for advanced usage)
|
|
2846
|
+
*/
|
|
2847
|
+
getSummarizer() {
|
|
2848
|
+
return this.summarizer;
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Get config manager (for advanced usage)
|
|
2852
|
+
*/
|
|
2853
|
+
getConfigManager() {
|
|
2854
|
+
return this.configManager;
|
|
2855
|
+
}
|
|
2856
|
+
// ============================================================
|
|
2857
|
+
// Phase 1: New Manager Integration Methods
|
|
2858
|
+
// ============================================================
|
|
2859
|
+
/**
|
|
2860
|
+
* Enable thread-based interaction mode
|
|
2861
|
+
*
|
|
2862
|
+
* @param options - Thread manager options
|
|
2863
|
+
* @returns Thread manager instance
|
|
2864
|
+
*/
|
|
2865
|
+
enableThreadMode(options) {
|
|
2866
|
+
if (this.threadManager) {
|
|
2867
|
+
this.debug("Thread mode already enabled");
|
|
2868
|
+
return this.threadManager;
|
|
2869
|
+
}
|
|
2870
|
+
this.threadManager = new ThreadManager({
|
|
2871
|
+
...options,
|
|
2872
|
+
debug: options?.debug ?? this.options.debug
|
|
2873
|
+
});
|
|
2874
|
+
this.threadManager.on("thread:created", (thread) => {
|
|
2875
|
+
this.emit("thread:created", thread);
|
|
2876
|
+
});
|
|
2877
|
+
this.threadManager.on("thread:closed", (thread) => {
|
|
2878
|
+
this.emit("thread:closed", thread);
|
|
2879
|
+
});
|
|
2880
|
+
this.threadManager.on("thread:compressed", (data) => {
|
|
2881
|
+
this.emit("thread:compressed", data);
|
|
2882
|
+
});
|
|
2883
|
+
this.debug("Thread mode enabled");
|
|
2884
|
+
return this.threadManager;
|
|
2885
|
+
}
|
|
2886
|
+
/**
|
|
2887
|
+
* Enable plan acceptance integration
|
|
2888
|
+
*
|
|
2889
|
+
* @param options - Plan acceptance options
|
|
2890
|
+
* @returns Plan acceptance manager instance
|
|
2891
|
+
*/
|
|
2892
|
+
enablePlanAcceptance(options) {
|
|
2893
|
+
if (this.planAcceptanceManager) {
|
|
2894
|
+
this.debug("Plan acceptance already enabled");
|
|
2895
|
+
return this.planAcceptanceManager;
|
|
2896
|
+
}
|
|
2897
|
+
this.planAcceptanceManager = new PlanAcceptanceManager(this, {
|
|
2898
|
+
...options,
|
|
2899
|
+
debug: options?.debug ?? this.options.debug
|
|
2900
|
+
});
|
|
2901
|
+
if (this.threadManager) {
|
|
2902
|
+
this.planAcceptanceManager.setThreadManager(this.threadManager);
|
|
2903
|
+
}
|
|
2904
|
+
this.planAcceptanceManager.on("plan:accepted", (plan) => {
|
|
2905
|
+
this.emit("plan:accepted", plan);
|
|
2906
|
+
});
|
|
2907
|
+
this.planAcceptanceManager.on("plan:compression_completed", (summary) => {
|
|
2908
|
+
this.emit("plan:compression_completed", summary);
|
|
2909
|
+
});
|
|
2910
|
+
this.planAcceptanceManager.on("plan:rate_limited", (data) => {
|
|
2911
|
+
this.emit("plan:rate_limited", data);
|
|
2912
|
+
});
|
|
2913
|
+
this.debug("Plan acceptance enabled");
|
|
2914
|
+
return this.planAcceptanceManager;
|
|
2915
|
+
}
|
|
2916
|
+
/**
|
|
2917
|
+
* Enable auto-summarization
|
|
2918
|
+
*
|
|
2919
|
+
* @param options - Auto-summarize options
|
|
2920
|
+
* @returns Auto-summarize manager instance
|
|
2921
|
+
*/
|
|
2922
|
+
enableAutoSummarize(options) {
|
|
2923
|
+
if (this.autoSummarizeManager) {
|
|
2924
|
+
this.debug("Auto-summarize already enabled");
|
|
2925
|
+
return this.autoSummarizeManager;
|
|
2926
|
+
}
|
|
2927
|
+
this.autoSummarizeManager = new AutoSummarizeManager(this, {
|
|
2928
|
+
...options,
|
|
2929
|
+
debug: options?.debug ?? this.options.debug
|
|
2930
|
+
});
|
|
2931
|
+
this.autoSummarizeManager.on("summarize:completed", (summary) => {
|
|
2932
|
+
this.emit("summarize:completed", summary);
|
|
2933
|
+
});
|
|
2934
|
+
this.autoSummarizeManager.on("summarize:rate_limited", (data) => {
|
|
2935
|
+
this.emit("summarize:rate_limited", data);
|
|
2936
|
+
});
|
|
2937
|
+
this.debug("Auto-summarize enabled");
|
|
2938
|
+
return this.autoSummarizeManager;
|
|
2939
|
+
}
|
|
2940
|
+
/**
|
|
2941
|
+
* Get thread manager instance
|
|
2942
|
+
*/
|
|
2943
|
+
getThreadManager() {
|
|
2944
|
+
return this.threadManager;
|
|
2945
|
+
}
|
|
2946
|
+
/**
|
|
2947
|
+
* Get plan acceptance manager instance
|
|
2948
|
+
*/
|
|
2949
|
+
getPlanAcceptanceManager() {
|
|
2950
|
+
return this.planAcceptanceManager;
|
|
2951
|
+
}
|
|
2952
|
+
/**
|
|
2953
|
+
* Get auto-summarize manager instance
|
|
2954
|
+
*/
|
|
2955
|
+
getAutoSummarizeManager() {
|
|
2956
|
+
return this.autoSummarizeManager;
|
|
2957
|
+
}
|
|
2958
|
+
/**
|
|
2959
|
+
* Handle plan acceptance (convenience method)
|
|
2960
|
+
*
|
|
2961
|
+
* @param plan - Plan to accept
|
|
2962
|
+
*/
|
|
2963
|
+
async acceptPlan(plan) {
|
|
2964
|
+
if (!this.planAcceptanceManager) {
|
|
2965
|
+
throw new Error("Plan acceptance not enabled. Call enablePlanAcceptance() first.");
|
|
2966
|
+
}
|
|
2967
|
+
await this.planAcceptanceManager.onPlanAccepted(plan);
|
|
2968
|
+
}
|
|
2969
|
+
/**
|
|
2970
|
+
* Get all messages in current session
|
|
2971
|
+
*
|
|
2972
|
+
* @returns Array of messages
|
|
2973
|
+
*/
|
|
2974
|
+
async getMessages() {
|
|
2975
|
+
return [...this.messageHistory];
|
|
2976
|
+
}
|
|
2977
|
+
/**
|
|
2978
|
+
* Store a summary
|
|
2979
|
+
*
|
|
2980
|
+
* @param summary - Summary to store
|
|
2981
|
+
*/
|
|
2982
|
+
async storeSummary(summary) {
|
|
2983
|
+
if (!this.currentStorageSession) {
|
|
2984
|
+
throw new Error("No active session. Call startSession() first.");
|
|
2985
|
+
}
|
|
2986
|
+
await this.storageManager.saveSummary(
|
|
2987
|
+
this.currentStorageSession.meta.id,
|
|
2988
|
+
summary.content
|
|
2989
|
+
);
|
|
2990
|
+
this.debug("Summary stored");
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* Reset context manager state
|
|
2994
|
+
* Clears message history and resets counters
|
|
2995
|
+
*/
|
|
2996
|
+
async reset() {
|
|
2997
|
+
this.messageHistory = [];
|
|
2998
|
+
this.totalMessages = 0;
|
|
2999
|
+
this.lastCompressionTime = null;
|
|
3000
|
+
this.compressedTokens = 0;
|
|
3001
|
+
if (this.threadManager) {
|
|
3002
|
+
const currentThread = this.threadManager.getCurrentThread();
|
|
3003
|
+
if (currentThread && currentThread.status === "active") {
|
|
3004
|
+
await this.threadManager.closeThread();
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
this.debug("Context manager reset");
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
function createContextManager(options) {
|
|
3011
|
+
return new ContextManager(options);
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
class ClaudeHistoryMonitor {
|
|
3015
|
+
historyFile;
|
|
3016
|
+
watcher = null;
|
|
3017
|
+
contextManager;
|
|
3018
|
+
lastPosition = 0;
|
|
3019
|
+
options;
|
|
3020
|
+
isRunning = false;
|
|
3021
|
+
pollingTimer = null;
|
|
3022
|
+
constructor(contextManager, options = {}) {
|
|
3023
|
+
this.historyFile = join(homedir(), ".claude", "history.jsonl");
|
|
3024
|
+
this.contextManager = contextManager;
|
|
3025
|
+
this.options = {
|
|
3026
|
+
debug: options.debug ?? false,
|
|
3027
|
+
pollingInterval: options.pollingInterval ?? 1e3
|
|
238
3028
|
};
|
|
239
|
-
}
|
|
3029
|
+
}
|
|
3030
|
+
/**
|
|
3031
|
+
* Start monitoring
|
|
3032
|
+
*/
|
|
3033
|
+
start() {
|
|
3034
|
+
if (this.isRunning) {
|
|
3035
|
+
this.log("Already running");
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
if (!existsSync(this.historyFile)) {
|
|
3039
|
+
this.log(`History file not found: ${this.historyFile}`);
|
|
3040
|
+
this.log("Will start monitoring when file is created");
|
|
3041
|
+
} else {
|
|
3042
|
+
this.lastPosition = statSync(this.historyFile).size;
|
|
3043
|
+
this.log(`Starting from position: ${this.lastPosition}`);
|
|
3044
|
+
}
|
|
3045
|
+
try {
|
|
3046
|
+
this.watcher = watch(this.historyFile, async (event) => {
|
|
3047
|
+
if (event === "change") {
|
|
3048
|
+
await this.processNewEntries();
|
|
3049
|
+
}
|
|
3050
|
+
});
|
|
3051
|
+
this.log("Using fs.watch for monitoring");
|
|
3052
|
+
} catch (error) {
|
|
3053
|
+
this.log("fs.watch failed, falling back to polling");
|
|
3054
|
+
this.startPolling();
|
|
3055
|
+
}
|
|
3056
|
+
this.isRunning = true;
|
|
3057
|
+
this.log("\u{1F4CA} Claude History Monitor started");
|
|
3058
|
+
}
|
|
3059
|
+
/**
|
|
3060
|
+
* Start polling (fallback method)
|
|
3061
|
+
*/
|
|
3062
|
+
startPolling() {
|
|
3063
|
+
this.pollingTimer = setInterval(async () => {
|
|
3064
|
+
await this.processNewEntries();
|
|
3065
|
+
}, this.options.pollingInterval);
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* Process new entries in history file
|
|
3069
|
+
*/
|
|
3070
|
+
async processNewEntries() {
|
|
3071
|
+
try {
|
|
3072
|
+
if (!existsSync(this.historyFile)) {
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
const currentSize = statSync(this.historyFile).size;
|
|
3076
|
+
if (currentSize <= this.lastPosition) {
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
3079
|
+
if (currentSize < this.lastPosition) {
|
|
3080
|
+
this.log("File was truncated, resetting position");
|
|
3081
|
+
this.lastPosition = 0;
|
|
3082
|
+
}
|
|
3083
|
+
const newContent = await this.readNewContent(this.lastPosition, currentSize);
|
|
3084
|
+
this.lastPosition = currentSize;
|
|
3085
|
+
const lines = newContent.split("\n").filter((line) => line.trim());
|
|
3086
|
+
for (const line of lines) {
|
|
3087
|
+
try {
|
|
3088
|
+
const entry = JSON.parse(line);
|
|
3089
|
+
await this.processHistoryEntry(entry);
|
|
3090
|
+
} catch (error) {
|
|
3091
|
+
this.log(`Failed to parse entry: ${error}`);
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
} catch (error) {
|
|
3095
|
+
this.log(`Error processing entries: ${error}`);
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
/**
|
|
3099
|
+
* Read new content from file
|
|
3100
|
+
*/
|
|
3101
|
+
async readNewContent(start, end) {
|
|
3102
|
+
return new Promise((resolve, reject) => {
|
|
3103
|
+
const chunks = [];
|
|
3104
|
+
const stream = createReadStream(this.historyFile, {
|
|
3105
|
+
start,
|
|
3106
|
+
end: end - 1,
|
|
3107
|
+
encoding: "utf-8"
|
|
3108
|
+
});
|
|
3109
|
+
stream.on("data", (chunk) => {
|
|
3110
|
+
chunks.push(Buffer.from(chunk));
|
|
3111
|
+
});
|
|
3112
|
+
stream.on("end", () => {
|
|
3113
|
+
resolve(Buffer.concat(chunks).toString("utf-8"));
|
|
3114
|
+
});
|
|
3115
|
+
stream.on("error", reject);
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
/**
|
|
3119
|
+
* Process a single history entry
|
|
3120
|
+
*/
|
|
3121
|
+
async processHistoryEntry(entry) {
|
|
3122
|
+
this.log(`Processing entry: ${entry.display.substring(0, 50)}...`);
|
|
3123
|
+
await this.contextManager.addMessage({
|
|
3124
|
+
role: "user",
|
|
3125
|
+
content: entry.display || "",
|
|
3126
|
+
timestamp: entry.timestamp,
|
|
3127
|
+
metadata: {
|
|
3128
|
+
sessionId: entry.sessionId,
|
|
3129
|
+
project: entry.project,
|
|
3130
|
+
pastedContents: entry.pastedContents
|
|
3131
|
+
}
|
|
3132
|
+
});
|
|
3133
|
+
const autoSummarize = this.contextManager.getAutoSummarizeManager();
|
|
3134
|
+
if (autoSummarize) {
|
|
3135
|
+
const stats = await this.contextManager.getStats();
|
|
3136
|
+
if (autoSummarize.shouldSummarize(stats.currentTokens)) {
|
|
3137
|
+
this.log(`\u{1F916} Triggering auto-summarization (${stats.currentTokens} tokens)`);
|
|
3138
|
+
const result = await autoSummarize.summarize();
|
|
3139
|
+
if (result.performed) {
|
|
3140
|
+
this.log(`\u2705 Compressed: ${result.summary.originalTokens} \u2192 ${result.summary.compressedTokens} tokens`);
|
|
3141
|
+
} else {
|
|
3142
|
+
this.log(`\u23F3 Summarization skipped: ${result.reason}`);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
/**
|
|
3148
|
+
* Stop monitoring
|
|
3149
|
+
*/
|
|
3150
|
+
stop() {
|
|
3151
|
+
if (!this.isRunning) {
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
if (this.watcher) {
|
|
3155
|
+
this.watcher.close();
|
|
3156
|
+
this.watcher = null;
|
|
3157
|
+
}
|
|
3158
|
+
if (this.pollingTimer) {
|
|
3159
|
+
clearInterval(this.pollingTimer);
|
|
3160
|
+
this.pollingTimer = null;
|
|
3161
|
+
}
|
|
3162
|
+
this.isRunning = false;
|
|
3163
|
+
this.log("\u{1F4CA} Claude History Monitor stopped");
|
|
3164
|
+
}
|
|
3165
|
+
/**
|
|
3166
|
+
* Get current status
|
|
3167
|
+
*/
|
|
3168
|
+
getStatus() {
|
|
240
3169
|
return {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
message: "Failed to uninstall shell hook",
|
|
245
|
-
error: error instanceof Error ? error.message : String(error)
|
|
3170
|
+
isRunning: this.isRunning,
|
|
3171
|
+
lastPosition: this.lastPosition,
|
|
3172
|
+
historyFile: this.historyFile
|
|
246
3173
|
};
|
|
247
3174
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (!rcFile) {
|
|
256
|
-
return null;
|
|
3175
|
+
/**
|
|
3176
|
+
* Debug logging
|
|
3177
|
+
*/
|
|
3178
|
+
log(message) {
|
|
3179
|
+
if (this.options.debug) {
|
|
3180
|
+
console.log(`[ClaudeHistoryMonitor] ${message}`);
|
|
3181
|
+
}
|
|
257
3182
|
}
|
|
258
|
-
const hookScript = generateHookScript(detectedShell);
|
|
259
|
-
return {
|
|
260
|
-
shellType: detectedShell,
|
|
261
|
-
hookScript,
|
|
262
|
-
rcFile
|
|
263
|
-
};
|
|
264
3183
|
}
|
|
265
3184
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return true;
|
|
3185
|
+
class ClaudePlansMonitor {
|
|
3186
|
+
plansDir;
|
|
3187
|
+
watcher = null;
|
|
3188
|
+
planAcceptanceManager;
|
|
3189
|
+
options;
|
|
3190
|
+
isRunning = false;
|
|
3191
|
+
processedPlans = /* @__PURE__ */ new Set();
|
|
3192
|
+
constructor(planAcceptanceManager, options = {}) {
|
|
3193
|
+
this.plansDir = join(homedir(), ".claude", "plans");
|
|
3194
|
+
this.planAcceptanceManager = planAcceptanceManager;
|
|
3195
|
+
this.options = {
|
|
3196
|
+
debug: options.debug ?? false,
|
|
3197
|
+
readDelay: options.readDelay ?? 100
|
|
3198
|
+
};
|
|
281
3199
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
3200
|
+
/**
|
|
3201
|
+
* Start monitoring
|
|
3202
|
+
*/
|
|
3203
|
+
start() {
|
|
3204
|
+
if (this.isRunning) {
|
|
3205
|
+
this.log("Already running");
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
if (!existsSync(this.plansDir)) {
|
|
3209
|
+
mkdirSync(this.plansDir, { recursive: true });
|
|
3210
|
+
this.log(`Created plans directory: ${this.plansDir}`);
|
|
3211
|
+
}
|
|
3212
|
+
try {
|
|
3213
|
+
this.watcher = watch(this.plansDir, async (event, filename) => {
|
|
3214
|
+
if (filename && filename.endsWith(".md")) {
|
|
3215
|
+
await this.handlePlanFile(event, filename);
|
|
3216
|
+
}
|
|
3217
|
+
});
|
|
3218
|
+
this.isRunning = true;
|
|
3219
|
+
this.log("\u{1F4CB} Claude Plans Monitor started");
|
|
3220
|
+
} catch (error) {
|
|
3221
|
+
this.log(`Failed to start monitoring: ${error}`);
|
|
3222
|
+
throw error;
|
|
3223
|
+
}
|
|
294
3224
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
3225
|
+
/**
|
|
3226
|
+
* Handle plan file event
|
|
3227
|
+
*/
|
|
3228
|
+
async handlePlanFile(event, filename) {
|
|
3229
|
+
if (event !== "rename") {
|
|
3230
|
+
return;
|
|
3231
|
+
}
|
|
3232
|
+
const planPath = join(this.plansDir, filename);
|
|
3233
|
+
if (!existsSync(planPath)) {
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
3236
|
+
if (this.processedPlans.has(filename)) {
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
this.log(`New plan detected: ${filename}`);
|
|
3240
|
+
this.processedPlans.add(filename);
|
|
3241
|
+
await new Promise((resolve) => setTimeout(resolve, this.options.readDelay));
|
|
3242
|
+
try {
|
|
3243
|
+
await this.onPlanAccepted(filename, planPath);
|
|
3244
|
+
} catch (error) {
|
|
3245
|
+
this.log(`Error processing plan: ${error}`);
|
|
3246
|
+
this.processedPlans.delete(filename);
|
|
3247
|
+
}
|
|
300
3248
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
3249
|
+
/**
|
|
3250
|
+
* Handle plan acceptance
|
|
3251
|
+
*/
|
|
3252
|
+
async onPlanAccepted(filename, planPath) {
|
|
3253
|
+
this.log(`Reading plan: ${planPath}`);
|
|
3254
|
+
const content = await readFile(planPath, "utf-8");
|
|
3255
|
+
const plan = {
|
|
3256
|
+
id: filename.replace(".md", ""),
|
|
3257
|
+
content,
|
|
3258
|
+
metadata: {
|
|
3259
|
+
title: this.extractTitle(content),
|
|
3260
|
+
createdAt: Date.now(),
|
|
3261
|
+
filename,
|
|
3262
|
+
path: planPath
|
|
3263
|
+
}
|
|
3264
|
+
};
|
|
3265
|
+
this.log(`\u{1F4CB} Plan accepted: ${plan.metadata?.title || "Untitled"}`);
|
|
3266
|
+
try {
|
|
3267
|
+
await this.planAcceptanceManager.onPlanAccepted(plan);
|
|
3268
|
+
this.log(`\u2705 Plan acceptance workflow completed`);
|
|
3269
|
+
} catch (error) {
|
|
3270
|
+
this.log(`\u274C Plan acceptance workflow failed: ${error}`);
|
|
3271
|
+
throw error;
|
|
304
3272
|
}
|
|
305
|
-
await execClaudeDirect(claudePath, args);
|
|
306
|
-
return;
|
|
307
3273
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
3274
|
+
/**
|
|
3275
|
+
* Extract title from markdown content
|
|
3276
|
+
*/
|
|
3277
|
+
extractTitle(content) {
|
|
3278
|
+
const lines = content.split("\n");
|
|
3279
|
+
for (const line of lines) {
|
|
3280
|
+
const h1Match = line.match(/^#\s/);
|
|
3281
|
+
if (h1Match) {
|
|
3282
|
+
return line.replace(/^#\s+/, "").trim();
|
|
315
3283
|
}
|
|
316
|
-
});
|
|
317
|
-
process__default.exit(result.exitCode ?? 0);
|
|
318
|
-
} catch (error) {
|
|
319
|
-
if (error && typeof error === "object" && "signal" in error) {
|
|
320
|
-
const signal = error.signal;
|
|
321
|
-
const signalCodes = {
|
|
322
|
-
SIGINT: 130,
|
|
323
|
-
// 128 + 2
|
|
324
|
-
SIGTERM: 143,
|
|
325
|
-
// 128 + 15
|
|
326
|
-
SIGQUIT: 131
|
|
327
|
-
// 128 + 3
|
|
328
|
-
};
|
|
329
|
-
process__default.exit(signalCodes[signal] || 1);
|
|
330
3284
|
}
|
|
331
|
-
|
|
332
|
-
|
|
3285
|
+
for (const line of lines) {
|
|
3286
|
+
const h2Match = line.match(/^##\s/);
|
|
3287
|
+
if (h2Match) {
|
|
3288
|
+
return line.replace(/^##\s+/, "").trim();
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
const nonEmptyLines = lines.filter((line) => line.trim());
|
|
3292
|
+
if (nonEmptyLines.length > 0) {
|
|
3293
|
+
return nonEmptyLines[0].substring(0, 50).trim();
|
|
3294
|
+
}
|
|
3295
|
+
return "Untitled Plan";
|
|
3296
|
+
}
|
|
3297
|
+
/**
|
|
3298
|
+
* Stop monitoring
|
|
3299
|
+
*/
|
|
3300
|
+
stop() {
|
|
3301
|
+
if (!this.isRunning) {
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
if (this.watcher) {
|
|
3305
|
+
this.watcher.close();
|
|
3306
|
+
this.watcher = null;
|
|
3307
|
+
}
|
|
3308
|
+
this.isRunning = false;
|
|
3309
|
+
this.log("\u{1F4CB} Claude Plans Monitor stopped");
|
|
3310
|
+
}
|
|
3311
|
+
/**
|
|
3312
|
+
* Get current status
|
|
3313
|
+
*/
|
|
3314
|
+
getStatus() {
|
|
3315
|
+
return {
|
|
3316
|
+
isRunning: this.isRunning,
|
|
3317
|
+
plansDir: this.plansDir,
|
|
3318
|
+
processedPlansCount: this.processedPlans.size
|
|
3319
|
+
};
|
|
3320
|
+
}
|
|
3321
|
+
/**
|
|
3322
|
+
* Clear processed plans cache
|
|
3323
|
+
*/
|
|
3324
|
+
clearCache() {
|
|
3325
|
+
this.processedPlans.clear();
|
|
3326
|
+
this.log("Cleared processed plans cache");
|
|
3327
|
+
}
|
|
3328
|
+
/**
|
|
3329
|
+
* Debug logging
|
|
3330
|
+
*/
|
|
3331
|
+
log(message) {
|
|
3332
|
+
if (this.options.debug) {
|
|
3333
|
+
console.log(`[ClaudePlansMonitor] ${message}`);
|
|
3334
|
+
}
|
|
333
3335
|
}
|
|
334
3336
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
3337
|
+
|
|
3338
|
+
class EnhancedClaudeWrapper {
|
|
3339
|
+
contextManager;
|
|
3340
|
+
historyMonitor = null;
|
|
3341
|
+
plansMonitor = null;
|
|
3342
|
+
options;
|
|
3343
|
+
initialized = false;
|
|
3344
|
+
constructor(options = {}) {
|
|
3345
|
+
this.options = {
|
|
3346
|
+
debug: options.debug ?? false,
|
|
3347
|
+
enableThreadMode: options.enableThreadMode ?? true,
|
|
3348
|
+
enablePlanAcceptance: options.enablePlanAcceptance ?? true,
|
|
3349
|
+
enableAutoSummarize: options.enableAutoSummarize ?? true
|
|
3350
|
+
};
|
|
3351
|
+
this.contextManager = createContextManager({
|
|
3352
|
+
debug: this.options.debug
|
|
3353
|
+
});
|
|
339
3354
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
3355
|
+
/**
|
|
3356
|
+
* Initialize wrapper
|
|
3357
|
+
*/
|
|
3358
|
+
async initialize() {
|
|
3359
|
+
if (this.initialized) {
|
|
3360
|
+
return;
|
|
3361
|
+
}
|
|
3362
|
+
this.log("\u{1F680} Initializing CCJK Enhanced Wrapper...");
|
|
3363
|
+
await this.contextManager.initialize();
|
|
3364
|
+
if (this.options.enableThreadMode) {
|
|
3365
|
+
this.contextManager.enableThreadMode({
|
|
3366
|
+
defaultMaxTokens: 5e3,
|
|
3367
|
+
autoClose: true,
|
|
3368
|
+
debug: this.options.debug
|
|
3369
|
+
});
|
|
3370
|
+
this.log("\u2705 Thread mode enabled");
|
|
3371
|
+
}
|
|
3372
|
+
if (this.options.enablePlanAcceptance) {
|
|
3373
|
+
const planManager = this.contextManager.enablePlanAcceptance({
|
|
3374
|
+
minSummarizeInterval: 6e5,
|
|
3375
|
+
// 10 minutes
|
|
3376
|
+
autoCompress: true,
|
|
3377
|
+
clearContext: true,
|
|
3378
|
+
injectSummary: true,
|
|
3379
|
+
debug: this.options.debug
|
|
3380
|
+
});
|
|
3381
|
+
this.plansMonitor = new ClaudePlansMonitor(planManager, {
|
|
3382
|
+
debug: this.options.debug
|
|
3383
|
+
});
|
|
3384
|
+
this.plansMonitor.start();
|
|
3385
|
+
this.log("\u2705 Plan acceptance enabled");
|
|
3386
|
+
}
|
|
3387
|
+
if (this.options.enableAutoSummarize) {
|
|
3388
|
+
this.contextManager.enableAutoSummarize({
|
|
3389
|
+
enabled: true,
|
|
3390
|
+
minInterval: 6e5,
|
|
3391
|
+
// 10 minutes
|
|
3392
|
+
tokenThreshold: 1e5,
|
|
3393
|
+
// 100K tokens
|
|
3394
|
+
strategy: "miro-thinker",
|
|
3395
|
+
debug: this.options.debug
|
|
3396
|
+
});
|
|
3397
|
+
this.log("\u2705 Auto-summarize enabled");
|
|
3398
|
+
}
|
|
3399
|
+
this.historyMonitor = new ClaudeHistoryMonitor(this.contextManager, {
|
|
3400
|
+
debug: this.options.debug
|
|
3401
|
+
});
|
|
3402
|
+
this.historyMonitor.start();
|
|
3403
|
+
this.log("\u2705 History monitor started");
|
|
3404
|
+
this.initialized = true;
|
|
3405
|
+
this.log("\u2705 CCJK Enhanced Wrapper initialized");
|
|
3406
|
+
}
|
|
3407
|
+
/**
|
|
3408
|
+
* Execute Claude command
|
|
3409
|
+
*/
|
|
3410
|
+
async execute(args) {
|
|
3411
|
+
if (!this.initialized) {
|
|
3412
|
+
throw new Error("Wrapper not initialized. Call initialize() first.");
|
|
3413
|
+
}
|
|
3414
|
+
if (args[0] === "context") {
|
|
3415
|
+
return this.handleContextCommand(args.slice(1));
|
|
3416
|
+
}
|
|
3417
|
+
try {
|
|
3418
|
+
const claudePath = await this.findClaudeCLI();
|
|
3419
|
+
this.log(`Executing: ${claudePath} ${args.join(" ")}`);
|
|
3420
|
+
const result = await exec(claudePath, args, {
|
|
3421
|
+
nodeOptions: {
|
|
3422
|
+
stdio: "inherit"
|
|
3423
|
+
// Pass through stdin/stdout/stderr
|
|
3424
|
+
}
|
|
3425
|
+
});
|
|
3426
|
+
return result.exitCode || 0;
|
|
3427
|
+
} catch (error) {
|
|
3428
|
+
console.error("\u274C Error executing Claude CLI:", error);
|
|
3429
|
+
return 1;
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
/**
|
|
3433
|
+
* Handle context management commands
|
|
3434
|
+
*/
|
|
3435
|
+
async handleContextCommand(args) {
|
|
3436
|
+
const command = args[0];
|
|
3437
|
+
try {
|
|
3438
|
+
switch (command) {
|
|
3439
|
+
case "stats":
|
|
3440
|
+
await this.showStats();
|
|
346
3441
|
break;
|
|
347
|
-
case "
|
|
348
|
-
await
|
|
3442
|
+
case "compress":
|
|
3443
|
+
await this.manualCompress();
|
|
3444
|
+
break;
|
|
3445
|
+
case "reset":
|
|
3446
|
+
await this.contextManager.reset();
|
|
3447
|
+
console.log("\u2705 Context reset");
|
|
349
3448
|
break;
|
|
350
3449
|
case "status":
|
|
351
|
-
await showStatus(
|
|
3450
|
+
await this.showStatus();
|
|
352
3451
|
break;
|
|
353
|
-
|
|
354
|
-
showHelp();
|
|
3452
|
+
case "help":
|
|
3453
|
+
this.showHelp();
|
|
355
3454
|
break;
|
|
3455
|
+
default:
|
|
3456
|
+
console.log(`\u274C Unknown context command: ${command}`);
|
|
3457
|
+
console.log('Run "claude context help" for available commands');
|
|
3458
|
+
return 1;
|
|
356
3459
|
}
|
|
357
|
-
return;
|
|
3460
|
+
return 0;
|
|
3461
|
+
} catch (error) {
|
|
3462
|
+
console.error(`\u274C Error executing context command: ${error}`);
|
|
3463
|
+
return 1;
|
|
358
3464
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
3465
|
+
}
|
|
3466
|
+
/**
|
|
3467
|
+
* Show context statistics
|
|
3468
|
+
*/
|
|
3469
|
+
async showStats() {
|
|
3470
|
+
const stats = await this.contextManager.getStats();
|
|
3471
|
+
const threadManager = this.contextManager.getThreadManager();
|
|
3472
|
+
const autoSummarize = this.contextManager.getAutoSummarizeManager();
|
|
3473
|
+
const planManager = this.contextManager.getPlanAcceptanceManager();
|
|
3474
|
+
console.log("\n\u{1F4CA} CCJK Context Statistics\n");
|
|
3475
|
+
console.log("=".repeat(50));
|
|
3476
|
+
console.log("\n\u{1F4C8} Context Usage:");
|
|
3477
|
+
console.log(` Current Tokens: ${stats.currentTokens.toLocaleString()}`);
|
|
3478
|
+
console.log(` Compressed Tokens: ${stats.compressedTokens.toLocaleString()}`);
|
|
3479
|
+
console.log(` Compression Ratio: ${(stats.compressionRatio * 100).toFixed(1)}%`);
|
|
3480
|
+
console.log(` Total Messages: ${stats.totalMessages}`);
|
|
3481
|
+
console.log(` Context Usage: ${(stats.contextUsage * 100).toFixed(1)}%`);
|
|
3482
|
+
if (threadManager) {
|
|
3483
|
+
const threadStats = threadManager.getStats();
|
|
3484
|
+
console.log("\n\u{1F9F5} Thread Management:");
|
|
3485
|
+
console.log(` Total Threads: ${threadStats.totalThreads}`);
|
|
3486
|
+
console.log(` Active Threads: ${threadStats.activeThreads}`);
|
|
3487
|
+
console.log(` Completed Threads: ${threadStats.completedThreads}`);
|
|
3488
|
+
console.log(` Avg Compression: ${(threadStats.averageCompressionRatio * 100).toFixed(1)}%`);
|
|
373
3489
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
3490
|
+
if (autoSummarize) {
|
|
3491
|
+
const autoStats = autoSummarize.getStats();
|
|
3492
|
+
console.log("\n\u{1F916} Auto-Summarize:");
|
|
3493
|
+
console.log(` Status: ${autoStats.enabled ? "\u2705 Enabled" : "\u274C Disabled"}`);
|
|
3494
|
+
console.log(` Can Summarize: ${autoStats.canSummarize ? "Yes" : "No"}`);
|
|
3495
|
+
console.log(` Summarize Count: ${autoStats.summarizeCount}`);
|
|
3496
|
+
if (!autoStats.canSummarize && autoStats.timeUntilNextSummarize > 0) {
|
|
3497
|
+
const minutes = Math.floor(autoStats.timeUntilNextSummarize / 6e4);
|
|
3498
|
+
const seconds = Math.floor(autoStats.timeUntilNextSummarize % 6e4 / 1e3);
|
|
3499
|
+
console.log(` Time Until Next: ${minutes}m ${seconds}s`);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
if (planManager) {
|
|
3503
|
+
const planStats = planManager.getStats();
|
|
3504
|
+
console.log("\n\u{1F4CB} Plan Acceptance:");
|
|
3505
|
+
console.log(` Can Summarize: ${planStats.canSummarize ? "Yes" : "No"}`);
|
|
3506
|
+
if (!planStats.canSummarize && planStats.timeUntilNextSummarize > 0) {
|
|
3507
|
+
const minutes = Math.floor(planStats.timeUntilNextSummarize / 6e4);
|
|
3508
|
+
const seconds = Math.floor(planStats.timeUntilNextSummarize % 6e4 / 1e3);
|
|
3509
|
+
console.log(` Time Until Next: ${minutes}m ${seconds}s`);
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
console.log(`
|
|
3513
|
+
${"=".repeat(50)}
|
|
3514
|
+
`);
|
|
377
3515
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
console.log();
|
|
401
|
-
if (!installed) {
|
|
402
|
-
console.log(i18n.t("context:installHint"));
|
|
403
|
-
console.log(` ccjk context install
|
|
3516
|
+
/**
|
|
3517
|
+
* Show system status
|
|
3518
|
+
*/
|
|
3519
|
+
async showStatus() {
|
|
3520
|
+
console.log("\n\u{1F50D} CCJK System Status\n");
|
|
3521
|
+
console.log("=".repeat(50));
|
|
3522
|
+
if (this.historyMonitor) {
|
|
3523
|
+
const historyStatus = this.historyMonitor.getStatus();
|
|
3524
|
+
console.log("\n\u{1F4CA} History Monitor:");
|
|
3525
|
+
console.log(` Status: ${historyStatus.isRunning ? "\u2705 Running" : "\u274C Stopped"}`);
|
|
3526
|
+
console.log(` File: ${historyStatus.historyFile}`);
|
|
3527
|
+
console.log(` Position: ${historyStatus.lastPosition} bytes`);
|
|
3528
|
+
}
|
|
3529
|
+
if (this.plansMonitor) {
|
|
3530
|
+
const plansStatus = this.plansMonitor.getStatus();
|
|
3531
|
+
console.log("\n\u{1F4CB} Plans Monitor:");
|
|
3532
|
+
console.log(` Status: ${plansStatus.isRunning ? "\u2705 Running" : "\u274C Stopped"}`);
|
|
3533
|
+
console.log(` Directory: ${plansStatus.plansDir}`);
|
|
3534
|
+
console.log(` Processed Plans: ${plansStatus.processedPlansCount}`);
|
|
3535
|
+
}
|
|
3536
|
+
console.log(`
|
|
3537
|
+
${"=".repeat(50)}
|
|
404
3538
|
`);
|
|
405
|
-
} else {
|
|
406
|
-
console.log(i18n.t("context:hookActive"));
|
|
407
3539
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
console.log();
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
console.
|
|
3540
|
+
/**
|
|
3541
|
+
* Manual compression
|
|
3542
|
+
*/
|
|
3543
|
+
async manualCompress() {
|
|
3544
|
+
const autoSummarize = this.contextManager.getAutoSummarizeManager();
|
|
3545
|
+
if (!autoSummarize) {
|
|
3546
|
+
console.log("\u274C Auto-summarize not enabled");
|
|
3547
|
+
return;
|
|
3548
|
+
}
|
|
3549
|
+
console.log("\u{1F5DC}\uFE0F Compressing context...");
|
|
3550
|
+
const result = await autoSummarize.summarize();
|
|
3551
|
+
if (result.performed) {
|
|
3552
|
+
console.log(`\u2705 Compressed: ${result.summary.originalTokens.toLocaleString()} \u2192 ${result.summary.compressedTokens.toLocaleString()} tokens`);
|
|
3553
|
+
console.log(` Ratio: ${(result.summary.compressionRatio * 100).toFixed(1)}%`);
|
|
3554
|
+
} else {
|
|
3555
|
+
console.log(`\u23F3 Skipped: ${result.reason}`);
|
|
3556
|
+
if (result.timeUntilNext) {
|
|
3557
|
+
const minutes = Math.floor(result.timeUntilNext / 6e4);
|
|
3558
|
+
const seconds = Math.floor(result.timeUntilNext % 6e4 / 1e3);
|
|
3559
|
+
console.log(` Time until next: ${minutes}m ${seconds}s`);
|
|
3560
|
+
}
|
|
422
3561
|
}
|
|
423
|
-
process__default.exit(1);
|
|
424
3562
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
console.log(
|
|
431
|
-
console.log();
|
|
432
|
-
console.log(
|
|
433
|
-
console.log(
|
|
3563
|
+
/**
|
|
3564
|
+
* Show help
|
|
3565
|
+
*/
|
|
3566
|
+
showHelp() {
|
|
3567
|
+
console.log("\n\u{1F4D6} CCJK Context Management Commands\n");
|
|
3568
|
+
console.log("Usage: claude context <command>\n");
|
|
3569
|
+
console.log("Commands:");
|
|
3570
|
+
console.log(" stats Show context statistics");
|
|
3571
|
+
console.log(" status Show system status");
|
|
3572
|
+
console.log(" compress Manually trigger compression");
|
|
3573
|
+
console.log(" reset Reset context manager");
|
|
3574
|
+
console.log(" help Show this help message");
|
|
434
3575
|
console.log();
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
3576
|
+
}
|
|
3577
|
+
/**
|
|
3578
|
+
* Find Claude CLI path
|
|
3579
|
+
*/
|
|
3580
|
+
async findClaudeCLI() {
|
|
3581
|
+
const paths = [
|
|
3582
|
+
"/usr/local/bin/claude",
|
|
3583
|
+
"/opt/homebrew/bin/claude",
|
|
3584
|
+
join(homedir(), ".local/bin/claude")
|
|
3585
|
+
];
|
|
3586
|
+
for (const path of paths) {
|
|
3587
|
+
if (existsSync(path)) {
|
|
3588
|
+
return path;
|
|
3589
|
+
}
|
|
439
3590
|
}
|
|
3591
|
+
try {
|
|
3592
|
+
const result = await exec("which claude");
|
|
3593
|
+
const path = result.stdout.trim();
|
|
3594
|
+
if (path) {
|
|
3595
|
+
return path;
|
|
3596
|
+
}
|
|
3597
|
+
} catch {
|
|
3598
|
+
}
|
|
3599
|
+
throw new Error("Claude CLI not found. Please install Claude Code CLI first.");
|
|
3600
|
+
}
|
|
3601
|
+
/**
|
|
3602
|
+
* Cleanup
|
|
3603
|
+
*/
|
|
3604
|
+
async cleanup() {
|
|
3605
|
+
this.log("\u{1F9F9} Cleaning up...");
|
|
3606
|
+
if (this.historyMonitor) {
|
|
3607
|
+
this.historyMonitor.stop();
|
|
3608
|
+
}
|
|
3609
|
+
if (this.plansMonitor) {
|
|
3610
|
+
this.plansMonitor.stop();
|
|
3611
|
+
}
|
|
3612
|
+
await this.contextManager.cleanup();
|
|
3613
|
+
this.initialized = false;
|
|
3614
|
+
this.log("\u2705 Cleanup complete");
|
|
3615
|
+
}
|
|
3616
|
+
/**
|
|
3617
|
+
* Debug logging
|
|
3618
|
+
*/
|
|
3619
|
+
log(message) {
|
|
3620
|
+
if (this.options.debug) {
|
|
3621
|
+
console.log(`[EnhancedClaudeWrapper] ${message}`);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
async function claudeWrapperCommand(args) {
|
|
3627
|
+
const wrapper = new EnhancedClaudeWrapper({
|
|
3628
|
+
debug: process__default.env.CCJK_DEBUG === "true"
|
|
3629
|
+
});
|
|
3630
|
+
try {
|
|
3631
|
+
await wrapper.initialize();
|
|
3632
|
+
const exitCode = await wrapper.execute(args);
|
|
3633
|
+
await wrapper.cleanup();
|
|
3634
|
+
process__default.exit(exitCode);
|
|
3635
|
+
} catch (error) {
|
|
3636
|
+
console.error("\u274C Error:", error instanceof Error ? error.message : error);
|
|
3637
|
+
await wrapper.cleanup();
|
|
440
3638
|
process__default.exit(1);
|
|
441
3639
|
}
|
|
442
3640
|
}
|
|
443
|
-
function
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
console.log(" ccjk context install");
|
|
458
|
-
console.log(" ccjk context uninstall");
|
|
459
|
-
console.log();
|
|
3641
|
+
async function claudeWrapper(args) {
|
|
3642
|
+
const wrapper = new EnhancedClaudeWrapper({
|
|
3643
|
+
debug: process__default.env.CCJK_DEBUG === "true"
|
|
3644
|
+
});
|
|
3645
|
+
try {
|
|
3646
|
+
await wrapper.initialize();
|
|
3647
|
+
const exitCode = await wrapper.execute(args);
|
|
3648
|
+
await wrapper.cleanup();
|
|
3649
|
+
process__default.exit(exitCode);
|
|
3650
|
+
} catch (error) {
|
|
3651
|
+
console.error("\u274C Error:", error instanceof Error ? error.message : error);
|
|
3652
|
+
await wrapper.cleanup();
|
|
3653
|
+
process__default.exit(1);
|
|
3654
|
+
}
|
|
460
3655
|
}
|
|
461
3656
|
|
|
462
|
-
export { claudeWrapper,
|
|
3657
|
+
export { claudeWrapper, claudeWrapperCommand };
|