memonaut 0.1.0 → 0.3.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.
- package/dist/cli-main.d.ts +12 -1
- package/dist/cli-main.d.ts.map +1 -1
- package/dist/cli-main.js +185 -25
- package/dist/cli-main.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +13 -0
- package/dist/format.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/model.d.ts +23 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/regex.d.ts +75 -0
- package/dist/regex.d.ts.map +1 -0
- package/dist/regex.js +242 -0
- package/dist/regex.js.map +1 -0
- package/dist/resources.d.ts +2 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +25 -0
- package/dist/resources.js.map +1 -0
- package/dist/ripgrep.d.ts +52 -0
- package/dist/ripgrep.d.ts.map +1 -0
- package/dist/ripgrep.js +217 -0
- package/dist/ripgrep.js.map +1 -0
- package/dist/search.d.ts +22 -1
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +47 -23
- package/dist/search.js.map +1 -1
- package/dist/skills.d.ts +34 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +59 -0
- package/dist/skills.js.map +1 -0
- package/package.json +4 -3
- package/skills/memonaut/SKILL.md +120 -0
- package/src/cli-main.ts +230 -30
- package/src/cli.ts +1 -1
- package/src/format.ts +12 -0
- package/src/index.ts +10 -0
- package/src/model.ts +23 -1
- package/src/regex.ts +378 -0
- package/src/resources.ts +26 -0
- package/src/ripgrep.ts +263 -0
- package/src/search.ts +68 -26
- package/src/skills.ts +86 -0
package/src/cli-main.ts
CHANGED
|
@@ -11,11 +11,24 @@ import {
|
|
|
11
11
|
renderTable,
|
|
12
12
|
relativeTime,
|
|
13
13
|
tildify,
|
|
14
|
+
type Style,
|
|
14
15
|
} from './format.js';
|
|
15
16
|
import {index} from './indexer.js';
|
|
16
|
-
import {TIERS, type ChunkKind, type Tier} from './model.js';
|
|
17
|
+
import {TIERS, type ChunkKind, type SearchHit, type Tier} from './model.js';
|
|
17
18
|
import {readRawEntry} from './pi-source.js';
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_MAX_MATCHES,
|
|
21
|
+
DEFAULT_MAX_PER_FILE,
|
|
22
|
+
regexSearch,
|
|
23
|
+
} from './regex.js';
|
|
18
24
|
import {indexStats, readThread, resolveThread, search} from './search.js';
|
|
25
|
+
import {
|
|
26
|
+
availableSkills,
|
|
27
|
+
destinationFor,
|
|
28
|
+
installSkills,
|
|
29
|
+
isInstalled,
|
|
30
|
+
type Scope,
|
|
31
|
+
} from './skills.js';
|
|
19
32
|
|
|
20
33
|
const USAGE = `memonaut — search your agent conversation transcripts
|
|
21
34
|
|
|
@@ -25,10 +38,12 @@ USAGE
|
|
|
25
38
|
COMMANDS
|
|
26
39
|
index Bring the index up to date (incremental by default)
|
|
27
40
|
search <query...> Search transcripts, grouped by fork lineage
|
|
41
|
+
search --regex <pat> Exact regex search over the original transcripts
|
|
28
42
|
show <ref> Print a thread (file id, session uuid prefix, name or path)
|
|
29
43
|
sql <query> Run a read-only SQL query against the index
|
|
30
44
|
stats Summarise what is indexed
|
|
31
45
|
config Show config paths, or write a starter config
|
|
46
|
+
skills [install] List or install the agent skill (into ~/.agents/skills)
|
|
32
47
|
|
|
33
48
|
SEARCH OPTIONS
|
|
34
49
|
--cwd <glob> Filter by working directory (repeatable, globs, ~ ok)
|
|
@@ -47,6 +62,22 @@ SEARCH OPTIONS
|
|
|
47
62
|
--json Machine-readable output
|
|
48
63
|
--no-sync Skip the incremental catch-up before querying
|
|
49
64
|
|
|
65
|
+
REGEX OPTIONS
|
|
66
|
+
--regex <pattern> Exact regex search, run by ripgrep over the ORIGINAL
|
|
67
|
+
transcripts instead of the index. It can therefore
|
|
68
|
+
match text the index does not hold at all, notably
|
|
69
|
+
tool output (42% of the bytes, excluded by default),
|
|
70
|
+
and it can match inside identifiers, which FTS5
|
|
71
|
+
cannot. There is no relevance ranking: results are
|
|
72
|
+
ordered by recency. Needs ripgrep installed.
|
|
73
|
+
Every filter above applies, and the candidate files
|
|
74
|
+
come from the index, so ignored and (unless --private)
|
|
75
|
+
private transcripts are still never read.
|
|
76
|
+
-i, --ignore-case Case-insensitive matching (put it AFTER the pattern)
|
|
77
|
+
-F, --fixed Treat the pattern as a literal string, not a regex
|
|
78
|
+
--max-per-file <n> Stop after n matches in one transcript (default ${DEFAULT_MAX_PER_FILE}, 0 = all)
|
|
79
|
+
--max-matches <n> Stop the scan after n matching entries (default ${DEFAULT_MAX_MATCHES})
|
|
80
|
+
|
|
50
81
|
INDEX OPTIONS
|
|
51
82
|
--full Rebuild from scratch
|
|
52
83
|
--tier <t> slim | default | full (overrides config for this run)
|
|
@@ -57,10 +88,15 @@ QUERY SYNTAX
|
|
|
57
88
|
match in order, OR / NOT / NEAR(a b, 5) work, and trailing * is a prefix.
|
|
58
89
|
If that fails to parse, the query is retried as quoted literal tokens.
|
|
59
90
|
|
|
91
|
+
SKILLS OPTIONS
|
|
92
|
+
--project Install into ./.agents/skills instead of ~/.agents/skills
|
|
93
|
+
|
|
60
94
|
EXAMPLES
|
|
61
95
|
recall search steering queue --project wherever --since 30d
|
|
62
96
|
recall search '"fork point" OR parentSession' --threads all
|
|
63
97
|
recall search --tool bash --kind bash 'fuser -k'
|
|
98
|
+
recall search --regex 'ERR_[A-Z_]+' --since 30d
|
|
99
|
+
recall search --regex 'handleToolCall(' --fixed --project wherever
|
|
64
100
|
recall sql "select project, count(*) n from file group by 1 order by n desc limit 10"
|
|
65
101
|
`;
|
|
66
102
|
|
|
@@ -179,10 +215,89 @@ function cmdIndex(argv: string[]): void {
|
|
|
179
215
|
);
|
|
180
216
|
}
|
|
181
217
|
|
|
182
|
-
|
|
218
|
+
/** Flags that only mean something on the regex path. */
|
|
219
|
+
const REGEX_ONLY_FLAGS = [
|
|
220
|
+
'ignore-case',
|
|
221
|
+
'fixed',
|
|
222
|
+
'max-per-file',
|
|
223
|
+
'max-matches',
|
|
224
|
+
];
|
|
225
|
+
/** Flags that only mean something to the full-text ranker. */
|
|
226
|
+
const FTS_ONLY_FLAGS = ['raw', 'no-recency'];
|
|
227
|
+
|
|
228
|
+
/** Numeric flags and the smallest value that means anything for each. */
|
|
229
|
+
const NUMERIC_FLAGS: Array<[string, number]> = [
|
|
230
|
+
['max-per-file', 0], // 0 is documented as "no per-file cap"
|
|
231
|
+
['max-matches', 1],
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Everything about a search that can be judged from the arguments alone.
|
|
236
|
+
*
|
|
237
|
+
* Pure, and exported, so the rules are testable without a process: the whole
|
|
238
|
+
* point of them is to refuse silently-wrong input, and "silently" is exactly
|
|
239
|
+
* what a hand-check misses. `Number('nonsense')` is NaN, and NaN quietly fails
|
|
240
|
+
* every `> 0` guard downstream, so a mistyped cap would REMOVE the bound it was
|
|
241
|
+
* meant to tighten. Those caps are the only thing between a pathological
|
|
242
|
+
* pattern and an unbounded read.
|
|
243
|
+
*/
|
|
244
|
+
export function searchFlagError(
|
|
245
|
+
given: Record<string, unknown>,
|
|
246
|
+
text: string,
|
|
247
|
+
pattern: string | undefined,
|
|
248
|
+
): string | null {
|
|
249
|
+
if (pattern === undefined) {
|
|
250
|
+
if (!text) return 'nothing to search for. Try `recall search <words>`';
|
|
251
|
+
const stray = REGEX_ONLY_FLAGS.find((f) => given[f] !== undefined);
|
|
252
|
+
if (stray) return `--${stray} only applies to --regex`;
|
|
253
|
+
} else {
|
|
254
|
+
if (text)
|
|
255
|
+
return `--regex takes the pattern itself, so "${text}" has nowhere to go. Try \`recall search --regex '${pattern}'\``;
|
|
256
|
+
if (!pattern.trim()) return '--regex needs a pattern';
|
|
257
|
+
const stray = FTS_ONLY_FLAGS.find((f) => given[f] === true);
|
|
258
|
+
if (stray)
|
|
259
|
+
return `--${stray} applies to full-text ranking, and a regex search has no bm25 to rank with`;
|
|
260
|
+
}
|
|
261
|
+
for (const [flag, min] of NUMERIC_FLAGS) {
|
|
262
|
+
const raw = given[flag];
|
|
263
|
+
if (raw === undefined) continue;
|
|
264
|
+
const n = Number(raw);
|
|
265
|
+
if (!Number.isInteger(n) || n < min)
|
|
266
|
+
return `--${flag} needs a whole number ${min} or more, not "${String(raw)}"`;
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** One rendered block per hit, or a plain reason there are none. */
|
|
272
|
+
function writeHits(
|
|
273
|
+
hits: SearchHit[],
|
|
274
|
+
opts: {style: Style; showPath: boolean},
|
|
275
|
+
nothing: string,
|
|
276
|
+
): void {
|
|
277
|
+
if (hits.length === 0) {
|
|
278
|
+
process.stdout.write(nothing + '\n');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
process.stdout.write(
|
|
282
|
+
hits
|
|
283
|
+
.map((hit) =>
|
|
284
|
+
renderHit(hit, {style: opts.style, showPath: opts.showPath}),
|
|
285
|
+
)
|
|
286
|
+
.join('\n\n') + '\n',
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function cmdSearch(argv: string[]): Promise<void> {
|
|
183
291
|
const {values, positionals} = parseArgs({
|
|
184
292
|
args: argv,
|
|
185
293
|
options: {
|
|
294
|
+
// No defaults on the mode-specific flags: `undefined` is what makes
|
|
295
|
+
// "you passed this to the wrong search" detectable.
|
|
296
|
+
regex: {type: 'string'},
|
|
297
|
+
'ignore-case': {type: 'boolean', short: 'i'},
|
|
298
|
+
fixed: {type: 'boolean', short: 'F'},
|
|
299
|
+
'max-per-file': {type: 'string'},
|
|
300
|
+
'max-matches': {type: 'string'},
|
|
186
301
|
cwd: {type: 'string', multiple: true},
|
|
187
302
|
project: {type: 'string', multiple: true},
|
|
188
303
|
role: {type: 'string', multiple: true},
|
|
@@ -202,15 +317,28 @@ function cmdSearch(argv: string[]): void {
|
|
|
202
317
|
allowPositionals: true,
|
|
203
318
|
});
|
|
204
319
|
const text = positionals.join(' ').trim();
|
|
205
|
-
|
|
320
|
+
const pattern = values.regex as string | undefined;
|
|
321
|
+
|
|
322
|
+
// Everything that can be judged from the arguments alone is judged here,
|
|
323
|
+
// before a config is loaded, an index is synced or a file is opened.
|
|
324
|
+
const given = values as Record<string, unknown>;
|
|
325
|
+
const problem = searchFlagError(given, text, pattern);
|
|
326
|
+
if (problem) fail(problem);
|
|
327
|
+
const maxPerFile =
|
|
328
|
+
given['max-per-file'] === undefined
|
|
329
|
+
? DEFAULT_MAX_PER_FILE
|
|
330
|
+
: Number(given['max-per-file']);
|
|
331
|
+
const maxMatches =
|
|
332
|
+
given['max-matches'] === undefined
|
|
333
|
+
? DEFAULT_MAX_MATCHES
|
|
334
|
+
: Number(given['max-matches']);
|
|
206
335
|
|
|
207
336
|
const config = loadConfig();
|
|
208
337
|
if (!values['no-sync']) sync(config, Boolean(values.json));
|
|
209
338
|
|
|
210
339
|
const db = openDb(config.dbPath, {readOnly: true});
|
|
211
340
|
const threadsRaw = String(values.threads);
|
|
212
|
-
const
|
|
213
|
-
text,
|
|
341
|
+
const common = {
|
|
214
342
|
cwd: values.cwd as string[] | undefined,
|
|
215
343
|
project: values.project as string[] | undefined,
|
|
216
344
|
role: values.role as string[] | undefined,
|
|
@@ -221,32 +349,58 @@ function cmdSearch(argv: string[]): void {
|
|
|
221
349
|
includePrivate: Boolean(values.private),
|
|
222
350
|
limit: Number(values.limit),
|
|
223
351
|
threadLimit: threadsRaw === 'all' ? -1 : Number(threadsRaw),
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
352
|
+
};
|
|
353
|
+
const style = makeStyle(colorsEnabled());
|
|
354
|
+
const render = {style, showPath: Boolean(values.path)};
|
|
355
|
+
const json = (payload: unknown): void => {
|
|
356
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
357
|
+
};
|
|
358
|
+
const note = (line: string): void => {
|
|
359
|
+
process.stderr.write(style.dim(line + '\n'));
|
|
360
|
+
};
|
|
227
361
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
362
|
+
try {
|
|
363
|
+
if (pattern !== undefined) {
|
|
364
|
+
const outcome = await regexSearch(db, {
|
|
365
|
+
...common,
|
|
366
|
+
pattern,
|
|
367
|
+
ignoreCase: Boolean(values['ignore-case']),
|
|
368
|
+
fixed: Boolean(values.fixed),
|
|
369
|
+
maxPerFile,
|
|
370
|
+
maxMatches,
|
|
371
|
+
});
|
|
372
|
+
if (values.json) return json(outcome);
|
|
373
|
+
writeHits(
|
|
374
|
+
outcome.hits,
|
|
375
|
+
render,
|
|
376
|
+
`no matches in ${outcome.filesSearched} transcript(s)`,
|
|
377
|
+
);
|
|
378
|
+
if (outcome.truncated)
|
|
379
|
+
note(
|
|
380
|
+
`(stopped at ${maxMatches} matching entries; narrow the pattern or raise --max-matches)`,
|
|
381
|
+
);
|
|
382
|
+
// The index is caught up before the query, so this means the transcripts
|
|
383
|
+
// moved underneath it: worth saying, since those lines were skipped.
|
|
384
|
+
if (outcome.unresolved > 0)
|
|
385
|
+
note(
|
|
386
|
+
`(${outcome.unresolved} matching line(s) are not in the index yet; \`recall index\` will pick them up)`,
|
|
387
|
+
);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
233
390
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
391
|
+
const outcome = search(db, {
|
|
392
|
+
...common,
|
|
393
|
+
text,
|
|
394
|
+
noRecency: Boolean(values['no-recency']),
|
|
395
|
+
raw: Boolean(values.raw),
|
|
396
|
+
});
|
|
397
|
+
if (values.json) return json(outcome);
|
|
398
|
+
if (outcome.quotedFallback)
|
|
399
|
+
note(`(query retried as literal tokens: ${outcome.usedQuery})`);
|
|
400
|
+
writeHits(outcome.hits, render, 'no matches');
|
|
401
|
+
} finally {
|
|
237
402
|
db.close();
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
if (outcome.quotedFallback) {
|
|
241
|
-
process.stderr.write(
|
|
242
|
-
style.dim(`(query retried as literal tokens: ${outcome.usedQuery})\n`),
|
|
243
|
-
);
|
|
244
403
|
}
|
|
245
|
-
const rendered = outcome.hits.map((hit) =>
|
|
246
|
-
renderHit(hit, {style, showPath: Boolean(values.path)}),
|
|
247
|
-
);
|
|
248
|
-
process.stdout.write(rendered.join('\n\n') + '\n');
|
|
249
|
-
db.close();
|
|
250
404
|
}
|
|
251
405
|
|
|
252
406
|
function cmdShow(argv: string[]): void {
|
|
@@ -432,7 +586,51 @@ function cmdConfig(argv: string[]): void {
|
|
|
432
586
|
);
|
|
433
587
|
}
|
|
434
588
|
|
|
435
|
-
|
|
589
|
+
/**
|
|
590
|
+
* `recall skills [list|install]`.
|
|
591
|
+
*
|
|
592
|
+
* Kept flag-light on purpose: the only real decision is user versus project scope.
|
|
593
|
+
*/
|
|
594
|
+
function cmdSkills(argv: string[]): void {
|
|
595
|
+
const scope: Scope = argv.includes('--project') ? 'project' : 'user';
|
|
596
|
+
const verb = argv.find((argument) => !argument.startsWith('-')) ?? 'list';
|
|
597
|
+
if (verb !== 'list' && verb !== 'install')
|
|
598
|
+
fail(`unknown skills command "${verb}". Try \`list\` or \`install\``);
|
|
599
|
+
|
|
600
|
+
const available = availableSkills();
|
|
601
|
+
if (available.length === 0) {
|
|
602
|
+
fail(
|
|
603
|
+
'no skills found beside this package. A published install carries them; a checkout keeps them at the repo root',
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const style = makeStyle(colorsEnabled());
|
|
608
|
+
if (verb === 'list') {
|
|
609
|
+
process.stdout.write(tildify(destinationFor(scope)) + '\n\n');
|
|
610
|
+
for (const skill of available) {
|
|
611
|
+
const mark = isInstalled(skill, scope) ? 'installed' : 'not installed';
|
|
612
|
+
process.stdout.write(
|
|
613
|
+
` ${style.bold(skill.name)} ${style.dim(`(${mark})`)}\n ${skill.description}\n\n`,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
process.stdout.write('Run `recall skills install` to copy them in.\n');
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const installed = installSkills(scope);
|
|
621
|
+
for (const entry of installed) {
|
|
622
|
+
process.stdout.write(
|
|
623
|
+
` ${entry.replaced ? 'replaced' : 'installed'} ${tildify(entry.to)}\n`,
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
process.stdout.write(
|
|
627
|
+
style.dim(
|
|
628
|
+
`\n${installed.length} skill${installed.length === 1 ? '' : 's'} copied. They are copies, so re-run this after upgrading memonaut.\n`,
|
|
629
|
+
),
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async function main(): Promise<void> {
|
|
436
634
|
const argv = process.argv.slice(2);
|
|
437
635
|
const command = argv[0];
|
|
438
636
|
const rest = argv.slice(1);
|
|
@@ -450,6 +648,8 @@ function main(): void {
|
|
|
450
648
|
return cmdStats(rest);
|
|
451
649
|
case 'config':
|
|
452
650
|
return cmdConfig(rest);
|
|
651
|
+
case 'skills':
|
|
652
|
+
return cmdSkills(rest);
|
|
453
653
|
case '--version':
|
|
454
654
|
case '-v':
|
|
455
655
|
process.stdout.write(version() + '\n');
|
|
@@ -466,9 +666,9 @@ function main(): void {
|
|
|
466
666
|
}
|
|
467
667
|
|
|
468
668
|
/** Entry point. Loaded dynamically by `cli.ts`, see the note there. */
|
|
469
|
-
export function run(): void {
|
|
669
|
+
export async function run(): Promise<void> {
|
|
470
670
|
try {
|
|
471
|
-
main();
|
|
671
|
+
await main();
|
|
472
672
|
} catch (err) {
|
|
473
673
|
fail((err as Error).message);
|
|
474
674
|
}
|
package/src/cli.ts
CHANGED
package/src/format.ts
CHANGED
|
@@ -117,6 +117,18 @@ export function renderHit(hit: SearchHit, opts: RenderOptions): string {
|
|
|
117
117
|
meta.push(
|
|
118
118
|
`${hit.otherHits} more match${hit.otherHits === 1 ? '' : 'es'} in this lineage`,
|
|
119
119
|
);
|
|
120
|
+
// The regex path reads the original transcripts, so it can match text no
|
|
121
|
+
// full-text query could reach: a kind the tier excludes (tool output is 42%
|
|
122
|
+
// of the bytes), or text past where a chunk was truncated. Saying so is the
|
|
123
|
+
// difference between a surprising result and an informative one. A match on
|
|
124
|
+
// the JSON scaffolding is NOT that, and has to say something else: the
|
|
125
|
+
// content around it is very probably indexed.
|
|
126
|
+
if (hit.matchedIn === 'transcript') {
|
|
127
|
+
const what = hit.kind ? `this ${hit.kind} text` : 'this text';
|
|
128
|
+
meta.push(`read from the transcript · ${what} is not indexed`);
|
|
129
|
+
} else if (hit.matchedIn === 'structure') {
|
|
130
|
+
meta.push('matched the transcript JSON, not the message text');
|
|
131
|
+
}
|
|
120
132
|
if (meta.length) lines.push(' ' + style.dim(meta.join(' · ')));
|
|
121
133
|
|
|
122
134
|
for (const thread of hit.threads) {
|
package/src/index.ts
CHANGED
|
@@ -6,5 +6,15 @@ export * from './pi-source.js';
|
|
|
6
6
|
export * from './db.js';
|
|
7
7
|
export * from './indexer.js';
|
|
8
8
|
export * from './search.js';
|
|
9
|
+
export * from './regex.js';
|
|
10
|
+
// Only what a caller of `regexSearch` needs: the errors it can throw and the
|
|
11
|
+
// pre-flight check. Spawning rg is an implementation detail, not API.
|
|
12
|
+
export {
|
|
13
|
+
RipgrepError,
|
|
14
|
+
RipgrepMissingError,
|
|
15
|
+
ripgrepAvailable,
|
|
16
|
+
ripgrepBinary,
|
|
17
|
+
} from './ripgrep.js';
|
|
9
18
|
export * from './format.js';
|
|
19
|
+
export * from './skills.js';
|
|
10
20
|
export {silenceSqliteWarning} from './quiet.js';
|
package/src/model.ts
CHANGED
|
@@ -107,7 +107,13 @@ export interface SearchHit {
|
|
|
107
107
|
role: string;
|
|
108
108
|
tool: string | null;
|
|
109
109
|
ts: string | null;
|
|
110
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Which extracted text this hit is about. Always set on the full-text path,
|
|
112
|
+
* where a hit IS a chunk. Absent on the regex path when the match landed
|
|
113
|
+
* outside any extracted text (see `matchedIn`), because inventing a kind
|
|
114
|
+
* there would be a guess presented as a fact.
|
|
115
|
+
*/
|
|
116
|
+
kind?: ChunkKind;
|
|
111
117
|
snippet: string;
|
|
112
118
|
score: number;
|
|
113
119
|
/** Threads carrying this entry, most recently active first (may be capped). */
|
|
@@ -116,6 +122,22 @@ export interface SearchHit {
|
|
|
116
122
|
threadTotal: number;
|
|
117
123
|
/** Further matches in the same lineage that were folded into this group. */
|
|
118
124
|
otherHits: number;
|
|
125
|
+
/**
|
|
126
|
+
* Regex path only: where the matched bytes actually live.
|
|
127
|
+
*
|
|
128
|
+
* - `index`: in extracted text that is also indexed, so a full-text query
|
|
129
|
+
* could in principle have found it too.
|
|
130
|
+
* - `transcript`: in what someone actually said or ran, but NOT in the index.
|
|
131
|
+
* Either the tier excludes that kind (tool output, by default) or the text
|
|
132
|
+
* sits past where the chunk was truncated. This is the whole reason to
|
|
133
|
+
* reach for a regex search.
|
|
134
|
+
* - `structure`: in the transcript's JSON scaffolding, a key, an id or an
|
|
135
|
+
* escape sequence, rather than in anything anyone wrote. Distinct from
|
|
136
|
+
* `transcript` on purpose: the content around it may well be indexed, and
|
|
137
|
+
* claiming otherwise would be exactly the dishonesty the flag exists to
|
|
138
|
+
* prevent.
|
|
139
|
+
*/
|
|
140
|
+
matchedIn?: 'index' | 'transcript' | 'structure';
|
|
119
141
|
}
|
|
120
142
|
|
|
121
143
|
export interface ThreadRef {
|