codemini-cli 0.5.5 → 0.5.7
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/README.md +274 -472
- package/codemini-web/codemini_logo.png +0 -0
- package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-DC-rSI65.js → highlighted-body-OFNGDK62-Dp1CwQdI.js} +1 -1
- package/codemini-web/dist/assets/index-Bvd2jj3t.js +428 -0
- package/codemini-web/dist/assets/index-Csjkc1MY.css +2 -0
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-DSVp--w4.js +1 -0
- package/codemini-web/dist/index.html +2 -2
- package/codemini-web/lib/runtime-bridge.js +15 -0
- package/codemini-web/server.js +152 -72
- package/package.json +6 -2
- package/src/cli.js +1 -3
- package/src/commands/chat.js +2 -2
- package/src/core/chat-runtime.js +69 -18
- package/src/core/context-compact.js +158 -6
- package/src/core/fff-adapter.js +2 -5
- package/src/core/session-store.js +132 -21
- package/src/core/version.js +11 -0
- package/codemini-web/dist/assets/index-DErltUwI.css +0 -2
- package/codemini-web/dist/assets/index-cYRgZ3WI.js +0 -426
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-DdvTRIYD.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codemini-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
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
|
|
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}
|
package/src/commands/chat.js
CHANGED
|
@@ -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
|
|
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:
|
|
178
|
+
version: VERSION
|
|
179
179
|
})
|
|
180
180
|
);
|
|
181
181
|
|
package/src/core/chat-runtime.js
CHANGED
|
@@ -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';
|
|
@@ -2401,6 +2403,33 @@ async function generateSessionTitle({ userText, assistantText = '', config, sign
|
|
|
2401
2403
|
}
|
|
2402
2404
|
}
|
|
2403
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
|
+
|
|
2404
2433
|
function estimatePromptTokensForRequest(sessionMessages, userText = '') {
|
|
2405
2434
|
const tokenMsgs = [
|
|
2406
2435
|
...(Array.isArray(sessionMessages) ? sessionMessages : []),
|
|
@@ -2641,9 +2670,10 @@ async function askModel({
|
|
|
2641
2670
|
if (needsMacro) {
|
|
2642
2671
|
const sourceIsCompacted = Boolean(compacted);
|
|
2643
2672
|
const macroSource = compacted ?? session.messages;
|
|
2644
|
-
const auto = compactMessagesLocally(macroSource, {
|
|
2673
|
+
const auto = await compactMessagesLocally(macroSource, {
|
|
2645
2674
|
mode: preflightPct >= hardPct ? 'aggressive' : 'conservative',
|
|
2646
|
-
force: true
|
|
2675
|
+
force: true,
|
|
2676
|
+
generateSummary: createCompactSummaryGenerator(config, signal)
|
|
2647
2677
|
});
|
|
2648
2678
|
if (auto.changed) {
|
|
2649
2679
|
compacted = auto.compacted.map((m) => ({ ...m, at: new Date().toISOString() }));
|
|
@@ -2720,7 +2750,7 @@ async function askModel({
|
|
|
2720
2750
|
compacted.push({ ...userMessage });
|
|
2721
2751
|
if (onCompactedUpdate) onCompactedUpdate(compacted);
|
|
2722
2752
|
}
|
|
2723
|
-
if (
|
|
2753
|
+
if (shouldReplaceSessionTitle(session.title)) {
|
|
2724
2754
|
session.title = deriveSessionTitle(session.messages);
|
|
2725
2755
|
}
|
|
2726
2756
|
session.model = model || config.model.name;
|
|
@@ -2969,18 +2999,26 @@ async function askModel({
|
|
|
2969
2999
|
}
|
|
2970
3000
|
}
|
|
2971
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
|
|
2972
3007
|
if (shouldReplaceSessionTitle(session.title)) {
|
|
2973
|
-
|
|
3008
|
+
const titleSessionId = session.id;
|
|
3009
|
+
generateSessionTitle({
|
|
2974
3010
|
userText: text,
|
|
2975
3011
|
assistantText: loopResult.text || '',
|
|
2976
3012
|
config,
|
|
2977
3013
|
signal
|
|
2978
|
-
})
|
|
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(() => {});
|
|
2979
3021
|
}
|
|
2980
|
-
session.model = model || config.model.name;
|
|
2981
|
-
session.mode = executionMode || config.execution?.mode || 'normal';
|
|
2982
|
-
await flushScheduledSave();
|
|
2983
|
-
await saveSession(session);
|
|
2984
3022
|
try {
|
|
2985
3023
|
await pruneSessions(config.sessions || {});
|
|
2986
3024
|
} catch {
|
|
@@ -4160,6 +4198,7 @@ export async function createChatRuntime({
|
|
|
4160
4198
|
session.projectDir = process.cwd();
|
|
4161
4199
|
}
|
|
4162
4200
|
let activeRequestToolApproval = typeof requestToolApproval === 'function' ? requestToolApproval : null;
|
|
4201
|
+
let onTitleUpdateCallback = null;
|
|
4163
4202
|
const startupEvents = [];
|
|
4164
4203
|
const initialIndex = await initializeProjectIndex(process.cwd()).catch(() => null);
|
|
4165
4204
|
if (initialIndex?.summary) {
|
|
@@ -4729,16 +4768,24 @@ export async function createChatRuntime({
|
|
|
4729
4768
|
if (assistantText) {
|
|
4730
4769
|
appendSessionMessage(stampedMessage('assistant', assistantText, extra));
|
|
4731
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
|
|
4732
4775
|
if (shouldReplaceSessionTitle(currentSession.title)) {
|
|
4733
|
-
|
|
4776
|
+
const titleSessionId = currentSession.id;
|
|
4777
|
+
generateSessionTitle({
|
|
4734
4778
|
userText,
|
|
4735
4779
|
assistantText,
|
|
4736
4780
|
config
|
|
4737
|
-
})
|
|
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(() => {});
|
|
4738
4788
|
}
|
|
4739
|
-
currentSession.model = model || config.model.name;
|
|
4740
|
-
currentSession.mode = executionMode || config.execution?.mode || 'normal';
|
|
4741
|
-
await saveSession(currentSession);
|
|
4742
4789
|
};
|
|
4743
4790
|
|
|
4744
4791
|
const persistUserExchange = async (userText) => {
|
|
@@ -5720,7 +5767,7 @@ export async function createChatRuntime({
|
|
|
5720
5767
|
|
|
5721
5768
|
const sourceIsCompacted = Boolean(compactedForModel);
|
|
5722
5769
|
const macroSource = compactedForModel ?? currentSession.messages;
|
|
5723
|
-
const result = compactMessagesLocally(macroSource, { mode: compactState.mode, force: true });
|
|
5770
|
+
const result = await compactMessagesLocally(macroSource, { mode: compactState.mode, force: true, generateSummary: createCompactSummaryGenerator(config, null) });
|
|
5724
5771
|
if (!result.changed) {
|
|
5725
5772
|
return { type: 'system', text: 'Nothing to compact yet' };
|
|
5726
5773
|
}
|
|
@@ -5957,9 +6004,10 @@ export async function createChatRuntime({
|
|
|
5957
6004
|
if (needsMacro) {
|
|
5958
6005
|
const sourceIsCompacted = Boolean(compactedForModel);
|
|
5959
6006
|
const macroSource = compactedForModel ?? currentSession.messages;
|
|
5960
|
-
const autoResult = compactMessagesLocally(macroSource, {
|
|
6007
|
+
const autoResult = await compactMessagesLocally(macroSource, {
|
|
5961
6008
|
mode: compactState.mode,
|
|
5962
|
-
force: true
|
|
6009
|
+
force: true,
|
|
6010
|
+
generateSummary: createCompactSummaryGenerator(config, null)
|
|
5963
6011
|
});
|
|
5964
6012
|
if (autoResult.changed) {
|
|
5965
6013
|
setCompactedView(
|
|
@@ -6102,6 +6150,9 @@ export async function createChatRuntime({
|
|
|
6102
6150
|
activeRequestToolApproval = typeof handler === 'function' ? handler : null;
|
|
6103
6151
|
return true;
|
|
6104
6152
|
},
|
|
6153
|
+
setOnTitleUpdate: (cb) => {
|
|
6154
|
+
onTitleUpdateCallback = typeof cb === 'function' ? cb : null;
|
|
6155
|
+
},
|
|
6105
6156
|
dispose: async () => {
|
|
6106
6157
|
if (typeof disposeTools === 'function') {
|
|
6107
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
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
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
|
|
326
|
+
const compacted = [{ role: 'assistant', content: summary }, ...recent];
|
|
327
|
+
const boundaryIndex = recentStart;
|
|
176
328
|
|
|
177
329
|
return {
|
|
178
330
|
compacted,
|
package/src/core/fff-adapter.js
CHANGED
|
@@ -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.5'
|
|
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
|
-
|
|
@@ -7,6 +7,8 @@ import { normalizeTodos } from './todo-state.js';
|
|
|
7
7
|
const ALLOWED_ROLES = new Set(['system', 'user', 'assistant', 'tool']);
|
|
8
8
|
const SESSION_LEGACY_EXT = '.json';
|
|
9
9
|
const SESSION_JSONL_EXT = '.jsonl';
|
|
10
|
+
const SESSION_INDEX_FILE = 'index.json';
|
|
11
|
+
const SESSION_INDEX_VERSION = 1;
|
|
10
12
|
const DEFAULT_SESSION_TITLE = '新会话';
|
|
11
13
|
|
|
12
14
|
function createSessionId() {
|
|
@@ -153,6 +155,10 @@ function sessionPathById(sessionId, ext = SESSION_JSONL_EXT) {
|
|
|
153
155
|
return path.join(getSessionsDir(), `${sessionId}${ext}`);
|
|
154
156
|
}
|
|
155
157
|
|
|
158
|
+
function sessionIndexPath() {
|
|
159
|
+
return path.join(getSessionsDir(), SESSION_INDEX_FILE);
|
|
160
|
+
}
|
|
161
|
+
|
|
156
162
|
function isSafeSessionId(sessionId) {
|
|
157
163
|
return /^[A-Za-z0-9_.-]+$/.test(String(sessionId || ''));
|
|
158
164
|
}
|
|
@@ -172,6 +178,35 @@ async function listSessionFiles() {
|
|
|
172
178
|
.map((e) => path.join(dir, e.name));
|
|
173
179
|
}
|
|
174
180
|
|
|
181
|
+
async function listSessionFileMeta() {
|
|
182
|
+
const files = await listSessionFiles();
|
|
183
|
+
const meta = [];
|
|
184
|
+
for (const file of files) {
|
|
185
|
+
try {
|
|
186
|
+
const stat = await fs.stat(file);
|
|
187
|
+
meta.push({
|
|
188
|
+
name: path.basename(file),
|
|
189
|
+
size: stat.size,
|
|
190
|
+
mtimeMs: Math.trunc(stat.mtimeMs)
|
|
191
|
+
});
|
|
192
|
+
} catch {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
meta.sort((a, b) => a.name.localeCompare(b.name));
|
|
197
|
+
return meta;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function sameSessionFileMeta(a = [], b = []) {
|
|
201
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
202
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
203
|
+
if (a[i]?.name !== b[i]?.name) return false;
|
|
204
|
+
if (Number(a[i]?.size || 0) !== Number(b[i]?.size || 0)) return false;
|
|
205
|
+
if (Number(a[i]?.mtimeMs || 0) !== Number(b[i]?.mtimeMs || 0)) return false;
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
175
210
|
function summarizeParsedSession(parsed, filePath) {
|
|
176
211
|
const id = parsed.id || sessionIdFromFileName(path.basename(filePath));
|
|
177
212
|
const updatedAt = parsed.updatedAt || parsed.createdAt || '';
|
|
@@ -195,6 +230,88 @@ async function tryReadJson(filePath) {
|
|
|
195
230
|
return JSON.parse(raw);
|
|
196
231
|
}
|
|
197
232
|
|
|
233
|
+
async function readSessionIndex() {
|
|
234
|
+
try {
|
|
235
|
+
const index = await tryReadJson(sessionIndexPath());
|
|
236
|
+
if (index?.version !== SESSION_INDEX_VERSION || !Array.isArray(index?.sessions) || !Array.isArray(index?.files)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return index;
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function writeSessionIndex(index) {
|
|
246
|
+
const dir = getSessionsDir();
|
|
247
|
+
await fs.mkdir(dir, { recursive: true });
|
|
248
|
+
const filePath = sessionIndexPath();
|
|
249
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
250
|
+
const payload = {
|
|
251
|
+
version: SESSION_INDEX_VERSION,
|
|
252
|
+
updatedAt: new Date().toISOString(),
|
|
253
|
+
files: Array.isArray(index?.files) ? index.files : [],
|
|
254
|
+
sessions: Array.isArray(index?.sessions) ? index.sessions : []
|
|
255
|
+
};
|
|
256
|
+
await fs.writeFile(tempPath, `${JSON.stringify(payload)}\n`, 'utf8');
|
|
257
|
+
await fs.rename(tempPath, filePath);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function rebuildSessionIndex(fileMeta = null) {
|
|
261
|
+
const files = await listSessionFiles();
|
|
262
|
+
const sessionsById = new Map();
|
|
263
|
+
for (const file of files) {
|
|
264
|
+
try {
|
|
265
|
+
const parsed = file.endsWith(SESSION_JSONL_EXT) ? await loadLatestJsonlObject(file) : await tryReadJson(file);
|
|
266
|
+
const summary = summarizeParsedSession(parsed, file);
|
|
267
|
+
if (!summary.id) continue;
|
|
268
|
+
const existing = sessionsById.get(summary.id);
|
|
269
|
+
if (!existing || String(summary.updatedAt) > String(existing.updatedAt)) {
|
|
270
|
+
sessionsById.set(summary.id, summary);
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const sessions = Array.from(sessionsById.values());
|
|
278
|
+
sessions.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
|
|
279
|
+
const filesMeta = fileMeta || await listSessionFileMeta();
|
|
280
|
+
const index = { files: filesMeta, sessions };
|
|
281
|
+
await writeSessionIndex(index);
|
|
282
|
+
return { ...index, version: SESSION_INDEX_VERSION };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function getSessionIndex() {
|
|
286
|
+
const fileMeta = await listSessionFileMeta();
|
|
287
|
+
const index = await readSessionIndex();
|
|
288
|
+
if (index && sameSessionFileMeta(index.files, fileMeta)) return index;
|
|
289
|
+
return rebuildSessionIndex(fileMeta);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function upsertSessionIndexEntry(session, filePath) {
|
|
293
|
+
try {
|
|
294
|
+
const summary = summarizeParsedSession(session, filePath);
|
|
295
|
+
if (!summary.id) return;
|
|
296
|
+
const stat = await fs.stat(filePath);
|
|
297
|
+
const fileEntry = {
|
|
298
|
+
name: path.basename(filePath),
|
|
299
|
+
size: stat.size,
|
|
300
|
+
mtimeMs: Math.trunc(stat.mtimeMs)
|
|
301
|
+
};
|
|
302
|
+
const index = await readSessionIndex();
|
|
303
|
+
const files = Array.isArray(index?.files) ? index.files.filter((entry) => entry?.name !== fileEntry.name) : [];
|
|
304
|
+
files.push(fileEntry);
|
|
305
|
+
files.sort((a, b) => a.name.localeCompare(b.name));
|
|
306
|
+
const sessions = Array.isArray(index?.sessions) ? index.sessions.filter((entry) => entry?.id !== summary.id) : [];
|
|
307
|
+
sessions.push(summary);
|
|
308
|
+
sessions.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
|
|
309
|
+
await writeSessionIndex({ files, sessions });
|
|
310
|
+
} catch {
|
|
311
|
+
// Index updates are an optimization; session data remains authoritative.
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
198
315
|
async function loadLatestJsonlObject(filePath) {
|
|
199
316
|
const raw = await fs.readFile(filePath, 'utf8');
|
|
200
317
|
const lines = String(raw || '')
|
|
@@ -242,6 +359,7 @@ export async function createSession(projectDir = process.cwd()) {
|
|
|
242
359
|
messages: []
|
|
243
360
|
};
|
|
244
361
|
await fs.writeFile(filePath, `${JSON.stringify(payload)}\n`, 'utf8');
|
|
362
|
+
await upsertSessionIndexEntry(payload, filePath);
|
|
245
363
|
return payload;
|
|
246
364
|
}
|
|
247
365
|
|
|
@@ -257,6 +375,7 @@ export async function saveSession(session) {
|
|
|
257
375
|
normalized.updatedAt = new Date().toISOString();
|
|
258
376
|
const filePath = sessionPathById(normalized.id, SESSION_JSONL_EXT);
|
|
259
377
|
await fs.appendFile(filePath, `${JSON.stringify(normalized)}\n`, 'utf8');
|
|
378
|
+
await upsertSessionIndexEntry(normalized, filePath);
|
|
260
379
|
}
|
|
261
380
|
|
|
262
381
|
export async function resolveSession(sessionId) {
|
|
@@ -266,27 +385,11 @@ export async function resolveSession(sessionId) {
|
|
|
266
385
|
return createSession();
|
|
267
386
|
}
|
|
268
387
|
|
|
269
|
-
export async function listSessions(limit = 30) {
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
try {
|
|
275
|
-
const parsed = file.endsWith(SESSION_JSONL_EXT) ? await loadLatestJsonlObject(file) : await tryReadJson(file);
|
|
276
|
-
const summary = summarizeParsedSession(parsed, file);
|
|
277
|
-
if (!summary.id) continue;
|
|
278
|
-
const existing = sessionsById.get(summary.id);
|
|
279
|
-
if (!existing || String(summary.updatedAt) > String(existing.updatedAt)) {
|
|
280
|
-
sessionsById.set(summary.id, summary);
|
|
281
|
-
}
|
|
282
|
-
} catch {
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const sessions = Array.from(sessionsById.values());
|
|
288
|
-
sessions.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
|
|
289
|
-
return sessions.filter((s) => Number(s.messageCount || 0) > 0).slice(0, limit);
|
|
388
|
+
export async function listSessions(limit = 30, { includeEmpty = false } = {}) {
|
|
389
|
+
const index = await getSessionIndex();
|
|
390
|
+
return [...index.sessions]
|
|
391
|
+
.filter((s) => includeEmpty || Number(s.messageCount || 0) > 0)
|
|
392
|
+
.slice(0, limit);
|
|
290
393
|
}
|
|
291
394
|
|
|
292
395
|
export async function deleteSession(sessionId) {
|
|
@@ -322,6 +425,11 @@ export async function deleteSession(sessionId) {
|
|
|
322
425
|
if (error?.code !== 'ENOENT') throw error;
|
|
323
426
|
}
|
|
324
427
|
}
|
|
428
|
+
if (removed > 0) {
|
|
429
|
+
try {
|
|
430
|
+
await rebuildSessionIndex();
|
|
431
|
+
} catch {}
|
|
432
|
+
}
|
|
325
433
|
return { removed };
|
|
326
434
|
}
|
|
327
435
|
|
|
@@ -359,5 +467,8 @@ export async function pruneSessions(policy = {}) {
|
|
|
359
467
|
continue;
|
|
360
468
|
}
|
|
361
469
|
}
|
|
470
|
+
try {
|
|
471
|
+
await rebuildSessionIndex();
|
|
472
|
+
} catch {}
|
|
362
473
|
return { removed, kept: keepIds.size };
|
|
363
474
|
}
|
|
@@ -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
|
+
}
|