claude-simple 0.2.6 → 0.2.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.
Files changed (60) hide show
  1. package/README.md +2 -0
  2. package/dist/app.0.2.8.min.js +1080 -0
  3. package/dist/index.html +2 -2
  4. package/dist/style.0.2.8.min.css +1598 -0
  5. package/gateway.js +0 -0
  6. package/package.json +1 -1
  7. package/public/addon-fit.min.js +0 -0
  8. package/public/agent-panel.js +245 -1
  9. package/public/api.js +4 -0
  10. package/public/app.js +0 -0
  11. package/public/base.css +0 -0
  12. package/public/cache.js +0 -0
  13. package/public/chat.js +118 -17
  14. package/public/files.js +0 -0
  15. package/public/git.js +0 -0
  16. package/public/icons/icon-180.png +0 -0
  17. package/public/icons/icon-192.png +0 -0
  18. package/public/icons/icon-512.png +0 -0
  19. package/public/icons/icon-maskable-512.png +0 -0
  20. package/public/index.html +2 -2
  21. package/public/lib.js +0 -0
  22. package/public/manifest.webmanifest +0 -0
  23. package/public/mobile.js +0 -0
  24. package/public/notifications.js +215 -0
  25. package/public/offline.html +0 -0
  26. package/public/pwa.js +0 -0
  27. package/public/router.js +0 -0
  28. package/public/settings.js +0 -0
  29. package/public/shell/boot.js +0 -0
  30. package/public/shell/dashboard.js +72 -23
  31. package/public/shell/hub.js +0 -0
  32. package/public/shell/layout.js +41 -12
  33. package/public/shell/notes.js +0 -0
  34. package/public/shell/session-context.js +0 -0
  35. package/public/shell/session-list.js +27 -28
  36. package/public/shell/session-nav.js +36 -1
  37. package/public/shell/templates.js +45 -7
  38. package/public/shell/util.js +0 -0
  39. package/public/shell.js +56 -7
  40. package/public/state.js +53 -13
  41. package/public/style.css +100 -6
  42. package/public/sw.js +2 -2
  43. package/public/terminal.js +77 -3
  44. package/public/wake-lock.js +0 -0
  45. package/public/xterm.min.css +0 -0
  46. package/public/xterm.min.js +0 -0
  47. package/src/agents/claude.js +1 -1
  48. package/src/agents/codex.js +1 -0
  49. package/src/agents/grok.js +180 -298
  50. package/src/agents/meta-agent.js +332 -1
  51. package/src/agents/models.js +0 -0
  52. package/src/agents/sessions.js +106 -26
  53. package/src/agents/stream-batch.js +0 -0
  54. package/src/app.js +88 -11
  55. package/src/auto-update.js +0 -0
  56. package/src/runtime/approvals.js +0 -0
  57. package/src/runtime/outbox.js +0 -0
  58. package/src/runtime/protocol.js +0 -0
  59. package/dist/app.0.2.6.min.js +0 -953
  60. package/dist/style.0.2.6.min.css +0 -1504
@@ -41,7 +41,7 @@ export const AI_DEFAULTS = {
41
41
  export const DEFAULT_SYSTEM = `你是 Claude Simple UI 的内置工作助手(Meta Agent)。你能访问本机上的 Claude / Codex / Grok 会话、项目笔记、Git 与文件,并生成「我做了啥」的工作报告。
42
42
 
43
43
  规则:
44
- 1. 需要会话 id / 项目路径时,先 list_projects 或 search_activity / list_sessions,再深入 get_session_summary。
44
+ 1. 需要会话 id / 项目路径时,先 list_projects 或 search_activity / list_sessions,再深入 get_session_brief / get_session_summary。超长会话先读 get_session_brief;只有需要逐事件定位问题时才读 get_session_jsonl。
45
45
  2. 回答要具体:引用项目路径、会话标题、时间范围;不确定就再查。
46
46
  3. 写日报/周报用 generate_activity_digest 拉快照,再组织成清晰 Markdown(亮点、按项目、未完成、建议)。
47
47
  4. 改文件或跑命令前说明意图;危险命令拒绝(rm -rf /、格式化磁盘等)。
@@ -133,6 +133,39 @@ const TOOLS = [
133
133
  },
134
134
  },
135
135
  },
136
+ {
137
+ type: 'function',
138
+ function: {
139
+ name: 'get_session_brief',
140
+ description: '读取会话压缩上下文,适合非常长的 session;包含项目/会话备注、关键用户意图与最近进展',
141
+ parameters: {
142
+ type: 'object',
143
+ required: ['sessionId'],
144
+ properties: {
145
+ sessionId: { type: 'string' },
146
+ agent: { type: 'string' },
147
+ maxTurns: { type: 'integer', description: '默认 6,返回多少个采样转折点' },
148
+ },
149
+ },
150
+ },
151
+ },
152
+ {
153
+ type: 'function',
154
+ function: {
155
+ name: 'get_session_jsonl',
156
+ description: '读取 Claude/Codex/Grok 指定会话的原始 JSONL 行,用于逐事件、工具调用或失败原因分析;内容可能敏感,只在当前回答中使用',
157
+ parameters: {
158
+ type: 'object',
159
+ required: ['sessionId'],
160
+ properties: {
161
+ sessionId: { type: 'string' },
162
+ agent: { type: 'string', enum: ['claude', 'codex', 'grok'], description: '建议明确指定来源 agent' },
163
+ offset: { type: 'integer', description: '从第几行开始,默认 0' },
164
+ maxLines: { type: 'integer', description: '每次最多 2000 行,默认 500' },
165
+ },
166
+ },
167
+ },
168
+ },
136
169
  {
137
170
  type: 'function',
138
171
  function: {
@@ -161,6 +194,62 @@ const TOOLS = [
161
194
  },
162
195
  },
163
196
  },
197
+ {
198
+ type: 'function',
199
+ function: {
200
+ name: 'list_meta_notes',
201
+ description: '列出 Meta Notes(Hub 侧持久化工作笔记)',
202
+ parameters: {
203
+ type: 'object',
204
+ properties: {
205
+ scope: { type: 'string', enum: ['all', 'project', 'session', 'general'], description: '默认 all' },
206
+ q: { type: 'string', description: '按标题/正文搜索' },
207
+ cwd: { type: 'string', description: '按项目路径过滤' },
208
+ sessionId: { type: 'string', description: '按 session 过滤' },
209
+ agent: { type: 'string', description: 'session 过滤时可指定 agent' },
210
+ limit: { type: 'integer', description: '默认 30' },
211
+ },
212
+ },
213
+ },
214
+ },
215
+ {
216
+ type: 'function',
217
+ function: {
218
+ name: 'save_meta_note',
219
+ description: '创建或更新一条 Meta Note,可关联项目或 session',
220
+ parameters: {
221
+ type: 'object',
222
+ required: ['title', 'content'],
223
+ properties: {
224
+ id: { type: 'string' },
225
+ title: { type: 'string' },
226
+ content: { type: 'string' },
227
+ cwd: { type: 'string' },
228
+ sessionId: { type: 'string' },
229
+ agent: { type: 'string' },
230
+ pinned: { type: 'boolean' },
231
+ tags: {
232
+ type: 'array',
233
+ items: { type: 'string' },
234
+ },
235
+ },
236
+ },
237
+ },
238
+ },
239
+ {
240
+ type: 'function',
241
+ function: {
242
+ name: 'delete_meta_note',
243
+ description: '删除一条 Meta Note',
244
+ parameters: {
245
+ type: 'object',
246
+ required: ['id'],
247
+ properties: {
248
+ id: { type: 'string' },
249
+ },
250
+ },
251
+ },
252
+ },
164
253
  {
165
254
  type: 'function',
166
255
  function: {
@@ -399,10 +488,12 @@ export function createMetaAgent(deps) {
399
488
  listSessions,
400
489
  searchActivity,
401
490
  loadSessionMessages,
491
+ loadSessionJsonl,
402
492
  groupByProject,
403
493
  toClientSession,
404
494
  getProjectNotesStore,
405
495
  saveProjectNotesStore,
496
+ getSessionMetaStore,
406
497
  normalizeProjectEntry,
407
498
  git,
408
499
  isGitRepo,
@@ -417,6 +508,8 @@ export function createMetaAgent(deps) {
417
508
  const aiDir = process.env.AI_DATA_DIR
418
509
  ? expandPath(process.env.AI_DATA_DIR)
419
510
  : path.join(rootDir, '.ai');
511
+ const notesFile = path.join(aiDir, 'meta-notes.json');
512
+ const sessionBriefsFile = path.join(aiDir, 'session-briefs.json');
420
513
  const sessionsDir = path.join(aiDir, 'sessions');
421
514
  /** @deprecated global vlm — only for one-time migration */
422
515
  const legacyGlobalVlmDir = path.join(aiDir, 'vlm');
@@ -461,6 +554,8 @@ export function createMetaAgent(deps) {
461
554
 
462
555
  let configCache = null;
463
556
  let reportsCache = null;
557
+ let notesCache = null;
558
+ let sessionBriefsCache = null;
464
559
  /** @type {{ version: number, sessions: Array<{id,title,createdAt,updatedAt,messageCount}> } | null} */
465
560
  let sessionsIndexCache = null;
466
561
  /** in-memory cache of full sessions (id → session) */
@@ -477,6 +572,42 @@ export function createMetaAgent(deps) {
477
572
  await fs.mkdir(sessionsDir, { recursive: true });
478
573
  }
479
574
 
575
+ async function loadNotesStore() {
576
+ if (notesCache) return notesCache;
577
+ await ensureAiDirs();
578
+ try {
579
+ const raw = JSON.parse(await fs.readFile(notesFile, 'utf8'));
580
+ notesCache = {
581
+ version: raw.version || 1,
582
+ notes: Array.isArray(raw.notes) ? raw.notes : [],
583
+ };
584
+ } catch {
585
+ notesCache = { version: 1, notes: [] };
586
+ }
587
+ return notesCache;
588
+ }
589
+
590
+ async function saveNotesStore() {
591
+ if (!notesCache) return;
592
+ await atomicWriteJson(notesFile, notesCache);
593
+ }
594
+
595
+ async function loadSessionBriefsStore() {
596
+ if (sessionBriefsCache) return sessionBriefsCache;
597
+ await ensureAiDirs();
598
+ try {
599
+ sessionBriefsCache = JSON.parse(await fs.readFile(sessionBriefsFile, 'utf8'));
600
+ } catch {
601
+ sessionBriefsCache = {};
602
+ }
603
+ return sessionBriefsCache;
604
+ }
605
+
606
+ async function saveSessionBriefsStore() {
607
+ if (!sessionBriefsCache) return;
608
+ await atomicWriteJson(sessionBriefsFile, sessionBriefsCache);
609
+ }
610
+
480
611
  async function atomicWriteText(filePath, text) {
481
612
  await fs.mkdir(path.dirname(filePath), { recursive: true });
482
613
  const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
@@ -918,6 +1049,178 @@ export function createMetaAgent(deps) {
918
1049
  return '';
919
1050
  }
920
1051
 
1052
+ function extractRoleText(message) {
1053
+ if (!message) return '';
1054
+ const text = extractTextParts(message.content ?? message.message?.content ?? message.text);
1055
+ return String(text || '').replace(/\s+/g, ' ').trim();
1056
+ }
1057
+
1058
+ function pickSample(items, count) {
1059
+ if (!Array.isArray(items) || !items.length) return [];
1060
+ if (items.length <= count) return items.slice();
1061
+ const out = [];
1062
+ const last = items.length - 1;
1063
+ for (let i = 0; i < count; i += 1) {
1064
+ const idx = Math.round((i * last) / Math.max(1, count - 1));
1065
+ out.push(items[idx]);
1066
+ }
1067
+ return out.filter((item, i, arr) => i === arr.findIndex((x) => x === item));
1068
+ }
1069
+
1070
+ async function listMetaNotes(query = {}) {
1071
+ const store = await loadNotesStore();
1072
+ const scope = query.scope || 'all';
1073
+ const q = String(query.q || '').trim().toLowerCase();
1074
+ const cwd = query.cwd ? expandPath(query.cwd) : '';
1075
+ const sessionId = String(query.sessionId || '').trim();
1076
+ const agent = String(query.agent || '').trim();
1077
+ const limit = Math.min(query.limit || 30, 200);
1078
+ let rows = [...store.notes];
1079
+ if (scope !== 'all') rows = rows.filter((n) => n.scope === scope);
1080
+ if (cwd) rows = rows.filter((n) => n.cwd === cwd);
1081
+ if (sessionId) rows = rows.filter((n) => n.sessionId === sessionId);
1082
+ if (agent) rows = rows.filter((n) => n.agent === agent);
1083
+ if (q) {
1084
+ rows = rows.filter((n) => `${n.title || ''}\n${n.content || ''}\n${(n.tags || []).join(' ')}`.toLowerCase().includes(q));
1085
+ }
1086
+ rows.sort((a, b) => {
1087
+ if (!!b.pinned !== !!a.pinned) return Number(b.pinned) - Number(a.pinned);
1088
+ return (b.updatedAt || 0) - (a.updatedAt || 0);
1089
+ });
1090
+ return rows.slice(0, limit);
1091
+ }
1092
+
1093
+ async function getMetaNote(id) {
1094
+ if (!id) return null;
1095
+ const store = await loadNotesStore();
1096
+ return store.notes.find((n) => n.id === id) || null;
1097
+ }
1098
+
1099
+ async function saveMetaNote(input = {}) {
1100
+ const store = await loadNotesStore();
1101
+ const now = Date.now();
1102
+ const id = input.id ? String(input.id) : crypto.randomUUID();
1103
+ const cwd = input.cwd ? expandPath(input.cwd) : '';
1104
+ const sessionId = String(input.sessionId || '').trim();
1105
+ const agent = String(input.agent || '').trim() || (sessionId ? 'claude' : '');
1106
+ const scope = sessionId ? 'session' : cwd ? 'project' : 'general';
1107
+ const tags = Array.isArray(input.tags)
1108
+ ? [...new Set(input.tags.map((x) => String(x || '').trim()).filter(Boolean))].slice(0, 12)
1109
+ : [];
1110
+ const prev = store.notes.find((n) => n.id === id);
1111
+ const note = {
1112
+ id,
1113
+ scope,
1114
+ title: String(input.title || '').trim().slice(0, 200),
1115
+ content: String(input.content || '').trim(),
1116
+ cwd,
1117
+ sessionId,
1118
+ agent,
1119
+ pinned: Boolean(input.pinned),
1120
+ tags,
1121
+ createdAt: prev?.createdAt || now,
1122
+ updatedAt: now,
1123
+ };
1124
+ if (!note.title) throw new Error('title required');
1125
+ if (!note.content) throw new Error('content required');
1126
+ if (prev) {
1127
+ const idx = store.notes.findIndex((n) => n.id === id);
1128
+ store.notes[idx] = note;
1129
+ } else {
1130
+ store.notes.unshift(note);
1131
+ }
1132
+ await saveNotesStore();
1133
+ return note;
1134
+ }
1135
+
1136
+ async function deleteMetaNote(id) {
1137
+ if (!id) return { ok: false };
1138
+ const store = await loadNotesStore();
1139
+ const next = store.notes.filter((n) => n.id !== id);
1140
+ const changed = next.length !== store.notes.length;
1141
+ store.notes = next;
1142
+ if (changed) await saveNotesStore();
1143
+ return { ok: changed };
1144
+ }
1145
+
1146
+ async function getSessionMetaNote(sessionId, agent) {
1147
+ if (!sessionId || !getSessionMetaStore) return null;
1148
+ const db = await getSessionMetaStore();
1149
+ const key = `${agent || 'claude'}:${sessionId}`;
1150
+ return db?.[key] || null;
1151
+ }
1152
+
1153
+ async function findSessionRecord(sessionId, preferredAgent = null) {
1154
+ const sessions = await listSessions(null, preferredAgent || null);
1155
+ let hit = sessions.find((s) => s.sessionId === sessionId && (!preferredAgent || s.agent === preferredAgent));
1156
+ if (!hit && !preferredAgent) hit = sessions.find((s) => s.sessionId === sessionId);
1157
+ return hit || null;
1158
+ }
1159
+
1160
+ async function buildSessionBrief(args = {}) {
1161
+ const sessionId = args.sessionId || args.id;
1162
+ if (!sessionId) return { error: 'sessionId required' };
1163
+ const maxTurns = Math.min(args.maxTurns || 6, 12);
1164
+ const record = await findSessionRecord(sessionId, args.agent || null);
1165
+ const resolvedAgent = record?.agent || args.agent || null;
1166
+ const updatedAt = record?.updatedAt || 0;
1167
+ const cacheKey = `${resolvedAgent || '*'}:${sessionId}`;
1168
+ const cache = await loadSessionBriefsStore();
1169
+ const cached = cache[cacheKey];
1170
+ if (cached && cached.updatedAt === updatedAt && cached.maxTurns === maxTurns) return cached.brief;
1171
+
1172
+ const bundle = await loadSessionMessages(sessionId, resolvedAgent, sessionOpts());
1173
+ if (!bundle) return { error: 'session not found' };
1174
+ const msgs = Array.isArray(bundle) ? bundle : (bundle.messages || []);
1175
+ const userTurns = [];
1176
+ const assistantTurns = [];
1177
+ for (const m of msgs) {
1178
+ const role = m.role || m.type;
1179
+ if (role !== 'user' && role !== 'assistant') continue;
1180
+ const text = extractRoleText(m);
1181
+ if (!text || text.startsWith('Caveat:') || text.startsWith('<command-')) continue;
1182
+ const row = { role, text: shortTitle(text, role === 'user' ? 260 : 220) };
1183
+ if (role === 'user') userTurns.push(row);
1184
+ else assistantTurns.push(row);
1185
+ }
1186
+ const checkpoints = pickSample(userTurns, maxTurns).map((turn, idx) => ({
1187
+ index: idx + 1,
1188
+ prompt: turn.text,
1189
+ }));
1190
+ const cwd = record?.cwd || null;
1191
+ const projectNotesStore = cwd ? await getProjectNotesStore() : null;
1192
+ const projectNotes = cwd ? normalizeProjectEntry(projectNotesStore?.[cwd] || {}) : { goal: '', notes: '' };
1193
+ const sessionMeta = await getSessionMetaNote(sessionId, resolvedAgent || 'claude');
1194
+ const linkedNotes = await listMetaNotes({ scope: 'session', sessionId, agent: resolvedAgent || undefined, limit: 8 });
1195
+ const brief = {
1196
+ sessionId,
1197
+ agent: Array.isArray(bundle) ? resolvedAgent : (bundle.agent || resolvedAgent),
1198
+ cwd,
1199
+ projectName: record?.projectName || (cwd ? path.basename(cwd) : ''),
1200
+ title: shortTitle(record?.display || record?.title || sessionId, 120),
1201
+ updatedAt,
1202
+ totalMessages: msgs.length,
1203
+ userTurnCount: userTurns.length,
1204
+ assistantTurnCount: assistantTurns.length,
1205
+ firstUserPrompt: userTurns[0]?.text || '',
1206
+ latestUserPrompt: userTurns[userTurns.length - 1]?.text || '',
1207
+ latestAssistantReply: assistantTurns[assistantTurns.length - 1]?.text || '',
1208
+ checkpoints,
1209
+ projectGoal: projectNotes.goal || '',
1210
+ projectNotes: projectNotes.notes ? shortTitle(projectNotes.notes, 300) : '',
1211
+ sessionNote: (sessionMeta?.notes || '').trim(),
1212
+ linkedNotes: linkedNotes.map((n) => ({
1213
+ id: n.id,
1214
+ title: n.title,
1215
+ content: shortTitle(n.content, 240),
1216
+ updatedAt: n.updatedAt,
1217
+ })),
1218
+ };
1219
+ cache[cacheKey] = { updatedAt, maxTurns, brief, cachedAt: Date.now() };
1220
+ await saveSessionBriefsStore();
1221
+ return brief;
1222
+ }
1223
+
921
1224
  function indexEntryFromSession(s) {
922
1225
  return {
923
1226
  id: s.id,
@@ -1441,6 +1744,20 @@ export function createMetaAgent(deps) {
1441
1744
  messages: summary,
1442
1745
  };
1443
1746
  }
1747
+ case 'get_session_brief': {
1748
+ return buildSessionBrief(args);
1749
+ }
1750
+ case 'get_session_jsonl': {
1751
+ const sid = args.sessionId || args.id;
1752
+ if (!sid) return { error: 'sessionId required' };
1753
+ if (!loadSessionJsonl) return { error: 'raw session reader unavailable' };
1754
+ const result = await loadSessionJsonl(sid, args.agent || null, sessionOpts(), {
1755
+ offset: args.offset,
1756
+ maxLines: args.maxLines,
1757
+ });
1758
+ if (!result) return { error: 'session JSONL not found' };
1759
+ return result;
1760
+ }
1444
1761
  case 'get_project_notes': {
1445
1762
  const store = await getProjectNotesStore();
1446
1763
  const cwd = expandPath(args.cwd);
@@ -1457,6 +1774,15 @@ export function createMetaAgent(deps) {
1457
1774
  await saveProjectNotesStore();
1458
1775
  return { ok: true, cwd, ...cur };
1459
1776
  }
1777
+ case 'list_meta_notes': {
1778
+ return listMetaNotes(args);
1779
+ }
1780
+ case 'save_meta_note': {
1781
+ return saveMetaNote(args);
1782
+ }
1783
+ case 'delete_meta_note': {
1784
+ return deleteMetaNote(args.id);
1785
+ }
1460
1786
  case 'git_status': {
1461
1787
  const cwd = expandPath(args.cwd);
1462
1788
  if (!(await isGitRepo(cwd))) return { isGit: false };
@@ -1975,6 +2301,11 @@ export function createMetaAgent(deps) {
1975
2301
  .slice(0, limit);
1976
2302
  },
1977
2303
  buildDigest,
2304
+ listMetaNotes,
2305
+ getMetaNote,
2306
+ saveMetaNote,
2307
+ deleteMetaNote,
2308
+ buildSessionBrief,
1978
2309
  executeTool,
1979
2310
  runLoop,
1980
2311
  resolveApproval,
File without changes
@@ -1187,40 +1187,121 @@ export async function loadSessionMemory(sessionId, agent, { claudeConfigDirs, co
1187
1187
  if (agent === 'grok') {
1188
1188
  const dir = await findGrokSessionDir(sessionId, grokHome);
1189
1189
  if (!dir) return null;
1190
- // Grok may store memory differently; best-effort empty
1191
- return { index: null, files: [] };
1190
+ return listMemoryFiles(path.join(dir, 'memory'), 'grok');
1192
1191
  }
1193
1192
  if (agent === 'codex') {
1194
- return { index: null, files: [] };
1193
+ // Codex memory is account-level rather than nested beneath each rollout.
1194
+ const home = codexHome || path.join(os.homedir(), '.codex');
1195
+ return listMemoryFiles(path.join(home, 'memories'), 'codex', { codexRoot: true });
1195
1196
  }
1196
1197
  const file = await findClaudeSessionFile(sessionId, claudeConfigDirs);
1197
1198
  if (!file) return null;
1198
- return readProjectMemory(path.dirname(file));
1199
+ const primaryRoot = path.dirname(file);
1200
+ const primary = await listMemoryFiles(primaryRoot, 'claude', { claudeProject: true, rootKind: 'agent-project' });
1201
+ // Some Claude setups keep durable memory in the actual project (cwd/memory)
1202
+ // instead of beside the agent transcript. Expose it when the transcript's
1203
+ // own project directory has no memory files.
1204
+ if (primary.files.length) return primary;
1205
+ const meta = await readClaudeSessionMeta(file);
1206
+ if (!meta.cwd || !path.isAbsolute(meta.cwd)) return primary;
1207
+ return listMemoryFiles(meta.cwd, 'claude', { claudeProject: true, rootKind: 'workspace' });
1199
1208
  }
1200
1209
 
1201
- export async function readProjectMemory(projectDir) {
1202
- const memoryDir = path.join(projectDir, 'memory');
1203
- let index = null;
1204
- try { index = await fs.readFile(path.join(projectDir, 'MEMORY.md'), 'utf8'); }
1205
- catch {}
1206
-
1210
+ async function listMemoryFiles(root, agent, opts = {}) {
1207
1211
  const files = [];
1208
- let entries = [];
1209
- try { entries = await fs.readdir(memoryDir, { withFileTypes: true }); }
1210
- catch {}
1211
- await Promise.all(
1212
- entries
1213
- .filter(e => e.isFile() && e.name.endsWith('.md'))
1214
- .map(async e => {
1215
- try {
1216
- const content = await fs.readFile(path.join(memoryDir, e.name), 'utf8');
1217
- const stat = await fs.stat(path.join(memoryDir, e.name));
1218
- files.push({ name: e.name, content, updatedAt: stat.mtimeMs });
1219
- } catch {}
1220
- })
1221
- );
1212
+ const add = async (absolute, id, scope) => {
1213
+ try {
1214
+ const stat = await fs.stat(absolute);
1215
+ if (stat.isFile()) files.push({ id, name: id, agent, scope, updatedAt: stat.mtimeMs, size: stat.size });
1216
+ } catch {}
1217
+ };
1218
+ if (opts.claudeProject) {
1219
+ await add(path.join(root, 'MEMORY.md'), 'MEMORY.md', 'project');
1220
+ let entries = [];
1221
+ try { entries = await fs.readdir(path.join(root, 'memory'), { withFileTypes: true }); } catch {}
1222
+ await Promise.all(entries.filter(e => e.isFile() && e.name.endsWith('.md'))
1223
+ .map(e => add(path.join(root, 'memory', e.name), `memory/${e.name}`, 'project')));
1224
+ } else if (opts.codexRoot) {
1225
+ // Durable Codex memory entry points; rollout summaries and skill packages
1226
+ // remain out of this concise tab.
1227
+ await Promise.all([
1228
+ add(path.join(root, 'MEMORY.md'), 'MEMORY.md', 'account'),
1229
+ add(path.join(root, 'memory_summary.md'), 'memory_summary.md', 'account'),
1230
+ ]);
1231
+ let entries = [];
1232
+ const notes = path.join(root, 'extensions', 'ad_hoc', 'notes');
1233
+ try { entries = await fs.readdir(notes, { withFileTypes: true }); } catch {}
1234
+ await Promise.all(entries.filter(e => e.isFile() && e.name.endsWith('.md'))
1235
+ .map(e => add(path.join(notes, e.name), `extensions/ad_hoc/notes/${e.name}`, 'account')));
1236
+ } else {
1237
+ let entries = [];
1238
+ try { entries = await fs.readdir(root, { withFileTypes: true }); } catch {}
1239
+ await Promise.all(entries.filter(e => e.isFile() && e.name.endsWith('.md'))
1240
+ .map(e => add(path.join(root, e.name), e.name, 'session')));
1241
+ }
1222
1242
  files.sort((a, b) => a.name.localeCompare(b.name));
1223
- return { index, files };
1243
+ return { agent, files };
1244
+ }
1245
+
1246
+ /** Read one previously-listed memory file. File ids are relative and validated. */
1247
+ export async function readSessionMemoryFile(sessionId, agent, fileId, opts) {
1248
+ const id = String(fileId || '').replaceAll('\\', '/');
1249
+ if (!id || id.startsWith('/') || id.split('/').some(p => p === '..' || !p)) return null;
1250
+ let root;
1251
+ if (agent === 'codex') root = path.join(opts.codexHome || path.join(os.homedir(), '.codex'), 'memories');
1252
+ else if (agent === 'grok') {
1253
+ const dir = await findGrokSessionDir(sessionId, opts.grokHome);
1254
+ if (!dir) return null;
1255
+ root = path.join(dir, 'memory');
1256
+ } else {
1257
+ const file = await findClaudeSessionFile(sessionId, opts.claudeConfigDirs);
1258
+ if (!file) return null;
1259
+ root = path.dirname(file);
1260
+ // Match the same workspace fallback used by loadSessionMemory.
1261
+ const primary = await listMemoryFiles(root, 'claude', { claudeProject: true, rootKind: 'agent-project' });
1262
+ if (!primary.files.length) {
1263
+ const meta = await readClaudeSessionMeta(file);
1264
+ if (meta.cwd && path.isAbsolute(meta.cwd)) root = meta.cwd;
1265
+ }
1266
+ }
1267
+ const listed = await loadSessionMemory(sessionId, agent, opts);
1268
+ const meta = listed?.files?.find(f => f.id === id);
1269
+ if (!meta) return null;
1270
+ try { return { ...meta, content: await fs.readFile(path.resolve(root, id), 'utf8') }; }
1271
+ catch { return null; }
1272
+ }
1273
+
1274
+ /** Return bounded raw JSONL for a particular agent session, for Meta Agent analysis. */
1275
+ export async function loadSessionJsonl(sessionId, agent, { claudeConfigDirs, codexHome, grokHome }, { offset = 0, maxLines = 500 } = {}) {
1276
+ if (!agent) {
1277
+ for (const candidate of ['claude', 'codex', 'grok']) {
1278
+ const found = await loadSessionJsonl(sessionId, candidate, { claudeConfigDirs, codexHome, grokHome }, { offset, maxLines });
1279
+ if (found) return found;
1280
+ }
1281
+ return null;
1282
+ }
1283
+ const a = (agent || 'claude').toLowerCase();
1284
+ let file;
1285
+ if (a === 'codex') file = await findCodexSessionFile(sessionId, codexHome);
1286
+ else if (a === 'grok') {
1287
+ const dir = await findGrokSessionDir(sessionId, grokHome);
1288
+ file = dir ? path.join(dir, 'chat_history.jsonl') : null;
1289
+ } else file = await findClaudeSessionFile(sessionId, claudeConfigDirs);
1290
+ if (!file) return null;
1291
+ const start = Math.max(0, Number(offset) || 0);
1292
+ const limit = Math.min(Math.max(1, Number(maxLines) || 500), 2000);
1293
+ const lines = [];
1294
+ let n = 0;
1295
+ let hasMore = false;
1296
+ const stream = fsSync.createReadStream(file, { encoding: 'utf8' });
1297
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
1298
+ for await (const line of rl) {
1299
+ if (n++ < start) continue;
1300
+ if (lines.length >= limit) { hasMore = true; rl.close(); stream.destroy(); break; }
1301
+ lines.push(line);
1302
+ }
1303
+ const stat = await fs.stat(file).catch(() => null);
1304
+ return { agent: a, fileName: path.basename(file), offset: start, lines, nextOffset: start + lines.length, truncated: hasMore, size: stat?.size || 0 };
1224
1305
  }
1225
1306
 
1226
1307
  export async function getSessionMeta(sessionId, agent, { claudeConfigDirs, codexHome, grokHome }) {
@@ -1505,4 +1586,3 @@ export async function convertSession(sourceSessionId, sourceAgent, targetAgent,
1505
1586
 
1506
1587
  return { targetSessionId };
1507
1588
  }
1508
-
File without changes