codemini-cli 0.5.6 → 0.5.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.
@@ -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 files = await listSessionFiles();
271
-
272
- const sessionsById = new Map();
273
- for (const file of files) {
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
  }