@danypops/pi-lector 0.1.7 → 0.1.9

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
@@ -8,6 +8,10 @@ daemon-backed, hash-guarded filesystem. Requires a running Lector daemon
8
8
  pi install npm:@danypops/pi-lector
9
9
  ```
10
10
 
11
+ `package_source` resolves an installed npm package through its lockfile and registry metadata, verifies an exact Git commit, and registers the package source read-only for `search_code`, `find_symbols`, and the semantic tools.
12
+
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
+
11
15
  `populate_symbol_graph` submits bounded background work and waits briefly. If the
12
16
  graph is still loading, it returns a job id immediately; `job_status` polls that id
13
17
  later without forcing the agent into a blocking or blind polling loop.
@@ -1,16 +1,4 @@
1
- import type {
2
- CallHierarchyEntry,
3
- Diagnostic,
4
- DocumentSymbolEntry,
5
- Hover,
6
- IncomingCall,
7
- JobSnapshot,
8
- OutgoingCall,
9
- PopulateSymbolGraphResult,
10
- SymbolEdgeKind,
11
- SymbolNode,
12
- WorkspaceLocation,
13
- } from "@danypops/lector";
1
+ import type { JobSnapshot, OperationOutputs, PopulateSymbolGraphResult, SymbolEdgeKind, SymbolNode } from "@danypops/lector";
14
2
  import { lectorClient, withWorkspace, workspaceForCodeIntelligencePath } from "./lector-client.ts";
15
3
 
16
4
  /**
@@ -27,15 +15,15 @@ import { lectorClient, withWorkspace, workspaceForCodeIntelligencePath } from ".
27
15
  * unlike read/write/edit) -- never a value captured once at session start.
28
16
  */
29
17
  export interface CodeIntelligenceOperations {
30
- goToDefinition(path: string, line: number, character: number): Promise<readonly WorkspaceLocation[]>;
31
- goToImplementation(path: string, line: number, character: number): Promise<readonly WorkspaceLocation[]>;
32
- findReferences(path: string, line: number, character: number, includeDeclaration: boolean): Promise<readonly WorkspaceLocation[]>;
33
- hover(path: string, line: number, character: number): Promise<Hover | undefined>;
34
- documentSymbols(path: string): Promise<readonly DocumentSymbolEntry[]>;
35
- diagnostics(path: string): Promise<readonly Diagnostic[]>;
36
- prepareCallHierarchy(path: string, line: number, character: number): Promise<readonly CallHierarchyEntry[]>;
37
- incomingCalls(path: string, line: number, character: number): Promise<readonly IncomingCall[]>;
38
- outgoingCalls(path: string, line: number, character: number): Promise<readonly OutgoingCall[]>;
18
+ goToDefinition(path: string, line: number, character: number): Promise<OperationOutputs["workspace.goToDefinition"]>;
19
+ goToImplementation(path: string, line: number, character: number): Promise<OperationOutputs["workspace.goToImplementation"]>;
20
+ findReferences(path: string, line: number, character: number, includeDeclaration: boolean): Promise<OperationOutputs["workspace.findReferences"]>;
21
+ hover(path: string, line: number, character: number): Promise<OperationOutputs["workspace.hover"]>;
22
+ documentSymbols(path: string): Promise<OperationOutputs["workspace.documentSymbols"]>;
23
+ diagnostics(path: string): Promise<OperationOutputs["workspace.diagnostics"]>;
24
+ prepareCallHierarchy(path: string, line: number, character: number): Promise<OperationOutputs["workspace.prepareCallHierarchy"]>;
25
+ incomingCalls(path: string, line: number, character: number): Promise<OperationOutputs["workspace.incomingCalls"]>;
26
+ outgoingCalls(path: string, line: number, character: number): Promise<OperationOutputs["workspace.outgoingCalls"]>;
39
27
  populateSymbolGraph(path: string, maxFiles: number, maxSymbolsPerFile: number, waitMs?: number): Promise<JobSnapshot<PopulateSymbolGraphResult>>;
40
28
  jobStatus(jobId: string): Promise<JobSnapshot<PopulateSymbolGraphResult>>;
41
29
  reachableFrom(path: string, line: number, character: number, maxDepth: number, kind?: SymbolEdgeKind): Promise<readonly SymbolNode[]>;
@@ -50,8 +38,7 @@ export function createLectorCodeIntelligenceOperations(): CodeIntelligenceOperat
50
38
  () => workspaceForCodeIntelligencePath(path),
51
39
  async ({ workspaceId }) => {
52
40
  const client = await lectorClient();
53
- const { locations } = await client.call("workspace.goToDefinition", { workspaceId, path, line, character });
54
- return locations;
41
+ return client.call("workspace.goToDefinition", { workspaceId, path, line, character });
55
42
  },
56
43
  );
57
44
  },
@@ -60,8 +47,7 @@ export function createLectorCodeIntelligenceOperations(): CodeIntelligenceOperat
60
47
  () => workspaceForCodeIntelligencePath(path),
61
48
  async ({ workspaceId }) => {
62
49
  const client = await lectorClient();
63
- const { locations } = await client.call("workspace.goToImplementation", { workspaceId, path, line, character });
64
- return locations;
50
+ return client.call("workspace.goToImplementation", { workspaceId, path, line, character });
65
51
  },
66
52
  );
67
53
  },
@@ -70,8 +56,7 @@ export function createLectorCodeIntelligenceOperations(): CodeIntelligenceOperat
70
56
  () => workspaceForCodeIntelligencePath(path),
71
57
  async ({ workspaceId }) => {
72
58
  const client = await lectorClient();
73
- const { locations } = await client.call("workspace.findReferences", { workspaceId, path, line, character, includeDeclaration });
74
- return locations;
59
+ return client.call("workspace.findReferences", { workspaceId, path, line, character, includeDeclaration });
75
60
  },
76
61
  );
77
62
  },
@@ -80,8 +65,7 @@ export function createLectorCodeIntelligenceOperations(): CodeIntelligenceOperat
80
65
  () => workspaceForCodeIntelligencePath(path),
81
66
  async ({ workspaceId }) => {
82
67
  const client = await lectorClient();
83
- const { hover } = await client.call("workspace.hover", { workspaceId, path, line, character });
84
- return hover;
68
+ return client.call("workspace.hover", { workspaceId, path, line, character });
85
69
  },
86
70
  );
87
71
  },
@@ -90,8 +74,7 @@ export function createLectorCodeIntelligenceOperations(): CodeIntelligenceOperat
90
74
  () => workspaceForCodeIntelligencePath(path),
91
75
  async ({ workspaceId }) => {
92
76
  const client = await lectorClient();
93
- const { symbols } = await client.call("workspace.documentSymbols", { workspaceId, path });
94
- return symbols;
77
+ return client.call("workspace.documentSymbols", { workspaceId, path });
95
78
  },
96
79
  );
97
80
  },
@@ -100,8 +83,7 @@ export function createLectorCodeIntelligenceOperations(): CodeIntelligenceOperat
100
83
  () => workspaceForCodeIntelligencePath(path),
101
84
  async ({ workspaceId }) => {
102
85
  const client = await lectorClient();
103
- const { diagnostics } = await client.call("workspace.diagnostics", { workspaceId, path });
104
- return diagnostics;
86
+ return client.call("workspace.diagnostics", { workspaceId, path });
105
87
  },
106
88
  );
107
89
  },
@@ -110,8 +92,7 @@ export function createLectorCodeIntelligenceOperations(): CodeIntelligenceOperat
110
92
  () => workspaceForCodeIntelligencePath(path),
111
93
  async ({ workspaceId }) => {
112
94
  const client = await lectorClient();
113
- const { items } = await client.call("workspace.prepareCallHierarchy", { workspaceId, path, line, character });
114
- return items;
95
+ return client.call("workspace.prepareCallHierarchy", { workspaceId, path, line, character });
115
96
  },
116
97
  );
117
98
  },
@@ -120,8 +101,7 @@ export function createLectorCodeIntelligenceOperations(): CodeIntelligenceOperat
120
101
  () => workspaceForCodeIntelligencePath(path),
121
102
  async ({ workspaceId }) => {
122
103
  const client = await lectorClient();
123
- const { calls } = await client.call("workspace.incomingCalls", { workspaceId, path, line, character });
124
- return calls;
104
+ return client.call("workspace.incomingCalls", { workspaceId, path, line, character });
125
105
  },
126
106
  );
127
107
  },
@@ -130,8 +110,7 @@ export function createLectorCodeIntelligenceOperations(): CodeIntelligenceOperat
130
110
  () => workspaceForCodeIntelligencePath(path),
131
111
  async ({ workspaceId }) => {
132
112
  const client = await lectorClient();
133
- const { calls } = await client.call("workspace.outgoingCalls", { workspaceId, path, line, character });
134
- return calls;
113
+ return client.call("workspace.outgoingCalls", { workspaceId, path, line, character });
135
114
  },
136
115
  );
137
116
  },
@@ -1,4 +1,4 @@
1
- import type { TextSearchResult, WorkspaceQueryOutcome, WorkspaceSymbol } from "@danypops/lector";
1
+ import type { SymbolSearchResult, TextSearchResult, WorkspaceQueryOutcome } from "@danypops/lector";
2
2
  import { lectorClient, workspaceForDirectory } from "./lector-client.ts";
3
3
 
4
4
  /**
@@ -10,11 +10,7 @@ import { lectorClient, workspaceForDirectory } from "./lector-client.ts";
10
10
  * required, same "no implicit fallback" convention as find_symbols/search_code.
11
11
  */
12
12
  export interface CrossWorkspaceSearchOperations {
13
- findSymbols(
14
- query: string,
15
- directories: readonly string[],
16
- timeoutMs?: number,
17
- ): Promise<readonly WorkspaceQueryOutcome<{ symbols: readonly WorkspaceSymbol[] }>[]>;
13
+ findSymbols(query: string, directories: readonly string[], timeoutMs?: number): Promise<readonly WorkspaceQueryOutcome<SymbolSearchResult>[]>;
18
14
  searchText(
19
15
  query: string,
20
16
  directories: readonly string[],
@@ -1,4 +1,4 @@
1
- import type { TextSearchResult, WorkspaceQueryOutcome, WorkspaceSymbol } from "@danypops/lector";
1
+ import type { SymbolSearchResult, TextSearchResult, WorkspaceQueryOutcome } from "@danypops/lector";
2
2
  import { keyHint } from "@earendil-works/pi-coding-agent";
3
3
  import type { LectorTheme } from "./lector-tui-theme.ts";
4
4
 
@@ -17,7 +17,7 @@ function formatOutcomeHeader(outcome: WorkspaceQueryOutcome<unknown>, theme: Lec
17
17
  }
18
18
 
19
19
  export function formatFindSymbolsAcrossProjectsResult(
20
- results: readonly WorkspaceQueryOutcome<{ symbols: readonly WorkspaceSymbol[] }>[] | undefined,
20
+ results: readonly WorkspaceQueryOutcome<SymbolSearchResult>[] | undefined,
21
21
  expanded: boolean,
22
22
  theme: LectorTheme,
23
23
  ): string {
@@ -26,6 +26,9 @@ export function formatFindSymbolsAcrossProjectsResult(
26
26
  for (const outcome of results) {
27
27
  lines.push(formatOutcomeHeader(outcome, theme));
28
28
  if (outcome.status !== "ready") continue;
29
+ lines.push(
30
+ theme.fg("muted", ` ${outcome.result.provenance.fidelity} via ${outcome.result.provenance.backend}${outcome.result.truncated ? " (truncated)" : ""}`),
31
+ );
29
32
  if (outcome.result.symbols.length === 0) {
30
33
  lines.push(theme.fg("dim", " no symbols matched"));
31
34
  continue;
@@ -1,4 +1,4 @@
1
- import type { WorkspaceSymbol } from "@danypops/lector";
1
+ import type { SymbolSearchResult } from "@danypops/lector";
2
2
  import { lectorClient, withWorkspace, workspaceForDirectory } from "./lector-client.ts";
3
3
 
4
4
  /**
@@ -15,7 +15,7 @@ import { lectorClient, withWorkspace, workspaceForDirectory } from "./lector-cli
15
15
  * is no implicit fallback anywhere in this module.
16
16
  */
17
17
  export interface FindSymbolsOperations {
18
- findSymbols(query: string, directory: string): Promise<readonly WorkspaceSymbol[]>;
18
+ findSymbols(query: string, directory: string): Promise<SymbolSearchResult>;
19
19
  }
20
20
 
21
21
  export function createLectorFindSymbolsOperations(): FindSymbolsOperations {
@@ -25,8 +25,7 @@ export function createLectorFindSymbolsOperations(): FindSymbolsOperations {
25
25
  () => workspaceForDirectory(directory),
26
26
  async ({ workspaceId }) => {
27
27
  const client = await lectorClient();
28
- const { symbols } = await client.call("workspace.findSymbols", { workspaceId, query });
29
- return symbols;
28
+ return client.call("workspace.findSymbols", { workspaceId, query });
30
29
  },
31
30
  );
32
31
  },
@@ -1,4 +1,4 @@
1
- import type { WorkspaceSymbol } from "@danypops/lector";
1
+ import type { SymbolSearchResult, WorkspaceSymbol } from "@danypops/lector";
2
2
  import { keyHint } from "@earendil-works/pi-coding-agent";
3
3
  import { colorForKind, formatLocation, type LectorTheme } from "./lector-tui-theme.ts";
4
4
 
@@ -32,14 +32,17 @@ function formatSymbolLine(symbol: WorkspaceSymbol, theme: FindSymbolsTheme, kind
32
32
  return `${kind} ${name} ${location}`;
33
33
  }
34
34
 
35
- export function formatFindSymbolsResult(symbols: readonly WorkspaceSymbol[] | undefined, query: string, expanded: boolean, theme: FindSymbolsTheme): string {
36
- if (!symbols || symbols.length === 0) {
37
- return theme.fg("dim", `No symbols found matching "${query}".`);
35
+ export function formatFindSymbolsResult(result: SymbolSearchResult | undefined, query: string, expanded: boolean, theme: FindSymbolsTheme): string {
36
+ if (!result) return theme.fg("dim", `No symbols found matching "${query}".`);
37
+ const { symbols, provenance, truncated } = result;
38
+ const source = `${provenance.fidelity} via ${provenance.backend}${truncated ? " (truncated)" : ""}`;
39
+ if (symbols.length === 0) {
40
+ return `${theme.fg("muted", source)}\n${theme.fg("dim", `No symbols found matching "${query}".`)}`;
38
41
  }
39
42
 
40
43
  const kindColumnWidth = Math.max(...symbols.map((symbol) => symbol.kind.length));
41
44
  const displayCount = expanded ? symbols.length : Math.min(symbols.length, DEFAULT_VISIBLE_RESULTS);
42
- const lines = [theme.fg("muted", `${symbols.length} symbol${symbols.length === 1 ? "" : "s"} matching "${query}":`)];
45
+ const lines = [theme.fg("muted", source), theme.fg("muted", `${symbols.length} symbol${symbols.length === 1 ? "" : "s"} matching "${query}":`)];
43
46
 
44
47
  for (const symbol of symbols.slice(0, displayCount)) {
45
48
  lines.push(formatSymbolLine(symbol, theme, kindColumnWidth));
@@ -8,15 +8,17 @@ import type {
8
8
  GitStatusSummary,
9
9
  Hover,
10
10
  IncomingCall,
11
+ IntelligenceProvenance,
11
12
  JobSnapshot,
12
13
  OutgoingCall,
14
+ PackageSourceOperationResult,
13
15
  PopulateSymbolGraphResult,
14
16
  RepoFetchResult,
15
17
  SymbolNode,
18
+ SymbolSearchResult,
16
19
  TextSearchResult,
17
20
  WorkspaceLocation,
18
21
  WorkspaceQueryOutcome,
19
- WorkspaceSymbol,
20
22
  } from "@danypops/lector";
21
23
  import { createEditToolDefinition, createReadToolDefinition, createWriteToolDefinition, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
22
24
  import { Text } from "@earendil-works/pi-tui";
@@ -55,14 +57,30 @@ import { formatFindSymbolsCall, formatFindSymbolsResult } from "./find-symbols-r
55
57
  import { createLectorGitOperations } from "./git-operations.ts";
56
58
  import { formatGitDiffCall, formatGitDiffResult, formatGitLogCall, formatGitLogResult, formatGitStatusCall, formatGitStatusResult } from "./git-rendering.ts";
57
59
  import { nearestGitRoot } from "./nearest-workspace-root.ts";
60
+ import { createLectorPackageSourceOperations } from "./package-source-operations.ts";
61
+ import { formatPackageSourceCall, formatPackageSourceResult } from "./package-source-rendering.ts";
58
62
  import { createLectorReadOperations } from "./read-operations.ts";
59
63
  import { createLectorRepoFetchOperations } from "./repo-fetch-operations.ts";
60
64
  import { formatRepoFetchCall, formatRepoFetchResult } from "./repo-fetch-rendering.ts";
61
65
  import { createLectorSearchOperations } from "./search-operations.ts";
62
66
  import { formatSearchCall, formatSearchResult } from "./search-rendering.ts";
63
- import { type CachePresentationState, createWorkspaceCacheOperations, monitorWorkspaceCache } from "./workspace-cache-operations.ts";
67
+ import {
68
+ type CachePresentationState,
69
+ cacheContextMessage,
70
+ createWorkspaceCacheOperations,
71
+ describeCacheState,
72
+ monitorWorkspaceCache,
73
+ } from "./workspace-cache-operations.ts";
64
74
  import { createLectorWriteOperations } from "./write-operations.ts";
65
75
 
76
+ function describeIntelligenceSource(provenance: IntelligenceProvenance): string {
77
+ return `${provenance.fidelity} via ${provenance.backend}`;
78
+ }
79
+
80
+ function renderIntelligenceSource(body: string, provenance: IntelligenceProvenance | undefined, theme: { fg(color: "muted", text: string): string }): string {
81
+ return provenance ? `${theme.fg("muted", describeIntelligenceSource(provenance))}\n${body}` : body;
82
+ }
83
+
66
84
  /**
67
85
  * pi-lector -- the thin Pi host adapter for Lector. Overrides the built-in
68
86
  * read/write/edit tools by name with Lector-backed Operations, so built-in
@@ -87,13 +105,6 @@ export default function (pi: ExtensionAPI) {
87
105
  let cacheState: CachePresentationState | undefined;
88
106
  let lastInjectedCacheState: string | undefined;
89
107
 
90
- function describeCacheState(state: CachePresentationState): string {
91
- if (state.status === "not-cached") return `not cached (${state.reason})`;
92
- if (state.status === "caching") return `caching (job ${state.jobId})`;
93
- if (state.status === "finished-caching") return `finished caching (job ${state.job.id})`;
94
- return "cached";
95
- }
96
-
97
108
  pi.on("before_agent_start", () => {
98
109
  if (!cacheState) return;
99
110
  const description = describeCacheState(cacheState);
@@ -102,7 +113,7 @@ export default function (pi: ExtensionAPI) {
102
113
  return {
103
114
  message: {
104
115
  customType: "lector-cache-status",
105
- content: `Lector workspace cache: ${description}. A caching graph is still loading; use live code-intelligence operations until it becomes cached.`,
116
+ content: cacheContextMessage(cacheState),
106
117
  display: false,
107
118
  },
108
119
  };
@@ -160,7 +171,7 @@ export default function (pi: ExtensionAPI) {
160
171
  "Search a workspace for functions, classes, interfaces, types, enums, and methods by name " +
161
172
  "(case-insensitive substring match). Returns each match's kind and file location. `directory` " +
162
173
  "selects which project to search -- pass the current working directory to search it, or any " +
163
- "other project's directory to get code intelligence there without needing to be in it.",
174
+ "other project's directory to get code intelligence there without needing to be in it. Results identify semantic language-server authority or structural compiler/parser fallback.",
164
175
  promptSnippet: "Search a workspace for a symbol (function, class, etc.) by name",
165
176
  promptGuidelines: [
166
177
  "Use find_symbols to locate where a function, class, interface, type, enum, or method is declared by name, instead of grepping for it.",
@@ -172,14 +183,16 @@ export default function (pi: ExtensionAPI) {
172
183
  }),
173
184
  async execute(_toolCallId, params) {
174
185
  const directory = resolve(cwd, params.directory);
175
- const symbols = await findSymbolsOperations.findSymbols(params.query, directory);
186
+ const result = await findSymbolsOperations.findSymbols(params.query, directory);
187
+ const { symbols, provenance, truncated } = result;
188
+ const source = `${provenance.fidelity} via ${provenance.backend}${truncated ? " (truncated)" : ""}`;
176
189
  const text =
177
190
  symbols.length === 0
178
- ? `No symbols found matching "${params.query}".`
179
- : symbols
191
+ ? `${source}\nNo symbols found matching "${params.query}".`
192
+ : `${source}\n${symbols
180
193
  .map((symbol) => `${symbol.kind} ${symbol.name} -- ${symbol.location.path}:${symbol.location.line}:${symbol.location.character}`)
181
- .join("\n");
182
- return { content: [{ type: "text", text }], details: { symbols } };
194
+ .join("\n")}`;
195
+ return { content: [{ type: "text", text }], details: result };
183
196
  },
184
197
  renderCall(args, theme, context) {
185
198
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -197,10 +210,10 @@ export default function (pi: ExtensionAPI) {
197
210
  .join("\n");
198
211
  return new Text(theme.fg("error", errorText || "find_symbols failed"), 0, 0);
199
212
  }
200
- const details = result.details as { symbols?: readonly WorkspaceSymbol[] } | undefined;
213
+ const details = result.details as SymbolSearchResult | undefined;
201
214
  const query = typeof context.args?.query === "string" ? context.args.query : "";
202
215
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
203
- text.setText(formatFindSymbolsResult(details?.symbols, query, expanded, theme));
216
+ text.setText(formatFindSymbolsResult(details, query, expanded, theme));
204
217
  return text;
205
218
  },
206
219
  });
@@ -221,9 +234,9 @@ export default function (pi: ExtensionAPI) {
221
234
  parameters: Type.Object(positionParameters),
222
235
  async execute(_toolCallId, params) {
223
236
  const path = resolve(cwd, params.path);
224
- const locations = await codeIntelligenceOperations.goToDefinition(path, params.line, params.character);
225
- const text = locations.length === 0 ? "No definition found." : locations.map((l) => `${l.path}:${l.line}:${l.character}`).join("\n");
226
- return { content: [{ type: "text", text }], details: { locations } };
237
+ const details = await codeIntelligenceOperations.goToDefinition(path, params.line, params.character);
238
+ const text = details.locations.length === 0 ? "No definition found." : details.locations.map((l) => `${l.path}:${l.line}:${l.character}`).join("\n");
239
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
227
240
  },
228
241
  renderCall(args, theme, context) {
229
242
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -239,9 +252,9 @@ export default function (pi: ExtensionAPI) {
239
252
  .join("\n");
240
253
  return new Text(theme.fg("error", errorText || "go_to_definition failed"), 0, 0);
241
254
  }
242
- const details = result.details as { locations?: readonly WorkspaceLocation[] } | undefined;
255
+ const details = result.details as { locations?: readonly WorkspaceLocation[]; provenance?: IntelligenceProvenance } | undefined;
243
256
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
244
- text.setText(formatGoToDefinitionResult(details?.locations, expanded, theme));
257
+ text.setText(renderIntelligenceSource(formatGoToDefinitionResult(details?.locations, expanded, theme), details?.provenance, theme));
245
258
  return text;
246
259
  },
247
260
  });
@@ -258,9 +271,10 @@ export default function (pi: ExtensionAPI) {
258
271
  parameters: Type.Object(positionParameters),
259
272
  async execute(_toolCallId, params) {
260
273
  const path = resolve(cwd, params.path);
261
- const locations = await codeIntelligenceOperations.goToImplementation(path, params.line, params.character);
262
- const text = locations.length === 0 ? "No implementation found." : locations.map((l) => `${l.path}:${l.line}:${l.character}`).join("\n");
263
- return { content: [{ type: "text", text }], details: { locations } };
274
+ const details = await codeIntelligenceOperations.goToImplementation(path, params.line, params.character);
275
+ const text =
276
+ details.locations.length === 0 ? "No implementation found." : details.locations.map((l) => `${l.path}:${l.line}:${l.character}`).join("\n");
277
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
264
278
  },
265
279
  renderCall(args, theme, context) {
266
280
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -276,9 +290,9 @@ export default function (pi: ExtensionAPI) {
276
290
  .join("\n");
277
291
  return new Text(theme.fg("error", errorText || "go_to_implementation failed"), 0, 0);
278
292
  }
279
- const details = result.details as { locations?: readonly WorkspaceLocation[] } | undefined;
293
+ const details = result.details as { locations?: readonly WorkspaceLocation[]; provenance?: IntelligenceProvenance } | undefined;
280
294
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
281
- text.setText(formatGoToImplementationResult(details?.locations, expanded, theme));
295
+ text.setText(renderIntelligenceSource(formatGoToImplementationResult(details?.locations, expanded, theme), details?.provenance, theme));
282
296
  return text;
283
297
  },
284
298
  });
@@ -298,9 +312,9 @@ export default function (pi: ExtensionAPI) {
298
312
  }),
299
313
  async execute(_toolCallId, params) {
300
314
  const path = resolve(cwd, params.path);
301
- const locations = await codeIntelligenceOperations.findReferences(path, params.line, params.character, params.includeDeclaration);
302
- const text = locations.length === 0 ? "No references found." : locations.map((l) => `${l.path}:${l.line}:${l.character}`).join("\n");
303
- return { content: [{ type: "text", text }], details: { locations } };
315
+ const details = await codeIntelligenceOperations.findReferences(path, params.line, params.character, params.includeDeclaration);
316
+ const text = details.locations.length === 0 ? "No references found." : details.locations.map((l) => `${l.path}:${l.line}:${l.character}`).join("\n");
317
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
304
318
  },
305
319
  renderCall(args, theme, context) {
306
320
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -316,9 +330,9 @@ export default function (pi: ExtensionAPI) {
316
330
  .join("\n");
317
331
  return new Text(theme.fg("error", errorText || "find_references failed"), 0, 0);
318
332
  }
319
- const details = result.details as { locations?: readonly WorkspaceLocation[] } | undefined;
333
+ const details = result.details as { locations?: readonly WorkspaceLocation[]; provenance?: IntelligenceProvenance } | undefined;
320
334
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
321
- text.setText(formatFindReferencesResult(details?.locations, expanded, theme));
335
+ text.setText(renderIntelligenceSource(formatFindReferencesResult(details?.locations, expanded, theme), details?.provenance, theme));
322
336
  return text;
323
337
  },
324
338
  });
@@ -334,8 +348,13 @@ export default function (pi: ExtensionAPI) {
334
348
  parameters: Type.Object(positionParameters),
335
349
  async execute(_toolCallId, params) {
336
350
  const path = resolve(cwd, params.path);
337
- const hover = await codeIntelligenceOperations.hover(path, params.line, params.character);
338
- return { content: [{ type: "text", text: hover?.contents ?? "No hover information available." }], details: { hover } };
351
+ const details = await codeIntelligenceOperations.hover(path, params.line, params.character);
352
+ return {
353
+ content: [
354
+ { type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${details.hover?.contents ?? "No hover information available."}` },
355
+ ],
356
+ details,
357
+ };
339
358
  },
340
359
  renderCall(args, theme, context) {
341
360
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -351,9 +370,9 @@ export default function (pi: ExtensionAPI) {
351
370
  .join("\n");
352
371
  return new Text(theme.fg("error", errorText || "hover failed"), 0, 0);
353
372
  }
354
- const details = result.details as { hover?: Hover } | undefined;
373
+ const details = result.details as { hover?: Hover; provenance?: IntelligenceProvenance } | undefined;
355
374
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
356
- text.setText(formatHoverResult(details?.hover, expanded, theme));
375
+ text.setText(renderIntelligenceSource(formatHoverResult(details?.hover, expanded, theme), details?.provenance, theme));
357
376
  return text;
358
377
  },
359
378
  });
@@ -367,9 +386,9 @@ export default function (pi: ExtensionAPI) {
367
386
  parameters: Type.Object({ path: Type.String({ description: "Absolute or cwd-relative path to the file" }) }),
368
387
  async execute(_toolCallId, params) {
369
388
  const path = resolve(cwd, params.path);
370
- const symbols = await codeIntelligenceOperations.documentSymbols(path);
371
- const text = symbols.length === 0 ? "No symbols found." : symbols.map((s) => `${s.kind} ${s.name}`).join("\n");
372
- return { content: [{ type: "text", text }], details: { symbols } };
389
+ const details = await codeIntelligenceOperations.documentSymbols(path);
390
+ const text = details.symbols.length === 0 ? "No symbols found." : details.symbols.map((s) => `${s.kind} ${s.name}`).join("\n");
391
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
373
392
  },
374
393
  renderCall(args, theme, context) {
375
394
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -385,9 +404,9 @@ export default function (pi: ExtensionAPI) {
385
404
  .join("\n");
386
405
  return new Text(theme.fg("error", errorText || "document_symbols failed"), 0, 0);
387
406
  }
388
- const details = result.details as { symbols?: readonly DocumentSymbolEntry[] } | undefined;
407
+ const details = result.details as { symbols?: readonly DocumentSymbolEntry[]; provenance?: IntelligenceProvenance } | undefined;
389
408
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
390
- text.setText(formatDocumentSymbolsResult(details?.symbols, expanded, theme));
409
+ text.setText(renderIntelligenceSource(formatDocumentSymbolsResult(details?.symbols, expanded, theme), details?.provenance, theme));
391
410
  return text;
392
411
  },
393
412
  });
@@ -401,12 +420,12 @@ export default function (pi: ExtensionAPI) {
401
420
  parameters: Type.Object({ path: Type.String({ description: "Absolute or cwd-relative path to the file" }) }),
402
421
  async execute(_toolCallId, params) {
403
422
  const path = resolve(cwd, params.path);
404
- const diagnostics = await codeIntelligenceOperations.diagnostics(path);
423
+ const details = await codeIntelligenceOperations.diagnostics(path);
405
424
  const text =
406
- diagnostics.length === 0
425
+ details.diagnostics.length === 0
407
426
  ? "No diagnostics."
408
- : diagnostics.map((d) => `${d.severity} ${d.range.path}:${d.range.start.line}:${d.range.start.character} -- ${d.message}`).join("\n");
409
- return { content: [{ type: "text", text }], details: { diagnostics } };
427
+ : details.diagnostics.map((d) => `${d.severity} ${d.range.path}:${d.range.start.line}:${d.range.start.character} -- ${d.message}`).join("\n");
428
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
410
429
  },
411
430
  renderCall(args, theme, context) {
412
431
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -422,9 +441,9 @@ export default function (pi: ExtensionAPI) {
422
441
  .join("\n");
423
442
  return new Text(theme.fg("error", errorText || "diagnostics failed"), 0, 0);
424
443
  }
425
- const details = result.details as { diagnostics?: readonly Diagnostic[] } | undefined;
444
+ const details = result.details as { diagnostics?: readonly Diagnostic[]; provenance?: IntelligenceProvenance } | undefined;
426
445
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
427
- text.setText(formatDiagnosticsResult(details?.diagnostics, expanded, theme));
446
+ text.setText(renderIntelligenceSource(formatDiagnosticsResult(details?.diagnostics, expanded, theme), details?.provenance, theme));
428
447
  return text;
429
448
  },
430
449
  });
@@ -440,12 +459,12 @@ export default function (pi: ExtensionAPI) {
440
459
  parameters: Type.Object(positionParameters),
441
460
  async execute(_toolCallId, params) {
442
461
  const path = resolve(cwd, params.path);
443
- const items = await codeIntelligenceOperations.prepareCallHierarchy(path, params.line, params.character);
462
+ const details = await codeIntelligenceOperations.prepareCallHierarchy(path, params.line, params.character);
444
463
  const text =
445
- items.length === 0
464
+ details.items.length === 0
446
465
  ? "No call-hierarchy root at this position."
447
- : items.map((i) => `${i.kind} ${i.name} -- ${i.location.path}:${i.location.line}:${i.location.character}`).join("\n");
448
- return { content: [{ type: "text", text }], details: { items } };
466
+ : details.items.map((i) => `${i.kind} ${i.name} -- ${i.location.path}:${i.location.line}:${i.location.character}`).join("\n");
467
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
449
468
  },
450
469
  renderCall(args, theme, context) {
451
470
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -461,9 +480,9 @@ export default function (pi: ExtensionAPI) {
461
480
  .join("\n");
462
481
  return new Text(theme.fg("error", errorText || "prepare_call_hierarchy failed"), 0, 0);
463
482
  }
464
- const details = result.details as { items?: readonly CallHierarchyEntry[] } | undefined;
483
+ const details = result.details as { items?: readonly CallHierarchyEntry[]; provenance?: IntelligenceProvenance } | undefined;
465
484
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
466
- text.setText(formatPrepareCallHierarchyResult(details?.items, theme));
485
+ text.setText(renderIntelligenceSource(formatPrepareCallHierarchyResult(details?.items, theme), details?.provenance, theme));
467
486
  return text;
468
487
  },
469
488
  });
@@ -479,12 +498,14 @@ export default function (pi: ExtensionAPI) {
479
498
  parameters: Type.Object(positionParameters),
480
499
  async execute(_toolCallId, params) {
481
500
  const path = resolve(cwd, params.path);
482
- const calls = await codeIntelligenceOperations.incomingCalls(path, params.line, params.character);
501
+ const details = await codeIntelligenceOperations.incomingCalls(path, params.line, params.character);
483
502
  const text =
484
- calls.length === 0
503
+ details.calls.length === 0
485
504
  ? "No incoming calls found."
486
- : calls.map((c) => `${c.from.kind} ${c.from.name} -- ${c.from.location.path}:${c.from.location.line}:${c.from.location.character}`).join("\n");
487
- return { content: [{ type: "text", text }], details: { calls } };
505
+ : details.calls
506
+ .map((c) => `${c.from.kind} ${c.from.name} -- ${c.from.location.path}:${c.from.location.line}:${c.from.location.character}`)
507
+ .join("\n");
508
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
488
509
  },
489
510
  renderCall(args, theme, context) {
490
511
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -500,9 +521,9 @@ export default function (pi: ExtensionAPI) {
500
521
  .join("\n");
501
522
  return new Text(theme.fg("error", errorText || "incoming_calls failed"), 0, 0);
502
523
  }
503
- const details = result.details as { calls?: readonly IncomingCall[] } | undefined;
524
+ const details = result.details as { calls?: readonly IncomingCall[]; provenance?: IntelligenceProvenance } | undefined;
504
525
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
505
- text.setText(formatIncomingCallsResult(details?.calls, expanded, theme));
526
+ text.setText(renderIntelligenceSource(formatIncomingCallsResult(details?.calls, expanded, theme), details?.provenance, theme));
506
527
  return text;
507
528
  },
508
529
  });
@@ -516,12 +537,12 @@ export default function (pi: ExtensionAPI) {
516
537
  parameters: Type.Object(positionParameters),
517
538
  async execute(_toolCallId, params) {
518
539
  const path = resolve(cwd, params.path);
519
- const calls = await codeIntelligenceOperations.outgoingCalls(path, params.line, params.character);
540
+ const details = await codeIntelligenceOperations.outgoingCalls(path, params.line, params.character);
520
541
  const text =
521
- calls.length === 0
542
+ details.calls.length === 0
522
543
  ? "No outgoing calls found."
523
- : calls.map((c) => `${c.to.kind} ${c.to.name} -- ${c.to.location.path}:${c.to.location.line}:${c.to.location.character}`).join("\n");
524
- return { content: [{ type: "text", text }], details: { calls } };
544
+ : details.calls.map((c) => `${c.to.kind} ${c.to.name} -- ${c.to.location.path}:${c.to.location.line}:${c.to.location.character}`).join("\n");
545
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
525
546
  },
526
547
  renderCall(args, theme, context) {
527
548
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -537,9 +558,9 @@ export default function (pi: ExtensionAPI) {
537
558
  .join("\n");
538
559
  return new Text(theme.fg("error", errorText || "outgoing_calls failed"), 0, 0);
539
560
  }
540
- const details = result.details as { calls?: readonly OutgoingCall[] } | undefined;
561
+ const details = result.details as { calls?: readonly OutgoingCall[]; provenance?: IntelligenceProvenance } | undefined;
541
562
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
542
- text.setText(formatOutgoingCallsResult(details?.calls, expanded, theme));
563
+ text.setText(renderIntelligenceSource(formatOutgoingCallsResult(details?.calls, expanded, theme), details?.provenance, theme));
543
564
  return text;
544
565
  },
545
566
  });
@@ -825,6 +846,45 @@ export default function (pi: ExtensionAPI) {
825
846
  },
826
847
  });
827
848
 
849
+ const packageSourceOperations = createLectorPackageSourceOperations();
850
+ pi.registerTool({
851
+ name: "package_source",
852
+ label: "Package Source",
853
+ description:
854
+ "Resolve an installed npm package to verified exact repository source. Uses the project's lockfile, bounded registry metadata, and an exact Git ref/commit; registers verified source as a read-only workspace for the other Lector tools.",
855
+ promptSnippet: "Resolve an installed npm package to exact read-only source",
856
+ parameters: Type.Object({
857
+ directory: Type.String({ description: "Project directory containing the npm-family lockfile" }),
858
+ name: Type.String({ description: "Installed package name, including scope when present" }),
859
+ version: Type.Optional(Type.String({ description: "Exact installed version; required when the lockfile contains several versions" })),
860
+ registry: Type.Optional(Type.String({ description: "npm registry URL; defaults to the public npm registry" })),
861
+ }),
862
+ async execute(_toolCallId, params) {
863
+ const directory = resolve(cwd, params.directory);
864
+ const result = await packageSourceOperations.resolve(directory, params.name, params.version ?? null, params.registry ?? null);
865
+ return { content: [{ type: "text", text: JSON.stringify(result) }], details: { result } };
866
+ },
867
+ renderCall(args, theme, context) {
868
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
869
+ text.setText(formatPackageSourceCall(args as { directory?: unknown; name?: unknown; version?: unknown }, theme));
870
+ return text;
871
+ },
872
+ renderResult(result, { expanded, isPartial }, theme, context) {
873
+ if (isPartial) return new Text(theme.fg("warning", "Resolving package source..."), 0, 0);
874
+ if (context.isError) {
875
+ const errorText = result.content
876
+ .filter((block) => block.type === "text")
877
+ .map((block) => block.text)
878
+ .join("\n");
879
+ return new Text(theme.fg("error", errorText || "package_source failed"), 0, 0);
880
+ }
881
+ const details = result.details as { result?: PackageSourceOperationResult } | undefined;
882
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
883
+ text.setText(formatPackageSourceResult(details?.result, expanded, theme));
884
+ return text;
885
+ },
886
+ });
887
+
828
888
  const repoFetchOperations = createLectorRepoFetchOperations();
829
889
  pi.registerTool({
830
890
  name: "repo_fetch",
@@ -894,7 +954,7 @@ export default function (pi: ExtensionAPI) {
894
954
  .join("\n");
895
955
  return new Text(theme.fg("error", errorText || "find_symbols_across_projects failed"), 0, 0);
896
956
  }
897
- const details = result.details as { results?: readonly WorkspaceQueryOutcome<{ symbols: readonly WorkspaceSymbol[] }>[] } | undefined;
957
+ const details = result.details as { results?: readonly WorkspaceQueryOutcome<SymbolSearchResult>[] } | undefined;
898
958
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
899
959
  text.setText(formatFindSymbolsAcrossProjectsResult(details?.results, expanded, theme));
900
960
  return text;
@@ -0,0 +1,21 @@
1
+ import { DEFAULT_PACKAGE_SOURCE_BOUNDS, type PackageSourceOperationResult } from "@danypops/lector";
2
+ import { lectorClient } from "./lector-client.ts";
3
+
4
+ export interface PackageSourceOperations {
5
+ resolve(directory: string, name: string, requestedVersion: string | null, registry: string | null): Promise<PackageSourceOperationResult>;
6
+ }
7
+
8
+ export function createLectorPackageSourceOperations(): PackageSourceOperations {
9
+ return {
10
+ async resolve(directory, name, requestedVersion, registry) {
11
+ const client = await lectorClient();
12
+ return client.call("package.resolveSource", {
13
+ request: {
14
+ projectRoot: directory,
15
+ coordinate: { ecosystem: "npm", registry, name, requestedVersion },
16
+ },
17
+ bounds: DEFAULT_PACKAGE_SOURCE_BOUNDS,
18
+ });
19
+ },
20
+ };
21
+ }
@@ -0,0 +1,37 @@
1
+ import type { PackageSourceOperationResult } from "@danypops/lector";
2
+ import type { LectorTheme } from "./lector-tui-theme.ts";
3
+
4
+ const DEFAULT_VISIBLE_CANDIDATES = 5;
5
+
6
+ export function formatPackageSourceCall(args: { directory?: unknown; name?: unknown; version?: unknown }, theme: LectorTheme): string {
7
+ const name = typeof args.name === "string" ? args.name : "";
8
+ const version = typeof args.version === "string" ? `@${args.version}` : "";
9
+ const directory = typeof args.directory === "string" ? args.directory : "";
10
+ return `${theme.fg("toolTitle", theme.bold("package_source"))} ${theme.fg("accent", `${name}${version}`)} ${theme.fg("dim", directory)}`.trim();
11
+ }
12
+
13
+ export function formatPackageSourceResult(result: PackageSourceOperationResult | undefined, expanded: boolean, theme: LectorTheme): string {
14
+ if (!result) return theme.fg("dim", "No package-source result.");
15
+ const { outcome } = result;
16
+ if (outcome.status === "verified") {
17
+ return [
18
+ `${theme.fg("accent", result.workspaceId ?? "unregistered")} ${theme.fg("success", `${outcome.coordinate.name}@${outcome.coordinate.resolvedVersion}`)}`,
19
+ `${outcome.workspace.cachePath}`,
20
+ `${outcome.repository.url ?? "local source"}@${outcome.repository.resolvedRef ?? "local"} ${outcome.repository.commit ?? outcome.verification.integrity}`,
21
+ ].join("\n");
22
+ }
23
+ if (outcome.status === "ambiguous") {
24
+ const visible = expanded ? outcome.candidates : outcome.candidates.slice(0, DEFAULT_VISIBLE_CANDIDATES);
25
+ const lines = [theme.fg("warning", `Ambiguous package source (${outcome.code})`)];
26
+ for (const candidate of visible) lines.push(`${candidate.version} -- ${candidate.source}`);
27
+ const hidden = outcome.candidates.length - visible.length;
28
+ if (hidden > 0 || outcome.truncated) lines.push(theme.fg("dim", outcome.truncated ? "More candidates were truncated by the daemon." : `… ${hidden} more`));
29
+ return lines.join("\n");
30
+ }
31
+ if (outcome.status === "unauthenticated") {
32
+ return theme.fg("warning", `Authentication required (${outcome.code}): configure ${outcome.requiredCredentialNames.join(", ")}`);
33
+ }
34
+ if (outcome.status === "oversized") return theme.fg("warning", `Source resolution exceeded ${outcome.resource} limit ${outcome.limit}.`);
35
+ if (outcome.status === "mismatched") return theme.fg("error", `Source mismatch (${outcome.code}): expected ${outcome.expected}, got ${outcome.actual}.`);
36
+ return theme.fg("warning", `Source unavailable (${outcome.code}).`);
37
+ }
@@ -46,6 +46,20 @@ export type CachePresentationState =
46
46
  | { readonly status: "finished-caching"; readonly job: JobSnapshot<PopulateSymbolGraphResult> & { readonly status: "succeeded" } }
47
47
  | { readonly status: "cached" };
48
48
 
49
+ export function describeCacheState(state: CachePresentationState): string {
50
+ if (state.status === "not-cached") return `not cached (${state.reason})`;
51
+ if (state.status === "caching") return `caching (job ${state.jobId})`;
52
+ if (state.status === "finished-caching") return `finished caching (job ${state.job.id})`;
53
+ return "cached";
54
+ }
55
+
56
+ export function cacheContextMessage(state: CachePresentationState): string {
57
+ const prefix = `Lector workspace cache: ${describeCacheState(state)}.`;
58
+ if (state.status === "not-cached") return `${prefix} Live code-intelligence operations remain available.`;
59
+ if (state.status === "caching") return `${prefix} The cached graph is still building; use live code-intelligence operations until it is ready.`;
60
+ return `${prefix} The cached graph is ready.`;
61
+ }
62
+
49
63
  export interface MonitorWorkspaceCacheOptions {
50
64
  readonly directory: string;
51
65
  readonly maxFiles: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-lector",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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,7 +18,7 @@
18
18
  "typebox": "*"
19
19
  },
20
20
  "dependencies": {
21
- "@danypops/lector": "^0.1.7"
21
+ "@danypops/lector": "^0.1.8"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@earendil-works/pi-ai": "^0.81.1",