codemini-cli 0.3.4 → 0.3.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 +20 -18
- package/package.json +6 -6
- package/souls/anime.md +12 -9
- package/souls/caveman.md +6 -6
- package/souls/ceo.md +10 -9
- package/souls/default.md +1 -1
- package/souls/pirate.md +6 -6
- package/souls/playful.md +7 -7
- package/souls/professional.md +1 -1
- package/src/cli.js +3 -1
- package/src/commands/run.js +229 -16
- package/src/core/agent-loop.js +167 -49
- package/src/core/ast.js +40 -0
- package/src/core/chat-runtime.js +720 -126
- package/src/core/command-policy.js +56 -0
- package/src/core/config-store.js +0 -3
- package/src/core/crypto-utils.js +6 -2
- package/src/core/memory-store.js +3 -3
- package/src/core/project-index.js +4 -18
- package/src/core/provider/anthropic.js +15 -2
- package/src/core/provider/anthropic.sdk-backup.js +439 -0
- package/src/core/provider/openai-compatible.js +93 -11
- package/src/core/provider/openai-compatible.sdk-backup.js +412 -0
- package/src/core/session-store.js +90 -25
- package/src/core/shell-profile.js +26 -6
- package/src/core/string-utils.js +37 -0
- package/src/core/tools.js +216 -405
- package/src/tui/chat-app.js +490 -146
- package/src/tui/tool-activity/presenters/files.js +2 -2
- package/src/tui/tool-narration.js +0 -3
- package/src/tui/tool-narration/presenters/patch.js +0 -3
|
@@ -4,6 +4,8 @@ import { getSessionsDir } from './paths.js';
|
|
|
4
4
|
import { normalizeTodos } from './todo-state.js';
|
|
5
5
|
|
|
6
6
|
const ALLOWED_ROLES = new Set(['system', 'user', 'assistant', 'tool']);
|
|
7
|
+
const SESSION_LEGACY_EXT = '.json';
|
|
8
|
+
const SESSION_JSONL_EXT = '.jsonl';
|
|
7
9
|
|
|
8
10
|
function createSessionId() {
|
|
9
11
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
@@ -41,6 +43,14 @@ function sanitizeMessage(msg) {
|
|
|
41
43
|
if (msg?.tool_call_id) out.tool_call_id = String(msg.tool_call_id);
|
|
42
44
|
if (typeof msg?.name === 'string' && msg.name.trim()) out.name = msg.name.trim();
|
|
43
45
|
if (typeof msg?.at === 'string' && msg.at.trim()) out.at = msg.at;
|
|
46
|
+
if (typeof msg?.reasoning_content === 'string' && msg.reasoning_content) {
|
|
47
|
+
out.reasoning_content = msg.reasoning_content;
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(msg?.reasoning_details) && msg.reasoning_details.length > 0) {
|
|
50
|
+
out.reasoning_details = msg.reasoning_details
|
|
51
|
+
.filter((detail) => detail && typeof detail === 'object')
|
|
52
|
+
.map((detail) => ({ ...detail }));
|
|
53
|
+
}
|
|
44
54
|
|
|
45
55
|
if (Array.isArray(msg?.tool_calls)) {
|
|
46
56
|
const toolCalls = msg.tool_calls.map(sanitizeToolCall).filter(Boolean);
|
|
@@ -93,25 +103,85 @@ function sanitizeSession(session, fallbackId = '') {
|
|
|
93
103
|
return out;
|
|
94
104
|
}
|
|
95
105
|
|
|
106
|
+
function sessionPathById(sessionId, ext = SESSION_JSONL_EXT) {
|
|
107
|
+
return path.join(getSessionsDir(), `${sessionId}${ext}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sessionIdFromFileName(fileName) {
|
|
111
|
+
if (fileName.endsWith(SESSION_JSONL_EXT)) return fileName.slice(0, -SESSION_JSONL_EXT.length);
|
|
112
|
+
if (fileName.endsWith(SESSION_LEGACY_EXT)) return fileName.slice(0, -SESSION_LEGACY_EXT.length);
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function summarizeParsedSession(parsed, filePath) {
|
|
117
|
+
const id = parsed.id || sessionIdFromFileName(path.basename(filePath));
|
|
118
|
+
const updatedAt = parsed.updatedAt || parsed.createdAt || '';
|
|
119
|
+
const latestMessage = Array.isArray(parsed.messages) ? parsed.messages.at(-1) : null;
|
|
120
|
+
const preview = latestMessage?.content ? String(latestMessage.content).replace(/\s+/g, ' ').slice(0, 80) : '';
|
|
121
|
+
return {
|
|
122
|
+
id,
|
|
123
|
+
updatedAt,
|
|
124
|
+
messageCount: Array.isArray(parsed.messages) ? parsed.messages.length : 0,
|
|
125
|
+
preview
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function tryReadJson(filePath) {
|
|
130
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
131
|
+
return JSON.parse(raw);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function loadLatestJsonlObject(filePath) {
|
|
135
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
136
|
+
const lines = String(raw || '')
|
|
137
|
+
.split('\n')
|
|
138
|
+
.map((line) => line.trim())
|
|
139
|
+
.filter(Boolean);
|
|
140
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
141
|
+
try {
|
|
142
|
+
return JSON.parse(lines[i]);
|
|
143
|
+
} catch {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`No valid JSONL record found: ${filePath}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function loadSessionPayload(sessionId) {
|
|
151
|
+
const jsonlPath = sessionPathById(sessionId, SESSION_JSONL_EXT);
|
|
152
|
+
let jsonlError = null;
|
|
153
|
+
try {
|
|
154
|
+
return await loadLatestJsonlObject(jsonlPath);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error?.code !== 'ENOENT') jsonlError = error;
|
|
157
|
+
}
|
|
158
|
+
const legacyPath = sessionPathById(sessionId, SESSION_LEGACY_EXT);
|
|
159
|
+
try {
|
|
160
|
+
return await tryReadJson(legacyPath);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (jsonlError) throw jsonlError;
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
96
167
|
export async function createSession() {
|
|
97
168
|
const sessionId = createSessionId();
|
|
98
169
|
const dir = getSessionsDir();
|
|
99
170
|
await fs.mkdir(dir, { recursive: true });
|
|
100
|
-
const filePath =
|
|
171
|
+
const filePath = sessionPathById(sessionId, SESSION_JSONL_EXT);
|
|
101
172
|
const payload = {
|
|
102
173
|
id: sessionId,
|
|
103
174
|
createdAt: new Date().toISOString(),
|
|
104
175
|
updatedAt: new Date().toISOString(),
|
|
105
176
|
messages: []
|
|
106
177
|
};
|
|
107
|
-
await fs.writeFile(filePath, `${JSON.stringify(payload
|
|
178
|
+
await fs.writeFile(filePath, `${JSON.stringify(payload)}\n`, 'utf8');
|
|
108
179
|
return payload;
|
|
109
180
|
}
|
|
110
181
|
|
|
111
182
|
export async function loadSession(sessionId) {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
return sanitizeSession(JSON.parse(raw), sessionId);
|
|
183
|
+
const parsed = await loadSessionPayload(sessionId);
|
|
184
|
+
return sanitizeSession(parsed, sessionId);
|
|
115
185
|
}
|
|
116
186
|
|
|
117
187
|
export async function saveSession(session) {
|
|
@@ -119,8 +189,8 @@ export async function saveSession(session) {
|
|
|
119
189
|
await fs.mkdir(dir, { recursive: true });
|
|
120
190
|
const normalized = sanitizeSession(session);
|
|
121
191
|
normalized.updatedAt = new Date().toISOString();
|
|
122
|
-
const filePath =
|
|
123
|
-
await fs.
|
|
192
|
+
const filePath = sessionPathById(normalized.id, SESSION_JSONL_EXT);
|
|
193
|
+
await fs.appendFile(filePath, `${JSON.stringify(normalized)}\n`, 'utf8');
|
|
124
194
|
}
|
|
125
195
|
|
|
126
196
|
export async function resolveSession(sessionId) {
|
|
@@ -135,31 +205,25 @@ export async function listSessions(limit = 30) {
|
|
|
135
205
|
await fs.mkdir(dir, { recursive: true });
|
|
136
206
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
137
207
|
const files = entries
|
|
138
|
-
.filter((e) => e.isFile() && e.name.endsWith(
|
|
208
|
+
.filter((e) => e.isFile() && (e.name.endsWith(SESSION_JSONL_EXT) || e.name.endsWith(SESSION_LEGACY_EXT)))
|
|
139
209
|
.map((e) => path.join(dir, e.name));
|
|
140
210
|
|
|
141
|
-
const
|
|
211
|
+
const sessionsById = new Map();
|
|
142
212
|
for (const file of files) {
|
|
143
213
|
try {
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
: '';
|
|
152
|
-
sessions.push({
|
|
153
|
-
id,
|
|
154
|
-
updatedAt,
|
|
155
|
-
messageCount: Array.isArray(parsed.messages) ? parsed.messages.length : 0,
|
|
156
|
-
preview
|
|
157
|
-
});
|
|
214
|
+
const parsed = file.endsWith(SESSION_JSONL_EXT) ? await loadLatestJsonlObject(file) : await tryReadJson(file);
|
|
215
|
+
const summary = summarizeParsedSession(parsed, file);
|
|
216
|
+
if (!summary.id) continue;
|
|
217
|
+
const existing = sessionsById.get(summary.id);
|
|
218
|
+
if (!existing || String(summary.updatedAt) > String(existing.updatedAt)) {
|
|
219
|
+
sessionsById.set(summary.id, summary);
|
|
220
|
+
}
|
|
158
221
|
} catch {
|
|
159
222
|
continue;
|
|
160
223
|
}
|
|
161
224
|
}
|
|
162
225
|
|
|
226
|
+
const sessions = Array.from(sessionsById.values());
|
|
163
227
|
sessions.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
|
|
164
228
|
return sessions.filter((s) => Number(s.messageCount || 0) > 0).slice(0, limit);
|
|
165
229
|
}
|
|
@@ -187,8 +251,9 @@ export async function pruneSessions(policy = {}) {
|
|
|
187
251
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
188
252
|
let removed = 0;
|
|
189
253
|
for (const e of entries) {
|
|
190
|
-
if (!e.isFile()
|
|
191
|
-
const id =
|
|
254
|
+
if (!e.isFile()) continue;
|
|
255
|
+
const id = sessionIdFromFileName(e.name);
|
|
256
|
+
if (!id) continue;
|
|
192
257
|
if (keepIds.has(id)) continue;
|
|
193
258
|
try {
|
|
194
259
|
await fs.unlink(path.join(dir, e.name));
|
|
@@ -61,6 +61,7 @@ const SHELL_PROFILES = {
|
|
|
61
61
|
shell: 'bash',
|
|
62
62
|
label: 'bash',
|
|
63
63
|
command_allowlist: [
|
|
64
|
+
'cd',
|
|
64
65
|
'rg',
|
|
65
66
|
'find',
|
|
66
67
|
'grep',
|
|
@@ -73,6 +74,22 @@ const SHELL_PROFILES = {
|
|
|
73
74
|
'ls',
|
|
74
75
|
'cat',
|
|
75
76
|
'sed',
|
|
77
|
+
'head',
|
|
78
|
+
'tail',
|
|
79
|
+
'wc',
|
|
80
|
+
'test',
|
|
81
|
+
'sort',
|
|
82
|
+
'uniq',
|
|
83
|
+
'cut',
|
|
84
|
+
'tr',
|
|
85
|
+
'xargs',
|
|
86
|
+
'basename',
|
|
87
|
+
'dirname',
|
|
88
|
+
'paste',
|
|
89
|
+
'echo',
|
|
90
|
+
'sleep',
|
|
91
|
+
'true',
|
|
92
|
+
'false',
|
|
76
93
|
'cp',
|
|
77
94
|
'mv',
|
|
78
95
|
'mkdir',
|
|
@@ -144,13 +161,14 @@ Use update_todos with these rules:
|
|
|
144
161
|
|
|
145
162
|
Some tools are loaded on demand through tool_search. Common examples:
|
|
146
163
|
- glob for pattern-based file lookup
|
|
147
|
-
- ast_query and read_ast_node for AST-scoped edits
|
|
148
|
-
- generate_diff and patch for explicit diff workflows
|
|
164
|
+
- ast_query and read_ast_node for advanced AST-scoped reads and edits
|
|
149
165
|
- list_background_tasks, get_background_task, and stop_background_task for managing long-running background commands
|
|
150
166
|
- remember_user, remember_global, remember_project, list_memory, search_memory, and forget_memory for persistent memory operations
|
|
151
167
|
|
|
152
|
-
For structural code edits (functions, classes, methods),
|
|
153
|
-
|
|
168
|
+
For structural code edits (functions, classes, methods), prefer AST-scoped reads before editing:
|
|
169
|
+
- Common one-shot workflow: read(path, query=..., capture_name=...) → edit with symbol or ast_target
|
|
170
|
+
- If you already have ast_target: read(ast_target=...) → edit with ast_target
|
|
171
|
+
- Advanced multi-step workflow: tool_search("ast_query") → ast_query → read_ast_node → edit with ast_target and kind=replace_block
|
|
154
172
|
Fall back to plain grep/read/edit only when AST is not appropriate.
|
|
155
173
|
|
|
156
174
|
For background commands: use run to launch. If you need management tools that are not currently visible, load list_background_tasks/get_background_task/stop_background_task with tool_search. Prefer reading the returned output_file with read instead of asking for a separate logs tool.
|
|
@@ -192,8 +210,10 @@ Common tool call patterns:
|
|
|
192
210
|
|
|
193
211
|
# Tone and style
|
|
194
212
|
|
|
195
|
-
-
|
|
196
|
-
-
|
|
213
|
+
- Keep answers compact and easy to scan
|
|
214
|
+
- Lead with the answer or next action, not scene-setting
|
|
215
|
+
- Do not restate the user's request unless a brief restatement prevents ambiguity
|
|
197
216
|
- When referencing code, use file_path:line_number format
|
|
217
|
+
- Keep technical wording, commands, paths, and error details exact
|
|
198
218
|
- Only use emojis if the user explicitly requests it`;
|
|
199
219
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 共享字符串/路径工具函数。
|
|
3
|
+
* 统一 trimInline、路径标准化、escapeRegex 等多处重复实现。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 将字符串截断到指定长度,超出时添加省略号。
|
|
8
|
+
* 会先将空白折叠为单个空格再截断。
|
|
9
|
+
*/
|
|
10
|
+
export function trimInline(value, maxLen = 72) {
|
|
11
|
+
const s = String(value || '').replace(/\s+/g, ' ').trim();
|
|
12
|
+
if (!s) return '';
|
|
13
|
+
if (s.length <= maxLen) return s;
|
|
14
|
+
return `${s.slice(0, maxLen - 3)}...`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 将路径中的反斜杠替换为正斜杠,并去掉开头的 "./" 前缀。
|
|
19
|
+
* 用于统一 Windows / Unix 路径格式。
|
|
20
|
+
*/
|
|
21
|
+
export function normalizePath(value) {
|
|
22
|
+
return String(value || '').replace(/\\/g, '/').replace(/^\.\/+/, '');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 将路径标准化为相对路径格式:反斜杠→正斜杠,去掉 "./" 和开头的 "/"。
|
|
27
|
+
*/
|
|
28
|
+
export function normalizeRelativePath(value) {
|
|
29
|
+
return String(value || '').replace(/\\/g, '/').replace(/^\.\/+/, '').replace(/^\/+/, '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 转义正则表达式中的特殊字符。
|
|
34
|
+
*/
|
|
35
|
+
export function escapeRegex(value) {
|
|
36
|
+
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
37
|
+
}
|