@danypops/pi-lector 0.13.6 → 0.13.7
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/operations.ts +21 -0
- package/extension/src/cross-workspace-search/rendering.ts +2 -1
- package/extension/src/editor/index.ts +7 -1
- package/extension/src/editor/modal-editor-component.ts +44 -6
- package/extension/src/index.ts +119 -25
- package/extension/src/mutation-history/operations.ts +57 -10
- package/extension/src/mutation-history/rendering.ts +20 -0
- package/extension/src/package-source/operations.ts +9 -3
- package/extension/src/search/rendering.ts +2 -1
- package/package.json +6 -4
|
@@ -50,6 +50,18 @@ export interface CodeIntelligenceOperations {
|
|
|
50
50
|
/** Never spawns a symbol index -- safe to call opportunistically (e.g. before deciding whether to enrich a result). */
|
|
51
51
|
hasWarmIndex(path: string): Promise<boolean>;
|
|
52
52
|
workspaceMap(path: string, maxNodes: number, maxEdges: number, maxEntries: number, maxBytes: number): Promise<OperationOutputs["workspace.map"]>;
|
|
53
|
+
localizeContext(
|
|
54
|
+
path: string,
|
|
55
|
+
query: string,
|
|
56
|
+
options?: {
|
|
57
|
+
seedSymbols?: readonly string[];
|
|
58
|
+
seedLocations?: readonly { path: string; line: number; character?: number }[];
|
|
59
|
+
maxSymbols?: number;
|
|
60
|
+
maxBytes?: number;
|
|
61
|
+
maxDepth?: number;
|
|
62
|
+
deadlineMs?: number;
|
|
63
|
+
},
|
|
64
|
+
): Promise<OperationOutputs["workspace.localizeContext"]>;
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
export function createLectorCodeIntelligenceOperations(ownerId?: string): CodeIntelligenceOperations {
|
|
@@ -184,5 +196,14 @@ export function createLectorCodeIntelligenceOperations(ownerId?: string): CodeIn
|
|
|
184
196
|
},
|
|
185
197
|
);
|
|
186
198
|
},
|
|
199
|
+
async localizeContext(path, query, options = {}) {
|
|
200
|
+
return withWorkspace(
|
|
201
|
+
() => workspaceForPathOrDirectory(path),
|
|
202
|
+
async ({ workspaceId }) => {
|
|
203
|
+
const client = await lectorClient();
|
|
204
|
+
return client.call("workspace.localizeContext", { workspaceId, query, ...options });
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
},
|
|
187
208
|
};
|
|
188
209
|
}
|
|
@@ -92,7 +92,8 @@ export function formatSearchTextAcrossProjectsResult(
|
|
|
92
92
|
items: outcome.result.matches,
|
|
93
93
|
expanded,
|
|
94
94
|
visibleCount: DEFAULT_VISIBLE_PER_WORKSPACE,
|
|
95
|
-
formatItem: (match) =>
|
|
95
|
+
formatItem: (match) =>
|
|
96
|
+
` ${match.path}:${match.lineNumber}: ${match.line.replace(/\n$/, "")}${match.lineTruncated ? theme.fg("warning", " (line truncated)") : ""}`,
|
|
96
97
|
moreLine: (hidden) => theme.fg("dim", ` ... ${hidden} more (${keyHint("app.tools.expand", "to expand")})`),
|
|
97
98
|
truncationWarning: outcome.result.truncated
|
|
98
99
|
? theme.fg("warning", " (this workspace's search was itself truncated by maxMatches/maxBytes)")
|
|
@@ -38,4 +38,10 @@ export { ExplorerComponent, type ExplorerResult, joinExplorerPath } from "./expl
|
|
|
38
38
|
// exact, already-tested loop without re-deriving it, instead of only this package's own Pi
|
|
39
39
|
// extension entry point being able to.
|
|
40
40
|
export { type ExplorerFlowHost, runExplorerFlow } from "./explorer-flow.ts";
|
|
41
|
-
export {
|
|
41
|
+
export {
|
|
42
|
+
type EditorBufferSnapshot,
|
|
43
|
+
type EditorHoverOutcome,
|
|
44
|
+
type EditorHoverRequest,
|
|
45
|
+
ModalEditorComponent,
|
|
46
|
+
type ModalEditorHost,
|
|
47
|
+
} from "./modal-editor-component.ts";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { extname } from "node:path";
|
|
2
|
-
import type { HighlightSpan } from "@danypops/lector";
|
|
3
|
-
import { highlightSpans } from "@danypops/lector";
|
|
2
|
+
import type { ContentHash, HighlightSpan } from "@danypops/lector";
|
|
3
|
+
import { contentHashOf, highlightSpans } from "@danypops/lector";
|
|
4
4
|
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
6
6
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
@@ -10,12 +10,30 @@ import type { EditorTheme } from "./editor-theme.ts";
|
|
|
10
10
|
|
|
11
11
|
export type { EditorTheme } from "./editor-theme.ts";
|
|
12
12
|
|
|
13
|
+
export interface EditorBufferSnapshot {
|
|
14
|
+
readonly text: string;
|
|
15
|
+
readonly hash: ContentHash;
|
|
16
|
+
readonly dirty: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface EditorHoverRequest {
|
|
20
|
+
readonly line: number;
|
|
21
|
+
readonly character: number;
|
|
22
|
+
readonly buffer: EditorBufferSnapshot;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type EditorHoverOutcome =
|
|
26
|
+
| { readonly kind: "ready"; readonly hover?: { readonly contents: string } }
|
|
27
|
+
| { readonly kind: "stale-active-buffer"; readonly bufferHash: ContentHash };
|
|
28
|
+
|
|
13
29
|
export interface ModalEditorHost {
|
|
14
30
|
filePath: string;
|
|
15
31
|
/** Saves the buffer's current text through Lector's hash-guarded write. Throws (surfaced as a status message, not a crash) on a genuinely concurrent external change. */
|
|
16
32
|
save(text: string): Promise<void>;
|
|
17
|
-
/**
|
|
33
|
+
/** Resolves hover against the saved file for hosts that do not expose active-buffer semantics. */
|
|
18
34
|
hover(line: number, character: number): Promise<{ contents: string } | undefined>;
|
|
35
|
+
/** Resolves hover against the supplied active-buffer snapshot or reports that semantic evidence is stale. */
|
|
36
|
+
hoverSnapshot?(request: EditorHoverRequest): Promise<EditorHoverOutcome>;
|
|
19
37
|
}
|
|
20
38
|
|
|
21
39
|
const CAPTURE_COLOR: Record<string, ThemeColor> = {
|
|
@@ -111,9 +129,29 @@ export class ModalEditorComponent implements Component {
|
|
|
111
129
|
this.done();
|
|
112
130
|
return;
|
|
113
131
|
case "hover": {
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
132
|
+
const text = this.state.buffer.text;
|
|
133
|
+
const request = {
|
|
134
|
+
line: this.state.cursorLine,
|
|
135
|
+
character: this.state.cursorCharacter,
|
|
136
|
+
buffer: { text, hash: contentHashOf(text), dirty: this.state.dirty },
|
|
137
|
+
};
|
|
138
|
+
const outcome = this.host.hoverSnapshot
|
|
139
|
+
? await this.host.hoverSnapshot(request)
|
|
140
|
+
: { kind: "ready" as const, hover: await this.host.hover(request.line, request.character) };
|
|
141
|
+
switch (outcome.kind) {
|
|
142
|
+
case "ready": {
|
|
143
|
+
const firstLine = outcome.hover?.contents.split("\n")[0];
|
|
144
|
+
this.statusMessage = firstLine ?? "no hover info at this position";
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "stale-active-buffer":
|
|
148
|
+
this.statusMessage = "stale active buffer: save or discard changes before semantic queries";
|
|
149
|
+
break;
|
|
150
|
+
default: {
|
|
151
|
+
const exhaustive: never = outcome;
|
|
152
|
+
throw new Error(`Unhandled hover outcome: ${JSON.stringify(exhaustive)}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
117
155
|
break;
|
|
118
156
|
}
|
|
119
157
|
default: {
|
package/extension/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import type {
|
|
5
5
|
CachedRepositoryPage,
|
|
6
6
|
ContentHash,
|
|
7
|
+
ContextBundleResult,
|
|
7
8
|
Diagnostic,
|
|
8
9
|
DocumentSymbolEntry,
|
|
9
10
|
EditOutcome,
|
|
@@ -96,7 +97,8 @@ import { formatGitCall, formatGitResult, type GitToolDetails } from "./git/rende
|
|
|
96
97
|
import { nearestGitWorkspaceRoot, setNewWorkspaceObserver } from "./lector-client.ts";
|
|
97
98
|
import { createLectorLineEditOperations } from "./line-edit/operations.ts";
|
|
98
99
|
import { formatLineEditCall, formatLineEditResult } from "./line-edit/rendering.ts";
|
|
99
|
-
import { createMutationHistoryOperations } from "./mutation-history/operations.ts";
|
|
100
|
+
import { createMutationHistoryOperations, type MutationTransactionRevertOutcome } from "./mutation-history/operations.ts";
|
|
101
|
+
import { formatMutationHistoryList, formatMutationTransactionRevert } from "./mutation-history/rendering.ts";
|
|
100
102
|
import { createLectorPackageSourceOperations, type PackageSourceListPage } from "./package-source/operations.ts";
|
|
101
103
|
import {
|
|
102
104
|
buildPackageSourceListTableRows,
|
|
@@ -407,6 +409,74 @@ export default function (pi: ExtensionAPI) {
|
|
|
407
409
|
|
|
408
410
|
const codeIntelligenceOperations = createLectorCodeIntelligenceOperations(ownerId);
|
|
409
411
|
|
|
412
|
+
registerLectorTool({
|
|
413
|
+
name: "localize_context",
|
|
414
|
+
label: "Localize Context",
|
|
415
|
+
description:
|
|
416
|
+
"Localize a natural-language coding task to a bounded, ranked set of workspace symbols. Combines lexical source matches with the persisted call/reference/containment graph, returns compact signatures and explicit score reasons, and reports incomplete or unavailable graph coverage. The daemon does not invoke an LLM. `directory` selects the project explicitly.",
|
|
417
|
+
promptSnippet: "Localize a coding task to ranked symbols and compact graph-backed context",
|
|
418
|
+
promptGuidelines: [
|
|
419
|
+
"Use localize_context near the start of an unfamiliar implementation or debugging task to get a bounded candidate set before reading files one by one.",
|
|
420
|
+
"Treat every reason as provenance for a retrieval signal, not proof of semantic dataflow; inspect completeness before relying on graph omissions.",
|
|
421
|
+
],
|
|
422
|
+
parameters: Type.Object({
|
|
423
|
+
query: Type.String({ description: "Natural-language coding task or issue text" }),
|
|
424
|
+
directory: Type.String({ description: "Directory of the project to localize, absolute or relative to the current working directory" }),
|
|
425
|
+
seedSymbols: Type.Optional(Type.Array(Type.String(), { description: "Optional exact symbol names that anchor graph expansion", maxItems: 100 })),
|
|
426
|
+
seedLocations: Type.Optional(
|
|
427
|
+
Type.Array(
|
|
428
|
+
Type.Object({
|
|
429
|
+
path: Type.String(),
|
|
430
|
+
line: Type.Number({ description: "1-indexed line" }),
|
|
431
|
+
character: Type.Optional(Type.Number({ description: "Optional 1-indexed declaration character" })),
|
|
432
|
+
}),
|
|
433
|
+
{ description: "Optional declaration locations that anchor graph expansion", maxItems: 100 },
|
|
434
|
+
),
|
|
435
|
+
),
|
|
436
|
+
maxSymbols: Type.Optional(Type.Number({ description: "Maximum candidates returned (default 20, maximum 500)" })),
|
|
437
|
+
maxBytes: Type.Optional(Type.Number({ description: "Maximum serialized candidate bytes (default 30000, maximum 2 MiB)" })),
|
|
438
|
+
maxDepth: Type.Optional(Type.Number({ description: "Maximum call/reference/containment expansion depth (default 2, maximum 5)" })),
|
|
439
|
+
deadlineMs: Type.Optional(Type.Number({ description: "Wall-clock budget in milliseconds (default 5000, maximum 30000)" })),
|
|
440
|
+
}),
|
|
441
|
+
async execute(_toolCallId, params) {
|
|
442
|
+
const directory = resolve(cwd, params.directory);
|
|
443
|
+
const result = await codeIntelligenceOperations.localizeContext(directory, params.query, {
|
|
444
|
+
...(params.seedSymbols ? { seedSymbols: params.seedSymbols } : {}),
|
|
445
|
+
...(params.seedLocations ? { seedLocations: params.seedLocations.map((seed) => ({ ...seed, path: resolve(cwd, seed.path) })) } : {}),
|
|
446
|
+
...(params.maxSymbols !== undefined ? { maxSymbols: params.maxSymbols } : {}),
|
|
447
|
+
...(params.maxBytes !== undefined ? { maxBytes: params.maxBytes } : {}),
|
|
448
|
+
...(params.maxDepth !== undefined ? { maxDepth: params.maxDepth } : {}),
|
|
449
|
+
...(params.deadlineMs !== undefined ? { deadlineMs: params.deadlineMs } : {}),
|
|
450
|
+
});
|
|
451
|
+
const completeness = `lexical=${result.completeness.lexical}, graph=${result.completeness.graph}${result.completeness.deadlineReached ? ", deadline reached" : ""}${result.truncated ? ", truncated" : ""}`;
|
|
452
|
+
const text =
|
|
453
|
+
result.candidates.length === 0
|
|
454
|
+
? `No localization candidates.\nCompleteness: ${completeness}`
|
|
455
|
+
: `Primary candidates\n\n${result.candidates
|
|
456
|
+
.map(
|
|
457
|
+
(candidate, index) =>
|
|
458
|
+
`${index + 1}. ${candidate.name}\n ${candidate.path}:${candidate.line}:${candidate.character}\n ${candidate.signature ?? candidate.kind}\n Reasons:\n${candidate.reasons.map((reason) => ` - ${reason.detail} (+${reason.score})`).join("\n")}`,
|
|
459
|
+
)
|
|
460
|
+
.join("\n\n")}\n\nCompleteness: ${completeness}`;
|
|
461
|
+
return { content: [{ type: "text", text }], details: result };
|
|
462
|
+
},
|
|
463
|
+
renderCall(args, theme) {
|
|
464
|
+
return new Text(theme.fg("toolTitle", `localize ${typeof args.query === "string" ? args.query : "context"}`), 0, 0);
|
|
465
|
+
},
|
|
466
|
+
renderResult(result, { isPartial }, theme, context) {
|
|
467
|
+
if (isPartial) return new Text(theme.fg("warning", "Localizing..."), 0, 0);
|
|
468
|
+
if (context.isError) {
|
|
469
|
+
const errorText = result.content
|
|
470
|
+
.filter((block) => block.type === "text")
|
|
471
|
+
.map((block) => block.text)
|
|
472
|
+
.join("\n");
|
|
473
|
+
return new Text(theme.fg("error", errorText || "localize_context failed"), 0, 0);
|
|
474
|
+
}
|
|
475
|
+
const details = result.details as ContextBundleResult | undefined;
|
|
476
|
+
return new Text(details ? `${details.candidates.length} candidates · graph ${details.completeness.graph}` : "Localization complete", 0, 0);
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
|
|
410
480
|
const editorOverlayOptions = { overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", anchor: "center" } } as const;
|
|
411
481
|
|
|
412
482
|
async function openFileInEditor(commandCtx: ExtensionCommandContext, absolutePath: string): Promise<void> {
|
|
@@ -426,6 +496,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
426
496
|
const result = await codeIntelligenceOperations.hover(absolutePath, line, character);
|
|
427
497
|
return result.hover;
|
|
428
498
|
},
|
|
499
|
+
hoverSnapshot: async (request) => {
|
|
500
|
+
if (request.buffer.dirty) return { kind: "stale-active-buffer", bufferHash: request.buffer.hash };
|
|
501
|
+
const result = await codeIntelligenceOperations.hover(absolutePath, request.line, request.character);
|
|
502
|
+
return { kind: "ready", hover: result.hover };
|
|
503
|
+
},
|
|
429
504
|
};
|
|
430
505
|
return new ModalEditorComponent(tui, theme, host, session.content, () => done(undefined));
|
|
431
506
|
}, editorOverlayOptions);
|
|
@@ -1145,14 +1220,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
1145
1220
|
async execute(_toolCallId, params) {
|
|
1146
1221
|
const path = resolve(cwd, params.path);
|
|
1147
1222
|
const result = await codeIntelligenceOperations.workspaceMap(path, params.maxNodes, params.maxEdges, params.maxEntries, params.maxBytes);
|
|
1148
|
-
const
|
|
1223
|
+
const coverage = `Candidate coverage: languages=${result.candidateSelection.representedLanguages.join(",") || "none"}; scopes=${result.candidateSelection.representedScopes.join(",") || "none"}${result.candidateSelection.omittedScopes.length > 0 ? `; omitted=${result.candidateSelection.omittedScopes.join(",")}` : ""}; strategy=${result.candidateSelection.strategy}.`;
|
|
1224
|
+
const text = `${
|
|
1149
1225
|
result.entries.length === 0
|
|
1150
1226
|
? "No ranked symbols (the workspace's symbol graph may still be populating in the background -- retry shortly)."
|
|
1151
1227
|
: result.entries
|
|
1152
1228
|
.map(
|
|
1153
1229
|
(entry) => `${entry.kind} ${entry.name} -- ${entry.path}:${entry.line}:${entry.character}${entry.signature ? ` -- ${entry.signature}` : ""}`,
|
|
1154
1230
|
)
|
|
1155
|
-
.join("\n")
|
|
1231
|
+
.join("\n")
|
|
1232
|
+
}\n${coverage}`;
|
|
1156
1233
|
return { content: [{ type: "text", text }], details: { result } };
|
|
1157
1234
|
},
|
|
1158
1235
|
renderCall(args, theme, context) {
|
|
@@ -1622,23 +1699,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
1622
1699
|
|
|
1623
1700
|
type MutationHistoryToolDetails =
|
|
1624
1701
|
| { readonly action: "list"; readonly entries: readonly MutationHistoryEntry[] }
|
|
1625
|
-
| { readonly action: "revert"; readonly reverted: { readonly path: string; readonly newHash: string | null } }
|
|
1702
|
+
| { readonly action: "revert"; readonly reverted: { readonly path: string; readonly newHash: string | null } }
|
|
1703
|
+
| { readonly action: "revert-transaction"; readonly reverted: MutationTransactionRevertOutcome };
|
|
1626
1704
|
|
|
1627
1705
|
const mutationHistoryOperations = createMutationHistoryOperations();
|
|
1628
1706
|
registerLectorTool({
|
|
1629
1707
|
name: "mutation_history",
|
|
1630
1708
|
label: "Mutation History",
|
|
1631
1709
|
description:
|
|
1632
|
-
"List
|
|
1633
|
-
promptSnippet: "List or revert
|
|
1710
|
+
"List a file's recorded edit history, revert a standalone entry, or atomically revert every file in a rename/multi-file transaction. Every successful edit/line_edit/apply_patch is recorded (newest first, bounded, not durable across a daemon restart). Reverts are hash-guarded and further-revertible. A transaction member is never reverted alone: use action=revert-transaction with its transactionId.",
|
|
1711
|
+
promptSnippet: "List or safely revert standalone or transaction-grouped edit history",
|
|
1634
1712
|
promptGuidelines: [
|
|
1635
|
-
"list first
|
|
1713
|
+
"list first. Use action=revert only for a standalone entry; if list reports a transactionId, use action=revert-transaction so every member is restored atomically.",
|
|
1636
1714
|
],
|
|
1637
1715
|
parameters: Type.Object({
|
|
1638
|
-
action: Type.Union([Type.Literal("list"), Type.Literal("revert")]),
|
|
1639
|
-
path: Type.String({ description: "Absolute or workspace-relative path to the
|
|
1716
|
+
action: Type.Union([Type.Literal("list"), Type.Literal("revert"), Type.Literal("revert-transaction")]),
|
|
1717
|
+
path: Type.String({ description: "Absolute or workspace-relative path used to resolve the owning workspace" }),
|
|
1640
1718
|
maxResults: Type.Optional(Type.Number({ description: "Required for action=list -- maximum entries to return, newest first" })),
|
|
1641
|
-
entryId: Type.Optional(Type.String({ description: "Required for action=revert --
|
|
1719
|
+
entryId: Type.Optional(Type.String({ description: "Required for action=revert -- a standalone entry id returned by list" })),
|
|
1720
|
+
transactionId: Type.Optional(Type.String({ description: "Required for action=revert-transaction -- a transaction id returned by list" })),
|
|
1642
1721
|
}),
|
|
1643
1722
|
async execute(toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<MutationHistoryToolDetails>> {
|
|
1644
1723
|
const absolutePath = resolve(cwd, params.path);
|
|
@@ -1651,17 +1730,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
1651
1730
|
if (params.action === "list") {
|
|
1652
1731
|
if (params.maxResults === undefined) throw new Error("mutation_history action=list requires maxResults");
|
|
1653
1732
|
const entries = await mutationHistoryOperations.list(absolutePath, params.maxResults, vehicleCall);
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1733
|
+
return { content: [{ type: "text", text: formatMutationHistoryList(entries) }], details: { action: "list", entries } };
|
|
1734
|
+
}
|
|
1735
|
+
if (params.action === "revert") {
|
|
1736
|
+
if (params.entryId === undefined) throw new Error("mutation_history action=revert requires entryId");
|
|
1737
|
+
const reverted = await mutationHistoryOperations.revert(absolutePath, params.entryId, vehicleCall);
|
|
1738
|
+
return {
|
|
1739
|
+
content: [{ type: "text", text: `${reverted.path} reverted -> ${reverted.newHash ?? "(deleted)"}` }],
|
|
1740
|
+
details: { action: "revert", reverted },
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
if (params.transactionId === undefined) throw new Error("mutation_history action=revert-transaction requires transactionId");
|
|
1744
|
+
const reverted = await mutationHistoryOperations.revertTransaction(absolutePath, params.transactionId, vehicleCall);
|
|
1662
1745
|
return {
|
|
1663
|
-
content: [{ type: "text", text:
|
|
1664
|
-
details: { action: "revert", reverted },
|
|
1746
|
+
content: [{ type: "text", text: formatMutationTransactionRevert(params.transactionId, reverted) }],
|
|
1747
|
+
details: { action: "revert-transaction", reverted },
|
|
1665
1748
|
};
|
|
1666
1749
|
},
|
|
1667
1750
|
renderCall(args, theme, context) {
|
|
@@ -1714,17 +1797,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
1714
1797
|
name: "package_source",
|
|
1715
1798
|
label: "Package Source",
|
|
1716
1799
|
description:
|
|
1717
|
-
"Resolve, list, remove, or clean bookkeeping for installed npm
|
|
1800
|
+
"Resolve, list, remove, or clean bookkeeping for installed packages (npm, pypi, ...) resolved to verified exact repository source. action=resolve uses the project's own lockfile-family, bounded registry metadata, and an exact Git ref/commit (or, for an editable/direct-VCS/local install, the already-known source directly, no registry lookup needed); registers verified source as a read-only workspace for the other Lector tools. action=list reports every package coordinate already resolved this way -- no re-resolution, no network. action=remove drops one bookkeeping entry by its exact ecosystem/name/resolvedVersion; refuses if it is still a currently-registered workspace. action=clean removes every non-in-use entry, optionally scoped to one ecosystem. Neither remove nor clean deletes the underlying repo_cache disk entry -- use repo_cache(action=evict) for that, since a monorepo can share one checkout across several package coordinates.",
|
|
1718
1801
|
promptSnippet: "Resolve, list, remove, or clean verified package source bookkeeping",
|
|
1719
1802
|
parameters: Type.Object({
|
|
1720
1803
|
action: Type.Optional(Type.Union([Type.Literal("resolve"), Type.Literal("list"), Type.Literal("remove"), Type.Literal("clean")])),
|
|
1721
|
-
directory: Type.Optional(Type.String({ description: "Required for action=resolve -- project directory containing the
|
|
1804
|
+
directory: Type.Optional(Type.String({ description: "Required for action=resolve -- project directory containing the package's own lockfile-family" })),
|
|
1722
1805
|
name: Type.Optional(Type.String({ description: "Required for action=resolve/remove -- installed package name, including scope when present" })),
|
|
1723
1806
|
version: Type.Optional(
|
|
1724
1807
|
Type.String({ description: "action=resolve only -- exact installed version; required when the lockfile contains several versions" }),
|
|
1725
1808
|
),
|
|
1726
|
-
registry: Type.Optional(Type.String({ description: "
|
|
1727
|
-
ecosystem: Type.Optional(
|
|
1809
|
+
registry: Type.Optional(Type.String({ description: "Registry URL; defaults to the ecosystem's own public registry (npm registry / pypi.org)" })),
|
|
1810
|
+
ecosystem: Type.Optional(
|
|
1811
|
+
Type.String({
|
|
1812
|
+
description:
|
|
1813
|
+
"npm/pypi/cargo/go/maven/conan/vcpkg/nuget/swiftpm -- action=resolve defaults to npm; required for action=remove; optional filter for action=list/clean",
|
|
1814
|
+
}),
|
|
1815
|
+
),
|
|
1728
1816
|
resolvedVersion: Type.Optional(Type.String({ description: "Required for action=remove -- the exact resolved version to remove" })),
|
|
1729
1817
|
text: Type.Optional(Type.String({ description: "action=list only -- case-insensitive substring match across ecosystem/name/resolvedVersion" })),
|
|
1730
1818
|
maxResults: Type.Optional(Type.Number({ description: "Required for action=list -- maximum entries to return in this page" })),
|
|
@@ -1757,7 +1845,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1757
1845
|
}
|
|
1758
1846
|
if (!params.directory || !params.name) throw new Error("package_source action=resolve requires directory and name");
|
|
1759
1847
|
const directory = resolve(cwd, params.directory);
|
|
1760
|
-
const result = await packageSourceOperations.resolve(
|
|
1848
|
+
const result = await packageSourceOperations.resolve(
|
|
1849
|
+
directory,
|
|
1850
|
+
params.name,
|
|
1851
|
+
params.version ?? null,
|
|
1852
|
+
params.registry ?? null,
|
|
1853
|
+
optionalPackageEcosystem(params.ecosystem),
|
|
1854
|
+
);
|
|
1761
1855
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "resolve", result } };
|
|
1762
1856
|
},
|
|
1763
1857
|
renderCall(args, theme, context) {
|
|
@@ -3,6 +3,13 @@ import { withWorkspace, workspaceForPath } from "../lector-client.ts";
|
|
|
3
3
|
import { invokeLectorVehicleOperation, type LectorVehicleCall } from "../vehicle-client.ts";
|
|
4
4
|
import { toWorkspaceRelativePath } from "../workspace-relative-path.ts";
|
|
5
5
|
|
|
6
|
+
const MAX_INTERNAL_HISTORY_LOOKUP_RESULTS = 2_000;
|
|
7
|
+
|
|
8
|
+
export interface MutationTransactionRevertOutcome {
|
|
9
|
+
readonly transactionId: string;
|
|
10
|
+
readonly reverted: readonly { readonly path: string; readonly newHash: string | null }[];
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
/** Match MUTATION_HISTORY_READ_PERMISSIONS/MUTATION_HISTORY_WRITE_PERMISSIONS' own declared values server-side (mutation-history/operation-registration.ts). */
|
|
7
14
|
const MUTATION_HISTORY_READ_PERMISSIONS = ["workspace:read"];
|
|
8
15
|
const MUTATION_HISTORY_WRITE_PERMISSIONS = ["workspace:write"];
|
|
@@ -15,32 +22,72 @@ const MUTATION_HISTORY_WRITE_PERMISSIONS = ["workspace:write"];
|
|
|
15
22
|
export interface MutationHistoryOperations {
|
|
16
23
|
list(absolutePath: string, maxResults: number, call: LectorVehicleCall): Promise<readonly MutationHistoryEntry[]>;
|
|
17
24
|
revert(absolutePath: string, entryId: string, call: LectorVehicleCall): Promise<{ path: string; newHash: string | null }>;
|
|
25
|
+
revertTransaction(absolutePath: string, transactionId: string, call: LectorVehicleCall): Promise<MutationTransactionRevertOutcome>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function listResolvedHistory(
|
|
29
|
+
workspaceId: string,
|
|
30
|
+
root: string,
|
|
31
|
+
absolutePath: string,
|
|
32
|
+
maxResults: number,
|
|
33
|
+
call: LectorVehicleCall,
|
|
34
|
+
): Promise<readonly MutationHistoryEntry[]> {
|
|
35
|
+
const relativePath = toWorkspaceRelativePath(root, absolutePath);
|
|
36
|
+
// Single-file edits historically record the caller's workspace-relative path, while LSP
|
|
37
|
+
// WorkspaceEdits record canonical absolute paths. Query both identities until the daemon's
|
|
38
|
+
// stored-history migration can normalize old entries, then deduplicate by immutable entry id.
|
|
39
|
+
const paths = relativePath === absolutePath ? [absolutePath] : [relativePath, absolutePath];
|
|
40
|
+
const pages = await Promise.all(
|
|
41
|
+
paths.map((path) =>
|
|
42
|
+
invokeLectorVehicleOperation<{ entries: readonly MutationHistoryEntry[] }>(
|
|
43
|
+
"workspace.mutationHistory",
|
|
44
|
+
{ workspaceId, path, maxResults },
|
|
45
|
+
MUTATION_HISTORY_READ_PERMISSIONS,
|
|
46
|
+
call,
|
|
47
|
+
),
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
const byId = new Map<string, MutationHistoryEntry>();
|
|
51
|
+
for (const page of pages) for (const entry of page.entries) byId.set(entry.id, entry);
|
|
52
|
+
return [...byId.values()].sort((a, b) => b.timestamp - a.timestamp).slice(0, maxResults);
|
|
18
53
|
}
|
|
19
54
|
|
|
20
55
|
export function createMutationHistoryOperations(): MutationHistoryOperations {
|
|
21
56
|
return {
|
|
22
57
|
list(absolutePath, maxResults, call) {
|
|
58
|
+
return withWorkspace(
|
|
59
|
+
() => workspaceForPath(absolutePath),
|
|
60
|
+
({ workspaceId, root }) => listResolvedHistory(workspaceId, root, absolutePath, maxResults, call),
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
revert(absolutePath, entryId, call) {
|
|
23
64
|
return withWorkspace(
|
|
24
65
|
() => workspaceForPath(absolutePath),
|
|
25
66
|
async ({ workspaceId, root }) => {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
67
|
+
const entries = await listResolvedHistory(workspaceId, root, absolutePath, MAX_INTERNAL_HISTORY_LOOKUP_RESULTS, call);
|
|
68
|
+
const target = entries.find((entry) => entry.id === entryId);
|
|
69
|
+
if (!target) throw new Error(`mutation history entry "${entryId}" is not recorded for "${absolutePath}" -- list that path again before reverting`);
|
|
70
|
+
if (target.transactionId !== null) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`mutation history entry "${entryId}" belongs to multi-file transaction "${target.transactionId}" -- refusing a partial revert; use action=revert-transaction with transactionId`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return invokeLectorVehicleOperation<{ path: string; newHash: string | null }>(
|
|
76
|
+
"workspace.revertMutation",
|
|
77
|
+
{ workspaceId, entryId },
|
|
78
|
+
MUTATION_HISTORY_WRITE_PERMISSIONS,
|
|
31
79
|
call,
|
|
32
80
|
);
|
|
33
|
-
return entries;
|
|
34
81
|
},
|
|
35
82
|
);
|
|
36
83
|
},
|
|
37
|
-
|
|
84
|
+
revertTransaction(absolutePath, transactionId, call) {
|
|
38
85
|
return withWorkspace(
|
|
39
86
|
() => workspaceForPath(absolutePath),
|
|
40
87
|
({ workspaceId }) =>
|
|
41
|
-
invokeLectorVehicleOperation<
|
|
42
|
-
"workspace.
|
|
43
|
-
{ workspaceId,
|
|
88
|
+
invokeLectorVehicleOperation<MutationTransactionRevertOutcome>(
|
|
89
|
+
"workspace.revertMutationTransaction",
|
|
90
|
+
{ workspaceId, transactionId },
|
|
44
91
|
MUTATION_HISTORY_WRITE_PERMISSIONS,
|
|
45
92
|
call,
|
|
46
93
|
),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MutationHistoryEntry } from "@danypops/lector";
|
|
2
|
+
import type { MutationTransactionRevertOutcome } from "./operations.ts";
|
|
3
|
+
|
|
4
|
+
export function formatMutationHistoryList(entries: readonly MutationHistoryEntry[]): string {
|
|
5
|
+
if (entries.length === 0) return "no recorded mutation history for this path";
|
|
6
|
+
return entries
|
|
7
|
+
.map((entry) => {
|
|
8
|
+
const grouping = entry.transactionId === null ? "standalone mutation" : `transaction ${entry.transactionId}`;
|
|
9
|
+
return `${entry.id} ${new Date(entry.timestamp).toISOString()} ${entry.operation} ${grouping}`;
|
|
10
|
+
})
|
|
11
|
+
.join("\n");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatMutationTransactionRevert(originalTransactionId: string, outcome: MutationTransactionRevertOutcome): string {
|
|
15
|
+
const lines = [
|
|
16
|
+
`${originalTransactionId} reverted atomically; revert recorded as transaction ${outcome.transactionId}`,
|
|
17
|
+
...outcome.reverted.map((entry) => `${entry.path} -> ${entry.newHash ?? "(deleted)"}`),
|
|
18
|
+
];
|
|
19
|
+
return lines.join("\n");
|
|
20
|
+
}
|
|
@@ -7,7 +7,13 @@ export interface PackageSourceListPage {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export interface PackageSourceOperations {
|
|
10
|
-
resolve(
|
|
10
|
+
resolve(
|
|
11
|
+
directory: string,
|
|
12
|
+
name: string,
|
|
13
|
+
requestedVersion: string | null,
|
|
14
|
+
registry: string | null,
|
|
15
|
+
ecosystem?: PackageEcosystem,
|
|
16
|
+
): Promise<PackageSourceOperationResult>;
|
|
11
17
|
list(options: { ecosystem?: PackageEcosystem; text?: string; maxResults: number; cursor?: string }): Promise<PackageSourceListPage>;
|
|
12
18
|
remove(ecosystem: PackageEcosystem, registry: string | null, name: string, resolvedVersion: string): Promise<{ removed: boolean }>;
|
|
13
19
|
clean(ecosystem: PackageEcosystem | undefined): Promise<{ removed: number; skipped: number }>;
|
|
@@ -15,12 +21,12 @@ export interface PackageSourceOperations {
|
|
|
15
21
|
|
|
16
22
|
export function createLectorPackageSourceOperations(): PackageSourceOperations {
|
|
17
23
|
return {
|
|
18
|
-
async resolve(directory, name, requestedVersion, registry) {
|
|
24
|
+
async resolve(directory, name, requestedVersion, registry, ecosystem = "npm") {
|
|
19
25
|
const client = await lectorClient();
|
|
20
26
|
return client.callOnce("package.resolveSource", {
|
|
21
27
|
request: {
|
|
22
28
|
projectRoot: directory,
|
|
23
|
-
coordinate: { ecosystem
|
|
29
|
+
coordinate: { ecosystem, registry, name, requestedVersion },
|
|
24
30
|
},
|
|
25
31
|
bounds: DEFAULT_PACKAGE_SOURCE_BOUNDS,
|
|
26
32
|
});
|
|
@@ -17,7 +17,8 @@ export function formatSearchResult(result: TextSearchResult | undefined, expande
|
|
|
17
17
|
items: result.matches,
|
|
18
18
|
expanded,
|
|
19
19
|
visibleCount: DEFAULT_VISIBLE_MATCHES,
|
|
20
|
-
formatItem: (match) =>
|
|
20
|
+
formatItem: (match) =>
|
|
21
|
+
`${theme.fg("accent", match.path)}:${match.lineNumber}: ${match.line.replace(/\n$/, "")}${match.lineTruncated ? theme.fg("warning", " (line truncated)") : ""}`,
|
|
21
22
|
moreLine: (hidden) => theme.fg("dim", `... ${hidden} more (${keyHint("app.tools.expand", "to expand")})`),
|
|
22
23
|
truncationWarning: result.truncated ? theme.fg("warning", "(search itself was truncated by maxMatches/maxBytes -- results are incomplete)") : undefined,
|
|
23
24
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-lector",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.7",
|
|
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",
|
|
@@ -22,17 +22,19 @@
|
|
|
22
22
|
"@danypops/vehicle-client-pi": "^0.45.0"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@danypops/lector": "^0.
|
|
25
|
+
"@danypops/lector": "^0.20.0",
|
|
26
26
|
"@danypops/vehicle-client": "^0.10.3",
|
|
27
27
|
"@danypops/vehicle-core": "^0.17.1",
|
|
28
28
|
"malevich-tui-components": "^0.32.1",
|
|
29
29
|
"picomatch": "^4.0.5"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@danypops/
|
|
33
|
-
"@danypops/vehicle-conformance": "^0.4.0",
|
|
32
|
+
"@danypops/pi-eval-harness": "^0.1.0",
|
|
34
33
|
"@danypops/pi-extension-harness": "^0.2.0",
|
|
34
|
+
"@danypops/pi-process-harness": "^0.1.3",
|
|
35
35
|
"@danypops/pi-tui-harness": "^0.0.1",
|
|
36
|
+
"@danypops/vehicle-client-pi": "^0.45.0",
|
|
37
|
+
"@danypops/vehicle-conformance": "^0.4.0",
|
|
36
38
|
"@earendil-works/pi-ai": "^0.81.1",
|
|
37
39
|
"@earendil-works/pi-coding-agent": "^0.81.1",
|
|
38
40
|
"@earendil-works/pi-tui": "^0.81.1",
|