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.
@@ -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 { i18n, initI18n } from './index2.mjs';
4
- import { findRealCommandPath, findCommandPath } from './platform.mjs';
5
- import * as fs from 'node:fs';
6
- import * as os from 'node:os';
7
- import * as path from 'node:path';
8
- import 'node:url';
9
- import 'i18next';
10
- import 'i18next-fs-backend';
11
- import 'pathe';
12
-
13
- function detectShellType() {
14
- const shell = process__default.env.SHELL || "";
15
- if (shell.includes("bash")) {
16
- return "bash";
17
- }
18
- if (shell.includes("zsh")) {
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 getShellRcFile(shellType) {
27
- const home = os.homedir();
28
- switch (shellType) {
29
- case "bash":
30
- if (fs.existsSync(path.join(home, ".bashrc"))) {
31
- return path.join(home, ".bashrc");
32
- }
33
- return path.join(home, ".bash_profile");
34
- case "zsh":
35
- return path.join(home, ".zshrc");
36
- case "fish":
37
- return path.join(home, ".config", "fish", "config.fish");
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 null;
49
+ return `[${toolName} completed]`;
40
50
  }
41
51
  }
42
- function generateHookScript(shellType) {
43
- const ccjkPath = process__default.argv[1];
44
- switch (shellType) {
45
- case "bash":
46
- case "zsh":
47
- return `
48
- # CCJK Context Compression Hook - DO NOT EDIT THIS BLOCK
49
- # This hook enables transparent context compression for claude commands
50
- # Native slash commands (/plugin, /doctor, etc.) are passed through directly
51
- claude() {
52
- # Find the real claude command path (bypass this function)
53
- local real_claude
54
- # Use which -a to find all occurrences and filter out this function
55
- real_claude=$(which -a claude 2>/dev/null | grep -v "^claude () {" | grep "^/" | head -n 1)
56
-
57
- # If first arg starts with '/', it's a native slash command - pass through directly
58
- # Examples: /plugin, /doctor, /config, /permissions, /allowed-tools, etc.
59
- if [[ "$1" == /* ]]; then
60
- if [ -n "$real_claude" ]; then
61
- "$real_claude" "$@"
62
- else
63
- # Fallback to direct execution if path not found
64
- command claude "$@"
65
- fi
66
- return $?
67
- fi
68
-
69
- # Check for recursion - if already in wrapper, call real claude directly
70
- if [ -n "$CCJK_WRAPPER_ACTIVE" ]; then
71
- if [ -n "$real_claude" ]; then
72
- "$real_claude" "$@"
73
- else
74
- command claude "$@"
75
- fi
76
- return $?
77
- fi
78
-
79
- export CCJK_WRAPPER_ACTIVE=1
80
- ${ccjkPath} claude "$@"
81
- local exit_code=$?
82
- unset CCJK_WRAPPER_ACTIVE
83
- return $exit_code
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
- # END CCJK Context Compression Hook
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
- case "fish":
88
- return `
89
- # CCJK Context Compression Hook - DO NOT EDIT THIS BLOCK
90
- # This hook enables transparent context compression for claude commands
91
- # Native slash commands are handled by CCJK plugin system
92
- function claude
93
- # Find the real claude command path
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
- default:
124
- return "";
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
- function isHookInstalled(rcFile) {
128
- if (!fs.existsSync(rcFile)) {
129
- return false;
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
- const content = fs.readFileSync(rcFile, "utf-8");
132
- return content.includes("# CCJK Context Compression Hook");
133
- }
134
- async function installShellHook(shellType) {
135
- const detectedShell = shellType || detectShellType();
136
- if (detectedShell === "unknown") {
137
- return {
138
- success: false,
139
- shellType: detectedShell,
140
- rcFile: "",
141
- message: "Unknown shell type",
142
- error: "Could not detect shell type. Please specify manually."
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
- const rcFile = getShellRcFile(detectedShell);
146
- if (!rcFile) {
147
- return {
148
- success: false,
149
- shellType: detectedShell,
150
- rcFile: "",
151
- message: "RC file not found",
152
- error: `Could not find RC file for ${detectedShell}`
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
- if (isHookInstalled(rcFile)) {
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
- success: true,
158
- shellType: detectedShell,
159
- rcFile,
160
- message: "Shell hook is already installed"
2322
+ content: summaryContent,
2323
+ originalTokens: compressed.originalTokens,
2324
+ compressedTokens: compressed.compressedTokens,
2325
+ compressionRatio: compressed.compressionRatio,
2326
+ timestamp: Date.now()
161
2327
  };
162
2328
  }
163
- const hookScript = generateHookScript(detectedShell);
164
- try {
165
- if (!fs.existsSync(rcFile)) {
166
- const dir = path.dirname(rcFile);
167
- if (!fs.existsSync(dir)) {
168
- fs.mkdirSync(dir, { recursive: true });
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
- fs.appendFileSync(rcFile, `
173
- ${hookScript}
174
- `, "utf-8");
175
- return {
176
- success: true,
177
- shellType: detectedShell,
178
- rcFile,
179
- message: "Shell hook installed successfully"
180
- };
181
- } catch (error) {
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
- success: false,
184
- shellType: detectedShell,
185
- rcFile,
186
- message: "Failed to install shell hook",
187
- error: error instanceof Error ? error.message : String(error)
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
- async function uninstallShellHook(shellType) {
192
- const detectedShell = shellType || detectShellType();
193
- if (detectedShell === "unknown") {
194
- return {
195
- success: false,
196
- shellType: detectedShell,
197
- rcFile: "",
198
- message: "Unknown shell type",
199
- error: "Could not detect shell type. Please specify manually."
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
- const rcFile = getShellRcFile(detectedShell);
203
- if (!rcFile) {
204
- return {
205
- success: false,
206
- shellType: detectedShell,
207
- rcFile: "",
208
- message: "RC file not found",
209
- error: `Could not find RC file for ${detectedShell}`
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
- if (!fs.existsSync(rcFile)) {
213
- return {
214
- success: true,
215
- shellType: detectedShell,
216
- rcFile,
217
- message: "Shell hook is not installed"
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
- if (!isHookInstalled(rcFile)) {
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
- success: true,
223
- shellType: detectedShell,
224
- rcFile,
225
- message: "Shell hook is not installed"
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
- try {
229
- const content = fs.readFileSync(rcFile, "utf-8");
230
- const hookPattern = /\n*# CCJK Context Compression Hook[\s\S]*?# END CCJK Context Compression Hook\n*/g;
231
- const newContent = content.replace(hookPattern, "\n");
232
- fs.writeFileSync(rcFile, newContent, "utf-8");
233
- return {
234
- success: true,
235
- shellType: detectedShell,
236
- rcFile,
237
- message: "Shell hook uninstalled successfully"
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
- } catch (error) {
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
- success: false,
242
- shellType: detectedShell,
243
- rcFile,
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
- function getShellHookConfig(shellType) {
250
- const detectedShell = shellType || detectShellType();
251
- if (detectedShell === "unknown") {
252
- return null;
253
- }
254
- const rcFile = getShellRcFile(detectedShell);
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
- const PASSTHROUGH_ARGS = /* @__PURE__ */ new Set([
267
- "--help",
268
- "-h",
269
- "--version",
270
- "-v",
271
- "--mcp-list",
272
- "--mcp-debug",
273
- "update",
274
- "config"
275
- ]);
276
- function shouldPassthrough(args) {
277
- if (args.length === 0)
278
- return false;
279
- if (args[0]?.startsWith("/")) {
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
- return args.some((arg) => PASSTHROUGH_ARGS.has(arg));
283
- }
284
- async function claudeWrapper(args, options = {}) {
285
- const { debug = false, noWrap = false } = options;
286
- if (!i18n.isInitialized) {
287
- await initI18n();
288
- }
289
- const claudePath = await findRealCommandPath("claude");
290
- if (!claudePath) {
291
- console.error(i18n.t("context:claudeNotFound"));
292
- console.error(i18n.t("context:claudeNotFoundHint"));
293
- process__default.exit(1);
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
- if (debug) {
296
- console.log(`[DEBUG] Claude path: ${claudePath}`);
297
- console.log(`[DEBUG] Args: ${JSON.stringify(args)}`);
298
- console.log(`[DEBUG] No wrap: ${noWrap}`);
299
- console.log(`[DEBUG] Passthrough: ${shouldPassthrough(args)}`);
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
- if (noWrap || shouldPassthrough(args)) {
302
- if (debug) {
303
- console.log("[DEBUG] Using direct passthrough mode");
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
- await execClaudeDirect(claudePath, args);
309
- }
310
- async function execClaudeDirect(claudePath, args) {
311
- try {
312
- const result = await exec(claudePath, args, {
313
- nodeOptions: {
314
- stdio: "inherit"
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
- console.error(i18n.t("context:wrapperError"), error);
332
- process__default.exit(1);
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
- async function contextCommand(action, args, options = {}) {
336
- const { verbose = false } = options;
337
- if (!i18n.isInitialized) {
338
- await initI18n();
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
- try {
341
- if (action === "hook") {
342
- const hookAction = args[0];
343
- switch (hookAction) {
344
- case "install":
345
- await installHook(verbose);
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 "uninstall":
348
- await uninstallHook(verbose);
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(verbose);
3450
+ await this.showStatus();
352
3451
  break;
353
- default:
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
- switch (action) {
360
- case "status":
361
- await showStatus(verbose);
362
- break;
363
- case "install":
364
- await installHook(verbose);
365
- break;
366
- case "uninstall":
367
- await uninstallHook(verbose);
368
- break;
369
- case "help":
370
- default:
371
- showHelp();
372
- break;
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
- } catch (error) {
375
- console.error(i18n.t("context:commandError"), error);
376
- process__default.exit(1);
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
- async function showStatus(verbose) {
380
- const shellType = detectShellType();
381
- const rcFile = getShellRcFile(shellType);
382
- const installed = rcFile ? isHookInstalled(rcFile) : false;
383
- const claudePath = await findCommandPath("claude");
384
- console.log(`
385
- ${i18n.t("context:statusTitle")}`);
386
- console.log("\u2500".repeat(50));
387
- console.log(`${i18n.t("context:shellType")}: ${shellType}`);
388
- console.log(`${i18n.t("context:rcFile")}: ${rcFile || "N/A"}`);
389
- console.log(`${i18n.t("context:hookStatus")}: ${installed ? i18n.t("context:installed") : i18n.t("context:notInstalled")}`);
390
- if (verbose) {
391
- console.log(`Claude Path: ${claudePath || "Not found"}`);
392
- if (shellType !== "unknown") {
393
- const config = getShellHookConfig(shellType);
394
- if (config) {
395
- console.log("\nHook Script:");
396
- console.log(config.hookScript);
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
- async function installHook(_verbose) {
410
- console.log(i18n.t("context:installingHook"));
411
- const result = await installShellHook();
412
- if (result.success) {
413
- console.log(`\u2705 ${i18n.t("context:shellHookInstalled", { rcFile: result.rcFile })}`);
414
- console.log();
415
- console.log(i18n.t("context:restartShell"));
416
- console.log(` source ${result.rcFile}`);
417
- console.log();
418
- } else {
419
- console.error(`\u274C ${i18n.t("context:shellHookInstallFailed")}`);
420
- if (result.error) {
421
- console.error(` ${result.error}`);
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
- async function uninstallHook(_verbose) {
427
- console.log(i18n.t("context:uninstallingHook"));
428
- const result = await uninstallShellHook();
429
- if (result.success) {
430
- console.log(`\u2705 ${i18n.t("context:shellHookUninstalled", { rcFile: result.rcFile })}`);
431
- console.log();
432
- console.log(i18n.t("context:restartShell"));
433
- console.log(` source ${result.rcFile}`);
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
- } else {
436
- console.error(`\u274C ${i18n.t("context:shellHookUninstallFailed")}`);
437
- if (result.error) {
438
- console.error(` ${result.error}`);
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 showHelp() {
444
- console.log(`
445
- ${i18n.t("context:helpTitle")}`);
446
- console.log("\u2500".repeat(50));
447
- console.log(`
448
- ${i18n.t("context:helpUsage")}`);
449
- console.log(`
450
- ${i18n.t("context:helpActions")}`);
451
- console.log(` status - ${i18n.t("context:helpStatusDesc")}`);
452
- console.log(` install - ${i18n.t("context:helpInstallDesc")}`);
453
- console.log(` uninstall - ${i18n.t("context:helpUninstallDesc")}`);
454
- console.log(`
455
- ${i18n.t("context:helpExamples")}`);
456
- console.log(" ccjk context status");
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, contextCommand };
3657
+ export { claudeWrapper, claudeWrapperCommand };