nova-terminal-assistant 0.1.0

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.

Potentially problematic release.


This version of nova-terminal-assistant might be problematic. Click here for more details.

Files changed (192) hide show
  1. package/README.md +358 -0
  2. package/bin/nova +38 -0
  3. package/bin/nova.js +12 -0
  4. package/package.json +67 -0
  5. package/src/cli/commands/SmartCompletion.ts +458 -0
  6. package/src/cli/index.ts +5 -0
  7. package/src/cli/startup/IFlowRepl.ts +212 -0
  8. package/src/cli/startup/InkBasedRepl.ts +1056 -0
  9. package/src/cli/startup/InteractiveRepl.ts +2833 -0
  10. package/src/cli/startup/NovaApp.ts +1861 -0
  11. package/src/cli/startup/index.ts +4 -0
  12. package/src/cli/startup/parseArgs.ts +293 -0
  13. package/src/cli/test-modules.ts +27 -0
  14. package/src/cli/ui/IFlowDropdown.ts +425 -0
  15. package/src/cli/ui/ModernReplUI.ts +276 -0
  16. package/src/cli/ui/SimpleSelector2.ts +215 -0
  17. package/src/cli/ui/components/ConfirmDialog.ts +176 -0
  18. package/src/cli/ui/components/ErrorPanel.ts +364 -0
  19. package/src/cli/ui/components/InkAppRunner.tsx +67 -0
  20. package/src/cli/ui/components/InkComponents.tsx +613 -0
  21. package/src/cli/ui/components/NovaInkApp.tsx +312 -0
  22. package/src/cli/ui/components/ProgressBar.ts +177 -0
  23. package/src/cli/ui/components/ProgressIndicator.ts +298 -0
  24. package/src/cli/ui/components/QuickActions.ts +396 -0
  25. package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
  26. package/src/cli/ui/components/StatusBar.ts +194 -0
  27. package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
  28. package/src/cli/ui/components/index.ts +27 -0
  29. package/src/cli/ui/ink-prototype.tsx +347 -0
  30. package/src/cli/utils/CliUI.ts +336 -0
  31. package/src/cli/utils/CompletionHelper.ts +388 -0
  32. package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
  33. package/src/cli/utils/EnhancedCompleter.ts +513 -0
  34. package/src/cli/utils/ErrorEnhancer.ts +429 -0
  35. package/src/cli/utils/OutputFormatter.ts +193 -0
  36. package/src/cli/utils/index.ts +9 -0
  37. package/src/core/agents/AgentOrchestrator.ts +515 -0
  38. package/src/core/agents/index.ts +17 -0
  39. package/src/core/audit/AuditLogger.ts +509 -0
  40. package/src/core/audit/index.ts +11 -0
  41. package/src/core/auth/AuthManager.d.ts.map +1 -0
  42. package/src/core/auth/AuthManager.ts +138 -0
  43. package/src/core/auth/index.d.ts.map +1 -0
  44. package/src/core/auth/index.ts +2 -0
  45. package/src/core/config/ConfigManager.d.ts.map +1 -0
  46. package/src/core/config/ConfigManager.test.ts +183 -0
  47. package/src/core/config/ConfigManager.ts +1219 -0
  48. package/src/core/config/index.d.ts.map +1 -0
  49. package/src/core/config/index.ts +1 -0
  50. package/src/core/context/ContextBuilder.d.ts.map +1 -0
  51. package/src/core/context/ContextBuilder.ts +171 -0
  52. package/src/core/context/ContextCompressor.d.ts.map +1 -0
  53. package/src/core/context/ContextCompressor.ts +642 -0
  54. package/src/core/context/LayeredMemoryManager.ts +657 -0
  55. package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
  56. package/src/core/context/MemoryDiscovery.ts +175 -0
  57. package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
  58. package/src/core/context/defaultSystemPrompt.ts +35 -0
  59. package/src/core/context/index.d.ts.map +1 -0
  60. package/src/core/context/index.ts +22 -0
  61. package/src/core/extensions/SkillGenerator.ts +421 -0
  62. package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
  63. package/src/core/extensions/SkillInstaller.ts +257 -0
  64. package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
  65. package/src/core/extensions/SkillRegistry.ts +361 -0
  66. package/src/core/extensions/SkillValidator.ts +525 -0
  67. package/src/core/extensions/index.ts +15 -0
  68. package/src/core/index.d.ts.map +1 -0
  69. package/src/core/index.ts +42 -0
  70. package/src/core/mcp/McpManager.d.ts.map +1 -0
  71. package/src/core/mcp/McpManager.ts +632 -0
  72. package/src/core/mcp/index.d.ts.map +1 -0
  73. package/src/core/mcp/index.ts +2 -0
  74. package/src/core/model/ModelClient.d.ts.map +1 -0
  75. package/src/core/model/ModelClient.ts +217 -0
  76. package/src/core/model/ModelConnectionTester.ts +363 -0
  77. package/src/core/model/ModelValidator.ts +348 -0
  78. package/src/core/model/index.d.ts.map +1 -0
  79. package/src/core/model/index.ts +6 -0
  80. package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
  81. package/src/core/model/providers/AnthropicProvider.ts +279 -0
  82. package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
  83. package/src/core/model/providers/CodingPlanProvider.ts +210 -0
  84. package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
  85. package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
  86. package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
  87. package/src/core/model/providers/OllamaManager.ts +201 -0
  88. package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
  89. package/src/core/model/providers/OllamaProvider.ts +73 -0
  90. package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
  91. package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
  92. package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
  93. package/src/core/model/providers/OpenAIProvider.ts +29 -0
  94. package/src/core/model/providers/index.d.ts.map +1 -0
  95. package/src/core/model/providers/index.ts +12 -0
  96. package/src/core/model/types.d.ts.map +1 -0
  97. package/src/core/model/types.ts +77 -0
  98. package/src/core/security/ApprovalManager.d.ts.map +1 -0
  99. package/src/core/security/ApprovalManager.ts +174 -0
  100. package/src/core/security/FileFilter.d.ts.map +1 -0
  101. package/src/core/security/FileFilter.ts +141 -0
  102. package/src/core/security/HookExecutor.d.ts.map +1 -0
  103. package/src/core/security/HookExecutor.ts +178 -0
  104. package/src/core/security/SandboxExecutor.ts +447 -0
  105. package/src/core/security/index.d.ts.map +1 -0
  106. package/src/core/security/index.ts +8 -0
  107. package/src/core/session/AgentLoop.d.ts.map +1 -0
  108. package/src/core/session/AgentLoop.ts +501 -0
  109. package/src/core/session/SessionManager.d.ts.map +1 -0
  110. package/src/core/session/SessionManager.test.ts +183 -0
  111. package/src/core/session/SessionManager.ts +460 -0
  112. package/src/core/session/index.d.ts.map +1 -0
  113. package/src/core/session/index.ts +3 -0
  114. package/src/core/telemetry/Telemetry.d.ts.map +1 -0
  115. package/src/core/telemetry/Telemetry.ts +90 -0
  116. package/src/core/telemetry/TelemetryService.ts +531 -0
  117. package/src/core/telemetry/index.d.ts.map +1 -0
  118. package/src/core/telemetry/index.ts +12 -0
  119. package/src/core/testing/AutoFixer.ts +385 -0
  120. package/src/core/testing/ErrorAnalyzer.ts +499 -0
  121. package/src/core/testing/TestRunner.ts +265 -0
  122. package/src/core/testing/agent-cli-tests.ts +538 -0
  123. package/src/core/testing/index.ts +11 -0
  124. package/src/core/tools/ToolRegistry.d.ts.map +1 -0
  125. package/src/core/tools/ToolRegistry.test.ts +206 -0
  126. package/src/core/tools/ToolRegistry.ts +260 -0
  127. package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
  128. package/src/core/tools/impl/EditFileTool.ts +97 -0
  129. package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
  130. package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
  131. package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
  132. package/src/core/tools/impl/MemoryTool.ts +102 -0
  133. package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
  134. package/src/core/tools/impl/ReadFileTool.ts +58 -0
  135. package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
  136. package/src/core/tools/impl/SearchContentTool.ts +94 -0
  137. package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
  138. package/src/core/tools/impl/SearchFileTool.ts +61 -0
  139. package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
  140. package/src/core/tools/impl/ShellTool.ts +118 -0
  141. package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
  142. package/src/core/tools/impl/TaskTool.ts +207 -0
  143. package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
  144. package/src/core/tools/impl/TodoTool.ts +122 -0
  145. package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
  146. package/src/core/tools/impl/WebFetchTool.ts +103 -0
  147. package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
  148. package/src/core/tools/impl/WebSearchTool.ts +89 -0
  149. package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
  150. package/src/core/tools/impl/WriteFileTool.ts +49 -0
  151. package/src/core/tools/impl/index.d.ts.map +1 -0
  152. package/src/core/tools/impl/index.ts +16 -0
  153. package/src/core/tools/index.d.ts.map +1 -0
  154. package/src/core/tools/index.ts +7 -0
  155. package/src/core/tools/schemas/execution.d.ts.map +1 -0
  156. package/src/core/tools/schemas/execution.ts +42 -0
  157. package/src/core/tools/schemas/file.d.ts.map +1 -0
  158. package/src/core/tools/schemas/file.ts +119 -0
  159. package/src/core/tools/schemas/index.d.ts.map +1 -0
  160. package/src/core/tools/schemas/index.ts +11 -0
  161. package/src/core/tools/schemas/memory.d.ts.map +1 -0
  162. package/src/core/tools/schemas/memory.ts +52 -0
  163. package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
  164. package/src/core/tools/schemas/orchestration.ts +44 -0
  165. package/src/core/tools/schemas/search.d.ts.map +1 -0
  166. package/src/core/tools/schemas/search.ts +112 -0
  167. package/src/core/tools/schemas/todo.d.ts.map +1 -0
  168. package/src/core/tools/schemas/todo.ts +32 -0
  169. package/src/core/tools/schemas/web.d.ts.map +1 -0
  170. package/src/core/tools/schemas/web.ts +86 -0
  171. package/src/core/types/config.d.ts.map +1 -0
  172. package/src/core/types/config.ts +200 -0
  173. package/src/core/types/errors.d.ts.map +1 -0
  174. package/src/core/types/errors.ts +204 -0
  175. package/src/core/types/index.d.ts.map +1 -0
  176. package/src/core/types/index.ts +8 -0
  177. package/src/core/types/session.d.ts.map +1 -0
  178. package/src/core/types/session.ts +216 -0
  179. package/src/core/types/tools.d.ts.map +1 -0
  180. package/src/core/types/tools.ts +157 -0
  181. package/src/core/utils/CheckpointManager.d.ts.map +1 -0
  182. package/src/core/utils/CheckpointManager.ts +327 -0
  183. package/src/core/utils/Logger.d.ts.map +1 -0
  184. package/src/core/utils/Logger.ts +98 -0
  185. package/src/core/utils/RetryManager.ts +471 -0
  186. package/src/core/utils/TokenCounter.d.ts.map +1 -0
  187. package/src/core/utils/TokenCounter.ts +414 -0
  188. package/src/core/utils/VectorMemoryStore.ts +440 -0
  189. package/src/core/utils/helpers.d.ts.map +1 -0
  190. package/src/core/utils/helpers.ts +89 -0
  191. package/src/core/utils/index.d.ts.map +1 -0
  192. package/src/core/utils/index.ts +19 -0
@@ -0,0 +1,642 @@
1
+ // ============================================================================
2
+ // ContextCompressor - Intelligent context compression with structured summarization
3
+ // Reference: Factory.ai research (98.6% compression, 4.04/5 accuracy)
4
+ // ============================================================================
5
+
6
+ import type { Message, ContentBlock, SessionId } from '../types/session.js';
7
+
8
+ // --- Structured Summary Types ---
9
+
10
+ export interface FileChange {
11
+ path: string;
12
+ action: 'created' | 'modified' | 'deleted';
13
+ summary: string;
14
+ timestamp: number;
15
+ }
16
+
17
+ export interface Decision {
18
+ id: string;
19
+ topic: string;
20
+ reasoning: string;
21
+ outcome: string;
22
+ timestamp: number;
23
+ }
24
+
25
+ export interface StructuredSummary {
26
+ /** Core intent of the current session */
27
+ sessionIntent: string;
28
+ /** Tracked file modifications */
29
+ fileModifications: FileChange[];
30
+ /** Key decisions made during the session */
31
+ decisions: Decision[];
32
+ /** Pending tasks / next steps */
33
+ nextSteps: string[];
34
+ /** Summary of completed actions */
35
+ completedActions: string[];
36
+ /** Total token count of this summary */
37
+ tokenCount: number;
38
+ /** Compression ratio (original / summary) */
39
+ compressionRatio: number;
40
+ /** When this summary was last updated */
41
+ lastUpdated: number;
42
+ }
43
+
44
+ export interface ContextState {
45
+ /** Current token usage estimate */
46
+ tokenUsage: number;
47
+ /** Maximum context window */
48
+ maxTokens: number;
49
+ /** Whether the conversation has precise history that shouldn't be lost */
50
+ hasPreciseHistory: boolean;
51
+ /** Number of messages in the conversation */
52
+ messageCount: number;
53
+ }
54
+
55
+ // --- Compression Strategy Types ---
56
+
57
+ type CompressionAction = 'compress' | 'keep' | 'summarize' | 'retrieve';
58
+
59
+ export interface CompressionResult {
60
+ action: CompressionAction;
61
+ /** Reasoning for the chosen action */
62
+ reason: string;
63
+ /** The resulting message list (may be compressed or original) */
64
+ messages: Message[];
65
+ /** If compression happened, the generated summary */
66
+ summary?: StructuredSummary;
67
+ /** Token count after compression */
68
+ resultingTokens: number;
69
+ /** Token count before compression */
70
+ originalTokens: number;
71
+ }
72
+
73
+ export interface CompressionOptions {
74
+ /** Maximum tokens for the context window (default: 128000) */
75
+ maxTokens?: number;
76
+ /** Threshold to start compressing (default: 0.6 = 60%) */
77
+ compressThreshold?: number;
78
+ /** Threshold for aggressive compression (default: 0.85 = 85%) */
79
+ aggressiveThreshold?: number;
80
+ /** Maximum number of recent messages to always keep (default: 5) */
81
+ keepRecentCount?: number;
82
+ /** Whether to extract structured info from removed messages */
83
+ extractStructuredInfo?: boolean;
84
+ /** Existing summary to merge into (for incremental compression) */
85
+ existingSummary?: StructuredSummary;
86
+ }
87
+
88
+ // --- Constants ---
89
+
90
+ const DEFAULT_OPTIONS: Required<CompressionOptions> = {
91
+ maxTokens: 128000,
92
+ compressThreshold: 0.6,
93
+ aggressiveThreshold: 0.85,
94
+ keepRecentCount: 5,
95
+ extractStructuredInfo: true,
96
+ existingSummary: undefined,
97
+ };
98
+
99
+ // --- ContextCompressor ---
100
+
101
+ export interface ContextCompressorOptions {
102
+ sessionId?: SessionId;
103
+ maxTokens?: number;
104
+ summaryModel?: string;
105
+ }
106
+
107
+ export class ContextCompressor {
108
+ private currentSummary: StructuredSummary | null = null;
109
+ private sessionId: SessionId | null = null;
110
+ private maxTokens: number;
111
+
112
+ constructor(options?: ContextCompressorOptions | SessionId) {
113
+ if (typeof options === 'string') {
114
+ this.sessionId = options;
115
+ this.maxTokens = DEFAULT_OPTIONS.maxTokens;
116
+ } else {
117
+ this.sessionId = options?.sessionId || null;
118
+ this.maxTokens = options?.maxTokens || DEFAULT_OPTIONS.maxTokens;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Analyze the context state and determine the best compression action.
124
+ */
125
+ shouldCompress(state: ContextState): CompressionAction {
126
+ const usage = state.tokenUsage / state.maxTokens;
127
+
128
+ if (usage < DEFAULT_OPTIONS.compressThreshold) {
129
+ return 'keep';
130
+ }
131
+
132
+ if (usage >= DEFAULT_OPTIONS.aggressiveThreshold && state.hasPreciseHistory) {
133
+ return 'retrieve';
134
+ }
135
+
136
+ if (usage >= DEFAULT_OPTIONS.aggressiveThreshold) {
137
+ return 'summarize';
138
+ }
139
+
140
+ // Between 60-85%: light compression
141
+ return 'compress';
142
+ }
143
+
144
+ /**
145
+ * Compress messages intelligently.
146
+ * - Keeps system messages always
147
+ * - Keeps the N most recent messages
148
+ * - Extracts structured information from removed messages
149
+ * - Generates a structured summary for the LLM context
150
+ */
151
+ compress(
152
+ messages: Message[],
153
+ options?: CompressionOptions
154
+ ): CompressionResult {
155
+ const opts = { ...DEFAULT_OPTIONS, ...options };
156
+ const originalTokens = this.estimateMessagesTokens(messages);
157
+
158
+ const state: ContextState = {
159
+ tokenUsage: originalTokens,
160
+ maxTokens: opts.maxTokens,
161
+ hasPreciseHistory: this.hasPreciseHistory(messages),
162
+ messageCount: messages.length,
163
+ };
164
+
165
+ const action = this.shouldCompress(state);
166
+
167
+ if (action === 'keep') {
168
+ return {
169
+ action: 'keep',
170
+ reason: `Context usage at ${((state.tokenUsage / state.maxTokens) * 100).toFixed(0)}%, below threshold of ${opts.compressThreshold * 100}%`,
171
+ messages,
172
+ resultingTokens: originalTokens,
173
+ originalTokens,
174
+ };
175
+ }
176
+
177
+ // Separate message types
178
+ const systemMessages = messages.filter((m) => m.role === 'system');
179
+ const nonSystemMessages = messages.filter((m) => m.role !== 'system');
180
+
181
+ if (nonSystemMessages.length <= opts.keepRecentCount) {
182
+ // Not enough messages to compress meaningfully
183
+ return {
184
+ action: 'keep',
185
+ reason: 'Too few non-system messages to compress',
186
+ messages,
187
+ resultingTokens: originalTokens,
188
+ originalTokens,
189
+ };
190
+ }
191
+
192
+ // Split into "old" (to be summarized) and "recent" (to be kept)
193
+ const recentMessages = nonSystemMessages.slice(-opts.keepRecentCount);
194
+ const oldMessages = nonSystemMessages.slice(0, -opts.keepRecentCount);
195
+
196
+ // Extract structured information from old messages
197
+ let summary = opts.existingSummary || this.currentSummary;
198
+ if (opts.extractStructuredInfo && oldMessages.length > 0) {
199
+ summary = this.extractStructuredInfo(oldMessages, summary);
200
+ this.currentSummary = summary;
201
+ }
202
+
203
+ // Build the compressed message list
204
+ const compressedMessages: Message[] = [
205
+ ...systemMessages,
206
+ ];
207
+
208
+ // Inject summary as a system-level context message if we have one
209
+ if (summary) {
210
+ compressedMessages.push(this.createSummaryMessage(summary));
211
+ }
212
+
213
+ // Add the recent messages back
214
+ compressedMessages.push(...recentMessages);
215
+
216
+ const resultingTokens = this.estimateMessagesTokens(compressedMessages);
217
+
218
+ return {
219
+ action,
220
+ reason: this.getCompressionReason(action, oldMessages.length, originalTokens, resultingTokens),
221
+ messages: compressedMessages,
222
+ summary,
223
+ resultingTokens,
224
+ originalTokens,
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Incremental compression: only process new messages since last compression,
230
+ * merge into existing summary. More efficient than full recompression.
231
+ */
232
+ incrementalCompress(
233
+ newMessages: Message[],
234
+ existingSummary: StructuredSummary | null
235
+ ): StructuredSummary {
236
+ if (!existingSummary) {
237
+ return this.extractStructuredInfo(newMessages, null);
238
+ }
239
+
240
+ const newInfo = this.extractNewInfo(newMessages);
241
+ return this.mergeSummary(existingSummary, newInfo);
242
+ }
243
+
244
+ /**
245
+ * Extract a structured summary from a set of messages.
246
+ * This analyzes tool calls, file operations, and conversation content
247
+ * to build a structured representation of what happened.
248
+ */
249
+ extractStructuredInfo(
250
+ messages: Message[],
251
+ base: StructuredSummary | null
252
+ ): StructuredSummary {
253
+ const fileChanges = base ? [...base.fileModifications] : [];
254
+ const decisions = base ? [...base.decisions] : [];
255
+ const nextSteps: string[] = base ? [...base.nextSteps] : [];
256
+ const completedActions: string[] = base ? [...base.completedActions] : [];
257
+
258
+ let sessionIntent = base?.sessionIntent || '';
259
+
260
+ for (const message of messages) {
261
+ for (const block of message.content) {
262
+ if (block.type === 'tool_use') {
263
+ this.processToolUse(block.name, block.input, fileChanges, completedActions, decisions);
264
+ } else if (block.type === 'text') {
265
+ // Try to extract session intent from user messages
266
+ if (message.role === 'user' && !sessionIntent && block.text.length > 5) {
267
+ sessionIntent = block.text.slice(0, 200);
268
+ }
269
+ // Extract explicit decisions or next steps patterns
270
+ this.extractFromText(block.text, decisions, nextSteps, completedActions);
271
+ }
272
+ }
273
+ }
274
+
275
+ // Deduplicate
276
+ const uniqueDecisions = this.deduplicateDecisions(decisions);
277
+ const uniqueNextSteps = [...new Set(nextSteps.filter((s) => s.length > 0))];
278
+
279
+ const summaryText = this.renderSummary(sessionIntent, fileChanges, uniqueDecisions, uniqueNextSteps, completedActions);
280
+ const tokenCount = this.estimateTokensText(summaryText);
281
+
282
+ return {
283
+ sessionIntent: sessionIntent || 'User conversation',
284
+ fileModifications: fileChanges.slice(-20), // Keep last 20 file changes
285
+ decisions: uniqueDecisions.slice(-10), // Keep last 10 decisions
286
+ nextSteps: uniqueNextSteps.slice(-10),
287
+ completedActions: completedActions.slice(-20),
288
+ tokenCount,
289
+ compressionRatio: 0, // Will be calculated externally
290
+ lastUpdated: Date.now(),
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Get the current structured summary.
296
+ */
297
+ getSummary(): StructuredSummary | null {
298
+ return this.currentSummary;
299
+ }
300
+
301
+ /**
302
+ * Set an existing summary (e.g., loaded from persistence).
303
+ */
304
+ setSummary(summary: StructuredSummary): void {
305
+ this.currentSummary = summary;
306
+ }
307
+
308
+ /**
309
+ * Clear the current summary.
310
+ */
311
+ clearSummary(): void {
312
+ this.currentSummary = null;
313
+ }
314
+
315
+ /**
316
+ * Render a structured summary into a text format suitable for LLM context.
317
+ */
318
+ renderSummaryToText(summary: StructuredSummary): string {
319
+ return this.renderSummary(
320
+ summary.sessionIntent,
321
+ summary.fileModifications,
322
+ summary.decisions,
323
+ summary.nextSteps,
324
+ summary.completedActions
325
+ );
326
+ }
327
+
328
+ // --- Private Methods ---
329
+
330
+ private createSummaryMessage(summary: StructuredSummary): Message {
331
+ const text = this.renderSummaryToText(summary);
332
+ return {
333
+ id: `summary-${Date.now()}` as any,
334
+ role: 'user' as const,
335
+ content: [{ type: 'text', text }],
336
+ timestamp: Date.now(),
337
+ createdAt: new Date(),
338
+ metadata: { isContextSummary: true },
339
+ };
340
+ }
341
+
342
+ private renderSummary(
343
+ intent: string,
344
+ files: FileChange[],
345
+ decisions: Decision[],
346
+ nextSteps: string[],
347
+ completed: string[]
348
+ ): string {
349
+ const parts: string[] = [];
350
+ const safeCompleted = Array.isArray(completed) ? completed : [];
351
+ const safeFiles = Array.isArray(files) ? files : [];
352
+ const safeDecisions = Array.isArray(decisions) ? decisions : [];
353
+ const safeNextSteps = Array.isArray(nextSteps) ? nextSteps : [];
354
+
355
+ parts.push('<context-summary>');
356
+ parts.push(`Session goal: ${intent}`);
357
+
358
+ if (safeCompleted.length > 0) {
359
+ parts.push(`Completed: ${safeCompleted.join('; ')}`);
360
+ }
361
+
362
+ if (safeFiles.length > 0) {
363
+ const fileStr = safeFiles.map((f) => `${f.action} ${f.path} (${f.summary})`).join('; ');
364
+ parts.push(`Files changed: ${fileStr}`);
365
+ }
366
+
367
+ if (safeDecisions.length > 0) {
368
+ const decisionStr = safeDecisions.map((d) => `${d.topic}: ${d.outcome}`).join('; ');
369
+ parts.push(`Decisions: ${decisionStr}`);
370
+ }
371
+
372
+ if (safeNextSteps.length > 0) {
373
+ parts.push(`Next: ${safeNextSteps.join('; ')}`);
374
+ }
375
+
376
+ parts.push('</context-summary>');
377
+ return parts.join('\n');
378
+ }
379
+
380
+ private extractNewInfo(messages: Message[]): Partial<StructuredSummary> {
381
+ // For incremental: just extract from the new messages
382
+ const tempBase: StructuredSummary = {
383
+ sessionIntent: '',
384
+ fileModifications: [],
385
+ decisions: [],
386
+ nextSteps: [],
387
+ completedActions: [],
388
+ tokenCount: 0,
389
+ compressionRatio: 0,
390
+ lastUpdated: Date.now(),
391
+ };
392
+ const extracted = this.extractStructuredInfo(messages, null);
393
+ return {
394
+ sessionIntent: extracted.sessionIntent,
395
+ fileModifications: extracted.fileModifications,
396
+ decisions: extracted.decisions,
397
+ nextSteps: extracted.nextSteps,
398
+ completedActions: extracted.completedActions,
399
+ };
400
+ }
401
+
402
+ private mergeSummary(
403
+ existing: StructuredSummary,
404
+ newInfo: Partial<StructuredSummary>
405
+ ): StructuredSummary {
406
+ const mergedFiles = [...(Array.isArray(existing.fileModifications) ? existing.fileModifications : [])];
407
+ for (const fc of newInfo.fileModifications || []) {
408
+ // Update if same path, otherwise add
409
+ const existingIdx = mergedFiles.findIndex((f) => f.path === fc.path);
410
+ if (existingIdx >= 0) {
411
+ mergedFiles[existingIdx] = fc; // Replace with newer version
412
+ } else {
413
+ mergedFiles.push(fc);
414
+ }
415
+ }
416
+
417
+ const mergedDecisions = [...(Array.isArray(existing.decisions) ? existing.decisions : [])];
418
+ for (const d of newInfo.decisions || []) {
419
+ const existingIdx = mergedDecisions.findIndex((ed) => ed.topic === d.topic);
420
+ if (existingIdx >= 0) {
421
+ mergedDecisions[existingIdx] = d;
422
+ } else {
423
+ mergedDecisions.push(d);
424
+ }
425
+ }
426
+
427
+ // Next steps: keep new ones, remove completed ones
428
+ const completedSet = new Set(newInfo.completedActions || []);
429
+ const mergedNextSteps = (newInfo.nextSteps || [])
430
+ .filter((s) => !completedSet.has(s))
431
+ .concat(
432
+ (existing.nextSteps || []).filter(
433
+ (s) => !completedSet.has(s) && !(newInfo.nextSteps || []).includes(s)
434
+ )
435
+ );
436
+
437
+ const allCompleted = [
438
+ ...(existing.completedActions || []),
439
+ ...(newInfo.completedActions || []),
440
+ ].filter((v, i, a) => a.indexOf(v) === i);
441
+
442
+ const summaryText = this.renderSummary(
443
+ newInfo.sessionIntent || existing.sessionIntent,
444
+ mergedFiles.slice(-20),
445
+ mergedDecisions.slice(-10),
446
+ mergedNextSteps.slice(-10),
447
+ allCompleted.slice(-20)
448
+ );
449
+
450
+ return {
451
+ sessionIntent: newInfo.sessionIntent || existing.sessionIntent,
452
+ fileModifications: mergedFiles.slice(-20),
453
+ decisions: mergedDecisions.slice(-10),
454
+ nextSteps: mergedNextSteps.slice(-10),
455
+ completedActions: allCompleted.slice(-20),
456
+ tokenCount: this.estimateTokensText(summaryText),
457
+ compressionRatio: 0,
458
+ lastUpdated: Date.now(),
459
+ };
460
+ }
461
+
462
+ private processToolUse(
463
+ toolName: string,
464
+ input: Record<string, unknown>,
465
+ fileChanges: FileChange[],
466
+ completedActions: string[],
467
+ decisions: Decision[]
468
+ ): void {
469
+ switch (toolName) {
470
+ case 'write_file': {
471
+ const path = String(input.file_path || input.filePath || 'unknown');
472
+ const summary = String(input.content || '').slice(0, 100);
473
+ fileChanges.push({
474
+ path,
475
+ action: 'created',
476
+ summary: `Created (${summary.length} chars)`,
477
+ timestamp: Date.now(),
478
+ });
479
+ completedActions.push(`Created ${path}`);
480
+ break;
481
+ }
482
+ case 'edit_file': {
483
+ const path = String(input.file_path || input.filePath || 'unknown');
484
+ // Check if we already have a recent change for this file
485
+ const existing = fileChanges.find((f) => f.path === path && Date.now() - f.timestamp < 60000);
486
+ if (existing) {
487
+ existing.action = 'modified';
488
+ existing.summary = `Modified (${Date.now() - existing.timestamp}ms ago)`;
489
+ existing.timestamp = Date.now();
490
+ } else {
491
+ fileChanges.push({
492
+ path,
493
+ action: 'modified',
494
+ summary: 'Modified',
495
+ timestamp: Date.now(),
496
+ });
497
+ }
498
+ completedActions.push(`Edited ${path}`);
499
+ break;
500
+ }
501
+ case 'execute_command': {
502
+ const cmd = String((input as any).command || (input as any).params?.command || 'unknown');
503
+ completedActions.push(`Ran: ${cmd.slice(0, 80)}`);
504
+ break;
505
+ }
506
+ case 'search_content':
507
+ case 'search_file': {
508
+ const query = String(input.query || input.pattern || input.glob_pattern || 'unknown');
509
+ completedActions.push(`Searched: ${query.slice(0, 60)}`);
510
+ break;
511
+ }
512
+ case 'read_file': {
513
+ const path = String(input.file_path || input.filePath || 'unknown');
514
+ completedActions.push(`Read ${path}`);
515
+ break;
516
+ }
517
+ }
518
+ }
519
+
520
+ private extractFromText(
521
+ text: string,
522
+ decisions: Decision[],
523
+ nextSteps: string[],
524
+ completedActions: string[]
525
+ ): void {
526
+ // Extract "decided to" or "chose to" patterns
527
+ const decisionPatterns = [
528
+ /(?:decided|chose|opted)\s+to\s+(.+)/gi,
529
+ /(?:going|will)\s+(?:use|implement|go with|try)\s+(.+)/gi,
530
+ ];
531
+ for (const pattern of decisionPatterns) {
532
+ const matches = text.matchAll(pattern);
533
+ for (const match of matches) {
534
+ decisions.push({
535
+ id: `d-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
536
+ topic: match[1].slice(0, 50),
537
+ reasoning: '',
538
+ outcome: match[1].slice(0, 100),
539
+ timestamp: Date.now(),
540
+ });
541
+ }
542
+ }
543
+
544
+ // Extract "next step" / "todo" / "need to" patterns
545
+ const nextPatterns = [
546
+ /(?:next\s+(?:step|up)|todo|need\s+to|should|must)\s*[:\-]?\s*(.+)/gi,
547
+ /\d+\.\s+\*\*(.+)\*\*/g, // Markdown numbered bold items
548
+ ];
549
+ for (const pattern of nextPatterns) {
550
+ const matches = text.matchAll(pattern);
551
+ for (const match of matches) {
552
+ const step = match[1].trim().slice(0, 100);
553
+ if (step.length > 3) {
554
+ nextSteps.push(step);
555
+ }
556
+ }
557
+ }
558
+ }
559
+
560
+ private hasPreciseHistory(messages: Message[]): boolean {
561
+ // Check if there are any file edits or commands that represent precise work
562
+ for (const msg of messages) {
563
+ for (const block of msg.content) {
564
+ if (block.type === 'tool_use') {
565
+ if (block.name === 'edit_file' || block.name === 'write_file' || block.name === 'execute_command') {
566
+ return true;
567
+ }
568
+ }
569
+ }
570
+ }
571
+ return false;
572
+ }
573
+
574
+ private deduplicateDecisions(decisions: Decision[]): Decision[] {
575
+ const seen = new Set<string>();
576
+ return decisions.filter((d) => {
577
+ const key = d.topic.toLowerCase().slice(0, 30);
578
+ if (seen.has(key)) return false;
579
+ seen.add(key);
580
+ return true;
581
+ });
582
+ }
583
+
584
+ private getCompressionReason(
585
+ action: CompressionAction,
586
+ removedCount: number,
587
+ originalTokens: number,
588
+ resultingTokens: number
589
+ ): string {
590
+ const saved = originalTokens - resultingTokens;
591
+ const pct = originalTokens > 0 ? ((saved / originalTokens) * 100).toFixed(0) : '0';
592
+ switch (action) {
593
+ case 'compress':
594
+ return `Light compression: removed ${removedCount} old messages, saved ~${pct}% tokens`;
595
+ case 'summarize':
596
+ return `Summarized ${removedCount} old messages into structured summary, saved ~${pct}% tokens`;
597
+ case 'retrieve':
598
+ return `Aggressive compression with retrieval: summarized ${removedCount} messages, saved ~${pct}% tokens`;
599
+ default:
600
+ return `No compression needed`;
601
+ }
602
+ }
603
+
604
+ /** Exposed token estimation for messages (public API) */
605
+ estimateTokens(messages: Message[]): number {
606
+ return this.estimateMessagesTokens(messages);
607
+ }
608
+
609
+ /** Get compression history */
610
+ getCompressionHistory(): Array<{ action: CompressionAction; timestamp: Date; originalTokens: number; resultingTokens: number }> {
611
+ return [];
612
+ }
613
+
614
+ /** Get compressor stats */
615
+ getStats(): { totalCompressions: number; tokensSaved: number } {
616
+ return { totalCompressions: 0, tokensSaved: 0 };
617
+ }
618
+
619
+ private estimateTokensText(text: string): number {
620
+ return Math.ceil(text.length / 4);
621
+ }
622
+
623
+ private estimateMessagesTokens(messages: Message[]): number {
624
+ let total = 0;
625
+ for (const msg of messages) {
626
+ const content = Array.isArray(msg.content) ? msg.content : [];
627
+ for (const block of content) {
628
+ if (block.type === 'text') {
629
+ total += this.estimateTokensText(block.text);
630
+ } else if (block.type === 'tool_use') {
631
+ total += this.estimateTokensText(JSON.stringify(block.input));
632
+ } else if (block.type === 'tool_result') {
633
+ const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
634
+ total += this.estimateTokensText(content);
635
+ } else if (block.type === 'thinking') {
636
+ total += this.estimateTokensText(block.thinking);
637
+ }
638
+ }
639
+ }
640
+ return total;
641
+ }
642
+ }