claude-simple 0.2.6 → 0.2.7

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.
@@ -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
-
package/src/app.js CHANGED
@@ -18,6 +18,8 @@ import {
18
18
  scanAllSessions,
19
19
  loadSessionMessages,
20
20
  loadSessionMemory,
21
+ readSessionMemoryFile,
22
+ loadSessionJsonl,
21
23
  loadSessionUsage,
22
24
  searchActivity,
23
25
  groupByProject,
@@ -1008,19 +1010,37 @@ export function createApp() {
1008
1010
  codexHome: CODEX_HOME,
1009
1011
  grokHome: GROK_HOME,
1010
1012
  };
1011
- const order = [...new Set([preferred, ...ENABLED_AGENTS, 'claude', 'codex', 'grok'].filter(Boolean))];
1013
+ // A selected session already knows its agent. Do not fall back to another
1014
+ // agent's account-level memory (especially Codex) when that session has none.
1015
+ const order = preferred
1016
+ ? [preferred]
1017
+ : [...new Set([...ENABLED_AGENTS, 'claude', 'codex', 'grok'])];
1012
1018
  let memory = null;
1013
1019
  for (const a of order) {
1014
1020
  memory = await loadSessionMemory(sessionId, a, opts);
1015
1021
  if (memory) break;
1016
1022
  }
1017
- // Memory is Claude-centric; return empty instead of 404 so the tab doesn't hard-fail
1018
- res.json(memory || { index: null, files: [] });
1023
+ res.json(memory || { agent: preferred || null, files: [] });
1019
1024
  } catch (e) {
1020
1025
  res.status(500).json({ error: e.message });
1021
1026
  }
1022
1027
  });
1023
1028
 
1029
+ app.get('/api/sessions/:sessionId/memory/file', authMiddleware, async (req, res) => {
1030
+ try {
1031
+ const { sessionId } = req.params;
1032
+ const agent = typeof req.query.agent === 'string' ? req.query.agent : 'claude';
1033
+ const file = typeof req.query.file === 'string' ? req.query.file : '';
1034
+ if (!/^[0-9a-f-]{36}$/i.test(sessionId)) return res.status(400).json({ error: 'Invalid session id' });
1035
+ const row = await readSessionMemoryFile(sessionId, agent, file, {
1036
+ claudeConfigDirs: CLAUDE_CONFIG_DIRS, codexHome: CODEX_HOME, grokHome: GROK_HOME,
1037
+ });
1038
+ if (!row) return res.status(404).json({ error: 'Memory file not found' });
1039
+ res.setHeader('Cache-Control', 'private, max-age=30');
1040
+ res.json(row);
1041
+ } catch (e) { res.status(500).json({ error: e.message }); }
1042
+ });
1043
+
1024
1044
  app.get('/api/projects/:id/git/log', authMiddleware, async (req, res) => {
1025
1045
  try {
1026
1046
  const projectPath = resolveProjectRoot(req);
@@ -1044,10 +1064,12 @@ export function createApp() {
1044
1064
  listSessions,
1045
1065
  searchActivity,
1046
1066
  loadSessionMessages,
1067
+ loadSessionJsonl,
1047
1068
  groupByProject,
1048
1069
  toClientSession,
1049
1070
  getProjectNotesStore: loadProjectNotes,
1050
1071
  saveProjectNotesStore: saveProjectNotes,
1072
+ getSessionMetaStore: loadSessionMeta,
1051
1073
  normalizeProjectEntry,
1052
1074
  git,
1053
1075
  isGitRepo,
@@ -1122,6 +1144,50 @@ export function createApp() {
1122
1144
  }
1123
1145
  });
1124
1146
 
1147
+ app.get('/api/ai/notes', authMiddleware, async (req, res) => {
1148
+ try {
1149
+ const limit = Math.min(parseInt(req.query.limit || '30', 10) || 30, 200);
1150
+ const scope = typeof req.query.scope === 'string' ? req.query.scope : 'all';
1151
+ const q = typeof req.query.q === 'string' ? req.query.q : '';
1152
+ const cwd = typeof req.query.cwd === 'string' ? req.query.cwd : '';
1153
+ const sessionId = typeof req.query.sessionId === 'string' ? req.query.sessionId : '';
1154
+ const agent = typeof req.query.agent === 'string' ? req.query.agent : '';
1155
+ res.json({
1156
+ notes: await meta.listMetaNotes({ limit, scope, q, cwd, sessionId, agent }),
1157
+ });
1158
+ } catch (e) {
1159
+ res.status(500).json({ error: e.message });
1160
+ }
1161
+ });
1162
+
1163
+ app.get('/api/ai/notes/:id', authMiddleware, async (req, res) => {
1164
+ try {
1165
+ const note = await meta.getMetaNote(req.params.id);
1166
+ if (!note) return res.status(404).json({ error: 'not found' });
1167
+ res.json(note);
1168
+ } catch (e) {
1169
+ res.status(500).json({ error: e.message });
1170
+ }
1171
+ });
1172
+
1173
+ app.post('/api/ai/notes', authMiddleware, async (req, res) => {
1174
+ try {
1175
+ const note = await meta.saveMetaNote(req.body || {});
1176
+ res.json(note);
1177
+ } catch (e) {
1178
+ res.status(500).json({ error: e.message });
1179
+ }
1180
+ });
1181
+
1182
+ app.delete('/api/ai/notes/:id', authMiddleware, async (req, res) => {
1183
+ try {
1184
+ const result = await meta.deleteMetaNote(req.params.id);
1185
+ res.json(result);
1186
+ } catch (e) {
1187
+ res.status(500).json({ error: e.message });
1188
+ }
1189
+ });
1190
+
1125
1191
  // SSE: generate work report
1126
1192
  app.post('/api/ai/report', authMiddleware, async (req, res) => {
1127
1193
  res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
@@ -1392,24 +1458,35 @@ export function createApp() {
1392
1458
  if (cmd.sessionId) options.sessionId = cmd.sessionId;
1393
1459
  if (cmd.cwd) options.cwd = cmd.cwd;
1394
1460
  if (options.sessionId) client.sessionId = options.sessionId;
1395
- const agentSend = createAgentSend(outbox, agent);
1461
+ // A socket may subscribe to another session while this agent is still
1462
+ // streaming. Keep this run's delivery target in its own closure rather
1463
+ // than consulting mutable client.sessionId in the shared socket outbox.
1464
+ let runSessionId = options.sessionId || client.sessionId || null;
1465
+ const runOutbox = createOutbox((data) => {
1466
+ broadcastChat(runSessionId, data, ws);
1467
+ }, {
1468
+ maxWait: 48,
1469
+ maxDeltaChars: 720,
1470
+ maxBatchItems: 10,
1471
+ maxBatchBytes: 10_000,
1472
+ });
1473
+ const agentSend = createAgentSend(runOutbox, agent);
1396
1474
  const runSend = (data) => {
1475
+ if (data?.sessionId) runSessionId = data.sessionId;
1397
1476
  if (data?.type === 'result' || data?.type === 'complete' || data?.type === 'error') {
1398
- markSessionIdle(data.sessionId || client.sessionId, options.sessionId);
1399
- outbox.flush();
1477
+ markSessionIdle(data.sessionId || runSessionId, options.sessionId);
1478
+ runOutbox.flush();
1400
1479
  }
1401
- if (data?.sessionId) client.sessionId = data.sessionId;
1402
- if (data?.type === 'session-created' && data.sessionId) client.sessionId = data.sessionId;
1403
1480
  // permission_request bypasses map filter double-pass carefully
1404
1481
  if (data?.type === 'permission_request' || data?.type === 'permission_resolved') {
1405
- outbox.send(data);
1406
- outbox.flush();
1482
+ runOutbox.send(data);
1483
+ runOutbox.flush();
1407
1484
  return;
1408
1485
  }
1409
1486
  agentSend(data);
1410
1487
  };
1411
1488
  await dispatchAgent(agent, cmd.command || '', options, runSend);
1412
- outbox.flush();
1489
+ runOutbox.flush();
1413
1490
  return;
1414
1491
  }
1415
1492