@swarmify/agents-cli 1.11.2 → 1.11.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/CHANGELOG.md +8 -0
- package/dist/commands/__tests__/sessions.test.js +64 -1
- package/dist/commands/__tests__/sessions.test.js.map +1 -1
- package/dist/commands/drive.js +1 -1
- package/dist/commands/drive.js.map +1 -1
- package/dist/commands/sessions.d.ts.map +1 -1
- package/dist/commands/sessions.js +11 -12
- package/dist/commands/sessions.js.map +1 -1
- package/dist/lib/daemon.js +1 -1
- package/dist/lib/daemon.js.map +1 -1
- package/dist/lib/exec.d.ts +0 -1
- package/dist/lib/exec.d.ts.map +1 -1
- package/dist/lib/pty-server.js +1 -1
- package/dist/lib/pty-server.js.map +1 -1
- package/dist/lib/session/__tests__/discover.test.d.ts +2 -0
- package/dist/lib/session/__tests__/discover.test.d.ts.map +1 -0
- package/dist/lib/session/__tests__/discover.test.js +100 -0
- package/dist/lib/session/__tests__/discover.test.js.map +1 -0
- package/dist/lib/session/discover.d.ts +17 -2
- package/dist/lib/session/discover.d.ts.map +1 -1
- package/dist/lib/session/discover.js +187 -81
- package/dist/lib/session/discover.js.map +1 -1
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/session/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildBM25Index, scoreBM25 } from '../discover.js';
|
|
3
|
+
function session(id, topic, userText) {
|
|
4
|
+
return {
|
|
5
|
+
id,
|
|
6
|
+
shortId: id.slice(0, 8),
|
|
7
|
+
agent: 'claude',
|
|
8
|
+
timestamp: '2026-04-17T19:00:00.000Z',
|
|
9
|
+
filePath: `/tmp/${id}.jsonl`,
|
|
10
|
+
topic,
|
|
11
|
+
_userTerms: userText ? [userText] : undefined,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
describe('buildBM25Index', () => {
|
|
15
|
+
it('captures term frequencies and per-doc lengths', () => {
|
|
16
|
+
const docs = [
|
|
17
|
+
session('doc-a', 'auth middleware bug', 'fix the auth auth middleware'),
|
|
18
|
+
session('doc-b', 'payment refund', 'handle refund edge case'),
|
|
19
|
+
];
|
|
20
|
+
const index = buildBM25Index(docs);
|
|
21
|
+
expect(index.N).toBe(2);
|
|
22
|
+
// doc-a: "auth middleware bug fix the auth auth middleware" -> 8 tokens
|
|
23
|
+
expect(index.docLengths.get('doc-a')).toBe(8);
|
|
24
|
+
// doc-b: "payment refund handle refund edge case" -> 6 tokens
|
|
25
|
+
expect(index.docLengths.get('doc-b')).toBe(6);
|
|
26
|
+
expect(index.avgdl).toBe(7);
|
|
27
|
+
// "auth" appears 3 times in doc-a, 0 in doc-b
|
|
28
|
+
expect(index.postings.get('auth')?.get('doc-a')).toBe(3);
|
|
29
|
+
expect(index.postings.get('auth')?.has('doc-b')).toBe(false);
|
|
30
|
+
// "refund" appears twice in doc-b
|
|
31
|
+
expect(index.postings.get('refund')?.get('doc-b')).toBe(2);
|
|
32
|
+
});
|
|
33
|
+
it('skips tokens shorter than 2 characters', () => {
|
|
34
|
+
const docs = [session('short', 'a b ab')];
|
|
35
|
+
const index = buildBM25Index(docs);
|
|
36
|
+
expect(index.postings.has('a')).toBe(false);
|
|
37
|
+
expect(index.postings.has('b')).toBe(false);
|
|
38
|
+
expect(index.postings.get('ab')?.get('short')).toBe(1);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('scoreBM25', () => {
|
|
42
|
+
it('ranks a rare term higher than a common one via IDF', () => {
|
|
43
|
+
// "bug" in 1 doc, "session" in all 4 — rare term should dominate
|
|
44
|
+
const docs = [
|
|
45
|
+
session('doc-0', 'session bug', 'the bug'),
|
|
46
|
+
session('doc-1', 'session notes', 'session notes'),
|
|
47
|
+
session('doc-2', 'session thoughts', 'session thoughts'),
|
|
48
|
+
session('doc-3', 'session plan', 'session plan'),
|
|
49
|
+
];
|
|
50
|
+
const index = buildBM25Index(docs);
|
|
51
|
+
const rareHit = scoreBM25(index, 'bug');
|
|
52
|
+
const commonHit = scoreBM25(index, 'session');
|
|
53
|
+
const bugScore = rareHit.get('doc-0').score;
|
|
54
|
+
const sessionScore = commonHit.get('doc-0').score;
|
|
55
|
+
expect(bugScore).toBeGreaterThan(sessionScore);
|
|
56
|
+
});
|
|
57
|
+
it('ranks shorter docs above longer docs at equal term frequency', () => {
|
|
58
|
+
// Both docs mention "widget" once, but one is much longer.
|
|
59
|
+
const docs = [
|
|
60
|
+
session('short-doc', 'widget', 'widget'),
|
|
61
|
+
session('long-doc', 'widget', 'widget ' + 'padding '.repeat(50).trim()),
|
|
62
|
+
];
|
|
63
|
+
const index = buildBM25Index(docs);
|
|
64
|
+
const results = scoreBM25(index, 'widget');
|
|
65
|
+
const ids = [...results.keys()];
|
|
66
|
+
expect(ids[0]).toBe('short-doc');
|
|
67
|
+
expect(ids[1]).toBe('long-doc');
|
|
68
|
+
});
|
|
69
|
+
it('accumulates scores across multiple query terms', () => {
|
|
70
|
+
const docs = [
|
|
71
|
+
session('both', 'auth token', 'auth token handling'),
|
|
72
|
+
session('one', 'auth only', 'auth only'),
|
|
73
|
+
session('none', 'unrelated', 'unrelated notes'),
|
|
74
|
+
];
|
|
75
|
+
const index = buildBM25Index(docs);
|
|
76
|
+
const results = scoreBM25(index, 'auth token');
|
|
77
|
+
expect(results.get('both').matchedTerms.sort()).toEqual(['auth', 'token']);
|
|
78
|
+
expect(results.get('one').matchedTerms).toEqual(['auth']);
|
|
79
|
+
expect(results.has('none')).toBe(false);
|
|
80
|
+
expect(results.get('both').score).toBeGreaterThan(results.get('one').score);
|
|
81
|
+
});
|
|
82
|
+
it('returns an empty map for queries with no indexed terms', () => {
|
|
83
|
+
const docs = [session('doc-0', 'hello world')];
|
|
84
|
+
const index = buildBM25Index(docs);
|
|
85
|
+
expect(scoreBM25(index, '').size).toBe(0);
|
|
86
|
+
expect(scoreBM25(index, 'nonexistent').size).toBe(0);
|
|
87
|
+
});
|
|
88
|
+
it('orders results by score descending (Map insertion order)', () => {
|
|
89
|
+
const docs = [
|
|
90
|
+
session('low', 'apple', 'apple'),
|
|
91
|
+
session('mid', 'apple apple', 'apple apple'),
|
|
92
|
+
session('high', 'apple apple apple', 'apple apple apple'),
|
|
93
|
+
];
|
|
94
|
+
const index = buildBM25Index(docs);
|
|
95
|
+
const results = scoreBM25(index, 'apple');
|
|
96
|
+
const ordered = [...results.keys()];
|
|
97
|
+
expect(ordered).toEqual(['high', 'mid', 'low']);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
//# sourceMappingURL=discover.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.test.js","sourceRoot":"","sources":["../../../../src/lib/session/__tests__/discover.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3D,SAAS,OAAO,CAAC,EAAU,EAAE,KAAa,EAAE,QAAiB;IAC3D,OAAO;QACL,EAAE;QACF,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;QACvB,KAAK,EAAE,QAAQ;QACf,SAAS,EAAE,0BAA0B;QACrC,QAAQ,EAAE,QAAQ,EAAE,QAAQ;QAC5B,KAAK;QACL,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS;KAC9C,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,IAAI,GAAG;YACX,OAAO,CAAC,OAAO,EAAE,qBAAqB,EAAE,8BAA8B,CAAC;YACvE,OAAO,CAAC,OAAO,EAAE,gBAAgB,EAAE,yBAAyB,CAAC;SAC9D,CAAC;QACF,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QAEnC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACxB,wEAAwE;QACxE,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC9C,8DAA8D;QAC9D,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC9C,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAE5B,8CAA8C;QAC9C,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACzD,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7D,kCAAkC;QAClC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC1C,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACnC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5C,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5C,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,iEAAiE;QACjE,MAAM,IAAI,GAAG;YACX,OAAO,CAAC,OAAO,EAAE,aAAa,EAAE,SAAS,CAAC;YAC1C,OAAO,CAAC,OAAO,EAAE,eAAe,EAAE,eAAe,CAAC;YAClD,OAAO,CAAC,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,CAAC;YACxD,OAAO,CAAC,OAAO,EAAE,cAAc,EAAE,cAAc,CAAC;SACjD,CAAC;QACF,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QAEnC,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACxC,MAAM,SAAS,GAAG,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QAE9C,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAE,CAAC,KAAK,CAAC;QAC7C,MAAM,YAAY,GAAG,SAAS,CAAC,GAAG,CAAC,OAAO,CAAE,CAAC,KAAK,CAAC;QACnD,MAAM,CAAC,QAAQ,CAAC,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;QACtE,2DAA2D;QAC3D,MAAM,IAAI,GAAG;YACX,OAAO,CAAC,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC;YACxC,OAAO,CAAC,UAAU,EAAE,QAAQ,EAAE,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;SACxE,CAAC;QACF,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAC3C,MAAM,GAAG,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;QAChC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACjC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,IAAI,GAAG;YACX,OAAO,CAAC,MAAM,EAAE,YAAY,EAAE,qBAAqB,CAAC;YACpD,OAAO,CAAC,KAAK,EAAE,WAAW,EAAE,WAAW,CAAC;YACxC,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,iBAAiB,CAAC;SAChD,CAAC;QACF,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;QAE/C,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAE,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;QAC5E,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAE,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QAC3D,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAE,CAAC,KAAK,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAE,CAAC,KAAK,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC;QAC/C,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QAEnC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,MAAM,IAAI,GAAG;YACX,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC;YAChC,OAAO,CAAC,KAAK,EAAE,aAAa,EAAE,aAAa,CAAC;YAC5C,OAAO,CAAC,MAAM,EAAE,mBAAmB,EAAE,mBAAmB,CAAC;SAC1D,CAAC;QACF,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAC1C,MAAM,OAAO,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;QACpC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -20,6 +20,13 @@ export declare function discoverSessions(options?: DiscoverOptions): Promise<Ses
|
|
|
20
20
|
* Resolve a session by full or short ID from the full index.
|
|
21
21
|
*/
|
|
22
22
|
export declare function resolveSessionById(sessions: SessionMeta[], idQuery: string): SessionMeta[];
|
|
23
|
+
export interface BM25Index {
|
|
24
|
+
N: number;
|
|
25
|
+
avgdl: number;
|
|
26
|
+
docLengths: Map<string, number>;
|
|
27
|
+
postings: Map<string, Map<string, number>>;
|
|
28
|
+
}
|
|
29
|
+
export declare function buildBM25Index(sessions: SessionMeta[]): BM25Index;
|
|
23
30
|
/**
|
|
24
31
|
* Collect all directories to scan for an agent's sessions.
|
|
25
32
|
* Scans: active config dir, all installed version homes, and backups.
|
|
@@ -38,8 +45,16 @@ export declare function readFirstLines(filePath: string, maxLines: number): Prom
|
|
|
38
45
|
export declare function walkForFiles(dir: string, ext: string, limit: number): string[];
|
|
39
46
|
export declare function parseTimeFilter(input: string): number;
|
|
40
47
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
48
|
+
* Pure BM25 scorer over an in-memory index. Returns sessionId -> score+matched terms,
|
|
49
|
+
* sorted by score descending (Map preserves insertion order).
|
|
50
|
+
*/
|
|
51
|
+
export declare function scoreBM25(index: BM25Index, query: string): Map<string, {
|
|
52
|
+
score: number;
|
|
53
|
+
matchedTerms: string[];
|
|
54
|
+
}>;
|
|
55
|
+
/**
|
|
56
|
+
* Score sessions using Okapi BM25 against the persisted content index.
|
|
57
|
+
* Returns a Map sorted by score descending, with matched terms attached.
|
|
43
58
|
*/
|
|
44
59
|
export declare function searchContentIndex(sessions: SessionMeta[], query: string): Map<string, SessionMeta>;
|
|
45
60
|
//# sourceMappingURL=discover.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../../src/lib/session/discover.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../../src/lib/session/discover.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAc9D,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA6BD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CA0ExF;AAQD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,CAW1F;AAmDD,MAAM,WAAW,SAAS;IACxB,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAC5C;AAyBD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,SAAS,CAsBjE;AAwED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAqC3E;AA4yBD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAmBpF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA8B9E;AAsLD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUrD;AAMD;;;GAGG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,SAAS,EAChB,KAAK,EAAE,MAAM,GACZ,GAAG,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAmCxD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,WAAW,EAAE,EACvB,KAAK,EAAE,MAAM,GACZ,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAoB1B"}
|
|
@@ -4,7 +4,7 @@ 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';
|
|
7
|
+
import { AGENTS, getCliVersion } from '../agents.js';
|
|
8
8
|
import { getConfigSymlinkVersion } from '../shims.js';
|
|
9
9
|
import { SESSION_AGENTS } from './types.js';
|
|
10
10
|
import { extractSessionTopic } from './prompt.js';
|
|
@@ -13,6 +13,7 @@ const AGENTS_DIR = path.join(HOME, '.agents');
|
|
|
13
13
|
const SESSIONS_DIR = path.join(AGENTS_DIR, 'sessions');
|
|
14
14
|
const INDEX_PATH = path.join(SESSIONS_DIR, 'index.jsonl');
|
|
15
15
|
const CONTENT_INDEX_PATH = path.join(SESSIONS_DIR, 'content_index.jsonl');
|
|
16
|
+
let cachedOpenClawWorkspaces = null;
|
|
16
17
|
const cachedAgentVersions = new Map();
|
|
17
18
|
/**
|
|
18
19
|
* Discover sessions across all installed agents, versions, and backups.
|
|
@@ -48,9 +49,9 @@ export async function discoverSessions(options) {
|
|
|
48
49
|
toSave.set(s.id, s);
|
|
49
50
|
}
|
|
50
51
|
saveIndex([...toSave.values()]);
|
|
51
|
-
// Build content index for all discovered sessions
|
|
52
|
-
const
|
|
53
|
-
|
|
52
|
+
// Build BM25 content index for all discovered sessions
|
|
53
|
+
const bm25 = buildBM25Index(sessions);
|
|
54
|
+
saveBM25Index(bm25);
|
|
54
55
|
const projectQuery = options?.project?.trim();
|
|
55
56
|
// Filter by project (case-insensitive substring match)
|
|
56
57
|
if (projectQuery) {
|
|
@@ -142,78 +143,127 @@ function saveIndex(sessions) {
|
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
145
|
// ---------------------------------------------------------------------------
|
|
145
|
-
//
|
|
146
|
+
// BM25 content index
|
|
146
147
|
// ---------------------------------------------------------------------------
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
148
|
+
const BM25_K1 = 1.2;
|
|
149
|
+
const BM25_B = 0.75;
|
|
150
|
+
const BM25_INDEX_VERSION = 2;
|
|
151
|
+
function tokenizeCounted(text) {
|
|
152
|
+
const counts = new Map();
|
|
153
|
+
let length = 0;
|
|
154
|
+
const raw = text.toLowerCase().split(/[^a-z0-9]+/);
|
|
155
|
+
for (const token of raw) {
|
|
156
|
+
if (token.length < 2)
|
|
157
|
+
continue;
|
|
158
|
+
length++;
|
|
159
|
+
counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
156
160
|
}
|
|
157
|
-
return
|
|
161
|
+
return { length, counts };
|
|
158
162
|
}
|
|
159
|
-
function
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
return
|
|
163
|
+
function collectSessionText(s) {
|
|
164
|
+
const parts = [];
|
|
165
|
+
if (s.topic)
|
|
166
|
+
parts.push(s.topic);
|
|
167
|
+
if (s.project)
|
|
168
|
+
parts.push(s.project);
|
|
169
|
+
if (s.cwd)
|
|
170
|
+
parts.push(s.cwd);
|
|
171
|
+
if (s.gitBranch)
|
|
172
|
+
parts.push(s.gitBranch);
|
|
173
|
+
if (s.account)
|
|
174
|
+
parts.push(s.account);
|
|
175
|
+
if (s._userTerms)
|
|
176
|
+
parts.push(s._userTerms.join('\n'));
|
|
177
|
+
return parts.join('\n');
|
|
174
178
|
}
|
|
175
|
-
function
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
for (const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
179
|
+
export function buildBM25Index(sessions) {
|
|
180
|
+
const docLengths = new Map();
|
|
181
|
+
const postings = new Map();
|
|
182
|
+
let totalLength = 0;
|
|
183
|
+
for (const session of sessions) {
|
|
184
|
+
const { length, counts } = tokenizeCounted(collectSessionText(session));
|
|
185
|
+
docLengths.set(session.id, length);
|
|
186
|
+
totalLength += length;
|
|
187
|
+
for (const [term, tf] of counts) {
|
|
188
|
+
let termPostings = postings.get(term);
|
|
189
|
+
if (!termPostings) {
|
|
190
|
+
termPostings = new Map();
|
|
191
|
+
postings.set(term, termPostings);
|
|
192
|
+
}
|
|
193
|
+
termPostings.set(session.id, tf);
|
|
194
|
+
}
|
|
184
195
|
}
|
|
185
|
-
|
|
196
|
+
const N = sessions.length;
|
|
197
|
+
const avgdl = N > 0 ? totalLength / N : 0;
|
|
198
|
+
return { N, avgdl, docLengths, postings };
|
|
186
199
|
}
|
|
187
|
-
function
|
|
200
|
+
function saveBM25Index(index) {
|
|
188
201
|
try {
|
|
189
202
|
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
190
203
|
const lines = [];
|
|
191
|
-
|
|
192
|
-
|
|
204
|
+
lines.push(JSON.stringify({ v: BM25_INDEX_VERSION, N: index.N, avgdl: index.avgdl }));
|
|
205
|
+
for (const [sid, len] of index.docLengths) {
|
|
206
|
+
lines.push(JSON.stringify({ d: sid, l: len }));
|
|
207
|
+
}
|
|
208
|
+
for (const [term, termPostings] of index.postings) {
|
|
209
|
+
const p = [];
|
|
210
|
+
for (const [sid, tf] of termPostings)
|
|
211
|
+
p.push([sid, tf]);
|
|
212
|
+
lines.push(JSON.stringify({ t: term, p }));
|
|
193
213
|
}
|
|
194
214
|
fs.writeFileSync(CONTENT_INDEX_PATH, lines.join('\n') + '\n', 'utf-8');
|
|
195
215
|
}
|
|
196
|
-
catch { /*
|
|
216
|
+
catch { /* non-fatal */ }
|
|
197
217
|
}
|
|
198
|
-
function
|
|
199
|
-
const index = new Map();
|
|
218
|
+
function loadBM25Index() {
|
|
200
219
|
if (!fs.existsSync(CONTENT_INDEX_PATH))
|
|
201
|
-
return
|
|
220
|
+
return null;
|
|
221
|
+
let content;
|
|
202
222
|
try {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
223
|
+
content = fs.readFileSync(CONTENT_INDEX_PATH, 'utf-8');
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
229
|
+
if (lines.length === 0)
|
|
230
|
+
return null;
|
|
231
|
+
let header;
|
|
232
|
+
try {
|
|
233
|
+
header = JSON.parse(lines[0]);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
if (header?.v !== BM25_INDEX_VERSION)
|
|
239
|
+
return null;
|
|
240
|
+
const index = {
|
|
241
|
+
N: Number(header.N) || 0,
|
|
242
|
+
avgdl: Number(header.avgdl) || 0,
|
|
243
|
+
docLengths: new Map(),
|
|
244
|
+
postings: new Map(),
|
|
245
|
+
};
|
|
246
|
+
for (let i = 1; i < lines.length; i++) {
|
|
247
|
+
let entry;
|
|
248
|
+
try {
|
|
249
|
+
entry = JSON.parse(lines[i]);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (typeof entry.d === 'string' && typeof entry.l === 'number') {
|
|
255
|
+
index.docLengths.set(entry.d, entry.l);
|
|
256
|
+
}
|
|
257
|
+
else if (typeof entry.t === 'string' && Array.isArray(entry.p)) {
|
|
258
|
+
const termPostings = new Map();
|
|
259
|
+
for (const pair of entry.p) {
|
|
260
|
+
if (Array.isArray(pair) && pair.length >= 2) {
|
|
261
|
+
termPostings.set(String(pair[0]), Number(pair[1]));
|
|
211
262
|
}
|
|
212
263
|
}
|
|
213
|
-
|
|
264
|
+
index.postings.set(entry.t, termPostings);
|
|
214
265
|
}
|
|
215
266
|
}
|
|
216
|
-
catch { /* index load failure is non-fatal */ }
|
|
217
267
|
return index;
|
|
218
268
|
}
|
|
219
269
|
// ---------------------------------------------------------------------------
|
|
@@ -781,6 +831,7 @@ async function discoverOpenClawSessions() {
|
|
|
781
831
|
agent: 'openclaw',
|
|
782
832
|
timestamp: new Date().toISOString(),
|
|
783
833
|
project: name,
|
|
834
|
+
cwd: getOpenClawSessionCwd(agentId),
|
|
784
835
|
version: currentVersion,
|
|
785
836
|
filePath: '',
|
|
786
837
|
});
|
|
@@ -815,7 +866,6 @@ async function discoverOpenClawSessions() {
|
|
|
815
866
|
// [schedule+next, last, status, target, agentId, model]
|
|
816
867
|
const rest = line.slice(headMatch[0].length).trim();
|
|
817
868
|
const cols = rest.split(/\s{2,}/);
|
|
818
|
-
const status = cols[2] || '';
|
|
819
869
|
const agentId = cols[4] || '';
|
|
820
870
|
sessions.push({
|
|
821
871
|
id: `openclaw-cron-${jobId}`,
|
|
@@ -823,7 +873,7 @@ async function discoverOpenClawSessions() {
|
|
|
823
873
|
agent: 'openclaw',
|
|
824
874
|
timestamp: new Date().toISOString(),
|
|
825
875
|
project: `${jobName} (${agentId || 'unknown'})`,
|
|
826
|
-
cwd:
|
|
876
|
+
cwd: getOpenClawSessionCwd(agentId),
|
|
827
877
|
version: currentVersion,
|
|
828
878
|
filePath: '',
|
|
829
879
|
});
|
|
@@ -978,6 +1028,36 @@ async function scanCodexSession(filePath) {
|
|
|
978
1028
|
userTerms: userTexts.length > 0 ? userTexts : undefined,
|
|
979
1029
|
};
|
|
980
1030
|
}
|
|
1031
|
+
function getOpenClawSessionCwd(agentId) {
|
|
1032
|
+
const workspace = agentId ? getOpenClawWorkspaceMap().get(agentId) : undefined;
|
|
1033
|
+
if (workspace)
|
|
1034
|
+
return workspace;
|
|
1035
|
+
const configDir = AGENTS.openclaw.configDir;
|
|
1036
|
+
return safeRealpathSync(configDir) || configDir;
|
|
1037
|
+
}
|
|
1038
|
+
function getOpenClawWorkspaceMap() {
|
|
1039
|
+
if (cachedOpenClawWorkspaces)
|
|
1040
|
+
return cachedOpenClawWorkspaces;
|
|
1041
|
+
const workspaces = new Map();
|
|
1042
|
+
const configPath = path.join(AGENTS.openclaw.configDir, 'openclaw.json');
|
|
1043
|
+
if (!fs.existsSync(configPath)) {
|
|
1044
|
+
cachedOpenClawWorkspaces = workspaces;
|
|
1045
|
+
return workspaces;
|
|
1046
|
+
}
|
|
1047
|
+
try {
|
|
1048
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1049
|
+
for (const agent of config.agents?.list || []) {
|
|
1050
|
+
if (!agent.id || !agent.workspace)
|
|
1051
|
+
continue;
|
|
1052
|
+
workspaces.set(agent.id, safeRealpathSync(agent.workspace) || agent.workspace);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
catch {
|
|
1056
|
+
// Ignore invalid OpenClaw config and fall back to ~/.openclaw.
|
|
1057
|
+
}
|
|
1058
|
+
cachedOpenClawWorkspaces = workspaces;
|
|
1059
|
+
return workspaces;
|
|
1060
|
+
}
|
|
981
1061
|
// ---------------------------------------------------------------------------
|
|
982
1062
|
// Utilities
|
|
983
1063
|
// ---------------------------------------------------------------------------
|
|
@@ -1204,50 +1284,76 @@ export function parseTimeFilter(input) {
|
|
|
1204
1284
|
const value = parseInt(relativeMatch[1], 10);
|
|
1205
1285
|
const unit = relativeMatch[2].toLowerCase();
|
|
1206
1286
|
if (unit === 'd')
|
|
1207
|
-
return Date.now() - value *
|
|
1287
|
+
return Date.now() - value * 86_400_000;
|
|
1208
1288
|
if (unit === 'w')
|
|
1209
|
-
return Date.now() - value * 7 *
|
|
1289
|
+
return Date.now() - value * 7 * 86_400_000;
|
|
1210
1290
|
}
|
|
1211
1291
|
const ts = new Date(input).getTime();
|
|
1212
1292
|
return Number.isNaN(ts) ? 0 : ts;
|
|
1213
1293
|
}
|
|
1214
1294
|
// ---------------------------------------------------------------------------
|
|
1215
|
-
//
|
|
1295
|
+
// BM25 content index search
|
|
1216
1296
|
// ---------------------------------------------------------------------------
|
|
1217
1297
|
/**
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1298
|
+
* Pure BM25 scorer over an in-memory index. Returns sessionId -> score+matched terms,
|
|
1299
|
+
* sorted by score descending (Map preserves insertion order).
|
|
1220
1300
|
*/
|
|
1221
|
-
export function
|
|
1222
|
-
const
|
|
1223
|
-
if (index.
|
|
1224
|
-
return
|
|
1225
|
-
const
|
|
1226
|
-
if (
|
|
1227
|
-
return
|
|
1301
|
+
export function scoreBM25(index, query) {
|
|
1302
|
+
const result = new Map();
|
|
1303
|
+
if (index.N === 0)
|
|
1304
|
+
return result;
|
|
1305
|
+
const { counts: queryTerms } = tokenizeCounted(query);
|
|
1306
|
+
if (queryTerms.size === 0)
|
|
1307
|
+
return result;
|
|
1308
|
+
const avgdl = index.avgdl || 1;
|
|
1228
1309
|
const scored = new Map();
|
|
1229
|
-
for (const term of
|
|
1230
|
-
const
|
|
1231
|
-
if (!
|
|
1310
|
+
for (const [term] of queryTerms) {
|
|
1311
|
+
const termPostings = index.postings.get(term);
|
|
1312
|
+
if (!termPostings)
|
|
1232
1313
|
continue;
|
|
1233
|
-
|
|
1314
|
+
const df = termPostings.size;
|
|
1315
|
+
const idf = Math.log(1 + (index.N - df + 0.5) / (df + 0.5));
|
|
1316
|
+
for (const [sessionId, tf] of termPostings) {
|
|
1317
|
+
const dl = index.docLengths.get(sessionId) ?? avgdl;
|
|
1318
|
+
const norm = 1 - BM25_B + BM25_B * (dl / avgdl);
|
|
1319
|
+
const termScore = idf * (tf * (BM25_K1 + 1)) / (tf + BM25_K1 * norm);
|
|
1234
1320
|
const entry = scored.get(sessionId);
|
|
1235
1321
|
if (!entry) {
|
|
1236
|
-
scored.set(sessionId, { score:
|
|
1322
|
+
scored.set(sessionId, { score: termScore, matchedTerms: [term] });
|
|
1237
1323
|
}
|
|
1238
1324
|
else {
|
|
1239
|
-
entry.score +=
|
|
1240
|
-
if (!entry.matchedTerms.includes(term))
|
|
1325
|
+
entry.score += termScore;
|
|
1326
|
+
if (!entry.matchedTerms.includes(term))
|
|
1241
1327
|
entry.matchedTerms.push(term);
|
|
1242
|
-
}
|
|
1243
1328
|
}
|
|
1244
1329
|
}
|
|
1245
1330
|
}
|
|
1331
|
+
const sorted = [...scored.entries()].sort((a, b) => b[1].score - a[1].score);
|
|
1332
|
+
for (const [sid, info] of sorted)
|
|
1333
|
+
result.set(sid, info);
|
|
1334
|
+
return result;
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Score sessions using Okapi BM25 against the persisted content index.
|
|
1338
|
+
* Returns a Map sorted by score descending, with matched terms attached.
|
|
1339
|
+
*/
|
|
1340
|
+
export function searchContentIndex(sessions, query) {
|
|
1341
|
+
const index = loadBM25Index();
|
|
1342
|
+
if (!index)
|
|
1343
|
+
return new Map();
|
|
1344
|
+
const scored = scoreBM25(index, query);
|
|
1345
|
+
if (scored.size === 0)
|
|
1346
|
+
return new Map();
|
|
1347
|
+
const sessionsById = new Map(sessions.map(s => [s.id, s]));
|
|
1246
1348
|
const result = new Map();
|
|
1247
1349
|
for (const [sessionId, info] of scored) {
|
|
1248
|
-
const session =
|
|
1350
|
+
const session = sessionsById.get(sessionId);
|
|
1249
1351
|
if (session) {
|
|
1250
|
-
result.set(sessionId, {
|
|
1352
|
+
result.set(sessionId, {
|
|
1353
|
+
...session,
|
|
1354
|
+
_matchedTerms: info.matchedTerms,
|
|
1355
|
+
_bm25Score: info.score,
|
|
1356
|
+
});
|
|
1251
1357
|
}
|
|
1252
1358
|
}
|
|
1253
1359
|
return result;
|