codemini-cli 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemini-cli",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
5
5
  "keywords": [
6
6
  "cli",
@@ -29,7 +29,10 @@
29
29
  "test": "node --test tests/*.test.js",
30
30
  "build:web": "npm install --prefix codemini-web && npm run build --prefix codemini-web",
31
31
  "prepack": "npm run build:web",
32
- "pack:offline": "npm pack"
32
+ "pack:offline": "npm pack",
33
+ "bump:patch": "npm version patch --no-git-tag-version",
34
+ "bump:minor": "npm version minor --no-git-tag-version",
35
+ "bump:major": "npm version major --no-git-tag-version"
33
36
  },
34
37
  "files": [
35
38
  "bin",
@@ -37,6 +40,7 @@
37
40
  "codemini-web/server.js",
38
41
  "codemini-web/lib",
39
42
  "codemini-web/dist",
43
+ "codemini-web/codemini_logo.png",
40
44
  "souls",
41
45
  "templates",
42
46
  "skills",
package/src/cli.js CHANGED
@@ -4,9 +4,7 @@ import { handleConfig } from './commands/config.js';
4
4
  import { handleDoctor } from './commands/doctor.js';
5
5
  import { handleSkill } from './commands/skill.js';
6
6
  import { handleWeb } from './commands/web.js';
7
- import pkg from '../package.json' with { type: 'json' };
8
-
9
- const VERSION = pkg.version;
7
+ import { VERSION } from './core/version.js';
10
8
 
11
9
  function printHelp() {
12
10
  console.log(`codemini ${VERSION}
@@ -4,7 +4,7 @@ import { loadConfig } from '../core/config-store.js';
4
4
  import { createChatRuntime } from '../core/chat-runtime.js';
5
5
  import { buildDefaultSystemPrompt } from '../core/default-system-prompt.js';
6
6
  import { resolveSession } from '../core/session-store.js';
7
- import pkg from '../../package.json' with { type: 'json' };
7
+ import { VERSION } from '../core/version.js';
8
8
 
9
9
  function parseChatArgs(args) {
10
10
  const parsed = {
@@ -175,7 +175,7 @@ export async function handleChat(args) {
175
175
  language: config.ui?.language || 'zh',
176
176
  shellName: config.shell?.default || 'powershell',
177
177
  safeMode: config.policy?.safe_mode !== false,
178
- version: pkg.version
178
+ version: VERSION
179
179
  })
180
180
  );
181
181
 
@@ -21,7 +21,9 @@ import {
21
21
  compactMessagesLocally,
22
22
  estimateMessagesTokens,
23
23
  microCompactMessages,
24
- parseCompactArgs
24
+ parseCompactArgs,
25
+ buildTranscriptForLLM,
26
+ COMPACT_SUMMARY_PROMPT
25
27
  } from './context-compact.js';
26
28
  import { getReplyLanguage, getReplyLanguageName } from './reply-language.js';
27
29
  import { composeSystemPrompt } from './system-prompt-composer.js';
@@ -64,6 +66,15 @@ function toOpenAIMessages(sessionMessages) {
64
66
  return mapped;
65
67
  }
66
68
 
69
+ function translateCompactBoundaryToOriginal(sourceIsCompacted, compactMeta, compactBoundaryIndex) {
70
+ const boundary = Number(compactBoundaryIndex);
71
+ if (!Number.isFinite(boundary)) return undefined;
72
+ if (!sourceIsCompacted) return Math.max(0, boundary);
73
+ const previousBoundary = Number(compactMeta?.boundaryIndex);
74
+ if (!Number.isFinite(previousBoundary)) return Math.max(0, boundary);
75
+ return Math.max(0, previousBoundary + Math.max(0, boundary - 1));
76
+ }
77
+
67
78
  function slugify(input) {
68
79
  const base = String(input || '')
69
80
  .toLowerCase()
@@ -2392,6 +2403,33 @@ async function generateSessionTitle({ userText, assistantText = '', config, sign
2392
2403
  }
2393
2404
  }
2394
2405
 
2406
+ function createCompactSummaryGenerator(config, signal) {
2407
+ return async (olderMessages) => {
2408
+ const latestConfig = await loadConfig().catch(() => config);
2409
+ const effectiveConfig = latestConfig || config;
2410
+ const fastModel = resolveFastModel(effectiveConfig);
2411
+ if (!fastModel) throw new Error('No fast model');
2412
+ const transcript = buildTranscriptForLLM(olderMessages);
2413
+ const result = await createChatCompletion({
2414
+ sdkProvider: effectiveConfig.sdk?.provider,
2415
+ baseUrl: effectiveConfig.gateway.base_url,
2416
+ apiKey: effectiveConfig.gateway.api_key,
2417
+ model: fastModel,
2418
+ messages: [
2419
+ { role: 'system', content: COMPACT_SUMMARY_PROMPT },
2420
+ { role: 'user', content: transcript.slice(0, 12000) }
2421
+ ],
2422
+ tools: [],
2423
+ timeoutMs: Math.min(Number(effectiveConfig.gateway?.timeout_ms || 30000), 60000),
2424
+ maxRetries: 0,
2425
+ signal
2426
+ });
2427
+ const text = result?.text?.trim();
2428
+ if (!text) throw new Error('Empty summary');
2429
+ return text;
2430
+ };
2431
+ }
2432
+
2395
2433
  function estimatePromptTokensForRequest(sessionMessages, userText = '') {
2396
2434
  const tokenMsgs = [
2397
2435
  ...(Array.isArray(sessionMessages) ? sessionMessages : []),
@@ -2630,14 +2668,21 @@ async function askModel({
2630
2668
  }
2631
2669
  }
2632
2670
  if (needsMacro) {
2633
- const macroSource = compacted ?? compactSource;
2634
- const auto = compactMessagesLocally(macroSource, {
2671
+ const sourceIsCompacted = Boolean(compacted);
2672
+ const macroSource = compacted ?? session.messages;
2673
+ const auto = await compactMessagesLocally(macroSource, {
2635
2674
  mode: preflightPct >= hardPct ? 'aggressive' : 'conservative',
2636
- force: true
2675
+ force: true,
2676
+ generateSummary: createCompactSummaryGenerator(config, signal)
2637
2677
  });
2638
2678
  if (auto.changed) {
2639
2679
  compacted = auto.compacted.map((m) => ({ ...m, at: new Date().toISOString() }));
2640
- if (onCompactedUpdate) onCompactedUpdate(compacted, { boundaryIndex: auto.boundaryIndex, mode: preflightPct >= hardPct ? 'aggressive' : 'conservative' });
2680
+ if (onCompactedUpdate) {
2681
+ onCompactedUpdate(compacted, {
2682
+ boundaryIndex: translateCompactBoundaryToOriginal(sourceIsCompacted, session.compact, auto.boundaryIndex),
2683
+ mode: preflightPct >= hardPct ? 'aggressive' : 'conservative'
2684
+ });
2685
+ }
2641
2686
  if (onAgentEvent) {
2642
2687
  onAgentEvent({
2643
2688
  type: 'compact:auto',
@@ -2699,8 +2744,13 @@ async function askModel({
2699
2744
  const shouldGenerateTitle = !session.messages.some((msg) => msg?.role === 'user');
2700
2745
  const modelExtra =
2701
2746
  typeof modelText === 'string' && modelText && modelText !== text ? { model_content: modelText } : {};
2702
- session.messages.push(stampedMessage('user', text, modelExtra));
2703
- if (!shouldGenerateTitle) {
2747
+ const userMessage = stampedMessage('user', text, modelExtra);
2748
+ session.messages.push(userMessage);
2749
+ if (compacted) {
2750
+ compacted.push({ ...userMessage });
2751
+ if (onCompactedUpdate) onCompactedUpdate(compacted);
2752
+ }
2753
+ if (shouldReplaceSessionTitle(session.title)) {
2704
2754
  session.title = deriveSessionTitle(session.messages);
2705
2755
  }
2706
2756
  session.model = model || config.model.name;
@@ -2949,18 +2999,26 @@ async function askModel({
2949
2999
  }
2950
3000
  }
2951
3001
  }
3002
+ session.model = model || config.model.name;
3003
+ session.mode = executionMode || config.execution?.mode || 'normal';
3004
+ await flushScheduledSave();
3005
+ await saveSession(session);
3006
+ // Generate a better title asynchronously after saving
2952
3007
  if (shouldReplaceSessionTitle(session.title)) {
2953
- session.title = await generateSessionTitle({
3008
+ const titleSessionId = session.id;
3009
+ generateSessionTitle({
2954
3010
  userText: text,
2955
3011
  assistantText: loopResult.text || '',
2956
3012
  config,
2957
3013
  signal
2958
- });
3014
+ }).then(async (generatedTitle) => {
3015
+ if (generatedTitle && generatedTitle !== session.title) {
3016
+ session.title = generatedTitle;
3017
+ await saveSession(session);
3018
+ onTitleUpdateCallback?.(titleSessionId, generatedTitle);
3019
+ }
3020
+ }).catch(() => {});
2959
3021
  }
2960
- session.model = model || config.model.name;
2961
- session.mode = executionMode || config.execution?.mode || 'normal';
2962
- await flushScheduledSave();
2963
- await saveSession(session);
2964
3022
  try {
2965
3023
  await pruneSessions(config.sessions || {});
2966
3024
  } catch {
@@ -4140,6 +4198,7 @@ export async function createChatRuntime({
4140
4198
  session.projectDir = process.cwd();
4141
4199
  }
4142
4200
  let activeRequestToolApproval = typeof requestToolApproval === 'function' ? requestToolApproval : null;
4201
+ let onTitleUpdateCallback = null;
4143
4202
  const startupEvents = [];
4144
4203
  const initialIndex = await initializeProjectIndex(process.cwd()).catch(() => null);
4145
4204
  if (initialIndex?.summary) {
@@ -4205,9 +4264,16 @@ export async function createChatRuntime({
4205
4264
  const setCompactedView = (view, meta = {}) => {
4206
4265
  compactedForModel = view;
4207
4266
  currentSession.compact = view
4208
- ? { view, timestamp: new Date().toISOString(), ...meta }
4267
+ ? { ...(currentSession.compact || {}), view, timestamp: new Date().toISOString(), ...meta }
4209
4268
  : null;
4210
4269
  };
4270
+ const appendSessionMessage = (message) => {
4271
+ currentSession.messages.push(message);
4272
+ if (compactedForModel) {
4273
+ compactedForModel.push({ ...message });
4274
+ setCompactedView(compactedForModel);
4275
+ }
4276
+ };
4211
4277
  let historyIdCache = [currentSession.id];
4212
4278
  let historySessionCache = [
4213
4279
  {
@@ -4682,10 +4748,10 @@ export async function createChatRuntime({
4682
4748
 
4683
4749
  const persistLocalExchange = async (userText, systemText, { includeUser = true } = {}) => {
4684
4750
  if (includeUser && userText) {
4685
- currentSession.messages.push(stampedMessage('user', userText));
4751
+ appendSessionMessage(stampedMessage('user', userText));
4686
4752
  }
4687
4753
  if (systemText) {
4688
- currentSession.messages.push(stampedMessage('system', systemText));
4754
+ appendSessionMessage(stampedMessage('system', systemText));
4689
4755
  }
4690
4756
  if (shouldReplaceSessionTitle(currentSession.title)) {
4691
4757
  currentSession.title = deriveSessionTitle(currentSession.messages);
@@ -4697,26 +4763,34 @@ export async function createChatRuntime({
4697
4763
 
4698
4764
  const persistAssistantExchange = async (userText, assistantText, { includeUser = true, extra = {} } = {}) => {
4699
4765
  if (includeUser && userText) {
4700
- currentSession.messages.push(stampedMessage('user', userText));
4766
+ appendSessionMessage(stampedMessage('user', userText));
4701
4767
  }
4702
4768
  if (assistantText) {
4703
- currentSession.messages.push(stampedMessage('assistant', assistantText, extra));
4769
+ appendSessionMessage(stampedMessage('assistant', assistantText, extra));
4704
4770
  }
4771
+ currentSession.model = model || config.model.name;
4772
+ currentSession.mode = executionMode || config.execution?.mode || 'normal';
4773
+ await saveSession(currentSession);
4774
+ // Generate a better title asynchronously after saving
4705
4775
  if (shouldReplaceSessionTitle(currentSession.title)) {
4706
- currentSession.title = await generateSessionTitle({
4776
+ const titleSessionId = currentSession.id;
4777
+ generateSessionTitle({
4707
4778
  userText,
4708
4779
  assistantText,
4709
4780
  config
4710
- });
4781
+ }).then(async (generatedTitle) => {
4782
+ if (generatedTitle && generatedTitle !== currentSession.title) {
4783
+ currentSession.title = generatedTitle;
4784
+ await saveSession(currentSession);
4785
+ onTitleUpdateCallback?.(titleSessionId, generatedTitle);
4786
+ }
4787
+ }).catch(() => {});
4711
4788
  }
4712
- currentSession.model = model || config.model.name;
4713
- currentSession.mode = executionMode || config.execution?.mode || 'normal';
4714
- await saveSession(currentSession);
4715
4789
  };
4716
4790
 
4717
4791
  const persistUserExchange = async (userText) => {
4718
4792
  if (!userText) return;
4719
- currentSession.messages.push(stampedMessage('user', userText));
4793
+ appendSessionMessage(stampedMessage('user', userText));
4720
4794
  if (shouldReplaceSessionTitle(currentSession.title)) {
4721
4795
  currentSession.title = deriveSessionTitle(currentSession.messages);
4722
4796
  }
@@ -5691,7 +5765,9 @@ export async function createChatRuntime({
5691
5765
  return { type: 'system', text: report };
5692
5766
  }
5693
5767
 
5694
- const result = compactMessagesLocally(compactSource, { mode: compactState.mode, force: true });
5768
+ const sourceIsCompacted = Boolean(compactedForModel);
5769
+ const macroSource = compactedForModel ?? currentSession.messages;
5770
+ const result = await compactMessagesLocally(macroSource, { mode: compactState.mode, force: true, generateSummary: createCompactSummaryGenerator(config, null) });
5695
5771
  if (!result.changed) {
5696
5772
  return { type: 'system', text: 'Nothing to compact yet' };
5697
5773
  }
@@ -5704,7 +5780,10 @@ export async function createChatRuntime({
5704
5780
 
5705
5781
  setCompactedView(
5706
5782
  result.compacted.map((m) => ({ ...m, at: new Date().toISOString() })),
5707
- { boundaryIndex: result.boundaryIndex, mode: compactState.mode }
5783
+ {
5784
+ boundaryIndex: translateCompactBoundaryToOriginal(sourceIsCompacted, currentSession.compact, result.boundaryIndex),
5785
+ mode: compactState.mode
5786
+ }
5708
5787
  );
5709
5788
  await captureCompactSummary({
5710
5789
  summary: result.summary,
@@ -5923,10 +6002,12 @@ export async function createChatRuntime({
5923
6002
  }
5924
6003
  // Phase 1: macro compact if still over threshold
5925
6004
  if (needsMacro) {
5926
- const macroSource = compactedForModel ?? compactSource;
5927
- const autoResult = compactMessagesLocally(macroSource, {
6005
+ const sourceIsCompacted = Boolean(compactedForModel);
6006
+ const macroSource = compactedForModel ?? currentSession.messages;
6007
+ const autoResult = await compactMessagesLocally(macroSource, {
5928
6008
  mode: compactState.mode,
5929
- force: true
6009
+ force: true,
6010
+ generateSummary: createCompactSummaryGenerator(config, null)
5930
6011
  });
5931
6012
  if (autoResult.changed) {
5932
6013
  setCompactedView(
@@ -5934,7 +6015,14 @@ export async function createChatRuntime({
5934
6015
  ...m,
5935
6016
  at: new Date().toISOString()
5936
6017
  })),
5937
- { boundaryIndex: autoResult.boundaryIndex, mode: compactState.mode }
6018
+ {
6019
+ boundaryIndex: translateCompactBoundaryToOriginal(
6020
+ sourceIsCompacted,
6021
+ currentSession.compact,
6022
+ autoResult.boundaryIndex
6023
+ ),
6024
+ mode: compactState.mode
6025
+ }
5938
6026
  );
5939
6027
  await captureCompactSummary({
5940
6028
  summary: autoResult.summary,
@@ -6062,6 +6150,9 @@ export async function createChatRuntime({
6062
6150
  activeRequestToolApproval = typeof handler === 'function' ? handler : null;
6063
6151
  return true;
6064
6152
  },
6153
+ setOnTitleUpdate: (cb) => {
6154
+ onTitleUpdateCallback = typeof cb === 'function' ? cb : null;
6155
+ },
6065
6156
  dispose: async () => {
6066
6157
  if (typeof disposeTools === 'function') {
6067
6158
  await disposeTools();
@@ -39,6 +39,103 @@ function modeToKeepRecent(mode) {
39
39
  return 6;
40
40
  }
41
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
+
42
139
  function buildLocalSummary(messages) {
43
140
  const goal = [];
44
141
  const constraints = [];
@@ -103,6 +200,50 @@ function buildLocalSummary(messages) {
103
200
  return lines.join('\n').trim();
104
201
  }
105
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
+
106
247
  /**
107
248
  * Micro-compact: in-place clearing of old tool result content.
108
249
  * Does NOT change message count or order — only replaces tool result text
@@ -151,7 +292,7 @@ export function microCompactMessages(messages, { keepRecent = 5, enabled = true
151
292
  return { messages: result, changed: true, tokensSaved };
152
293
  }
153
294
 
154
- export function compactMessagesLocally(messages, { mode = 'default', force = false } = {}) {
295
+ export async function compactMessagesLocally(messages, { mode = 'default', force = false, generateSummary = null } = {}) {
155
296
  const keepRecent = modeToKeepRecent(mode);
156
297
  if (!Array.isArray(messages) || messages.length <= 1) {
157
298
  return {
@@ -167,12 +308,23 @@ export function compactMessagesLocally(messages, { mode = 'default', force = fal
167
308
  };
168
309
  }
169
310
 
170
- const older = messages.slice(0, Math.max(0, messages.length - keepRecent));
171
- const recent = messages.slice(Math.max(0, messages.length - keepRecent));
172
- const summary = buildLocalSummary(older);
173
- const compacted = [{ role: 'assistant', content: summary }, ...recent];
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
+ }
174
325
 
175
- const boundaryIndex = Math.max(0, messages.length - keepRecent);
326
+ const compacted = [{ role: 'assistant', content: summary }, ...recent];
327
+ const boundaryIndex = recentStart;
176
328
 
177
329
  return {
178
330
  compacted,
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { LANGUAGE_FILE_TYPES } from './constants.js';
3
+ import { getPackageInfo } from './version.js';
3
4
 
4
5
  const DEFAULT_COMMAND = 'fff-mcp';
5
6
  const DEFAULT_TIMEOUT_MS = 15_000;
@@ -109,10 +110,7 @@ class FffMcpClient {
109
110
  await this.sendRequest('initialize', {
110
111
  protocolVersion: '2024-11-05',
111
112
  capabilities: {},
112
- clientInfo: {
113
- name: 'codemini-cli',
114
- version: '0.5.4'
115
- }
113
+ clientInfo: getPackageInfo()
116
114
  });
117
115
  this.sendNotification('notifications/initialized', {});
118
116
  }
@@ -383,4 +381,3 @@ export function createFffAdapter({ workspaceRoot, config }) {
383
381
  }
384
382
  };
385
383
  }
386
-
@@ -0,0 +1,11 @@
1
+ import pkg from '../../package.json' with { type: 'json' };
2
+
3
+ export const PACKAGE_NAME = pkg.name || 'codemini-cli';
4
+ export const VERSION = pkg.version;
5
+
6
+ export function getPackageInfo() {
7
+ return {
8
+ name: PACKAGE_NAME,
9
+ version: VERSION
10
+ };
11
+ }