@danypops/pi-lector 0.6.0 → 0.8.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 +11 -5
- package/extension/src/code-intelligence-operations.ts +7 -0
- package/extension/src/code-intelligence-rendering.ts +4 -27
- package/extension/src/index.ts +18 -95
- package/extension/src/lector-client.ts +1 -1
- package/extension/src/nearest-workspace-root.ts +46 -25
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -12,11 +12,17 @@ pi install npm:@danypops/pi-lector
|
|
|
12
12
|
|
|
13
13
|
Symbol and semantic tool results identify their backend and fidelity. `typescript-language-server` results are semantic; compiler or parser fallback results are structural and list their limitations.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
Every workspace's symbol graph auto-populates in the background the moment any tool
|
|
16
|
+
first touches it -- reachable_from, workspace_map, reference_based_rename, and
|
|
17
|
+
symbol_annotations all return an empty or refusing result rather than an error if
|
|
18
|
+
it's still building, instead of requiring an explicit "start indexing" call.
|
|
18
19
|
|
|
19
20
|
When a session starts inside a Git repository, pi-lector checks a durable, bounded
|
|
20
21
|
source-content manifest without blocking startup. The footer reports not cached,
|
|
21
|
-
caching, or cached
|
|
22
|
-
|
|
22
|
+
caching, or cached across every workspace touched so far this session; completion
|
|
23
|
+
also emits a one-shot notification. Session shutdown stops polling, and the agent
|
|
24
|
+
receives each state transition once in its context.
|
|
25
|
+
|
|
26
|
+
For an explicit, custom-bound population outside of Pi (a larger scan than the
|
|
27
|
+
default 500 files / 100 symbols per file), use `lector workspace populate-symbol-graph`
|
|
28
|
+
and `lector job status` directly.
|
|
@@ -37,7 +37,14 @@ export interface CodeIntelligenceOperations {
|
|
|
37
37
|
prepareCallHierarchy(path: string, line: number, character: number): Promise<OperationOutputs["workspace.prepareCallHierarchy"]>;
|
|
38
38
|
incomingCalls(path: string, line: number, character: number): Promise<OperationOutputs["workspace.incomingCalls"]>;
|
|
39
39
|
outgoingCalls(path: string, line: number, character: number): Promise<OperationOutputs["workspace.outgoingCalls"]>;
|
|
40
|
+
/**
|
|
41
|
+
* Not exposed as a standalone Pi tool -- every workspace auto-populates on first touch via
|
|
42
|
+
* monitorWorkspaceCache (workspace-cache-operations.ts). Kept here as an internal capability
|
|
43
|
+
* for tests that need a populated graph. For an explicit/custom-bound population outside
|
|
44
|
+
* Pi entirely, `lector workspace populate-symbol-graph` calls the daemon directly.
|
|
45
|
+
*/
|
|
40
46
|
populateSymbolGraph(path: string, maxFiles: number, maxSymbolsPerFile: number, waitMs?: number): Promise<JobSnapshot<PopulateSymbolGraphResult>>;
|
|
47
|
+
/** Not exposed as a standalone Pi tool -- see populateSymbolGraph. */
|
|
41
48
|
jobStatus(jobId: string): Promise<JobSnapshot<PopulateSymbolGraphResult>>;
|
|
42
49
|
reachableFrom(path: string, line: number, character: number, maxDepth: number, kind?: SymbolEdgeKind): Promise<readonly SymbolNode[]>;
|
|
43
50
|
/** Never spawns a symbol index -- safe to call opportunistically (e.g. before deciding whether to enrich a result). */
|
|
@@ -5,9 +5,7 @@ import type {
|
|
|
5
5
|
Hover,
|
|
6
6
|
IncomingCall,
|
|
7
7
|
IntelligenceProvenance,
|
|
8
|
-
JobSnapshot,
|
|
9
8
|
OutgoingCall,
|
|
10
|
-
PopulateSymbolGraphResult,
|
|
11
9
|
SymbolNode,
|
|
12
10
|
WorkspaceLocation,
|
|
13
11
|
WorkspaceMapResult,
|
|
@@ -213,29 +211,6 @@ export function formatCallHierarchyResult(details: CallHierarchyToolDetails | un
|
|
|
213
211
|
return formatOutgoingCallsResult(details.calls, expanded, theme);
|
|
214
212
|
}
|
|
215
213
|
|
|
216
|
-
export function formatPopulateSymbolGraphCall(args: { path?: unknown; maxFiles?: unknown; maxSymbolsPerFile?: unknown }, theme: LectorTheme): string {
|
|
217
|
-
const path = typeof args.path === "string" ? args.path : "";
|
|
218
|
-
return `${theme.fg("toolTitle", theme.bold("populate_symbol_graph"))} ${theme.fg("accent", path)}`;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function describePopulateSymbolGraphJob(job: JobSnapshot<PopulateSymbolGraphResult>): string {
|
|
222
|
-
if (job.status === "queued") return `Source workspace is registered; symbol graph is queued and still loading (job ${job.id}). Poll job_status.`;
|
|
223
|
-
if (job.status === "running") return `Source workspace is registered; symbol graph is still loading (job ${job.id}). Poll job_status.`;
|
|
224
|
-
if (job.status === "failed") return `Job ${job.id} failed [${job.error.code}] -- ${job.error.message}`;
|
|
225
|
-
const result = job.result;
|
|
226
|
-
const counts = `${result.filesProcessed}/${result.filesAttempted} files, ${result.symbolsProcessed} symbol${result.symbolsProcessed === 1 ? "" : "s"}, ${result.nodesAdded} node${result.nodesAdded === 1 ? "" : "s"}, ${result.edgesAdded} edge${result.edgesAdded === 1 ? "" : "s"}`;
|
|
227
|
-
if (result.completeness === "complete") return `Job ${job.id} cached ${counts}`;
|
|
228
|
-
const first = result.failures[0];
|
|
229
|
-
const failure = first ? ` First failure: ${first.path} [${first.code} via ${first.provenance.backend}] ${first.message}` : "";
|
|
230
|
-
return `Job ${job.id} partially cached ${counts}; ${result.filesFailed} failed file${result.filesFailed === 1 ? "" : "s"} (${result.failureCount} failed operations).${failure}`;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export function formatPopulateSymbolGraphResult(job: JobSnapshot<PopulateSymbolGraphResult> | undefined, theme: LectorTheme): string {
|
|
234
|
-
if (!job) return theme.fg("dim", "No job result.");
|
|
235
|
-
const color = job.status === "failed" ? "error" : job.status === "succeeded" ? "muted" : "warning";
|
|
236
|
-
return theme.fg(color, describePopulateSymbolGraphJob(job));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
214
|
export function formatReachableFromCall(args: { path?: unknown; line?: unknown; character?: unknown; maxDepth?: unknown }, theme: LectorTheme): string {
|
|
240
215
|
const base = formatPositionalCall("reachable_from", args, theme);
|
|
241
216
|
const maxDepth = typeof args.maxDepth === "number" ? args.maxDepth : "?";
|
|
@@ -243,7 +218,8 @@ export function formatReachableFromCall(args: { path?: unknown; line?: unknown;
|
|
|
243
218
|
}
|
|
244
219
|
|
|
245
220
|
export function formatReachableFromResult(symbols: readonly SymbolNode[] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
246
|
-
if (!symbols || symbols.length === 0)
|
|
221
|
+
if (!symbols || symbols.length === 0)
|
|
222
|
+
return theme.fg("dim", "Nothing reachable at this position (the workspace's symbol graph may still be populating in the background -- retry shortly).");
|
|
247
223
|
|
|
248
224
|
const displayCount = expanded ? symbols.length : Math.min(symbols.length, DEFAULT_VISIBLE_CALLS);
|
|
249
225
|
const lines = [theme.fg("muted", `${symbols.length} reachable symbol${symbols.length === 1 ? "" : "s"}:`)];
|
|
@@ -261,7 +237,8 @@ export function formatWorkspaceMapCall(args: { path?: unknown; maxEntries?: unkn
|
|
|
261
237
|
}
|
|
262
238
|
|
|
263
239
|
export function formatWorkspaceMapResult(result: WorkspaceMapResult | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
264
|
-
if (!result || result.entries.length === 0)
|
|
240
|
+
if (!result || result.entries.length === 0)
|
|
241
|
+
return theme.fg("dim", "No ranked symbols (the workspace's symbol graph may still be populating in the background -- retry shortly).");
|
|
265
242
|
|
|
266
243
|
const displayCount = expanded ? result.entries.length : Math.min(result.entries.length, DEFAULT_VISIBLE_SYMBOLS);
|
|
267
244
|
const lines = [
|
package/extension/src/index.ts
CHANGED
|
@@ -9,14 +9,12 @@ import type {
|
|
|
9
9
|
GithubRepoSearchResult,
|
|
10
10
|
Hover,
|
|
11
11
|
IntelligenceProvenance,
|
|
12
|
-
JobSnapshot,
|
|
13
12
|
LineEdit,
|
|
14
13
|
LineEditOutcome,
|
|
15
14
|
MutationHistoryEntry,
|
|
16
15
|
NpmPackageCandidate,
|
|
17
16
|
OperationOutputs,
|
|
18
17
|
PackageSourceOperationResult,
|
|
19
|
-
PopulateSymbolGraphResult,
|
|
20
18
|
RepoFetchResult,
|
|
21
19
|
SourcegraphCodeCandidate,
|
|
22
20
|
SymbolAnnotation,
|
|
@@ -27,6 +25,7 @@ import type {
|
|
|
27
25
|
WorkspaceMapResult,
|
|
28
26
|
WorkspaceQueryOutcome,
|
|
29
27
|
} from "@danypops/lector";
|
|
28
|
+
import { DEFAULT_EXTERNAL_SEARCH_MAX_RESULTS } from "@danypops/lector";
|
|
30
29
|
import {
|
|
31
30
|
type AgentToolResult,
|
|
32
31
|
createEditToolDefinition,
|
|
@@ -41,7 +40,6 @@ import { formatApplyPatchCall, formatApplyPatchResult } from "./apply-patch-rend
|
|
|
41
40
|
import { createLectorCodeIntelligenceOperations } from "./code-intelligence-operations.ts";
|
|
42
41
|
import {
|
|
43
42
|
type CallHierarchyToolDetails,
|
|
44
|
-
describePopulateSymbolGraphJob,
|
|
45
43
|
formatCallHierarchyCall,
|
|
46
44
|
formatCallHierarchyResult,
|
|
47
45
|
formatDiagnosticsCall,
|
|
@@ -56,8 +54,6 @@ import {
|
|
|
56
54
|
formatGoToImplementationResult,
|
|
57
55
|
formatHoverCall,
|
|
58
56
|
formatHoverResult,
|
|
59
|
-
formatPopulateSymbolGraphCall,
|
|
60
|
-
formatPopulateSymbolGraphResult,
|
|
61
57
|
formatReachableFromCall,
|
|
62
58
|
formatReachableFromResult,
|
|
63
59
|
formatWorkspaceMapCall,
|
|
@@ -83,7 +79,7 @@ import { setNewWorkspaceObserver } from "./lector-client.ts";
|
|
|
83
79
|
import { createLectorLineEditOperations } from "./line-edit-operations.ts";
|
|
84
80
|
import { formatLineEditCall, formatLineEditResult } from "./line-edit-rendering.ts";
|
|
85
81
|
import { createMutationHistoryOperations } from "./mutation-history-operations.ts";
|
|
86
|
-
import { nearestGitRoot } from "./nearest-workspace-root.ts";
|
|
82
|
+
import { isFilesystemRoot, nearestGitRoot } from "./nearest-workspace-root.ts";
|
|
87
83
|
import { createLectorPackageSourceOperations } from "./package-source-operations.ts";
|
|
88
84
|
import { formatPackageSourceCall, formatPackageSourceResult } from "./package-source-rendering.ts";
|
|
89
85
|
import { createLectorReadOperations } from "./read-operations.ts";
|
|
@@ -176,8 +172,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
176
172
|
uiContext.ui.setStatus("lector-cache", uiContext.ui.theme.fg(worst, `Lector: ${summary}`));
|
|
177
173
|
}
|
|
178
174
|
|
|
179
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* Starts (or restarts, on a stale generation) monitoring one workspace root's cache
|
|
177
|
+
* lifecycle -- shared by session_start's own cwd root and every later root a tool call
|
|
178
|
+
* first touches. Refuses a bare filesystem root outright: workspaceForPath's own
|
|
179
|
+
* intentional fallback for a raw read/write of a file outside any git repo can register
|
|
180
|
+
* exactly this as a "new workspace", and auto-populating it would attempt a full
|
|
181
|
+
* filesystem-wide symbol-graph scan -- confirmed live as a real, previously-shipped bug.
|
|
182
|
+
*/
|
|
180
183
|
function startMonitoringRoot(root: string, ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1]): void {
|
|
184
|
+
if (isFilesystemRoot(root)) return;
|
|
181
185
|
if (monitoringRoots.has(root)) return;
|
|
182
186
|
monitoringRoots.add(root);
|
|
183
187
|
const thisGeneration = sessionGeneration;
|
|
@@ -614,87 +618,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
614
618
|
},
|
|
615
619
|
});
|
|
616
620
|
|
|
617
|
-
pi.registerTool({
|
|
618
|
-
name: "populate_symbol_graph",
|
|
619
|
-
label: "Populate Symbol Graph",
|
|
620
|
-
description:
|
|
621
|
-
"Walk a workspace's real call relationships into a persisted graph, so reachable_from can answer multi-hop questions (transitive callers, reachability) without chaining many find_references/call_hierarchy calls by hand. Run this once before reachable_from.",
|
|
622
|
-
promptSnippet: "Populate a workspace's symbol graph for multi-hop queries",
|
|
623
|
-
promptGuidelines: [
|
|
624
|
-
"Run populate_symbol_graph once for a workspace before using reachable_from against it; an unpopulated workspace's graph is empty, not an error.",
|
|
625
|
-
"populate_symbol_graph waits briefly, then returns a job id with an explicit still-loading state instead of blocking the turn. Use job_status later; do not spin in a blind polling loop.",
|
|
626
|
-
"maxFiles and maxSymbolsPerFile are both required and bound the scan explicitly -- a symbol-dense file (many interfaces/properties) can easily exceed a small maxSymbolsPerFile before reaching the functions/methods that actually matter.",
|
|
627
|
-
],
|
|
628
|
-
parameters: Type.Object({
|
|
629
|
-
path: Type.String({ description: "Any absolute or cwd-relative path inside the workspace to populate" }),
|
|
630
|
-
maxFiles: Type.Number({ description: "Maximum number of source files to scan" }),
|
|
631
|
-
maxSymbolsPerFile: Type.Number({ description: "Maximum number of declarations to process per file" }),
|
|
632
|
-
initialWaitMs: Type.Optional(Type.Number({ description: "Bounded initial wait before returning a still-loading job; defaults to 500, maximum 30000" })),
|
|
633
|
-
}),
|
|
634
|
-
async execute(_toolCallId, params) {
|
|
635
|
-
const path = resolve(cwd, params.path);
|
|
636
|
-
const job = await codeIntelligenceOperations.populateSymbolGraph(path, params.maxFiles, params.maxSymbolsPerFile, params.initialWaitMs);
|
|
637
|
-
return {
|
|
638
|
-
content: [{ type: "text", text: describePopulateSymbolGraphJob(job) }],
|
|
639
|
-
details: { job },
|
|
640
|
-
};
|
|
641
|
-
},
|
|
642
|
-
renderCall(args, theme, context) {
|
|
643
|
-
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
644
|
-
text.setText(formatPopulateSymbolGraphCall(args, theme));
|
|
645
|
-
return text;
|
|
646
|
-
},
|
|
647
|
-
renderResult(result, { isPartial }, theme, context) {
|
|
648
|
-
if (isPartial) return new Text(theme.fg("warning", "Populating symbol graph..."), 0, 0);
|
|
649
|
-
if (context.isError) {
|
|
650
|
-
const errorText = result.content
|
|
651
|
-
.filter((block) => block.type === "text")
|
|
652
|
-
.map((block) => block.text)
|
|
653
|
-
.join("\n");
|
|
654
|
-
return new Text(theme.fg("error", errorText || "populate_symbol_graph failed"), 0, 0);
|
|
655
|
-
}
|
|
656
|
-
const details = result.details as { job?: JobSnapshot<PopulateSymbolGraphResult> } | undefined;
|
|
657
|
-
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
658
|
-
text.setText(formatPopulateSymbolGraphResult(details?.job, theme));
|
|
659
|
-
return text;
|
|
660
|
-
},
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
pi.registerTool({
|
|
664
|
-
name: "job_status",
|
|
665
|
-
label: "Job Status",
|
|
666
|
-
description:
|
|
667
|
-
"Poll one process-lifetime Lector background job. Returns queued/running with an actionable still-loading state, succeeded with the bounded result, or failed with a stable error code and message. Jobs are bounded and do not survive daemon restart; an unknown id explains expiry/restart rather than returning empty data.",
|
|
668
|
-
promptSnippet: "Poll a Lector background job by id",
|
|
669
|
-
parameters: Type.Object({
|
|
670
|
-
jobId: Type.String({ description: "Job id returned by populate_symbol_graph" }),
|
|
671
|
-
}),
|
|
672
|
-
async execute(_toolCallId, params) {
|
|
673
|
-
const job = await codeIntelligenceOperations.jobStatus(params.jobId);
|
|
674
|
-
return { content: [{ type: "text", text: describePopulateSymbolGraphJob(job) }], details: { job } };
|
|
675
|
-
},
|
|
676
|
-
renderCall(args, theme, context) {
|
|
677
|
-
const jobId = typeof args.jobId === "string" ? args.jobId : "";
|
|
678
|
-
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
679
|
-
text.setText(`${theme.fg("toolTitle", theme.bold("job_status"))} ${theme.fg("accent", jobId)}`);
|
|
680
|
-
return text;
|
|
681
|
-
},
|
|
682
|
-
renderResult(result, { isPartial }, theme, context) {
|
|
683
|
-
if (isPartial) return new Text(theme.fg("warning", "Checking background job..."), 0, 0);
|
|
684
|
-
if (context.isError) {
|
|
685
|
-
const errorText = result.content
|
|
686
|
-
.filter((block) => block.type === "text")
|
|
687
|
-
.map((block) => block.text)
|
|
688
|
-
.join("\n");
|
|
689
|
-
return new Text(theme.fg("error", errorText || "job_status failed"), 0, 0);
|
|
690
|
-
}
|
|
691
|
-
const details = result.details as { job?: JobSnapshot<PopulateSymbolGraphResult> } | undefined;
|
|
692
|
-
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
693
|
-
text.setText(formatPopulateSymbolGraphResult(details?.job, theme));
|
|
694
|
-
return text;
|
|
695
|
-
},
|
|
696
|
-
});
|
|
697
|
-
|
|
698
621
|
pi.registerTool({
|
|
699
622
|
name: "reference_based_rename",
|
|
700
623
|
label: "Reference-Based Rename",
|
|
@@ -702,7 +625,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
702
625
|
"Move/rename a file and rewrite every static import/export specifier the workspace's own populated symbol graph knows references it -- atomically, rolled back entirely on any failure. Non-LSP: uses find_references + a real parse of import/export declarations, not a language server's own rename. Refuses outright (touches nothing) unless the workspace's symbol graph is fully populated and current for the given bounds -- a partial rename that silently misses a reference is worse than refusing (Sourcegraph's CodeScaleBench finding). Does not follow dynamic import(expr)/require(expr) or any plain string reference to the file -- always check the returned caveats.",
|
|
703
626
|
promptSnippet: "Move a file and update every import that references it",
|
|
704
627
|
promptGuidelines: [
|
|
705
|
-
"
|
|
628
|
+
"The workspace's symbol graph auto-populates in the background (default bounds: 500 files, 100 symbols/file) the first time this workspace is touched. If this refuses because the graph isn't populated at the requested maxFiles/maxSymbolsPerFile, run `lector workspace populate-symbol-graph <path> --max-files <n> --max-symbols-per-file <n>` via bash for a larger scan, then retry.",
|
|
706
629
|
"Always read the returned caveats: this never rewrites a dynamic import(expr)/require(expr) or a plain string reference to the old path, even if one exists.",
|
|
707
630
|
],
|
|
708
631
|
parameters: Type.Object({
|
|
@@ -831,10 +754,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
831
754
|
name: "symbol_annotations",
|
|
832
755
|
label: "Symbol Annotations",
|
|
833
756
|
description:
|
|
834
|
-
'Agent-authored narrative content anchored to one or more symbols in the workspace\'s persisted graph -- e.g. a "user story dataflow" note spanning every symbol touched end-to-end. Every anchor must resolve to a real, currently-known symbol (
|
|
757
|
+
'Agent-authored narrative content anchored to one or more symbols in the workspace\'s persisted graph -- e.g. a "user story dataflow" note spanning every symbol touched end-to-end. Every anchor must resolve to a real, currently-known symbol (the workspace\'s symbol graph auto-populates in the background on first touch). get/list/tree live-check staleness against the current graph/workspace on every call and persist a correction before returning, so a returned status never disagrees with reality -- a stale annotation must be refreshed (re-authored and re-anchored) or scrubbed (soft-deleted, restorable) by an explicit decision; Lector never rewrites the narrative itself. contain/uncontain build a reusable, nestable structure on top of plain annotations: a container (e.g. a "data flow") can contain other annotations -- including per-symbol notes shared by more than one container (DRY reuse) or another container one level deeper (nested data flows) -- without duplicating their content. tree reads a whole bounded subtree in one call. Actions: create, get, list, refresh, scrub, restore, contain, uncontain, tree.',
|
|
835
758
|
promptSnippet: "Attach, read, or invalidate narrative annotations on the symbol graph",
|
|
836
759
|
promptGuidelines: [
|
|
837
|
-
"Resolve real anchor positions first (find_symbols/document_symbols/go_to_definition) -- an anchor position must match
|
|
760
|
+
"Resolve real anchor positions first (find_symbols/document_symbols/go_to_definition) -- an anchor position must match the workspace's own symbol graph's recorded position for that symbol, not just any occurrence of its name.",
|
|
838
761
|
"A stale annotation's body may no longer describe the code accurately -- read it, decide whether to refresh (re-author) or scrub (remove), never trust it as-is.",
|
|
839
762
|
"Prefer reusing an existing per-symbol annotation as a shared child of several containers over re-authoring the same explanation in each -- that reuse is the reason contain/uncontain exist.",
|
|
840
763
|
"contain/uncontain are idempotent (containing an already-contained child, or uncontaining an already-absent relationship, is a no-op, not an error) and reject a cycle up front rather than accepting one.",
|
|
@@ -996,7 +919,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
996
919
|
name: "reachable_from",
|
|
997
920
|
label: "Reachable From",
|
|
998
921
|
description:
|
|
999
|
-
"Every symbol reachable from an exact file position by following the workspace's persisted call graph up to maxDepth hops -- transitive callers/reachability that would otherwise require chaining many find_references/call_hierarchy calls by hand.
|
|
922
|
+
"Every symbol reachable from an exact file position by following the workspace's persisted call graph up to maxDepth hops -- transitive callers/reachability that would otherwise require chaining many find_references/call_hierarchy calls by hand. The workspace's symbol graph auto-populates in the background the first time this workspace is touched; if it's still building, this returns an empty result rather than an error -- wait a moment and retry.",
|
|
1000
923
|
promptSnippet: "Find symbols reachable from a position, up to N hops, via the persisted graph",
|
|
1001
924
|
promptGuidelines: [
|
|
1002
925
|
"Use reachable_from for multi-hop questions (does A eventually call C through B); use call_hierarchy (direction=incoming/outgoing) for a single direct hop live against the language server.",
|
|
@@ -1044,7 +967,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1044
967
|
name: "workspace_map",
|
|
1045
968
|
label: "Workspace Map",
|
|
1046
969
|
description:
|
|
1047
|
-
"A ranked, budget-bounded summary of the workspace's most structurally central symbols (aider-repomap-shaped) -- signature-only, highest-ranked first by PageRank over the populated call/reference graph, not full file dumps. Use when orienting in an unfamiliar or large codebase instead of reading many files one by one.
|
|
970
|
+
"A ranked, budget-bounded summary of the workspace's most structurally central symbols (aider-repomap-shaped) -- signature-only, highest-ranked first by PageRank over the populated call/reference graph, not full file dumps. Use when orienting in an unfamiliar or large codebase instead of reading many files one by one. The workspace's symbol graph auto-populates in the background the first time this workspace is touched; if it's still building, this returns empty rather than an error -- wait a moment and retry.",
|
|
1048
971
|
promptSnippet: "Get a ranked, signature-only overview of the workspace's most central symbols",
|
|
1049
972
|
promptGuidelines: [
|
|
1050
973
|
"Prefer this over reading many files to get oriented in a large or unfamiliar codebase -- it surfaces the most-referenced symbols first, not an arbitrary file order.",
|
|
@@ -1062,7 +985,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1062
985
|
const result = await codeIntelligenceOperations.workspaceMap(path, params.maxNodes, params.maxEdges, params.maxEntries, params.maxBytes);
|
|
1063
986
|
const text =
|
|
1064
987
|
result.entries.length === 0
|
|
1065
|
-
? "No ranked symbols (
|
|
988
|
+
? "No ranked symbols (the workspace's symbol graph may still be populating in the background -- retry shortly)."
|
|
1066
989
|
: result.entries
|
|
1067
990
|
.map(
|
|
1068
991
|
(entry) => `${entry.kind} ${entry.name} -- ${entry.path}:${entry.line}:${entry.character}${entry.signature ? ` -- ${entry.signature}` : ""}`,
|
|
@@ -1550,7 +1473,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1550
1473
|
maxResults: Type.Optional(Type.Number({ description: "Maximum candidates to return (default 20)" })),
|
|
1551
1474
|
}),
|
|
1552
1475
|
async execute(_toolCallId, params): Promise<AgentToolResult<ExternalSearchToolDetails>> {
|
|
1553
|
-
const maxResults = params.maxResults ??
|
|
1476
|
+
const maxResults = params.maxResults ?? DEFAULT_EXTERNAL_SEARCH_MAX_RESULTS;
|
|
1554
1477
|
if (params.action === "github_repos") {
|
|
1555
1478
|
const result = await externalSearchOperations.githubRepos(params.query, maxResults);
|
|
1556
1479
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "github_repos", result } };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, extname, parse } from "node:path";
|
|
3
|
-
import { createRetryingClient, type RetryingClient } from "@danypops/daemon-kit/pi-client";
|
|
4
3
|
import {
|
|
5
4
|
connectLectorClient,
|
|
6
5
|
descriptorForExtension,
|
|
@@ -11,6 +10,7 @@ import {
|
|
|
11
10
|
remoteErrorIs,
|
|
12
11
|
type WorkspaceId,
|
|
13
12
|
} from "@danypops/lector";
|
|
13
|
+
import { createRetryingClient, type RetryingClient } from "@danypops/vehicle-client/daemon-client";
|
|
14
14
|
import { nearestGitRoot, nearestProjectRoot } from "./nearest-workspace-root.ts";
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -2,39 +2,60 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join, parse } from "node:path";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* The
|
|
6
|
-
* a
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* This -- not a Pi session's original cwd -- is Lector's real workspace
|
|
15
|
-
* granularity. A session routinely touches many unrelated repos, sibling
|
|
16
|
-
* projects, and scratch paths in one run; pi's built-in read/write/edit
|
|
17
|
-
* tools have never restricted which absolute path can be touched, and
|
|
18
|
-
* Lector must not either. (Real, shipped bug this fixes: read/write/edit
|
|
19
|
-
* hard-locked to whatever directory the session happened to start in,
|
|
20
|
-
* refusing every legitimate path outside it with a "Lector-registered
|
|
21
|
-
* workspace root" error -- discovered live, in a separate session, working
|
|
22
|
-
* against a completely different, unrelated repository.)
|
|
5
|
+
* The bare filesystem root is never a legitimate discovered project root, even if it happens
|
|
6
|
+
* to contain a marker file (a stray `git init /`, a leftover `package.json`) -- matches the
|
|
7
|
+
* same convention already established elsewhere in this house (oculus/survey/rust_scanner.go's
|
|
8
|
+
* findCrateRoot, oculus/locator/match.go's effectiveParent: reaching "/" during a walk-up means
|
|
9
|
+
* "not found", never "found here"). Confirmed live: without this, a Lector daemon registered
|
|
10
|
+
* "/" as a workspace and a background job attempted to symbol-graph the entire filesystem.
|
|
11
|
+
* `exists` is injectable so a test can simulate "a marker exists at the filesystem root"
|
|
12
|
+
* without ever touching the real one.
|
|
23
13
|
*/
|
|
24
|
-
function walkUpForMarkers(startDirectory: string, markers: readonly string[]): string | undefined {
|
|
14
|
+
function walkUpForMarkers(startDirectory: string, markers: readonly string[], exists: (path: string) => boolean = existsSync): string | undefined {
|
|
25
15
|
let dir = startDirectory;
|
|
26
16
|
const fsRoot = parse(dir).root;
|
|
27
17
|
while (dir !== fsRoot) {
|
|
28
|
-
if (markers.some((marker) =>
|
|
18
|
+
if (markers.some((marker) => exists(join(dir, marker)))) return dir;
|
|
29
19
|
const parent = dirname(dir);
|
|
30
20
|
if (parent === dir) break; // defensive: dirname must be strictly ascending
|
|
31
21
|
dir = parent;
|
|
32
22
|
}
|
|
33
|
-
return
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The nearest enclosing git repository root starting from (and including) a given directory,
|
|
28
|
+
* or undefined if none is found (e.g. /tmp scratch files, dotfiles outside any repo, or the
|
|
29
|
+
* walk reaching the filesystem root without a match). Callers choose their own fallback -- see
|
|
30
|
+
* lector-client.ts's workspaceForPath (falls back to the filesystem root: any absolute path is
|
|
31
|
+
* fair game for read/write/edit, exactly as Pi's built-in tools already allow) vs.
|
|
32
|
+
* workspaceForDirectory (falls back to the directory itself: widening a symbol-search scope all
|
|
33
|
+
* the way to the entire filesystem when a project isn't a git repo would be absurd).
|
|
34
|
+
*
|
|
35
|
+
* This -- not a Pi session's original cwd -- is Lector's real workspace granularity. A session
|
|
36
|
+
* routinely touches many unrelated repos, sibling projects, and scratch paths in one run; Pi's
|
|
37
|
+
* built-in read/write/edit tools have never restricted which absolute path can be touched, and
|
|
38
|
+
* Lector must not either. (Real, shipped bug this fixes: read/write/edit hard-locked to
|
|
39
|
+
* whatever directory the session happened to start in, refusing every legitimate path outside
|
|
40
|
+
* it with a "Lector-registered workspace root" error -- discovered live, in a separate session,
|
|
41
|
+
* working against a completely different, unrelated repository.)
|
|
42
|
+
*
|
|
43
|
+
* `exists` is injectable for tests -- see walkUpForMarkers.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* True for the bare filesystem root itself ("/" on Linux/macOS, "C:\\" on Windows) -- the one
|
|
47
|
+
* path a caller must never treat as a real project to auto-index. workspaceForPath's own
|
|
48
|
+
* intentional fallback for a raw read/write of a file outside any git repo can still produce
|
|
49
|
+
* this value; callers that trigger background work (auto-population, cache monitoring) off a
|
|
50
|
+
* newly-registered workspace must check this explicitly rather than assuming
|
|
51
|
+
* nearestGitRoot/nearestProjectRoot are the only paths that can hand them a workspace root.
|
|
52
|
+
*/
|
|
53
|
+
export function isFilesystemRoot(path: string): boolean {
|
|
54
|
+
return parse(path).root === path;
|
|
34
55
|
}
|
|
35
56
|
|
|
36
|
-
export function nearestGitRoot(startDirectory: string): string | undefined {
|
|
37
|
-
return walkUpForMarkers(startDirectory, [".git"]);
|
|
57
|
+
export function nearestGitRoot(startDirectory: string, exists: (path: string) => boolean = existsSync): string | undefined {
|
|
58
|
+
return walkUpForMarkers(startDirectory, [".git"], exists);
|
|
38
59
|
}
|
|
39
60
|
|
|
40
61
|
/**
|
|
@@ -45,6 +66,6 @@ export function nearestGitRoot(startDirectory: string): string | undefined {
|
|
|
45
66
|
* misattributes its whole project to the repo root, handing the language server the wrong
|
|
46
67
|
* rootUri (and, for TypeScript, the wrong tsconfig.json) even though a closer one exists.
|
|
47
68
|
*/
|
|
48
|
-
export function nearestProjectRoot(startDirectory: string, rootMarkers: readonly string[]): string | undefined {
|
|
49
|
-
return walkUpForMarkers(startDirectory, [...rootMarkers, ".git"]);
|
|
69
|
+
export function nearestProjectRoot(startDirectory: string, rootMarkers: readonly string[], exists: (path: string) => boolean = existsSync): string | undefined {
|
|
70
|
+
return walkUpForMarkers(startDirectory, [...rootMarkers, ".git"], exists);
|
|
50
71
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-lector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Pi host adapter for Lector: overrides read/write/edit with a daemon-backed, hash-guarded filesystem",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"typebox": "*"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@danypops/
|
|
22
|
-
"@danypops/lector": "^0.
|
|
21
|
+
"@danypops/vehicle-client": "^0.1.1",
|
|
22
|
+
"@danypops/lector": "^0.9.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@earendil-works/pi-ai": "^0.81.1",
|