@swarmify/agents-cli 1.10.4 → 1.11.1
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/CHANGELOG.md +31 -0
- package/README.md +73 -2
- package/dist/commands/__tests__/sessions.test.js +333 -0
- package/dist/commands/__tests__/sessions.test.js.map +1 -1
- package/dist/commands/commands.d.ts.map +1 -1
- package/dist/commands/commands.js +5 -11
- package/dist/commands/commands.js.map +1 -1
- package/dist/commands/exec.d.ts.map +1 -1
- package/dist/commands/exec.js +11 -1
- package/dist/commands/exec.js.map +1 -1
- package/dist/commands/hooks.d.ts.map +1 -1
- package/dist/commands/hooks.js +5 -11
- package/dist/commands/hooks.js.map +1 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +109 -43
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/packages.d.ts.map +1 -1
- package/dist/commands/packages.js +92 -42
- package/dist/commands/packages.js.map +1 -1
- package/dist/commands/permissions.d.ts.map +1 -1
- package/dist/commands/permissions.js +38 -34
- package/dist/commands/permissions.js.map +1 -1
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/commands/pull.js +14 -15
- package/dist/commands/pull.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +2 -0
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/rules.d.ts.map +1 -1
- package/dist/commands/rules.js +5 -11
- package/dist/commands/rules.js.map +1 -1
- package/dist/commands/sessions.d.ts.map +1 -1
- package/dist/commands/sessions.js +217 -34
- package/dist/commands/sessions.js.map +1 -1
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +5 -11
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/versions.d.ts.map +1 -1
- package/dist/commands/versions.js +31 -9
- package/dist/commands/versions.js.map +1 -1
- package/dist/commands/view.d.ts.map +1 -1
- package/dist/commands/view.js +38 -58
- package/dist/commands/view.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/__tests__/exec.test.js +50 -1
- package/dist/lib/__tests__/exec.test.js.map +1 -1
- package/dist/lib/__tests__/usage.test.d.ts +2 -0
- package/dist/lib/__tests__/usage.test.d.ts.map +1 -0
- package/dist/lib/__tests__/usage.test.js +145 -0
- package/dist/lib/__tests__/usage.test.js.map +1 -0
- package/dist/lib/agents.d.ts +19 -0
- package/dist/lib/agents.d.ts.map +1 -1
- package/dist/lib/agents.js +177 -16
- package/dist/lib/agents.js.map +1 -1
- package/dist/lib/exec.d.ts +3 -0
- package/dist/lib/exec.d.ts.map +1 -1
- package/dist/lib/exec.js +37 -0
- package/dist/lib/exec.js.map +1 -1
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/lib/registry.js +8 -0
- package/dist/lib/registry.js.map +1 -1
- package/dist/lib/resources.d.ts +2 -0
- package/dist/lib/resources.d.ts.map +1 -1
- package/dist/lib/resources.js +7 -7
- package/dist/lib/resources.js.map +1 -1
- package/dist/lib/session/discover.d.ts +10 -0
- package/dist/lib/session/discover.d.ts.map +1 -1
- package/dist/lib/session/discover.js +563 -119
- package/dist/lib/session/discover.js.map +1 -1
- package/dist/lib/session/prompt.d.ts +3 -0
- package/dist/lib/session/prompt.d.ts.map +1 -0
- package/dist/lib/session/prompt.js +41 -0
- package/dist/lib/session/prompt.js.map +1 -0
- package/dist/lib/session/render.d.ts.map +1 -1
- package/dist/lib/session/render.js +6 -37
- package/dist/lib/session/render.js.map +1 -1
- package/dist/lib/session/types.d.ts +7 -0
- package/dist/lib/session/types.d.ts.map +1 -1
- package/dist/lib/shims.d.ts +5 -0
- package/dist/lib/shims.d.ts.map +1 -1
- package/dist/lib/shims.js +20 -1
- package/dist/lib/shims.js.map +1 -1
- package/dist/lib/types.d.ts +1 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/usage.d.ts +26 -0
- package/dist/lib/usage.d.ts.map +1 -1
- package/dist/lib/usage.js +125 -48
- package/dist/lib/usage.js.map +1 -1
- package/dist/lib/versions.d.ts +28 -0
- package/dist/lib/versions.d.ts.map +1 -1
- package/dist/lib/versions.js +181 -1
- package/dist/lib/versions.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,11 +4,16 @@ import * as os from 'os';
|
|
|
4
4
|
import * as crypto from 'crypto';
|
|
5
5
|
import * as readline from 'readline';
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
|
+
import { getCliVersion } from '../agents.js';
|
|
8
|
+
import { getConfigSymlinkVersion } from '../shims.js';
|
|
7
9
|
import { SESSION_AGENTS } from './types.js';
|
|
10
|
+
import { extractSessionTopic } from './prompt.js';
|
|
8
11
|
const HOME = os.homedir();
|
|
9
12
|
const AGENTS_DIR = path.join(HOME, '.agents');
|
|
10
13
|
const SESSIONS_DIR = path.join(AGENTS_DIR, 'sessions');
|
|
11
14
|
const INDEX_PATH = path.join(SESSIONS_DIR, 'index.jsonl');
|
|
15
|
+
const CONTENT_INDEX_PATH = path.join(SESSIONS_DIR, 'content_index.jsonl');
|
|
16
|
+
const cachedAgentVersions = new Map();
|
|
12
17
|
/**
|
|
13
18
|
* Discover sessions across all installed agents, versions, and backups.
|
|
14
19
|
* Merges with a persistent index so sessions survive version removal.
|
|
@@ -43,12 +48,27 @@ export async function discoverSessions(options) {
|
|
|
43
48
|
toSave.set(s.id, s);
|
|
44
49
|
}
|
|
45
50
|
saveIndex([...toSave.values()]);
|
|
51
|
+
// Build content index for all discovered sessions
|
|
52
|
+
const contentIndex = await buildContentIndex(sessions);
|
|
53
|
+
saveContentIndex(contentIndex);
|
|
54
|
+
const projectQuery = options?.project?.trim();
|
|
46
55
|
// Filter by project (case-insensitive substring match)
|
|
47
|
-
if (
|
|
48
|
-
const query =
|
|
56
|
+
if (projectQuery) {
|
|
57
|
+
const query = projectQuery.toLowerCase();
|
|
49
58
|
sessions = sessions.filter(s => s.project?.toLowerCase().includes(query));
|
|
50
59
|
}
|
|
51
|
-
|
|
60
|
+
// Apply time range filters
|
|
61
|
+
if (options?.since || options?.until) {
|
|
62
|
+
const sinceMs = options.since ? parseTimeFilter(options.since) : 0;
|
|
63
|
+
const untilMs = options.until ? new Date(options.until).getTime() : Infinity;
|
|
64
|
+
sessions = sessions.filter(s => {
|
|
65
|
+
const ts = new Date(s.timestamp).getTime();
|
|
66
|
+
return ts >= sinceMs && ts <= untilMs;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// An explicit project search should scan across directories instead of
|
|
70
|
+
// intersecting with the default cwd-only scope.
|
|
71
|
+
if (!options?.all && !projectQuery) {
|
|
52
72
|
const currentDir = normalizeCwd(options?.cwd || process.cwd());
|
|
53
73
|
sessions = sessions.filter(s => normalizeCwd(s.cwd) === currentDir);
|
|
54
74
|
}
|
|
@@ -113,18 +133,7 @@ function saveIndex(sessions) {
|
|
|
113
133
|
if (seen.has(s.id))
|
|
114
134
|
continue;
|
|
115
135
|
seen.add(s.id);
|
|
116
|
-
lines.push(JSON.stringify(
|
|
117
|
-
id: s.id,
|
|
118
|
-
shortId: s.shortId,
|
|
119
|
-
agent: s.agent,
|
|
120
|
-
timestamp: s.timestamp,
|
|
121
|
-
project: s.project,
|
|
122
|
-
cwd: s.cwd,
|
|
123
|
-
filePath: s.filePath,
|
|
124
|
-
gitBranch: s.gitBranch,
|
|
125
|
-
version: s.version,
|
|
126
|
-
account: s.account,
|
|
127
|
-
}));
|
|
136
|
+
lines.push(JSON.stringify(s));
|
|
128
137
|
}
|
|
129
138
|
fs.writeFileSync(INDEX_PATH, lines.join('\n') + '\n', 'utf-8');
|
|
130
139
|
}
|
|
@@ -133,6 +142,81 @@ function saveIndex(sessions) {
|
|
|
133
142
|
}
|
|
134
143
|
}
|
|
135
144
|
// ---------------------------------------------------------------------------
|
|
145
|
+
// Content index (inverted term -> session terms)
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
async function buildContentIndex(sessions) {
|
|
148
|
+
const index = new Map();
|
|
149
|
+
for (const session of sessions) {
|
|
150
|
+
const terms = extractSessionTerms(session);
|
|
151
|
+
for (const term of terms) {
|
|
152
|
+
if (!index.has(term))
|
|
153
|
+
index.set(term, new Set());
|
|
154
|
+
index.get(term).add(session.id);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return index;
|
|
158
|
+
}
|
|
159
|
+
function extractSessionTerms(session) {
|
|
160
|
+
const textParts = [];
|
|
161
|
+
if (session.topic)
|
|
162
|
+
textParts.push(session.topic);
|
|
163
|
+
if (session.project)
|
|
164
|
+
textParts.push(session.project);
|
|
165
|
+
if (session.cwd)
|
|
166
|
+
textParts.push(session.cwd);
|
|
167
|
+
if (session.gitBranch)
|
|
168
|
+
textParts.push(session.gitBranch);
|
|
169
|
+
if (session.account)
|
|
170
|
+
textParts.push(session.account);
|
|
171
|
+
if (session._userTerms)
|
|
172
|
+
textParts.push(session._userTerms.join('\n'));
|
|
173
|
+
return tokenizeText(textParts.join('\n'));
|
|
174
|
+
}
|
|
175
|
+
function tokenizeText(text) {
|
|
176
|
+
const seen = new Set();
|
|
177
|
+
const terms = [];
|
|
178
|
+
const tokens = text.toLowerCase().split(/[^a-z0-9]+/);
|
|
179
|
+
for (const token of tokens) {
|
|
180
|
+
if (token.length < 2 || seen.has(token))
|
|
181
|
+
continue;
|
|
182
|
+
seen.add(token);
|
|
183
|
+
terms.push(token);
|
|
184
|
+
}
|
|
185
|
+
return terms;
|
|
186
|
+
}
|
|
187
|
+
function saveContentIndex(index) {
|
|
188
|
+
try {
|
|
189
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
190
|
+
const lines = [];
|
|
191
|
+
for (const [term, sessionIds] of index) {
|
|
192
|
+
lines.push(JSON.stringify({ term, sessions: [...sessionIds] }));
|
|
193
|
+
}
|
|
194
|
+
fs.writeFileSync(CONTENT_INDEX_PATH, lines.join('\n') + '\n', 'utf-8');
|
|
195
|
+
}
|
|
196
|
+
catch { /* index save failure is non-fatal */ }
|
|
197
|
+
}
|
|
198
|
+
function loadContentIndex() {
|
|
199
|
+
const index = new Map();
|
|
200
|
+
if (!fs.existsSync(CONTENT_INDEX_PATH))
|
|
201
|
+
return index;
|
|
202
|
+
try {
|
|
203
|
+
const content = fs.readFileSync(CONTENT_INDEX_PATH, 'utf-8');
|
|
204
|
+
for (const line of content.split('\n')) {
|
|
205
|
+
if (!line.trim())
|
|
206
|
+
continue;
|
|
207
|
+
try {
|
|
208
|
+
const entry = JSON.parse(line);
|
|
209
|
+
if (entry.term && entry.sessions) {
|
|
210
|
+
index.set(entry.term, new Set(entry.sessions));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch { /* malformed index entry, skip */ }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch { /* index load failure is non-fatal */ }
|
|
217
|
+
return index;
|
|
218
|
+
}
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
136
220
|
// Multi-version directory scanning
|
|
137
221
|
// ---------------------------------------------------------------------------
|
|
138
222
|
/**
|
|
@@ -269,41 +353,25 @@ async function discoverClaudeSessions() {
|
|
|
269
353
|
return sessions;
|
|
270
354
|
}
|
|
271
355
|
async function readClaudeMeta(filePath, sessionId, account) {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if ((parsed.type === 'user' || parsed.type === 'assistant') && parsed.timestamp) {
|
|
292
|
-
const cwd = parsed.cwd || '';
|
|
293
|
-
return {
|
|
294
|
-
id: sessionId,
|
|
295
|
-
shortId: sessionId.slice(0, 8),
|
|
296
|
-
agent: 'claude',
|
|
297
|
-
timestamp: parsed.timestamp,
|
|
298
|
-
project: cwd ? path.basename(cwd) : undefined,
|
|
299
|
-
cwd,
|
|
300
|
-
filePath,
|
|
301
|
-
gitBranch: parsed.gitBranch || undefined,
|
|
302
|
-
version: parsed.version || undefined,
|
|
303
|
-
account,
|
|
304
|
-
topic,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
356
|
+
const scan = await scanClaudeSession(filePath);
|
|
357
|
+
if (scan.timestamp) {
|
|
358
|
+
const cwd = scan.cwd || '';
|
|
359
|
+
return {
|
|
360
|
+
id: sessionId,
|
|
361
|
+
shortId: sessionId.slice(0, 8),
|
|
362
|
+
agent: 'claude',
|
|
363
|
+
timestamp: scan.timestamp,
|
|
364
|
+
project: cwd ? path.basename(cwd) : undefined,
|
|
365
|
+
cwd,
|
|
366
|
+
filePath,
|
|
367
|
+
gitBranch: scan.gitBranch,
|
|
368
|
+
version: scan.version,
|
|
369
|
+
account,
|
|
370
|
+
topic: scan.topic,
|
|
371
|
+
messageCount: scan.messageCount,
|
|
372
|
+
tokenCount: scan.tokenCount,
|
|
373
|
+
_userTerms: scan.userTerms?.flatMap(splitLines),
|
|
374
|
+
};
|
|
307
375
|
}
|
|
308
376
|
// Fallback: use file mtime
|
|
309
377
|
const stat = safeStatSync(filePath);
|
|
@@ -314,6 +382,10 @@ async function readClaudeMeta(filePath, sessionId, account) {
|
|
|
314
382
|
timestamp: stat ? stat.mtime.toISOString() : new Date().toISOString(),
|
|
315
383
|
filePath,
|
|
316
384
|
account,
|
|
385
|
+
messageCount: scan.messageCount,
|
|
386
|
+
tokenCount: scan.tokenCount,
|
|
387
|
+
topic: scan.topic,
|
|
388
|
+
_userTerms: scan.userTerms?.flatMap(splitLines),
|
|
317
389
|
};
|
|
318
390
|
}
|
|
319
391
|
// ---------------------------------------------------------------------------
|
|
@@ -366,12 +438,13 @@ async function discoverCodexSessions() {
|
|
|
366
438
|
const sessions = [];
|
|
367
439
|
const seen = new Set();
|
|
368
440
|
const account = getCodexAccount();
|
|
441
|
+
const currentVersion = await getCurrentAgentVersion('codex');
|
|
369
442
|
let skipped = 0;
|
|
370
443
|
for (const sessionsDir of getAgentSessionDirs('codex', 'sessions')) {
|
|
371
444
|
const jsonlFiles = walkForFiles(sessionsDir, '.jsonl', 200);
|
|
372
445
|
for (const filePath of jsonlFiles) {
|
|
373
446
|
try {
|
|
374
|
-
const meta = await readCodexMeta(filePath, account);
|
|
447
|
+
const meta = await readCodexMeta(filePath, account, currentVersion);
|
|
375
448
|
if (meta && !seen.has(meta.id)) {
|
|
376
449
|
seen.add(meta.id);
|
|
377
450
|
sessions.push(meta);
|
|
@@ -387,52 +460,27 @@ async function discoverCodexSessions() {
|
|
|
387
460
|
}
|
|
388
461
|
return sessions;
|
|
389
462
|
}
|
|
390
|
-
async function readCodexMeta(filePath, account) {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
return null;
|
|
394
|
-
let parsed;
|
|
395
|
-
try {
|
|
396
|
-
parsed = JSON.parse(lines[0]);
|
|
397
|
-
}
|
|
398
|
-
catch {
|
|
399
|
-
return null;
|
|
400
|
-
}
|
|
401
|
-
if (parsed.type !== 'session_meta')
|
|
402
|
-
return null;
|
|
403
|
-
const payload = parsed.payload || {};
|
|
404
|
-
const sessionId = payload.id || '';
|
|
463
|
+
async function readCodexMeta(filePath, account, currentVersion) {
|
|
464
|
+
const scan = await scanCodexSession(filePath);
|
|
465
|
+
const sessionId = scan.sessionId || '';
|
|
405
466
|
if (!sessionId)
|
|
406
467
|
return null;
|
|
407
|
-
|
|
408
|
-
let topic;
|
|
409
|
-
for (let i = 1; i < lines.length; i++) {
|
|
410
|
-
try {
|
|
411
|
-
const ev = JSON.parse(lines[i]);
|
|
412
|
-
if (ev.type === 'message' && ev.role === 'user' && ev.content) {
|
|
413
|
-
const text = typeof ev.content === 'string' ? ev.content
|
|
414
|
-
: Array.isArray(ev.content) ? ev.content.find((b) => b.type === 'input_text')?.text : undefined;
|
|
415
|
-
if (text) {
|
|
416
|
-
topic = extractTopic(text);
|
|
417
|
-
break;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
catch { /* malformed event line */ }
|
|
422
|
-
}
|
|
423
|
-
const cwd = payload.cwd || '';
|
|
468
|
+
const cwd = scan.cwd || '';
|
|
424
469
|
return {
|
|
425
470
|
id: sessionId,
|
|
426
471
|
shortId: sessionId.slice(0, 8),
|
|
427
472
|
agent: 'codex',
|
|
428
|
-
timestamp:
|
|
473
|
+
timestamp: scan.timestamp || new Date().toISOString(),
|
|
429
474
|
project: cwd ? path.basename(cwd) : undefined,
|
|
430
475
|
cwd,
|
|
431
476
|
filePath,
|
|
432
|
-
gitBranch:
|
|
433
|
-
version:
|
|
434
|
-
topic,
|
|
477
|
+
gitBranch: scan.gitBranch,
|
|
478
|
+
version: resolveSessionVersion('codex', filePath, scan.version, currentVersion),
|
|
479
|
+
topic: scan.topic,
|
|
480
|
+
messageCount: scan.messageCount,
|
|
481
|
+
tokenCount: scan.tokenCount,
|
|
435
482
|
account,
|
|
483
|
+
_userTerms: scan.userTerms?.flatMap(splitLines),
|
|
436
484
|
};
|
|
437
485
|
}
|
|
438
486
|
// ---------------------------------------------------------------------------
|
|
@@ -442,6 +490,7 @@ async function discoverGeminiSessions() {
|
|
|
442
490
|
const projectMap = buildGeminiProjectMap();
|
|
443
491
|
const sessions = [];
|
|
444
492
|
const seen = new Set();
|
|
493
|
+
const currentVersion = await getCurrentAgentVersion('gemini');
|
|
445
494
|
let skipped = 0;
|
|
446
495
|
for (const tmpDir of getAgentSessionDirs('gemini', 'tmp')) {
|
|
447
496
|
let hashDirs;
|
|
@@ -465,7 +514,7 @@ async function discoverGeminiSessions() {
|
|
|
465
514
|
for (const file of chatFiles) {
|
|
466
515
|
const filePath = path.join(chatsDir, file);
|
|
467
516
|
try {
|
|
468
|
-
const meta = readGeminiMeta(filePath, hashDir, projectMap);
|
|
517
|
+
const meta = readGeminiMeta(filePath, hashDir, projectMap, currentVersion);
|
|
469
518
|
if (meta && !seen.has(meta.id)) {
|
|
470
519
|
seen.add(meta.id);
|
|
471
520
|
sessions.push(meta);
|
|
@@ -482,17 +531,22 @@ async function discoverGeminiSessions() {
|
|
|
482
531
|
}
|
|
483
532
|
return sessions;
|
|
484
533
|
}
|
|
485
|
-
function readGeminiMeta(filePath, hashDir, projectMap) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const sessionId =
|
|
494
|
-
const startTime =
|
|
495
|
-
const projectHash =
|
|
534
|
+
function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
|
|
535
|
+
let session;
|
|
536
|
+
try {
|
|
537
|
+
session = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
const sessionId = typeof session.sessionId === 'string' ? session.sessionId : '';
|
|
543
|
+
const startTime = typeof session.startTime === 'string' ? session.startTime : '';
|
|
544
|
+
const projectHash = typeof session.projectHash === 'string' ? session.projectHash : '';
|
|
545
|
+
const embeddedVersion = typeof session.version === 'string'
|
|
546
|
+
? session.version
|
|
547
|
+
: typeof session.cliVersion === 'string'
|
|
548
|
+
? session.cliVersion
|
|
549
|
+
: undefined;
|
|
496
550
|
if (!sessionId)
|
|
497
551
|
return null;
|
|
498
552
|
// Resolve project name from hash
|
|
@@ -500,11 +554,33 @@ function readGeminiMeta(filePath, hashDir, projectMap) {
|
|
|
500
554
|
const project = projectInfo?.name || hashDir.slice(0, 12);
|
|
501
555
|
const cwd = projectInfo?.path;
|
|
502
556
|
const stat = safeStatSync(filePath);
|
|
503
|
-
|
|
557
|
+
const messages = Array.isArray(session.messages) ? session.messages : [];
|
|
504
558
|
let topic;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
559
|
+
let messageCount = 0;
|
|
560
|
+
let tokenCount = 0;
|
|
561
|
+
let sawTokenCount = false;
|
|
562
|
+
const userTerms = [];
|
|
563
|
+
for (const message of messages) {
|
|
564
|
+
if (message.type === 'user') {
|
|
565
|
+
const text = extractGeminiMessageText(message.content);
|
|
566
|
+
if (text) {
|
|
567
|
+
messageCount++;
|
|
568
|
+
userTerms.push(text);
|
|
569
|
+
if (!topic)
|
|
570
|
+
topic = extractSessionTopic(text);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
else if (message.type === 'gemini') {
|
|
574
|
+
if (extractGeminiMessageText(message.content)) {
|
|
575
|
+
messageCount++;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const total = getGeminiTokenCount(message.tokens);
|
|
579
|
+
if (total !== null) {
|
|
580
|
+
tokenCount += total;
|
|
581
|
+
sawTokenCount = true;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
508
584
|
return {
|
|
509
585
|
id: sessionId,
|
|
510
586
|
shortId: sessionId.slice(0, 8),
|
|
@@ -513,7 +589,11 @@ function readGeminiMeta(filePath, hashDir, projectMap) {
|
|
|
513
589
|
project,
|
|
514
590
|
cwd,
|
|
515
591
|
filePath,
|
|
592
|
+
version: resolveSessionVersion('gemini', filePath, embeddedVersion, currentVersion),
|
|
516
593
|
topic,
|
|
594
|
+
messageCount,
|
|
595
|
+
tokenCount: sawTokenCount ? tokenCount : undefined,
|
|
596
|
+
_userTerms: userTerms.length > 0 ? userTerms : undefined,
|
|
517
597
|
};
|
|
518
598
|
}
|
|
519
599
|
function buildGeminiProjectMap() {
|
|
@@ -594,13 +674,37 @@ async function discoverOpenCodeSessions() {
|
|
|
594
674
|
if (!fs.existsSync(OPENCODE_DB))
|
|
595
675
|
return [];
|
|
596
676
|
const account = getOpenCodeAccount();
|
|
677
|
+
const currentVersion = await getCurrentAgentVersion('opencode');
|
|
597
678
|
try {
|
|
598
679
|
// Query sessions. time_created is millisecond epoch. Limit to 200 most recent.
|
|
599
680
|
// Use session.title as topic (OpenCode auto-generates good titles).
|
|
600
681
|
const query = `
|
|
601
|
-
SELECT
|
|
602
|
-
|
|
603
|
-
|
|
682
|
+
SELECT
|
|
683
|
+
s.id,
|
|
684
|
+
s.title,
|
|
685
|
+
s.directory,
|
|
686
|
+
s.version,
|
|
687
|
+
s.time_created,
|
|
688
|
+
COALESCE(stats.message_count, 0),
|
|
689
|
+
stats.token_count,
|
|
690
|
+
COALESCE(stats.has_token_data, 0)
|
|
691
|
+
FROM session s
|
|
692
|
+
LEFT JOIN (
|
|
693
|
+
SELECT
|
|
694
|
+
session_id,
|
|
695
|
+
COUNT(*) AS message_count,
|
|
696
|
+
SUM(
|
|
697
|
+
COALESCE(json_extract(data, '$.tokens.input'), 0) +
|
|
698
|
+
COALESCE(json_extract(data, '$.tokens.output'), 0) +
|
|
699
|
+
COALESCE(json_extract(data, '$.tokens.reasoning'), 0) +
|
|
700
|
+
COALESCE(json_extract(data, '$.tokens.cache.read'), 0) +
|
|
701
|
+
COALESCE(json_extract(data, '$.tokens.cache.write'), 0)
|
|
702
|
+
) AS token_count,
|
|
703
|
+
MAX(CASE WHEN json_type(data, '$.tokens') IS NOT NULL THEN 1 ELSE 0 END) AS has_token_data
|
|
704
|
+
FROM message
|
|
705
|
+
GROUP BY session_id
|
|
706
|
+
) stats ON stats.session_id = s.id
|
|
707
|
+
WHERE s.parent_id IS NULL
|
|
604
708
|
ORDER BY time_created DESC
|
|
605
709
|
LIMIT 200;
|
|
606
710
|
`.replace(/\n/g, ' ');
|
|
@@ -609,10 +713,13 @@ async function discoverOpenCodeSessions() {
|
|
|
609
713
|
for (const line of out.split('\n')) {
|
|
610
714
|
if (!line.trim())
|
|
611
715
|
continue;
|
|
612
|
-
const [id, title, directory, version, timeCreatedStr] = line.split('|||');
|
|
716
|
+
const [id, title, directory, version, timeCreatedStr, messageCountStr, tokenCountStr, hasTokenDataStr] = line.split('|||');
|
|
613
717
|
if (!id)
|
|
614
718
|
continue;
|
|
615
719
|
const timeCreated = parseInt(timeCreatedStr, 10);
|
|
720
|
+
const messageCount = parseInt(messageCountStr, 10);
|
|
721
|
+
const tokenCount = parseInt(tokenCountStr, 10);
|
|
722
|
+
const hasTokenData = hasTokenDataStr === '1';
|
|
616
723
|
const timestamp = isNaN(timeCreated) ? new Date().toISOString() : new Date(timeCreated).toISOString();
|
|
617
724
|
const topic = title || undefined;
|
|
618
725
|
sessions.push({
|
|
@@ -623,9 +730,11 @@ async function discoverOpenCodeSessions() {
|
|
|
623
730
|
project: directory ? path.basename(directory) : undefined,
|
|
624
731
|
cwd: directory || undefined,
|
|
625
732
|
filePath: `${OPENCODE_DB}#${id}`,
|
|
626
|
-
version: version || undefined,
|
|
733
|
+
version: resolveSessionVersion('opencode', OPENCODE_DB, version || undefined, currentVersion),
|
|
627
734
|
account,
|
|
628
735
|
topic,
|
|
736
|
+
messageCount: Number.isNaN(messageCount) ? undefined : messageCount,
|
|
737
|
+
tokenCount: hasTokenData && !Number.isNaN(tokenCount) ? tokenCount : undefined,
|
|
629
738
|
});
|
|
630
739
|
}
|
|
631
740
|
return sessions;
|
|
@@ -649,6 +758,7 @@ async function discoverOpenClawSessions() {
|
|
|
649
758
|
catch {
|
|
650
759
|
return sessions;
|
|
651
760
|
}
|
|
761
|
+
const currentVersion = await getCurrentAgentVersion('openclaw');
|
|
652
762
|
// Discover active channels
|
|
653
763
|
// Format: "- Telegram default (Jeff): enabled, configured, running, out:2h ago, mode:polling, token:config"
|
|
654
764
|
try {
|
|
@@ -671,6 +781,7 @@ async function discoverOpenClawSessions() {
|
|
|
671
781
|
agent: 'openclaw',
|
|
672
782
|
timestamp: new Date().toISOString(),
|
|
673
783
|
project: name,
|
|
784
|
+
version: currentVersion,
|
|
674
785
|
filePath: '',
|
|
675
786
|
});
|
|
676
787
|
}
|
|
@@ -713,6 +824,7 @@ async function discoverOpenClawSessions() {
|
|
|
713
824
|
timestamp: new Date().toISOString(),
|
|
714
825
|
project: `${jobName} (${agentId || 'unknown'})`,
|
|
715
826
|
cwd: status,
|
|
827
|
+
version: currentVersion,
|
|
716
828
|
filePath: '',
|
|
717
829
|
});
|
|
718
830
|
}
|
|
@@ -722,6 +834,150 @@ async function discoverOpenClawSessions() {
|
|
|
722
834
|
}
|
|
723
835
|
return sessions;
|
|
724
836
|
}
|
|
837
|
+
async function scanClaudeSession(filePath) {
|
|
838
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
839
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
840
|
+
let timestamp;
|
|
841
|
+
let cwd;
|
|
842
|
+
let gitBranch;
|
|
843
|
+
let version;
|
|
844
|
+
let topic;
|
|
845
|
+
let messageCount = 0;
|
|
846
|
+
let tokenCount = 0;
|
|
847
|
+
let sawTokenCount = false;
|
|
848
|
+
const seenAssistantIds = new Set();
|
|
849
|
+
const userTexts = [];
|
|
850
|
+
try {
|
|
851
|
+
for await (const line of rl) {
|
|
852
|
+
if (!line.trim())
|
|
853
|
+
continue;
|
|
854
|
+
let parsed;
|
|
855
|
+
try {
|
|
856
|
+
parsed = JSON.parse(line);
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
if (!timestamp && (parsed.type === 'user' || parsed.type === 'assistant') && parsed.timestamp) {
|
|
862
|
+
timestamp = parsed.timestamp;
|
|
863
|
+
cwd = parsed.cwd || '';
|
|
864
|
+
gitBranch = parsed.gitBranch || undefined;
|
|
865
|
+
version = parsed.version || undefined;
|
|
866
|
+
}
|
|
867
|
+
if (parsed.type === 'user') {
|
|
868
|
+
const text = extractClaudeUserText(parsed);
|
|
869
|
+
if (text) {
|
|
870
|
+
messageCount++;
|
|
871
|
+
userTexts.push(text);
|
|
872
|
+
if (!topic)
|
|
873
|
+
topic = extractSessionTopic(text);
|
|
874
|
+
}
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
if (parsed.type !== 'assistant')
|
|
878
|
+
continue;
|
|
879
|
+
const assistantId = typeof parsed.message?.id === 'string'
|
|
880
|
+
? parsed.message.id
|
|
881
|
+
: typeof parsed.uuid === 'string'
|
|
882
|
+
? parsed.uuid
|
|
883
|
+
: undefined;
|
|
884
|
+
const logicalId = assistantId || `${parsed.timestamp || ''}:${seenAssistantIds.size}`;
|
|
885
|
+
if (seenAssistantIds.has(logicalId))
|
|
886
|
+
continue;
|
|
887
|
+
seenAssistantIds.add(logicalId);
|
|
888
|
+
messageCount++;
|
|
889
|
+
const usage = getClaudeUsageTotal(parsed.message?.usage || parsed.usage);
|
|
890
|
+
if (usage !== null) {
|
|
891
|
+
tokenCount += usage;
|
|
892
|
+
sawTokenCount = true;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
finally {
|
|
897
|
+
rl.close();
|
|
898
|
+
stream.destroy();
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
timestamp,
|
|
902
|
+
cwd,
|
|
903
|
+
gitBranch,
|
|
904
|
+
version,
|
|
905
|
+
topic,
|
|
906
|
+
messageCount,
|
|
907
|
+
tokenCount: sawTokenCount ? tokenCount : undefined,
|
|
908
|
+
userTerms: userTexts.length > 0 ? userTexts : undefined,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
async function scanCodexSession(filePath) {
|
|
912
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
913
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
914
|
+
let sessionId;
|
|
915
|
+
let timestamp;
|
|
916
|
+
let cwd;
|
|
917
|
+
let gitBranch;
|
|
918
|
+
let version;
|
|
919
|
+
let topic;
|
|
920
|
+
let messageCount = 0;
|
|
921
|
+
let tokenCount;
|
|
922
|
+
const userTexts = [];
|
|
923
|
+
try {
|
|
924
|
+
for await (const line of rl) {
|
|
925
|
+
if (!line.trim())
|
|
926
|
+
continue;
|
|
927
|
+
let parsed;
|
|
928
|
+
try {
|
|
929
|
+
parsed = JSON.parse(line);
|
|
930
|
+
}
|
|
931
|
+
catch {
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
if (parsed.type === 'session_meta') {
|
|
935
|
+
const payload = parsed.payload || {};
|
|
936
|
+
sessionId = payload.id || sessionId;
|
|
937
|
+
timestamp = payload.timestamp || parsed.timestamp || timestamp;
|
|
938
|
+
cwd = payload.cwd || cwd;
|
|
939
|
+
gitBranch = payload.git?.branch || gitBranch;
|
|
940
|
+
version = payload.cli_version || payload.version || version;
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
if (parsed.type === 'response_item' && parsed.payload?.type === 'message') {
|
|
944
|
+
const role = parsed.payload.role === 'user' || parsed.payload.role === 'developer'
|
|
945
|
+
? 'user'
|
|
946
|
+
: 'assistant';
|
|
947
|
+
const text = extractCodexMessageText(parsed.payload.content, role);
|
|
948
|
+
if (!text)
|
|
949
|
+
continue;
|
|
950
|
+
messageCount++;
|
|
951
|
+
if (role === 'user') {
|
|
952
|
+
userTexts.push(text);
|
|
953
|
+
if (!topic)
|
|
954
|
+
topic = extractSessionTopic(text);
|
|
955
|
+
}
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
if (parsed.type === 'event_msg' && parsed.payload?.type === 'token_count') {
|
|
959
|
+
const total = getCodexTokenCount(parsed.payload.info?.total_token_usage);
|
|
960
|
+
if (total !== null)
|
|
961
|
+
tokenCount = total;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
finally {
|
|
966
|
+
rl.close();
|
|
967
|
+
stream.destroy();
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
sessionId,
|
|
971
|
+
timestamp,
|
|
972
|
+
cwd,
|
|
973
|
+
gitBranch,
|
|
974
|
+
version,
|
|
975
|
+
topic,
|
|
976
|
+
messageCount,
|
|
977
|
+
tokenCount,
|
|
978
|
+
userTerms: userTexts.length > 0 ? userTexts : undefined,
|
|
979
|
+
};
|
|
980
|
+
}
|
|
725
981
|
// ---------------------------------------------------------------------------
|
|
726
982
|
// Utilities
|
|
727
983
|
// ---------------------------------------------------------------------------
|
|
@@ -777,11 +1033,6 @@ export function walkForFiles(dir, ext, limit) {
|
|
|
777
1033
|
results.sort((a, b) => b.mtime - a.mtime);
|
|
778
1034
|
return results.slice(0, limit).map(r => r.path);
|
|
779
1035
|
}
|
|
780
|
-
function extractJsonField(text, field) {
|
|
781
|
-
const re = new RegExp(`"${field}"\\s*:\\s*"([^"]*)"`, 'i');
|
|
782
|
-
const match = text.match(re);
|
|
783
|
-
return match ? match[1] : '';
|
|
784
|
-
}
|
|
785
1036
|
function sha256(input) {
|
|
786
1037
|
return crypto.createHash('sha256').update(input).digest('hex');
|
|
787
1038
|
}
|
|
@@ -801,11 +1052,204 @@ function safeRealpathSync(p) {
|
|
|
801
1052
|
return null;
|
|
802
1053
|
}
|
|
803
1054
|
}
|
|
804
|
-
function
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1055
|
+
function extractClaudeUserText(parsed) {
|
|
1056
|
+
if (parsed.isMeta === true)
|
|
1057
|
+
return undefined;
|
|
1058
|
+
const content = parsed.message?.content;
|
|
1059
|
+
if (typeof content === 'string') {
|
|
1060
|
+
const text = content.trim();
|
|
1061
|
+
return isLocalCommandMessage(text) ? undefined : text || undefined;
|
|
1062
|
+
}
|
|
1063
|
+
if (!Array.isArray(content))
|
|
1064
|
+
return undefined;
|
|
1065
|
+
const text = content
|
|
1066
|
+
.filter((block) => block.type === 'text')
|
|
1067
|
+
.map((block) => String(block.text || '').trim())
|
|
1068
|
+
.find((value) => value && !value.startsWith('[Request interrupted'));
|
|
1069
|
+
if (!text || isLocalCommandMessage(text))
|
|
1070
|
+
return undefined;
|
|
1071
|
+
return text;
|
|
1072
|
+
}
|
|
1073
|
+
function isLocalCommandMessage(text) {
|
|
1074
|
+
return /<local-command-caveat>|<bash-(input|stdout|stderr)>/i.test(text);
|
|
1075
|
+
}
|
|
1076
|
+
function splitLines(text) {
|
|
1077
|
+
return text.split('\n').map(l => l.trim()).filter(Boolean);
|
|
1078
|
+
}
|
|
1079
|
+
function getClaudeUsageTotal(usage) {
|
|
1080
|
+
if (!usage || typeof usage !== 'object')
|
|
1081
|
+
return null;
|
|
1082
|
+
return sumKnownNumbers([
|
|
1083
|
+
usage.input_tokens,
|
|
1084
|
+
usage.output_tokens,
|
|
1085
|
+
usage.cache_creation_input_tokens,
|
|
1086
|
+
usage.cache_read_input_tokens,
|
|
1087
|
+
]);
|
|
1088
|
+
}
|
|
1089
|
+
function extractCodexMessageText(contentBlocks, role) {
|
|
1090
|
+
if (!Array.isArray(contentBlocks))
|
|
1091
|
+
return undefined;
|
|
1092
|
+
const matches = role === 'user'
|
|
1093
|
+
? contentBlocks.filter((block) => block.type === 'input_text')
|
|
1094
|
+
: contentBlocks.filter((block) => block.type === 'output_text');
|
|
1095
|
+
const text = matches
|
|
1096
|
+
.map((block) => String(block.text || '').trim())
|
|
1097
|
+
.find((value) => {
|
|
1098
|
+
if (!value)
|
|
1099
|
+
return false;
|
|
1100
|
+
if (role === 'user' && (value.length >= 2000 || value.includes('<permissions instructions>') || value.startsWith('# AGENTS.md instructions'))) {
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
return true;
|
|
1104
|
+
});
|
|
1105
|
+
return text || undefined;
|
|
1106
|
+
}
|
|
1107
|
+
function normalizeVersion(version) {
|
|
1108
|
+
const trimmed = version?.trim();
|
|
1109
|
+
return trimmed ? trimmed : undefined;
|
|
1110
|
+
}
|
|
1111
|
+
function extractVersionFromManagedPath(agent, sourcePath) {
|
|
1112
|
+
if (!sourcePath)
|
|
1113
|
+
return undefined;
|
|
1114
|
+
const candidates = [sourcePath, safeRealpathSync(sourcePath) || ''];
|
|
1115
|
+
const marker = `/.agents/versions/${agent}/`;
|
|
1116
|
+
for (const candidate of candidates) {
|
|
1117
|
+
if (!candidate)
|
|
1118
|
+
continue;
|
|
1119
|
+
const normalized = candidate.split(path.sep).join('/');
|
|
1120
|
+
const start = normalized.indexOf(marker);
|
|
1121
|
+
if (start === -1)
|
|
1122
|
+
continue;
|
|
1123
|
+
const version = normalized.slice(start + marker.length).split('/')[0];
|
|
1124
|
+
if (version)
|
|
1125
|
+
return version;
|
|
1126
|
+
}
|
|
1127
|
+
return undefined;
|
|
1128
|
+
}
|
|
1129
|
+
async function getCurrentAgentVersion(agent) {
|
|
1130
|
+
const cached = cachedAgentVersions.get(agent);
|
|
1131
|
+
if (cached)
|
|
1132
|
+
return cached;
|
|
1133
|
+
const promise = (async () => {
|
|
1134
|
+
const symlinkVersion = normalizeVersion(getConfigSymlinkVersion(agent));
|
|
1135
|
+
if (symlinkVersion)
|
|
1136
|
+
return symlinkVersion;
|
|
1137
|
+
return normalizeVersion(await getCliVersion(agent));
|
|
1138
|
+
})();
|
|
1139
|
+
cachedAgentVersions.set(agent, promise);
|
|
1140
|
+
return promise;
|
|
1141
|
+
}
|
|
1142
|
+
function resolveSessionVersion(agent, sourcePath, embeddedVersion, currentVersion) {
|
|
1143
|
+
return normalizeVersion(embeddedVersion)
|
|
1144
|
+
|| extractVersionFromManagedPath(agent, sourcePath)
|
|
1145
|
+
|| normalizeVersion(currentVersion);
|
|
1146
|
+
}
|
|
1147
|
+
function getCodexTokenCount(totalTokenUsage) {
|
|
1148
|
+
if (!totalTokenUsage || typeof totalTokenUsage !== 'object')
|
|
1149
|
+
return null;
|
|
1150
|
+
return sumKnownNumbers([
|
|
1151
|
+
totalTokenUsage.input_tokens,
|
|
1152
|
+
totalTokenUsage.cached_input_tokens,
|
|
1153
|
+
totalTokenUsage.output_tokens,
|
|
1154
|
+
totalTokenUsage.reasoning_output_tokens,
|
|
1155
|
+
]);
|
|
1156
|
+
}
|
|
1157
|
+
function extractGeminiMessageText(content) {
|
|
1158
|
+
if (typeof content === 'string')
|
|
1159
|
+
return content.trim();
|
|
1160
|
+
if (Array.isArray(content)) {
|
|
1161
|
+
return content
|
|
1162
|
+
.map((part) => {
|
|
1163
|
+
if (typeof part === 'string')
|
|
1164
|
+
return part;
|
|
1165
|
+
if (typeof part?.text === 'string')
|
|
1166
|
+
return part.text;
|
|
1167
|
+
return '';
|
|
1168
|
+
})
|
|
1169
|
+
.join('\n')
|
|
1170
|
+
.trim();
|
|
1171
|
+
}
|
|
1172
|
+
return '';
|
|
1173
|
+
}
|
|
1174
|
+
function getGeminiTokenCount(tokens) {
|
|
1175
|
+
if (!tokens || typeof tokens !== 'object')
|
|
1176
|
+
return null;
|
|
1177
|
+
if (typeof tokens.total === 'number')
|
|
1178
|
+
return tokens.total;
|
|
1179
|
+
return sumKnownNumbers([
|
|
1180
|
+
tokens.input,
|
|
1181
|
+
tokens.output,
|
|
1182
|
+
tokens.cached,
|
|
1183
|
+
tokens.thoughts,
|
|
1184
|
+
tokens.tool,
|
|
1185
|
+
]);
|
|
1186
|
+
}
|
|
1187
|
+
function sumKnownNumbers(values) {
|
|
1188
|
+
let total = 0;
|
|
1189
|
+
let found = false;
|
|
1190
|
+
for (const value of values) {
|
|
1191
|
+
if (typeof value !== 'number' || Number.isNaN(value))
|
|
1192
|
+
continue;
|
|
1193
|
+
total += value;
|
|
1194
|
+
found = true;
|
|
1195
|
+
}
|
|
1196
|
+
return found ? total : null;
|
|
1197
|
+
}
|
|
1198
|
+
// ---------------------------------------------------------------------------
|
|
1199
|
+
// Time range parsing
|
|
1200
|
+
// ---------------------------------------------------------------------------
|
|
1201
|
+
export function parseTimeFilter(input) {
|
|
1202
|
+
const relativeMatch = input.match(/^(\d+)([dw])$/i);
|
|
1203
|
+
if (relativeMatch) {
|
|
1204
|
+
const value = parseInt(relativeMatch[1], 10);
|
|
1205
|
+
const unit = relativeMatch[2].toLowerCase();
|
|
1206
|
+
if (unit === 'd')
|
|
1207
|
+
return Date.now() - value * 86_400_000;
|
|
1208
|
+
if (unit === 'w')
|
|
1209
|
+
return Date.now() - value * 7 * 86_400_000;
|
|
1210
|
+
}
|
|
1211
|
+
const ts = new Date(input).getTime();
|
|
1212
|
+
return Number.isNaN(ts) ? 0 : ts;
|
|
1213
|
+
}
|
|
1214
|
+
// ---------------------------------------------------------------------------
|
|
1215
|
+
// Content index search
|
|
1216
|
+
// ---------------------------------------------------------------------------
|
|
1217
|
+
/**
|
|
1218
|
+
* Score sessions by matching against terms from the content index.
|
|
1219
|
+
* Returns sessions with matched terms attached for highlighting.
|
|
1220
|
+
*/
|
|
1221
|
+
export function searchContentIndex(sessions, query) {
|
|
1222
|
+
const index = loadContentIndex();
|
|
1223
|
+
if (index.size === 0)
|
|
1224
|
+
return new Map();
|
|
1225
|
+
const terms = tokenizeText(query);
|
|
1226
|
+
if (terms.length === 0)
|
|
1227
|
+
return new Map();
|
|
1228
|
+
const scored = new Map();
|
|
1229
|
+
for (const term of terms) {
|
|
1230
|
+
const matchingSessions = index.get(term);
|
|
1231
|
+
if (!matchingSessions)
|
|
1232
|
+
continue;
|
|
1233
|
+
for (const sessionId of matchingSessions) {
|
|
1234
|
+
const entry = scored.get(sessionId);
|
|
1235
|
+
if (!entry) {
|
|
1236
|
+
scored.set(sessionId, { score: 1, matchedTerms: [term] });
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
entry.score += 1;
|
|
1240
|
+
if (!entry.matchedTerms.includes(term)) {
|
|
1241
|
+
entry.matchedTerms.push(term);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
const result = new Map();
|
|
1247
|
+
for (const [sessionId, info] of scored) {
|
|
1248
|
+
const session = sessions.find(s => s.id === sessionId);
|
|
1249
|
+
if (session) {
|
|
1250
|
+
result.set(sessionId, { ...session, _matchedTerms: info.matchedTerms });
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return result;
|
|
810
1254
|
}
|
|
811
1255
|
//# sourceMappingURL=discover.js.map
|