@danypops/pi-lector 0.1.7 → 0.1.8

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,6 +57,8 @@ 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";
@@ -63,6 +67,14 @@ import { formatSearchCall, formatSearchResult } from "./search-rendering.ts";
63
67
  import { type CachePresentationState, createWorkspaceCacheOperations, monitorWorkspaceCache } from "./workspace-cache-operations.ts";
64
68
  import { createLectorWriteOperations } from "./write-operations.ts";
65
69
 
70
+ function describeIntelligenceSource(provenance: IntelligenceProvenance): string {
71
+ return `${provenance.fidelity} via ${provenance.backend}`;
72
+ }
73
+
74
+ function renderIntelligenceSource(body: string, provenance: IntelligenceProvenance | undefined, theme: { fg(color: "muted", text: string): string }): string {
75
+ return provenance ? `${theme.fg("muted", describeIntelligenceSource(provenance))}\n${body}` : body;
76
+ }
77
+
66
78
  /**
67
79
  * pi-lector -- the thin Pi host adapter for Lector. Overrides the built-in
68
80
  * read/write/edit tools by name with Lector-backed Operations, so built-in
@@ -160,7 +172,7 @@ export default function (pi: ExtensionAPI) {
160
172
  "Search a workspace for functions, classes, interfaces, types, enums, and methods by name " +
161
173
  "(case-insensitive substring match). Returns each match's kind and file location. `directory` " +
162
174
  "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.",
175
+ "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
176
  promptSnippet: "Search a workspace for a symbol (function, class, etc.) by name",
165
177
  promptGuidelines: [
166
178
  "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 +184,16 @@ export default function (pi: ExtensionAPI) {
172
184
  }),
173
185
  async execute(_toolCallId, params) {
174
186
  const directory = resolve(cwd, params.directory);
175
- const symbols = await findSymbolsOperations.findSymbols(params.query, directory);
187
+ const result = await findSymbolsOperations.findSymbols(params.query, directory);
188
+ const { symbols, provenance, truncated } = result;
189
+ const source = `${provenance.fidelity} via ${provenance.backend}${truncated ? " (truncated)" : ""}`;
176
190
  const text =
177
191
  symbols.length === 0
178
- ? `No symbols found matching "${params.query}".`
179
- : symbols
192
+ ? `${source}\nNo symbols found matching "${params.query}".`
193
+ : `${source}\n${symbols
180
194
  .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 } };
195
+ .join("\n")}`;
196
+ return { content: [{ type: "text", text }], details: result };
183
197
  },
184
198
  renderCall(args, theme, context) {
185
199
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -197,10 +211,10 @@ export default function (pi: ExtensionAPI) {
197
211
  .join("\n");
198
212
  return new Text(theme.fg("error", errorText || "find_symbols failed"), 0, 0);
199
213
  }
200
- const details = result.details as { symbols?: readonly WorkspaceSymbol[] } | undefined;
214
+ const details = result.details as SymbolSearchResult | undefined;
201
215
  const query = typeof context.args?.query === "string" ? context.args.query : "";
202
216
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
203
- text.setText(formatFindSymbolsResult(details?.symbols, query, expanded, theme));
217
+ text.setText(formatFindSymbolsResult(details, query, expanded, theme));
204
218
  return text;
205
219
  },
206
220
  });
@@ -221,9 +235,9 @@ export default function (pi: ExtensionAPI) {
221
235
  parameters: Type.Object(positionParameters),
222
236
  async execute(_toolCallId, params) {
223
237
  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 } };
238
+ const details = await codeIntelligenceOperations.goToDefinition(path, params.line, params.character);
239
+ const text = details.locations.length === 0 ? "No definition found." : details.locations.map((l) => `${l.path}:${l.line}:${l.character}`).join("\n");
240
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
227
241
  },
228
242
  renderCall(args, theme, context) {
229
243
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -239,9 +253,9 @@ export default function (pi: ExtensionAPI) {
239
253
  .join("\n");
240
254
  return new Text(theme.fg("error", errorText || "go_to_definition failed"), 0, 0);
241
255
  }
242
- const details = result.details as { locations?: readonly WorkspaceLocation[] } | undefined;
256
+ const details = result.details as { locations?: readonly WorkspaceLocation[]; provenance?: IntelligenceProvenance } | undefined;
243
257
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
244
- text.setText(formatGoToDefinitionResult(details?.locations, expanded, theme));
258
+ text.setText(renderIntelligenceSource(formatGoToDefinitionResult(details?.locations, expanded, theme), details?.provenance, theme));
245
259
  return text;
246
260
  },
247
261
  });
@@ -258,9 +272,10 @@ export default function (pi: ExtensionAPI) {
258
272
  parameters: Type.Object(positionParameters),
259
273
  async execute(_toolCallId, params) {
260
274
  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 } };
275
+ const details = await codeIntelligenceOperations.goToImplementation(path, params.line, params.character);
276
+ const text =
277
+ details.locations.length === 0 ? "No implementation found." : details.locations.map((l) => `${l.path}:${l.line}:${l.character}`).join("\n");
278
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
264
279
  },
265
280
  renderCall(args, theme, context) {
266
281
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -276,9 +291,9 @@ export default function (pi: ExtensionAPI) {
276
291
  .join("\n");
277
292
  return new Text(theme.fg("error", errorText || "go_to_implementation failed"), 0, 0);
278
293
  }
279
- const details = result.details as { locations?: readonly WorkspaceLocation[] } | undefined;
294
+ const details = result.details as { locations?: readonly WorkspaceLocation[]; provenance?: IntelligenceProvenance } | undefined;
280
295
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
281
- text.setText(formatGoToImplementationResult(details?.locations, expanded, theme));
296
+ text.setText(renderIntelligenceSource(formatGoToImplementationResult(details?.locations, expanded, theme), details?.provenance, theme));
282
297
  return text;
283
298
  },
284
299
  });
@@ -298,9 +313,9 @@ export default function (pi: ExtensionAPI) {
298
313
  }),
299
314
  async execute(_toolCallId, params) {
300
315
  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 } };
316
+ const details = await codeIntelligenceOperations.findReferences(path, params.line, params.character, params.includeDeclaration);
317
+ const text = details.locations.length === 0 ? "No references found." : details.locations.map((l) => `${l.path}:${l.line}:${l.character}`).join("\n");
318
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
304
319
  },
305
320
  renderCall(args, theme, context) {
306
321
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -316,9 +331,9 @@ export default function (pi: ExtensionAPI) {
316
331
  .join("\n");
317
332
  return new Text(theme.fg("error", errorText || "find_references failed"), 0, 0);
318
333
  }
319
- const details = result.details as { locations?: readonly WorkspaceLocation[] } | undefined;
334
+ const details = result.details as { locations?: readonly WorkspaceLocation[]; provenance?: IntelligenceProvenance } | undefined;
320
335
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
321
- text.setText(formatFindReferencesResult(details?.locations, expanded, theme));
336
+ text.setText(renderIntelligenceSource(formatFindReferencesResult(details?.locations, expanded, theme), details?.provenance, theme));
322
337
  return text;
323
338
  },
324
339
  });
@@ -334,8 +349,13 @@ export default function (pi: ExtensionAPI) {
334
349
  parameters: Type.Object(positionParameters),
335
350
  async execute(_toolCallId, params) {
336
351
  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 } };
352
+ const details = await codeIntelligenceOperations.hover(path, params.line, params.character);
353
+ return {
354
+ content: [
355
+ { type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${details.hover?.contents ?? "No hover information available."}` },
356
+ ],
357
+ details,
358
+ };
339
359
  },
340
360
  renderCall(args, theme, context) {
341
361
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -351,9 +371,9 @@ export default function (pi: ExtensionAPI) {
351
371
  .join("\n");
352
372
  return new Text(theme.fg("error", errorText || "hover failed"), 0, 0);
353
373
  }
354
- const details = result.details as { hover?: Hover } | undefined;
374
+ const details = result.details as { hover?: Hover; provenance?: IntelligenceProvenance } | undefined;
355
375
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
356
- text.setText(formatHoverResult(details?.hover, expanded, theme));
376
+ text.setText(renderIntelligenceSource(formatHoverResult(details?.hover, expanded, theme), details?.provenance, theme));
357
377
  return text;
358
378
  },
359
379
  });
@@ -367,9 +387,9 @@ export default function (pi: ExtensionAPI) {
367
387
  parameters: Type.Object({ path: Type.String({ description: "Absolute or cwd-relative path to the file" }) }),
368
388
  async execute(_toolCallId, params) {
369
389
  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 } };
390
+ const details = await codeIntelligenceOperations.documentSymbols(path);
391
+ const text = details.symbols.length === 0 ? "No symbols found." : details.symbols.map((s) => `${s.kind} ${s.name}`).join("\n");
392
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
373
393
  },
374
394
  renderCall(args, theme, context) {
375
395
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -385,9 +405,9 @@ export default function (pi: ExtensionAPI) {
385
405
  .join("\n");
386
406
  return new Text(theme.fg("error", errorText || "document_symbols failed"), 0, 0);
387
407
  }
388
- const details = result.details as { symbols?: readonly DocumentSymbolEntry[] } | undefined;
408
+ const details = result.details as { symbols?: readonly DocumentSymbolEntry[]; provenance?: IntelligenceProvenance } | undefined;
389
409
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
390
- text.setText(formatDocumentSymbolsResult(details?.symbols, expanded, theme));
410
+ text.setText(renderIntelligenceSource(formatDocumentSymbolsResult(details?.symbols, expanded, theme), details?.provenance, theme));
391
411
  return text;
392
412
  },
393
413
  });
@@ -401,12 +421,12 @@ export default function (pi: ExtensionAPI) {
401
421
  parameters: Type.Object({ path: Type.String({ description: "Absolute or cwd-relative path to the file" }) }),
402
422
  async execute(_toolCallId, params) {
403
423
  const path = resolve(cwd, params.path);
404
- const diagnostics = await codeIntelligenceOperations.diagnostics(path);
424
+ const details = await codeIntelligenceOperations.diagnostics(path);
405
425
  const text =
406
- diagnostics.length === 0
426
+ details.diagnostics.length === 0
407
427
  ? "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 } };
428
+ : details.diagnostics.map((d) => `${d.severity} ${d.range.path}:${d.range.start.line}:${d.range.start.character} -- ${d.message}`).join("\n");
429
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
410
430
  },
411
431
  renderCall(args, theme, context) {
412
432
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -422,9 +442,9 @@ export default function (pi: ExtensionAPI) {
422
442
  .join("\n");
423
443
  return new Text(theme.fg("error", errorText || "diagnostics failed"), 0, 0);
424
444
  }
425
- const details = result.details as { diagnostics?: readonly Diagnostic[] } | undefined;
445
+ const details = result.details as { diagnostics?: readonly Diagnostic[]; provenance?: IntelligenceProvenance } | undefined;
426
446
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
427
- text.setText(formatDiagnosticsResult(details?.diagnostics, expanded, theme));
447
+ text.setText(renderIntelligenceSource(formatDiagnosticsResult(details?.diagnostics, expanded, theme), details?.provenance, theme));
428
448
  return text;
429
449
  },
430
450
  });
@@ -440,12 +460,12 @@ export default function (pi: ExtensionAPI) {
440
460
  parameters: Type.Object(positionParameters),
441
461
  async execute(_toolCallId, params) {
442
462
  const path = resolve(cwd, params.path);
443
- const items = await codeIntelligenceOperations.prepareCallHierarchy(path, params.line, params.character);
463
+ const details = await codeIntelligenceOperations.prepareCallHierarchy(path, params.line, params.character);
444
464
  const text =
445
- items.length === 0
465
+ details.items.length === 0
446
466
  ? "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 } };
467
+ : details.items.map((i) => `${i.kind} ${i.name} -- ${i.location.path}:${i.location.line}:${i.location.character}`).join("\n");
468
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
449
469
  },
450
470
  renderCall(args, theme, context) {
451
471
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -461,9 +481,9 @@ export default function (pi: ExtensionAPI) {
461
481
  .join("\n");
462
482
  return new Text(theme.fg("error", errorText || "prepare_call_hierarchy failed"), 0, 0);
463
483
  }
464
- const details = result.details as { items?: readonly CallHierarchyEntry[] } | undefined;
484
+ const details = result.details as { items?: readonly CallHierarchyEntry[]; provenance?: IntelligenceProvenance } | undefined;
465
485
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
466
- text.setText(formatPrepareCallHierarchyResult(details?.items, theme));
486
+ text.setText(renderIntelligenceSource(formatPrepareCallHierarchyResult(details?.items, theme), details?.provenance, theme));
467
487
  return text;
468
488
  },
469
489
  });
@@ -479,12 +499,14 @@ export default function (pi: ExtensionAPI) {
479
499
  parameters: Type.Object(positionParameters),
480
500
  async execute(_toolCallId, params) {
481
501
  const path = resolve(cwd, params.path);
482
- const calls = await codeIntelligenceOperations.incomingCalls(path, params.line, params.character);
502
+ const details = await codeIntelligenceOperations.incomingCalls(path, params.line, params.character);
483
503
  const text =
484
- calls.length === 0
504
+ details.calls.length === 0
485
505
  ? "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 } };
506
+ : details.calls
507
+ .map((c) => `${c.from.kind} ${c.from.name} -- ${c.from.location.path}:${c.from.location.line}:${c.from.location.character}`)
508
+ .join("\n");
509
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
488
510
  },
489
511
  renderCall(args, theme, context) {
490
512
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -500,9 +522,9 @@ export default function (pi: ExtensionAPI) {
500
522
  .join("\n");
501
523
  return new Text(theme.fg("error", errorText || "incoming_calls failed"), 0, 0);
502
524
  }
503
- const details = result.details as { calls?: readonly IncomingCall[] } | undefined;
525
+ const details = result.details as { calls?: readonly IncomingCall[]; provenance?: IntelligenceProvenance } | undefined;
504
526
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
505
- text.setText(formatIncomingCallsResult(details?.calls, expanded, theme));
527
+ text.setText(renderIntelligenceSource(formatIncomingCallsResult(details?.calls, expanded, theme), details?.provenance, theme));
506
528
  return text;
507
529
  },
508
530
  });
@@ -516,12 +538,12 @@ export default function (pi: ExtensionAPI) {
516
538
  parameters: Type.Object(positionParameters),
517
539
  async execute(_toolCallId, params) {
518
540
  const path = resolve(cwd, params.path);
519
- const calls = await codeIntelligenceOperations.outgoingCalls(path, params.line, params.character);
541
+ const details = await codeIntelligenceOperations.outgoingCalls(path, params.line, params.character);
520
542
  const text =
521
- calls.length === 0
543
+ details.calls.length === 0
522
544
  ? "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 } };
545
+ : details.calls.map((c) => `${c.to.kind} ${c.to.name} -- ${c.to.location.path}:${c.to.location.line}:${c.to.location.character}`).join("\n");
546
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
525
547
  },
526
548
  renderCall(args, theme, context) {
527
549
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -537,9 +559,9 @@ export default function (pi: ExtensionAPI) {
537
559
  .join("\n");
538
560
  return new Text(theme.fg("error", errorText || "outgoing_calls failed"), 0, 0);
539
561
  }
540
- const details = result.details as { calls?: readonly OutgoingCall[] } | undefined;
562
+ const details = result.details as { calls?: readonly OutgoingCall[]; provenance?: IntelligenceProvenance } | undefined;
541
563
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
542
- text.setText(formatOutgoingCallsResult(details?.calls, expanded, theme));
564
+ text.setText(renderIntelligenceSource(formatOutgoingCallsResult(details?.calls, expanded, theme), details?.provenance, theme));
543
565
  return text;
544
566
  },
545
567
  });
@@ -825,6 +847,45 @@ export default function (pi: ExtensionAPI) {
825
847
  },
826
848
  });
827
849
 
850
+ const packageSourceOperations = createLectorPackageSourceOperations();
851
+ pi.registerTool({
852
+ name: "package_source",
853
+ label: "Package Source",
854
+ description:
855
+ "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.",
856
+ promptSnippet: "Resolve an installed npm package to exact read-only source",
857
+ parameters: Type.Object({
858
+ directory: Type.String({ description: "Project directory containing the npm-family lockfile" }),
859
+ name: Type.String({ description: "Installed package name, including scope when present" }),
860
+ version: Type.Optional(Type.String({ description: "Exact installed version; required when the lockfile contains several versions" })),
861
+ registry: Type.Optional(Type.String({ description: "npm registry URL; defaults to the public npm registry" })),
862
+ }),
863
+ async execute(_toolCallId, params) {
864
+ const directory = resolve(cwd, params.directory);
865
+ const result = await packageSourceOperations.resolve(directory, params.name, params.version ?? null, params.registry ?? null);
866
+ return { content: [{ type: "text", text: JSON.stringify(result) }], details: { result } };
867
+ },
868
+ renderCall(args, theme, context) {
869
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
870
+ text.setText(formatPackageSourceCall(args as { directory?: unknown; name?: unknown; version?: unknown }, theme));
871
+ return text;
872
+ },
873
+ renderResult(result, { expanded, isPartial }, theme, context) {
874
+ if (isPartial) return new Text(theme.fg("warning", "Resolving package source..."), 0, 0);
875
+ if (context.isError) {
876
+ const errorText = result.content
877
+ .filter((block) => block.type === "text")
878
+ .map((block) => block.text)
879
+ .join("\n");
880
+ return new Text(theme.fg("error", errorText || "package_source failed"), 0, 0);
881
+ }
882
+ const details = result.details as { result?: PackageSourceOperationResult } | undefined;
883
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
884
+ text.setText(formatPackageSourceResult(details?.result, expanded, theme));
885
+ return text;
886
+ },
887
+ });
888
+
828
889
  const repoFetchOperations = createLectorRepoFetchOperations();
829
890
  pi.registerTool({
830
891
  name: "repo_fetch",
@@ -894,7 +955,7 @@ export default function (pi: ExtensionAPI) {
894
955
  .join("\n");
895
956
  return new Text(theme.fg("error", errorText || "find_symbols_across_projects failed"), 0, 0);
896
957
  }
897
- const details = result.details as { results?: readonly WorkspaceQueryOutcome<{ symbols: readonly WorkspaceSymbol[] }>[] } | undefined;
958
+ const details = result.details as { results?: readonly WorkspaceQueryOutcome<SymbolSearchResult>[] } | undefined;
898
959
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
899
960
  text.setText(formatFindSymbolsAcrossProjectsResult(details?.results, expanded, theme));
900
961
  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
+ }
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.8",
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",