@danypops/pi-lector 0.3.0 → 0.5.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/index.ts +335 -57
- package/extension/src/lector-client.ts +14 -0
- package/extension/src/mutation-history-operations.ts +34 -0
- package/extension/src/reference-based-rename-operations.ts +26 -0
- package/extension/src/rename-operations.ts +36 -0
- package/extension/src/repo-cache-evict-operations.ts +19 -0
- package/extension/src/repo-cache-list-operations.ts +24 -0
- package/extension/src/repo-cache-rendering.ts +42 -0
- package/extension/src/repo-fetch-operations.ts +3 -3
- package/package.json +2 -2
- package/extension/src/repo-fetch-rendering.ts +0 -21
package/extension/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import type {
|
|
3
|
+
CachedRepositoryPage,
|
|
3
4
|
ContentHash,
|
|
4
5
|
Diagnostic,
|
|
5
6
|
DocumentSymbolEntry,
|
|
@@ -10,6 +11,8 @@ import type {
|
|
|
10
11
|
JobSnapshot,
|
|
11
12
|
LineEdit,
|
|
12
13
|
LineEditOutcome,
|
|
14
|
+
MutationHistoryEntry,
|
|
15
|
+
OperationOutputs,
|
|
13
16
|
PackageSourceOperationResult,
|
|
14
17
|
PopulateSymbolGraphResult,
|
|
15
18
|
RepoFetchResult,
|
|
@@ -66,14 +69,20 @@ import { createLectorFindSymbolsOperations } from "./find-symbols-operations.ts"
|
|
|
66
69
|
import { describeFindSymbolSources, formatFindSymbolsCall, formatFindSymbolsResult } from "./find-symbols-rendering.ts";
|
|
67
70
|
import { createLectorGitOperations } from "./git-operations.ts";
|
|
68
71
|
import { formatGitCall, formatGitResult, type GitToolDetails } from "./git-rendering.ts";
|
|
72
|
+
import { setNewWorkspaceObserver } from "./lector-client.ts";
|
|
69
73
|
import { createLectorLineEditOperations } from "./line-edit-operations.ts";
|
|
70
74
|
import { formatLineEditCall, formatLineEditResult } from "./line-edit-rendering.ts";
|
|
75
|
+
import { createMutationHistoryOperations } from "./mutation-history-operations.ts";
|
|
71
76
|
import { nearestGitRoot } from "./nearest-workspace-root.ts";
|
|
72
77
|
import { createLectorPackageSourceOperations } from "./package-source-operations.ts";
|
|
73
78
|
import { formatPackageSourceCall, formatPackageSourceResult } from "./package-source-rendering.ts";
|
|
74
79
|
import { createLectorReadOperations } from "./read-operations.ts";
|
|
80
|
+
import { createReferenceBasedRenameOperations } from "./reference-based-rename-operations.ts";
|
|
81
|
+
import { createRenameOperations } from "./rename-operations.ts";
|
|
82
|
+
import { createRepoCacheEvictOperations } from "./repo-cache-evict-operations.ts";
|
|
83
|
+
import { createRepoCacheListOperations } from "./repo-cache-list-operations.ts";
|
|
84
|
+
import { formatRepoCacheCall, formatRepoCacheEvictResult, formatRepoCacheListResult, formatRepoFetchResult } from "./repo-cache-rendering.ts";
|
|
75
85
|
import { createLectorRepoFetchOperations } from "./repo-fetch-operations.ts";
|
|
76
|
-
import { formatRepoFetchCall, formatRepoFetchResult } from "./repo-fetch-rendering.ts";
|
|
77
86
|
import { createLectorSearchOperations } from "./search-operations.ts";
|
|
78
87
|
import { formatSearchCall, formatSearchResult } from "./search-rendering.ts";
|
|
79
88
|
import { type AnnotationAnchorInput, createLectorSymbolAnnotationOperations } from "./symbol-annotation-operations.ts";
|
|
@@ -115,63 +124,115 @@ function renderIntelligenceSource(body: string, provenance: IntelligenceProvenan
|
|
|
115
124
|
*/
|
|
116
125
|
export default function (pi: ExtensionAPI) {
|
|
117
126
|
const cacheOperations = createWorkspaceCacheOperations();
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
// One generation counter shared by every root's monitor loop, not per-root -- a new session
|
|
128
|
+
// (or shutdown) invalidates every previous session's in-flight monitor regardless of which
|
|
129
|
+
// root it tracked, and there is exactly one "current session" at a time.
|
|
130
|
+
let sessionGeneration = 0;
|
|
131
|
+
// Every workspace root actually touched so far this session, not just one fixed cwd root --
|
|
132
|
+
// populated by setNewWorkspaceObserver below, the real "first touch" trigger.
|
|
133
|
+
const cacheStatesByRoot = new Map<string, CachePresentationState>();
|
|
134
|
+
// Roots already monitored this session -- guards against starting the SAME root's monitor
|
|
135
|
+
// twice: session_start's own direct kick-off for the cwd root itself calls
|
|
136
|
+
// cacheOperations.status(), which registers that root via workspace.registerPath, which fires
|
|
137
|
+
// setNewWorkspaceObserver for it a moment later -- without this guard that would start a
|
|
138
|
+
// second, redundant concurrent monitor loop for the exact same root.
|
|
139
|
+
const monitoringRoots = new Set<string>();
|
|
140
|
+
let lastInjectedSummary: string | undefined;
|
|
141
|
+
let uiContext: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1] | undefined;
|
|
142
|
+
|
|
143
|
+
function combinedSummary(): string {
|
|
144
|
+
const states = [...cacheStatesByRoot.values()];
|
|
145
|
+
if (states.length === 0) return "";
|
|
146
|
+
const [only] = states;
|
|
147
|
+
if (states.length === 1 && only) return describeCacheState(only);
|
|
148
|
+
const counts = new Map<string, number>();
|
|
149
|
+
for (const state of states) counts.set(state.status, (counts.get(state.status) ?? 0) + 1);
|
|
150
|
+
return [...counts.entries()].map(([status, count]) => `${count} ${status}`).join(", ");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function refreshStatusBar(): void {
|
|
154
|
+
if (!uiContext) return;
|
|
155
|
+
const summary = combinedSummary();
|
|
156
|
+
if (!summary) {
|
|
157
|
+
uiContext.ui.setStatus("lector-cache", undefined);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const states = [...cacheStatesByRoot.values()];
|
|
161
|
+
const worst = states.some((state) => state.status === "not-cached" || state.status === "caching")
|
|
162
|
+
? "warning"
|
|
163
|
+
: states.every((state) => state.status === "cached")
|
|
164
|
+
? "success"
|
|
165
|
+
: "accent";
|
|
166
|
+
uiContext.ui.setStatus("lector-cache", uiContext.ui.theme.fg(worst, `Lector: ${summary}`));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Starts (or restarts, on a stale generation) monitoring one workspace root's cache lifecycle -- shared by session_start's own cwd root and every later root a tool call first touches. */
|
|
170
|
+
function startMonitoringRoot(root: string, ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1]): void {
|
|
171
|
+
if (monitoringRoots.has(root)) return;
|
|
172
|
+
monitoringRoots.add(root);
|
|
173
|
+
const thisGeneration = sessionGeneration;
|
|
174
|
+
void monitorWorkspaceCache(cacheOperations, {
|
|
175
|
+
directory: root,
|
|
176
|
+
maxFiles: 500,
|
|
177
|
+
maxSymbolsPerFile: 100,
|
|
178
|
+
pollIntervalMs: 1_000,
|
|
179
|
+
maxPolls: 300,
|
|
180
|
+
shouldContinue: () => sessionGeneration === thisGeneration,
|
|
181
|
+
onState: (state) => {
|
|
182
|
+
if (sessionGeneration !== thisGeneration) return;
|
|
183
|
+
cacheStatesByRoot.set(root, state);
|
|
184
|
+
if (state.status === "finished-caching") {
|
|
185
|
+
if (ctx.hasUI) ctx.ui.notify(`Lector finished caching ${root}`, "info");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
refreshStatusBar();
|
|
189
|
+
},
|
|
190
|
+
}).catch((error: unknown) => {
|
|
191
|
+
if (sessionGeneration !== thisGeneration) return;
|
|
192
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
193
|
+
cacheStatesByRoot.delete(root);
|
|
194
|
+
refreshStatusBar();
|
|
195
|
+
if (ctx.hasUI) ctx.ui.notify(`Lector cache failed for ${root}: ${message}`, "error");
|
|
196
|
+
});
|
|
197
|
+
}
|
|
121
198
|
|
|
122
199
|
pi.on("before_agent_start", () => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
200
|
+
const summary = combinedSummary();
|
|
201
|
+
if (!summary || summary === lastInjectedSummary) return;
|
|
202
|
+
lastInjectedSummary = summary;
|
|
203
|
+
const messages = [...cacheStatesByRoot.entries()]
|
|
204
|
+
.filter(([, state]) => state.status !== "cached")
|
|
205
|
+
.map(([root, state]) => `${root}: ${cacheContextMessage(state)}`);
|
|
206
|
+
if (messages.length === 0) return;
|
|
127
207
|
return {
|
|
128
208
|
message: {
|
|
129
209
|
customType: "lector-cache-status",
|
|
130
|
-
content:
|
|
210
|
+
content: messages.join("\n"),
|
|
131
211
|
display: false,
|
|
132
212
|
},
|
|
133
213
|
};
|
|
134
214
|
});
|
|
135
215
|
|
|
136
216
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
217
|
+
sessionGeneration++;
|
|
218
|
+
cacheStatesByRoot.clear();
|
|
219
|
+
monitoringRoots.clear();
|
|
220
|
+
lastInjectedSummary = undefined;
|
|
221
|
+
uiContext = undefined;
|
|
140
222
|
ctx.ui.setStatus("lector-cache", undefined);
|
|
141
223
|
});
|
|
142
224
|
|
|
143
225
|
pi.on("session_start", (_event, ctx) => {
|
|
144
226
|
const { cwd } = ctx;
|
|
227
|
+
sessionGeneration++;
|
|
228
|
+
cacheStatesByRoot.clear();
|
|
229
|
+
monitoringRoots.clear();
|
|
230
|
+
lastInjectedSummary = undefined;
|
|
231
|
+
uiContext = ctx;
|
|
232
|
+
setNewWorkspaceObserver((root) => startMonitoringRoot(root, ctx));
|
|
145
233
|
const projectRoot = nearestGitRoot(cwd);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
lastInjectedCacheState = undefined;
|
|
149
|
-
if (projectRoot) {
|
|
150
|
-
void monitorWorkspaceCache(cacheOperations, {
|
|
151
|
-
directory: projectRoot,
|
|
152
|
-
maxFiles: 500,
|
|
153
|
-
maxSymbolsPerFile: 100,
|
|
154
|
-
pollIntervalMs: 1_000,
|
|
155
|
-
maxPolls: 300,
|
|
156
|
-
shouldContinue: () => cacheRun === thisRun,
|
|
157
|
-
onState: (state) => {
|
|
158
|
-
cacheState = state;
|
|
159
|
-
if (state.status === "finished-caching") {
|
|
160
|
-
if (ctx.hasUI) ctx.ui.notify(`Lector finished caching ${projectRoot}`, "info");
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
const color = state.status === "cached" ? "success" : state.status === "caching" ? "accent" : "warning";
|
|
164
|
-
ctx.ui.setStatus("lector-cache", ctx.ui.theme.fg(color, `Lector: ${describeCacheState(state)}`));
|
|
165
|
-
},
|
|
166
|
-
}).catch((error: unknown) => {
|
|
167
|
-
if (cacheRun !== thisRun) return;
|
|
168
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
169
|
-
ctx.ui.setStatus("lector-cache", ctx.ui.theme.fg("error", "Lector: cache error"));
|
|
170
|
-
if (ctx.hasUI) ctx.ui.notify(`Lector cache failed: ${message}`, "error");
|
|
171
|
-
});
|
|
172
|
-
} else {
|
|
173
|
-
ctx.ui.setStatus("lector-cache", undefined);
|
|
174
|
-
}
|
|
234
|
+
if (projectRoot) startMonitoringRoot(projectRoot, ctx);
|
|
235
|
+
else ctx.ui.setStatus("lector-cache", undefined);
|
|
175
236
|
|
|
176
237
|
pi.registerTool(createReadToolDefinition(cwd, { operations: createLectorReadOperations() }));
|
|
177
238
|
pi.registerTool(createWriteToolDefinition(cwd, { operations: createLectorWriteOperations() }));
|
|
@@ -241,6 +302,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
241
302
|
});
|
|
242
303
|
|
|
243
304
|
const codeIntelligenceOperations = createLectorCodeIntelligenceOperations();
|
|
305
|
+
const referenceBasedRenameOperations = createReferenceBasedRenameOperations();
|
|
306
|
+
const renameOperations = createRenameOperations();
|
|
244
307
|
const positionParameters = {
|
|
245
308
|
path: Type.String({ description: "Absolute or cwd-relative path to the file" }),
|
|
246
309
|
line: Type.Number({ description: "1-indexed line number" }),
|
|
@@ -622,6 +685,125 @@ export default function (pi: ExtensionAPI) {
|
|
|
622
685
|
},
|
|
623
686
|
});
|
|
624
687
|
|
|
688
|
+
pi.registerTool({
|
|
689
|
+
name: "reference_based_rename",
|
|
690
|
+
label: "Reference-Based Rename",
|
|
691
|
+
description:
|
|
692
|
+
"Move/rename a file and rewrite every static import/export specifier the workspace's own populated symbol graph knows references it -- atomically, rolled back entirely on any failure. Non-LSP: uses find_references + a real parse of import/export declarations, not a language server's own rename. Refuses outright (touches nothing) unless the workspace's symbol graph is fully populated and current for the given bounds -- a partial rename that silently misses a reference is worse than refusing (Sourcegraph's CodeScaleBench finding). Does not follow dynamic import(expr)/require(expr) or any plain string reference to the file -- always check the returned caveats.",
|
|
693
|
+
promptSnippet: "Move a file and update every import that references it",
|
|
694
|
+
promptGuidelines: [
|
|
695
|
+
"Run populate_symbol_graph for this workspace first if cache_status/has_warm_index doesn't already show a fully cached (never partial) graph -- reference_based_rename refuses outright otherwise.",
|
|
696
|
+
"Always read the returned caveats: this never rewrites a dynamic import(expr)/require(expr) or a plain string reference to the old path, even if one exists.",
|
|
697
|
+
],
|
|
698
|
+
parameters: Type.Object({
|
|
699
|
+
fromPath: Type.String({ description: "Absolute or cwd-relative path to the file to move" }),
|
|
700
|
+
toPath: Type.String({ description: "Absolute or cwd-relative path for its new location" }),
|
|
701
|
+
maxFiles: Type.Number({ description: "Maximum number of source files the workspace's symbol graph must have scanned" }),
|
|
702
|
+
maxSymbolsPerFile: Type.Number({ description: "Maximum number of declarations per file the workspace's symbol graph must have processed" }),
|
|
703
|
+
}),
|
|
704
|
+
async execute(_toolCallId, params) {
|
|
705
|
+
const fromPath = resolve(cwd, params.fromPath);
|
|
706
|
+
const toPath = resolve(cwd, params.toPath);
|
|
707
|
+
const outcome = await referenceBasedRenameOperations.rename(fromPath, toPath, params.maxFiles, params.maxSymbolsPerFile);
|
|
708
|
+
const lines = [
|
|
709
|
+
`moved to ${outcome.movedTo}`,
|
|
710
|
+
outcome.filesUpdated.length === 0
|
|
711
|
+
? "no other files referenced it"
|
|
712
|
+
: `updated imports in ${outcome.filesUpdated.length} file(s): ${outcome.filesUpdated.join(", ")}`,
|
|
713
|
+
...outcome.caveats.map((caveat) => `caveat: ${caveat}`),
|
|
714
|
+
];
|
|
715
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { outcome } };
|
|
716
|
+
},
|
|
717
|
+
renderCall(args, theme, context) {
|
|
718
|
+
const fromPath = typeof args.fromPath === "string" ? args.fromPath : "";
|
|
719
|
+
const toPath = typeof args.toPath === "string" ? args.toPath : "";
|
|
720
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
721
|
+
text.setText(
|
|
722
|
+
`${theme.fg("toolTitle", theme.bold("reference_based_rename"))} ${theme.fg("accent", fromPath)} ${theme.fg("dim", "->")} ${theme.fg("accent", toPath)}`,
|
|
723
|
+
);
|
|
724
|
+
return text;
|
|
725
|
+
},
|
|
726
|
+
renderResult(result, { isPartial }, theme, context) {
|
|
727
|
+
if (isPartial) return new Text(theme.fg("warning", "Renaming..."), 0, 0);
|
|
728
|
+
if (context.isError) {
|
|
729
|
+
const errorText = result.content
|
|
730
|
+
.filter((block) => block.type === "text")
|
|
731
|
+
.map((block) => block.text)
|
|
732
|
+
.join("\n");
|
|
733
|
+
return new Text(theme.fg("error", errorText || "reference_based_rename failed"), 0, 0);
|
|
734
|
+
}
|
|
735
|
+
const details = result.details as { outcome?: { movedTo: string; filesUpdated: readonly string[] } } | undefined;
|
|
736
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
737
|
+
text.setText(
|
|
738
|
+
details?.outcome
|
|
739
|
+
? `${theme.fg("success", "moved")} ${theme.fg("accent", details.outcome.movedTo)} ${theme.fg("dim", `(${details.outcome.filesUpdated.length} import(s) updated)`)}`
|
|
740
|
+
: theme.fg("success", "rename complete"),
|
|
741
|
+
);
|
|
742
|
+
return text;
|
|
743
|
+
},
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
interface RenameToolDetails {
|
|
747
|
+
prepared?: OperationOutputs["workspace.prepareRename"];
|
|
748
|
+
applied?: OperationOutputs["workspace.rename"];
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
pi.registerTool({
|
|
752
|
+
name: "rename",
|
|
753
|
+
label: "Rename",
|
|
754
|
+
description:
|
|
755
|
+
"LSP-driven rename via the negotiated language server's own textDocument/prepareRename and textDocument/rename -- the semantic sibling of reference_based_rename, cross-file and identity-aware (resolves through re-exports/aliasing, not just static import specifiers), but only where the workspace's language server actually implements rename. prepare checks whether the symbol at a position can be renamed at all before committing; apply requests the rename and applies the server's own WorkspaceEdit atomically across every file it touches, rolled back entirely on any failure. Actions: prepare, apply.",
|
|
756
|
+
promptSnippet: "Rename a symbol everywhere it's used, via the language server",
|
|
757
|
+
promptGuidelines: [
|
|
758
|
+
"Call prepare first when unsure a position is renameable -- a null range means nothing to rename there, not an error.",
|
|
759
|
+
"apply fails outright if the negotiated server never advertised rename support -- reference_based_rename is the non-LSP fallback for that case.",
|
|
760
|
+
],
|
|
761
|
+
parameters: Type.Object({
|
|
762
|
+
action: Type.String({ description: "prepare | apply" }),
|
|
763
|
+
...positionParameters,
|
|
764
|
+
newName: Type.Optional(Type.String({ description: "Required for apply" })),
|
|
765
|
+
}),
|
|
766
|
+
async execute(_toolCallId, params): Promise<{ content: [{ type: "text"; text: string }]; details: RenameToolDetails }> {
|
|
767
|
+
const path = resolve(cwd, params.path);
|
|
768
|
+
if (params.action === "prepare") {
|
|
769
|
+
const prepared = await renameOperations.prepareRename(path, params.line, params.character);
|
|
770
|
+
const text = prepared.range
|
|
771
|
+
? `renameable${prepared.range.placeholder ? `: "${prepared.range.placeholder}"` : ""}`
|
|
772
|
+
: "nothing renameable at this position";
|
|
773
|
+
return { content: [{ type: "text", text }], details: { prepared } };
|
|
774
|
+
}
|
|
775
|
+
if (params.action === "apply") {
|
|
776
|
+
if (!params.newName) throw new Error("rename apply requires newName");
|
|
777
|
+
const applied = await renameOperations.rename(path, params.line, params.character, params.newName);
|
|
778
|
+
const text = `renamed to "${params.newName}" -- updated ${applied.touchedPaths.length} file(s): ${applied.touchedPaths.join(", ")}`;
|
|
779
|
+
return { content: [{ type: "text", text }], details: { applied } };
|
|
780
|
+
}
|
|
781
|
+
throw new Error(`unknown rename action "${params.action}" -- expected prepare or apply`);
|
|
782
|
+
},
|
|
783
|
+
renderCall(args, theme, context) {
|
|
784
|
+
const action = typeof args.action === "string" ? args.action : "";
|
|
785
|
+
const path = typeof args.path === "string" ? args.path : "";
|
|
786
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
787
|
+
text.setText(`${theme.fg("toolTitle", theme.bold("rename"))} ${theme.fg("dim", action)} ${theme.fg("accent", path)}`);
|
|
788
|
+
return text;
|
|
789
|
+
},
|
|
790
|
+
renderResult(result, { isPartial }, theme, context) {
|
|
791
|
+
if (isPartial) return new Text(theme.fg("warning", "Renaming..."), 0, 0);
|
|
792
|
+
if (context.isError) {
|
|
793
|
+
const errorText = result.content
|
|
794
|
+
.filter((block) => block.type === "text")
|
|
795
|
+
.map((block) => block.text)
|
|
796
|
+
.join("\n");
|
|
797
|
+
return new Text(theme.fg("error", errorText || "rename failed"), 0, 0);
|
|
798
|
+
}
|
|
799
|
+
const text = result.content
|
|
800
|
+
.filter((block) => block.type === "text")
|
|
801
|
+
.map((block) => block.text)
|
|
802
|
+
.join("\n");
|
|
803
|
+
return new Text(theme.fg("success", text), 0, 0);
|
|
804
|
+
},
|
|
805
|
+
});
|
|
806
|
+
|
|
625
807
|
const symbolAnnotationOperations = createLectorSymbolAnnotationOperations();
|
|
626
808
|
function resolveAnchorInputs(anchors: readonly { path: string; line: number; character: number }[]): AnnotationAnchorInput[] {
|
|
627
809
|
return anchors.map((anchor) => ({ path: resolve(cwd, anchor.path), line: anchor.line, character: anchor.character }));
|
|
@@ -1165,6 +1347,67 @@ export default function (pi: ExtensionAPI) {
|
|
|
1165
1347
|
},
|
|
1166
1348
|
});
|
|
1167
1349
|
|
|
1350
|
+
type MutationHistoryToolDetails =
|
|
1351
|
+
| { readonly action: "list"; readonly entries: readonly MutationHistoryEntry[] }
|
|
1352
|
+
| { readonly action: "revert"; readonly reverted: { readonly path: string; readonly newHash: string | null } };
|
|
1353
|
+
|
|
1354
|
+
const mutationHistoryOperations = createMutationHistoryOperations();
|
|
1355
|
+
pi.registerTool({
|
|
1356
|
+
name: "mutation_history",
|
|
1357
|
+
label: "Mutation History",
|
|
1358
|
+
description:
|
|
1359
|
+
"List or revert a file's recorded edit history. Every successful edit/line_edit/apply_patch is recorded (newest first, bounded, not durable across a daemon restart). Reverting restores the file to its exact content immediately before that entry's own mutation, guarded the same way every Lector write is -- refuses if the file changed since, rather than silently clobbering a newer change. A revert is itself a real, further-revertible mutation -- reverting a revert works.",
|
|
1360
|
+
promptSnippet: "List or revert a file's recorded edit history",
|
|
1361
|
+
promptGuidelines: [
|
|
1362
|
+
"list first to find the entry id you want, then revert -- an id from a different file's history, or one already evicted by the bounded history, fails closed rather than guessing.",
|
|
1363
|
+
],
|
|
1364
|
+
parameters: Type.Object({
|
|
1365
|
+
action: Type.Union([Type.Literal("list"), Type.Literal("revert")]),
|
|
1366
|
+
path: Type.String({ description: "Absolute or workspace-relative path to the file" }),
|
|
1367
|
+
maxResults: Type.Optional(Type.Number({ description: "Required for action=list -- maximum entries to return, newest first" })),
|
|
1368
|
+
entryId: Type.Optional(Type.String({ description: "Required for action=revert -- an id returned by a prior action=list" })),
|
|
1369
|
+
}),
|
|
1370
|
+
async execute(_toolCallId, params): Promise<AgentToolResult<MutationHistoryToolDetails>> {
|
|
1371
|
+
const absolutePath = resolve(cwd, params.path);
|
|
1372
|
+
if (params.action === "list") {
|
|
1373
|
+
if (params.maxResults === undefined) throw new Error("mutation_history action=list requires maxResults");
|
|
1374
|
+
const entries = await mutationHistoryOperations.list(absolutePath, params.maxResults);
|
|
1375
|
+
const text =
|
|
1376
|
+
entries.length === 0
|
|
1377
|
+
? "no recorded mutation history for this path"
|
|
1378
|
+
: entries.map((entry) => `${entry.id} ${new Date(entry.timestamp).toISOString()} ${entry.operation}`).join("\n");
|
|
1379
|
+
return { content: [{ type: "text", text }], details: { action: "list", entries } };
|
|
1380
|
+
}
|
|
1381
|
+
if (params.entryId === undefined) throw new Error("mutation_history action=revert requires entryId");
|
|
1382
|
+
const reverted = await mutationHistoryOperations.revert(absolutePath, params.entryId);
|
|
1383
|
+
return {
|
|
1384
|
+
content: [{ type: "text", text: `${reverted.path} reverted -> ${reverted.newHash ?? "(deleted)"}` }],
|
|
1385
|
+
details: { action: "revert", reverted },
|
|
1386
|
+
};
|
|
1387
|
+
},
|
|
1388
|
+
renderCall(args, theme, context) {
|
|
1389
|
+
const action = typeof args.action === "string" ? args.action : "";
|
|
1390
|
+
const path = typeof args.path === "string" ? args.path : "";
|
|
1391
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1392
|
+
text.setText(`${theme.fg("toolTitle", theme.bold("mutation_history"))} ${theme.fg("accent", action)} ${theme.fg("dim", path)}`);
|
|
1393
|
+
return text;
|
|
1394
|
+
},
|
|
1395
|
+
renderResult(result, { isPartial }, theme, context) {
|
|
1396
|
+
if (isPartial) return new Text(theme.fg("warning", "Working on mutation history..."), 0, 0);
|
|
1397
|
+
if (context.isError) {
|
|
1398
|
+
const errorText = result.content
|
|
1399
|
+
.filter((block) => block.type === "text")
|
|
1400
|
+
.map((block) => block.text)
|
|
1401
|
+
.join("\n");
|
|
1402
|
+
return new Text(theme.fg("error", errorText || "mutation_history failed"), 0, 0);
|
|
1403
|
+
}
|
|
1404
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1405
|
+
const textBlock = result.content.find((block) => block.type === "text");
|
|
1406
|
+
text.setText(theme.fg("success", textBlock && "text" in textBlock ? textBlock.text : "done"));
|
|
1407
|
+
return text;
|
|
1408
|
+
},
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1168
1411
|
const packageSourceOperations = createLectorPackageSourceOperations();
|
|
1169
1412
|
pi.registerTool({
|
|
1170
1413
|
name: "package_source",
|
|
@@ -1204,40 +1447,75 @@ export default function (pi: ExtensionAPI) {
|
|
|
1204
1447
|
},
|
|
1205
1448
|
});
|
|
1206
1449
|
|
|
1450
|
+
type RepoCacheToolDetails =
|
|
1451
|
+
| { readonly action: "fetch"; readonly result: RepoFetchResult & { workspaceId: string } }
|
|
1452
|
+
| { readonly action: "list"; readonly page: CachedRepositoryPage }
|
|
1453
|
+
| { readonly action: "evict"; readonly result: { evicted: boolean } };
|
|
1454
|
+
|
|
1207
1455
|
const repoFetchOperations = createLectorRepoFetchOperations();
|
|
1456
|
+
const repoCacheListOperations = createRepoCacheListOperations();
|
|
1457
|
+
const repoCacheEvictOperations = createRepoCacheEvictOperations();
|
|
1208
1458
|
pi.registerTool({
|
|
1209
|
-
name: "
|
|
1210
|
-
label: "Repo
|
|
1459
|
+
name: "repo_cache",
|
|
1460
|
+
label: "Repo Cache",
|
|
1211
1461
|
description:
|
|
1212
|
-
"
|
|
1213
|
-
promptSnippet: "Fetch
|
|
1462
|
+
"Fetch, list/query, or evict entries in Lector's external-repo cache. action=fetch shallow-clones an external repository into a disk-bounded cache and registers it as a read-only project -- every other tool (search_code, find_symbols, go_to_definition, ...) then works on it unchanged; explicit owner/repo[@ref] only, no discovery/search (use web_fetch to find candidates first); forceRefresh reclones even when an unexpired cache entry already exists, for a caller that has already positively confirmed the remote moved. action=list queries the cache -- no network call, no mutation -- filtering by any combination of host/owner/repo/ref (exact match) and text (case-insensitive substring), bounded and paginated via cursor; each entry reports whether it's currently a registered workspace or just present on disk. action=evict removes one cache entry from disk by its exact host/owner/repo/ref identity; refuses with a clear error if it is still a currently-registered workspace.",
|
|
1463
|
+
promptSnippet: "Fetch, list, or evict entries in the external-repo cache",
|
|
1214
1464
|
parameters: Type.Object({
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1465
|
+
action: Type.Union([Type.Literal("fetch"), Type.Literal("list"), Type.Literal("evict")]),
|
|
1466
|
+
owner: Type.Optional(Type.String({ description: "Required for action=fetch/evict -- repository owner or organization" })),
|
|
1467
|
+
repo: Type.Optional(Type.String({ description: "Required for action=fetch/evict -- repository name" })),
|
|
1468
|
+
ref: Type.Optional(
|
|
1469
|
+
Type.String({
|
|
1470
|
+
description:
|
|
1471
|
+
"fetch/evict: branch, tag, or commit; defaults to the repository's default branch. list: matches either the requested or the resolved ref",
|
|
1472
|
+
}),
|
|
1473
|
+
),
|
|
1474
|
+
host: Type.Optional(Type.String({ description: "Git host; defaults to github.com for fetch/evict, unfiltered for list" })),
|
|
1475
|
+
forceRefresh: Type.Optional(Type.Boolean({ description: "action=fetch only -- reclone even if an unexpired cache entry already exists" })),
|
|
1476
|
+
text: Type.Optional(Type.String({ description: "action=list only -- case-insensitive substring match across host/owner/repo/refs" })),
|
|
1477
|
+
maxResults: Type.Optional(Type.Number({ description: "Required for action=list -- maximum entries to return in this page" })),
|
|
1478
|
+
cursor: Type.Optional(Type.String({ description: "action=list only -- opaque cursor from a prior call's nextCursor, to fetch the next page" })),
|
|
1219
1479
|
}),
|
|
1220
|
-
async execute(_toolCallId, params) {
|
|
1221
|
-
|
|
1222
|
-
|
|
1480
|
+
async execute(_toolCallId, params): Promise<AgentToolResult<RepoCacheToolDetails>> {
|
|
1481
|
+
if (params.action === "fetch") {
|
|
1482
|
+
if (!params.owner || !params.repo) throw new Error("repo_cache action=fetch requires owner and repo");
|
|
1483
|
+
const result = await repoFetchOperations.fetch(params.host ?? "github.com", params.owner, params.repo, params.ref ?? null, params.forceRefresh);
|
|
1484
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "fetch", result } };
|
|
1485
|
+
}
|
|
1486
|
+
if (params.action === "evict") {
|
|
1487
|
+
if (!params.owner || !params.repo) throw new Error("repo_cache action=evict requires owner and repo");
|
|
1488
|
+
const result = await repoCacheEvictOperations.evict(params.host ?? "github.com", params.owner, params.repo, params.ref ?? null);
|
|
1489
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "evict", result } };
|
|
1490
|
+
}
|
|
1491
|
+
if (params.maxResults === undefined) throw new Error("repo_cache action=list requires maxResults");
|
|
1492
|
+
const page = await repoCacheListOperations.list(
|
|
1493
|
+
{ text: params.text, host: params.host, owner: params.owner, repo: params.repo, ref: params.ref },
|
|
1494
|
+
params.maxResults,
|
|
1495
|
+
params.cursor,
|
|
1496
|
+
);
|
|
1497
|
+
return { content: [{ type: "text", text: JSON.stringify(page) }], details: { action: "list", page } };
|
|
1223
1498
|
},
|
|
1224
1499
|
renderCall(args, theme, context) {
|
|
1500
|
+
const action = args.action === "list" || args.action === "evict" ? args.action : "fetch";
|
|
1225
1501
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1226
|
-
text.setText(
|
|
1502
|
+
text.setText(formatRepoCacheCall(action, args, theme));
|
|
1227
1503
|
return text;
|
|
1228
1504
|
},
|
|
1229
1505
|
renderResult(result, { isPartial }, theme, context) {
|
|
1230
|
-
if (isPartial) return new Text(theme.fg("warning", "
|
|
1506
|
+
if (isPartial) return new Text(theme.fg("warning", "Working on repo cache..."), 0, 0);
|
|
1231
1507
|
if (context.isError) {
|
|
1232
1508
|
const errorText = result.content
|
|
1233
1509
|
.filter((block) => block.type === "text")
|
|
1234
1510
|
.map((block) => block.text)
|
|
1235
1511
|
.join("\n");
|
|
1236
|
-
return new Text(theme.fg("error", errorText || "
|
|
1512
|
+
return new Text(theme.fg("error", errorText || "repo_cache failed"), 0, 0);
|
|
1237
1513
|
}
|
|
1238
|
-
const details = result.details as
|
|
1514
|
+
const details = result.details as RepoCacheToolDetails | undefined;
|
|
1239
1515
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1240
|
-
text.setText(
|
|
1516
|
+
if (details?.action === "list") text.setText(formatRepoCacheListResult(details.page, theme));
|
|
1517
|
+
else if (details?.action === "evict") text.setText(formatRepoCacheEvictResult(details.result, theme));
|
|
1518
|
+
else text.setText(formatRepoFetchResult(details?.action === "fetch" ? details.result : undefined, theme));
|
|
1241
1519
|
return text;
|
|
1242
1520
|
},
|
|
1243
1521
|
});
|
|
@@ -39,6 +39,19 @@ let connector: ClientConnector = () => connectLectorClient();
|
|
|
39
39
|
const retryingClient: RetryingClient<LectorClient> = createRetryingClient(() => connector(), { label: "Lector" });
|
|
40
40
|
const workspaceIdByRoot = new Map<string, WorkspaceId>();
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Fires exactly once per distinct root, the moment it's first registered in this process --
|
|
44
|
+
* never on a later call that reuses the cached workspaceId. The single choke point every
|
|
45
|
+
* resolver (workspaceForPath, workspaceForDirectory, workspaceForCodeIntelligencePath,
|
|
46
|
+
* workspaceForPathOrDirectory) funnels through, so this is genuinely "the first time any tool
|
|
47
|
+
* call resolves this workspace," not just the one cwd workspace at session start.
|
|
48
|
+
*/
|
|
49
|
+
let onNewWorkspace: ((root: string) => void) | undefined;
|
|
50
|
+
|
|
51
|
+
export function setNewWorkspaceObserver(observer: ((root: string) => void) | undefined): void {
|
|
52
|
+
onNewWorkspace = observer;
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
export interface RetryingLectorClient {
|
|
43
56
|
call<Name extends OperationName>(operation: Name, input: OperationInputs[Name]): Promise<OperationOutputs[Name]>;
|
|
44
57
|
}
|
|
@@ -64,6 +77,7 @@ async function workspaceForRoot(root: string): Promise<ResolvedWorkspace> {
|
|
|
64
77
|
const client = await lectorClient();
|
|
65
78
|
const { workspaceId } = await client.call("workspace.registerPath", { path: root });
|
|
66
79
|
workspaceIdByRoot.set(root, workspaceId);
|
|
80
|
+
onNewWorkspace?.(root);
|
|
67
81
|
return { workspaceId, root };
|
|
68
82
|
}
|
|
69
83
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { MutationHistoryEntry } from "@danypops/lector";
|
|
2
|
+
import { lectorClient, withWorkspace, workspaceForPath } from "./lector-client.ts";
|
|
3
|
+
import { toWorkspaceRelativePath } from "./workspace-relative-path.ts";
|
|
4
|
+
|
|
5
|
+
/** Thin wrapper over Lector's mutation history: every successful edit is recorded, and any entry can be reverted -- guarded the same way every other Lector write is. */
|
|
6
|
+
export interface MutationHistoryOperations {
|
|
7
|
+
list(absolutePath: string, maxResults: number): Promise<readonly MutationHistoryEntry[]>;
|
|
8
|
+
revert(absolutePath: string, entryId: string): Promise<{ path: string; newHash: string | null }>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createMutationHistoryOperations(): MutationHistoryOperations {
|
|
12
|
+
return {
|
|
13
|
+
list(absolutePath, maxResults) {
|
|
14
|
+
return withWorkspace(
|
|
15
|
+
() => workspaceForPath(absolutePath),
|
|
16
|
+
async ({ workspaceId, root }) => {
|
|
17
|
+
const client = await lectorClient();
|
|
18
|
+
const path = toWorkspaceRelativePath(root, absolutePath);
|
|
19
|
+
const { entries } = await client.call("workspace.mutationHistory", { workspaceId, path, maxResults });
|
|
20
|
+
return entries;
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
},
|
|
24
|
+
revert(absolutePath, entryId) {
|
|
25
|
+
return withWorkspace(
|
|
26
|
+
() => workspaceForPath(absolutePath),
|
|
27
|
+
async ({ workspaceId }) => {
|
|
28
|
+
const client = await lectorClient();
|
|
29
|
+
return client.call("workspace.revertMutation", { workspaceId, entryId });
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { OperationOutputs } from "@danypops/lector";
|
|
2
|
+
import { lectorClient, withWorkspace, workspaceForCodeIntelligencePath } from "./lector-client.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Thin wrapper over Lector's non-LSP reference-based rename: moves a file and rewrites every
|
|
6
|
+
* static import/export specifier the workspace's own populated symbol graph knows references it.
|
|
7
|
+
* `fromPath` resolves its own workspace (workspaceForCodeIntelligencePath -- this spawns a real
|
|
8
|
+
* language server), matching every other code-intelligence operation's convention.
|
|
9
|
+
*/
|
|
10
|
+
export interface ReferenceBasedRenameOperations {
|
|
11
|
+
rename(fromPath: string, toPath: string, maxFiles: number, maxSymbolsPerFile: number): Promise<OperationOutputs["workspace.referenceBasedRename"]>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createReferenceBasedRenameOperations(): ReferenceBasedRenameOperations {
|
|
15
|
+
return {
|
|
16
|
+
async rename(fromPath, toPath, maxFiles, maxSymbolsPerFile) {
|
|
17
|
+
return withWorkspace(
|
|
18
|
+
() => workspaceForCodeIntelligencePath(fromPath),
|
|
19
|
+
async ({ workspaceId }) => {
|
|
20
|
+
const client = await lectorClient();
|
|
21
|
+
return client.call("workspace.referenceBasedRename", { workspaceId, fromPath, toPath, maxFiles, maxSymbolsPerFile });
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { OperationOutputs } from "@danypops/lector";
|
|
2
|
+
import { lectorClient, withWorkspace, workspaceForCodeIntelligencePath } from "./lector-client.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Thin wrappers over Lector's LSP-driven prepareRename/rename -- position-based (path + 1-indexed
|
|
6
|
+
* line + character), matching every other code-intelligence operation's convention. `path`
|
|
7
|
+
* resolves its own workspace per call (workspaceForCodeIntelligencePath -- spawns a real
|
|
8
|
+
* language server).
|
|
9
|
+
*/
|
|
10
|
+
export interface RenameOperations {
|
|
11
|
+
prepareRename(path: string, line: number, character: number): Promise<OperationOutputs["workspace.prepareRename"]>;
|
|
12
|
+
rename(path: string, line: number, character: number, newName: string): Promise<OperationOutputs["workspace.rename"]>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createRenameOperations(): RenameOperations {
|
|
16
|
+
return {
|
|
17
|
+
async prepareRename(path, line, character) {
|
|
18
|
+
return withWorkspace(
|
|
19
|
+
() => workspaceForCodeIntelligencePath(path),
|
|
20
|
+
async ({ workspaceId }) => {
|
|
21
|
+
const client = await lectorClient();
|
|
22
|
+
return client.call("workspace.prepareRename", { workspaceId, path, line, character });
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
},
|
|
26
|
+
async rename(path, line, character, newName) {
|
|
27
|
+
return withWorkspace(
|
|
28
|
+
() => workspaceForCodeIntelligencePath(path),
|
|
29
|
+
async ({ workspaceId }) => {
|
|
30
|
+
const client = await lectorClient();
|
|
31
|
+
return client.call("workspace.rename", { workspaceId, path, line, character, newName });
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { lectorClient } from "./lector-client.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thin wrapper over repo.evictCache -- no `directory`/workspaceForDirectory resolution (matching
|
|
5
|
+
* repo-fetch-operations.ts and repo-cache-list-operations.ts: this targets the daemon-wide fetch
|
|
6
|
+
* cache, not a workspace-scoped concept).
|
|
7
|
+
*/
|
|
8
|
+
export interface RepoCacheEvictOperations {
|
|
9
|
+
evict(host: string, owner: string, repo: string, ref: string | null): Promise<{ evicted: boolean }>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createRepoCacheEvictOperations(): RepoCacheEvictOperations {
|
|
13
|
+
return {
|
|
14
|
+
async evict(host, owner, repo, ref) {
|
|
15
|
+
const client = await lectorClient();
|
|
16
|
+
return client.call("repo.evictCache", { host, owner, repo, ref });
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CachedRepositoryPage } from "@danypops/lector";
|
|
2
|
+
import { lectorClient } from "./lector-client.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Thin wrapper over repo.listCache -- no network, no cache mutation, no `directory`/
|
|
6
|
+
* workspaceForDirectory resolution (matching repo-fetch-operations.ts: this queries the
|
|
7
|
+
* daemon-wide fetch cache, not a workspace-scoped concept).
|
|
8
|
+
*/
|
|
9
|
+
export interface RepoCacheListOperations {
|
|
10
|
+
list(
|
|
11
|
+
filters: { text?: string; host?: string; owner?: string; repo?: string; ref?: string },
|
|
12
|
+
maxResults: number,
|
|
13
|
+
cursor?: string,
|
|
14
|
+
): Promise<CachedRepositoryPage>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createRepoCacheListOperations(): RepoCacheListOperations {
|
|
18
|
+
return {
|
|
19
|
+
async list(filters, maxResults, cursor) {
|
|
20
|
+
const client = await lectorClient();
|
|
21
|
+
return client.call("repo.listCache", { ...filters, maxResults, cursor });
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CachedRepositoryPage, RepoFetchResult } from "@danypops/lector";
|
|
2
|
+
import type { LectorTheme } from "./lector-tui-theme.ts";
|
|
3
|
+
|
|
4
|
+
type RepoCacheAction = "fetch" | "list" | "evict";
|
|
5
|
+
|
|
6
|
+
export function formatRepoCacheCall(
|
|
7
|
+
action: RepoCacheAction,
|
|
8
|
+
args: { owner?: unknown; repo?: unknown; ref?: unknown; host?: unknown; text?: unknown },
|
|
9
|
+
theme: LectorTheme,
|
|
10
|
+
): string {
|
|
11
|
+
const label = theme.fg("toolTitle", theme.bold("repo_cache"));
|
|
12
|
+
if (action === "list") {
|
|
13
|
+
const filter = typeof args.text === "string" && args.text.length > 0 ? args.text : typeof args.repo === "string" ? args.repo : "";
|
|
14
|
+
return `${label} ${theme.fg("accent", "list")}${filter ? ` ${theme.fg("dim", filter)}` : ""}`;
|
|
15
|
+
}
|
|
16
|
+
const host = typeof args.host === "string" && args.host.length > 0 ? args.host : "github.com";
|
|
17
|
+
const owner = typeof args.owner === "string" ? args.owner : "";
|
|
18
|
+
const repo = typeof args.repo === "string" ? args.repo : "";
|
|
19
|
+
const ref = typeof args.ref === "string" ? `@${args.ref}` : "";
|
|
20
|
+
return `${label} ${theme.fg("accent", action)} ${theme.fg("dim", `${host}/${owner}/${repo}${ref}`)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatRepoFetchResult(result: (RepoFetchResult & { workspaceId: string }) | undefined, theme: LectorTheme): string {
|
|
24
|
+
if (!result) return theme.fg("dim", "No result.");
|
|
25
|
+
const lines = [
|
|
26
|
+
`${theme.fg("accent", result.workspaceId)} ${result.fromCache ? theme.fg("dim", "(from cache)") : theme.fg("toolTitle", "(fetched)")} -- ${result.path}`,
|
|
27
|
+
];
|
|
28
|
+
if (result.refFallbackOccurred) {
|
|
29
|
+
lines.push(theme.fg("warning", `requested ref not found; fell back to the default branch (resolved: ${result.resolvedRef})`));
|
|
30
|
+
}
|
|
31
|
+
return lines.join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatRepoCacheListResult(page: CachedRepositoryPage | undefined, theme: LectorTheme): string {
|
|
35
|
+
const count = page?.entries.length ?? 0;
|
|
36
|
+
return count === 0 ? theme.fg("dim", "no cached repositories") : theme.fg("success", `${count} cached repositor${count === 1 ? "y" : "ies"}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatRepoCacheEvictResult(result: { evicted: boolean } | undefined, theme: LectorTheme): string {
|
|
40
|
+
if (!result) return theme.fg("dim", "No result.");
|
|
41
|
+
return result.evicted ? theme.fg("success", "evicted") : theme.fg("dim", "nothing cached for that reference");
|
|
42
|
+
}
|
|
@@ -7,14 +7,14 @@ import { lectorClient } from "./lector-client.ts";
|
|
|
7
7
|
* creates a new registered workspace from a fetched external repo.
|
|
8
8
|
*/
|
|
9
9
|
export interface RepoFetchOperations {
|
|
10
|
-
fetch(host: string, owner: string, repo: string, ref: string | null): Promise<RepoFetchResult & { workspaceId: string }>;
|
|
10
|
+
fetch(host: string, owner: string, repo: string, ref: string | null, forceRefresh?: boolean): Promise<RepoFetchResult & { workspaceId: string }>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function createLectorRepoFetchOperations(): RepoFetchOperations {
|
|
14
14
|
return {
|
|
15
|
-
async fetch(host, owner, repo, ref) {
|
|
15
|
+
async fetch(host, owner, repo, ref, forceRefresh) {
|
|
16
16
|
const client = await lectorClient();
|
|
17
|
-
return client.call("repo.fetch", { host, owner, repo, ref });
|
|
17
|
+
return client.call("repo.fetch", { host, owner, repo, ref, forceRefresh });
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-lector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@danypops/daemon-kit": "^0.22.1",
|
|
22
|
-
"@danypops/lector": "^0.
|
|
22
|
+
"@danypops/lector": "^0.6.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@earendil-works/pi-ai": "^0.81.1",
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { RepoFetchResult } from "@danypops/lector";
|
|
2
|
-
import type { LectorTheme } from "./lector-tui-theme.ts";
|
|
3
|
-
|
|
4
|
-
export function formatRepoFetchCall(args: { owner?: unknown; repo?: unknown; ref?: unknown; host?: unknown }, theme: LectorTheme): string {
|
|
5
|
-
const host = typeof args.host === "string" && args.host.length > 0 ? args.host : "github.com";
|
|
6
|
-
const owner = typeof args.owner === "string" ? args.owner : "";
|
|
7
|
-
const repo = typeof args.repo === "string" ? args.repo : "";
|
|
8
|
-
const ref = typeof args.ref === "string" ? `@${args.ref}` : "";
|
|
9
|
-
return `${theme.fg("toolTitle", theme.bold("repo_fetch"))} ${theme.fg("accent", `${host}/${owner}/${repo}${ref}`)}`;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function formatRepoFetchResult(result: (RepoFetchResult & { workspaceId: string }) | undefined, theme: LectorTheme): string {
|
|
13
|
-
if (!result) return theme.fg("dim", "No result.");
|
|
14
|
-
const lines = [
|
|
15
|
-
`${theme.fg("accent", result.workspaceId)} ${result.fromCache ? theme.fg("dim", "(from cache)") : theme.fg("toolTitle", "(fetched)")} -- ${result.path}`,
|
|
16
|
-
];
|
|
17
|
-
if (result.refFallbackOccurred) {
|
|
18
|
-
lines.push(theme.fg("warning", `requested ref not found; fell back to the default branch (resolved: ${result.resolvedRef})`));
|
|
19
|
-
}
|
|
20
|
-
return lines.join("\n");
|
|
21
|
-
}
|