ccjk 4.0.0-beta.1 → 4.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,248 +1,771 @@
1
- import chalk from 'chalk';
2
- import ora from 'ora';
1
+ import * as nodeFs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import ansis from 'ansis';
5
+ import * as crypto from 'node:crypto';
6
+ import process__default from 'node:process';
3
7
 
4
- const { green, yellow, cyan, gray, red, blue } = chalk;
5
- async function contextCommand(action, id, options) {
6
- switch (action) {
7
- case "status":
8
- case "s":
9
- await showStatus(options);
10
- break;
11
- case "compress":
12
- case "c":
13
- await triggerCompress(id, options);
14
- break;
15
- case "history":
16
- case "h":
17
- await showHistory(options);
18
- break;
19
- case "restore":
20
- case "r":
21
- await restoreContext(id, options);
22
- break;
23
- case "config":
24
- case "cfg":
25
- await configureContext(options);
26
- break;
27
- case "clear":
28
- await clearContext(options);
29
- break;
30
- case "hook":
31
- await handleHookCommand(id, options);
32
- break;
33
- case "help":
34
- default:
35
- showHelp();
8
+ class ContextAnalyzer {
9
+ /**
10
+ * 分析消息重要性
11
+ */
12
+ static analyzeImportance(message) {
13
+ let score = 0;
14
+ const content = this.extractTextContent(message);
15
+ if (this.containsDecision(content))
16
+ score += 30;
17
+ if (this.containsCodeChange(message))
18
+ score += 25;
19
+ if (this.containsErrorResolution(content))
20
+ score += 20;
21
+ if (message.type === "user" && content.length > 50)
22
+ score += 15;
23
+ if (message.toolUseResult)
24
+ score += 10;
25
+ const age = Date.now() - new Date(message.timestamp).getTime();
26
+ const hourAge = age / (1e3 * 60 * 60);
27
+ score += Math.max(0, 20 - hourAge * 2);
28
+ return Math.min(100, score);
29
+ }
30
+ /**
31
+ * 提取文本内容
32
+ */
33
+ static extractTextContent(message) {
34
+ const content = message.message.content;
35
+ if (typeof content === "string")
36
+ return content;
37
+ return content.filter(
38
+ (block) => block.type === "text" && typeof block.text === "string"
39
+ ).map((block) => block.text).join("\n");
40
+ }
41
+ /**
42
+ * 检测是否包含决策
43
+ */
44
+ static containsDecision(content) {
45
+ const decisionPatterns = [
46
+ /决定|决策|选择|采用|使用|方案/,
47
+ /decide|decision|choose|adopt|use|approach/i,
48
+ /我们应该|建议|推荐/,
49
+ /should|recommend|suggest/i,
50
+ /✅|✔|确定|confirmed/i
51
+ ];
52
+ return decisionPatterns.some((p) => p.test(content));
53
+ }
54
+ /**
55
+ * 检测是否包含代码变更
56
+ */
57
+ static containsCodeChange(message) {
58
+ const content = message.message.content;
59
+ if (typeof content === "string")
60
+ return false;
61
+ return content.some(
62
+ (block) => block.type === "tool_use" && ["Write", "Edit", "Bash"].includes(block.name || "")
63
+ );
64
+ }
65
+ /**
66
+ * 检测是否包含错误解决
67
+ */
68
+ static containsErrorResolution(content) {
69
+ const patterns = [
70
+ /修复|解决|fix|resolve|solved/i,
71
+ /错误|error|bug|issue/i,
72
+ /成功|success|works|working/i
73
+ ];
74
+ return patterns.filter((p) => p.test(content)).length >= 2;
75
+ }
76
+ /**
77
+ * 提取关键决策
78
+ */
79
+ static extractDecisions(messages) {
80
+ const decisions = [];
81
+ for (const msg of messages) {
82
+ const content = this.extractTextContent(msg);
83
+ if (this.containsDecision(content)) {
84
+ const sentences = content.split(/[。.!!?\n]/).filter((s) => s.trim());
85
+ for (const sentence of sentences) {
86
+ if (this.containsDecision(sentence) && sentence.length < 200) {
87
+ decisions.push(sentence.trim());
88
+ }
89
+ }
90
+ }
91
+ }
92
+ return [...new Set(decisions)].slice(0, 10);
93
+ }
94
+ /**
95
+ * 提取代码变更
96
+ */
97
+ static extractCodeChanges(messages) {
98
+ const changes = [];
99
+ for (const msg of messages) {
100
+ const content = msg.message.content;
101
+ if (typeof content === "string")
102
+ continue;
103
+ for (const block of content) {
104
+ if (block.type === "tool_use") {
105
+ const input = block.input;
106
+ if (block.name === "Write" && input?.file_path) {
107
+ changes.push({
108
+ file: String(input.file_path),
109
+ action: "create",
110
+ description: `Created file`
111
+ });
112
+ } else if (block.name === "Edit" && input?.file_path) {
113
+ changes.push({
114
+ file: String(input.file_path),
115
+ action: "modify",
116
+ description: `Modified file`
117
+ });
118
+ }
119
+ }
120
+ }
121
+ }
122
+ const merged = /* @__PURE__ */ new Map();
123
+ for (const change of changes) {
124
+ const existing = merged.get(change.file);
125
+ if (existing) {
126
+ existing.action = "modify";
127
+ } else {
128
+ merged.set(change.file, change);
129
+ }
130
+ }
131
+ return Array.from(merged.values());
132
+ }
133
+ /**
134
+ * 提取主题
135
+ */
136
+ static extractTopics(messages) {
137
+ const topicKeywords = /* @__PURE__ */ new Map();
138
+ for (const msg of messages) {
139
+ const content = this.extractTextContent(msg).toLowerCase();
140
+ const techPatterns = [
141
+ /\b(mcp|api|cli|sdk|npm|git|docker)\b/gi,
142
+ /\b(typescript|javascript|python|rust|go)\b/gi,
143
+ /\b(react|vue|angular|node|express)\b/gi,
144
+ /\b(database|cache|redis|mongodb|postgres)\b/gi,
145
+ /\b(performance|optimization|refactor|debug)\b/gi
146
+ ];
147
+ for (const pattern of techPatterns) {
148
+ const matches = content.match(pattern) || [];
149
+ for (const match of matches) {
150
+ const key = match.toLowerCase();
151
+ topicKeywords.set(key, (topicKeywords.get(key) || 0) + 1);
152
+ }
153
+ }
154
+ }
155
+ return Array.from(topicKeywords.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([topic]) => topic);
156
+ }
157
+ /**
158
+ * 估算 token 数量
159
+ */
160
+ static estimateTokens(messages) {
161
+ let totalChars = 0;
162
+ for (const msg of messages) {
163
+ const content = this.extractTextContent(msg);
164
+ totalChars += content.length;
165
+ if (msg.toolUseResult?.stdout) {
166
+ totalChars += msg.toolUseResult.stdout.length;
167
+ }
168
+ }
169
+ return Math.ceil(totalChars / 4);
36
170
  }
37
171
  }
38
- async function handleHookCommand(subAction, options) {
39
- const { contextCommand: hookContextCommand } = await import('./claude-wrapper.mjs');
40
- await hookContextCommand(subAction || "status", [], {
41
- lang: options.lang,
42
- verbose: options.verbose
43
- });
44
- }
45
- async function showStatus(options) {
46
- const spinner = ora("Loading context status...").start();
47
- try {
48
- const { getContextManager } = await import('./context-manager.mjs');
49
- const manager = getContextManager();
50
- const status = await manager.getStatus();
51
- spinner.stop();
52
- console.log(`
53
- ${cyan("\u{1F9E0} Context Compression Status")}
54
- `);
55
- console.log(`${yellow("Current Session:")}`);
56
- console.log(` ${gray("ID:")} ${status.sessionId || "None"}`);
57
- console.log(` ${gray("Started:")} ${status.startTime ? new Date(status.startTime).toLocaleString() : "N/A"}`);
58
- console.log(` ${gray("Duration:")} ${status.duration || "N/A"}`);
59
- console.log(`
60
- ${yellow("Context Statistics:")}`);
61
- console.log(` ${gray("Total Tokens:")} ${status.totalTokens?.toLocaleString() || 0}`);
62
- console.log(` ${gray("Compressed Tokens:")} ${status.compressedTokens?.toLocaleString() || 0}`);
63
- console.log(` ${gray("Compression Ratio:")} ${status.compressionRatio ? `${(status.compressionRatio * 100).toFixed(1)}%` : "N/A"}`);
64
- console.log(` ${gray("Savings:")} ${status.tokensSaved?.toLocaleString() || 0} tokens`);
65
- console.log(`
66
- ${yellow("Compression Status:")}`);
67
- console.log(` ${gray("Auto-compress:")} ${status.autoCompress ? green("Enabled") : gray("Disabled")}`);
68
- console.log(` ${gray("Threshold:")} ${status.threshold?.toLocaleString() || "N/A"} tokens`);
69
- console.log(` ${gray("Last Compressed:")} ${status.lastCompressed ? new Date(status.lastCompressed).toLocaleString() : "Never"}`);
70
- console.log(` ${gray("Compressions Today:")} ${status.compressionsToday || 0}`);
71
- if (options.verbose) {
72
- console.log(`
73
- ${yellow("Detailed Info:")}`);
74
- console.log(` ${gray("Storage Path:")} ${status.storagePath || "N/A"}`);
75
- console.log(` ${gray("Cache Size:")} ${status.cacheSize || "N/A"}`);
76
- console.log(` ${gray("Model:")} ${status.model || "haiku"}`);
172
+ class ContextManager {
173
+ claudeDir;
174
+ projectsDir;
175
+ archiveDir;
176
+ summaryDir;
177
+ constructor() {
178
+ this.claudeDir = path.join(os.homedir(), ".claude");
179
+ this.projectsDir = path.join(this.claudeDir, "projects");
180
+ this.archiveDir = path.join(this.claudeDir, "archive");
181
+ this.summaryDir = path.join(this.claudeDir, "summaries");
182
+ this.ensureDirectories();
183
+ }
184
+ ensureDirectories() {
185
+ for (const dir of [this.archiveDir, this.summaryDir]) {
186
+ if (!nodeFs.existsSync(dir)) {
187
+ nodeFs.mkdirSync(dir, { recursive: true });
188
+ }
77
189
  }
78
- console.log();
79
- } catch (error) {
80
- spinner.fail(red("Failed to get context status"));
81
- if (options.verbose) {
82
- console.error(error);
83
- } else {
84
- console.log(gray("Run with --verbose for details"));
190
+ }
191
+ /**
192
+ * 获取当前项目的会话文件
193
+ */
194
+ getProjectSessions(projectPath) {
195
+ const projectKey = projectPath ? projectPath.replace(/\//g, "-").replace(/^-/, "") : this.detectCurrentProject();
196
+ const projectDir = path.join(this.projectsDir, projectKey);
197
+ if (!nodeFs.existsSync(projectDir)) {
198
+ return [];
85
199
  }
200
+ return nodeFs.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => path.join(projectDir, f));
201
+ }
202
+ /**
203
+ * 检测当前项目
204
+ */
205
+ detectCurrentProject() {
206
+ const cwd = process__default.cwd();
207
+ return cwd.replace(/\//g, "-").replace(/^-/, "");
86
208
  }
209
+ /**
210
+ * 读取会话消息
211
+ */
212
+ readSessionMessages(sessionFile) {
213
+ if (!nodeFs.existsSync(sessionFile)) {
214
+ return [];
215
+ }
216
+ const content = nodeFs.readFileSync(sessionFile, "utf-8");
217
+ const lines = content.trim().split("\n").filter((l) => l.trim());
218
+ return lines.map((line) => {
219
+ try {
220
+ return JSON.parse(line);
221
+ } catch {
222
+ return null;
223
+ }
224
+ }).filter((msg) => msg !== null);
225
+ }
226
+ /**
227
+ * 压缩会话 - 核心功能
228
+ */
229
+ async compact(sessionFile, options = {}) {
230
+ const {
231
+ keepLastN = 20,
232
+ archiveThreshold = 200,
233
+ preserveDecisions = true,
234
+ preserveCodeChanges = true
235
+ } = options;
236
+ const messages = this.readSessionMessages(sessionFile);
237
+ const originalCount = messages.length;
238
+ const originalTokens = ContextAnalyzer.estimateTokens(messages);
239
+ if (messages.length <= keepLastN) {
240
+ return {
241
+ originalMessages: originalCount,
242
+ compactedMessages: originalCount,
243
+ summaryGenerated: false,
244
+ archived: false,
245
+ tokensSaved: 0
246
+ };
247
+ }
248
+ const scoredMessages = messages.map((msg) => ({
249
+ message: msg,
250
+ score: ContextAnalyzer.analyzeImportance(msg)
251
+ }));
252
+ const recentMessages = messages.slice(-keepLastN);
253
+ const olderMessages = messages.slice(0, -keepLastN);
254
+ const importantOlder = scoredMessages.slice(0, -keepLastN).filter((m) => m.score >= 50).map((m) => m.message);
255
+ const summary = this.generateSummary(olderMessages);
256
+ this.saveSummary(sessionFile, summary);
257
+ let archived = false;
258
+ if (originalCount >= archiveThreshold) {
259
+ this.archiveSession(sessionFile, olderMessages);
260
+ archived = true;
261
+ }
262
+ const compactedMessages = [];
263
+ compactedMessages.push(this.createSummaryMessage(summary));
264
+ if (preserveDecisions || preserveCodeChanges) {
265
+ compactedMessages.push(...importantOlder.slice(0, 10));
266
+ }
267
+ compactedMessages.push(...recentMessages);
268
+ this.writeSession(sessionFile, compactedMessages);
269
+ const compactedTokens = ContextAnalyzer.estimateTokens(compactedMessages);
270
+ return {
271
+ originalMessages: originalCount,
272
+ compactedMessages: compactedMessages.length,
273
+ summaryGenerated: true,
274
+ archived,
275
+ tokensSaved: originalTokens - compactedTokens,
276
+ summary
277
+ };
278
+ }
279
+ /**
280
+ * 生成会话摘要
281
+ */
282
+ generateSummary(messages) {
283
+ const id = crypto.randomUUID();
284
+ const now = (/* @__PURE__ */ new Date()).toISOString();
285
+ const keyDecisions = ContextAnalyzer.extractDecisions(messages);
286
+ const codeChanges = ContextAnalyzer.extractCodeChanges(messages);
287
+ const topics = ContextAnalyzer.extractTopics(messages);
288
+ const tokenEstimate = ContextAnalyzer.estimateTokens(messages);
289
+ const summaryParts = [];
290
+ if (topics.length > 0) {
291
+ summaryParts.push(`\u4E3B\u8981\u8BDD\u9898: ${topics.join(", ")}`);
292
+ }
293
+ if (keyDecisions.length > 0) {
294
+ summaryParts.push(`\u5173\u952E\u51B3\u7B56:
295
+ ${keyDecisions.map((d) => ` - ${d}`).join("\n")}`);
296
+ }
297
+ if (codeChanges.length > 0) {
298
+ summaryParts.push(`\u4EE3\u7801\u53D8\u66F4:
299
+ ${codeChanges.map((c) => ` - ${c.action}: ${c.file}`).join("\n")}`);
300
+ }
301
+ return {
302
+ id,
303
+ createdAt: now,
304
+ updatedAt: now,
305
+ messageCount: messages.length,
306
+ tokenEstimate,
307
+ keyDecisions,
308
+ codeChanges,
309
+ topics,
310
+ summary: summaryParts.join("\n\n")
311
+ };
312
+ }
313
+ /**
314
+ * 保存摘要
315
+ */
316
+ saveSummary(sessionFile, summary) {
317
+ const sessionId = path.basename(sessionFile, ".jsonl");
318
+ const summaryFile = path.join(this.summaryDir, `${sessionId}-${summary.id}.json`);
319
+ nodeFs.writeFileSync(summaryFile, JSON.stringify(summary, null, 2));
320
+ return summaryFile;
321
+ }
322
+ /**
323
+ * 归档会话
324
+ */
325
+ archiveSession(sessionFile, messages) {
326
+ const sessionId = path.basename(sessionFile, ".jsonl");
327
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
328
+ const archiveFile = path.join(this.archiveDir, `${sessionId}-${timestamp}.jsonl`);
329
+ const content = messages.map((m) => JSON.stringify(m)).join("\n");
330
+ nodeFs.writeFileSync(archiveFile, content);
331
+ return archiveFile;
332
+ }
333
+ /**
334
+ * 创建摘要消息
335
+ */
336
+ createSummaryMessage(summary) {
337
+ return {
338
+ uuid: crypto.randomUUID(),
339
+ type: "assistant",
340
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
341
+ message: {
342
+ role: "assistant",
343
+ content: `[\u4F1A\u8BDD\u6458\u8981 - ${summary.messageCount} \u6761\u6D88\u606F\u5DF2\u538B\u7F29]
344
+
345
+ ${summary.summary}`
346
+ },
347
+ metadata: {
348
+ isSummary: true,
349
+ summaryId: summary.id,
350
+ originalMessageCount: summary.messageCount,
351
+ originalTokenEstimate: summary.tokenEstimate
352
+ }
353
+ };
354
+ }
355
+ /**
356
+ * 写入会话
357
+ */
358
+ writeSession(sessionFile, messages) {
359
+ const content = messages.map((m) => JSON.stringify(m)).join("\n");
360
+ nodeFs.writeFileSync(sessionFile, content);
361
+ }
362
+ /**
363
+ * 获取会话状态
364
+ */
365
+ getSessionStatus(sessionFile) {
366
+ const messages = this.readSessionMessages(sessionFile);
367
+ const tokenEstimate = ContextAnalyzer.estimateTokens(messages);
368
+ return {
369
+ messageCount: messages.length,
370
+ tokenEstimate,
371
+ oldestMessage: messages[0]?.timestamp || "N/A",
372
+ newestMessage: messages[messages.length - 1]?.timestamp || "N/A",
373
+ needsCompact: messages.length > 50 || tokenEstimate > 5e4
374
+ };
375
+ }
376
+ /**
377
+ * 列出所有摘要
378
+ */
379
+ listSummaries() {
380
+ if (!nodeFs.existsSync(this.summaryDir)) {
381
+ return [];
382
+ }
383
+ return nodeFs.readdirSync(this.summaryDir).filter((f) => f.endsWith(".json")).map((f) => {
384
+ try {
385
+ const content = nodeFs.readFileSync(path.join(this.summaryDir, f), "utf-8");
386
+ return JSON.parse(content);
387
+ } catch {
388
+ return null;
389
+ }
390
+ }).filter((s) => s !== null);
391
+ }
392
+ /**
393
+ * 恢复归档的会话
394
+ */
395
+ restoreArchive(archiveFile, targetSession) {
396
+ if (!nodeFs.existsSync(archiveFile)) {
397
+ return false;
398
+ }
399
+ const archiveContent = nodeFs.readFileSync(archiveFile, "utf-8");
400
+ const currentContent = nodeFs.existsSync(targetSession) ? nodeFs.readFileSync(targetSession, "utf-8") : "";
401
+ const merged = `${archiveContent}
402
+ ${currentContent}`;
403
+ nodeFs.writeFileSync(targetSession, merged.trim());
404
+ return true;
405
+ }
406
+ }
407
+ const contextManager = new ContextManager();
408
+
409
+ const chalk = ansis;
410
+ function formatBytes(bytes) {
411
+ if (bytes < 1024)
412
+ return `${bytes} B`;
413
+ if (bytes < 1024 * 1024)
414
+ return `${(bytes / 1024).toFixed(2)} KB`;
415
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
416
+ }
417
+ function formatNumber(num) {
418
+ return num.toLocaleString();
87
419
  }
88
- async function triggerCompress(sessionId, options) {
89
- const spinner = ora("Compressing context...").start();
420
+ function formatDate(dateStr) {
90
421
  try {
91
- const { getContextManager } = await import('./context-manager.mjs');
92
- const manager = getContextManager();
93
- const targetSession = sessionId || options.session;
94
- const result = await manager.compress(targetSession);
95
- spinner.succeed(green("Context compressed successfully"));
96
- console.log(`
97
- ${cyan("Compression Result:")}`);
98
- console.log(` ${gray("Original Tokens:")} ${result.originalTokens.toLocaleString()}`);
99
- console.log(` ${gray("Compressed Tokens:")} ${result.compressedTokens.toLocaleString()}`);
100
- console.log(` ${gray("Reduction:")} ${((1 - result.compressedTokens / result.originalTokens) * 100).toFixed(1)}%`);
101
- console.log(` ${gray("Time:")} ${result.duration}ms`);
422
+ const date = new Date(dateStr);
423
+ return date.toLocaleString("zh-CN", {
424
+ year: "numeric",
425
+ month: "2-digit",
426
+ day: "2-digit",
427
+ hour: "2-digit",
428
+ minute: "2-digit"
429
+ });
430
+ } catch {
431
+ return dateStr;
432
+ }
433
+ }
434
+ function printHeader(title) {
435
+ console.log();
436
+ console.log(chalk.cyan.bold(`\u{1F4E6} ${title}`));
437
+ console.log(chalk.gray("\u2500".repeat(50)));
438
+ }
439
+ function printSuccess(message) {
440
+ console.log(chalk.green(`\u2714 ${message}`));
441
+ }
442
+ function printWarning(message) {
443
+ console.log(chalk.yellow(`\u26A0 ${message}`));
444
+ }
445
+ function printError(message) {
446
+ console.log(chalk.red(`\u2716 ${message}`));
447
+ }
448
+ function printInfo(label, value) {
449
+ console.log(` ${chalk.gray(label.padEnd(20))} ${chalk.white(value)}`);
450
+ }
451
+ async function contextStatus(_options = {}) {
452
+ printHeader("Context Status");
453
+ const sessions = contextManager.getProjectSessions();
454
+ if (sessions.length === 0) {
455
+ printWarning("No sessions found for current project");
456
+ return;
457
+ }
458
+ let totalMessages = 0;
459
+ let totalTokens = 0;
460
+ for (const sessionFile of sessions) {
461
+ const status = contextManager.getSessionStatus(sessionFile);
462
+ const sessionId = path.basename(sessionFile, ".jsonl");
102
463
  console.log();
103
- } catch (error) {
104
- spinner.fail(red("Compression failed"));
105
- if (options.verbose) {
106
- console.error(error);
464
+ console.log(chalk.white.bold(`Session: ${sessionId.slice(0, 8)}...`));
465
+ printInfo("Messages", formatNumber(status.messageCount));
466
+ printInfo("Est. Tokens", formatNumber(status.tokenEstimate));
467
+ printInfo("Oldest", formatDate(status.oldestMessage));
468
+ printInfo("Newest", formatDate(status.newestMessage));
469
+ if (status.needsCompact) {
470
+ console.log(chalk.yellow(` \u26A0 Needs compact (${status.messageCount > 50 ? "too many messages" : "high token count"})`));
471
+ } else {
472
+ console.log(chalk.green(` \u2714 Healthy`));
107
473
  }
474
+ totalMessages += status.messageCount;
475
+ totalTokens += status.tokenEstimate;
476
+ }
477
+ console.log();
478
+ console.log(chalk.gray("\u2500".repeat(50)));
479
+ console.log(chalk.cyan.bold("Total:"));
480
+ printInfo("Sessions", sessions.length.toString());
481
+ printInfo("Messages", formatNumber(totalMessages));
482
+ printInfo("Est. Tokens", formatNumber(totalTokens));
483
+ if (totalTokens > 1e5) {
484
+ console.log();
485
+ printWarning(`High token usage detected. Run ${chalk.cyan("ccjk context compact")} to optimize.`);
108
486
  }
109
487
  }
110
- async function showHistory(options) {
111
- const spinner = ora("Loading compression history...").start();
112
- try {
113
- const { getContextManager } = await import('./context-manager.mjs');
114
- const manager = getContextManager();
115
- const history = await manager.getHistory();
116
- spinner.stop();
117
- if (history.length === 0) {
118
- console.log(yellow("\nNo compression history found.\n"));
119
- return;
120
- }
121
- console.log(`
122
- ${cyan("\u{1F4DC} Compression History")}
123
- `);
124
- if (options.format === "json") {
125
- console.log(JSON.stringify(history, null, 2));
126
- return;
488
+ async function contextCompact(options = {}) {
489
+ printHeader("Context Compact");
490
+ const sessions = contextManager.getProjectSessions();
491
+ if (sessions.length === 0) {
492
+ printWarning("No sessions found for current project");
493
+ return;
494
+ }
495
+ const compactOptions = {
496
+ keepLastN: Number.parseInt(options.keep || "20", 10),
497
+ summarizeThreshold: Number.parseInt(options.threshold || "50", 10),
498
+ preserveDecisions: options.decisions !== false,
499
+ preserveCodeChanges: options.codeChanges !== false
500
+ };
501
+ console.log(chalk.gray("Compact options:"));
502
+ printInfo("Keep last N", compactOptions.keepLastN.toString());
503
+ printInfo("Threshold", compactOptions.summarizeThreshold.toString());
504
+ printInfo("Preserve decisions", compactOptions.preserveDecisions ? "Yes" : "No");
505
+ printInfo("Preserve code changes", compactOptions.preserveCodeChanges ? "Yes" : "No");
506
+ console.log();
507
+ let totalSaved = 0;
508
+ let compactedCount = 0;
509
+ for (const sessionFile of sessions) {
510
+ const sessionId = path.basename(sessionFile, ".jsonl").slice(0, 8);
511
+ const status = contextManager.getSessionStatus(sessionFile);
512
+ if (!options.force && !status.needsCompact) {
513
+ console.log(chalk.gray(` \u23ED ${sessionId}... (no compact needed)`));
514
+ continue;
127
515
  }
128
- console.log(`${gray("ID".padEnd(12))} ${gray("Date".padEnd(20))} ${gray("Original".padEnd(12))} ${gray("Compressed".padEnd(12))} ${gray("Ratio")}`);
129
- console.log(gray("\u2500".repeat(70)));
130
- for (const entry of history.slice(0, 20)) {
131
- const date = new Date(entry.timestamp).toLocaleString();
132
- const ratio = ((1 - entry.compressedTokens / entry.originalTokens) * 100).toFixed(1);
133
- console.log(
134
- `${entry.id.slice(0, 10).padEnd(12)} ${date.padEnd(20)} ${entry.originalTokens.toString().padEnd(12)} ${entry.compressedTokens.toString().padEnd(12)} ${green(`-${ratio}%`)}`
135
- );
516
+ if (options.dryRun) {
517
+ console.log(chalk.yellow(` \u{1F50D} ${sessionId}... (dry run)`));
518
+ console.log(chalk.gray(` Would compact ${status.messageCount} messages`));
519
+ continue;
136
520
  }
137
- if (history.length > 20) {
138
- console.log(gray(`
139
- ... and ${history.length - 20} more entries`));
521
+ try {
522
+ console.log(chalk.cyan(` \u23F3 Compacting ${sessionId}...`));
523
+ const result = await contextManager.compact(sessionFile, compactOptions);
524
+ if (result.summaryGenerated) {
525
+ printSuccess(`${sessionId}: ${result.originalMessages} \u2192 ${result.compactedMessages} messages`);
526
+ console.log(chalk.gray(` Tokens saved: ~${formatNumber(result.tokensSaved)}`));
527
+ if (result.archived) {
528
+ console.log(chalk.gray(` Archived to cold storage`));
529
+ }
530
+ if (result.summary) {
531
+ console.log(chalk.gray(` Topics: ${result.summary.topics.join(", ") || "N/A"}`));
532
+ }
533
+ totalSaved += result.tokensSaved;
534
+ compactedCount++;
535
+ } else {
536
+ console.log(chalk.gray(` \u23ED ${sessionId}... (already compact)`));
537
+ }
538
+ } catch (error) {
539
+ printError(`Failed to compact ${sessionId}: ${error}`);
140
540
  }
541
+ }
542
+ console.log();
543
+ console.log(chalk.gray("\u2500".repeat(50)));
544
+ if (compactedCount > 0) {
545
+ printSuccess(`Compacted ${compactedCount} session(s)`);
546
+ console.log(chalk.green(` Total tokens saved: ~${formatNumber(totalSaved)}`));
547
+ } else if (options.dryRun) {
548
+ printInfo("Dry run", "No changes made");
549
+ } else {
550
+ printInfo("Result", "All sessions already optimized");
551
+ }
552
+ }
553
+ async function contextSummaries(options = {}) {
554
+ printHeader("Session Summaries");
555
+ const summaries = contextManager.listSummaries();
556
+ if (summaries.length === 0) {
557
+ printWarning("No summaries found. Run `ccjk context compact` to generate summaries.");
558
+ return;
559
+ }
560
+ const sorted = summaries.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).slice(0, Number.parseInt(options.limit || "10", 10));
561
+ for (const summary of sorted) {
141
562
  console.log();
142
- } catch (error) {
143
- spinner.fail(red("Failed to load history"));
144
- if (options.verbose) {
145
- console.error(error);
563
+ console.log(chalk.white.bold(`Summary: ${summary.id.slice(0, 8)}...`));
564
+ printInfo("Created", formatDate(summary.createdAt));
565
+ printInfo("Messages", formatNumber(summary.messageCount));
566
+ printInfo("Est. Tokens", formatNumber(summary.tokenEstimate));
567
+ if (summary.topics.length > 0) {
568
+ printInfo("Topics", summary.topics.join(", "));
569
+ }
570
+ if (summary.keyDecisions.length > 0) {
571
+ console.log(chalk.gray(" Key Decisions:"));
572
+ for (const decision of summary.keyDecisions.slice(0, 3)) {
573
+ console.log(chalk.gray(` \u2022 ${decision.slice(0, 80)}${decision.length > 80 ? "..." : ""}`));
574
+ }
575
+ }
576
+ if (summary.codeChanges.length > 0) {
577
+ console.log(chalk.gray(" Code Changes:"));
578
+ for (const change of summary.codeChanges.slice(0, 5)) {
579
+ const fileName = path.basename(change.file);
580
+ console.log(chalk.gray(` \u2022 ${change.action}: ${fileName}`));
581
+ }
146
582
  }
147
583
  }
584
+ console.log();
585
+ console.log(chalk.gray(`Showing ${sorted.length} of ${summaries.length} summaries`));
148
586
  }
149
- async function restoreContext(id, options) {
150
- if (!id) {
151
- console.log(red("\nError: Please provide a context ID to restore."));
152
- console.log(gray("Usage: ccjk context restore <id>\n"));
587
+ async function contextArchives() {
588
+ printHeader("Archived Sessions");
589
+ const archiveDir = path.join(os.homedir(), ".claude", "archive");
590
+ if (!nodeFs.existsSync(archiveDir)) {
591
+ printWarning("No archives found.");
153
592
  return;
154
593
  }
155
- const spinner = ora(`Restoring context ${id}...`).start();
156
- try {
157
- const { getContextManager } = await import('./context-manager.mjs');
158
- const manager = getContextManager();
159
- await manager.restore(id);
160
- spinner.succeed(green(`Context ${id} restored successfully`));
594
+ const archives = nodeFs.readdirSync(archiveDir).filter((f) => f.endsWith(".jsonl")).map((f) => {
595
+ const filePath = path.join(archiveDir, f);
596
+ const stats = nodeFs.statSync(filePath);
597
+ return {
598
+ name: f,
599
+ path: filePath,
600
+ size: stats.size,
601
+ mtime: stats.mtime
602
+ };
603
+ }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
604
+ if (archives.length === 0) {
605
+ printWarning("No archives found.");
606
+ return;
607
+ }
608
+ for (const archive of archives) {
161
609
  console.log();
162
- } catch (error) {
163
- spinner.fail(red("Failed to restore context"));
164
- if (options.verbose) {
165
- console.error(error);
610
+ console.log(chalk.white.bold(archive.name));
611
+ printInfo("Size", formatBytes(archive.size));
612
+ printInfo("Archived", formatDate(archive.mtime.toISOString()));
613
+ }
614
+ console.log();
615
+ console.log(chalk.gray(`Total: ${archives.length} archive(s)`));
616
+ console.log();
617
+ console.log(chalk.gray(`To restore: ${chalk.cyan("ccjk context restore <archive-file>")}`));
618
+ }
619
+ async function contextRestore(archive, options = {}) {
620
+ printHeader("Restore Archive");
621
+ const archiveDir = path.join(os.homedir(), ".claude", "archive");
622
+ let archivePath = archive;
623
+ if (!path.isAbsolute(archive)) {
624
+ archivePath = path.join(archiveDir, archive);
625
+ }
626
+ if (!nodeFs.existsSync(archivePath)) {
627
+ printError(`Archive not found: ${archivePath}`);
628
+ return;
629
+ }
630
+ let targetSession = options.target;
631
+ if (!targetSession) {
632
+ const sessions = contextManager.getProjectSessions();
633
+ if (sessions.length === 0) {
634
+ printError("No active sessions found. Please specify target with --target");
635
+ return;
166
636
  }
637
+ targetSession = sessions[sessions.length - 1];
638
+ }
639
+ console.log(chalk.gray(`Archive: ${archivePath}`));
640
+ console.log(chalk.gray(`Target: ${targetSession}`));
641
+ console.log();
642
+ const success = contextManager.restoreArchive(archivePath, targetSession);
643
+ if (success) {
644
+ printSuccess("Archive restored successfully!");
645
+ console.log(chalk.gray("The archived messages have been prepended to the target session."));
646
+ } else {
647
+ printError("Failed to restore archive");
167
648
  }
168
649
  }
169
- async function configureContext(options) {
170
- try {
171
- const { getContextManager } = await import('./context-manager.mjs');
172
- const manager = getContextManager();
173
- const config = await manager.getConfig();
174
- console.log(`
175
- ${cyan("\u2699\uFE0F Context Compression Configuration")}
176
- `);
177
- console.log(`${yellow("Current Settings:")}`);
178
- console.log(` ${gray("Auto-compress:")} ${config.autoCompress ? green("Enabled") : gray("Disabled")}`);
179
- console.log(` ${gray("Threshold:")} ${config.threshold.toLocaleString()} tokens`);
180
- console.log(` ${gray("Model:")} ${config.model}`);
181
- console.log(` ${gray("Max History:")} ${config.maxHistory} entries`);
182
- console.log(` ${gray("Retention Days:")} ${config.retentionDays} days`);
183
- console.log(`
184
- ${yellow("To modify settings:")}`);
185
- console.log(` ${gray("Edit:")} ~/.ccjk/context-compression/config.json`);
186
- console.log(` ${gray("Or use:")} ccjk config context.<setting> <value>`);
650
+ async function contextAnalyze() {
651
+ printHeader("Context Analysis");
652
+ const sessions = contextManager.getProjectSessions();
653
+ if (sessions.length === 0) {
654
+ printWarning("No sessions found for current project");
655
+ return;
656
+ }
657
+ const latestSession = sessions[sessions.length - 1];
658
+ const messages = contextManager.readSessionMessages(latestSession);
659
+ if (messages.length === 0) {
660
+ printWarning("No messages in current session");
661
+ return;
662
+ }
663
+ console.log(chalk.gray(`Analyzing ${messages.length} messages...`));
664
+ console.log();
665
+ const topics = ContextAnalyzer.extractTopics(messages);
666
+ const decisions = ContextAnalyzer.extractDecisions(messages);
667
+ const codeChanges = ContextAnalyzer.extractCodeChanges(messages);
668
+ const tokenEstimate = ContextAnalyzer.estimateTokens(messages);
669
+ const importanceScores = messages.map((m) => ContextAnalyzer.analyzeImportance(m));
670
+ const avgImportance = importanceScores.reduce((a, b) => a + b, 0) / importanceScores.length;
671
+ const highImportance = importanceScores.filter((s) => s >= 50).length;
672
+ console.log(chalk.white.bold("\u{1F4CA} Statistics"));
673
+ printInfo("Total Messages", formatNumber(messages.length));
674
+ printInfo("Est. Tokens", formatNumber(tokenEstimate));
675
+ printInfo("Avg Importance", avgImportance.toFixed(1));
676
+ printInfo("High Importance", `${highImportance} (${(highImportance / messages.length * 100).toFixed(1)}%)`);
677
+ if (topics.length > 0) {
187
678
  console.log();
188
- } catch (error) {
189
- console.error(red("Failed to load configuration"));
190
- if (options.verbose) {
191
- console.error(error);
679
+ console.log(chalk.white.bold("\u{1F3F7}\uFE0F Topics"));
680
+ for (const topic of topics) {
681
+ console.log(chalk.gray(` \u2022 ${topic}`));
192
682
  }
193
683
  }
194
- }
195
- async function clearContext(options) {
196
- const spinner = ora("Clearing context cache...").start();
197
- try {
198
- const { getContextManager } = await import('./context-manager.mjs');
199
- const manager = getContextManager();
200
- await manager.clear();
201
- spinner.succeed(green("Context cache cleared"));
684
+ if (decisions.length > 0) {
685
+ console.log();
686
+ console.log(chalk.white.bold("\u{1F3AF} Key Decisions"));
687
+ for (const decision of decisions.slice(0, 5)) {
688
+ console.log(chalk.gray(` \u2022 ${decision.slice(0, 100)}${decision.length > 100 ? "..." : ""}`));
689
+ }
690
+ }
691
+ if (codeChanges.length > 0) {
202
692
  console.log();
203
- } catch (error) {
204
- spinner.fail(red("Failed to clear cache"));
205
- if (options.verbose) {
206
- console.error(error);
693
+ console.log(chalk.white.bold("\u{1F4DD} Code Changes"));
694
+ for (const change of codeChanges.slice(0, 10)) {
695
+ const fileName = path.basename(change.file);
696
+ const icon = change.action === "create" ? "\u2795" : change.action === "modify" ? "\u270F\uFE0F" : "\u{1F5D1}\uFE0F";
697
+ console.log(chalk.gray(` ${icon} ${fileName}`));
207
698
  }
208
699
  }
700
+ console.log();
701
+ console.log(chalk.white.bold("\u{1F4A1} Recommendations"));
702
+ if (tokenEstimate > 5e4) {
703
+ console.log(chalk.yellow(` \u2022 High token usage. Consider running ${chalk.cyan("ccjk context compact")}`));
704
+ }
705
+ if (messages.length > 100) {
706
+ console.log(chalk.yellow(` \u2022 Many messages. Consider archiving old conversations.`));
707
+ }
708
+ if (highImportance < messages.length * 0.1) {
709
+ console.log(chalk.gray(` \u2022 Most messages are low importance. Safe to compact.`));
710
+ }
711
+ if (tokenEstimate < 2e4 && messages.length < 50) {
712
+ console.log(chalk.green(` \u2022 Session is healthy. No action needed.`));
713
+ }
209
714
  }
210
- function showHelp() {
211
- console.log(`
212
- ${cyan("\u{1F9E0} Context Compression Commands")}
213
-
214
- ${yellow("Usage:")} ccjk context <action> [id] [options]
215
-
216
- ${yellow("Actions:")}
217
- ${green("status")} ${gray("s")} Show current context status
218
- ${green("compress")} ${gray("c")} Manually trigger compression
219
- ${green("history")} ${gray("h")} View compression history
220
- ${green("restore")} ${gray("r")} Restore a previous context
221
- ${green("config")} ${gray("cfg")} View/edit configuration
222
- ${green("clear")} Clear context cache
223
- ${green("hook")} Shell hook management
224
- ${green("help")} Show this help
225
-
226
- ${yellow("Hook Subcommands:")}
227
- ${green("hook install")} Install shell hook for transparent wrapping
228
- ${green("hook uninstall")} Remove shell hook
229
- ${green("hook status")} Show hook installation status
230
-
231
- ${yellow("Options:")}
232
- ${blue("--session, -s")} <id> Session ID to operate on
233
- ${blue("--format, -f")} <fmt> Output format (json, text)
234
- ${blue("--verbose, -v")} Show detailed output
235
-
236
- ${yellow("Examples:")}
237
- ${gray("ccjk context status")} Show current status
238
- ${gray("ccjk ctx s -v")} Status with details
239
- ${gray("ccjk context compress")} Compress current session
240
- ${gray("ccjk context history -f json")} History in JSON format
241
- ${gray("ccjk context restore abc123")} Restore specific context
242
- ${gray("ccjk context hook install")} Install shell hook
243
-
244
- ${yellow("Aliases:")} ctx
245
- `);
715
+ async function context(action, args = [], options = {}) {
716
+ switch (action) {
717
+ case "status":
718
+ await contextStatus({ all: options.all });
719
+ break;
720
+ case "compact":
721
+ await contextCompact({
722
+ keep: options.keep,
723
+ threshold: options.threshold,
724
+ decisions: options.decisions,
725
+ codeChanges: options.codeChanges,
726
+ force: options.force,
727
+ dryRun: options.dryRun
728
+ });
729
+ break;
730
+ case "summaries":
731
+ case "sum":
732
+ await contextSummaries({ limit: options.limit });
733
+ break;
734
+ case "archives":
735
+ case "arch":
736
+ await contextArchives();
737
+ break;
738
+ case "restore":
739
+ if (args.length === 0) {
740
+ printError("Please specify an archive file to restore");
741
+ console.log(chalk.gray(`Usage: ccjk context restore <archive-file>`));
742
+ return;
743
+ }
744
+ await contextRestore(args[0], { target: options.target });
745
+ break;
746
+ case "analyze":
747
+ await contextAnalyze();
748
+ break;
749
+ default:
750
+ console.log();
751
+ console.log(chalk.cyan.bold("\u{1F4E6} Context Management Commands"));
752
+ console.log();
753
+ console.log(chalk.white("Usage: ccjk context <action> [options]"));
754
+ console.log();
755
+ console.log(chalk.white("Actions:"));
756
+ console.log(`${chalk.gray(" status ")}View current session status`);
757
+ console.log(`${chalk.gray(" compact ")}Smart compress sessions (preserve key info)`);
758
+ console.log(`${chalk.gray(" summaries ")}View all session summaries`);
759
+ console.log(`${chalk.gray(" archives ")}View archived sessions`);
760
+ console.log(`${chalk.gray(" restore ")}Restore an archived session`);
761
+ console.log(`${chalk.gray(" analyze ")}Analyze current session content`);
762
+ console.log();
763
+ console.log(chalk.white("Examples:"));
764
+ console.log(chalk.gray(" ccjk context status"));
765
+ console.log(chalk.gray(" ccjk context compact --keep 20"));
766
+ console.log(chalk.gray(" ccjk context restore session-123.jsonl"));
767
+ break;
768
+ }
246
769
  }
247
770
 
248
- export { contextCommand };
771
+ export { context, contextAnalyze, contextArchives, contextCompact, contextRestore, contextStatus, contextSummaries };