memonaut 0.0.0 → 0.2.0

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.
Files changed (81) hide show
  1. package/LICENSE +661 -0
  2. package/dist/cli-main.d.ts +14 -0
  3. package/dist/cli-main.d.ts.map +1 -0
  4. package/dist/cli-main.js +551 -0
  5. package/dist/cli-main.js.map +1 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +16 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/config.d.ts +38 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +86 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/db.d.ts +37 -0
  15. package/dist/db.d.ts.map +1 -0
  16. package/dist/db.js +184 -0
  17. package/dist/db.js.map +1 -0
  18. package/dist/format.d.ts +22 -0
  19. package/dist/format.d.ts.map +1 -0
  20. package/dist/format.js +143 -0
  21. package/dist/format.js.map +1 -0
  22. package/dist/glob.d.ts +17 -0
  23. package/dist/glob.d.ts.map +1 -0
  24. package/dist/glob.js +77 -0
  25. package/dist/glob.js.map +1 -0
  26. package/dist/index.d.ts +13 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +15 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/indexer.d.ts +50 -0
  31. package/dist/indexer.d.ts.map +1 -0
  32. package/dist/indexer.js +404 -0
  33. package/dist/indexer.js.map +1 -0
  34. package/dist/lineage.d.ts +31 -0
  35. package/dist/lineage.d.ts.map +1 -0
  36. package/dist/lineage.js +94 -0
  37. package/dist/lineage.js.map +1 -0
  38. package/dist/model.d.ts +121 -0
  39. package/dist/model.d.ts.map +1 -0
  40. package/dist/model.js +41 -0
  41. package/dist/model.js.map +1 -0
  42. package/dist/pi-source.d.ts +47 -0
  43. package/dist/pi-source.d.ts.map +1 -0
  44. package/dist/pi-source.js +309 -0
  45. package/dist/pi-source.js.map +1 -0
  46. package/dist/quiet.d.ts +7 -0
  47. package/dist/quiet.d.ts.map +1 -0
  48. package/dist/quiet.js +19 -0
  49. package/dist/quiet.js.map +1 -0
  50. package/dist/regex.d.ts +75 -0
  51. package/dist/regex.d.ts.map +1 -0
  52. package/dist/regex.js +242 -0
  53. package/dist/regex.js.map +1 -0
  54. package/dist/ripgrep.d.ts +52 -0
  55. package/dist/ripgrep.d.ts.map +1 -0
  56. package/dist/ripgrep.js +217 -0
  57. package/dist/ripgrep.js.map +1 -0
  58. package/dist/search.d.ts +114 -0
  59. package/dist/search.d.ts.map +1 -0
  60. package/dist/search.js +309 -0
  61. package/dist/search.js.map +1 -0
  62. package/dist/silence-sqlite-warning.d.ts +2 -0
  63. package/dist/silence-sqlite-warning.d.ts.map +1 -0
  64. package/dist/silence-sqlite-warning.js +9 -0
  65. package/dist/silence-sqlite-warning.js.map +1 -0
  66. package/package.json +57 -2
  67. package/src/cli-main.ts +618 -0
  68. package/src/cli.ts +17 -0
  69. package/src/config.ts +130 -0
  70. package/src/db.ts +213 -0
  71. package/src/format.ts +185 -0
  72. package/src/glob.ts +79 -0
  73. package/src/index.ts +19 -0
  74. package/src/indexer.ts +534 -0
  75. package/src/lineage.ts +111 -0
  76. package/src/model.ts +157 -0
  77. package/src/pi-source.ts +328 -0
  78. package/src/quiet.ts +22 -0
  79. package/src/regex.ts +378 -0
  80. package/src/ripgrep.ts +263 -0
  81. package/src/search.ts +504 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regex.d.ts","sourceRoot":"","sources":["../src/regex.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAChC,OAAO,KAAK,EAAC,SAAS,EAAE,SAAS,EAAC,MAAM,YAAY,CAAC;AAKrD;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,UAAU;IAC1B,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gDAAgD;IAChD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,qEAAqE;IACrE,aAAa,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,cAAc,EAAE,MAAM,CAAC;IACvB,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,SAAS,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,oBAAoB,MAAM,CAAC;AACxC,eAAO,MAAM,mBAAmB,OAAO,CAAC;AA8GxC;;;GAGG;AACH,wBAAsB,WAAW,CAChC,EAAE,EAAE,EAAE,EACN,KAAK,EAAE,UAAU,GACf,OAAO,CAAC,YAAY,CAAC,CAyLvB"}
package/dist/regex.js ADDED
@@ -0,0 +1,242 @@
1
+ import { extractEntry } from './pi-source.js';
2
+ import { ripgrep } from './ripgrep.js';
3
+ import { collectThreads, loadFiles, visibleSets } from './search.js';
4
+ export const DEFAULT_MAX_PER_FILE = 200;
5
+ export const DEFAULT_MAX_MATCHES = 2000;
6
+ /** Context kept either side of the match in a snippet, in characters. */
7
+ const SNIPPET_RADIUS = 70;
8
+ /** Everything the extractor can see, so the snippet is not limited by the tier. */
9
+ const FULL_EXTRACT = {
10
+ tier: 'full',
11
+ toolResultHeadBytes: Number.MAX_SAFE_INTEGER,
12
+ toolArgsHeadBytes: Number.MAX_SAFE_INTEGER,
13
+ };
14
+ function windowAround(text, start, length, radius = SNIPPET_RADIUS) {
15
+ const from = Math.max(0, start - radius);
16
+ const to = Math.min(text.length, start + length + radius);
17
+ // \u0001/\u0002 are the same markers FTS5 `snippet()` emits, so the renderer
18
+ // paints a regex hit exactly like a normal one.
19
+ return ((from > 0 ? '…' : '') +
20
+ text.slice(from, start) +
21
+ '\u0001' +
22
+ text.slice(start, start + length) +
23
+ '\u0002' +
24
+ text.slice(start + length, to) +
25
+ (to < text.length ? '…' : ''));
26
+ }
27
+ function indexOfNeedle(haystack, needle, ignoreCase) {
28
+ if (!needle)
29
+ return -1;
30
+ if (!ignoreCase)
31
+ return haystack.indexOf(needle);
32
+ return haystack.toLowerCase().indexOf(needle.toLowerCase());
33
+ }
34
+ /**
35
+ * Turn a matched raw line into something a human can read, and say where the
36
+ * matched bytes actually live.
37
+ *
38
+ * The needle is rg's own `submatches[0]`, which is a LITERAL whatever the
39
+ * pattern was, so nothing here has to re-implement Rust regex semantics in
40
+ * JavaScript.
41
+ *
42
+ * Three outcomes, and keeping them apart is the honesty of the whole feature:
43
+ * the needle is in extracted text that is also indexed (`index`); it is in
44
+ * extracted text that is NOT indexed, because of the tier or because the chunk
45
+ * was truncated before it (`transcript`); or it is in none of the extracted
46
+ * text at all, so it matched a JSON key, an id or an escape sequence
47
+ * (`structure`). Only the middle one means "the index could never have found
48
+ * this". Labelling a match on `"timestamp":` that way would be a lie about an
49
+ * entry whose text is indexed perfectly well.
50
+ */
51
+ function describeMatch(raw, match, ignoreCase, indexedTexts) {
52
+ const extracted = extractEntry(raw, 0, 0, FULL_EXTRACT)?.chunks ?? [];
53
+ for (const chunk of extracted) {
54
+ const at = indexOfNeedle(chunk.text, match.needle, ignoreCase);
55
+ if (at < 0)
56
+ continue;
57
+ const indexed = indexedTexts.some((text) => indexOfNeedle(text, match.needle, ignoreCase) >= 0);
58
+ return {
59
+ kind: chunk.kind,
60
+ snippet: windowAround(chunk.text, at, match.needle.length),
61
+ matchedIn: indexed ? 'index' : 'transcript',
62
+ };
63
+ }
64
+ // Nothing anyone wrote: no kind fits, so none is invented.
65
+ const at = indexOfNeedle(match.line, match.needle, ignoreCase);
66
+ return {
67
+ snippet: at >= 0
68
+ ? windowAround(match.line.trimEnd(), at, match.needle.length)
69
+ : match.line.slice(0, SNIPPET_RADIUS * 2),
70
+ matchedIn: 'structure',
71
+ };
72
+ }
73
+ /**
74
+ * Search the transcripts themselves, and report the result exactly as the
75
+ * index-backed search would.
76
+ */
77
+ export async function regexSearch(db, query) {
78
+ const limit = query.limit ?? 20;
79
+ const threadLimit = query.threadLimit ?? 3;
80
+ // A cap that is NaN or zero would mean "collect every match in the corpus",
81
+ // which is exactly the unbounded read this is here to prevent.
82
+ const maxMatches = query.maxMatches !== undefined &&
83
+ Number.isFinite(query.maxMatches) &&
84
+ query.maxMatches > 0
85
+ ? query.maxMatches
86
+ : DEFAULT_MAX_MATCHES;
87
+ const files = loadFiles(db);
88
+ const visible = visibleSets(files, query);
89
+ // The earliest moment a lineage could hold: a fork's own `started` is later
90
+ // than the entries it inherited, so an `until` bound must be judged against
91
+ // the whole family or a fork whose parent is gone from disk would be skipped.
92
+ const lineageStart = new Map();
93
+ for (const file of files.values()) {
94
+ if (file.lineage_id === null || !file.started)
95
+ continue;
96
+ const id = Number(file.lineage_id);
97
+ const known = lineageStart.get(id);
98
+ if (!known || file.started < known)
99
+ lineageStart.set(id, file.started);
100
+ }
101
+ // Path to lineage: the lineage is all a matched line needs, because entry
102
+ // identity is (lineage_id, entry_key) and a fork copy resolves to the entry
103
+ // its ancestor owns.
104
+ const byPath = new Map();
105
+ const paths = [];
106
+ for (const file of files.values()) {
107
+ const fileId = Number(file.id);
108
+ if (!visible.files.has(fileId))
109
+ continue;
110
+ if (file.lineage_id === null)
111
+ continue;
112
+ const lineageId = Number(file.lineage_id);
113
+ // Time prefilters, kept conservative: `last_activity` is the newest entry
114
+ // in this file INCLUDING inherited ones, so it is a sound `since` bound.
115
+ if (query.since && file.last_activity && file.last_activity < query.since)
116
+ continue;
117
+ const start = lineageStart.get(lineageId);
118
+ if (query.until && start && start > query.until)
119
+ continue;
120
+ byPath.set(file.path, lineageId);
121
+ paths.push(file.path);
122
+ }
123
+ const outcome = {
124
+ hits: [],
125
+ filesSearched: paths.length,
126
+ rawMatches: 0,
127
+ entriesMatched: 0,
128
+ scanned: 0,
129
+ truncated: false,
130
+ unresolved: 0,
131
+ };
132
+ if (paths.length === 0)
133
+ return outcome;
134
+ const entryStmt = db.prepare(`SELECT id, entry_key, lineage_id, role, tool, ts
135
+ FROM entry WHERE lineage_id = ? AND entry_key = ?`);
136
+ const chunkStmt = db.prepare('SELECT text FROM chunk WHERE entry_id = ?');
137
+ const roleFilter = query.role?.length ? new Set(query.role) : null;
138
+ const toolFilter = query.tool?.length ? new Set(query.tool) : null;
139
+ const kindFilter = query.kind?.length ? new Set(query.kind) : null;
140
+ const seen = new Set();
141
+ const candidates = [];
142
+ await ripgrep(paths, {
143
+ pattern: query.pattern,
144
+ ignoreCase: query.ignoreCase,
145
+ fixed: query.fixed,
146
+ maxPerFile: query.maxPerFile ?? DEFAULT_MAX_PER_FILE,
147
+ binary: query.binary,
148
+ }, (match) => {
149
+ outcome.rawMatches++;
150
+ const lineage = byPath.get(match.path);
151
+ if (lineage === undefined)
152
+ return;
153
+ let raw;
154
+ try {
155
+ raw = JSON.parse(match.line);
156
+ }
157
+ catch {
158
+ outcome.unresolved++;
159
+ return;
160
+ }
161
+ const entryKey = typeof raw.id === 'string' ? raw.id : null;
162
+ if (!entryKey)
163
+ return; // the session header line carries no identity
164
+ const entry = entryStmt.get(lineage, entryKey);
165
+ if (!entry) {
166
+ outcome.unresolved++;
167
+ return;
168
+ }
169
+ // THE fork rule: a shared entry is reported by rg once per fork file
170
+ // that copied it, and must collapse to one hit here. `(lineage, key)`
171
+ // resolved to one entry row, so its id is the deduplication key.
172
+ const entryId = Number(entry.id);
173
+ if (seen.has(entryId))
174
+ return;
175
+ seen.add(entryId);
176
+ if (roleFilter && !roleFilter.has(entry.role))
177
+ return;
178
+ if (toolFilter && !(entry.tool && toolFilter.has(entry.tool)))
179
+ return;
180
+ if (query.since && (!entry.ts || entry.ts < query.since))
181
+ return;
182
+ if (query.until && (!entry.ts || entry.ts > query.until))
183
+ return;
184
+ const indexedTexts = chunkStmt.all(entryId).map((r) => r.text);
185
+ const described = describeMatch(raw, match, Boolean(query.ignoreCase), indexedTexts);
186
+ // A structural match has no kind, so a --kind filter excludes it: it is
187
+ // not text of that kind, and pretending otherwise would be a guess.
188
+ if (kindFilter && !(described.kind && kindFilter.has(described.kind)))
189
+ return;
190
+ candidates.push({ entry, ...described });
191
+ if (candidates.length >= maxMatches) {
192
+ outcome.truncated = true;
193
+ return false;
194
+ }
195
+ });
196
+ outcome.entriesMatched = candidates.length;
197
+ // No bm25 here, and inventing a score would be a lie. Recency of the matched
198
+ // entry is the ordering, newest first.
199
+ candidates.sort((a, b) => (b.entry.ts ?? '').localeCompare(a.entry.ts ?? ''));
200
+ const memberStmt = db.prepare('SELECT file_id, seq FROM membership WHERE entry_id = ?');
201
+ const groups = new Map();
202
+ const order = [];
203
+ for (const candidate of candidates) {
204
+ const lineageId = Number(candidate.entry.lineage_id);
205
+ const existing = groups.get(lineageId);
206
+ if (existing) {
207
+ // Same grouping as the FTS path: one chatty lineage cannot own the page,
208
+ // and the matches it displaced are counted rather than dropped silently.
209
+ existing.otherHits++;
210
+ continue;
211
+ }
212
+ const threads = collectThreads(Number(candidate.entry.id), memberStmt, files, visible.files);
213
+ if (threads.length === 0)
214
+ continue;
215
+ const hit = {
216
+ entryId: Number(candidate.entry.id),
217
+ entryKey: candidate.entry.entry_key,
218
+ lineageId,
219
+ role: candidate.entry.role,
220
+ tool: candidate.entry.tool,
221
+ ts: candidate.entry.ts,
222
+ kind: candidate.kind,
223
+ snippet: candidate.snippet,
224
+ // No bm25 to rank with, so no score is claimed; the array order is the
225
+ // answer, newest match first.
226
+ score: 0,
227
+ threads: threadLimit < 0 ? threads : threads.slice(0, Math.max(threadLimit, 1)),
228
+ threadTotal: threads.length,
229
+ otherHits: 0,
230
+ matchedIn: candidate.matchedIn,
231
+ };
232
+ groups.set(lineageId, hit);
233
+ order.push(lineageId);
234
+ }
235
+ outcome.scanned = order.length;
236
+ outcome.hits = order
237
+ .map((id) => groups.get(id))
238
+ .filter((h) => Boolean(h))
239
+ .slice(0, limit);
240
+ return outcome;
241
+ }
242
+ //# sourceMappingURL=regex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regex.js","sourceRoot":"","sources":["../src/regex.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Thin wrapper around `rg --json`.
3
+ *
4
+ * Regex over the raw transcripts is a genuinely different capability from the
5
+ * FTS index (see `docs/DESIGN.md`): FTS5 tokenises, so it can never do
6
+ * substring-inside-identifier or true regex, and the raw files hold text the
7
+ * index deliberately does not (tool output is 42% of the bytes). Rather than
8
+ * reimplement a scanner over 2 GB of JSONL in JavaScript, this shells out to
9
+ * the tool that already does it well, and refuses to pretend when it is absent.
10
+ */
11
+ export declare const DEFAULT_RG = "rg";
12
+ export declare function ripgrepBinary(env?: NodeJS.ProcessEnv): string;
13
+ export declare class RipgrepMissingError extends Error {
14
+ readonly binary: string;
15
+ constructor(binary: string);
16
+ }
17
+ export declare class RipgrepError extends Error {
18
+ constructor(message: string);
19
+ }
20
+ /** One matching line, as rg reported it. */
21
+ export interface RipgrepMatch {
22
+ /** Path exactly as it was handed to rg, so it can be looked up in the index. */
23
+ path: string;
24
+ /** The whole matching line, which for a transcript is one complete JSON entry. */
25
+ line: string;
26
+ /** Byte offset of the line within the file. */
27
+ offset: number;
28
+ /** The literal text the pattern matched. A literal, whatever the pattern was. */
29
+ needle: string;
30
+ }
31
+ export interface RipgrepOptions {
32
+ pattern: string;
33
+ ignoreCase?: boolean;
34
+ /** `-F`: the pattern is a literal string, not a regex. */
35
+ fixed?: boolean;
36
+ /** `-m`: per-file cap. 0 disables it. */
37
+ maxPerFile?: number;
38
+ binary?: string;
39
+ }
40
+ /**
41
+ * Run rg over `paths`, calling `onMatch` for each matching line.
42
+ *
43
+ * Output is consumed as it arrives, so at most ONE record is ever held: a
44
+ * single transcript line can be megabytes (a base64 image), and `--json` echoes
45
+ * the whole line back, so accumulating rg's output is not an option. What
46
+ * `onMatch` then does with that one line is its own business, and runs inline
47
+ * on this handler. Returning `false` stops the scan and kills rg.
48
+ */
49
+ export declare function ripgrep(paths: string[], opts: RipgrepOptions, onMatch: (match: RipgrepMatch) => boolean | void): Promise<void>;
50
+ /** True when a usable ripgrep is on PATH. For tests and for `--help` honesty. */
51
+ export declare function ripgrepAvailable(binary?: string): Promise<boolean>;
52
+ //# sourceMappingURL=ripgrep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ripgrep.d.ts","sourceRoot":"","sources":["../src/ripgrep.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,eAAO,MAAM,UAAU,OAAO,CAAC;AAE/B,wBAAgB,aAAa,CAAC,GAAG,oBAAc,GAAG,MAAM,CAEvD;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBACZ,MAAM,EAAE,MAAM;CAW1B;AAED,qBAAa,YAAa,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAI3B;AAED,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC5B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAiED;;;;;;;;GAQG;AACH,wBAAsB,OAAO,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,GAAG,IAAI,GAC9C,OAAO,CAAC,IAAI,CAAC,CAgBf;AA2FD,iFAAiF;AACjF,wBAAsB,gBAAgB,CACrC,MAAM,SAAkB,GACtB,OAAO,CAAC,OAAO,CAAC,CAalB"}
@@ -0,0 +1,217 @@
1
+ import { spawn } from 'node:child_process';
2
+ /**
3
+ * Thin wrapper around `rg --json`.
4
+ *
5
+ * Regex over the raw transcripts is a genuinely different capability from the
6
+ * FTS index (see `docs/DESIGN.md`): FTS5 tokenises, so it can never do
7
+ * substring-inside-identifier or true regex, and the raw files hold text the
8
+ * index deliberately does not (tool output is 42% of the bytes). Rather than
9
+ * reimplement a scanner over 2 GB of JSONL in JavaScript, this shells out to
10
+ * the tool that already does it well, and refuses to pretend when it is absent.
11
+ */
12
+ export const DEFAULT_RG = 'rg';
13
+ export function ripgrepBinary(env = process.env) {
14
+ return env.MEMONAUT_RG?.trim() || DEFAULT_RG;
15
+ }
16
+ export class RipgrepMissingError extends Error {
17
+ binary;
18
+ constructor(binary) {
19
+ super([
20
+ `regex search needs ripgrep, and \`${binary}\` was not found.`,
21
+ ' install it: apt install ripgrep · brew install ripgrep · cargo install ripgrep',
22
+ ' or point MEMONAUT_RG at the binary.',
23
+ ].join('\n'));
24
+ this.name = 'RipgrepMissingError';
25
+ this.binary = binary;
26
+ }
27
+ }
28
+ export class RipgrepError extends Error {
29
+ constructor(message) {
30
+ super(`ripgrep: ${message}`);
31
+ this.name = 'RipgrepError';
32
+ }
33
+ }
34
+ /**
35
+ * argv budget per invocation.
36
+ *
37
+ * `ARG_MAX` is ~2 MB on Linux and 256 KB on macOS, and a transcript path is
38
+ * ~110 bytes, so batching by both count and byte length keeps every platform
39
+ * far away from E2BIG.
40
+ */
41
+ const BATCH_FILES = 400;
42
+ const BATCH_BYTES = 96_000;
43
+ function decode(field) {
44
+ if (!field)
45
+ return '';
46
+ if (typeof field.text === 'string')
47
+ return field.text;
48
+ if (typeof field.bytes === 'string')
49
+ return Buffer.from(field.bytes, 'base64').toString('utf8');
50
+ return '';
51
+ }
52
+ function batches(paths) {
53
+ const out = [];
54
+ let current = [];
55
+ let bytes = 0;
56
+ for (const p of paths) {
57
+ if (current.length > 0 &&
58
+ (current.length >= BATCH_FILES || bytes + p.length + 1 > BATCH_BYTES)) {
59
+ out.push(current);
60
+ current = [];
61
+ bytes = 0;
62
+ }
63
+ current.push(p);
64
+ bytes += p.length + 1;
65
+ }
66
+ if (current.length)
67
+ out.push(current);
68
+ return out;
69
+ }
70
+ function baseArgs(opts) {
71
+ const args = [
72
+ '--json',
73
+ // Errors about files that vanished between the index scan and now are not
74
+ // the user's problem; a real pattern error still reaches stderr.
75
+ '--no-messages',
76
+ // The user's ripgrep config must not change what a search means here.
77
+ '--no-config',
78
+ // Explicit paths already come from the index, which is the only ignore
79
+ // list that matters. --text stops a transcript being silently skipped as
80
+ // binary; they are JSON, so there is nothing to blow up on.
81
+ '--no-ignore',
82
+ '--text',
83
+ ];
84
+ if (opts.ignoreCase)
85
+ args.push('-i');
86
+ if (opts.fixed)
87
+ args.push('-F');
88
+ // Explicitly finite: a NaN cap must not take the same branch as the
89
+ // documented "0 means all", or a mistyped flag silently removes the bound.
90
+ if (Number.isFinite(opts.maxPerFile) && opts.maxPerFile > 0)
91
+ args.push('-m', String(opts.maxPerFile));
92
+ // -e, so a pattern starting with `-` is a pattern and not a flag.
93
+ args.push('-e', opts.pattern);
94
+ return args;
95
+ }
96
+ /**
97
+ * Run rg over `paths`, calling `onMatch` for each matching line.
98
+ *
99
+ * Output is consumed as it arrives, so at most ONE record is ever held: a
100
+ * single transcript line can be megabytes (a base64 image), and `--json` echoes
101
+ * the whole line back, so accumulating rg's output is not an option. What
102
+ * `onMatch` then does with that one line is its own business, and runs inline
103
+ * on this handler. Returning `false` stops the scan and kills rg.
104
+ */
105
+ export async function ripgrep(paths, opts, onMatch) {
106
+ if (paths.length === 0)
107
+ return;
108
+ const binary = opts.binary ?? ripgrepBinary();
109
+ const args = baseArgs(opts);
110
+ let stop = false;
111
+ for (const batch of batches(paths)) {
112
+ if (stop)
113
+ return;
114
+ await runOne(binary, [...args, '--', ...batch], (match) => {
115
+ const keepGoing = onMatch(match);
116
+ if (keepGoing === false) {
117
+ stop = true;
118
+ return false;
119
+ }
120
+ return true;
121
+ });
122
+ }
123
+ }
124
+ function runOne(binary, args, onMatch) {
125
+ return new Promise((resolve, reject) => {
126
+ const child = spawn(binary, args, { stdio: ['ignore', 'pipe', 'pipe'] });
127
+ let stderr = '';
128
+ let buffer = '';
129
+ let killed = false;
130
+ let settled = false;
131
+ const finish = (err) => {
132
+ if (settled)
133
+ return;
134
+ settled = true;
135
+ if (err)
136
+ reject(err);
137
+ else
138
+ resolve();
139
+ };
140
+ child.on('error', (err) => {
141
+ const code = err.code;
142
+ if (code === 'ENOENT')
143
+ finish(new RipgrepMissingError(binary));
144
+ else
145
+ finish(err);
146
+ });
147
+ child.stderr.setEncoding('utf8');
148
+ child.stderr.on('data', (chunk) => {
149
+ if (stderr.length < 4096)
150
+ stderr += chunk;
151
+ });
152
+ child.stdout.setEncoding('utf8');
153
+ child.stdout.on('error', () => {
154
+ /* EPIPE after an early kill */
155
+ });
156
+ child.stdout.on('data', (chunk) => {
157
+ if (killed)
158
+ return;
159
+ buffer += chunk;
160
+ let nl;
161
+ while ((nl = buffer.indexOf('\n')) >= 0) {
162
+ const line = buffer.slice(0, nl);
163
+ buffer = buffer.slice(nl + 1);
164
+ if (!line)
165
+ continue;
166
+ let record;
167
+ try {
168
+ record = JSON.parse(line);
169
+ }
170
+ catch {
171
+ continue;
172
+ }
173
+ if (record.type !== 'match')
174
+ continue;
175
+ const data = record.data;
176
+ const sub = data.submatches?.[0];
177
+ const match = {
178
+ path: decode(data.path),
179
+ line: decode(data.lines),
180
+ offset: Number(data.absolute_offset ?? 0),
181
+ needle: decode(sub?.match),
182
+ };
183
+ if (!onMatch(match)) {
184
+ killed = true;
185
+ buffer = '';
186
+ child.kill();
187
+ return;
188
+ }
189
+ }
190
+ });
191
+ child.on('close', (code) => {
192
+ if (killed)
193
+ return finish();
194
+ // 0 = matches, 1 = no matches. 2 is "something went wrong", which with
195
+ // --no-messages is usually just a transcript that was deleted since the
196
+ // index last saw it; a real complaint (a bad pattern) reaches stderr.
197
+ if (code !== null && code >= 2 && stderr.trim())
198
+ return finish(new RipgrepError(stderr.trim()));
199
+ finish();
200
+ });
201
+ });
202
+ }
203
+ /** True when a usable ripgrep is on PATH. For tests and for `--help` honesty. */
204
+ export async function ripgrepAvailable(binary = ripgrepBinary()) {
205
+ try {
206
+ await new Promise((resolve, reject) => {
207
+ const child = spawn(binary, ['--version'], { stdio: 'ignore' });
208
+ child.on('error', reject);
209
+ child.on('close', (code) => code === 0 ? resolve() : reject(new Error(String(code))));
210
+ });
211
+ return true;
212
+ }
213
+ catch {
214
+ return false;
215
+ }
216
+ }
217
+ //# sourceMappingURL=ripgrep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ripgrep.js","sourceRoot":"","sources":["../src/ripgrep.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,114 @@
1
+ import type { DB } from './db.js';
2
+ import type { ChunkKind, SearchHit, ThreadRef } from './model.js';
3
+ export interface FileRecord {
4
+ id: number;
5
+ path: string;
6
+ source: string;
7
+ session_uuid: string | null;
8
+ cwd: string | null;
9
+ project: string | null;
10
+ name: string | null;
11
+ parent_id: number | null;
12
+ lineage_id: number | null;
13
+ lineage_depth: number;
14
+ orphaned: number;
15
+ private: number;
16
+ started: string | null;
17
+ last_activity: string | null;
18
+ entry_count: number;
19
+ }
20
+ export interface SearchQuery {
21
+ /** FTS5 MATCH expression (see `docs/QUERIES.md`). */
22
+ text: string;
23
+ cwd?: string[];
24
+ project?: string[];
25
+ role?: string[];
26
+ tool?: string[];
27
+ kind?: ChunkKind[];
28
+ since?: string;
29
+ until?: string;
30
+ includePrivate?: boolean;
31
+ limit?: number;
32
+ /** Threads listed per group; the total is always reported. */
33
+ threadLimit?: number;
34
+ /** Disable the recency nudge and rank purely on bm25 x kind weight. */
35
+ noRecency?: boolean;
36
+ /** Pass the query to FTS5 verbatim, never fall back to quoting. */
37
+ raw?: boolean;
38
+ }
39
+ export interface SearchOutcome {
40
+ hits: SearchHit[];
41
+ /** The MATCH expression actually used, after any quoting fallback. */
42
+ usedQuery: string;
43
+ /** Total groups found before `limit` was applied. */
44
+ scanned: number;
45
+ quotedFallback: boolean;
46
+ }
47
+ export declare function loadFiles(db: DB): Map<number, FileRecord>;
48
+ /** Quote every token, turning a hostile string into a valid FTS5 AND query. */
49
+ export declare function quoteQuery(text: string): string;
50
+ /**
51
+ * Which transcripts a query is allowed to see.
52
+ *
53
+ * Visibility is decided per THREAD, because a lineage can in principle span
54
+ * working directories; the lineage set is only a prefilter. Both the FTS and
55
+ * the regex path go through here, so `private` and the cwd/project filters
56
+ * cannot drift apart between them.
57
+ */
58
+ export declare function visibleSets(files: Map<number, FileRecord>, query: Pick<SearchQuery, 'cwd' | 'project' | 'includePrivate'>): {
59
+ files: Set<number>;
60
+ lineages: Set<number>;
61
+ totalLineages: Set<number>;
62
+ };
63
+ export declare function search(db: DB, query: SearchQuery, now?: number): SearchOutcome;
64
+ /**
65
+ * Fan one entry back out to every visible thread that carries it.
66
+ *
67
+ * Shared by the FTS and the regex path, because they must produce identical
68
+ * thread lists: an entry is stored once and `membership` is what turns it back
69
+ * into "the twelve threads that inherited this".
70
+ */
71
+ export declare function collectThreads(entryId: number, memberStmt: ReturnType<DB['prepare']>, files: Map<number, FileRecord>, visibleFiles: Set<number>): ThreadRef[];
72
+ /** Resolve a user-supplied reference to a thread: file id, uuid prefix, or path. */
73
+ export declare function resolveThread(db: DB, ref: string): FileRecord | null;
74
+ export interface ThreadEntry {
75
+ seq: number;
76
+ entryId: number;
77
+ entryKey: string;
78
+ role: string;
79
+ tool: string | null;
80
+ ts: string | null;
81
+ /** True when this entry was inherited from an ancestor rather than written here. */
82
+ shared: boolean;
83
+ /** Transcript that first recorded this entry, and where it lives verbatim. */
84
+ ownerPath: string | null;
85
+ byteOffset: number;
86
+ byteLength: number;
87
+ texts: Array<{
88
+ kind: ChunkKind;
89
+ text: string;
90
+ }>;
91
+ }
92
+ /**
93
+ * Read one thread back in order.
94
+ *
95
+ * `ownerPath` + byte range is the pointer back to the source transcript. The
96
+ * index deliberately stores tool output truncated (or not at all), so anything
97
+ * that needs full fidelity re-reads those bytes instead of trusting the cache.
98
+ */
99
+ export declare function readThread(db: DB, fileId: number, from?: number, limit?: number): ThreadEntry[];
100
+ export interface IndexStatsRow {
101
+ files: number;
102
+ lineages: number;
103
+ forks: number;
104
+ entries: number;
105
+ memberships: number;
106
+ chunks: number;
107
+ privateFiles: number;
108
+ orphans: number;
109
+ projects: number;
110
+ oldest: string | null;
111
+ newest: string | null;
112
+ }
113
+ export declare function indexStats(db: DB): IndexStatsRow;
114
+ //# sourceMappingURL=search.d.ts.map