codemini-cli 0.5.10 → 0.5.11

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.
Files changed (59) hide show
  1. package/OPERATIONS.md +242 -242
  2. package/README.md +588 -588
  3. package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-7HL7yft8.js → highlighted-body-OFNGDK62-CANOG7Xg.js} +1 -1
  4. package/codemini-web/dist/assets/{index-BK75hMb2.js → index-B71xykPM.js} +108 -108
  5. package/codemini-web/dist/assets/index-Dkq1DdDX.css +2 -0
  6. package/codemini-web/dist/assets/mermaid-GHXKKRXX-Z_w7M93P.js +1 -0
  7. package/codemini-web/dist/index.html +23 -23
  8. package/codemini-web/lib/approval-manager.js +32 -32
  9. package/codemini-web/lib/runtime-bridge.js +17 -11
  10. package/codemini-web/server.js +534 -205
  11. package/deployment.md +212 -212
  12. package/package.json +1 -1
  13. package/skills/brainstorm/SKILL.md +77 -77
  14. package/skills/codemini.skills.json +40 -40
  15. package/skills/grill-me/SKILL.md +30 -30
  16. package/skills/superpowers-lite/SKILL.md +82 -82
  17. package/src/cli.js +74 -74
  18. package/src/commands/chat.js +210 -210
  19. package/src/commands/run.js +313 -313
  20. package/src/commands/skill.js +438 -304
  21. package/src/commands/web.js +57 -57
  22. package/src/core/agent-loop.js +980 -980
  23. package/src/core/ast.js +309 -307
  24. package/src/core/chat-runtime.js +6261 -6253
  25. package/src/core/command-evaluator.js +72 -72
  26. package/src/core/command-loader.js +311 -311
  27. package/src/core/command-policy.js +301 -301
  28. package/src/core/command-risk.js +156 -156
  29. package/src/core/config-store.js +289 -289
  30. package/src/core/constants.js +18 -1
  31. package/src/core/context-compact.js +365 -365
  32. package/src/core/default-system-prompt.js +114 -107
  33. package/src/core/dream-audit.js +105 -105
  34. package/src/core/dream-consolidate.js +229 -229
  35. package/src/core/dream-evaluator.js +185 -185
  36. package/src/core/fff-adapter.js +383 -383
  37. package/src/core/memory-store.js +543 -543
  38. package/src/core/project-index.js +737 -548
  39. package/src/core/project-instructions.js +98 -98
  40. package/src/core/provider/anthropic.js +514 -514
  41. package/src/core/provider/openai-compatible.js +501 -501
  42. package/src/core/reflect-skill.js +178 -178
  43. package/src/core/reply-language.js +40 -40
  44. package/src/core/session-store.js +474 -474
  45. package/src/core/shell-profile.js +237 -237
  46. package/src/core/shell.js +323 -323
  47. package/src/core/soul.js +69 -69
  48. package/src/core/system-prompt-composer.js +52 -52
  49. package/src/core/tool-args.js +199 -154
  50. package/src/core/tool-output.js +184 -184
  51. package/src/core/tool-result-store.js +206 -206
  52. package/src/core/tools.js +3024 -2893
  53. package/src/core/version.js +11 -11
  54. package/src/tui/chat-app.js +5171 -5171
  55. package/src/tui/tool-activity/presenters/misc.js +30 -30
  56. package/src/tui/tool-activity/presenters/system.js +20 -20
  57. package/templates/project-requirements/report-shell.html +582 -582
  58. package/codemini-web/dist/assets/index-BSdIdn3L.css +0 -2
  59. package/codemini-web/dist/assets/mermaid-GHXKKRXX-Dg9qh8mg.js +0 -1
@@ -1,365 +1,365 @@
1
- import { trimInline } from './string-utils.js';
2
- import { summarizeToolResult } from './tool-result-store.js';
3
-
4
- const MICRO_CLEAR_MARKER = '[Old tool result cleared by micro-compact]';
5
-
6
- function textFromContent(content) {
7
- if (typeof content === 'string') return content;
8
- if (Array.isArray(content)) {
9
- return content
10
- .map((part) => {
11
- if (typeof part === 'string') return part;
12
- if (part?.type === 'text') return part.text || '';
13
- return '';
14
- })
15
- .join('');
16
- }
17
- return '';
18
- }
19
-
20
- export function estimateMessagesTokens(messages) {
21
- let total = 0;
22
- for (const message of messages || []) {
23
- const roleOverhead = 6;
24
- const text = textFromContent(message.content);
25
- let asciiChars = 0;
26
- let nonAsciiChars = 0;
27
- for (const char of text) {
28
- if (char.charCodeAt(0) <= 0x7f) asciiChars += 1;
29
- else nonAsciiChars += 1;
30
- }
31
- total += roleOverhead + Math.ceil(asciiChars / 4) + Math.ceil(nonAsciiChars / 2);
32
- }
33
- return total;
34
- }
35
-
36
- function modeToKeepRecent(mode) {
37
- if (mode === 'aggressive') return 4;
38
- if (mode === 'conservative') return 10;
39
- return 6;
40
- }
41
-
42
- function getToolCallId(call) {
43
- return String(call?.id || '').trim();
44
- }
45
-
46
- function getMessageToolCallIds(message) {
47
- if (!Array.isArray(message?.tool_calls)) return [];
48
- return message.tool_calls.map(getToolCallId).filter(Boolean);
49
- }
50
-
51
- function toolResultNote(message) {
52
- const text = textFromContent(message?.content);
53
- let parsed;
54
- try { parsed = JSON.parse(text); } catch { parsed = null; }
55
- const summary = parsed && typeof parsed === 'object'
56
- ? summarizeToolResult(parsed)
57
- : text.replace(/\s+/g, ' ').trim();
58
- const clipped = summary.length > 600 ? `${summary.slice(0, 597)}...` : summary;
59
- return `[Compacted orphan tool result]\n${clipped || 'No content'}`;
60
- }
61
-
62
- function expandRecentStartToToolBoundary(messages, start) {
63
- let adjusted = Math.max(0, Math.min(start, messages.length));
64
- while (adjusted > 0 && messages[adjusted]?.role === 'tool') {
65
- adjusted -= 1;
66
- }
67
- if (
68
- adjusted > 0 &&
69
- messages[adjusted]?.role !== 'assistant' &&
70
- messages[adjusted + 1]?.role === 'tool'
71
- ) {
72
- adjusted += 1;
73
- }
74
- return adjusted;
75
- }
76
-
77
- function sanitizeRecentMessagesForModel(messages) {
78
- const out = [];
79
- let activeAssistantIndex = -1;
80
- let expectedToolIds = new Set();
81
- let matchedToolIds = new Set();
82
-
83
- const finalizeActiveAssistant = () => {
84
- if (activeAssistantIndex < 0) return;
85
- const assistant = out[activeAssistantIndex];
86
- if (!Array.isArray(assistant?.tool_calls)) {
87
- activeAssistantIndex = -1;
88
- expectedToolIds = new Set();
89
- matchedToolIds = new Set();
90
- return;
91
- }
92
- const toolCalls = assistant.tool_calls.filter((call) => matchedToolIds.has(getToolCallId(call)));
93
- if (toolCalls.length > 0) {
94
- out[activeAssistantIndex] = { ...assistant, tool_calls: toolCalls };
95
- } else {
96
- const { tool_calls, ...rest } = assistant;
97
- out[activeAssistantIndex] = rest;
98
- }
99
- activeAssistantIndex = -1;
100
- expectedToolIds = new Set();
101
- matchedToolIds = new Set();
102
- };
103
-
104
- for (const message of messages) {
105
- if (!message || typeof message !== 'object') continue;
106
- if (message.role === 'assistant') {
107
- finalizeActiveAssistant();
108
- const clone = { ...message };
109
- out.push(clone);
110
- const ids = getMessageToolCallIds(clone);
111
- if (ids.length > 0) {
112
- activeAssistantIndex = out.length - 1;
113
- expectedToolIds = new Set(ids);
114
- matchedToolIds = new Set();
115
- }
116
- continue;
117
- }
118
-
119
- if (message.role === 'tool') {
120
- const id = String(message.tool_call_id || '').trim();
121
- if (id && expectedToolIds.has(id)) {
122
- out.push({ ...message });
123
- matchedToolIds.add(id);
124
- continue;
125
- }
126
- finalizeActiveAssistant();
127
- out.push({ role: 'assistant', content: toolResultNote(message), at: message.at });
128
- continue;
129
- }
130
-
131
- finalizeActiveAssistant();
132
- out.push({ ...message });
133
- }
134
-
135
- finalizeActiveAssistant();
136
- return out;
137
- }
138
-
139
- function buildLocalSummary(messages) {
140
- const goal = [];
141
- const constraints = [];
142
- const changedFiles = new Set();
143
- const verification = [];
144
- const openThreads = [];
145
- const limit = 16;
146
- for (const msg of messages.slice(-limit)) {
147
- if (msg.role === 'tool') {
148
- const text = textFromContent(msg.content);
149
- let parsed;
150
- try { parsed = JSON.parse(text); } catch { parsed = null; }
151
- if (parsed && typeof parsed === 'object') {
152
- const summary = summarizeToolResult(parsed);
153
- if (parsed.path) changedFiles.add(String(parsed.path));
154
- if (parsed.command || parsed.code != null || parsed.stderr || parsed.stdout) {
155
- verification.push(summary);
156
- } else {
157
- openThreads.push(`tool_result: ${summary}`);
158
- }
159
- } else {
160
- const clipped = text.length > 120 ? `${text.slice(0, 117)}...` : text;
161
- const match = clipped.match(/([A-Za-z0-9_./-]+\.[A-Za-z0-9]+):\d+/);
162
- if (match) changedFiles.add(match[1]);
163
- openThreads.push(`tool_result: ${clipped}`);
164
- }
165
- continue;
166
- }
167
- if (msg.role === 'assistant') {
168
- const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
169
- const toolCallCount = Array.isArray(msg.tool_calls) ? msg.tool_calls.length : 0;
170
- const toolInfo = toolCallCount > 0 ? ` [called ${toolCallCount} tool(s)]` : '';
171
- const clipped = text.length > 300 ? `${text.slice(0, 297)}...` : text;
172
- if (clipped) openThreads.push(`assistant: ${clipped}${toolInfo}`);
173
- continue;
174
- }
175
- if (msg.role === 'user') {
176
- const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
177
- const clipped = text.length > 200 ? `${text.slice(0, 197)}...` : text;
178
- if (goal.length === 0) goal.push(clipped);
179
- else constraints.push(clipped);
180
- continue;
181
- }
182
- const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
183
- if (!text) continue;
184
- const clipped = text.length > 160 ? `${text.slice(0, 157)}...` : text;
185
- openThreads.push(`${msg.role}: ${clipped}`);
186
- }
187
- const lines = [
188
- 'Context Summary',
189
- 'Goal:',
190
- goal.length > 0 ? `- ${goal[0]}` : '- Unknown from compacted context',
191
- 'Key Constraints:',
192
- ...(constraints.length > 0 ? constraints.slice(-4).map((item) => `- ${item}`) : ['- None recorded']),
193
- 'Changed Files:',
194
- ...(changedFiles.size > 0 ? [...changedFiles].slice(0, 8).map((item) => `- ${item}`) : ['- None recorded']),
195
- 'Verification:',
196
- ...(verification.length > 0 ? verification.slice(-4).map((item) => `- ${item}`) : ['- None recorded']),
197
- 'Open Threads:',
198
- ...(openThreads.length > 0 ? openThreads.slice(-8).map((item) => `- ${item}`) : ['- None recorded'])
199
- ];
200
- return lines.join('\n').trim();
201
- }
202
-
203
- /**
204
- * Build a conversation transcript from messages for LLM summarization input.
205
- * Includes structured metadata (tool calls, file changes) alongside the text.
206
- */
207
- export function buildTranscriptForLLM(messages) {
208
- const parts = [];
209
- for (const msg of messages) {
210
- const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
211
- if (!text && !Array.isArray(msg.tool_calls) && msg.role !== 'user') continue;
212
- if (msg.role === 'user') {
213
- parts.push(`[User]\n${text.slice(0, 600)}`);
214
- } else if (msg.role === 'assistant') {
215
- let block = `[Assistant]\n${text.slice(0, 600)}`;
216
- if (Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
217
- const toolNames = msg.tool_calls.map(tc => tc.function?.name || tc.name || 'tool').join(', ');
218
- block += `\n[Called tools: ${toolNames}]`;
219
- }
220
- parts.push(block);
221
- } else if (msg.role === 'tool') {
222
- let parsed;
223
- try { parsed = JSON.parse(text); } catch { parsed = null; }
224
- if (parsed && typeof parsed === 'object') {
225
- const summary = summarizeToolResult(parsed);
226
- parts.push(`[Tool Result]\n${summary.slice(0, 400)}`);
227
- } else {
228
- parts.push(`[Tool Result]\n${text.slice(0, 300)}`);
229
- }
230
- }
231
- }
232
- return parts.join('\n\n');
233
- }
234
-
235
- export const COMPACT_SUMMARY_PROMPT = `Summarize the following conversation into a structured context summary that preserves all critical information for continuing the task. Be thorough and specific.
236
-
237
- Include:
238
- - The user's goal and requirements
239
- - Key decisions made and reasoning
240
- - Files that were read, modified, or created (with paths)
241
- - Current progress and what remains
242
- - Any errors encountered and how they were resolved
243
- - Important constraints or conventions discovered
244
-
245
- Write in the same language as the conversation. Be concise but do not omit important details.`;
246
-
247
- /**
248
- * Micro-compact: in-place clearing of old tool result content.
249
- * Does NOT change message count or order — only replaces tool result text
250
- * with a lightweight marker, preserving conversation structure.
251
- *
252
- * Strategy inspired by Claude Code's Phase 0 micro-compact:
253
- * keep recent N tool results intact, clear the rest.
254
- */
255
- export function microCompactMessages(messages, { keepRecent = 5, enabled = true } = {}) {
256
- if (!enabled || !Array.isArray(messages)) {
257
- return { messages: [...messages], changed: false, tokensSaved: 0 };
258
- }
259
-
260
- // Collect indices of all tool-role messages
261
- const toolIndices = [];
262
- for (let i = 0; i < messages.length; i++) {
263
- if (messages[i].role === 'tool') toolIndices.push(i);
264
- }
265
-
266
- if (toolIndices.length <= keepRecent) {
267
- return { messages: [...messages], changed: false, tokensSaved: 0 };
268
- }
269
-
270
- // Indices to clear = all except the last keepRecent
271
- const keepSet = new Set(toolIndices.slice(-keepRecent));
272
- const clearSet = new Set(toolIndices.filter((idx) => !keepSet.has(idx)));
273
-
274
- if (clearSet.size === 0) {
275
- return { messages: [...messages], changed: false, tokensSaved: 0 };
276
- }
277
-
278
- const beforeTokens = estimateMessagesTokens(messages);
279
- const result = messages.map((msg, i) => {
280
- if (!clearSet.has(i)) return msg;
281
- const text = textFromContent(msg.content);
282
- if (!text || text === MICRO_CLEAR_MARKER) return msg;
283
- return { ...msg, content: MICRO_CLEAR_MARKER };
284
- });
285
- const afterTokens = estimateMessagesTokens(result);
286
- const tokensSaved = beforeTokens - afterTokens;
287
-
288
- if (tokensSaved <= 0) {
289
- return { messages: [...messages], changed: false, tokensSaved: 0 };
290
- }
291
-
292
- return { messages: result, changed: true, tokensSaved };
293
- }
294
-
295
- export async function compactMessagesLocally(messages, { mode = 'default', force = false, generateSummary = null } = {}) {
296
- const keepRecent = modeToKeepRecent(mode);
297
- if (!Array.isArray(messages) || messages.length <= 1) {
298
- return {
299
- compacted: [...(messages || [])],
300
- changed: false
301
- };
302
- }
303
- // Skip compact when message count is low enough to keep all, unless forced
304
- if (!force && messages.length <= keepRecent + 1) {
305
- return {
306
- compacted: [...(messages || [])],
307
- changed: false
308
- };
309
- }
310
-
311
- const recentStart = expandRecentStartToToolBoundary(messages, Math.max(0, messages.length - keepRecent));
312
- const older = messages.slice(0, recentStart);
313
- const recent = sanitizeRecentMessagesForModel(messages.slice(recentStart));
314
-
315
- let summary;
316
- if (typeof generateSummary === 'function') {
317
- try {
318
- summary = await generateSummary(older);
319
- } catch {
320
- summary = buildLocalSummary(older);
321
- }
322
- } else {
323
- summary = buildLocalSummary(older);
324
- }
325
-
326
- const compacted = [{ role: 'assistant', content: summary }, ...recent];
327
- const boundaryIndex = recentStart;
328
-
329
- return {
330
- compacted,
331
- changed: true,
332
- summary,
333
- boundaryIndex
334
- };
335
- }
336
-
337
- export function parseCompactArgs(args = []) {
338
- const parsed = {
339
- mode: 'default',
340
- preview: false,
341
- restore: false,
342
- micro: false,
343
- auto: undefined,
344
- threshold: undefined
345
- };
346
-
347
- for (let i = 0; i < args.length; i += 1) {
348
- const arg = args[i];
349
- if (arg === '--preview') parsed.preview = true;
350
- if (arg === '--restore') parsed.restore = true;
351
- if (arg === '--micro') parsed.micro = true;
352
- if (arg === '--aggressive') parsed.mode = 'aggressive';
353
- if (arg === '--conservative') parsed.mode = 'conservative';
354
- if (arg === '--default') parsed.mode = 'default';
355
- if (arg === '--auto-on') parsed.auto = 'on';
356
- if (arg === '--auto-off') parsed.auto = 'off';
357
- if (arg === '--threshold') {
358
- const n = Number(args[i + 1]);
359
- if (!Number.isNaN(n)) parsed.threshold = n;
360
- i += 1;
361
- }
362
- }
363
-
364
- return parsed;
365
- }
1
+ import { trimInline } from './string-utils.js';
2
+ import { summarizeToolResult } from './tool-result-store.js';
3
+
4
+ const MICRO_CLEAR_MARKER = '[Old tool result cleared by micro-compact]';
5
+
6
+ function textFromContent(content) {
7
+ if (typeof content === 'string') return content;
8
+ if (Array.isArray(content)) {
9
+ return content
10
+ .map((part) => {
11
+ if (typeof part === 'string') return part;
12
+ if (part?.type === 'text') return part.text || '';
13
+ return '';
14
+ })
15
+ .join('');
16
+ }
17
+ return '';
18
+ }
19
+
20
+ export function estimateMessagesTokens(messages) {
21
+ let total = 0;
22
+ for (const message of messages || []) {
23
+ const roleOverhead = 6;
24
+ const text = textFromContent(message.content);
25
+ let asciiChars = 0;
26
+ let nonAsciiChars = 0;
27
+ for (const char of text) {
28
+ if (char.charCodeAt(0) <= 0x7f) asciiChars += 1;
29
+ else nonAsciiChars += 1;
30
+ }
31
+ total += roleOverhead + Math.ceil(asciiChars / 4) + Math.ceil(nonAsciiChars / 2);
32
+ }
33
+ return total;
34
+ }
35
+
36
+ function modeToKeepRecent(mode) {
37
+ if (mode === 'aggressive') return 4;
38
+ if (mode === 'conservative') return 10;
39
+ return 6;
40
+ }
41
+
42
+ function getToolCallId(call) {
43
+ return String(call?.id || '').trim();
44
+ }
45
+
46
+ function getMessageToolCallIds(message) {
47
+ if (!Array.isArray(message?.tool_calls)) return [];
48
+ return message.tool_calls.map(getToolCallId).filter(Boolean);
49
+ }
50
+
51
+ function toolResultNote(message) {
52
+ const text = textFromContent(message?.content);
53
+ let parsed;
54
+ try { parsed = JSON.parse(text); } catch { parsed = null; }
55
+ const summary = parsed && typeof parsed === 'object'
56
+ ? summarizeToolResult(parsed)
57
+ : text.replace(/\s+/g, ' ').trim();
58
+ const clipped = summary.length > 600 ? `${summary.slice(0, 597)}...` : summary;
59
+ return `[Compacted orphan tool result]\n${clipped || 'No content'}`;
60
+ }
61
+
62
+ function expandRecentStartToToolBoundary(messages, start) {
63
+ let adjusted = Math.max(0, Math.min(start, messages.length));
64
+ while (adjusted > 0 && messages[adjusted]?.role === 'tool') {
65
+ adjusted -= 1;
66
+ }
67
+ if (
68
+ adjusted > 0 &&
69
+ messages[adjusted]?.role !== 'assistant' &&
70
+ messages[adjusted + 1]?.role === 'tool'
71
+ ) {
72
+ adjusted += 1;
73
+ }
74
+ return adjusted;
75
+ }
76
+
77
+ function sanitizeRecentMessagesForModel(messages) {
78
+ const out = [];
79
+ let activeAssistantIndex = -1;
80
+ let expectedToolIds = new Set();
81
+ let matchedToolIds = new Set();
82
+
83
+ const finalizeActiveAssistant = () => {
84
+ if (activeAssistantIndex < 0) return;
85
+ const assistant = out[activeAssistantIndex];
86
+ if (!Array.isArray(assistant?.tool_calls)) {
87
+ activeAssistantIndex = -1;
88
+ expectedToolIds = new Set();
89
+ matchedToolIds = new Set();
90
+ return;
91
+ }
92
+ const toolCalls = assistant.tool_calls.filter((call) => matchedToolIds.has(getToolCallId(call)));
93
+ if (toolCalls.length > 0) {
94
+ out[activeAssistantIndex] = { ...assistant, tool_calls: toolCalls };
95
+ } else {
96
+ const { tool_calls, ...rest } = assistant;
97
+ out[activeAssistantIndex] = rest;
98
+ }
99
+ activeAssistantIndex = -1;
100
+ expectedToolIds = new Set();
101
+ matchedToolIds = new Set();
102
+ };
103
+
104
+ for (const message of messages) {
105
+ if (!message || typeof message !== 'object') continue;
106
+ if (message.role === 'assistant') {
107
+ finalizeActiveAssistant();
108
+ const clone = { ...message };
109
+ out.push(clone);
110
+ const ids = getMessageToolCallIds(clone);
111
+ if (ids.length > 0) {
112
+ activeAssistantIndex = out.length - 1;
113
+ expectedToolIds = new Set(ids);
114
+ matchedToolIds = new Set();
115
+ }
116
+ continue;
117
+ }
118
+
119
+ if (message.role === 'tool') {
120
+ const id = String(message.tool_call_id || '').trim();
121
+ if (id && expectedToolIds.has(id)) {
122
+ out.push({ ...message });
123
+ matchedToolIds.add(id);
124
+ continue;
125
+ }
126
+ finalizeActiveAssistant();
127
+ out.push({ role: 'assistant', content: toolResultNote(message), at: message.at });
128
+ continue;
129
+ }
130
+
131
+ finalizeActiveAssistant();
132
+ out.push({ ...message });
133
+ }
134
+
135
+ finalizeActiveAssistant();
136
+ return out;
137
+ }
138
+
139
+ function buildLocalSummary(messages) {
140
+ const goal = [];
141
+ const constraints = [];
142
+ const changedFiles = new Set();
143
+ const verification = [];
144
+ const openThreads = [];
145
+ const limit = 16;
146
+ for (const msg of messages.slice(-limit)) {
147
+ if (msg.role === 'tool') {
148
+ const text = textFromContent(msg.content);
149
+ let parsed;
150
+ try { parsed = JSON.parse(text); } catch { parsed = null; }
151
+ if (parsed && typeof parsed === 'object') {
152
+ const summary = summarizeToolResult(parsed);
153
+ if (parsed.path) changedFiles.add(String(parsed.path));
154
+ if (parsed.command || parsed.code != null || parsed.stderr || parsed.stdout) {
155
+ verification.push(summary);
156
+ } else {
157
+ openThreads.push(`tool_result: ${summary}`);
158
+ }
159
+ } else {
160
+ const clipped = text.length > 120 ? `${text.slice(0, 117)}...` : text;
161
+ const match = clipped.match(/([A-Za-z0-9_./-]+\.[A-Za-z0-9]+):\d+/);
162
+ if (match) changedFiles.add(match[1]);
163
+ openThreads.push(`tool_result: ${clipped}`);
164
+ }
165
+ continue;
166
+ }
167
+ if (msg.role === 'assistant') {
168
+ const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
169
+ const toolCallCount = Array.isArray(msg.tool_calls) ? msg.tool_calls.length : 0;
170
+ const toolInfo = toolCallCount > 0 ? ` [called ${toolCallCount} tool(s)]` : '';
171
+ const clipped = text.length > 300 ? `${text.slice(0, 297)}...` : text;
172
+ if (clipped) openThreads.push(`assistant: ${clipped}${toolInfo}`);
173
+ continue;
174
+ }
175
+ if (msg.role === 'user') {
176
+ const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
177
+ const clipped = text.length > 200 ? `${text.slice(0, 197)}...` : text;
178
+ if (goal.length === 0) goal.push(clipped);
179
+ else constraints.push(clipped);
180
+ continue;
181
+ }
182
+ const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
183
+ if (!text) continue;
184
+ const clipped = text.length > 160 ? `${text.slice(0, 157)}...` : text;
185
+ openThreads.push(`${msg.role}: ${clipped}`);
186
+ }
187
+ const lines = [
188
+ 'Context Summary',
189
+ 'Goal:',
190
+ goal.length > 0 ? `- ${goal[0]}` : '- Unknown from compacted context',
191
+ 'Key Constraints:',
192
+ ...(constraints.length > 0 ? constraints.slice(-4).map((item) => `- ${item}`) : ['- None recorded']),
193
+ 'Changed Files:',
194
+ ...(changedFiles.size > 0 ? [...changedFiles].slice(0, 8).map((item) => `- ${item}`) : ['- None recorded']),
195
+ 'Verification:',
196
+ ...(verification.length > 0 ? verification.slice(-4).map((item) => `- ${item}`) : ['- None recorded']),
197
+ 'Open Threads:',
198
+ ...(openThreads.length > 0 ? openThreads.slice(-8).map((item) => `- ${item}`) : ['- None recorded'])
199
+ ];
200
+ return lines.join('\n').trim();
201
+ }
202
+
203
+ /**
204
+ * Build a conversation transcript from messages for LLM summarization input.
205
+ * Includes structured metadata (tool calls, file changes) alongside the text.
206
+ */
207
+ export function buildTranscriptForLLM(messages) {
208
+ const parts = [];
209
+ for (const msg of messages) {
210
+ const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
211
+ if (!text && !Array.isArray(msg.tool_calls) && msg.role !== 'user') continue;
212
+ if (msg.role === 'user') {
213
+ parts.push(`[User]\n${text.slice(0, 600)}`);
214
+ } else if (msg.role === 'assistant') {
215
+ let block = `[Assistant]\n${text.slice(0, 600)}`;
216
+ if (Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
217
+ const toolNames = msg.tool_calls.map(tc => tc.function?.name || tc.name || 'tool').join(', ');
218
+ block += `\n[Called tools: ${toolNames}]`;
219
+ }
220
+ parts.push(block);
221
+ } else if (msg.role === 'tool') {
222
+ let parsed;
223
+ try { parsed = JSON.parse(text); } catch { parsed = null; }
224
+ if (parsed && typeof parsed === 'object') {
225
+ const summary = summarizeToolResult(parsed);
226
+ parts.push(`[Tool Result]\n${summary.slice(0, 400)}`);
227
+ } else {
228
+ parts.push(`[Tool Result]\n${text.slice(0, 300)}`);
229
+ }
230
+ }
231
+ }
232
+ return parts.join('\n\n');
233
+ }
234
+
235
+ export const COMPACT_SUMMARY_PROMPT = `Summarize the following conversation into a structured context summary that preserves all critical information for continuing the task. Be thorough and specific.
236
+
237
+ Include:
238
+ - The user's goal and requirements
239
+ - Key decisions made and reasoning
240
+ - Files that were read, modified, or created (with paths)
241
+ - Current progress and what remains
242
+ - Any errors encountered and how they were resolved
243
+ - Important constraints or conventions discovered
244
+
245
+ Write in the same language as the conversation. Be concise but do not omit important details.`;
246
+
247
+ /**
248
+ * Micro-compact: in-place clearing of old tool result content.
249
+ * Does NOT change message count or order — only replaces tool result text
250
+ * with a lightweight marker, preserving conversation structure.
251
+ *
252
+ * Strategy inspired by Claude Code's Phase 0 micro-compact:
253
+ * keep recent N tool results intact, clear the rest.
254
+ */
255
+ export function microCompactMessages(messages, { keepRecent = 5, enabled = true } = {}) {
256
+ if (!enabled || !Array.isArray(messages)) {
257
+ return { messages: [...messages], changed: false, tokensSaved: 0 };
258
+ }
259
+
260
+ // Collect indices of all tool-role messages
261
+ const toolIndices = [];
262
+ for (let i = 0; i < messages.length; i++) {
263
+ if (messages[i].role === 'tool') toolIndices.push(i);
264
+ }
265
+
266
+ if (toolIndices.length <= keepRecent) {
267
+ return { messages: [...messages], changed: false, tokensSaved: 0 };
268
+ }
269
+
270
+ // Indices to clear = all except the last keepRecent
271
+ const keepSet = new Set(toolIndices.slice(-keepRecent));
272
+ const clearSet = new Set(toolIndices.filter((idx) => !keepSet.has(idx)));
273
+
274
+ if (clearSet.size === 0) {
275
+ return { messages: [...messages], changed: false, tokensSaved: 0 };
276
+ }
277
+
278
+ const beforeTokens = estimateMessagesTokens(messages);
279
+ const result = messages.map((msg, i) => {
280
+ if (!clearSet.has(i)) return msg;
281
+ const text = textFromContent(msg.content);
282
+ if (!text || text === MICRO_CLEAR_MARKER) return msg;
283
+ return { ...msg, content: MICRO_CLEAR_MARKER };
284
+ });
285
+ const afterTokens = estimateMessagesTokens(result);
286
+ const tokensSaved = beforeTokens - afterTokens;
287
+
288
+ if (tokensSaved <= 0) {
289
+ return { messages: [...messages], changed: false, tokensSaved: 0 };
290
+ }
291
+
292
+ return { messages: result, changed: true, tokensSaved };
293
+ }
294
+
295
+ export async function compactMessagesLocally(messages, { mode = 'default', force = false, generateSummary = null } = {}) {
296
+ const keepRecent = modeToKeepRecent(mode);
297
+ if (!Array.isArray(messages) || messages.length <= 1) {
298
+ return {
299
+ compacted: [...(messages || [])],
300
+ changed: false
301
+ };
302
+ }
303
+ // Skip compact when message count is low enough to keep all, unless forced
304
+ if (!force && messages.length <= keepRecent + 1) {
305
+ return {
306
+ compacted: [...(messages || [])],
307
+ changed: false
308
+ };
309
+ }
310
+
311
+ const recentStart = expandRecentStartToToolBoundary(messages, Math.max(0, messages.length - keepRecent));
312
+ const older = messages.slice(0, recentStart);
313
+ const recent = sanitizeRecentMessagesForModel(messages.slice(recentStart));
314
+
315
+ let summary;
316
+ if (typeof generateSummary === 'function') {
317
+ try {
318
+ summary = await generateSummary(older);
319
+ } catch {
320
+ summary = buildLocalSummary(older);
321
+ }
322
+ } else {
323
+ summary = buildLocalSummary(older);
324
+ }
325
+
326
+ const compacted = [{ role: 'assistant', content: summary }, ...recent];
327
+ const boundaryIndex = recentStart;
328
+
329
+ return {
330
+ compacted,
331
+ changed: true,
332
+ summary,
333
+ boundaryIndex
334
+ };
335
+ }
336
+
337
+ export function parseCompactArgs(args = []) {
338
+ const parsed = {
339
+ mode: 'default',
340
+ preview: false,
341
+ restore: false,
342
+ micro: false,
343
+ auto: undefined,
344
+ threshold: undefined
345
+ };
346
+
347
+ for (let i = 0; i < args.length; i += 1) {
348
+ const arg = args[i];
349
+ if (arg === '--preview') parsed.preview = true;
350
+ if (arg === '--restore') parsed.restore = true;
351
+ if (arg === '--micro') parsed.micro = true;
352
+ if (arg === '--aggressive') parsed.mode = 'aggressive';
353
+ if (arg === '--conservative') parsed.mode = 'conservative';
354
+ if (arg === '--default') parsed.mode = 'default';
355
+ if (arg === '--auto-on') parsed.auto = 'on';
356
+ if (arg === '--auto-off') parsed.auto = 'off';
357
+ if (arg === '--threshold') {
358
+ const n = Number(args[i + 1]);
359
+ if (!Number.isNaN(n)) parsed.threshold = n;
360
+ i += 1;
361
+ }
362
+ }
363
+
364
+ return parsed;
365
+ }