@danypops/pi-lector 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extension/src/code-intelligence-rendering.ts +26 -13
- package/extension/src/git-rendering.ts +23 -16
- package/extension/src/index.ts +162 -218
- package/extension/src/lector-client.ts +14 -6
- package/extension/src/nearest-workspace-root.ts +19 -3
- package/extension/src/symbol-annotation-operations.ts +38 -2
- package/package.json +3 -3
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
DocumentSymbolEntry,
|
|
5
5
|
Hover,
|
|
6
6
|
IncomingCall,
|
|
7
|
+
IntelligenceProvenance,
|
|
7
8
|
JobSnapshot,
|
|
8
9
|
OutgoingCall,
|
|
9
10
|
PopulateSymbolGraphResult,
|
|
@@ -159,20 +160,29 @@ function formatCallHierarchyEntry(entry: { kind: string; name: string; location:
|
|
|
159
160
|
return `${kind} ${name} -- ${location}`;
|
|
160
161
|
}
|
|
161
162
|
|
|
162
|
-
export
|
|
163
|
-
|
|
163
|
+
export type CallHierarchyDirection = "prepare" | "incoming" | "outgoing";
|
|
164
|
+
|
|
165
|
+
// A real discriminated union, not one shape with optional fields -- lets formatCallHierarchyResult
|
|
166
|
+
// narrow `items`/`calls` per branch without an unsafe assertion.
|
|
167
|
+
export type CallHierarchyToolDetails =
|
|
168
|
+
| { readonly direction: "prepare"; readonly items: readonly CallHierarchyEntry[]; readonly provenance?: IntelligenceProvenance }
|
|
169
|
+
| { readonly direction: "incoming"; readonly calls: readonly IncomingCall[]; readonly provenance?: IntelligenceProvenance }
|
|
170
|
+
| { readonly direction: "outgoing"; readonly calls: readonly OutgoingCall[]; readonly provenance?: IntelligenceProvenance };
|
|
171
|
+
|
|
172
|
+
export function formatCallHierarchyCall(args: { direction?: unknown; path?: unknown; line?: unknown; character?: unknown }, theme: LectorTheme): string {
|
|
173
|
+
const direction = typeof args.direction === "string" ? args.direction : "";
|
|
174
|
+
const path = typeof args.path === "string" ? args.path : "";
|
|
175
|
+
const line = typeof args.line === "number" ? args.line : "?";
|
|
176
|
+
const character = typeof args.character === "number" ? args.character : "?";
|
|
177
|
+
return `${theme.fg("toolTitle", theme.bold("call_hierarchy"))} ${theme.fg("muted", direction)} ${theme.fg("accent", `${path}:${line}:${character}`)}`;
|
|
164
178
|
}
|
|
165
179
|
|
|
166
|
-
|
|
180
|
+
function formatPrepareCallHierarchyResult(items: readonly CallHierarchyEntry[] | undefined, theme: LectorTheme): string {
|
|
167
181
|
if (!items || items.length === 0) return theme.fg("dim", "No call-hierarchy root at this position.");
|
|
168
182
|
return items.map((item) => formatCallHierarchyEntry(item, theme)).join("\n");
|
|
169
183
|
}
|
|
170
184
|
|
|
171
|
-
|
|
172
|
-
return formatPositionalCall("incoming_calls", args, theme);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function formatIncomingCallsResult(calls: readonly IncomingCall[] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
185
|
+
function formatIncomingCallsResult(calls: readonly IncomingCall[] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
176
186
|
if (!calls || calls.length === 0) return theme.fg("dim", "No incoming calls found.");
|
|
177
187
|
|
|
178
188
|
const displayCount = expanded ? calls.length : Math.min(calls.length, DEFAULT_VISIBLE_CALLS);
|
|
@@ -184,11 +194,7 @@ export function formatIncomingCallsResult(calls: readonly IncomingCall[] | undef
|
|
|
184
194
|
return lines.join("\n");
|
|
185
195
|
}
|
|
186
196
|
|
|
187
|
-
|
|
188
|
-
return formatPositionalCall("outgoing_calls", args, theme);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function formatOutgoingCallsResult(calls: readonly OutgoingCall[] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
197
|
+
function formatOutgoingCallsResult(calls: readonly OutgoingCall[] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
192
198
|
if (!calls || calls.length === 0) return theme.fg("dim", "No outgoing calls found.");
|
|
193
199
|
|
|
194
200
|
const displayCount = expanded ? calls.length : Math.min(calls.length, DEFAULT_VISIBLE_CALLS);
|
|
@@ -200,6 +206,13 @@ export function formatOutgoingCallsResult(calls: readonly OutgoingCall[] | undef
|
|
|
200
206
|
return lines.join("\n");
|
|
201
207
|
}
|
|
202
208
|
|
|
209
|
+
export function formatCallHierarchyResult(details: CallHierarchyToolDetails | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
210
|
+
if (!details) return theme.fg("dim", "No result.");
|
|
211
|
+
if (details.direction === "prepare") return formatPrepareCallHierarchyResult(details.items, theme);
|
|
212
|
+
if (details.direction === "incoming") return formatIncomingCallsResult(details.calls, expanded, theme);
|
|
213
|
+
return formatOutgoingCallsResult(details.calls, expanded, theme);
|
|
214
|
+
}
|
|
215
|
+
|
|
203
216
|
export function formatPopulateSymbolGraphCall(args: { path?: unknown; maxFiles?: unknown; maxSymbolsPerFile?: unknown }, theme: LectorTheme): string {
|
|
204
217
|
const path = typeof args.path === "string" ? args.path : "";
|
|
205
218
|
return `${theme.fg("toolTitle", theme.bold("populate_symbol_graph"))} ${theme.fg("accent", path)}`;
|
|
@@ -6,12 +6,30 @@ const DEFAULT_VISIBLE_FILES = 20;
|
|
|
6
6
|
const DEFAULT_VISIBLE_COMMITS = 10;
|
|
7
7
|
const DEFAULT_VISIBLE_DIFF_LINES = 60;
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export type GitAction = "status" | "log" | "diff";
|
|
10
|
+
|
|
11
|
+
export interface GitToolDetails {
|
|
12
|
+
readonly action: GitAction;
|
|
13
|
+
readonly summary?: GitStatusSummary;
|
|
14
|
+
readonly entries?: readonly GitLogEntry[];
|
|
15
|
+
readonly result?: GitDiffResult;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatGitCall(args: { action?: unknown; directory?: unknown; ref?: unknown }, theme: LectorTheme): string {
|
|
19
|
+
const action = typeof args.action === "string" ? args.action : "";
|
|
10
20
|
const directory = typeof args.directory === "string" ? args.directory : "";
|
|
11
|
-
|
|
21
|
+
const ref = typeof args.ref === "string" ? ` ${args.ref}` : "";
|
|
22
|
+
return `${theme.fg("toolTitle", theme.bold("git"))} ${theme.fg("muted", action)} ${theme.fg("accent", directory)}${ref}`;
|
|
12
23
|
}
|
|
13
24
|
|
|
14
|
-
export function
|
|
25
|
+
export function formatGitResult(details: GitToolDetails | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
26
|
+
if (!details) return theme.fg("dim", "No result.");
|
|
27
|
+
if (details.action === "status") return formatGitStatusResult(details.summary, expanded, theme);
|
|
28
|
+
if (details.action === "log") return formatGitLogResult(details.entries, expanded, theme);
|
|
29
|
+
return formatGitDiffResult(details.result, expanded, theme);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatGitStatusResult(summary: GitStatusSummary | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
15
33
|
if (!summary) return theme.fg("dim", "No status available.");
|
|
16
34
|
const branch = summary.current ?? "(detached)";
|
|
17
35
|
const tracking = summary.tracking ? `, tracking ${summary.tracking} (+${summary.ahead}/-${summary.behind})` : "";
|
|
@@ -30,12 +48,7 @@ export function formatGitStatusResult(summary: GitStatusSummary | undefined, exp
|
|
|
30
48
|
return lines.join("\n");
|
|
31
49
|
}
|
|
32
50
|
|
|
33
|
-
|
|
34
|
-
const directory = typeof args.directory === "string" ? args.directory : "";
|
|
35
|
-
return `${theme.fg("toolTitle", theme.bold("git_log"))} ${theme.fg("accent", directory)}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function formatGitLogResult(entries: readonly GitLogEntry[] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
51
|
+
function formatGitLogResult(entries: readonly GitLogEntry[] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
39
52
|
if (!entries || entries.length === 0) return theme.fg("dim", "No commits found.");
|
|
40
53
|
const displayCount = expanded ? entries.length : Math.min(DEFAULT_VISIBLE_COMMITS, entries.length);
|
|
41
54
|
const lines = entries
|
|
@@ -46,13 +59,7 @@ export function formatGitLogResult(entries: readonly GitLogEntry[] | undefined,
|
|
|
46
59
|
return lines.join("\n");
|
|
47
60
|
}
|
|
48
61
|
|
|
49
|
-
|
|
50
|
-
const directory = typeof args.directory === "string" ? args.directory : "";
|
|
51
|
-
const ref = typeof args.ref === "string" ? ` ${args.ref}` : "";
|
|
52
|
-
return `${theme.fg("toolTitle", theme.bold("git_diff"))} ${theme.fg("accent", directory)}${ref}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function formatGitDiffResult(result: GitDiffResult | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
62
|
+
function formatGitDiffResult(result: GitDiffResult | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
56
63
|
if (!result || result.diff.length === 0) return theme.fg("dim", "No differences.");
|
|
57
64
|
const lines = result.diff.split("\n");
|
|
58
65
|
const displayCount = expanded ? lines.length : Math.min(DEFAULT_VISIBLE_DIFF_LINES, lines.length);
|
package/extension/src/index.ts
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import type {
|
|
3
|
-
CallHierarchyEntry,
|
|
4
3
|
ContentHash,
|
|
5
4
|
Diagnostic,
|
|
6
5
|
DocumentSymbolEntry,
|
|
7
6
|
EditOutcome,
|
|
8
7
|
FindFilesResult,
|
|
9
|
-
GitDiffResult,
|
|
10
|
-
GitLogEntry,
|
|
11
|
-
GitStatusSummary,
|
|
12
8
|
Hover,
|
|
13
|
-
IncomingCall,
|
|
14
9
|
IntelligenceProvenance,
|
|
15
10
|
JobSnapshot,
|
|
16
11
|
LineEdit,
|
|
17
12
|
LineEditOutcome,
|
|
18
|
-
OutgoingCall,
|
|
19
13
|
PackageSourceOperationResult,
|
|
20
14
|
PopulateSymbolGraphResult,
|
|
21
15
|
RepoFetchResult,
|
|
@@ -27,14 +21,23 @@ import type {
|
|
|
27
21
|
WorkspaceMapResult,
|
|
28
22
|
WorkspaceQueryOutcome,
|
|
29
23
|
} from "@danypops/lector";
|
|
30
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
type AgentToolResult,
|
|
26
|
+
createEditToolDefinition,
|
|
27
|
+
createReadToolDefinition,
|
|
28
|
+
createWriteToolDefinition,
|
|
29
|
+
type ExtensionAPI,
|
|
30
|
+
} from "@earendil-works/pi-coding-agent";
|
|
31
31
|
import { Text } from "@earendil-works/pi-tui";
|
|
32
32
|
import { Type } from "typebox";
|
|
33
33
|
import { createLectorApplyPatchOperations } from "./apply-patch-operations.ts";
|
|
34
34
|
import { formatApplyPatchCall, formatApplyPatchResult } from "./apply-patch-rendering.ts";
|
|
35
35
|
import { createLectorCodeIntelligenceOperations } from "./code-intelligence-operations.ts";
|
|
36
36
|
import {
|
|
37
|
+
type CallHierarchyToolDetails,
|
|
37
38
|
describePopulateSymbolGraphJob,
|
|
39
|
+
formatCallHierarchyCall,
|
|
40
|
+
formatCallHierarchyResult,
|
|
38
41
|
formatDiagnosticsCall,
|
|
39
42
|
formatDiagnosticsResult,
|
|
40
43
|
formatDocumentSymbolsCall,
|
|
@@ -47,14 +50,8 @@ import {
|
|
|
47
50
|
formatGoToImplementationResult,
|
|
48
51
|
formatHoverCall,
|
|
49
52
|
formatHoverResult,
|
|
50
|
-
formatIncomingCallsCall,
|
|
51
|
-
formatIncomingCallsResult,
|
|
52
|
-
formatOutgoingCallsCall,
|
|
53
|
-
formatOutgoingCallsResult,
|
|
54
53
|
formatPopulateSymbolGraphCall,
|
|
55
54
|
formatPopulateSymbolGraphResult,
|
|
56
|
-
formatPrepareCallHierarchyCall,
|
|
57
|
-
formatPrepareCallHierarchyResult,
|
|
58
55
|
formatReachableFromCall,
|
|
59
56
|
formatReachableFromResult,
|
|
60
57
|
formatWorkspaceMapCall,
|
|
@@ -68,7 +65,7 @@ import { formatFindFilesCall, formatFindFilesResult } from "./find-files-renderi
|
|
|
68
65
|
import { createLectorFindSymbolsOperations } from "./find-symbols-operations.ts";
|
|
69
66
|
import { describeFindSymbolSources, formatFindSymbolsCall, formatFindSymbolsResult } from "./find-symbols-rendering.ts";
|
|
70
67
|
import { createLectorGitOperations } from "./git-operations.ts";
|
|
71
|
-
import {
|
|
68
|
+
import { formatGitCall, formatGitResult, type GitToolDetails } from "./git-rendering.ts";
|
|
72
69
|
import { createLectorLineEditOperations } from "./line-edit-operations.ts";
|
|
73
70
|
import { formatLineEditCall, formatLineEditResult } from "./line-edit-rendering.ts";
|
|
74
71
|
import { nearestGitRoot } from "./nearest-workspace-root.ts";
|
|
@@ -221,7 +218,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
221
218
|
},
|
|
222
219
|
renderCall(args, theme, context) {
|
|
223
220
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
224
|
-
text.setText(formatFindSymbolsCall(args
|
|
221
|
+
text.setText(formatFindSymbolsCall(args, theme));
|
|
225
222
|
return text;
|
|
226
223
|
},
|
|
227
224
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -236,7 +233,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
236
233
|
return new Text(theme.fg("error", errorText || "find_symbols failed"), 0, 0);
|
|
237
234
|
}
|
|
238
235
|
const details = result.details as SymbolSearchResult | undefined;
|
|
239
|
-
const query = typeof context.args
|
|
236
|
+
const query = typeof context.args.query === "string" ? context.args.query : "";
|
|
240
237
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
241
238
|
text.setText(formatFindSymbolsResult(details, query, expanded, theme));
|
|
242
239
|
return text;
|
|
@@ -265,7 +262,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
265
262
|
},
|
|
266
263
|
renderCall(args, theme, context) {
|
|
267
264
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
268
|
-
text.setText(formatGoToDefinitionCall(args
|
|
265
|
+
text.setText(formatGoToDefinitionCall(args, theme));
|
|
269
266
|
return text;
|
|
270
267
|
},
|
|
271
268
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -303,7 +300,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
303
300
|
},
|
|
304
301
|
renderCall(args, theme, context) {
|
|
305
302
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
306
|
-
text.setText(formatGoToImplementationCall(args
|
|
303
|
+
text.setText(formatGoToImplementationCall(args, theme));
|
|
307
304
|
return text;
|
|
308
305
|
},
|
|
309
306
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -348,7 +345,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
348
345
|
},
|
|
349
346
|
renderCall(args, theme, context) {
|
|
350
347
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
351
|
-
text.setText(formatFindReferencesCall(args
|
|
348
|
+
text.setText(formatFindReferencesCall(args, theme));
|
|
352
349
|
return text;
|
|
353
350
|
},
|
|
354
351
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -388,7 +385,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
388
385
|
},
|
|
389
386
|
renderCall(args, theme, context) {
|
|
390
387
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
391
|
-
text.setText(formatHoverCall(args
|
|
388
|
+
text.setText(formatHoverCall(args, theme));
|
|
392
389
|
return text;
|
|
393
390
|
},
|
|
394
391
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -422,7 +419,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
422
419
|
},
|
|
423
420
|
renderCall(args, theme, context) {
|
|
424
421
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
425
|
-
text.setText(formatDocumentSymbolsCall(args
|
|
422
|
+
text.setText(formatDocumentSymbolsCall(args, theme));
|
|
426
423
|
return text;
|
|
427
424
|
},
|
|
428
425
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -459,7 +456,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
459
456
|
},
|
|
460
457
|
renderCall(args, theme, context) {
|
|
461
458
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
462
|
-
text.setText(formatDiagnosticsCall(args
|
|
459
|
+
text.setText(formatDiagnosticsCall(args, theme));
|
|
463
460
|
return text;
|
|
464
461
|
},
|
|
465
462
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -479,118 +476,67 @@ export default function (pi: ExtensionAPI) {
|
|
|
479
476
|
});
|
|
480
477
|
|
|
481
478
|
pi.registerTool({
|
|
482
|
-
name: "
|
|
483
|
-
label: "
|
|
484
|
-
description:
|
|
485
|
-
|
|
479
|
+
name: "call_hierarchy",
|
|
480
|
+
label: "Call Hierarchy",
|
|
481
|
+
description:
|
|
482
|
+
"Resolve the symbol at an exact file position to its call-hierarchy root, or find its real callers/callees, project-wide, in one tool. ACTIONS: prepare (confirm what the position resolves to), incoming (who actually calls it, as distinct from find_references which also finds non-call usages), outgoing (what it itself calls).",
|
|
483
|
+
promptSnippet: "Resolve a position to a call-hierarchy root, or find its callers/callees",
|
|
486
484
|
promptGuidelines: [
|
|
487
|
-
"
|
|
485
|
+
"direction=prepare is optional -- incoming/outgoing already resolve the position internally, so calling prepare first is never required, only useful to confirm what a position resolves to.",
|
|
486
|
+
"direction=incoming finds real callers, distinct from find_references which also finds non-call usages like type positions or re-exports.",
|
|
488
487
|
],
|
|
489
|
-
parameters: Type.Object(
|
|
490
|
-
|
|
488
|
+
parameters: Type.Object({
|
|
489
|
+
direction: Type.String({ description: "prepare | incoming | outgoing" }),
|
|
490
|
+
...positionParameters,
|
|
491
|
+
}),
|
|
492
|
+
async execute(_toolCallId, params): Promise<AgentToolResult<CallHierarchyToolDetails>> {
|
|
491
493
|
const path = resolve(cwd, params.path);
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
501
|
-
text.setText(formatPrepareCallHierarchyCall(args as { path?: unknown; line?: unknown; character?: unknown }, theme));
|
|
502
|
-
return text;
|
|
503
|
-
},
|
|
504
|
-
renderResult(result, { isPartial }, theme, context) {
|
|
505
|
-
if (isPartial) return new Text(theme.fg("warning", "Resolving position..."), 0, 0);
|
|
506
|
-
if (context.isError) {
|
|
507
|
-
const errorText = result.content
|
|
508
|
-
.filter((block) => block.type === "text")
|
|
509
|
-
.map((block) => block.text)
|
|
510
|
-
.join("\n");
|
|
511
|
-
return new Text(theme.fg("error", errorText || "prepare_call_hierarchy failed"), 0, 0);
|
|
494
|
+
if (params.direction === "prepare") {
|
|
495
|
+
const { items, provenance } = await codeIntelligenceOperations.prepareCallHierarchy(path, params.line, params.character);
|
|
496
|
+
const text =
|
|
497
|
+
items.length === 0
|
|
498
|
+
? "No call-hierarchy root at this position."
|
|
499
|
+
: items.map((i) => `${i.kind} ${i.name} -- ${i.location.path}:${i.location.line}:${i.location.character}`).join("\n");
|
|
500
|
+
const details: CallHierarchyToolDetails = { direction: "prepare", items, provenance };
|
|
501
|
+
return { content: [{ type: "text", text: `${describeIntelligenceSource(provenance)}\n${text}` }], details };
|
|
512
502
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
name: "incoming_calls",
|
|
522
|
-
label: "Incoming Calls",
|
|
523
|
-
description: "Find every real caller of the function/method at an exact file position, project-wide.",
|
|
524
|
-
promptSnippet: "Find every caller of a function from an exact position",
|
|
525
|
-
promptGuidelines: [
|
|
526
|
-
"Use incoming_calls to see who actually calls a function, as distinct from find_references, which also finds non-call usages like type positions or re-exports.",
|
|
527
|
-
],
|
|
528
|
-
parameters: Type.Object(positionParameters),
|
|
529
|
-
async execute(_toolCallId, params) {
|
|
530
|
-
const path = resolve(cwd, params.path);
|
|
531
|
-
const details = await codeIntelligenceOperations.incomingCalls(path, params.line, params.character);
|
|
532
|
-
const text =
|
|
533
|
-
details.calls.length === 0
|
|
534
|
-
? "No incoming calls found."
|
|
535
|
-
: details.calls
|
|
536
|
-
.map((c) => `${c.from.kind} ${c.from.name} -- ${c.from.location.path}:${c.from.location.line}:${c.from.location.character}`)
|
|
537
|
-
.join("\n");
|
|
538
|
-
return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
|
|
539
|
-
},
|
|
540
|
-
renderCall(args, theme, context) {
|
|
541
|
-
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
542
|
-
text.setText(formatIncomingCallsCall(args as { path?: unknown; line?: unknown; character?: unknown }, theme));
|
|
543
|
-
return text;
|
|
544
|
-
},
|
|
545
|
-
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
546
|
-
if (isPartial) return new Text(theme.fg("warning", "Searching for callers..."), 0, 0);
|
|
547
|
-
if (context.isError) {
|
|
548
|
-
const errorText = result.content
|
|
549
|
-
.filter((block) => block.type === "text")
|
|
550
|
-
.map((block) => block.text)
|
|
551
|
-
.join("\n");
|
|
552
|
-
return new Text(theme.fg("error", errorText || "incoming_calls failed"), 0, 0);
|
|
503
|
+
if (params.direction === "incoming") {
|
|
504
|
+
const { calls, provenance } = await codeIntelligenceOperations.incomingCalls(path, params.line, params.character);
|
|
505
|
+
const text =
|
|
506
|
+
calls.length === 0
|
|
507
|
+
? "No incoming calls found."
|
|
508
|
+
: calls.map((c) => `${c.from.kind} ${c.from.name} -- ${c.from.location.path}:${c.from.location.line}:${c.from.location.character}`).join("\n");
|
|
509
|
+
const details: CallHierarchyToolDetails = { direction: "incoming", calls, provenance };
|
|
510
|
+
return { content: [{ type: "text", text: `${describeIntelligenceSource(provenance)}\n${text}` }], details };
|
|
553
511
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
description: "Find every function/method the function at an exact file position itself calls.",
|
|
565
|
-
promptSnippet: "Find every function a function itself calls",
|
|
566
|
-
promptGuidelines: ["Use outgoing_calls to see what a function calls internally, e.g. to trace a code path forward without opening every file by hand."],
|
|
567
|
-
parameters: Type.Object(positionParameters),
|
|
568
|
-
async execute(_toolCallId, params) {
|
|
569
|
-
const path = resolve(cwd, params.path);
|
|
570
|
-
const details = await codeIntelligenceOperations.outgoingCalls(path, params.line, params.character);
|
|
571
|
-
const text =
|
|
572
|
-
details.calls.length === 0
|
|
573
|
-
? "No outgoing calls found."
|
|
574
|
-
: details.calls.map((c) => `${c.to.kind} ${c.to.name} -- ${c.to.location.path}:${c.to.location.line}:${c.to.location.character}`).join("\n");
|
|
575
|
-
return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
|
|
512
|
+
if (params.direction === "outgoing") {
|
|
513
|
+
const { calls, provenance } = await codeIntelligenceOperations.outgoingCalls(path, params.line, params.character);
|
|
514
|
+
const text =
|
|
515
|
+
calls.length === 0
|
|
516
|
+
? "No outgoing calls found."
|
|
517
|
+
: calls.map((c) => `${c.to.kind} ${c.to.name} -- ${c.to.location.path}:${c.to.location.line}:${c.to.location.character}`).join("\n");
|
|
518
|
+
const details: CallHierarchyToolDetails = { direction: "outgoing", calls, provenance };
|
|
519
|
+
return { content: [{ type: "text", text: `${describeIntelligenceSource(provenance)}\n${text}` }], details };
|
|
520
|
+
}
|
|
521
|
+
throw new Error(`unknown call_hierarchy direction: ${String(params.direction)}`);
|
|
576
522
|
},
|
|
577
523
|
renderCall(args, theme, context) {
|
|
578
524
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
579
|
-
text.setText(
|
|
525
|
+
text.setText(formatCallHierarchyCall(args, theme));
|
|
580
526
|
return text;
|
|
581
527
|
},
|
|
582
528
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
583
|
-
if (isPartial) return new Text(theme.fg("warning", "
|
|
529
|
+
if (isPartial) return new Text(theme.fg("warning", "Resolving call hierarchy..."), 0, 0);
|
|
584
530
|
if (context.isError) {
|
|
585
531
|
const errorText = result.content
|
|
586
532
|
.filter((block) => block.type === "text")
|
|
587
533
|
.map((block) => block.text)
|
|
588
534
|
.join("\n");
|
|
589
|
-
return new Text(theme.fg("error", errorText || "
|
|
535
|
+
return new Text(theme.fg("error", errorText || "call_hierarchy failed"), 0, 0);
|
|
590
536
|
}
|
|
591
|
-
const details = result.details as
|
|
537
|
+
const details = result.details as CallHierarchyToolDetails | undefined;
|
|
592
538
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
593
|
-
text.setText(renderIntelligenceSource(
|
|
539
|
+
text.setText(renderIntelligenceSource(formatCallHierarchyResult(details, expanded, theme), details?.provenance, theme));
|
|
594
540
|
return text;
|
|
595
541
|
},
|
|
596
542
|
});
|
|
@@ -599,7 +545,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
599
545
|
name: "populate_symbol_graph",
|
|
600
546
|
label: "Populate Symbol Graph",
|
|
601
547
|
description:
|
|
602
|
-
"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/
|
|
548
|
+
"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.",
|
|
603
549
|
promptSnippet: "Populate a workspace's symbol graph for multi-hop queries",
|
|
604
550
|
promptGuidelines: [
|
|
605
551
|
"Run populate_symbol_graph once for a workspace before using reachable_from against it; an unpopulated workspace's graph is empty, not an error.",
|
|
@@ -622,7 +568,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
622
568
|
},
|
|
623
569
|
renderCall(args, theme, context) {
|
|
624
570
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
625
|
-
text.setText(formatPopulateSymbolGraphCall(args
|
|
571
|
+
text.setText(formatPopulateSymbolGraphCall(args, theme));
|
|
626
572
|
return text;
|
|
627
573
|
},
|
|
628
574
|
renderResult(result, { isPartial }, theme, context) {
|
|
@@ -685,20 +631,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
685
631
|
annotations?: readonly SymbolAnnotation[];
|
|
686
632
|
scrubbed?: boolean;
|
|
687
633
|
restored?: boolean;
|
|
634
|
+
contained?: boolean;
|
|
635
|
+
uncontained?: boolean;
|
|
688
636
|
}
|
|
689
637
|
|
|
690
638
|
pi.registerTool({
|
|
691
639
|
name: "symbol_annotations",
|
|
692
640
|
label: "Symbol Annotations",
|
|
693
641
|
description:
|
|
694
|
-
'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 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. Actions: create, get, list, refresh, scrub, restore.',
|
|
642
|
+
'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.',
|
|
695
643
|
promptSnippet: "Attach, read, or invalidate narrative annotations on the symbol graph",
|
|
696
644
|
promptGuidelines: [
|
|
697
645
|
"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.",
|
|
698
646
|
"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.",
|
|
647
|
+
"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.",
|
|
648
|
+
"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.",
|
|
699
649
|
],
|
|
700
650
|
parameters: Type.Object({
|
|
701
|
-
action: Type.String({ description: "create | get | list | refresh | scrub | restore" }),
|
|
651
|
+
action: Type.String({ description: "create | get | list | refresh | scrub | restore | contain | uncontain | tree" }),
|
|
702
652
|
path: Type.String({ description: "Absolute or cwd-relative path used to resolve which workspace this annotation belongs to" }),
|
|
703
653
|
id: Type.Optional(Type.String({ description: "Annotation id -- required for get/refresh/scrub/restore" })),
|
|
704
654
|
subtype: Type.Optional(Type.String({ description: 'Free-form kind, e.g. "user-story-dataflow" or "comment" -- required for create/refresh' })),
|
|
@@ -716,7 +666,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
716
666
|
),
|
|
717
667
|
listStatus: Type.Optional(Type.String({ description: "fresh | stale | scrubbed -- for list; defaults to excluding scrubbed" })),
|
|
718
668
|
listSubtype: Type.Optional(Type.String({ description: "For list: filter by subtype" })),
|
|
669
|
+
listQuery: Type.Optional(
|
|
670
|
+
Type.String({ description: "For list: case-insensitive substring match against title or body -- the near-term free-text search over annotations" }),
|
|
671
|
+
),
|
|
719
672
|
maxResults: Type.Optional(Type.Number({ description: "For list: bounds the number of results" })),
|
|
673
|
+
parentId: Type.Optional(Type.String({ description: "The containing annotation's id -- required for contain/uncontain" })),
|
|
674
|
+
childId: Type.Optional(Type.String({ description: "The contained annotation's id -- required for contain/uncontain" })),
|
|
675
|
+
rootId: Type.Optional(Type.String({ description: "The subtree's root annotation id -- required for tree" })),
|
|
676
|
+
maxDepth: Type.Optional(Type.Number({ description: "Maximum containment hops from rootId to include -- required for tree" })),
|
|
720
677
|
}),
|
|
721
678
|
async execute(_toolCallId, params) {
|
|
722
679
|
const path = resolve(cwd, params.path);
|
|
@@ -736,7 +693,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
736
693
|
text = annotation ? formatAnnotationDetail(annotation) : `no annotation "${params.id}"`;
|
|
737
694
|
} else if (params.action === "list") {
|
|
738
695
|
const status = params.listStatus === "fresh" || params.listStatus === "stale" || params.listStatus === "scrubbed" ? params.listStatus : undefined;
|
|
739
|
-
const { annotations } = await symbolAnnotationOperations.list(path, {
|
|
696
|
+
const { annotations } = await symbolAnnotationOperations.list(path, {
|
|
697
|
+
subtype: params.listSubtype,
|
|
698
|
+
status,
|
|
699
|
+
maxResults: params.maxResults,
|
|
700
|
+
query: params.listQuery,
|
|
701
|
+
});
|
|
740
702
|
details.annotations = annotations;
|
|
741
703
|
text = annotations.length === 0 ? "no annotations" : annotations.map(formatAnnotationDetail).join("\n\n");
|
|
742
704
|
} else if (params.action === "refresh") {
|
|
@@ -763,6 +725,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
763
725
|
const { restored } = await symbolAnnotationOperations.restore(path, params.id);
|
|
764
726
|
details.restored = restored;
|
|
765
727
|
text = restored ? `restored ${params.id}` : `"${params.id}" was not scrubbed or does not exist`;
|
|
728
|
+
} else if (params.action === "contain") {
|
|
729
|
+
if (!params.parentId || !params.childId) throw new Error("symbol_annotations contain requires parentId and childId");
|
|
730
|
+
const { contained } = await symbolAnnotationOperations.contain(path, params.parentId, params.childId);
|
|
731
|
+
details.contained = contained;
|
|
732
|
+
text = `"${params.parentId}" now contains "${params.childId}"`;
|
|
733
|
+
} else if (params.action === "uncontain") {
|
|
734
|
+
if (!params.parentId || !params.childId) throw new Error("symbol_annotations uncontain requires parentId and childId");
|
|
735
|
+
const { uncontained } = await symbolAnnotationOperations.uncontain(path, params.parentId, params.childId);
|
|
736
|
+
details.uncontained = uncontained;
|
|
737
|
+
text = uncontained ? `"${params.parentId}" no longer contains "${params.childId}"` : `"${params.parentId}" did not contain "${params.childId}"`;
|
|
738
|
+
} else if (params.action === "tree") {
|
|
739
|
+
if (!params.rootId || params.maxDepth === undefined) throw new Error("symbol_annotations tree requires rootId and maxDepth");
|
|
740
|
+
const { annotations } = await symbolAnnotationOperations.tree(path, params.rootId, params.maxDepth);
|
|
741
|
+
details.annotations = annotations;
|
|
742
|
+
text = annotations.length === 0 ? `no annotation "${params.rootId}"` : annotations.map(formatAnnotationDetail).join("\n\n");
|
|
766
743
|
} else {
|
|
767
744
|
throw new Error(`unknown symbol_annotations action: ${String(params.action)}`);
|
|
768
745
|
}
|
|
@@ -770,7 +747,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
770
747
|
},
|
|
771
748
|
renderCall(args, theme, context) {
|
|
772
749
|
const action = typeof args.action === "string" ? args.action : "";
|
|
773
|
-
const id =
|
|
750
|
+
const id =
|
|
751
|
+
typeof args.id === "string"
|
|
752
|
+
? ` ${args.id}`
|
|
753
|
+
: typeof args.parentId === "string" && typeof args.childId === "string"
|
|
754
|
+
? ` ${args.parentId} -> ${args.childId}`
|
|
755
|
+
: typeof args.rootId === "string"
|
|
756
|
+
? ` ${args.rootId}`
|
|
757
|
+
: "";
|
|
774
758
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
775
759
|
text.setText(`${theme.fg("toolTitle", theme.bold("symbol_annotations"))} ${theme.fg("accent", action)}${theme.fg("dim", id)}`);
|
|
776
760
|
return text;
|
|
@@ -803,6 +787,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
803
787
|
text.setText(details.restored ? theme.fg("success", "restored") : theme.fg("muted", "not scrubbed or not found"));
|
|
804
788
|
return text;
|
|
805
789
|
}
|
|
790
|
+
if (details?.contained !== undefined) {
|
|
791
|
+
text.setText(theme.fg("success", "contained"));
|
|
792
|
+
return text;
|
|
793
|
+
}
|
|
794
|
+
if (details?.uncontained !== undefined) {
|
|
795
|
+
text.setText(details.uncontained ? theme.fg("success", "uncontained") : theme.fg("muted", "was not contained"));
|
|
796
|
+
return text;
|
|
797
|
+
}
|
|
806
798
|
text.setText(theme.fg("muted", "done"));
|
|
807
799
|
return text;
|
|
808
800
|
},
|
|
@@ -812,10 +804,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
812
804
|
name: "reachable_from",
|
|
813
805
|
label: "Reachable From",
|
|
814
806
|
description:
|
|
815
|
-
"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/
|
|
807
|
+
"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.",
|
|
816
808
|
promptSnippet: "Find symbols reachable from a position, up to N hops, via the persisted graph",
|
|
817
809
|
promptGuidelines: [
|
|
818
|
-
"Use reachable_from for multi-hop questions (does A eventually call C through B); use
|
|
810
|
+
"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.",
|
|
819
811
|
],
|
|
820
812
|
parameters: Type.Object({
|
|
821
813
|
...positionParameters,
|
|
@@ -837,7 +829,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
837
829
|
},
|
|
838
830
|
renderCall(args, theme, context) {
|
|
839
831
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
840
|
-
text.setText(formatReachableFromCall(args
|
|
832
|
+
text.setText(formatReachableFromCall(args, theme));
|
|
841
833
|
return text;
|
|
842
834
|
},
|
|
843
835
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -888,7 +880,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
888
880
|
},
|
|
889
881
|
renderCall(args, theme, context) {
|
|
890
882
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
891
|
-
text.setText(formatWorkspaceMapCall(args
|
|
883
|
+
text.setText(formatWorkspaceMapCall(args, theme));
|
|
892
884
|
return text;
|
|
893
885
|
},
|
|
894
886
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -909,111 +901,61 @@ export default function (pi: ExtensionAPI) {
|
|
|
909
901
|
|
|
910
902
|
const gitOperations = createLectorGitOperations();
|
|
911
903
|
pi.registerTool({
|
|
912
|
-
name: "
|
|
913
|
-
label: "Git
|
|
904
|
+
name: "git",
|
|
905
|
+
label: "Git",
|
|
914
906
|
description:
|
|
915
|
-
"Working tree status for a real git repository
|
|
916
|
-
promptSnippet: "Show a repository's
|
|
907
|
+
"Working tree status, recent commit log, and unified diff for a real git repository, in one tool. Fails clearly if `directory` is not inside a git repository. ACTIONS: status (working tree state, ahead/behind tracking), log (recent commits, bounded by maxCount), diff (unified diff against `ref`, defaulting to HEAD, bounded by maxBytes).",
|
|
908
|
+
promptSnippet: "Show a repository's status, log, or diff",
|
|
909
|
+
promptGuidelines: [
|
|
910
|
+
"maxCount is required for action=log; maxBytes is required for action=diff -- every bounded query needs its bound stated explicitly, never defaulted silently.",
|
|
911
|
+
],
|
|
917
912
|
parameters: Type.Object({
|
|
913
|
+
action: Type.String({ description: "status | log | diff" }),
|
|
918
914
|
directory: Type.String({ description: "Directory inside the repository to check, absolute or relative to the current working directory" }),
|
|
915
|
+
maxCount: Type.Optional(Type.Number({ description: "Maximum number of commits to return, most recent first -- required for action=log" })),
|
|
916
|
+
ref: Type.Optional(Type.String({ description: "Ref to diff against; defaults to HEAD -- only used for action=diff" })),
|
|
917
|
+
maxBytes: Type.Optional(Type.Number({ description: "Maximum diff size in bytes before truncating -- required for action=diff" })),
|
|
919
918
|
}),
|
|
920
919
|
async execute(_toolCallId, params) {
|
|
921
920
|
const directory = resolve(cwd, params.directory);
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
927
|
-
text.setText(formatGitStatusCall(args as { directory?: unknown }, theme));
|
|
928
|
-
return text;
|
|
929
|
-
},
|
|
930
|
-
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
931
|
-
if (isPartial) return new Text(theme.fg("warning", "Checking status..."), 0, 0);
|
|
932
|
-
if (context.isError) {
|
|
933
|
-
const errorText = result.content
|
|
934
|
-
.filter((block) => block.type === "text")
|
|
935
|
-
.map((block) => block.text)
|
|
936
|
-
.join("\n");
|
|
937
|
-
return new Text(theme.fg("error", errorText || "git_status failed"), 0, 0);
|
|
921
|
+
if (params.action === "status") {
|
|
922
|
+
const summary = await gitOperations.status(directory);
|
|
923
|
+
const details: GitToolDetails = { action: "status", summary };
|
|
924
|
+
return { content: [{ type: "text", text: JSON.stringify(summary) }], details };
|
|
938
925
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
pi.registerTool({
|
|
947
|
-
name: "git_log",
|
|
948
|
-
label: "Git Log",
|
|
949
|
-
description:
|
|
950
|
-
"Recent commits for a real git repository, most recent first, bounded to maxCount. Fails clearly if `directory` is not inside a git repository.",
|
|
951
|
-
promptSnippet: "List a repository's recent commits",
|
|
952
|
-
parameters: Type.Object({
|
|
953
|
-
directory: Type.String({ description: "Directory inside the repository to check, absolute or relative to the current working directory" }),
|
|
954
|
-
maxCount: Type.Number({ description: "Maximum number of commits to return, most recent first" }),
|
|
955
|
-
}),
|
|
956
|
-
async execute(_toolCallId, params) {
|
|
957
|
-
const directory = resolve(cwd, params.directory);
|
|
958
|
-
const entries = await gitOperations.log(directory, params.maxCount);
|
|
959
|
-
const text =
|
|
960
|
-
entries.length === 0 ? "No commits found." : entries.map((e) => `${e.sha.slice(0, 8)} ${e.authoredAt} ${e.authorName} -- ${e.message}`).join("\n");
|
|
961
|
-
return { content: [{ type: "text", text }], details: { entries } };
|
|
962
|
-
},
|
|
963
|
-
renderCall(args, theme, context) {
|
|
964
|
-
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
965
|
-
text.setText(formatGitLogCall(args as { directory?: unknown; maxCount?: unknown }, theme));
|
|
966
|
-
return text;
|
|
967
|
-
},
|
|
968
|
-
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
969
|
-
if (isPartial) return new Text(theme.fg("warning", "Reading log..."), 0, 0);
|
|
970
|
-
if (context.isError) {
|
|
971
|
-
const errorText = result.content
|
|
972
|
-
.filter((block) => block.type === "text")
|
|
973
|
-
.map((block) => block.text)
|
|
974
|
-
.join("\n");
|
|
975
|
-
return new Text(theme.fg("error", errorText || "git_log failed"), 0, 0);
|
|
926
|
+
if (params.action === "log") {
|
|
927
|
+
if (params.maxCount === undefined) throw new Error("git action=log requires maxCount");
|
|
928
|
+
const entries = await gitOperations.log(directory, params.maxCount);
|
|
929
|
+
const text =
|
|
930
|
+
entries.length === 0 ? "No commits found." : entries.map((e) => `${e.sha.slice(0, 8)} ${e.authoredAt} ${e.authorName} -- ${e.message}`).join("\n");
|
|
931
|
+
const details: GitToolDetails = { action: "log", entries };
|
|
932
|
+
return { content: [{ type: "text", text }], details };
|
|
976
933
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
pi.registerTool({
|
|
985
|
-
name: "git_diff",
|
|
986
|
-
label: "Git Diff",
|
|
987
|
-
description:
|
|
988
|
-
"Unified diff of the working tree against `ref` (defaults to HEAD) for a real git repository, bounded to maxBytes. Fails clearly if `directory` is not inside a git repository.",
|
|
989
|
-
promptSnippet: "Show a repository's working tree diff",
|
|
990
|
-
parameters: Type.Object({
|
|
991
|
-
directory: Type.String({ description: "Directory inside the repository to check, absolute or relative to the current working directory" }),
|
|
992
|
-
ref: Type.Optional(Type.String({ description: "Ref to diff against; defaults to HEAD" })),
|
|
993
|
-
maxBytes: Type.Number({ description: "Maximum diff size in bytes before truncating" }),
|
|
994
|
-
}),
|
|
995
|
-
async execute(_toolCallId, params) {
|
|
996
|
-
const directory = resolve(cwd, params.directory);
|
|
997
|
-
const result = await gitOperations.diff(directory, params.ref, params.maxBytes);
|
|
998
|
-
return { content: [{ type: "text", text: result.diff.length === 0 ? "No differences." : result.diff }], details: { result } };
|
|
934
|
+
if (params.action === "diff") {
|
|
935
|
+
if (params.maxBytes === undefined) throw new Error("git action=diff requires maxBytes");
|
|
936
|
+
const result = await gitOperations.diff(directory, params.ref, params.maxBytes);
|
|
937
|
+
const details: GitToolDetails = { action: "diff", result };
|
|
938
|
+
return { content: [{ type: "text", text: result.diff.length === 0 ? "No differences." : result.diff }], details };
|
|
939
|
+
}
|
|
940
|
+
throw new Error(`unknown git action: ${String(params.action)}`);
|
|
999
941
|
},
|
|
1000
942
|
renderCall(args, theme, context) {
|
|
1001
943
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1002
|
-
text.setText(
|
|
944
|
+
text.setText(formatGitCall(args, theme));
|
|
1003
945
|
return text;
|
|
1004
946
|
},
|
|
1005
947
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1006
|
-
if (isPartial) return new Text(theme.fg("warning", "
|
|
948
|
+
if (isPartial) return new Text(theme.fg("warning", "Running git..."), 0, 0);
|
|
1007
949
|
if (context.isError) {
|
|
1008
950
|
const errorText = result.content
|
|
1009
951
|
.filter((block) => block.type === "text")
|
|
1010
952
|
.map((block) => block.text)
|
|
1011
953
|
.join("\n");
|
|
1012
|
-
return new Text(theme.fg("error", errorText || "
|
|
954
|
+
return new Text(theme.fg("error", errorText || "git failed"), 0, 0);
|
|
1013
955
|
}
|
|
1014
|
-
const details = result.details as
|
|
956
|
+
const details = result.details as GitToolDetails | undefined;
|
|
1015
957
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1016
|
-
text.setText(
|
|
958
|
+
text.setText(formatGitResult(details, expanded, theme));
|
|
1017
959
|
return text;
|
|
1018
960
|
},
|
|
1019
961
|
});
|
|
@@ -1040,7 +982,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1040
982
|
},
|
|
1041
983
|
renderCall(args, theme, context) {
|
|
1042
984
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1043
|
-
text.setText(formatSearchCall(args
|
|
985
|
+
text.setText(formatSearchCall(args, theme));
|
|
1044
986
|
return text;
|
|
1045
987
|
},
|
|
1046
988
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -1086,7 +1028,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1086
1028
|
},
|
|
1087
1029
|
renderCall(args, theme, context) {
|
|
1088
1030
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1089
|
-
text.setText(formatFindFilesCall(args
|
|
1031
|
+
text.setText(formatFindFilesCall(args, theme));
|
|
1090
1032
|
return text;
|
|
1091
1033
|
},
|
|
1092
1034
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -1151,12 +1093,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1151
1093
|
// The Pi tool schema can only express plain strings for hash fields (TypeBox has no
|
|
1152
1094
|
// concept of Lector's branded LineHash) -- the daemon's own domain validation is the
|
|
1153
1095
|
// real runtime check regardless of what TypeScript sees at this call site.
|
|
1096
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1154
1097
|
const result = await lineEditOperations.lineEdit(absolutePath, params.edits as unknown as LineEdit[]);
|
|
1155
1098
|
return { content: [{ type: "text", text: `${result.path}: ${result.previousHash} -> ${result.newHash}` }], details: { result } };
|
|
1156
1099
|
},
|
|
1157
1100
|
renderCall(args, theme, context) {
|
|
1158
1101
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1159
|
-
text.setText(formatLineEditCall(args
|
|
1102
|
+
text.setText(formatLineEditCall(args, theme));
|
|
1160
1103
|
return text;
|
|
1161
1104
|
},
|
|
1162
1105
|
renderResult(result, { isPartial }, theme, context) {
|
|
@@ -1197,12 +1140,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1197
1140
|
const absolutePath = resolve(cwd, params.path);
|
|
1198
1141
|
// TypeBox has no concept of Lector's branded ContentHash -- the daemon's own domain
|
|
1199
1142
|
// validation is the real runtime check regardless of what TypeScript sees here.
|
|
1143
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1200
1144
|
const result = await applyPatchOperations.applyPatch(absolutePath, params.expectedHash as ContentHash, params.patchText);
|
|
1201
1145
|
return { content: [{ type: "text", text: `${result.path}: ${result.previousHash ?? "(new)"} -> ${result.newHash}` }], details: { result } };
|
|
1202
1146
|
},
|
|
1203
1147
|
renderCall(args, theme, context) {
|
|
1204
1148
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1205
|
-
text.setText(formatApplyPatchCall(args
|
|
1149
|
+
text.setText(formatApplyPatchCall(args, theme));
|
|
1206
1150
|
return text;
|
|
1207
1151
|
},
|
|
1208
1152
|
renderResult(result, { isPartial }, theme, context) {
|
|
@@ -1241,7 +1185,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1241
1185
|
},
|
|
1242
1186
|
renderCall(args, theme, context) {
|
|
1243
1187
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1244
|
-
text.setText(formatPackageSourceCall(args
|
|
1188
|
+
text.setText(formatPackageSourceCall(args, theme));
|
|
1245
1189
|
return text;
|
|
1246
1190
|
},
|
|
1247
1191
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -1279,7 +1223,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1279
1223
|
},
|
|
1280
1224
|
renderCall(args, theme, context) {
|
|
1281
1225
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1282
|
-
text.setText(formatRepoFetchCall(args
|
|
1226
|
+
text.setText(formatRepoFetchCall(args, theme));
|
|
1283
1227
|
return text;
|
|
1284
1228
|
},
|
|
1285
1229
|
renderResult(result, { isPartial }, theme, context) {
|
|
@@ -1317,7 +1261,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1317
1261
|
},
|
|
1318
1262
|
renderCall(args, theme, context) {
|
|
1319
1263
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1320
|
-
text.setText(formatCrossWorkspaceCall(args
|
|
1264
|
+
text.setText(formatCrossWorkspaceCall(args, theme));
|
|
1321
1265
|
return text;
|
|
1322
1266
|
},
|
|
1323
1267
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -1356,7 +1300,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1356
1300
|
},
|
|
1357
1301
|
renderCall(args, theme, context) {
|
|
1358
1302
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1359
|
-
text.setText(formatCrossWorkspaceCall(args
|
|
1303
|
+
text.setText(formatCrossWorkspaceCall(args, theme));
|
|
1360
1304
|
return text;
|
|
1361
1305
|
},
|
|
1362
1306
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync, statSync } from "node:fs";
|
|
2
|
-
import { dirname, parse } from "node:path";
|
|
2
|
+
import { dirname, extname, parse } from "node:path";
|
|
3
3
|
import { createRetryingClient, type RetryingClient } from "@danypops/daemon-kit/pi-client";
|
|
4
4
|
import {
|
|
5
5
|
connectLectorClient,
|
|
6
|
+
descriptorForExtension,
|
|
6
7
|
type LectorClient,
|
|
7
8
|
type OperationInputs,
|
|
8
9
|
type OperationName,
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
remoteErrorIs,
|
|
11
12
|
type WorkspaceId,
|
|
12
13
|
} from "@danypops/lector";
|
|
13
|
-
import { nearestGitRoot } from "./nearest-workspace-root.ts";
|
|
14
|
+
import { nearestGitRoot, nearestProjectRoot } from "./nearest-workspace-root.ts";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Lazily connects to a running Lector daemon and caches, per project root,
|
|
@@ -43,9 +44,8 @@ export interface RetryingLectorClient {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
// Kept async even though its own body has no await: every call site across this package does
|
|
46
|
-
// `await lectorClient()`, and dropping async here
|
|
47
|
-
//
|
|
48
|
-
// eslint-disable-next-line @typescript-eslint/require-await
|
|
47
|
+
// `await lectorClient()`, and dropping async here would turn an internal implementation detail
|
|
48
|
+
// into a signature change rippling through every one of them.
|
|
49
49
|
export async function lectorClient(): Promise<RetryingLectorClient> {
|
|
50
50
|
return {
|
|
51
51
|
call: (operation, input) => retryingClient.call((client) => client.call(operation, input)),
|
|
@@ -107,9 +107,17 @@ export function workspaceForDirectory(directory: string): Promise<ResolvedWorksp
|
|
|
107
107
|
* whose filesystem-root fallback would point a real server at scanning the
|
|
108
108
|
* whole disk. Falls back to the file's own containing directory instead,
|
|
109
109
|
* same bound as workspaceForDirectory.
|
|
110
|
+
*
|
|
111
|
+
* Unlike workspaceForDirectory, prefers the file's own language's root markers
|
|
112
|
+
* (tsconfig.json, go.mod, Cargo.toml, ...) over the nearest .git when both exist --
|
|
113
|
+
* a monorepo subproject's own root marker is nearer and wins, so its language server
|
|
114
|
+
* gets that subproject's rootUri instead of the whole repo's.
|
|
110
115
|
*/
|
|
111
116
|
export function workspaceForCodeIntelligencePath(absolutePath: string): Promise<ResolvedWorkspace> {
|
|
112
|
-
|
|
117
|
+
const directory = dirname(absolutePath);
|
|
118
|
+
const descriptor = descriptorForExtension(extname(absolutePath));
|
|
119
|
+
const root = descriptor ? (nearestProjectRoot(directory, descriptor.rootMarkers) ?? directory) : (nearestGitRoot(directory) ?? directory);
|
|
120
|
+
return workspaceForRoot(root);
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
/**
|
|
@@ -21,14 +21,30 @@ import { dirname, join, parse } from "node:path";
|
|
|
21
21
|
* workspace root" error -- discovered live, in a separate session, working
|
|
22
22
|
* against a completely different, unrelated repository.)
|
|
23
23
|
*/
|
|
24
|
-
|
|
24
|
+
function walkUpForMarkers(startDirectory: string, markers: readonly string[]): string | undefined {
|
|
25
25
|
let dir = startDirectory;
|
|
26
26
|
const fsRoot = parse(dir).root;
|
|
27
27
|
while (dir !== fsRoot) {
|
|
28
|
-
if (existsSync(join(dir,
|
|
28
|
+
if (markers.some((marker) => existsSync(join(dir, marker)))) return dir;
|
|
29
29
|
const parent = dirname(dir);
|
|
30
30
|
if (parent === dir) break; // defensive: dirname must be strictly ascending
|
|
31
31
|
dir = parent;
|
|
32
32
|
}
|
|
33
|
-
return existsSync(join(fsRoot,
|
|
33
|
+
return markers.some((marker) => existsSync(join(fsRoot, marker))) ? fsRoot : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function nearestGitRoot(startDirectory: string): string | undefined {
|
|
37
|
+
return walkUpForMarkers(startDirectory, [".git"]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Same nearest-enclosing-root walk as nearestGitRoot, but also checks a language's own root
|
|
42
|
+
* markers (tsconfig.json, go.mod, Cargo.toml, ...) at each directory, nearest first -- so a
|
|
43
|
+
* monorepo subproject with its own root marker resolves to itself, not the outer repo's .git.
|
|
44
|
+
* Found via @arvoretech/pi-lsp comparison: without this, a file inside a monorepo subproject
|
|
45
|
+
* misattributes its whole project to the repo root, handing the language server the wrong
|
|
46
|
+
* rootUri (and, for TypeScript, the wrong tsconfig.json) even though a closer one exists.
|
|
47
|
+
*/
|
|
48
|
+
export function nearestProjectRoot(startDirectory: string, rootMarkers: readonly string[]): string | undefined {
|
|
49
|
+
return walkUpForMarkers(startDirectory, [...rootMarkers, ".git"]);
|
|
34
50
|
}
|
|
@@ -25,7 +25,7 @@ export interface SymbolAnnotationOperations {
|
|
|
25
25
|
get(path: string, id: string): Promise<OperationOutputs["workspace.getAnnotation"]>;
|
|
26
26
|
list(
|
|
27
27
|
path: string,
|
|
28
|
-
options?: { subtype?: string; status?: OperationInputs["workspace.listAnnotations"]["status"]; maxResults?: number },
|
|
28
|
+
options?: { subtype?: string; status?: OperationInputs["workspace.listAnnotations"]["status"]; maxResults?: number; query?: string },
|
|
29
29
|
): Promise<OperationOutputs["workspace.listAnnotations"]>;
|
|
30
30
|
refresh(
|
|
31
31
|
path: string,
|
|
@@ -37,6 +37,9 @@ export interface SymbolAnnotationOperations {
|
|
|
37
37
|
): Promise<OperationOutputs["workspace.refreshAnnotation"]>;
|
|
38
38
|
scrub(path: string, id: string): Promise<OperationOutputs["workspace.scrubAnnotation"]>;
|
|
39
39
|
restore(path: string, id: string): Promise<OperationOutputs["workspace.restoreAnnotation"]>;
|
|
40
|
+
contain(path: string, parentId: string, childId: string): Promise<OperationOutputs["workspace.containAnnotation"]>;
|
|
41
|
+
uncontain(path: string, parentId: string, childId: string): Promise<OperationOutputs["workspace.uncontainAnnotation"]>;
|
|
42
|
+
tree(path: string, rootId: string, maxDepth: number): Promise<OperationOutputs["workspace.annotationTree"]>;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperations {
|
|
@@ -64,7 +67,13 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
|
|
|
64
67
|
() => workspaceForCodeIntelligencePath(path),
|
|
65
68
|
async ({ workspaceId }) => {
|
|
66
69
|
const client = await lectorClient();
|
|
67
|
-
return client.call("workspace.listAnnotations", {
|
|
70
|
+
return client.call("workspace.listAnnotations", {
|
|
71
|
+
workspaceId,
|
|
72
|
+
subtype: options.subtype,
|
|
73
|
+
status: options.status,
|
|
74
|
+
maxResults: options.maxResults,
|
|
75
|
+
query: options.query,
|
|
76
|
+
});
|
|
68
77
|
},
|
|
69
78
|
);
|
|
70
79
|
},
|
|
@@ -95,5 +104,32 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
|
|
|
95
104
|
},
|
|
96
105
|
);
|
|
97
106
|
},
|
|
107
|
+
async contain(path, parentId, childId) {
|
|
108
|
+
return withWorkspace(
|
|
109
|
+
() => workspaceForCodeIntelligencePath(path),
|
|
110
|
+
async ({ workspaceId }) => {
|
|
111
|
+
const client = await lectorClient();
|
|
112
|
+
return client.call("workspace.containAnnotation", { workspaceId, parentId, childId });
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
},
|
|
116
|
+
async uncontain(path, parentId, childId) {
|
|
117
|
+
return withWorkspace(
|
|
118
|
+
() => workspaceForCodeIntelligencePath(path),
|
|
119
|
+
async ({ workspaceId }) => {
|
|
120
|
+
const client = await lectorClient();
|
|
121
|
+
return client.call("workspace.uncontainAnnotation", { workspaceId, parentId, childId });
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
},
|
|
125
|
+
async tree(path, rootId, maxDepth) {
|
|
126
|
+
return withWorkspace(
|
|
127
|
+
() => workspaceForCodeIntelligencePath(path),
|
|
128
|
+
async ({ workspaceId }) => {
|
|
129
|
+
const client = await lectorClient();
|
|
130
|
+
return client.call("workspace.annotationTree", { workspaceId, rootId, maxDepth });
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
},
|
|
98
134
|
};
|
|
99
135
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-lector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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
|
-
"@danypops/lector": "^0.
|
|
21
|
+
"@danypops/daemon-kit": "^0.22.1",
|
|
22
|
+
"@danypops/lector": "^0.3.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@earendil-works/pi-ai": "^0.81.1",
|