pi-blackhole 0.2.2 → 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/README.md +23 -8
- package/index.ts +45 -0
- package/package.json +12 -5
- package/src/commands/pi-vcc.ts +18 -8
- package/src/commands/vcc-recall.ts +3 -2
- package/src/core/build-sections.ts +1 -1
- package/src/core/content.ts +70 -0
- package/src/core/drill-down.ts +273 -0
- package/src/core/format-recall.ts +107 -3
- package/src/core/load-messages.ts +3 -2
- package/src/core/recall-scope.ts +15 -4
- package/src/core/search-entries.ts +155 -10
- package/src/core/unified-config.ts +48 -17
- package/src/om/agents/dropper/agent.ts +144 -12
- package/src/om/agents/dropper/coverage.ts +128 -0
- package/src/om/agents/dropper/prompts.ts +12 -12
- package/src/om/agents/observer/agent.ts +22 -2
- package/src/om/agents/observer/prompts.ts +1 -1
- package/src/om/agents/reflector/agent.ts +51 -2
- package/src/om/agents/reflector/prompts.ts +8 -4
- package/src/om/consolidation.ts +1 -47
- package/src/om/cooldown.ts +11 -4
- package/src/om/ledger/projection.ts +13 -1
- package/src/om/ledger/render-summary.ts +51 -0
- package/src/om/pending.ts +8 -4
- package/src/tools/recall.ts +120 -44
- package/vitest.config.ts +1 -23
package/src/om/cooldown.ts
CHANGED
|
@@ -57,10 +57,17 @@ function readCooldownMap(): CooldownMap {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function writeCooldownMap(map: CooldownMap): void {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
try {
|
|
61
|
+
const path = cooldownPath();
|
|
62
|
+
const dir = dirname(path);
|
|
63
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
64
|
+
writeFileSync(path, `${JSON.stringify(map, null, 2)}\n`);
|
|
65
|
+
} catch {
|
|
66
|
+
// Best-effort: cooldowns are advisory. Losing them means a rate-limited
|
|
67
|
+
// model might be retried before its cooldown window expires — slightly
|
|
68
|
+
// more API traffic, no data loss. This also prevents a process crash
|
|
69
|
+
// on read-only filesystems.
|
|
70
|
+
}
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
// ── API ─────────────────────────────────────────────────────────────────────
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Upstream: https://github.com/elpapi42/pi-observational-memory (src/session-ledger/projection.ts)
|
|
5
5
|
* Unmodified.
|
|
6
6
|
*/
|
|
7
|
+
import { selectPriorObservations } from "./render-summary.js";
|
|
7
8
|
import {
|
|
8
9
|
OM_FOLDED,
|
|
9
10
|
isMemoryDetails,
|
|
@@ -207,10 +208,21 @@ export function buildCompactionProjection(
|
|
|
207
208
|
0,
|
|
208
209
|
);
|
|
209
210
|
const fullFold = observationTokens >= config.observationsPoolMaxTokens;
|
|
210
|
-
|
|
211
|
+
let projection = fullFold
|
|
211
212
|
? fullProjection(entries, firstKeptEntryId)
|
|
212
213
|
: normalProjection;
|
|
213
214
|
|
|
215
|
+
// Cap observations to budget using relevance-tiered + recency scoring.
|
|
216
|
+
// Even if the dropper determined some old observations are worth keeping,
|
|
217
|
+
// this safety valve ensures the compaction output never exceeds the pool
|
|
218
|
+
// token budget. Observations survive in the branch regardless.
|
|
219
|
+
if (config.observationsPoolMaxTokens > 0 && observationTokens >= config.observationsPoolMaxTokens) {
|
|
220
|
+
projection = {
|
|
221
|
+
observations: selectPriorObservations(projection.observations, config.observationsPoolMaxTokens),
|
|
222
|
+
reflections: projection.reflections,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
214
226
|
const details: MemoryDetails = {
|
|
215
227
|
type: OM_FOLDED,
|
|
216
228
|
version: 1,
|
|
@@ -25,6 +25,57 @@ export function observationToSummaryLine(observation: Observation): string {
|
|
|
25
25
|
return `[${observation.id}] ${observation.timestamp} [${observation.relevance}] ${observation.content}`;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Score an observation for cap/trim selection.
|
|
29
|
+
* Relevance tier dominates: medium (5+) always outranks low (max 2).
|
|
30
|
+
* Recency is based on position in the flat-mapped array (0 = oldest, N-1 = newest),
|
|
31
|
+
* avoiding wall-clock dependency that punishes sessions spanning days or weeks. */
|
|
32
|
+
export function scoreObservation(obs: Observation, index: number, total: number): number {
|
|
33
|
+
const base = obs.relevance === "high" || obs.relevance === "critical" ? 10
|
|
34
|
+
: obs.relevance === "medium" ? 5 : 1;
|
|
35
|
+
const recency = total > 1 ? index / (total - 1) : 1;
|
|
36
|
+
return base + recency;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Select observations up to a token budget, keeping all high-relevance items
|
|
40
|
+
* unconditionally and filling the remaining budget with the best-scoring
|
|
41
|
+
* medium and low observations (relevance-tiered + recency).
|
|
42
|
+
*
|
|
43
|
+
* Reflections are never trimmed — they are inherently rare and always stay.
|
|
44
|
+
* Observations stay in the branch either way; this only caps what is rendered
|
|
45
|
+
* in the compaction summary output. */
|
|
46
|
+
export function selectPriorObservations(observations: Observation[], maxTokens: number): Observation[] {
|
|
47
|
+
// Track original indices so we can restore chronological order after scoring
|
|
48
|
+
const indexed = observations.map((obs, i) => ({ obs, originalIndex: i }));
|
|
49
|
+
const high = indexed.filter(item => item.obs.relevance === "high" || item.obs.relevance === "critical");
|
|
50
|
+
const rest = indexed.filter(item => item.obs.relevance !== "high" && item.obs.relevance !== "critical");
|
|
51
|
+
|
|
52
|
+
// High always kept — consume budget first
|
|
53
|
+
let budget = maxTokens;
|
|
54
|
+
const selected = new Set<{ obs: Observation; originalIndex: number }>();
|
|
55
|
+
for (const item of high) {
|
|
56
|
+
const lineTokens = Math.ceil(observationToSummaryLine(item.obs).length / 4);
|
|
57
|
+
selected.add(item);
|
|
58
|
+
budget -= lineTokens;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Score medium + low and select best within remaining budget
|
|
62
|
+
if (rest.length > 0 && budget > 0) {
|
|
63
|
+
const scored = rest.map((item, i) => ({ item, score: scoreObservation(item.obs, i, rest.length) }));
|
|
64
|
+
scored.sort((a, b) => b.score - a.score); // highest score first
|
|
65
|
+
for (const { item } of scored) {
|
|
66
|
+
const lineTokens = Math.ceil(observationToSummaryLine(item.obs).length / 4);
|
|
67
|
+
if (budget - lineTokens < 0) break;
|
|
68
|
+
selected.add(item);
|
|
69
|
+
budget -= lineTokens;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Restore original chronological order before returning
|
|
74
|
+
return Array.from(selected)
|
|
75
|
+
.sort((a, b) => a.originalIndex - b.originalIndex)
|
|
76
|
+
.map(item => item.obs);
|
|
77
|
+
}
|
|
78
|
+
|
|
28
79
|
export function reflectionToSummaryLine(reflection: Reflection): string {
|
|
29
80
|
return `[${reflection.id}] ${reflection.content}`;
|
|
30
81
|
}
|
package/src/om/pending.ts
CHANGED
|
@@ -14,10 +14,8 @@
|
|
|
14
14
|
* concurrent pi sessions writing to a shared file.
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
17
|
-
import {
|
|
17
|
+
import { join } from "node:path";
|
|
18
18
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
19
|
-
import type { Observation, Reflection } from "./ledger/types.js";
|
|
20
|
-
|
|
21
19
|
// ── Types ───────────────────────────────────────────────────────────────────
|
|
22
20
|
|
|
23
21
|
export interface PendingObservation {
|
|
@@ -143,7 +141,13 @@ function writeSessionState(sessionId: string, state: PendingOMState): void {
|
|
|
143
141
|
}
|
|
144
142
|
} catch { /* best-effort — stale backup is optional */ }
|
|
145
143
|
|
|
146
|
-
|
|
144
|
+
try {
|
|
145
|
+
writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`);
|
|
146
|
+
} catch {
|
|
147
|
+
// Best-effort: pending state loss means consolidation may re-process
|
|
148
|
+
// the same data on the next run — safe (idempotent by design).
|
|
149
|
+
// Prevents process crash on read-only filesystems.
|
|
150
|
+
}
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
/**
|
package/src/tools/recall.ts
CHANGED
|
@@ -4,13 +4,17 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Created by pi-vcc-om. Replaces pi-vcc's vcc_recall and OM's standalone recall-observation.
|
|
6
6
|
*/
|
|
7
|
-
import { Type } from "
|
|
7
|
+
import { Type } from "typebox";
|
|
8
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
8
9
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
10
|
import { loadAllMessages } from "../core/load-messages";
|
|
10
|
-
import { searchEntries } from "../core/search-entries";
|
|
11
|
-
import {
|
|
11
|
+
import { searchEntries, getFileIndicators, getTouchedFiles } from "../core/search-entries";
|
|
12
|
+
import type { RenderedEntry } from "../core/render-entries";
|
|
13
|
+
import type { SearchHit } from "../core/search-entries";
|
|
14
|
+
import { formatRecallOutput, formatTouchedOutput } from "../core/format-recall";
|
|
12
15
|
import { getActiveLineageEntryIds } from "../core/lineage";
|
|
13
|
-
import { normalizeRecallScope } from "../core/recall-scope";
|
|
16
|
+
import { normalizeRecallScope, normalizeRecallMode } from "../core/recall-scope";
|
|
17
|
+
import { parseDrillDown, expandEntryFile } from "../core/drill-down.js";
|
|
14
18
|
import {
|
|
15
19
|
recallMemorySources,
|
|
16
20
|
type Entry,
|
|
@@ -29,20 +33,63 @@ import {
|
|
|
29
33
|
const DEFAULT_RECENT = 25;
|
|
30
34
|
const PAGE_SIZE = 5;
|
|
31
35
|
|
|
32
|
-
const invalidExpandIndices = (requested: number[], available: Set<number>): number[] =>
|
|
36
|
+
export const invalidExpandIndices = (requested: number[], available: Set<number>): number[] =>
|
|
33
37
|
requested.filter((i) => !Number.isInteger(i) || !available.has(i));
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Merge expanded (full-content) entries into search results.
|
|
41
|
+
* Overlapping entries get their summary replaced with full content.
|
|
42
|
+
* Non-overlapping expanded entries are appended. Results are sorted by index.
|
|
43
|
+
*/
|
|
44
|
+
export function mergeExpandedIntoSearchResults(
|
|
45
|
+
searchResults: SearchHit[],
|
|
46
|
+
expandedEntries: RenderedEntry[],
|
|
47
|
+
): SearchHit[] {
|
|
48
|
+
if (expandedEntries.length === 0) return searchResults;
|
|
49
|
+
|
|
50
|
+
const expandedByIndex = new Map(expandedEntries.map((e) => [e.index, e]));
|
|
51
|
+
|
|
52
|
+
// Replace truncated summaries with full content for expanded indices
|
|
53
|
+
const merged = searchResults.map((r) => {
|
|
54
|
+
const full = expandedByIndex.get(r.index);
|
|
55
|
+
return full ? { ...r, summary: full.summary } : r;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Append expand-only entries not already in search results
|
|
59
|
+
for (const fe of expandedEntries) {
|
|
60
|
+
if (!merged.some((r) => r.index === fe.index)) {
|
|
61
|
+
merged.push(fe as SearchHit);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Maintain natural order by index
|
|
66
|
+
merged.sort((a, b) => a.index - b.index);
|
|
67
|
+
return merged;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function vccRecall(params: { query?: string; expand?: number[]; page?: number; scope?: "lineage" | "all"; mode?: string }, ctx: any) {
|
|
36
71
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
37
72
|
if (!sessionFile) {
|
|
38
73
|
return { content: [{ type: "text" as const, text: "No session file available." }], details: undefined };
|
|
39
74
|
}
|
|
40
75
|
const scope = normalizeRecallScope(params.scope);
|
|
76
|
+
const mode = normalizeRecallMode(params.mode);
|
|
41
77
|
const lineageEntryIds = scope === "lineage" ? getActiveLineageEntryIds(ctx.sessionManager) : undefined;
|
|
78
|
+
|
|
79
|
+
// ── "touched" mode: aggregate file operations ──
|
|
80
|
+
if (mode === "touched") {
|
|
81
|
+
const { rendered, rawMessages } = loadAllMessages(sessionFile, false, lineageEntryIds);
|
|
82
|
+
const touched = getTouchedFiles(rawMessages, rendered);
|
|
83
|
+
const text = formatTouchedOutput(touched, params.page);
|
|
84
|
+
return { content: [{ type: "text" as const, text }], details: undefined };
|
|
85
|
+
}
|
|
86
|
+
|
|
42
87
|
const expandSet = new Set(params.expand ?? []);
|
|
43
88
|
const hasExpand = expandSet.size > 0;
|
|
44
89
|
|
|
45
|
-
if
|
|
90
|
+
// ── Pre-load full messages if expand is requested ──
|
|
91
|
+
let expandedFullEntries: RenderedEntry[] | undefined;
|
|
92
|
+
if (hasExpand) {
|
|
46
93
|
const { rendered: fullMsgs } = loadAllMessages(sessionFile, true, lineageEntryIds);
|
|
47
94
|
const requested = [...expandSet];
|
|
48
95
|
const byIndex = new Map(fullMsgs.map((m) => [m.index, m]));
|
|
@@ -50,34 +97,55 @@ async function vccRecall(params: { query?: string; expand?: number[]; page?: num
|
|
|
50
97
|
if (invalid.length > 0) {
|
|
51
98
|
return { content: [{ type: "text" as const, text: `Cannot expand indices outside ${scope === "all" ? "session history" : "active lineage"}: ${invalid.join(", ")}` }], details: undefined };
|
|
52
99
|
}
|
|
53
|
-
|
|
54
|
-
let output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(expanded);
|
|
100
|
+
expandedFullEntries = requested.map((i) => byIndex.get(i)).filter((m): m is NonNullable<typeof m> => Boolean(m));
|
|
55
101
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
const branchEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
61
|
-
const obs = findObservationsForEntryIds(branchEntries, expandedIds);
|
|
62
|
-
const refs = findReflectionsForEntryIds(branchEntries, expandedIds);
|
|
63
|
-
if (obs.length > 0 || refs.length > 0) {
|
|
64
|
-
output += "\n\n" + formatRelatedObservations(obs, refs);
|
|
65
|
-
}
|
|
66
|
-
} catch { /* branch may not be available */ }
|
|
67
|
-
}
|
|
102
|
+
// Expand-only path (no query): return expanded entries immediately
|
|
103
|
+
if (!params.query) {
|
|
104
|
+
let output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(expandedFullEntries);
|
|
68
105
|
|
|
69
|
-
|
|
106
|
+
// Coupling: look up related OM observations
|
|
107
|
+
const expandedIds = expandedFullEntries.map((e) => e.id).filter(Boolean);
|
|
108
|
+
if (expandedIds.length > 0) {
|
|
109
|
+
try {
|
|
110
|
+
const branchEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
111
|
+
const obs = findObservationsForEntryIds(branchEntries, expandedIds);
|
|
112
|
+
const refs = findReflectionsForEntryIds(branchEntries, expandedIds);
|
|
113
|
+
if (obs.length > 0 || refs.length > 0) {
|
|
114
|
+
output += "\n\n" + formatRelatedObservations(obs, refs);
|
|
115
|
+
}
|
|
116
|
+
} catch { /* branch may not be available */ }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { content: [{ type: "text" as const, text: output }], details: undefined };
|
|
120
|
+
}
|
|
121
|
+
// With query: fall through to search, then merge expanded entries into results
|
|
70
122
|
}
|
|
71
123
|
|
|
72
124
|
const { rendered: msgs, rawMessages } = loadAllMessages(sessionFile, false, lineageEntryIds);
|
|
73
|
-
|
|
74
|
-
? searchEntries(msgs, rawMessages, params.query)
|
|
75
|
-
: msgs.slice(-DEFAULT_RECENT)
|
|
125
|
+
let allResults: SearchHit[] = params.query?.trim()
|
|
126
|
+
? searchEntries(msgs, rawMessages, params.query, undefined, mode)
|
|
127
|
+
: msgs.slice(-DEFAULT_RECENT).map((entry, i) => {
|
|
128
|
+
const msgIndex = Math.max(0, msgs.length - DEFAULT_RECENT) + i;
|
|
129
|
+
const msg = rawMessages[msgIndex];
|
|
130
|
+
if (msg) {
|
|
131
|
+
const indicators = getFileIndicators(msg);
|
|
132
|
+
if (indicators.length > 0) {
|
|
133
|
+
return { ...entry, fileMatches: indicators };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return entry;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Merge expanded entries into full result set BEFORE pagination
|
|
140
|
+
// so pagination counts and positioning stay consistent
|
|
141
|
+
if (expandedFullEntries) {
|
|
142
|
+
allResults = mergeExpandedIntoSearchResults(allResults, expandedFullEntries);
|
|
143
|
+
}
|
|
76
144
|
|
|
77
145
|
if (params.query?.trim()) {
|
|
78
146
|
const page = Math.max(1, params.page ?? 1);
|
|
79
147
|
const start = (page - 1) * PAGE_SIZE;
|
|
80
|
-
const pageResults = allResults.slice(start, start + PAGE_SIZE);
|
|
148
|
+
const pageResults: SearchHit[] = allResults.slice(start, start + PAGE_SIZE);
|
|
81
149
|
const totalPages = Math.ceil(allResults.length / PAGE_SIZE);
|
|
82
150
|
const scopeSuffix = scope === "all" ? " (scope: all)" : "";
|
|
83
151
|
const header = totalPages > 1
|
|
@@ -86,6 +154,7 @@ async function vccRecall(params: { query?: string; expand?: number[]; page?: num
|
|
|
86
154
|
const footer = page < totalPages
|
|
87
155
|
? `\n--- Use page:${page + 1}${scope === "all" ? " with scope:'all'" : ""} for more results ---`
|
|
88
156
|
: "";
|
|
157
|
+
|
|
89
158
|
let output = formatRecallOutput(pageResults, params.query, header) + footer;
|
|
90
159
|
|
|
91
160
|
// Coupling: augment search results with related observations
|
|
@@ -104,6 +173,7 @@ async function vccRecall(params: { query?: string; expand?: number[]; page?: num
|
|
|
104
173
|
return { content: [{ type: "text" as const, text: output }], details: undefined };
|
|
105
174
|
}
|
|
106
175
|
|
|
176
|
+
// No query: show recent entries (expand already merged above)
|
|
107
177
|
const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(allResults, params.query);
|
|
108
178
|
return { content: [{ type: "text" as const, text: output }], details: undefined };
|
|
109
179
|
}
|
|
@@ -160,40 +230,46 @@ export function registerRecallTool(pi: ExtensionAPI): void {
|
|
|
160
230
|
name: "recall",
|
|
161
231
|
label: "Recall",
|
|
162
232
|
description:
|
|
163
|
-
"
|
|
164
|
-
"
|
|
165
|
-
"- A #N entry index to expand a specific transcript entry from compacted output.\n" +
|
|
166
|
-
"- A text/regex query to search conversation content. Multi-word queries use BM25 ranking with stopword filtering.\n" +
|
|
167
|
-
"Search tips: use unique concrete words (file names, function names, exact phrases), not conceptual questions. Regex metacharacters (|, *, .) trigger regex mode. Default scope is active lineage; use scope:'all' for off-lineage branches.",
|
|
233
|
+
"Search session history and earlier lines omitted, file write/edit content by text/regex. " +
|
|
234
|
+
"Expand entries (#N), drill-down file content (#N:path) with paging, or aggregate touched files (mode:touched).",
|
|
168
235
|
promptSnippet:
|
|
169
|
-
"recall:
|
|
236
|
+
"recall: Search session history + file write/edit content by text/regex. #N expand, #N:path drill-down with offset/limit paging, mode:file/transcript/touched.",
|
|
170
237
|
promptGuidelines: [
|
|
171
|
-
"Use recall
|
|
172
|
-
"Use recall
|
|
173
|
-
"Use recall
|
|
174
|
-
"
|
|
175
|
-
"
|
|
238
|
+
"Use recall — search is literal text/regex matching, NOT semantic. If no results, try different terms or a regex pattern. Set scope:'all' to search the full session.",
|
|
239
|
+
"Use recall — mode:file to search only write/edit file content, mode:transcript for conversation-only, mode:touched for aggregate view of all files written/edited across the session.",
|
|
240
|
+
"Use recall — drill-down supports paging: #42:auth.ts shows first 30 lines, #42:auth.ts:30 shows next 30, #42:auth.ts:full shows everything. Note: edit diffs are not indexed for text search — drill-down reads them from raw JSONL.",
|
|
241
|
+
"Use recall — when a drill-down path matches multiple files, options are listed. Narrow with a more specific path substring.",
|
|
242
|
+
"Use recall — hex observation/reflection ids (12-char hex) link memory evidence to session entries with cross-references for navigation.",
|
|
176
243
|
],
|
|
177
244
|
parameters: Type.Object({
|
|
178
245
|
query: Type.Optional(
|
|
179
|
-
Type.String({ description: "
|
|
246
|
+
Type.String({ description: "Text/regex search; #N expands entry; #N:path drills file (#N:file auto-selects); #N:path:full all lines; #N:path:offset:limit range; 12-char hex for observations. Only full-file writes indexed." }),
|
|
180
247
|
),
|
|
181
248
|
expand: Type.Optional(
|
|
182
|
-
Type.Array(Type.Number(), { description: "Entry indices to return full untruncated content for
|
|
249
|
+
Type.Array(Type.Number(), { description: "Entry indices to return full untruncated content for. Standalone or with query." }),
|
|
183
250
|
),
|
|
184
251
|
page: Type.Optional(
|
|
185
|
-
Type.Number({ description: "Page number (1-based) for paginated
|
|
252
|
+
Type.Number({ description: "Page number (1-based) for paginated results. Default: 1." }),
|
|
186
253
|
),
|
|
187
254
|
scope: Type.Optional(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
], { description: "
|
|
255
|
+
StringEnum(["lineage", "all"] as const, { description: "Search scope. lineage = active lineage (default), all = entire session." }),
|
|
256
|
+
),
|
|
257
|
+
mode: Type.Optional(
|
|
258
|
+
StringEnum(["hybrid", "file", "transcript", "touched"] as const, { description: "What content to search. hybrid (default) = transcript + file indicators. file = write/edit file content only. transcript = conversation only. touched = aggregate files grouped by path (not per-entry search)." }),
|
|
192
259
|
),
|
|
193
260
|
}),
|
|
194
261
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
195
262
|
const q = params.query?.trim();
|
|
196
263
|
// Dispatch by format
|
|
264
|
+
if (q && parseDrillDown(q)) {
|
|
265
|
+
const parsed = parseDrillDown(q)!;
|
|
266
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
267
|
+
if (!sessionFile) {
|
|
268
|
+
return { content: [{ type: "text" as const, text: "No session file available." }], details: undefined };
|
|
269
|
+
}
|
|
270
|
+
const text = expandEntryFile(sessionFile, parsed.index, parsed.pathPattern, parsed.full, parsed.offset, parsed.limit);
|
|
271
|
+
return { content: [{ type: "text" as const, text }], details: undefined };
|
|
272
|
+
}
|
|
197
273
|
if (q && VCC_ENTRY_PATTERN.test(q)) {
|
|
198
274
|
// #N → expand entry indices
|
|
199
275
|
const match = q.match(VCC_ENTRY_PATTERN);
|
package/vitest.config.ts
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
1
|
import { defineConfig } from "vitest/config";
|
|
2
2
|
|
|
3
|
-
// ── pnpm global store paths for @earendil-works packages ──────────────────
|
|
4
|
-
|
|
5
|
-
const GLOBAL_PNPM =
|
|
6
|
-
"/home/kovalik/.local/share/pnpm/global/5/.pnpm";
|
|
7
|
-
|
|
8
|
-
const PKGS: Record<string, string> = {
|
|
9
|
-
"@earendil-works/pi-ai": `${GLOBAL_PNPM}/@earendil-works+pi-ai@0.75.4_ws@8.20.1_zod@4.4.3/node_modules/@earendil-works/pi-ai`,
|
|
10
|
-
"@earendil-works/pi-ai/oauth": `${GLOBAL_PNPM}/@earendil-works+pi-ai@0.75.4_ws@8.20.1_zod@4.4.3/node_modules/@earendil-works/pi-ai/oauth`,
|
|
11
|
-
"@earendil-works/pi-agent-core": `${GLOBAL_PNPM}/@earendil-works+pi-agent-core@0.75.4_ws@8.20.1_zod@4.4.3/node_modules/@earendil-works/pi-agent-core`,
|
|
12
|
-
"@earendil-works/pi-coding-agent": `${GLOBAL_PNPM}/@earendil-works+pi-coding-agent@0.75.4_ws@8.20.1_zod@4.4.3/node_modules/@earendil-works/pi-coding-agent`,
|
|
13
|
-
"@earendil-works/pi-tui": `${GLOBAL_PNPM}/@earendil-works+pi-tui@0.75.4/node_modules/@earendil-works/pi-tui`,
|
|
14
|
-
};
|
|
15
|
-
|
|
16
3
|
export default defineConfig({
|
|
17
4
|
test: {
|
|
18
5
|
globals: true,
|
|
@@ -22,12 +9,7 @@ export default defineConfig({
|
|
|
22
9
|
},
|
|
23
10
|
resolve: {
|
|
24
11
|
alias: [
|
|
25
|
-
//
|
|
26
|
-
...Object.entries(PKGS).map(([name, path]) => ({
|
|
27
|
-
find: new RegExp(`^${escapeRegex(name)}$`),
|
|
28
|
-
replacement: path,
|
|
29
|
-
})),
|
|
30
|
-
// Resolve .js → extension-less for our source files
|
|
12
|
+
// Resolve .js → extension-less for our TypeScript source files
|
|
31
13
|
{
|
|
32
14
|
find: /\.js$/,
|
|
33
15
|
replacement: "",
|
|
@@ -35,7 +17,3 @@ export default defineConfig({
|
|
|
35
17
|
],
|
|
36
18
|
},
|
|
37
19
|
});
|
|
38
|
-
|
|
39
|
-
function escapeRegex(s: string): string {
|
|
40
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
41
|
-
}
|