ec-web 0.0.1 → 0.0.3
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 +15 -3
- package/dist/debug-log.js +7 -0
- package/dist/fs-utils.js +189 -0
- package/dist/index.js +137 -0
- package/dist/ipc-client.js +40 -0
- package/dist/paths.js +55 -0
- package/dist/process-utils.js +241 -0
- package/dist/server.js +820 -0
- package/dist/sources/aid.js +139 -0
- package/dist/sources/baseagent-detector.js +72 -0
- package/dist/sources/cache.js +43 -0
- package/dist/sources/gateway.js +44 -0
- package/dist/sources/logs.js +396 -0
- package/dist/sources/models.js +75 -0
- package/dist/sources/monitor.js +179 -0
- package/dist/sources/msg.js +625 -0
- package/dist/sources/parent-package.js +77 -0
- package/dist/sources/relation-roles.js +903 -0
- package/dist/sources/role-definitions.js +1195 -0
- package/dist/sources/session-codex.js +731 -0
- package/dist/sources/session.js +489 -0
- package/dist/sources/stats.js +481 -0
- package/dist/sources/system.js +83 -0
- package/dist/sources/triggers.js +138 -0
- package/dist/sources/types.js +10 -0
- package/dist/static/app.js +10753 -0
- package/dist/static/index.html +386 -0
- package/dist/static/style.css +4138 -0
- package/dist/types/cli-permissions.js +90 -0
- package/package.json +20 -11
- package/index.js +0 -7
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话数据源 — 多 baseagent 支持(Claude / Codex)。
|
|
3
|
+
*
|
|
4
|
+
* 根据 params.baseagent 参数路由到对应的实现:
|
|
5
|
+
* - claude: ~/.claude/projects/<encodedProject>/*.jsonl
|
|
6
|
+
* - codex: ~/.codex/state_*.sqlite + ~/.codex/sessions/YYYY/MM/DD/*.jsonl
|
|
7
|
+
*
|
|
8
|
+
* snapshot:
|
|
9
|
+
* - 无 sessionId: 项目列表 + 选中项目的全部 transcript 概要
|
|
10
|
+
* - 有 sessionId: 该 transcript 的结构化轮次 + 会话头统计
|
|
11
|
+
* subscribe: 监听对应目录的文件变化 + evolcore sessionsDir active.json 变化,防抖 150ms。
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { resolvePaths, ccProjectsDir } from '../paths.js';
|
|
16
|
+
import { encodePath, scanChatDirs, readJsonFile } from '../fs-utils.js';
|
|
17
|
+
import { snapshotCodex, subscribeCodex, setDebugLog as setCodexDebugLog } from './session-codex.js';
|
|
18
|
+
let _dlog = null;
|
|
19
|
+
export function setDebugLog(log) {
|
|
20
|
+
_dlog = log;
|
|
21
|
+
setCodexDebugLog(log); // 同步传递给 Codex 模块
|
|
22
|
+
}
|
|
23
|
+
function dlog(line) { if (_dlog)
|
|
24
|
+
try {
|
|
25
|
+
_dlog(line);
|
|
26
|
+
}
|
|
27
|
+
catch { } }
|
|
28
|
+
const CACHE_VERSION = 4;
|
|
29
|
+
const _metaCache = new Map();
|
|
30
|
+
function cacheDir(encoded) {
|
|
31
|
+
const dataDir = resolvePaths().dataDir;
|
|
32
|
+
const currentRoot = path.join(dataDir, 'ecweb-cache');
|
|
33
|
+
const legacyRoot = path.join(dataDir, 'watch-web-cache');
|
|
34
|
+
if (!fs.existsSync(currentRoot) && fs.existsSync(legacyRoot)) {
|
|
35
|
+
try {
|
|
36
|
+
fs.renameSync(legacyRoot, currentRoot);
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
}
|
|
40
|
+
return path.join(currentRoot, encoded);
|
|
41
|
+
}
|
|
42
|
+
function readDiskCache(encoded, id, mtime, size) {
|
|
43
|
+
try {
|
|
44
|
+
const rec = JSON.parse(fs.readFileSync(path.join(cacheDir(encoded), `${id}.json`), 'utf-8'));
|
|
45
|
+
if (rec.v === CACHE_VERSION && rec.mtime === mtime && rec.size === size && rec.meta)
|
|
46
|
+
return rec.meta;
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function writeDiskCache(encoded, id, mtime, size, meta) {
|
|
52
|
+
try {
|
|
53
|
+
const dir = cacheDir(encoded);
|
|
54
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
55
|
+
const tmp = path.join(dir, `${id}.json.tmp`);
|
|
56
|
+
fs.writeFileSync(tmp, JSON.stringify({ v: CACHE_VERSION, mtime, size, meta }));
|
|
57
|
+
fs.renameSync(tmp, path.join(dir, `${id}.json`));
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
}
|
|
61
|
+
function pruneCacheFiles(encoded, validIds) {
|
|
62
|
+
const dir = cacheDir(encoded);
|
|
63
|
+
try {
|
|
64
|
+
for (const name of fs.readdirSync(dir)) {
|
|
65
|
+
const match = name.match(/^(.+)\.json(?:\.tmp)?$/);
|
|
66
|
+
if (!match || validIds.has(match[1]))
|
|
67
|
+
continue;
|
|
68
|
+
try {
|
|
69
|
+
fs.unlinkSync(path.join(dir, name));
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
}
|
|
73
|
+
if (fs.readdirSync(dir).length === 0)
|
|
74
|
+
fs.rmdirSync(dir);
|
|
75
|
+
}
|
|
76
|
+
catch { }
|
|
77
|
+
}
|
|
78
|
+
function pruneCacheProjects(validProjects) {
|
|
79
|
+
const root = path.join(resolvePaths().dataDir, 'ecweb-cache');
|
|
80
|
+
try {
|
|
81
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
82
|
+
if (!entry.isDirectory() || entry.name === 'codex' || validProjects.has(entry.name))
|
|
83
|
+
continue;
|
|
84
|
+
try {
|
|
85
|
+
fs.rmSync(path.join(root, entry.name), { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
91
|
+
}
|
|
92
|
+
function extractMessageText(content) {
|
|
93
|
+
if (typeof content === 'string')
|
|
94
|
+
return content;
|
|
95
|
+
if (!Array.isArray(content))
|
|
96
|
+
return '';
|
|
97
|
+
return content
|
|
98
|
+
.filter((x) => x?.type === 'text' && typeof x.text === 'string')
|
|
99
|
+
.map((x) => x.text)
|
|
100
|
+
.join('\n');
|
|
101
|
+
}
|
|
102
|
+
function isToolResultOnly(content) {
|
|
103
|
+
return Array.isArray(content) && content.length > 0 && content.every((x) => x?.type === 'tool_result');
|
|
104
|
+
}
|
|
105
|
+
function isPureUserText(text) {
|
|
106
|
+
const t = text.trimStart();
|
|
107
|
+
return !!t && !t.startsWith('<');
|
|
108
|
+
}
|
|
109
|
+
function isCountableUserMessage(event, sessionId) {
|
|
110
|
+
if (sessionId.startsWith('agent-') || event?.isSidechain)
|
|
111
|
+
return false;
|
|
112
|
+
const content = event?.message?.content;
|
|
113
|
+
if (isToolResultOnly(content))
|
|
114
|
+
return false;
|
|
115
|
+
return isPureUserText(extractMessageText(content));
|
|
116
|
+
}
|
|
117
|
+
function extractMeta(file, id, stat) {
|
|
118
|
+
let raw = '';
|
|
119
|
+
try {
|
|
120
|
+
raw = fs.readFileSync(file, 'utf-8');
|
|
121
|
+
}
|
|
122
|
+
catch { }
|
|
123
|
+
let title = '', firstUser = '', gitBranch = '', version = '', userMsgs = 0, totalMsgs = 0;
|
|
124
|
+
for (const line of raw.split('\n')) {
|
|
125
|
+
if (!line)
|
|
126
|
+
continue;
|
|
127
|
+
const isAsst = line.indexOf('"type":"assistant"') !== -1;
|
|
128
|
+
const isUser = !isAsst && line.indexOf('"type":"user"') !== -1;
|
|
129
|
+
let o;
|
|
130
|
+
const needsParse = isUser || (!gitBranch || !version || !firstUser) && (line.indexOf('gitBranch') !== -1 || line.indexOf('"version"') !== -1 || line.indexOf('ai-title') !== -1);
|
|
131
|
+
if (needsParse) {
|
|
132
|
+
try {
|
|
133
|
+
o = JSON.parse(line);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
o = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (isAsst)
|
|
140
|
+
totalMsgs++;
|
|
141
|
+
else if (isUser) {
|
|
142
|
+
totalMsgs++;
|
|
143
|
+
if (o && isCountableUserMessage(o, id))
|
|
144
|
+
userMsgs++;
|
|
145
|
+
}
|
|
146
|
+
if ((!gitBranch || !version || !firstUser) && (line.indexOf('gitBranch') !== -1 || line.indexOf('"version"') !== -1 || line.indexOf('ai-title') !== -1 || (isUser && !firstUser))) {
|
|
147
|
+
if (!o) {
|
|
148
|
+
try {
|
|
149
|
+
o = JSON.parse(line);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!gitBranch && o.gitBranch)
|
|
156
|
+
gitBranch = o.gitBranch;
|
|
157
|
+
if (!version && o.version)
|
|
158
|
+
version = o.version;
|
|
159
|
+
if (o.type === 'ai-title' && o.title)
|
|
160
|
+
title = o.title;
|
|
161
|
+
if (!firstUser && o.type === 'user' && o.message) {
|
|
162
|
+
const t = extractMessageText(o.message.content);
|
|
163
|
+
if (isPureUserText(t))
|
|
164
|
+
firstUser = t.replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { id, title, firstUser, gitBranch, version, userMsgs, totalMsgs, sizeKB: Math.round(stat.size / 1024), lastActivity: stat.mtimeMs };
|
|
169
|
+
}
|
|
170
|
+
function listTranscripts(encoded) {
|
|
171
|
+
const dir = path.join(ccProjectsDir(), encoded);
|
|
172
|
+
let files;
|
|
173
|
+
try {
|
|
174
|
+
files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
pruneCacheFiles(encoded, new Set(files.map(file => file.replace(/\.jsonl$/, ''))));
|
|
180
|
+
const out = [];
|
|
181
|
+
let parsed = 0, hitMem = 0, hitDisk = 0;
|
|
182
|
+
for (const f of files) {
|
|
183
|
+
const file = path.join(dir, f);
|
|
184
|
+
const id = f.replace(/\.jsonl$/, '');
|
|
185
|
+
let stat;
|
|
186
|
+
try {
|
|
187
|
+
stat = fs.statSync(file);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const mtime = stat.mtimeMs;
|
|
193
|
+
const mem = _metaCache.get(file);
|
|
194
|
+
if (mem && mem.mtime === mtime) {
|
|
195
|
+
out.push(mem.meta);
|
|
196
|
+
hitMem++;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const disk = readDiskCache(encoded, id, mtime, stat.size);
|
|
200
|
+
if (disk) {
|
|
201
|
+
_metaCache.set(file, { mtime, meta: disk });
|
|
202
|
+
out.push(disk);
|
|
203
|
+
hitDisk++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const meta = extractMeta(file, id, stat);
|
|
207
|
+
_metaCache.set(file, { mtime, meta });
|
|
208
|
+
writeDiskCache(encoded, id, mtime, stat.size, meta);
|
|
209
|
+
out.push(meta);
|
|
210
|
+
parsed++;
|
|
211
|
+
}
|
|
212
|
+
dlog(`[session] listTranscripts ${encoded.slice(-20)}: ${files.length} files (mem=${hitMem} disk=${hitDisk} parsed=${parsed})`);
|
|
213
|
+
out.sort((a, b) => b.lastActivity - a.lastActivity);
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
function listProjects() {
|
|
217
|
+
const base = ccProjectsDir();
|
|
218
|
+
let dirs;
|
|
219
|
+
try {
|
|
220
|
+
dirs = fs.readdirSync(base, { withFileTypes: true });
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
const out = [];
|
|
226
|
+
for (const d of dirs) {
|
|
227
|
+
if (!d.isDirectory())
|
|
228
|
+
continue;
|
|
229
|
+
const dirPath = path.join(base, d.name);
|
|
230
|
+
let files;
|
|
231
|
+
try {
|
|
232
|
+
files = fs.readdirSync(dirPath).filter(f => f.endsWith('.jsonl'));
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (!files.length)
|
|
238
|
+
continue;
|
|
239
|
+
let last = 0;
|
|
240
|
+
for (const f of files) {
|
|
241
|
+
try {
|
|
242
|
+
const m = fs.statSync(path.join(dirPath, f)).mtimeMs;
|
|
243
|
+
if (m > last)
|
|
244
|
+
last = m;
|
|
245
|
+
}
|
|
246
|
+
catch { }
|
|
247
|
+
}
|
|
248
|
+
let cwd = d.name;
|
|
249
|
+
try {
|
|
250
|
+
const newest = files.map(f => ({ f, m: fs.statSync(path.join(dirPath, f)).mtimeMs })).sort((a, b) => b.m - a.m)[0];
|
|
251
|
+
if (newest) {
|
|
252
|
+
const fd = fs.openSync(path.join(dirPath, newest.f), 'r');
|
|
253
|
+
const buf = Buffer.alloc(16384);
|
|
254
|
+
const n = fs.readSync(fd, buf, 0, 16384, 0);
|
|
255
|
+
fs.closeSync(fd);
|
|
256
|
+
for (const line of buf.toString('utf-8', 0, n).split('\n')) {
|
|
257
|
+
if (!line.trim())
|
|
258
|
+
continue;
|
|
259
|
+
try {
|
|
260
|
+
const o = JSON.parse(line);
|
|
261
|
+
if (o.cwd) {
|
|
262
|
+
cwd = o.cwd;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch { }
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch { }
|
|
271
|
+
out.push({ encoded: d.name, cwd, label: cwd.replace(/[/\\]+$/, '').split(/[/\\]/).pop() || d.name, count: files.length, lastActivity: last });
|
|
272
|
+
}
|
|
273
|
+
pruneCacheProjects(new Set(out.map(project => project.encoded)));
|
|
274
|
+
out.sort((a, b) => b.lastActivity - a.lastActivity);
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
function resolveProject(params, projects) {
|
|
278
|
+
if (params.project) {
|
|
279
|
+
const found = projects.find(p => p.encoded === params.project);
|
|
280
|
+
if (found) {
|
|
281
|
+
dlog(`[session] resolveProject: matched param project=${params.project.slice(-24)}`);
|
|
282
|
+
return found;
|
|
283
|
+
}
|
|
284
|
+
dlog(`[session] resolveProject: param project=${params.project.slice(-24)} NOT in ${projects.length} projects → falling back`);
|
|
285
|
+
}
|
|
286
|
+
const curEncoded = encodePath(process.cwd());
|
|
287
|
+
const cur = projects.find(p => p.encoded === curEncoded);
|
|
288
|
+
if (cur) {
|
|
289
|
+
dlog(`[session] resolveProject: default to cwd project=${curEncoded.slice(-24)}`);
|
|
290
|
+
return cur;
|
|
291
|
+
}
|
|
292
|
+
dlog(`[session] resolveProject: cwd project=${curEncoded.slice(-24)} not found → using first=${projects[0]?.encoded.slice(-24)}`);
|
|
293
|
+
return projects[0] || null;
|
|
294
|
+
}
|
|
295
|
+
export function buildBindMap() {
|
|
296
|
+
const map = new Map();
|
|
297
|
+
try {
|
|
298
|
+
const p = resolvePaths();
|
|
299
|
+
for (const dir of scanChatDirs(p.sessionsDir)) {
|
|
300
|
+
const active = readJsonFile(path.join(dir.dirPath, 'active.json'));
|
|
301
|
+
if (!active || !active.agentSessionId)
|
|
302
|
+
continue;
|
|
303
|
+
map.set(active.agentSessionId, {
|
|
304
|
+
channelType: active.channelType || dir.channelType,
|
|
305
|
+
channelId: active.channelId || dir.channelId,
|
|
306
|
+
selfAID: active.selfAID || dir.selfAID,
|
|
307
|
+
peerName: (active.metadata && active.metadata.peerName) || null,
|
|
308
|
+
name: active.name,
|
|
309
|
+
updatedAt: active.updatedAt || 0,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch { }
|
|
314
|
+
return map;
|
|
315
|
+
}
|
|
316
|
+
// ── 完整 transcript 解析(省略 tool 参数/chat send 检测等细节,保留费用计算)──
|
|
317
|
+
const PRICING = {
|
|
318
|
+
'claude-opus-4-8': [5, 6.25, 0.5, 25], 'claude-opus-4': [5, 6.25, 0.5, 25],
|
|
319
|
+
'claude-sonnet-4-6': [3, 3.75, 0.3, 15], 'claude-sonnet-4': [3, 3.75, 0.3, 15],
|
|
320
|
+
'claude-haiku-4-5-20251001': [0.8, 1, 0.08, 4],
|
|
321
|
+
};
|
|
322
|
+
function pricingFor(model) {
|
|
323
|
+
for (const key of Object.keys(PRICING))
|
|
324
|
+
if (model.startsWith(key))
|
|
325
|
+
return PRICING[key];
|
|
326
|
+
if (model.includes('sonnet'))
|
|
327
|
+
return PRICING['claude-sonnet-4-6'];
|
|
328
|
+
if (model.includes('haiku'))
|
|
329
|
+
return PRICING['claude-haiku-4-5-20251001'];
|
|
330
|
+
return PRICING['claude-opus-4-8'];
|
|
331
|
+
}
|
|
332
|
+
function readTranscriptFile(file) {
|
|
333
|
+
const empty = { turns: [], totalTurns: 0, userMsgs: 0, totalMsgs: 0, counts: { userInput: 0, modelOutput: 0, toolCall: 0, toolResult: 0, msgSend: 0 }, inputTokens: 0, outputTokens: 0, contextTokens: 0, costUsd: 0, model: '', gitBranch: '', version: '', title: '', cwd: '' };
|
|
334
|
+
let raw;
|
|
335
|
+
try {
|
|
336
|
+
raw = fs.readFileSync(file, 'utf-8');
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return empty;
|
|
340
|
+
}
|
|
341
|
+
const turns = [];
|
|
342
|
+
const counts = { userInput: 0, modelOutput: 0, toolCall: 0, toolResult: 0, msgSend: 0 };
|
|
343
|
+
let inTok = 0, outTok = 0, model = '', branch = '', version = '', title = '', cwd = '', userMsgs = 0, totalMsgs = 0, contextTokens = 0, costUsd = 0, lastUsageKey = '';
|
|
344
|
+
for (const line of raw.split('\n')) {
|
|
345
|
+
if (!line.trim())
|
|
346
|
+
continue;
|
|
347
|
+
const isAsst = line.indexOf('"type":"assistant"') !== -1;
|
|
348
|
+
const isUser = !isAsst && line.indexOf('"type":"user"') !== -1;
|
|
349
|
+
let o;
|
|
350
|
+
try {
|
|
351
|
+
o = JSON.parse(line);
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (isAsst)
|
|
357
|
+
totalMsgs++;
|
|
358
|
+
else if (isUser) {
|
|
359
|
+
totalMsgs++;
|
|
360
|
+
if (isCountableUserMessage(o, path.basename(file, '.jsonl')))
|
|
361
|
+
userMsgs++;
|
|
362
|
+
}
|
|
363
|
+
if (!branch && o.gitBranch)
|
|
364
|
+
branch = o.gitBranch;
|
|
365
|
+
if (!version && o.version)
|
|
366
|
+
version = o.version;
|
|
367
|
+
if (!cwd && o.cwd)
|
|
368
|
+
cwd = o.cwd;
|
|
369
|
+
if (o.type === 'ai-title' && o.title)
|
|
370
|
+
title = o.title;
|
|
371
|
+
if (o.type === 'assistant' && o.message?.usage) {
|
|
372
|
+
const u = o.message.usage;
|
|
373
|
+
const inp = u.input_tokens || 0, cr = u.cache_read_input_tokens || 0, cc = u.cache_creation_input_tokens || 0, out = u.output_tokens || 0;
|
|
374
|
+
contextTokens = inp + cr + cc;
|
|
375
|
+
const key = `${inp},${cr},${cc},${out}`;
|
|
376
|
+
if (key !== lastUsageKey) {
|
|
377
|
+
lastUsageKey = key;
|
|
378
|
+
const [pi, pcw, pcr, po] = pricingFor(model || o.message.model);
|
|
379
|
+
costUsd += (inp * pi + cc * pcw + cr * pcr + out * po) / 1_000_000;
|
|
380
|
+
}
|
|
381
|
+
if (u.input_tokens)
|
|
382
|
+
inTok += u.input_tokens;
|
|
383
|
+
if (u.output_tokens)
|
|
384
|
+
outTok += u.output_tokens;
|
|
385
|
+
if (o.message.model)
|
|
386
|
+
model = o.message.model;
|
|
387
|
+
}
|
|
388
|
+
if (o.type === 'user' || o.type === 'assistant') {
|
|
389
|
+
const content = o.message?.content;
|
|
390
|
+
const arr = typeof content === 'string' ? [{ type: 'text', text: content }] : (Array.isArray(content) ? content : []);
|
|
391
|
+
const blocks = [];
|
|
392
|
+
let hasToolUse = false, hasToolResult = false;
|
|
393
|
+
for (const item of arr) {
|
|
394
|
+
if (!item || typeof item !== 'object')
|
|
395
|
+
continue;
|
|
396
|
+
if (item.type === 'text' && item.text) {
|
|
397
|
+
blocks.push({ kind: 'text', text: item.text });
|
|
398
|
+
}
|
|
399
|
+
else if (item.type === 'thinking' && item.thinking) {
|
|
400
|
+
blocks.push({ kind: 'thinking', text: item.thinking });
|
|
401
|
+
}
|
|
402
|
+
else if (item.type === 'tool_use') {
|
|
403
|
+
const inputStr = item.input ? JSON.stringify(item.input, null, 2) : '';
|
|
404
|
+
blocks.push({ kind: 'tool_use', name: item.name || '', input: item.input || {}, inputStr });
|
|
405
|
+
hasToolUse = true;
|
|
406
|
+
}
|
|
407
|
+
else if (item.type === 'tool_result') {
|
|
408
|
+
const c = item.content;
|
|
409
|
+
const text = typeof c === 'string' ? c : (Array.isArray(c) ? c.filter((x) => x?.type === 'text').map((x) => x.text).join('\n') : '');
|
|
410
|
+
blocks.push({ kind: 'tool_result', text, isError: !!item.is_error });
|
|
411
|
+
hasToolResult = true;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
let category;
|
|
415
|
+
if (o.type === 'user') {
|
|
416
|
+
category = hasToolResult ? 'tool_result' : 'user_input';
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
category = hasToolUse ? 'tool_call' : 'model_output';
|
|
420
|
+
}
|
|
421
|
+
counts[category === 'user_input' ? 'userInput' : category === 'model_output' ? 'modelOutput' : category === 'tool_call' ? 'toolCall' : 'toolResult']++;
|
|
422
|
+
turns.push({ role: o.type, ts: o.timestamp ? Date.parse(o.timestamp) : 0, uuid: o.uuid, category, blocks });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const shown = turns.length > 500 ? turns.slice(-500) : turns;
|
|
426
|
+
return { turns: shown, totalTurns: turns.length, userMsgs, totalMsgs, counts, inputTokens: inTok, outputTokens: outTok, contextTokens, costUsd, model, gitBranch: branch, version, title, cwd };
|
|
427
|
+
}
|
|
428
|
+
function buildSnapshot(params) {
|
|
429
|
+
const projects = listProjects();
|
|
430
|
+
const bindMap = buildBindMap();
|
|
431
|
+
const project = resolveProject(params, projects);
|
|
432
|
+
if (!project)
|
|
433
|
+
return { baseagent: 'claude', projects: [], project: null, transcripts: [], turns: [], sessionId: null };
|
|
434
|
+
const metas = listTranscripts(project.encoded);
|
|
435
|
+
const transcripts = metas.map(m => {
|
|
436
|
+
const bind = bindMap.get(m.id);
|
|
437
|
+
return { ...m, bound: !!bind, boundChannel: bind?.channelType ?? null, boundPeer: bind ? (bind.peerName || bind.channelId) : null, online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false };
|
|
438
|
+
});
|
|
439
|
+
const projList = projects.map(p => ({ encoded: p.encoded, label: p.label, cwd: p.cwd, count: p.count }));
|
|
440
|
+
const sessionId = params.sessionId || null;
|
|
441
|
+
if (!sessionId)
|
|
442
|
+
return { baseagent: 'claude', projects: projList, project: project.encoded, transcripts, turns: [], sessionId: null };
|
|
443
|
+
const detail = readTranscriptFile(path.join(ccProjectsDir(), project.encoded, `${sessionId}.jsonl`));
|
|
444
|
+
const bind = bindMap.get(sessionId);
|
|
445
|
+
const header = { sessionId, title: detail.title, model: detail.model, gitBranch: detail.gitBranch, version: detail.version, cwd: detail.cwd || project.cwd, totalTurns: detail.totalTurns, userMsgs: detail.userMsgs, totalMsgs: detail.totalMsgs, counts: detail.counts, inputTokens: detail.inputTokens, outputTokens: detail.outputTokens, contextTokens: detail.contextTokens, costUsd: detail.costUsd, bound: !!bind, boundChannel: bind?.channelType ?? null, boundPeer: bind ? (bind.peerName || bind.channelId) : null, online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false };
|
|
446
|
+
return { baseagent: 'claude', projects: projList, project: project.encoded, transcripts, turns: detail.turns, sessionId, header };
|
|
447
|
+
}
|
|
448
|
+
export const sessionSource = {
|
|
449
|
+
kind: 'session',
|
|
450
|
+
async snapshot(params = {}) {
|
|
451
|
+
const baseagent = params.baseagent || 'claude';
|
|
452
|
+
if (baseagent === 'codex') {
|
|
453
|
+
return snapshotCodex(params);
|
|
454
|
+
}
|
|
455
|
+
return buildSnapshot(params);
|
|
456
|
+
},
|
|
457
|
+
subscribe(params, push) {
|
|
458
|
+
const baseagent = params.baseagent || 'claude';
|
|
459
|
+
if (baseagent === 'codex') {
|
|
460
|
+
return subscribeCodex(params, push);
|
|
461
|
+
}
|
|
462
|
+
const p = resolvePaths();
|
|
463
|
+
let projectWatcher = null, sessionWatcher = null, debounce = null;
|
|
464
|
+
const fire = () => { if (debounce)
|
|
465
|
+
clearTimeout(debounce); debounce = setTimeout(() => { try {
|
|
466
|
+
push(buildSnapshot(params));
|
|
467
|
+
}
|
|
468
|
+
catch { } }, 150); };
|
|
469
|
+
const projects = listProjects();
|
|
470
|
+
const project = resolveProject(params, projects);
|
|
471
|
+
if (project) {
|
|
472
|
+
const dir = path.join(ccProjectsDir(), project.encoded);
|
|
473
|
+
try {
|
|
474
|
+
projectWatcher = fs.watch(dir, (_evt, filename) => { if (filename && String(filename).endsWith('.jsonl'))
|
|
475
|
+
fire(); });
|
|
476
|
+
}
|
|
477
|
+
catch { }
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
sessionWatcher = fs.watch(p.sessionsDir, { recursive: true }, (_evt, filename) => { if (filename && String(filename).endsWith('active.json'))
|
|
481
|
+
fire(); });
|
|
482
|
+
}
|
|
483
|
+
catch { }
|
|
484
|
+
return () => { if (projectWatcher)
|
|
485
|
+
projectWatcher.close(); if (sessionWatcher)
|
|
486
|
+
sessionWatcher.close(); if (debounce)
|
|
487
|
+
clearTimeout(debounce); };
|
|
488
|
+
},
|
|
489
|
+
};
|