@danypops/pi-lector 0.7.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 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
- `populate_symbol_graph` submits bounded background work and waits briefly. If the
16
- graph is still loading, it returns a job id immediately; `job_status` polls that id
17
- later without forcing the agent into a blocking or blind polling loop.
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; completion also emits a one-shot notification. Session shutdown
22
- stops polling, and the agent receives each state transition once in its context.
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) return theme.fg("dim", "Nothing reachable at this position (has the graph been populated for this workspace?).");
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) return theme.fg("dim", "No ranked symbols (has the graph been populated for this workspace?).");
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 = [
@@ -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,
@@ -42,7 +40,6 @@ import { formatApplyPatchCall, formatApplyPatchResult } from "./apply-patch-rend
42
40
  import { createLectorCodeIntelligenceOperations } from "./code-intelligence-operations.ts";
43
41
  import {
44
42
  type CallHierarchyToolDetails,
45
- describePopulateSymbolGraphJob,
46
43
  formatCallHierarchyCall,
47
44
  formatCallHierarchyResult,
48
45
  formatDiagnosticsCall,
@@ -57,8 +54,6 @@ import {
57
54
  formatGoToImplementationResult,
58
55
  formatHoverCall,
59
56
  formatHoverResult,
60
- formatPopulateSymbolGraphCall,
61
- formatPopulateSymbolGraphResult,
62
57
  formatReachableFromCall,
63
58
  formatReachableFromResult,
64
59
  formatWorkspaceMapCall,
@@ -623,87 +618,6 @@ export default function (pi: ExtensionAPI) {
623
618
  },
624
619
  });
625
620
 
626
- pi.registerTool({
627
- name: "populate_symbol_graph",
628
- label: "Populate Symbol Graph",
629
- description:
630
- "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.",
631
- promptSnippet: "Populate a workspace's symbol graph for multi-hop queries",
632
- promptGuidelines: [
633
- "Run populate_symbol_graph once for a workspace before using reachable_from against it; an unpopulated workspace's graph is empty, not an error.",
634
- "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.",
635
- "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.",
636
- ],
637
- parameters: Type.Object({
638
- path: Type.String({ description: "Any absolute or cwd-relative path inside the workspace to populate" }),
639
- maxFiles: Type.Number({ description: "Maximum number of source files to scan" }),
640
- maxSymbolsPerFile: Type.Number({ description: "Maximum number of declarations to process per file" }),
641
- initialWaitMs: Type.Optional(Type.Number({ description: "Bounded initial wait before returning a still-loading job; defaults to 500, maximum 30000" })),
642
- }),
643
- async execute(_toolCallId, params) {
644
- const path = resolve(cwd, params.path);
645
- const job = await codeIntelligenceOperations.populateSymbolGraph(path, params.maxFiles, params.maxSymbolsPerFile, params.initialWaitMs);
646
- return {
647
- content: [{ type: "text", text: describePopulateSymbolGraphJob(job) }],
648
- details: { job },
649
- };
650
- },
651
- renderCall(args, theme, context) {
652
- const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
653
- text.setText(formatPopulateSymbolGraphCall(args, theme));
654
- return text;
655
- },
656
- renderResult(result, { isPartial }, theme, context) {
657
- if (isPartial) return new Text(theme.fg("warning", "Populating symbol graph..."), 0, 0);
658
- if (context.isError) {
659
- const errorText = result.content
660
- .filter((block) => block.type === "text")
661
- .map((block) => block.text)
662
- .join("\n");
663
- return new Text(theme.fg("error", errorText || "populate_symbol_graph failed"), 0, 0);
664
- }
665
- const details = result.details as { job?: JobSnapshot<PopulateSymbolGraphResult> } | undefined;
666
- const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
667
- text.setText(formatPopulateSymbolGraphResult(details?.job, theme));
668
- return text;
669
- },
670
- });
671
-
672
- pi.registerTool({
673
- name: "job_status",
674
- label: "Job Status",
675
- description:
676
- "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.",
677
- promptSnippet: "Poll a Lector background job by id",
678
- parameters: Type.Object({
679
- jobId: Type.String({ description: "Job id returned by populate_symbol_graph" }),
680
- }),
681
- async execute(_toolCallId, params) {
682
- const job = await codeIntelligenceOperations.jobStatus(params.jobId);
683
- return { content: [{ type: "text", text: describePopulateSymbolGraphJob(job) }], details: { job } };
684
- },
685
- renderCall(args, theme, context) {
686
- const jobId = typeof args.jobId === "string" ? args.jobId : "";
687
- const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
688
- text.setText(`${theme.fg("toolTitle", theme.bold("job_status"))} ${theme.fg("accent", jobId)}`);
689
- return text;
690
- },
691
- renderResult(result, { isPartial }, theme, context) {
692
- if (isPartial) return new Text(theme.fg("warning", "Checking background job..."), 0, 0);
693
- if (context.isError) {
694
- const errorText = result.content
695
- .filter((block) => block.type === "text")
696
- .map((block) => block.text)
697
- .join("\n");
698
- return new Text(theme.fg("error", errorText || "job_status failed"), 0, 0);
699
- }
700
- const details = result.details as { job?: JobSnapshot<PopulateSymbolGraphResult> } | undefined;
701
- const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
702
- text.setText(formatPopulateSymbolGraphResult(details?.job, theme));
703
- return text;
704
- },
705
- });
706
-
707
621
  pi.registerTool({
708
622
  name: "reference_based_rename",
709
623
  label: "Reference-Based Rename",
@@ -711,7 +625,7 @@ export default function (pi: ExtensionAPI) {
711
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.",
712
626
  promptSnippet: "Move a file and update every import that references it",
713
627
  promptGuidelines: [
714
- "Run populate_symbol_graph for this workspace first if cache_status/has_warm_index doesn't already show a fully cached (never partial) graph -- reference_based_rename refuses outright otherwise.",
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.",
715
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.",
716
630
  ],
717
631
  parameters: Type.Object({
@@ -840,10 +754,10 @@ export default function (pi: ExtensionAPI) {
840
754
  name: "symbol_annotations",
841
755
  label: "Symbol Annotations",
842
756
  description:
843
- '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 (run populate_symbol_graph first). 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.',
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.',
844
758
  promptSnippet: "Attach, read, or invalidate narrative annotations on the symbol graph",
845
759
  promptGuidelines: [
846
- "Resolve real anchor positions first (find_symbols/document_symbols/go_to_definition) -- an anchor position must match populate_symbol_graph's own recorded position for that symbol, not just any occurrence of its name.",
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.",
847
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.",
848
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.",
849
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.",
@@ -1005,7 +919,7 @@ export default function (pi: ExtensionAPI) {
1005
919
  name: "reachable_from",
1006
920
  label: "Reachable From",
1007
921
  description:
1008
- "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. Requires populate_symbol_graph to have been run for this workspace first.",
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.",
1009
923
  promptSnippet: "Find symbols reachable from a position, up to N hops, via the persisted graph",
1010
924
  promptGuidelines: [
1011
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.",
@@ -1053,7 +967,7 @@ export default function (pi: ExtensionAPI) {
1053
967
  name: "workspace_map",
1054
968
  label: "Workspace Map",
1055
969
  description:
1056
- "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. Requires populate_symbol_graph to have been run for this workspace first; returns empty otherwise.",
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.",
1057
971
  promptSnippet: "Get a ranked, signature-only overview of the workspace's most central symbols",
1058
972
  promptGuidelines: [
1059
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.",
@@ -1071,7 +985,7 @@ export default function (pi: ExtensionAPI) {
1071
985
  const result = await codeIntelligenceOperations.workspaceMap(path, params.maxNodes, params.maxEdges, params.maxEntries, params.maxBytes);
1072
986
  const text =
1073
987
  result.entries.length === 0
1074
- ? "No ranked symbols (has the graph been populated for this workspace?)."
988
+ ? "No ranked symbols (the workspace's symbol graph may still be populating in the background -- retry shortly)."
1075
989
  : result.entries
1076
990
  .map(
1077
991
  (entry) => `${entry.kind} ${entry.name} -- ${entry.path}:${entry.line}:${entry.character}${entry.signature ? ` -- ${entry.signature}` : ""}`,
@@ -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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-lector",
3
- "version": "0.7.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/daemon-kit": "^0.22.1",
22
- "@danypops/lector": "^0.8.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",