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.
- package/README.md +2 -0
- package/dist/app.0.2.7.min.js +1039 -0
- package/dist/index.html +2 -2
- package/dist/style.0.2.7.min.css +1598 -0
- package/package.json +1 -1
- package/public/agent-panel.js +245 -1
- package/public/api.js +2 -0
- package/public/chat.js +81 -11
- package/public/index.html +2 -2
- package/public/shell/dashboard.js +39 -10
- package/public/shell/layout.js +39 -9
- package/public/shell/session-list.js +22 -14
- package/public/shell/session-nav.js +18 -0
- package/public/shell/templates.js +27 -7
- package/public/shell.js +32 -1
- package/public/state.js +4 -6
- package/public/style.css +100 -6
- package/public/sw.js +2 -2
- package/public/terminal.js +77 -3
- package/src/agents/claude.js +1 -1
- package/src/agents/codex.js +1 -0
- package/src/agents/grok.js +3 -2
- package/src/agents/meta-agent.js +332 -1
- package/src/agents/sessions.js +106 -26
- package/src/app.js +88 -11
- package/dist/app.0.2.6.min.js +0 -953
- package/dist/style.0.2.6.min.css +0 -1504
package/src/agents/sessions.js
CHANGED
|
@@ -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
|
-
|
|
1191
|
-
return { index: null, files: [] };
|
|
1190
|
+
return listMemoryFiles(path.join(dir, 'memory'), 'grok');
|
|
1192
1191
|
}
|
|
1193
1192
|
if (agent === 'codex') {
|
|
1194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
1399
|
-
|
|
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
|
-
|
|
1406
|
-
|
|
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
|
-
|
|
1489
|
+
runOutbox.flush();
|
|
1413
1490
|
return;
|
|
1414
1491
|
}
|
|
1415
1492
|
|