codemini-cli 0.4.6 → 0.4.8
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/deployment.md +5 -5
- package/package.json +1 -1
- package/src/cli.js +5 -5
- package/src/commands/chat.js +8 -2
- package/src/commands/run.js +9 -3
- package/src/commands/skill.js +1 -1
- package/src/core/agent-loop.js +53 -13
- package/src/core/chat-runtime.js +622 -83
- package/src/core/config-store.js +9 -2
- package/src/core/fff-adapter.js +1 -1
- package/src/core/session-store.js +104 -9
- package/src/core/soul.js +13 -0
- package/templates/project-requirements/report-shell.html +2 -1
package/src/core/config-store.js
CHANGED
|
@@ -24,6 +24,7 @@ const DEFAULT_CONFIG = {
|
|
|
24
24
|
},
|
|
25
25
|
model: {
|
|
26
26
|
name: 'gpt-4.1-mini',
|
|
27
|
+
fast_name: '',
|
|
27
28
|
max_context_tokens: 202752
|
|
28
29
|
},
|
|
29
30
|
context: {
|
|
@@ -36,7 +37,7 @@ const DEFAULT_CONFIG = {
|
|
|
36
37
|
prompt_budget_audit: false
|
|
37
38
|
},
|
|
38
39
|
execution: {
|
|
39
|
-
mode: '
|
|
40
|
+
mode: 'normal',
|
|
40
41
|
always_allow_tools: [
|
|
41
42
|
'read',
|
|
42
43
|
'grep',
|
|
@@ -140,9 +141,15 @@ function normalizePolicyLists(config) {
|
|
|
140
141
|
next.shell = next.shell || {};
|
|
141
142
|
next.shell.default = normalizeShellName(next.shell.default);
|
|
142
143
|
next.execution = next.execution || {};
|
|
144
|
+
next.model = next.model || {};
|
|
145
|
+
if (typeof next.model.fast_name !== 'string' && typeof next.model.lite_name === 'string') {
|
|
146
|
+
next.model.fast_name = next.model.lite_name;
|
|
147
|
+
}
|
|
148
|
+
next.model.name = String(next.model.name || DEFAULT_CONFIG.model.name).trim() || DEFAULT_CONFIG.model.name;
|
|
149
|
+
next.model.fast_name = String(next.model.fast_name || '').trim();
|
|
143
150
|
next.execution.mode = ['auto', 'normal', 'plan'].includes(String(next.execution.mode || '').toLowerCase())
|
|
144
151
|
? String(next.execution.mode).toLowerCase()
|
|
145
|
-
: '
|
|
152
|
+
: 'normal';
|
|
146
153
|
const rawTools = Array.isArray(next.execution.always_allow_tools)
|
|
147
154
|
? next.execution.always_allow_tools
|
|
148
155
|
: [];
|
package/src/core/fff-adapter.js
CHANGED
|
@@ -7,6 +7,7 @@ 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 DEFAULT_SESSION_TITLE = '新会话';
|
|
10
11
|
|
|
11
12
|
function createSessionId() {
|
|
12
13
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
@@ -20,7 +21,7 @@ function sanitizeToolCall(tc, index) {
|
|
|
20
21
|
const fnArgsRaw = tc?.function?.arguments ?? tc?.arguments ?? '{}';
|
|
21
22
|
const fnArgs = typeof fnArgsRaw === 'string' ? fnArgsRaw : JSON.stringify(fnArgsRaw);
|
|
22
23
|
if (!fnName) return null;
|
|
23
|
-
|
|
24
|
+
const out = {
|
|
24
25
|
id,
|
|
25
26
|
type: 'function',
|
|
26
27
|
function: {
|
|
@@ -28,6 +29,32 @@ function sanitizeToolCall(tc, index) {
|
|
|
28
29
|
arguments: fnArgs
|
|
29
30
|
}
|
|
30
31
|
};
|
|
32
|
+
if (Number.isFinite(Number(tc?.durationMs))) out.durationMs = Number(tc.durationMs);
|
|
33
|
+
if (typeof tc?.summary === 'string' && tc.summary.trim()) out.summary = tc.summary.trim();
|
|
34
|
+
if (typeof tc?.status === 'string' && tc.status.trim()) out.status = tc.status.trim();
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeWhitespace(value) {
|
|
39
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stripMarkdown(value) {
|
|
43
|
+
return normalizeWhitespace(value)
|
|
44
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
45
|
+
.replace(/`([^`]*)`/g, '$1')
|
|
46
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
47
|
+
.replace(/^[#>*\-\d.\s]+/g, '')
|
|
48
|
+
.trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function deriveSessionTitle(messages = []) {
|
|
52
|
+
const firstUser = Array.isArray(messages)
|
|
53
|
+
? messages.find((msg) => msg?.role === 'user' && normalizeWhitespace(msg?.content))
|
|
54
|
+
: null;
|
|
55
|
+
const text = stripMarkdown(firstUser?.content || '');
|
|
56
|
+
if (!text) return DEFAULT_SESSION_TITLE;
|
|
57
|
+
return text.length > 48 ? `${text.slice(0, 45).trimEnd()}...` : text;
|
|
31
58
|
}
|
|
32
59
|
|
|
33
60
|
function sanitizeMessage(msg) {
|
|
@@ -41,7 +68,11 @@ function sanitizeMessage(msg) {
|
|
|
41
68
|
content
|
|
42
69
|
};
|
|
43
70
|
|
|
71
|
+
if (typeof msg?.model_content === 'string' && msg.model_content) out.model_content = msg.model_content;
|
|
44
72
|
if (msg?.tool_call_id) out.tool_call_id = String(msg.tool_call_id);
|
|
73
|
+
if (Number.isFinite(Number(msg?.tool_duration_ms))) out.tool_duration_ms = Number(msg.tool_duration_ms);
|
|
74
|
+
if (typeof msg?.tool_summary === 'string' && msg.tool_summary.trim()) out.tool_summary = msg.tool_summary.trim();
|
|
75
|
+
if (typeof msg?.tool_status === 'string' && msg.tool_status.trim()) out.tool_status = msg.tool_status.trim();
|
|
45
76
|
if (typeof msg?.name === 'string' && msg.name.trim()) out.name = msg.name.trim();
|
|
46
77
|
if (typeof msg?.at === 'string' && msg.at.trim()) out.at = msg.at;
|
|
47
78
|
if (typeof msg?.reasoning_content === 'string' && msg.reasoning_content) {
|
|
@@ -58,6 +89,15 @@ function sanitizeMessage(msg) {
|
|
|
58
89
|
if (toolCalls.length > 0) out.tool_calls = toolCalls;
|
|
59
90
|
}
|
|
60
91
|
|
|
92
|
+
if (Array.isArray(msg?.plan_transcript)) {
|
|
93
|
+
out.plan_transcript = msg.plan_transcript
|
|
94
|
+
.filter((entry) => entry && typeof entry === 'object')
|
|
95
|
+
.map((entry) => ({
|
|
96
|
+
...entry,
|
|
97
|
+
segments: Array.isArray(entry.segments) ? entry.segments : []
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
|
|
61
101
|
return out;
|
|
62
102
|
}
|
|
63
103
|
|
|
@@ -73,9 +113,13 @@ function sanitizeSession(session, fallbackId = '') {
|
|
|
73
113
|
id,
|
|
74
114
|
createdAt,
|
|
75
115
|
updatedAt,
|
|
116
|
+
title: normalizeWhitespace(session?.title) || deriveSessionTitle(messages),
|
|
76
117
|
messages
|
|
77
118
|
};
|
|
78
119
|
|
|
120
|
+
if (typeof session?.projectDir === 'string' && session.projectDir.trim()) {
|
|
121
|
+
out.projectDir = session.projectDir.trim();
|
|
122
|
+
}
|
|
79
123
|
if (session?.model) out.model = String(session.model);
|
|
80
124
|
if (session?.mode) out.mode = String(session.mode);
|
|
81
125
|
const normalizedPlan = normalizePlanState(session?.planState);
|
|
@@ -91,22 +135,40 @@ function sessionPathById(sessionId, ext = SESSION_JSONL_EXT) {
|
|
|
91
135
|
return path.join(getSessionsDir(), `${sessionId}${ext}`);
|
|
92
136
|
}
|
|
93
137
|
|
|
138
|
+
function isSafeSessionId(sessionId) {
|
|
139
|
+
return /^[A-Za-z0-9_.-]+$/.test(String(sessionId || ''));
|
|
140
|
+
}
|
|
141
|
+
|
|
94
142
|
function sessionIdFromFileName(fileName) {
|
|
95
143
|
if (fileName.endsWith(SESSION_JSONL_EXT)) return fileName.slice(0, -SESSION_JSONL_EXT.length);
|
|
96
144
|
if (fileName.endsWith(SESSION_LEGACY_EXT)) return fileName.slice(0, -SESSION_LEGACY_EXT.length);
|
|
97
145
|
return '';
|
|
98
146
|
}
|
|
99
147
|
|
|
148
|
+
async function listSessionFiles() {
|
|
149
|
+
const dir = getSessionsDir();
|
|
150
|
+
await fs.mkdir(dir, { recursive: true });
|
|
151
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
152
|
+
return entries
|
|
153
|
+
.filter((e) => e.isFile() && (e.name.endsWith(SESSION_JSONL_EXT) || e.name.endsWith(SESSION_LEGACY_EXT)))
|
|
154
|
+
.map((e) => path.join(dir, e.name));
|
|
155
|
+
}
|
|
156
|
+
|
|
100
157
|
function summarizeParsedSession(parsed, filePath) {
|
|
101
158
|
const id = parsed.id || sessionIdFromFileName(path.basename(filePath));
|
|
102
159
|
const updatedAt = parsed.updatedAt || parsed.createdAt || '';
|
|
103
160
|
const latestMessage = Array.isArray(parsed.messages) ? parsed.messages.at(-1) : null;
|
|
104
161
|
const preview = latestMessage?.content ? String(latestMessage.content).replace(/\s+/g, ' ').slice(0, 80) : '';
|
|
162
|
+
const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
105
163
|
return {
|
|
106
164
|
id,
|
|
165
|
+
title: normalizeWhitespace(parsed.title) || deriveSessionTitle(messages),
|
|
107
166
|
updatedAt,
|
|
108
167
|
messageCount: Array.isArray(parsed.messages) ? parsed.messages.length : 0,
|
|
109
|
-
preview
|
|
168
|
+
preview,
|
|
169
|
+
projectDir: typeof parsed.projectDir === 'string' ? parsed.projectDir : '',
|
|
170
|
+
model: typeof parsed.model === 'string' ? parsed.model : '',
|
|
171
|
+
mode: typeof parsed.mode === 'string' ? parsed.mode : ''
|
|
110
172
|
};
|
|
111
173
|
}
|
|
112
174
|
|
|
@@ -148,7 +210,7 @@ async function loadSessionPayload(sessionId) {
|
|
|
148
210
|
}
|
|
149
211
|
}
|
|
150
212
|
|
|
151
|
-
export async function createSession() {
|
|
213
|
+
export async function createSession(projectDir = process.cwd()) {
|
|
152
214
|
const sessionId = createSessionId();
|
|
153
215
|
const dir = getSessionsDir();
|
|
154
216
|
await fs.mkdir(dir, { recursive: true });
|
|
@@ -157,6 +219,8 @@ export async function createSession() {
|
|
|
157
219
|
id: sessionId,
|
|
158
220
|
createdAt: new Date().toISOString(),
|
|
159
221
|
updatedAt: new Date().toISOString(),
|
|
222
|
+
title: DEFAULT_SESSION_TITLE,
|
|
223
|
+
projectDir: String(projectDir || process.cwd()),
|
|
160
224
|
messages: []
|
|
161
225
|
};
|
|
162
226
|
await fs.writeFile(filePath, `${JSON.stringify(payload)}\n`, 'utf8');
|
|
@@ -185,12 +249,7 @@ export async function resolveSession(sessionId) {
|
|
|
185
249
|
}
|
|
186
250
|
|
|
187
251
|
export async function listSessions(limit = 30) {
|
|
188
|
-
const
|
|
189
|
-
await fs.mkdir(dir, { recursive: true });
|
|
190
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
191
|
-
const files = entries
|
|
192
|
-
.filter((e) => e.isFile() && (e.name.endsWith(SESSION_JSONL_EXT) || e.name.endsWith(SESSION_LEGACY_EXT)))
|
|
193
|
-
.map((e) => path.join(dir, e.name));
|
|
252
|
+
const files = await listSessionFiles();
|
|
194
253
|
|
|
195
254
|
const sessionsById = new Map();
|
|
196
255
|
for (const file of files) {
|
|
@@ -212,6 +271,42 @@ export async function listSessions(limit = 30) {
|
|
|
212
271
|
return sessions.filter((s) => Number(s.messageCount || 0) > 0).slice(0, limit);
|
|
213
272
|
}
|
|
214
273
|
|
|
274
|
+
export async function deleteSession(sessionId) {
|
|
275
|
+
const id = String(sessionId || '').trim();
|
|
276
|
+
if (!id || !isSafeSessionId(id)) {
|
|
277
|
+
throw new Error('Invalid session id');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const files = await listSessionFiles();
|
|
281
|
+
const targets = new Set();
|
|
282
|
+
for (const file of files) {
|
|
283
|
+
const fileId = sessionIdFromFileName(path.basename(file));
|
|
284
|
+
if (fileId === id) {
|
|
285
|
+
targets.add(file);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const parsed = file.endsWith(SESSION_JSONL_EXT) ? await loadLatestJsonlObject(file) : await tryReadJson(file);
|
|
290
|
+
if (String(parsed?.id || '').trim() === id) targets.add(file);
|
|
291
|
+
} catch {}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let removed = 0;
|
|
295
|
+
const fallbackTargets = [
|
|
296
|
+
sessionPathById(id, SESSION_JSONL_EXT),
|
|
297
|
+
sessionPathById(id, SESSION_LEGACY_EXT)
|
|
298
|
+
];
|
|
299
|
+
for (const file of [...targets, ...fallbackTargets]) {
|
|
300
|
+
try {
|
|
301
|
+
await fs.unlink(file);
|
|
302
|
+
removed += 1;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return { removed };
|
|
308
|
+
}
|
|
309
|
+
|
|
215
310
|
export async function pruneSessions(policy = {}) {
|
|
216
311
|
const maxSessions = Number(policy.max_sessions || 100);
|
|
217
312
|
const retentionDays = Number(policy.retention_days || 30);
|
package/src/core/soul.js
CHANGED
|
@@ -7,6 +7,12 @@ import { buildSystemPromptWithReplyLanguage } from './reply-language.js';
|
|
|
7
7
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
8
8
|
const BUNDLED_SOULS_DIR = path.resolve(MODULE_DIR, '..', '..', 'souls');
|
|
9
9
|
|
|
10
|
+
function getCustomSoulsDir() {
|
|
11
|
+
return path.join(getBaseConfigDir(), 'souls');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { BUNDLED_SOULS_DIR, getCustomSoulsDir };
|
|
15
|
+
|
|
10
16
|
function normalizeSoulName(value) {
|
|
11
17
|
const name = String(value || '').trim().toLowerCase();
|
|
12
18
|
return name || 'default';
|
|
@@ -32,6 +38,13 @@ export async function loadSoulPrompt(config = {}) {
|
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
const preset = normalizeSoulName(config?.soul?.preset);
|
|
41
|
+
// Check custom souls dir first, then bundled
|
|
42
|
+
const customPresetPath = path.join(getCustomSoulsDir(), `${preset}.md`);
|
|
43
|
+
try {
|
|
44
|
+
const content = await fs.readFile(customPresetPath, 'utf8');
|
|
45
|
+
const text = String(content || '').trim();
|
|
46
|
+
if (text) return `[Soul preset: ${preset}]\n${text}`;
|
|
47
|
+
} catch {}
|
|
35
48
|
const presetPath = path.join(BUNDLED_SOULS_DIR, `${preset}.md`);
|
|
36
49
|
try {
|
|
37
50
|
const content = await fs.readFile(presetPath, 'utf8');
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
padding: 48px min(6vw, 80px) 28px;
|
|
46
46
|
border-bottom: 1px solid var(--line);
|
|
47
47
|
background: var(--bg);
|
|
48
|
+
text-align: center;
|
|
48
49
|
}
|
|
49
50
|
h1, h2, h3, h4 { line-height: 1.25; color: var(--text); }
|
|
50
51
|
h1 { margin: 0 0 8px; font-size: clamp(26px, 3.5vw, 38px); font-weight: 700; letter-spacing: -0.02em; }
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
color: var(--text-secondary);
|
|
57
58
|
font-size: 13px;
|
|
58
59
|
display: flex;
|
|
60
|
+
justify-content: center;
|
|
59
61
|
gap: 16px;
|
|
60
62
|
flex-wrap: wrap;
|
|
61
63
|
margin-top: 4px;
|
|
@@ -115,7 +117,6 @@
|
|
|
115
117
|
nav a.active .nav-dot { background: var(--accent); }
|
|
116
118
|
/* ── Main Content ── */
|
|
117
119
|
main {
|
|
118
|
-
max-width: 920px;
|
|
119
120
|
padding: 8px min(5vw, 80px) 80px;
|
|
120
121
|
}
|
|
121
122
|
section {
|