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.
@@ -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 = path.join(dir, `${sessionId}.json`);
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, null, 2)}\n`, 'utf8');
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 filePath = path.join(getSessionsDir(), `${sessionId}.json`);
113
- const raw = await fs.readFile(filePath, 'utf8');
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 = path.join(dir, `${normalized.id}.json`);
123
- await fs.writeFile(filePath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
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('.json'))
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 sessions = [];
211
+ const sessionsById = new Map();
142
212
  for (const file of files) {
143
213
  try {
144
- const raw = await fs.readFile(file, 'utf8');
145
- const parsed = JSON.parse(raw);
146
- const id = parsed.id || path.basename(file, '.json');
147
- const updatedAt = parsed.updatedAt || parsed.createdAt || '';
148
- const latestMessage = Array.isArray(parsed.messages) ? parsed.messages.at(-1) : null;
149
- const preview = latestMessage?.content
150
- ? String(latestMessage.content).replace(/\s+/g, ' ').slice(0, 80)
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() || !e.name.endsWith('.json')) continue;
191
- const id = path.basename(e.name, '.json');
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), load the AST tools and use the AST-first workflow:
153
- tool_search("ast_query") ast_query read_ast_node → edit with ast_target and kind=replace_block.
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
- - Be concise. Go straight to the point
196
- - Do not restate what the user said
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
+ }