claude-simple 0.2.5 → 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/bin/edge-ctl.js +28 -6
- 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.5.min.js +0 -953
- package/dist/style.0.2.5.min.css +0 -1504
package/public/terminal.js
CHANGED
|
@@ -11,6 +11,37 @@ let lastMachine = null;
|
|
|
11
11
|
let shellGen = 0;
|
|
12
12
|
let shellReconnectTimer = null;
|
|
13
13
|
let shellWanted = false;
|
|
14
|
+
let terminalEventsBound = false;
|
|
15
|
+
const mobileKeyData = {
|
|
16
|
+
'ctrl-c': '\x03', escape: '\x1b', tab: '\t',
|
|
17
|
+
up: '\x1b[A', down: '\x1b[B', left: '\x1b[D', right: '\x1b[C'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function sendInput(data) {
|
|
21
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
22
|
+
ws.send(JSON.stringify({ type: 'input', data }));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isMobileShell() {
|
|
27
|
+
return window.matchMedia('(max-width: 640px), (hover: none) and (pointer: coarse)').matches;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Mobile IMEs do not reliably drive xterm's off-screen textarea. Use a real
|
|
31
|
+
* input there so composition, prediction and paste remain visible and intact. */
|
|
32
|
+
function focusTerminal() {
|
|
33
|
+
if (isMobileShell()) {
|
|
34
|
+
document.getElementById('term-mobile-input')?.focus({ preventScroll: true });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
term?.focus();
|
|
38
|
+
const input = document.querySelector('#terminal-container .xterm-helper-textarea');
|
|
39
|
+
if (input) {
|
|
40
|
+
input.focus({ preventScroll: true });
|
|
41
|
+
// iOS may ignore the first focus while its visual viewport is settling.
|
|
42
|
+
requestAnimationFrame(() => input.focus({ preventScroll: true }));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
14
45
|
|
|
15
46
|
function initXterm() {
|
|
16
47
|
if (term) return;
|
|
@@ -59,15 +90,35 @@ function initXterm() {
|
|
|
59
90
|
fitTerminal();
|
|
60
91
|
|
|
61
92
|
term.onData(data => {
|
|
62
|
-
|
|
63
|
-
ws.send(JSON.stringify({ type: 'input', data }));
|
|
64
|
-
}
|
|
93
|
+
sendInput(data);
|
|
65
94
|
});
|
|
66
95
|
|
|
96
|
+
// On phones this focuses the native command input; desktop keeps xterm's
|
|
97
|
+
// direct keyboard interaction.
|
|
98
|
+
container.addEventListener('pointerdown', () => focusTerminal(), { passive: true });
|
|
99
|
+
container.addEventListener('touchend', () => focusTerminal(), { passive: true });
|
|
100
|
+
|
|
67
101
|
window.addEventListener('resize', () => {
|
|
68
102
|
fitTerminal();
|
|
69
103
|
sendResize();
|
|
70
104
|
});
|
|
105
|
+
|
|
106
|
+
const vv = window.visualViewport;
|
|
107
|
+
if (vv) {
|
|
108
|
+
let raf = 0;
|
|
109
|
+
const fitForVisualViewport = () => {
|
|
110
|
+
if (raf) cancelAnimationFrame(raf);
|
|
111
|
+
raf = requestAnimationFrame(() => {
|
|
112
|
+
raf = 0;
|
|
113
|
+
if (currentTab.peek() === 'shell') {
|
|
114
|
+
fitTerminal();
|
|
115
|
+
sendResize();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
vv.addEventListener('resize', fitForVisualViewport);
|
|
120
|
+
vv.addEventListener('scroll', fitForVisualViewport);
|
|
121
|
+
}
|
|
71
122
|
}
|
|
72
123
|
|
|
73
124
|
function fitTerminal() {
|
|
@@ -177,6 +228,29 @@ function activateShellTab() {
|
|
|
177
228
|
}
|
|
178
229
|
|
|
179
230
|
export function initTerminal() {
|
|
231
|
+
if (!terminalEventsBound) {
|
|
232
|
+
terminalEventsBound = true;
|
|
233
|
+
document.getElementById('term-keyboard')?.addEventListener('click', focusTerminal);
|
|
234
|
+
document.getElementById('term-mobile-composer')?.addEventListener('submit', event => {
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
const input = document.getElementById('term-mobile-input');
|
|
237
|
+
if (!input) return;
|
|
238
|
+
sendInput(`${input.value}\r`);
|
|
239
|
+
input.value = '';
|
|
240
|
+
input.focus({ preventScroll: true });
|
|
241
|
+
});
|
|
242
|
+
document.getElementById('term-mobile-keys')?.addEventListener('pointerdown', event => {
|
|
243
|
+
// Keep the native command input focused while using the touch shortcut row.
|
|
244
|
+
if (event.target.closest('[data-term-key]')) event.preventDefault();
|
|
245
|
+
});
|
|
246
|
+
document.getElementById('term-mobile-keys')?.addEventListener('click', event => {
|
|
247
|
+
const key = event.target.closest('[data-term-key]')?.dataset.termKey;
|
|
248
|
+
if (!key || !mobileKeyData[key]) return;
|
|
249
|
+
sendInput(mobileKeyData[key]);
|
|
250
|
+
focusTerminal();
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
180
254
|
document.addEventListener('visibilitychange', () => {
|
|
181
255
|
if (document.visibilityState !== 'visible') return;
|
|
182
256
|
if (currentTab.peek() !== 'shell' || !shellWanted) return;
|
package/src/agents/claude.js
CHANGED
|
@@ -23,7 +23,7 @@ export async function runClaude(command, options, send, { cliPath, activeSession
|
|
|
23
23
|
const env = { ...process.env };
|
|
24
24
|
if (options.configDir) env.CLAUDE_CONFIG_DIR = options.configDir;
|
|
25
25
|
|
|
26
|
-
const permissionMode = options.permissionMode || '
|
|
26
|
+
const permissionMode = options.permissionMode || 'bypassPermissions';
|
|
27
27
|
const autoApprove = permissionMode === 'bypassPermissions'
|
|
28
28
|
|| permissionMode === 'acceptEdits'
|
|
29
29
|
|| permissionMode === 'auto'
|
package/src/agents/codex.js
CHANGED
package/src/agents/grok.js
CHANGED
|
@@ -22,8 +22,9 @@ import { createDeltaBatcher } from './stream-batch.js';
|
|
|
22
22
|
|
|
23
23
|
function mapPermission(permissionMode, allowBypass) {
|
|
24
24
|
if (allowBypass || permissionMode === 'bypassPermissions') return 'bypassPermissions';
|
|
25
|
-
if (permissionMode
|
|
26
|
-
return null;
|
|
25
|
+
if (permissionMode == null) return 'bypassPermissions';
|
|
26
|
+
if (permissionMode === 'default') return null;
|
|
27
|
+
return permissionMode;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function buildArgs(command, options) {
|
package/src/agents/meta-agent.js
CHANGED
|
@@ -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,
|