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/README.md +274 -472
- package/codemini-web/codemini_logo.png +0 -0
- package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-C0p6xCdM.js → highlighted-body-OFNGDK62-Cc6TA1Qw.js} +1 -1
- package/codemini-web/dist/assets/{index-fbvf7RS1.js → index-BqNKEgHB.js} +93 -91
- package/codemini-web/dist/assets/index-dYs_njBc.css +2 -0
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-JDxagHq_.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 +117 -55
- package/deployment.md +5 -5
- package/package.json +6 -2
- package/src/cli.js +1 -3
- package/src/commands/chat.js +2 -2
- package/src/core/chat-runtime.js +121 -30
- package/src/core/context-compact.js +158 -6
- package/src/core/fff-adapter.js +2 -5
- package/src/core/version.js +11 -0
- package/codemini-web/dist/assets/index-SZnRN7a3.css +0 -2
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-DgSNlEad.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.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
|
|
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';
|
|
@@ -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
|
|
2634
|
-
const
|
|
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)
|
|
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
|
-
|
|
2703
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4751
|
+
appendSessionMessage(stampedMessage('user', userText));
|
|
4686
4752
|
}
|
|
4687
4753
|
if (systemText) {
|
|
4688
|
-
|
|
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
|
-
|
|
4766
|
+
appendSessionMessage(stampedMessage('user', userText));
|
|
4701
4767
|
}
|
|
4702
4768
|
if (assistantText) {
|
|
4703
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
{
|
|
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
|
|
5927
|
-
const
|
|
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
|
-
{
|
|
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
|
|
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.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
|
+
}
|