@danypops/pi-lector 0.12.9 → 0.12.11
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/editor/editor-state.ts +14 -1
- package/extension/src/editor/index.ts +19 -0
- package/extension/src/external-search/operations.ts +28 -14
- package/extension/src/git/operations.ts +25 -16
- package/extension/src/index.ts +69 -28
- package/extension/src/mutation-history/operations.ts +28 -12
- package/extension/src/repo-cache/evict-operations.ts +11 -8
- package/extension/src/repo-cache/list-operations.ts +12 -8
- package/extension/src/repo-fetch/operations.ts +22 -6
- package/extension/src/symbol-annotation/operations.ts +88 -59
- package/extension/src/vehicle-client.ts +124 -0
- package/package.json +7 -4
|
@@ -62,7 +62,7 @@ export class EditorState {
|
|
|
62
62
|
// ── Normal mode ──────────────────────────────────────────────────────────
|
|
63
63
|
|
|
64
64
|
private handleNormalKey(data: string): void {
|
|
65
|
-
// Two-key sequences: dd, gg, yy.
|
|
65
|
+
// Two-key sequences: dd, gg, yy, ZZ, ZQ.
|
|
66
66
|
if (this.normalPrefix) {
|
|
67
67
|
const prefix = this.normalPrefix;
|
|
68
68
|
this.normalPrefix = "";
|
|
@@ -78,6 +78,18 @@ export class EditorState {
|
|
|
78
78
|
this.yankedLine = this.currentLineText;
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
|
+
// Real vim's own close-on-demand mnemonics -- ZZ saves and quits (equivalent to :wq),
|
|
82
|
+
// ZQ quits without saving (equivalent to :q). Added so a host never needs to invent its
|
|
83
|
+
// own dismiss keybinding or reach into this class's internal mode/dirty state -- the
|
|
84
|
+
// editor decides its own exit paths, exactly like :q/:wq already do.
|
|
85
|
+
if (prefix === "Z" && data === "Z") {
|
|
86
|
+
this.pendingAction = { kind: "save-and-quit" };
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (prefix === "Z" && data === "Q") {
|
|
90
|
+
this.pendingAction = { kind: "quit" };
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
81
93
|
// Prefix didn't complete into a known sequence -- fall through and handle `data` fresh.
|
|
82
94
|
}
|
|
83
95
|
|
|
@@ -106,6 +118,7 @@ export class EditorState {
|
|
|
106
118
|
case "d":
|
|
107
119
|
case "g":
|
|
108
120
|
case "y":
|
|
121
|
+
case "Z":
|
|
109
122
|
this.normalPrefix = data;
|
|
110
123
|
return;
|
|
111
124
|
case "i":
|
|
@@ -15,8 +15,27 @@
|
|
|
15
15
|
* construct and mount a real Lector editor Component directly, against its own tui/theme
|
|
16
16
|
* implementation and its own ModalEditorHost backed by whatever it wants (a Lector daemon
|
|
17
17
|
* client, a Vehicle operation, a plain filesystem call -- this package doesn't care).
|
|
18
|
+
*
|
|
19
|
+
* The same holds for ExplorerComponent (the oil.nvim-style directory explorer, built on the same
|
|
20
|
+
* EditorState engine): it only depends on `tui`/`theme` plus a DirectoryExplorerSession, its own
|
|
21
|
+
* real external contract. DirectoryExplorerSession happens to be declared alongside
|
|
22
|
+
* openDirectoryExplorer in directory-explorer-operations.ts, but the two are not the same kind of
|
|
23
|
+
* thing -- exactly the ModalEditorHost/openEditorFile split repeats here. openDirectoryExplorer
|
|
24
|
+
* (and openEditorFile) are this package's OWN Pi-extension-internal construction helpers, built on
|
|
25
|
+
* this package's own lectorClient()/withWorkspace -- deliberately not exported. A real external
|
|
26
|
+
* host is expected to supply its own DirectoryExplorerSession implementation, backed by whatever
|
|
27
|
+
* it wants, the same way Alignment's own ModalEditorHost implementation never reuses
|
|
28
|
+
* openEditorFile either.
|
|
18
29
|
*/
|
|
19
30
|
|
|
31
|
+
export type { DirectoryExplorerSession } from "./directory-explorer-operations.ts";
|
|
20
32
|
export { type EditorAction, type EditorMode, EditorState } from "./editor-state.ts";
|
|
21
33
|
export type { EditorTheme } from "./editor-theme.ts";
|
|
34
|
+
export { ExplorerComponent, type ExplorerResult, joinExplorerPath } from "./explorer-component.ts";
|
|
35
|
+
// runExplorerFlow is pure orchestration over the two interfaces above (browse, open a file into
|
|
36
|
+
// the real editor, return to the explorer at that file's own directory once it quits) -- exported
|
|
37
|
+
// for the same reason ExplorerComponent/DirectoryExplorerSession are: any real host can drive this
|
|
38
|
+
// exact, already-tested loop without re-deriving it, instead of only this package's own Pi
|
|
39
|
+
// extension entry point being able to.
|
|
40
|
+
export { type ExplorerFlowHost, runExplorerFlow } from "./explorer-flow.ts";
|
|
22
41
|
export { ModalEditorComponent, type ModalEditorHost } from "./modal-editor-component.ts";
|
|
@@ -1,26 +1,40 @@
|
|
|
1
1
|
import type { GithubRepoSearchResult, NpmPackageCandidate, SourcegraphCodeCandidate } from "@danypops/lector";
|
|
2
|
-
import {
|
|
2
|
+
import { invokeLectorVehicleOperation, type LectorVehicleCall } from "../vehicle-client.ts";
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/** Matches EXTERNAL_SEARCH_PERMISSIONS' own declared value server-side (external-search/operation-registration.ts). */
|
|
5
|
+
const EXTERNAL_SEARCH_PERMISSIONS = ["external-search:read"];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Thin wrapper over search.githubRepos/search.npmPackages/search.sourcegraphCode, dispatched
|
|
9
|
+
* through invokeVehicleOperation -- explicit-query discovery inputs shaped for
|
|
10
|
+
* repo_cache/package_source, never open-ended discovery/trending.
|
|
11
|
+
*/
|
|
5
12
|
export interface ExternalSearchOperations {
|
|
6
|
-
githubRepos(query: string, maxResults: number): Promise<GithubRepoSearchResult>;
|
|
7
|
-
npmPackages(query: string, maxResults: number): Promise<{ candidates: readonly NpmPackageCandidate[] }>;
|
|
8
|
-
sourcegraphCode(query: string, maxResults: number): Promise<{ candidates: readonly SourcegraphCodeCandidate[] }>;
|
|
13
|
+
githubRepos(query: string, maxResults: number, call: LectorVehicleCall): Promise<GithubRepoSearchResult>;
|
|
14
|
+
npmPackages(query: string, maxResults: number, call: LectorVehicleCall): Promise<{ candidates: readonly NpmPackageCandidate[] }>;
|
|
15
|
+
sourcegraphCode(query: string, maxResults: number, call: LectorVehicleCall): Promise<{ candidates: readonly SourcegraphCodeCandidate[] }>;
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
export function createExternalSearchOperations(): ExternalSearchOperations {
|
|
12
19
|
return {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return client.call("search.githubRepos", { query, maxResults });
|
|
20
|
+
githubRepos(query, maxResults, call) {
|
|
21
|
+
return invokeLectorVehicleOperation<GithubRepoSearchResult>("search.githubRepos", { query, maxResults }, EXTERNAL_SEARCH_PERMISSIONS, call);
|
|
16
22
|
},
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
npmPackages(query, maxResults, call) {
|
|
24
|
+
return invokeLectorVehicleOperation<{ candidates: readonly NpmPackageCandidate[] }>(
|
|
25
|
+
"search.npmPackages",
|
|
26
|
+
{ query, maxResults },
|
|
27
|
+
EXTERNAL_SEARCH_PERMISSIONS,
|
|
28
|
+
call,
|
|
29
|
+
);
|
|
20
30
|
},
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
sourcegraphCode(query, maxResults, call) {
|
|
32
|
+
return invokeLectorVehicleOperation<{ candidates: readonly SourcegraphCodeCandidate[] }>(
|
|
33
|
+
"search.sourcegraphCode",
|
|
34
|
+
{ query, maxResults },
|
|
35
|
+
EXTERNAL_SEARCH_PERMISSIONS,
|
|
36
|
+
call,
|
|
37
|
+
);
|
|
24
38
|
},
|
|
25
39
|
};
|
|
26
40
|
}
|
|
@@ -1,48 +1,57 @@
|
|
|
1
1
|
import type { GitDiffResult, GitLogEntry, GitStatusSummary, OperationOutputs } from "@danypops/lector";
|
|
2
2
|
import { lectorClient, withWorkspace, workspaceForDirectory } from "../lector-client.ts";
|
|
3
|
+
import { invokeLectorVehicleOperation, type LectorVehicleCall } from "../vehicle-client.ts";
|
|
3
4
|
|
|
4
5
|
type SymbolComparison = OperationOutputs["workspace.compareSymbolAcrossVersions"];
|
|
5
6
|
|
|
7
|
+
/** Matches GIT_READ_PERMISSIONS' own declared value server-side (git/operation-registration.ts). */
|
|
8
|
+
const GIT_READ_PERMISSIONS = ["workspace:read"];
|
|
9
|
+
|
|
6
10
|
/**
|
|
7
11
|
* Thin wrappers over Lector's read-only git operations. `directory` is
|
|
8
12
|
* required, same convention as find_symbols -- no implicit "whatever the
|
|
9
13
|
* session's cwd is" fallback.
|
|
14
|
+
*
|
|
15
|
+
* status/log/diff dispatch through invokeLectorVehicleOperation (the real VehicleRegistry-backed
|
|
16
|
+
* workspace.gitStatus/gitLog/gitDiff operations -- see Lector Phase 1/2 of the vehicle-client-pi
|
|
17
|
+
* adoption epic) instead of a bare lectorClient().call(), gaining activity broadcasting, the
|
|
18
|
+
* local /safety ask gate, and idempotency-key/correlationId derivation for free. compareSymbol
|
|
19
|
+
* (workspace.compareSymbolAcrossVersions) has not migrated onto VehicleRegistry server-side yet,
|
|
20
|
+
* so it stays on the legacy dispatch unchanged.
|
|
10
21
|
*/
|
|
11
22
|
export interface GitOperations {
|
|
12
|
-
status(directory: string): Promise<GitStatusSummary>;
|
|
13
|
-
log(directory: string, maxCount: number): Promise<readonly GitLogEntry[]>;
|
|
14
|
-
diff(directory: string, ref: string | undefined, maxBytes: number): Promise<GitDiffResult>;
|
|
23
|
+
status(directory: string, call: LectorVehicleCall): Promise<GitStatusSummary>;
|
|
24
|
+
log(directory: string, maxCount: number, call: LectorVehicleCall): Promise<readonly GitLogEntry[]>;
|
|
25
|
+
diff(directory: string, ref: string | undefined, maxBytes: number, call: LectorVehicleCall): Promise<GitDiffResult>;
|
|
15
26
|
compareSymbol(directory: string, path: string, symbolName: string, fromRef: string, toRef: string | undefined, maxBytes: number): Promise<SymbolComparison>;
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
export function createLectorGitOperations(): GitOperations {
|
|
19
30
|
return {
|
|
20
|
-
async status(directory) {
|
|
31
|
+
async status(directory, call) {
|
|
21
32
|
return withWorkspace(
|
|
22
33
|
() => workspaceForDirectory(directory),
|
|
23
|
-
|
|
24
|
-
const client = await lectorClient();
|
|
25
|
-
return client.call("workspace.gitStatus", { workspaceId });
|
|
26
|
-
},
|
|
34
|
+
({ workspaceId }) => invokeLectorVehicleOperation<GitStatusSummary>("workspace.gitStatus", { workspaceId }, GIT_READ_PERMISSIONS, call),
|
|
27
35
|
);
|
|
28
36
|
},
|
|
29
|
-
async log(directory, maxCount) {
|
|
37
|
+
async log(directory, maxCount, call) {
|
|
30
38
|
return withWorkspace(
|
|
31
39
|
() => workspaceForDirectory(directory),
|
|
32
40
|
async ({ workspaceId }) => {
|
|
33
|
-
const
|
|
34
|
-
|
|
41
|
+
const { entries } = await invokeLectorVehicleOperation<{ entries: readonly GitLogEntry[] }>(
|
|
42
|
+
"workspace.gitLog",
|
|
43
|
+
{ workspaceId, maxCount },
|
|
44
|
+
GIT_READ_PERMISSIONS,
|
|
45
|
+
call,
|
|
46
|
+
);
|
|
35
47
|
return entries;
|
|
36
48
|
},
|
|
37
49
|
);
|
|
38
50
|
},
|
|
39
|
-
async diff(directory, ref, maxBytes) {
|
|
51
|
+
async diff(directory, ref, maxBytes, call) {
|
|
40
52
|
return withWorkspace(
|
|
41
53
|
() => workspaceForDirectory(directory),
|
|
42
|
-
|
|
43
|
-
const client = await lectorClient();
|
|
44
|
-
return client.call("workspace.gitDiff", { workspaceId, ref, maxBytes });
|
|
45
|
-
},
|
|
54
|
+
({ workspaceId }) => invokeLectorVehicleOperation<GitDiffResult>("workspace.gitDiff", { workspaceId, ref, maxBytes }, GIT_READ_PERMISSIONS, call),
|
|
46
55
|
);
|
|
47
56
|
},
|
|
48
57
|
async compareSymbol(directory, path, symbolName, fromRef, toRef, maxBytes) {
|
package/extension/src/index.ts
CHANGED
|
@@ -128,6 +128,7 @@ import { createLectorSearchOperations } from "./search/operations.ts";
|
|
|
128
128
|
import { formatSearchCall, formatSearchResult } from "./search/rendering.ts";
|
|
129
129
|
import { type AnnotationAnchorInput, createLectorSymbolAnnotationOperations } from "./symbol-annotation/operations.ts";
|
|
130
130
|
import { formatAnnotationDetail, formatAnnotationListSummary, formatAnnotationSummary } from "./symbol-annotation/rendering.ts";
|
|
131
|
+
import type { LectorVehicleCall } from "./vehicle-client.ts";
|
|
131
132
|
import {
|
|
132
133
|
type CachePresentationState,
|
|
133
134
|
cacheContextMessage,
|
|
@@ -906,30 +907,42 @@ export default function (pi: ExtensionAPI) {
|
|
|
906
907
|
rootId: Type.Optional(Type.String({ description: "The subtree's root annotation id -- required for tree" })),
|
|
907
908
|
maxDepth: Type.Optional(Type.Number({ description: "Maximum containment hops from rootId to include -- required for tree" })),
|
|
908
909
|
}),
|
|
909
|
-
async execute(
|
|
910
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<SymbolAnnotationToolDetails>> {
|
|
910
911
|
const path = resolve(cwd, params.path);
|
|
912
|
+
const vehicleCall: LectorVehicleCall = {
|
|
913
|
+
toolName: "symbol_annotations",
|
|
914
|
+
toolCallId,
|
|
915
|
+
signal,
|
|
916
|
+
context: ctx,
|
|
917
|
+
};
|
|
911
918
|
const details: SymbolAnnotationToolDetails = {};
|
|
912
919
|
let text: string;
|
|
913
920
|
if (params.action === "create") {
|
|
914
921
|
if (!params.subtype || !params.title || params.body === undefined || !params.anchors || params.anchors.length === 0) {
|
|
915
922
|
throw new Error("symbol_annotations create requires subtype, title, body, and at least one anchor");
|
|
916
923
|
}
|
|
917
|
-
const { annotation } = await symbolAnnotationOperations.create(
|
|
924
|
+
const { annotation } = await symbolAnnotationOperations.create(
|
|
925
|
+
path,
|
|
926
|
+
params.subtype,
|
|
927
|
+
params.title,
|
|
928
|
+
params.body,
|
|
929
|
+
resolveAnchorInputs(params.anchors),
|
|
930
|
+
vehicleCall,
|
|
931
|
+
);
|
|
918
932
|
details.annotation = annotation;
|
|
919
933
|
text = formatAnnotationDetail(annotation);
|
|
920
934
|
} else if (params.action === "get") {
|
|
921
935
|
if (!params.id) throw new Error("symbol_annotations get requires id");
|
|
922
|
-
const { annotation } = await symbolAnnotationOperations.get(path, params.id);
|
|
936
|
+
const { annotation } = await symbolAnnotationOperations.get(path, params.id, vehicleCall);
|
|
923
937
|
details.annotation = annotation;
|
|
924
938
|
text = annotation ? formatAnnotationDetail(annotation) : `no annotation "${params.id}"`;
|
|
925
939
|
} else if (params.action === "list") {
|
|
926
940
|
const status = params.listStatus === "fresh" || params.listStatus === "stale" || params.listStatus === "scrubbed" ? params.listStatus : undefined;
|
|
927
|
-
const { annotations } = await symbolAnnotationOperations.list(
|
|
928
|
-
|
|
929
|
-
status,
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
});
|
|
941
|
+
const { annotations } = await symbolAnnotationOperations.list(
|
|
942
|
+
path,
|
|
943
|
+
{ subtype: params.listSubtype, status, maxResults: params.maxResults, query: params.listQuery },
|
|
944
|
+
vehicleCall,
|
|
945
|
+
);
|
|
933
946
|
details.annotations = annotations;
|
|
934
947
|
text = annotations.length === 0 ? "no annotations" : annotations.map(formatAnnotationDetail).join("\n\n");
|
|
935
948
|
} else if (params.action === "refresh") {
|
|
@@ -943,32 +956,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
943
956
|
params.title,
|
|
944
957
|
params.body,
|
|
945
958
|
resolveAnchorInputs(params.anchors),
|
|
959
|
+
vehicleCall,
|
|
946
960
|
);
|
|
947
961
|
details.annotation = annotation;
|
|
948
962
|
text = annotation ? formatAnnotationDetail(annotation) : `no annotation "${params.id}"`;
|
|
949
963
|
} else if (params.action === "scrub") {
|
|
950
964
|
if (!params.id) throw new Error("symbol_annotations scrub requires id");
|
|
951
|
-
const { scrubbed } = await symbolAnnotationOperations.scrub(path, params.id);
|
|
965
|
+
const { scrubbed } = await symbolAnnotationOperations.scrub(path, params.id, vehicleCall);
|
|
952
966
|
details.scrubbed = scrubbed;
|
|
953
967
|
text = scrubbed ? `scrubbed ${params.id}` : `"${params.id}" was already scrubbed or does not exist`;
|
|
954
968
|
} else if (params.action === "restore") {
|
|
955
969
|
if (!params.id) throw new Error("symbol_annotations restore requires id");
|
|
956
|
-
const { restored } = await symbolAnnotationOperations.restore(path, params.id);
|
|
970
|
+
const { restored } = await symbolAnnotationOperations.restore(path, params.id, vehicleCall);
|
|
957
971
|
details.restored = restored;
|
|
958
972
|
text = restored ? `restored ${params.id}` : `"${params.id}" was not scrubbed or does not exist`;
|
|
959
973
|
} else if (params.action === "contain") {
|
|
960
974
|
if (!params.parentId || !params.childId) throw new Error("symbol_annotations contain requires parentId and childId");
|
|
961
|
-
const { contained } = await symbolAnnotationOperations.contain(path, params.parentId, params.childId);
|
|
975
|
+
const { contained } = await symbolAnnotationOperations.contain(path, params.parentId, params.childId, vehicleCall);
|
|
962
976
|
details.contained = contained;
|
|
963
977
|
text = `"${params.parentId}" now contains "${params.childId}"`;
|
|
964
978
|
} else if (params.action === "uncontain") {
|
|
965
979
|
if (!params.parentId || !params.childId) throw new Error("symbol_annotations uncontain requires parentId and childId");
|
|
966
|
-
const { uncontained } = await symbolAnnotationOperations.uncontain(path, params.parentId, params.childId);
|
|
980
|
+
const { uncontained } = await symbolAnnotationOperations.uncontain(path, params.parentId, params.childId, vehicleCall);
|
|
967
981
|
details.uncontained = uncontained;
|
|
968
982
|
text = uncontained ? `"${params.parentId}" no longer contains "${params.childId}"` : `"${params.parentId}" did not contain "${params.childId}"`;
|
|
969
983
|
} else if (params.action === "tree") {
|
|
970
984
|
if (!params.rootId || params.maxDepth === undefined) throw new Error("symbol_annotations tree requires rootId and maxDepth");
|
|
971
|
-
const { annotations } = await symbolAnnotationOperations.tree(path, params.rootId, params.maxDepth);
|
|
985
|
+
const { annotations } = await symbolAnnotationOperations.tree(path, params.rootId, params.maxDepth, vehicleCall);
|
|
972
986
|
details.annotations = annotations;
|
|
973
987
|
text = annotations.length === 0 ? `no annotation "${params.rootId}"` : annotations.map(formatAnnotationDetail).join("\n\n");
|
|
974
988
|
} else {
|
|
@@ -1253,16 +1267,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
1253
1267
|
Type.String({ description: "Git ref for the 'after' version; omit to compare against the current working tree -- action=compare-symbol only" }),
|
|
1254
1268
|
),
|
|
1255
1269
|
}),
|
|
1256
|
-
async execute(
|
|
1270
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<GitToolDetails>> {
|
|
1257
1271
|
const directory = resolve(cwd, params.directory);
|
|
1272
|
+
const vehicleCall: LectorVehicleCall = { toolName: "git", toolCallId, signal, context: ctx };
|
|
1258
1273
|
if (params.action === "status") {
|
|
1259
|
-
const summary = await gitOperations.status(directory);
|
|
1274
|
+
const summary = await gitOperations.status(directory, vehicleCall);
|
|
1260
1275
|
const details: GitToolDetails = { action: "status", summary };
|
|
1261
1276
|
return { content: [{ type: "text", text: JSON.stringify(summary) }], details };
|
|
1262
1277
|
}
|
|
1263
1278
|
if (params.action === "log") {
|
|
1264
1279
|
if (params.maxCount === undefined) throw new Error("git action=log requires maxCount");
|
|
1265
|
-
const entries = await gitOperations.log(directory, params.maxCount);
|
|
1280
|
+
const entries = await gitOperations.log(directory, params.maxCount, vehicleCall);
|
|
1266
1281
|
const text =
|
|
1267
1282
|
entries.length === 0 ? "No commits found." : entries.map((e) => `${e.sha.slice(0, 8)} ${e.authoredAt} ${e.authorName} -- ${e.message}`).join("\n");
|
|
1268
1283
|
const details: GitToolDetails = { action: "log", entries };
|
|
@@ -1270,7 +1285,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1270
1285
|
}
|
|
1271
1286
|
if (params.action === "diff") {
|
|
1272
1287
|
if (params.maxBytes === undefined) throw new Error("git action=diff requires maxBytes");
|
|
1273
|
-
const result = await gitOperations.diff(directory, params.ref, params.maxBytes);
|
|
1288
|
+
const result = await gitOperations.diff(directory, params.ref, params.maxBytes, vehicleCall);
|
|
1274
1289
|
const details: GitToolDetails = { action: "diff", result };
|
|
1275
1290
|
return { content: [{ type: "text", text: result.diff.length === 0 ? "No differences." : result.diff }], details };
|
|
1276
1291
|
}
|
|
@@ -1529,11 +1544,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
1529
1544
|
maxResults: Type.Optional(Type.Number({ description: "Required for action=list -- maximum entries to return, newest first" })),
|
|
1530
1545
|
entryId: Type.Optional(Type.String({ description: "Required for action=revert -- an id returned by a prior action=list" })),
|
|
1531
1546
|
}),
|
|
1532
|
-
async execute(
|
|
1547
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<MutationHistoryToolDetails>> {
|
|
1533
1548
|
const absolutePath = resolve(cwd, params.path);
|
|
1549
|
+
const vehicleCall: LectorVehicleCall = {
|
|
1550
|
+
toolName: "mutation_history",
|
|
1551
|
+
toolCallId,
|
|
1552
|
+
signal,
|
|
1553
|
+
context: ctx,
|
|
1554
|
+
};
|
|
1534
1555
|
if (params.action === "list") {
|
|
1535
1556
|
if (params.maxResults === undefined) throw new Error("mutation_history action=list requires maxResults");
|
|
1536
|
-
const entries = await mutationHistoryOperations.list(absolutePath, params.maxResults);
|
|
1557
|
+
const entries = await mutationHistoryOperations.list(absolutePath, params.maxResults, vehicleCall);
|
|
1537
1558
|
const text =
|
|
1538
1559
|
entries.length === 0
|
|
1539
1560
|
? "no recorded mutation history for this path"
|
|
@@ -1541,7 +1562,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1541
1562
|
return { content: [{ type: "text", text }], details: { action: "list", entries } };
|
|
1542
1563
|
}
|
|
1543
1564
|
if (params.entryId === undefined) throw new Error("mutation_history action=revert requires entryId");
|
|
1544
|
-
const reverted = await mutationHistoryOperations.revert(absolutePath, params.entryId);
|
|
1565
|
+
const reverted = await mutationHistoryOperations.revert(absolutePath, params.entryId, vehicleCall);
|
|
1545
1566
|
return {
|
|
1546
1567
|
content: [{ type: "text", text: `${reverted.path} reverted -> ${reverted.newHash ?? "(deleted)"}` }],
|
|
1547
1568
|
details: { action: "revert", reverted },
|
|
@@ -1711,15 +1732,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
1711
1732
|
maxResults: Type.Optional(Type.Number({ description: "Required for action=list -- maximum entries to return in this page" })),
|
|
1712
1733
|
cursor: Type.Optional(Type.String({ description: "action=list only -- opaque cursor from a prior call's nextCursor, to fetch the next page" })),
|
|
1713
1734
|
}),
|
|
1714
|
-
async execute(
|
|
1735
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<RepoCacheToolDetails>> {
|
|
1736
|
+
const vehicleCall: LectorVehicleCall = {
|
|
1737
|
+
toolName: "repo_cache",
|
|
1738
|
+
toolCallId,
|
|
1739
|
+
signal,
|
|
1740
|
+
context: ctx,
|
|
1741
|
+
};
|
|
1715
1742
|
if (params.action === "fetch") {
|
|
1716
1743
|
if (!params.owner || !params.repo) throw new Error("repo_cache action=fetch requires owner and repo");
|
|
1717
|
-
const result = await repoFetchOperations.fetch(
|
|
1744
|
+
const result = await repoFetchOperations.fetch(
|
|
1745
|
+
params.host ?? "github.com",
|
|
1746
|
+
params.owner,
|
|
1747
|
+
params.repo,
|
|
1748
|
+
params.ref ?? null,
|
|
1749
|
+
params.forceRefresh,
|
|
1750
|
+
vehicleCall,
|
|
1751
|
+
);
|
|
1718
1752
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "fetch", result } };
|
|
1719
1753
|
}
|
|
1720
1754
|
if (params.action === "evict") {
|
|
1721
1755
|
if (!params.owner || !params.repo) throw new Error("repo_cache action=evict requires owner and repo");
|
|
1722
|
-
const result = await repoCacheEvictOperations.evict(params.host ?? "github.com", params.owner, params.repo, params.ref ?? null);
|
|
1756
|
+
const result = await repoCacheEvictOperations.evict(params.host ?? "github.com", params.owner, params.repo, params.ref ?? null, vehicleCall);
|
|
1723
1757
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "evict", result } };
|
|
1724
1758
|
}
|
|
1725
1759
|
if (params.maxResults === undefined) throw new Error("repo_cache action=list requires maxResults");
|
|
@@ -1727,6 +1761,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1727
1761
|
{ text: params.text, host: params.host, owner: params.owner, repo: params.repo, ref: params.ref },
|
|
1728
1762
|
params.maxResults,
|
|
1729
1763
|
params.cursor,
|
|
1764
|
+
vehicleCall,
|
|
1730
1765
|
);
|
|
1731
1766
|
return { content: [{ type: "text", text: JSON.stringify(page) }], details: { action: "list", page } };
|
|
1732
1767
|
},
|
|
@@ -1791,17 +1826,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
1791
1826
|
}),
|
|
1792
1827
|
maxResults: Type.Optional(Type.Number({ description: "Maximum candidates to return (default 20)" })),
|
|
1793
1828
|
}),
|
|
1794
|
-
async execute(
|
|
1829
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<ExternalSearchToolDetails>> {
|
|
1795
1830
|
const maxResults = params.maxResults ?? DEFAULT_EXTERNAL_SEARCH_MAX_RESULTS;
|
|
1831
|
+
const vehicleCall: LectorVehicleCall = {
|
|
1832
|
+
toolName: "external_search",
|
|
1833
|
+
toolCallId,
|
|
1834
|
+
signal,
|
|
1835
|
+
context: ctx,
|
|
1836
|
+
};
|
|
1796
1837
|
if (params.action === "github_repos") {
|
|
1797
|
-
const result = await externalSearchOperations.githubRepos(params.query, maxResults);
|
|
1838
|
+
const result = await externalSearchOperations.githubRepos(params.query, maxResults, vehicleCall);
|
|
1798
1839
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "github_repos", result } };
|
|
1799
1840
|
}
|
|
1800
1841
|
if (params.action === "npm_packages") {
|
|
1801
|
-
const result = await externalSearchOperations.npmPackages(params.query, maxResults);
|
|
1842
|
+
const result = await externalSearchOperations.npmPackages(params.query, maxResults, vehicleCall);
|
|
1802
1843
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "npm_packages", result } };
|
|
1803
1844
|
}
|
|
1804
|
-
const result = await externalSearchOperations.sourcegraphCode(params.query, maxResults);
|
|
1845
|
+
const result = await externalSearchOperations.sourcegraphCode(params.query, maxResults, vehicleCall);
|
|
1805
1846
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "sourcegraph_code", result } };
|
|
1806
1847
|
},
|
|
1807
1848
|
renderCall(args, theme, context) {
|
|
@@ -1,33 +1,49 @@
|
|
|
1
1
|
import type { MutationHistoryEntry } from "@danypops/lector";
|
|
2
|
-
import {
|
|
2
|
+
import { withWorkspace, workspaceForPath } from "../lector-client.ts";
|
|
3
|
+
import { invokeLectorVehicleOperation, type LectorVehicleCall } from "../vehicle-client.ts";
|
|
3
4
|
import { toWorkspaceRelativePath } from "../workspace-relative-path.ts";
|
|
4
5
|
|
|
5
|
-
/**
|
|
6
|
+
/** Match MUTATION_HISTORY_READ_PERMISSIONS/MUTATION_HISTORY_WRITE_PERMISSIONS' own declared values server-side (mutation-history/operation-registration.ts). */
|
|
7
|
+
const MUTATION_HISTORY_READ_PERMISSIONS = ["workspace:read"];
|
|
8
|
+
const MUTATION_HISTORY_WRITE_PERMISSIONS = ["workspace:write"];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Thin wrapper over Lector's mutation history, dispatched through invokeLectorVehicleOperation:
|
|
12
|
+
* every successful edit is recorded, and any entry can be reverted -- guarded the same way every
|
|
13
|
+
* other Lector write is.
|
|
14
|
+
*/
|
|
6
15
|
export interface MutationHistoryOperations {
|
|
7
|
-
list(absolutePath: string, maxResults: number): Promise<readonly MutationHistoryEntry[]>;
|
|
8
|
-
revert(absolutePath: string, entryId: string): Promise<{ path: string; newHash: string | null }>;
|
|
16
|
+
list(absolutePath: string, maxResults: number, call: LectorVehicleCall): Promise<readonly MutationHistoryEntry[]>;
|
|
17
|
+
revert(absolutePath: string, entryId: string, call: LectorVehicleCall): Promise<{ path: string; newHash: string | null }>;
|
|
9
18
|
}
|
|
10
19
|
|
|
11
20
|
export function createMutationHistoryOperations(): MutationHistoryOperations {
|
|
12
21
|
return {
|
|
13
|
-
list(absolutePath, maxResults) {
|
|
22
|
+
list(absolutePath, maxResults, call) {
|
|
14
23
|
return withWorkspace(
|
|
15
24
|
() => workspaceForPath(absolutePath),
|
|
16
25
|
async ({ workspaceId, root }) => {
|
|
17
|
-
const client = await lectorClient();
|
|
18
26
|
const path = toWorkspaceRelativePath(root, absolutePath);
|
|
19
|
-
const { entries } = await
|
|
27
|
+
const { entries } = await invokeLectorVehicleOperation<{ entries: readonly MutationHistoryEntry[] }>(
|
|
28
|
+
"workspace.mutationHistory",
|
|
29
|
+
{ workspaceId, path, maxResults },
|
|
30
|
+
MUTATION_HISTORY_READ_PERMISSIONS,
|
|
31
|
+
call,
|
|
32
|
+
);
|
|
20
33
|
return entries;
|
|
21
34
|
},
|
|
22
35
|
);
|
|
23
36
|
},
|
|
24
|
-
revert(absolutePath, entryId) {
|
|
37
|
+
revert(absolutePath, entryId, call) {
|
|
25
38
|
return withWorkspace(
|
|
26
39
|
() => workspaceForPath(absolutePath),
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
({ workspaceId }) =>
|
|
41
|
+
invokeLectorVehicleOperation<{ path: string; newHash: string | null }>(
|
|
42
|
+
"workspace.revertMutation",
|
|
43
|
+
{ workspaceId, entryId },
|
|
44
|
+
MUTATION_HISTORY_WRITE_PERMISSIONS,
|
|
45
|
+
call,
|
|
46
|
+
),
|
|
31
47
|
);
|
|
32
48
|
},
|
|
33
49
|
};
|
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { invokeLectorVehicleOperation, type LectorVehicleCall } from "../vehicle-client.ts";
|
|
2
|
+
|
|
3
|
+
/** Matches REPO_WRITE_PERMISSIONS' own declared value server-side (repo-fetcher/operation-registration.ts). */
|
|
4
|
+
const REPO_WRITE_PERMISSIONS = ["workspace:write"];
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
|
-
* Thin wrapper over repo.evictCache
|
|
5
|
-
* repo-fetch/operations.ts and
|
|
6
|
-
* cache, not a
|
|
7
|
+
* Thin wrapper over repo.evictCache, dispatched through invokeVehicleOperation -- no
|
|
8
|
+
* `directory`/workspaceForDirectory resolution (matching repo-fetch/operations.ts and
|
|
9
|
+
* repo-cache/list-operations.ts: this targets the daemon-wide fetch cache, not a
|
|
10
|
+
* workspace-scoped concept).
|
|
7
11
|
*/
|
|
8
12
|
export interface RepoCacheEvictOperations {
|
|
9
|
-
evict(host: string, owner: string, repo: string, ref: string | null): Promise<{ evicted: boolean }>;
|
|
13
|
+
evict(host: string, owner: string, repo: string, ref: string | null, call: LectorVehicleCall): Promise<{ evicted: boolean }>;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
export function createRepoCacheEvictOperations(): RepoCacheEvictOperations {
|
|
13
17
|
return {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return client.callOnce("repo.evictCache", { host, owner, repo, ref });
|
|
18
|
+
evict(host, owner, repo, ref, call) {
|
|
19
|
+
return invokeLectorVehicleOperation<{ evicted: boolean }>("repo.evictCache", { host, owner, repo, ref }, REPO_WRITE_PERMISSIONS, call);
|
|
17
20
|
},
|
|
18
21
|
};
|
|
19
22
|
}
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import type { CachedRepositoryPage } from "@danypops/lector";
|
|
2
|
-
import {
|
|
2
|
+
import { invokeLectorVehicleOperation, type LectorVehicleCall } from "../vehicle-client.ts";
|
|
3
|
+
|
|
4
|
+
/** Matches REPO_LIST_CACHE_PERMISSIONS' own declared value server-side (repo-fetcher/operation-registration.ts). */
|
|
5
|
+
const REPO_LIST_CACHE_PERMISSIONS = ["workspace:read"];
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
|
-
* Thin wrapper over repo.listCache -- no network, no
|
|
6
|
-
* workspaceForDirectory resolution (matching
|
|
7
|
-
* daemon-wide fetch cache, not a workspace-scoped
|
|
8
|
+
* Thin wrapper over repo.listCache, dispatched through invokeVehicleOperation -- no network, no
|
|
9
|
+
* cache mutation, no `directory`/workspaceForDirectory resolution (matching
|
|
10
|
+
* repo-fetch/operations.ts: this queries the daemon-wide fetch cache, not a workspace-scoped
|
|
11
|
+
* concept).
|
|
8
12
|
*/
|
|
9
13
|
export interface RepoCacheListOperations {
|
|
10
14
|
list(
|
|
11
15
|
filters: { text?: string; host?: string; owner?: string; repo?: string; ref?: string },
|
|
12
16
|
maxResults: number,
|
|
13
|
-
cursor
|
|
17
|
+
cursor: string | undefined,
|
|
18
|
+
call: LectorVehicleCall,
|
|
14
19
|
): Promise<CachedRepositoryPage>;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
export function createRepoCacheListOperations(): RepoCacheListOperations {
|
|
18
23
|
return {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return client.call("repo.listCache", { ...filters, maxResults, cursor });
|
|
24
|
+
list(filters, maxResults, cursor, call) {
|
|
25
|
+
return invokeLectorVehicleOperation<CachedRepositoryPage>("repo.listCache", { ...filters, maxResults, cursor }, REPO_LIST_CACHE_PERMISSIONS, call);
|
|
22
26
|
},
|
|
23
27
|
};
|
|
24
28
|
}
|
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
import type { RepoFetchResult } from "@danypops/lector";
|
|
2
|
-
import {
|
|
2
|
+
import { invokeLectorVehicleOperation, type LectorVehicleCall } from "../vehicle-client.ts";
|
|
3
|
+
|
|
4
|
+
/** Matches REPO_WRITE_PERMISSIONS' own declared value server-side (repo-fetcher/operation-registration.ts). */
|
|
5
|
+
const REPO_WRITE_PERMISSIONS = ["workspace:write"];
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
|
-
* Thin wrapper over repo.fetch
|
|
8
|
+
* Thin wrapper over repo.fetch, dispatched through invokeVehicleOperation (the real
|
|
9
|
+
* VehicleRegistry-backed operation -- see Lector's vehicle-client-pi adoption epic) instead of a
|
|
10
|
+
* bare lectorClient().call(). No `directory`/workspaceForDirectory resolution here -- unlike
|
|
6
11
|
* every other tool in this extension, this one doesn't target an existing local directory, it
|
|
7
12
|
* creates a new registered workspace from a fetched external repo.
|
|
8
13
|
*/
|
|
9
14
|
export interface RepoFetchOperations {
|
|
10
|
-
fetch(
|
|
15
|
+
fetch(
|
|
16
|
+
host: string,
|
|
17
|
+
owner: string,
|
|
18
|
+
repo: string,
|
|
19
|
+
ref: string | null,
|
|
20
|
+
forceRefresh: boolean | undefined,
|
|
21
|
+
call: LectorVehicleCall,
|
|
22
|
+
): Promise<RepoFetchResult & { workspaceId: string }>;
|
|
11
23
|
}
|
|
12
24
|
|
|
13
25
|
export function createLectorRepoFetchOperations(): RepoFetchOperations {
|
|
14
26
|
return {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
fetch(host, owner, repo, ref, forceRefresh, call) {
|
|
28
|
+
return invokeLectorVehicleOperation<RepoFetchResult & { workspaceId: string }>(
|
|
29
|
+
"repo.fetch",
|
|
30
|
+
{ host, owner, repo, ref, forceRefresh },
|
|
31
|
+
REPO_WRITE_PERMISSIONS,
|
|
32
|
+
call,
|
|
33
|
+
);
|
|
18
34
|
},
|
|
19
35
|
};
|
|
20
36
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { OperationInputs, OperationOutputs } from "@danypops/lector";
|
|
2
|
-
import {
|
|
2
|
+
import { withWorkspace, workspaceForAnnotationPath } from "../lector-client.ts";
|
|
3
|
+
import { invokeLectorVehicleOperation, type LectorVehicleCall } from "../vehicle-client.ts";
|
|
4
|
+
|
|
5
|
+
/** Match ANNOTATION_READ_PERMISSIONS/ANNOTATION_WRITE_PERMISSIONS' own declared values server-side (symbol-annotation/operation-registration.ts). */
|
|
6
|
+
const ANNOTATION_READ_PERMISSIONS = ["workspace:read"];
|
|
7
|
+
const ANNOTATION_WRITE_PERMISSIONS = ["workspace:write"];
|
|
3
8
|
|
|
4
9
|
/** A bare symbol position an anchor is given as -- symbolNodeId and the anchor's baseline file hash are derived server-side, never supplied by the caller. */
|
|
5
10
|
export interface AnnotationAnchorInput {
|
|
@@ -23,11 +28,13 @@ export interface SymbolAnnotationOperations {
|
|
|
23
28
|
title: string,
|
|
24
29
|
body: string,
|
|
25
30
|
anchors: readonly AnnotationAnchorInput[],
|
|
31
|
+
call: LectorVehicleCall,
|
|
26
32
|
): Promise<OperationOutputs["workspace.createAnnotation"]>;
|
|
27
|
-
get(path: string, id: string): Promise<OperationOutputs["workspace.getAnnotation"]>;
|
|
33
|
+
get(path: string, id: string, call: LectorVehicleCall): Promise<OperationOutputs["workspace.getAnnotation"]>;
|
|
28
34
|
list(
|
|
29
35
|
path: string,
|
|
30
|
-
options
|
|
36
|
+
options: { subtype?: string; status?: OperationInputs["workspace.listAnnotations"]["status"]; maxResults?: number; query?: string },
|
|
37
|
+
call: LectorVehicleCall,
|
|
31
38
|
): Promise<OperationOutputs["workspace.listAnnotations"]>;
|
|
32
39
|
refresh(
|
|
33
40
|
path: string,
|
|
@@ -36,101 +43,123 @@ export interface SymbolAnnotationOperations {
|
|
|
36
43
|
title: string,
|
|
37
44
|
body: string,
|
|
38
45
|
anchors: readonly AnnotationAnchorInput[],
|
|
46
|
+
call: LectorVehicleCall,
|
|
39
47
|
): Promise<OperationOutputs["workspace.refreshAnnotation"]>;
|
|
40
|
-
scrub(path: string, id: string): Promise<OperationOutputs["workspace.scrubAnnotation"]>;
|
|
41
|
-
restore(path: string, id: string): Promise<OperationOutputs["workspace.restoreAnnotation"]>;
|
|
42
|
-
contain(path: string, parentId: string, childId: string): Promise<OperationOutputs["workspace.containAnnotation"]>;
|
|
43
|
-
uncontain(path: string, parentId: string, childId: string): Promise<OperationOutputs["workspace.uncontainAnnotation"]>;
|
|
44
|
-
tree(path: string, rootId: string, maxDepth: number): Promise<OperationOutputs["workspace.annotationTree"]>;
|
|
48
|
+
scrub(path: string, id: string, call: LectorVehicleCall): Promise<OperationOutputs["workspace.scrubAnnotation"]>;
|
|
49
|
+
restore(path: string, id: string, call: LectorVehicleCall): Promise<OperationOutputs["workspace.restoreAnnotation"]>;
|
|
50
|
+
contain(path: string, parentId: string, childId: string, call: LectorVehicleCall): Promise<OperationOutputs["workspace.containAnnotation"]>;
|
|
51
|
+
uncontain(path: string, parentId: string, childId: string, call: LectorVehicleCall): Promise<OperationOutputs["workspace.uncontainAnnotation"]>;
|
|
52
|
+
tree(path: string, rootId: string, maxDepth: number, call: LectorVehicleCall): Promise<OperationOutputs["workspace.annotationTree"]>;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperations {
|
|
48
56
|
return {
|
|
49
|
-
async create(path, subtype, title, body, anchors) {
|
|
57
|
+
async create(path, subtype, title, body, anchors, call) {
|
|
50
58
|
return withWorkspace(
|
|
51
59
|
() => workspaceForAnnotationPath(path),
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
({ workspaceId }) =>
|
|
61
|
+
invokeLectorVehicleOperation<OperationOutputs["workspace.createAnnotation"]>(
|
|
62
|
+
"workspace.createAnnotation",
|
|
63
|
+
{ workspaceId, subtype, title, body, anchors },
|
|
64
|
+
ANNOTATION_WRITE_PERMISSIONS,
|
|
65
|
+
call,
|
|
66
|
+
),
|
|
56
67
|
);
|
|
57
68
|
},
|
|
58
|
-
async get(path, id) {
|
|
69
|
+
async get(path, id, call) {
|
|
59
70
|
return withWorkspace(
|
|
60
71
|
() => workspaceForAnnotationPath(path),
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
({ workspaceId }) =>
|
|
73
|
+
invokeLectorVehicleOperation<OperationOutputs["workspace.getAnnotation"]>(
|
|
74
|
+
"workspace.getAnnotation",
|
|
75
|
+
{ workspaceId, id },
|
|
76
|
+
ANNOTATION_READ_PERMISSIONS,
|
|
77
|
+
call,
|
|
78
|
+
),
|
|
65
79
|
);
|
|
66
80
|
},
|
|
67
|
-
async list(path, options
|
|
81
|
+
async list(path, options, call) {
|
|
68
82
|
return withWorkspace(
|
|
69
83
|
() => workspaceForAnnotationPath(path),
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
workspaceId,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
query: options.query,
|
|
78
|
-
});
|
|
79
|
-
},
|
|
84
|
+
({ workspaceId }) =>
|
|
85
|
+
invokeLectorVehicleOperation<OperationOutputs["workspace.listAnnotations"]>(
|
|
86
|
+
"workspace.listAnnotations",
|
|
87
|
+
{ workspaceId, subtype: options.subtype, status: options.status, maxResults: options.maxResults, query: options.query },
|
|
88
|
+
ANNOTATION_READ_PERMISSIONS,
|
|
89
|
+
call,
|
|
90
|
+
),
|
|
80
91
|
);
|
|
81
92
|
},
|
|
82
|
-
async refresh(path, id, subtype, title, body, anchors) {
|
|
93
|
+
async refresh(path, id, subtype, title, body, anchors, call) {
|
|
83
94
|
return withWorkspace(
|
|
84
95
|
() => workspaceForAnnotationPath(path),
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
({ workspaceId }) =>
|
|
97
|
+
invokeLectorVehicleOperation<OperationOutputs["workspace.refreshAnnotation"]>(
|
|
98
|
+
"workspace.refreshAnnotation",
|
|
99
|
+
{ workspaceId, id, subtype, title, body, anchors },
|
|
100
|
+
ANNOTATION_WRITE_PERMISSIONS,
|
|
101
|
+
call,
|
|
102
|
+
),
|
|
89
103
|
);
|
|
90
104
|
},
|
|
91
|
-
async scrub(path, id) {
|
|
105
|
+
async scrub(path, id, call) {
|
|
92
106
|
return withWorkspace(
|
|
93
107
|
() => workspaceForAnnotationPath(path),
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
108
|
+
({ workspaceId }) =>
|
|
109
|
+
invokeLectorVehicleOperation<OperationOutputs["workspace.scrubAnnotation"]>(
|
|
110
|
+
"workspace.scrubAnnotation",
|
|
111
|
+
{ workspaceId, id },
|
|
112
|
+
ANNOTATION_WRITE_PERMISSIONS,
|
|
113
|
+
call,
|
|
114
|
+
),
|
|
98
115
|
);
|
|
99
116
|
},
|
|
100
|
-
async restore(path, id) {
|
|
117
|
+
async restore(path, id, call) {
|
|
101
118
|
return withWorkspace(
|
|
102
119
|
() => workspaceForAnnotationPath(path),
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
({ workspaceId }) =>
|
|
121
|
+
invokeLectorVehicleOperation<OperationOutputs["workspace.restoreAnnotation"]>(
|
|
122
|
+
"workspace.restoreAnnotation",
|
|
123
|
+
{ workspaceId, id },
|
|
124
|
+
ANNOTATION_WRITE_PERMISSIONS,
|
|
125
|
+
call,
|
|
126
|
+
),
|
|
107
127
|
);
|
|
108
128
|
},
|
|
109
|
-
async contain(path, parentId, childId) {
|
|
129
|
+
async contain(path, parentId, childId, call) {
|
|
110
130
|
return withWorkspace(
|
|
111
131
|
() => workspaceForAnnotationPath(path),
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
132
|
+
({ workspaceId }) =>
|
|
133
|
+
invokeLectorVehicleOperation<OperationOutputs["workspace.containAnnotation"]>(
|
|
134
|
+
"workspace.containAnnotation",
|
|
135
|
+
{ workspaceId, parentId, childId },
|
|
136
|
+
ANNOTATION_WRITE_PERMISSIONS,
|
|
137
|
+
call,
|
|
138
|
+
),
|
|
116
139
|
);
|
|
117
140
|
},
|
|
118
|
-
async uncontain(path, parentId, childId) {
|
|
141
|
+
async uncontain(path, parentId, childId, call) {
|
|
119
142
|
return withWorkspace(
|
|
120
143
|
() => workspaceForAnnotationPath(path),
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
144
|
+
({ workspaceId }) =>
|
|
145
|
+
invokeLectorVehicleOperation<OperationOutputs["workspace.uncontainAnnotation"]>(
|
|
146
|
+
"workspace.uncontainAnnotation",
|
|
147
|
+
{ workspaceId, parentId, childId },
|
|
148
|
+
ANNOTATION_WRITE_PERMISSIONS,
|
|
149
|
+
call,
|
|
150
|
+
),
|
|
125
151
|
);
|
|
126
152
|
},
|
|
127
|
-
async tree(path, rootId, maxDepth) {
|
|
153
|
+
async tree(path, rootId, maxDepth, call) {
|
|
128
154
|
return withWorkspace(
|
|
129
155
|
() => workspaceForAnnotationPath(path),
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
156
|
+
({ workspaceId }) =>
|
|
157
|
+
invokeLectorVehicleOperation<OperationOutputs["workspace.annotationTree"]>(
|
|
158
|
+
"workspace.annotationTree",
|
|
159
|
+
{ workspaceId, rootId, maxDepth },
|
|
160
|
+
ANNOTATION_READ_PERMISSIONS,
|
|
161
|
+
call,
|
|
162
|
+
),
|
|
134
163
|
);
|
|
135
164
|
},
|
|
136
165
|
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A real VehicleClient bridge to the Lector daemon's /vehicle/* HTTP surface (see Lector's own
|
|
3
|
+
* daemon.ts, which mounts @danypops/vehicle-server/http's createVehicleHttpApp additively
|
|
4
|
+
* alongside the legacy /api/v1/ops endpoint). Used by whichever pi-lector tool actions have
|
|
5
|
+
* migrated onto Vehicle's operation-descriptor style so far (git status/log/diff today -- see
|
|
6
|
+
* git/operations.ts) -- distinct from lector-client.ts's own LectorClient/OperationName
|
|
7
|
+
* dispatch, which every other tool action still uses and will keep using until its own backing
|
|
8
|
+
* operation migrates too.
|
|
9
|
+
*
|
|
10
|
+
* Same pattern already proven for web-spider's web_category (pi-web-spider's
|
|
11
|
+
* invokeWebSpiderVehicleOperation) -- see also @danypops/vehicle-client-pi's own
|
|
12
|
+
* invokeVehicleOperation() doc comment: a consumer whose tool deliberately consolidates several
|
|
13
|
+
* operations behind one action parameter (Anthropic's own tool-design guidance) gets the same
|
|
14
|
+
* cross-cutting policy layer (activity broadcasting, the local /safety ask gate, the server
|
|
15
|
+
* approval-required retry dance, idempotency-key/correlationId derivation) a
|
|
16
|
+
* registerVehicleTools()-registered tool gets automatically, without regressing its own
|
|
17
|
+
* consolidated shape into one Pi tool per operation.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately does NOT auto-spawn the daemon, matching lector-client.ts's own stated
|
|
20
|
+
* convention: a clear "start it with `lector serve`" error beats guessing at a lifecycle the
|
|
21
|
+
* user didn't ask for.
|
|
22
|
+
*/
|
|
23
|
+
import { resolveLectorDaemonConnection } from "@danypops/lector";
|
|
24
|
+
import { createReconnectingVehicleClient, daemonInstanceIdentity } from "@danypops/vehicle-client/daemon-client";
|
|
25
|
+
import { RemoteVehicleClient } from "@danypops/vehicle-client/http";
|
|
26
|
+
import { invokeVehicleOperation } from "@danypops/vehicle-client-pi";
|
|
27
|
+
import type { VehicleClient } from "@danypops/vehicle-core";
|
|
28
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
29
|
+
|
|
30
|
+
type VehicleClientConnector = () => Promise<VehicleClient>;
|
|
31
|
+
|
|
32
|
+
function connectLectorVehicleClient(): Promise<VehicleClient> {
|
|
33
|
+
const { host, port, token } = resolveLectorDaemonConnection();
|
|
34
|
+
return Promise.resolve(new RemoteVehicleClient({ baseUrl: `http://${host}:${port}`, token }));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveLectorVehicleIdentity() {
|
|
38
|
+
try {
|
|
39
|
+
const { host, port } = resolveLectorDaemonConnection();
|
|
40
|
+
return daemonInstanceIdentity(`http://${host}:${port}`);
|
|
41
|
+
} catch {
|
|
42
|
+
return daemonInstanceIdentity("unresolved");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let connector: VehicleClientConnector = connectLectorVehicleClient;
|
|
47
|
+
let vehicleClient: VehicleClient = createReconnectingVehicleClient(() => connector(), {
|
|
48
|
+
resolveIdentity: resolveLectorVehicleIdentity,
|
|
49
|
+
// connectRetry:true (vehicle-client's own bounded background retry budget) covers a daemon
|
|
50
|
+
// that crashed and is mid systemd-restart -- without it, the very first call during that
|
|
51
|
+
// window fails immediately instead of waiting the restart out, same policy as
|
|
52
|
+
// lector-client.ts's own createRetryingLectorClient.
|
|
53
|
+
connectRetry: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export function setLectorVehicleClientConnectorForTests(value: VehicleClientConnector): void {
|
|
57
|
+
connector = value;
|
|
58
|
+
vehicleClient = createReconnectingVehicleClient(() => connector());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resetLectorVehicleClientForTests(): void {
|
|
62
|
+
connector = connectLectorVehicleClient;
|
|
63
|
+
vehicleClient = createReconnectingVehicleClient(() => connector(), { resolveIdentity: resolveLectorVehicleIdentity, connectRetry: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Everything about a real Pi tool call needed to invoke a Vehicle operation through it, except
|
|
68
|
+
* which permissions this call declares -- that's operation-specific (matches whichever
|
|
69
|
+
* *_PERMISSIONS constant the backing operation's own registration declared server-side), so it's
|
|
70
|
+
* a separate invokeLectorVehicleOperation() argument, not part of this reusable per-tool-call
|
|
71
|
+
* envelope.
|
|
72
|
+
*
|
|
73
|
+
* Deliberately has no `onUpdate` field: invokeVehicleOperation()'s own progress callback is typed
|
|
74
|
+
* against PiVehicleToolDetails (vehicle-client-pi's generic {vehicle, progress}/{vehicle,
|
|
75
|
+
* presentation} shape), never a specific tool's own XToolDetails -- none of git/
|
|
76
|
+
* symbol_annotations/repo_cache/external_search/mutation_history's operations are long-running
|
|
77
|
+
* enough to emit real mid-flight progress in practice, and forwarding a caller's own
|
|
78
|
+
* differently-shaped onUpdate here would need an unsafe cast to paper over a real type mismatch,
|
|
79
|
+
* not just a lint complaint.
|
|
80
|
+
*/
|
|
81
|
+
export interface LectorVehicleCall {
|
|
82
|
+
readonly toolName: string;
|
|
83
|
+
readonly toolCallId: string;
|
|
84
|
+
readonly signal?: AbortSignal;
|
|
85
|
+
readonly context: ExtensionContext;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Dispatches one already-VehicleRegistry-backed Lector operation through vehicle-client-pi's
|
|
90
|
+
* cross-cutting policy layer instead of a bare lectorClient().call(), which would forfeit all of
|
|
91
|
+
* it. Fetches the manifest on every call rather than caching it: these are low-frequency,
|
|
92
|
+
* user-driven tool actions (not a hot loop), and a fresh manifest fetch is one cheap extra round
|
|
93
|
+
* trip that also self-heals if the daemon's own operation set ever changes between calls.
|
|
94
|
+
*
|
|
95
|
+
* Generic over T (the caller's own known output shape for this specific operation) so every call
|
|
96
|
+
* site gets a properly typed result without its own unsafe cast -- the one unavoidable narrowing
|
|
97
|
+
* (result.details.output is typed unknown; see PiVehicleToolDetails) happens exactly once, here,
|
|
98
|
+
* trusted because the caller already knows which operation it invoked and what shape that
|
|
99
|
+
* operation's own registered output schema produces -- same trust boundary Lector's own
|
|
100
|
+
* dispatch-through-registry.ts documents for its identical generic-unwrap seam.
|
|
101
|
+
*/
|
|
102
|
+
export async function invokeLectorVehicleOperation<T>(
|
|
103
|
+
operationName: string,
|
|
104
|
+
input: Record<string, unknown>,
|
|
105
|
+
permissions: readonly string[],
|
|
106
|
+
call: LectorVehicleCall,
|
|
107
|
+
): Promise<T> {
|
|
108
|
+
const manifest = await vehicleClient.manifest();
|
|
109
|
+
const descriptor = manifest.operations.find((op) => op.name === operationName);
|
|
110
|
+
if (!descriptor) throw new Error(`Lector Vehicle manifest has no operation named '${operationName}'`);
|
|
111
|
+
const result = await invokeVehicleOperation({
|
|
112
|
+
client: vehicleClient,
|
|
113
|
+
manifest,
|
|
114
|
+
descriptor,
|
|
115
|
+
toolName: call.toolName,
|
|
116
|
+
toolCallId: call.toolCallId,
|
|
117
|
+
input,
|
|
118
|
+
context: call.context,
|
|
119
|
+
signal: call.signal,
|
|
120
|
+
options: { permissions },
|
|
121
|
+
});
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- see this function's own doc comment.
|
|
123
|
+
return result.details.output as T;
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-lector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.11",
|
|
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,15 +18,18 @@
|
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"@earendil-works/pi-coding-agent": "*",
|
|
20
20
|
"@earendil-works/pi-tui": "*",
|
|
21
|
-
"typebox": "*"
|
|
21
|
+
"typebox": "*",
|
|
22
|
+
"@danypops/vehicle-client-pi": "^0.42.1"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"@danypops/vehicle-client": "^0.
|
|
25
|
-
"@danypops/
|
|
25
|
+
"@danypops/vehicle-client": "^0.10.1",
|
|
26
|
+
"@danypops/vehicle-core": "^0.17.1",
|
|
27
|
+
"@danypops/lector": "^0.19.3",
|
|
26
28
|
"malevich-tui-components": "^0.25.0",
|
|
27
29
|
"picomatch": "^4.0.5"
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|
|
32
|
+
"@danypops/vehicle-client-pi": "^0.42.1",
|
|
30
33
|
"@danypops/pi-extension-harness": "^0.2.0",
|
|
31
34
|
"@danypops/pi-tui-harness": "^0.0.1",
|
|
32
35
|
"@earendil-works/pi-ai": "^0.81.1",
|