@danypops/pi-lector 0.12.6 → 0.12.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { RetryingClientDiagnosticEvent } from "@danypops/vehicle-client/daemon-client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Formats one createRetryingClient diagnostic event as a single, compact line -- the same real
|
|
5
|
+
* RCA gap @danypops/vehicle-client's own onEvent hook was added to close: a scrubbed "connector
|
|
6
|
+
* unavailable" error at the tool-call boundary otherwise gives no way to tell, after the fact,
|
|
7
|
+
* whether it was a genuine fresh connect() failure, a circuit-breaker short-circuit (no connect
|
|
8
|
+
* attempted at all), or an in-flight operation's stale-connection retry. Never includes a stack
|
|
9
|
+
* trace -- name and message only, matching this house's other client-diagnostic channels
|
|
10
|
+
* (@danypops/vehicle-client-pi's own client-diagnostics.ts).
|
|
11
|
+
*/
|
|
12
|
+
/** Never risks Object's default `[object Object]` stringification for a non-Error, non-string throw. */
|
|
13
|
+
function describeError(error: unknown): { name: string; message: string } {
|
|
14
|
+
if (error instanceof Error) return { name: error.name, message: error.message };
|
|
15
|
+
if (typeof error === "string") return { name: "string", message: error };
|
|
16
|
+
if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") return { name: typeof error, message: String(error) };
|
|
17
|
+
return { name: typeof error, message: "(unprintable value)" };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatClientDiagnosticEvent(event: RetryingClientDiagnosticEvent): string {
|
|
21
|
+
const parts = [`[lector-client] ${event.type}`];
|
|
22
|
+
if (event.attempt !== undefined) parts.push(`attempt=${event.attempt}`);
|
|
23
|
+
if (event.consecutiveFailures !== undefined) parts.push(`consecutiveFailures=${event.consecutiveFailures}`);
|
|
24
|
+
if (event.operationId !== undefined) parts.push(`operationId=${event.operationId}`);
|
|
25
|
+
if (event.error !== undefined) {
|
|
26
|
+
const { name, message } = describeError(event.error);
|
|
27
|
+
parts.push(`error=${name}: ${message}`);
|
|
28
|
+
}
|
|
29
|
+
return parts.join(" ");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Logs a createRetryingClient diagnostic event to stderr, only when LECTOR_CLIENT_DIAG is set --
|
|
34
|
+
* zero cost and zero output for every session that never opts in, matching the env-gated
|
|
35
|
+
* convention @danypops/vehicle-client-pi's own VEHICLE_CLIENT_DIAG already established in this
|
|
36
|
+
* house. Never throws: onEvent's own contract requires it stay side-effect-light, and a broken
|
|
37
|
+
* local console (or a formatting bug) must not take down a real client call.
|
|
38
|
+
*/
|
|
39
|
+
export function logClientDiagnosticEvent(event: RetryingClientDiagnosticEvent): void {
|
|
40
|
+
if (!process.env.LECTOR_CLIENT_DIAG) return;
|
|
41
|
+
try {
|
|
42
|
+
console.error(formatClientDiagnosticEvent(event));
|
|
43
|
+
} catch {
|
|
44
|
+
// best-effort only -- see doc comment above.
|
|
45
|
+
}
|
|
46
|
+
}
|
package/extension/src/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
1
3
|
import { resolve } from "node:path";
|
|
2
4
|
import type {
|
|
3
5
|
CachedRepositoryPage,
|
|
@@ -44,7 +46,7 @@ import { Type } from "typebox";
|
|
|
44
46
|
/** Real ANSI-aware measurement for Table -- Malevich's own default is ASCII-only, unsafe against theme-styled cell/header text. */
|
|
45
47
|
const tableMeasure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
46
48
|
|
|
47
|
-
import { isFilesystemRoot } from "@danypops/lector";
|
|
49
|
+
import { classifyAutoPopulationRoot, isFilesystemRoot } from "@danypops/lector";
|
|
48
50
|
import { createLectorApplyPatchOperations } from "./apply-patch/operations.ts";
|
|
49
51
|
import { formatApplyPatchCall, formatApplyPatchResult } from "./apply-patch/rendering.ts";
|
|
50
52
|
import { createLectorCodeIntelligenceOperations } from "./code-intelligence/operations.ts";
|
|
@@ -214,10 +216,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
214
216
|
* intentional fallback for a raw read/write of a file outside any git repo can register
|
|
215
217
|
* exactly this as a "new workspace", and auto-populating it would attempt a full
|
|
216
218
|
* filesystem-wide symbol-graph scan -- confirmed live as a real, previously-shipped bug.
|
|
219
|
+
*
|
|
220
|
+
* Also short-circuits a broad host directory (home directory, an XDG config/cache/data
|
|
221
|
+
* root, a dotfile directory) the exact same way workspace.populateSymbolGraph's own
|
|
222
|
+
* server-side gate would refuse it -- avoids the round trip (and the queue-behind-real-
|
|
223
|
+
* projects ergonomics this caused live for ~/.pi/agent) entirely, using the identical
|
|
224
|
+
* classification Lector itself uses so the two never drift. A readdir failure (permission,
|
|
225
|
+
* race) is treated as "can't tell, don't block" -- the server-side gate is still authoritative.
|
|
217
226
|
*/
|
|
218
227
|
function startMonitoringRoot(root: string, ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1]): void {
|
|
219
228
|
if (isFilesystemRoot(root)) return;
|
|
220
229
|
if (monitoringRoots.has(root)) return;
|
|
230
|
+
try {
|
|
231
|
+
const topLevelEntries = readdirSync(root);
|
|
232
|
+
if (classifyAutoPopulationRoot({ rootPath: root, homeDir: homedir(), topLevelEntries }) === "broad-non-project") return;
|
|
233
|
+
} catch {
|
|
234
|
+
// Can't read it here -- let the server-side gate be the authoritative answer.
|
|
235
|
+
}
|
|
221
236
|
monitoringRoots.add(root);
|
|
222
237
|
const thisGeneration = sessionGeneration;
|
|
223
238
|
void monitorWorkspaceCache(cacheOperations, {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type WorkspaceResolutionRequest,
|
|
12
12
|
} from "@danypops/lector";
|
|
13
13
|
import { createRetryingClient, isLikelyStaleConnectionError, type RetryingClient } from "@danypops/vehicle-client/daemon-client";
|
|
14
|
+
import { logClientDiagnosticEvent } from "./client-diagnostics.ts";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Lazily connects to a running Lector daemon and caches, per resolution request, the
|
|
@@ -44,6 +45,9 @@ let connector: ClientConnector = () => connectLectorClient();
|
|
|
44
45
|
const retryingClient: RetryingClient<LectorClient> = createRetryingClient(() => connector(), {
|
|
45
46
|
label: "Lector",
|
|
46
47
|
isStaleConnectionError: (error) => error instanceof LectorDaemonUnavailable || isLikelyStaleConnectionError(error),
|
|
48
|
+
// Purely observational, opt-in via LECTOR_CLIENT_DIAG -- see client-diagnostics.ts's own doc
|
|
49
|
+
// comment for the exact RCA gap this closes.
|
|
50
|
+
onEvent: logClientDiagnosticEvent,
|
|
47
51
|
});
|
|
48
52
|
|
|
49
53
|
/**
|
|
@@ -220,6 +224,38 @@ export function workspaceForPathOrDirectory(path: string): Promise<ResolvedWorks
|
|
|
220
224
|
return resolveWorkspace({ strategy: "path-or-directory", path });
|
|
221
225
|
}
|
|
222
226
|
|
|
227
|
+
/** Raised by workspaceForAnnotationPath for a path that does not exist on disk at all -- a distinct, explicit failure the caller must handle, never silently guessed as "must be a file, take its dirname()." */
|
|
228
|
+
export class AnnotationPathDoesNotExist extends Error {
|
|
229
|
+
constructor(readonly path: string) {
|
|
230
|
+
super(`"${path}" does not exist -- a symbol-annotation scope must be a real project directory or an existing source file`);
|
|
231
|
+
this.name = "AnnotationPathDoesNotExist";
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* For symbol-annotation's own `path` parameter, which genuinely means "which workspace does this
|
|
237
|
+
* belong to" and can honestly be either an existing project directory or one specific source
|
|
238
|
+
* file -- unlike workspaceForCodeIntelligencePath (files only, dirname() unconditional), a real
|
|
239
|
+
* project directory resolves via its own language markers, not its parent's. A genuinely
|
|
240
|
+
* nonexistent path throws AnnotationPathDoesNotExist rather than being silently treated as a file.
|
|
241
|
+
*/
|
|
242
|
+
export async function workspaceForAnnotationPath(path: string): Promise<ResolvedWorkspace> {
|
|
243
|
+
const request: WorkspaceResolutionRequest = { strategy: "code-intelligence-path-or-directory", path };
|
|
244
|
+
const key = requestCacheKey(request);
|
|
245
|
+
const cached = resolutionCache.get(key);
|
|
246
|
+
if (cached) return cached;
|
|
247
|
+
const client = await lectorClient();
|
|
248
|
+
const outcome = await client.callOnce("workspace.resolvePath", request);
|
|
249
|
+
if (!outcome.found) {
|
|
250
|
+
if (outcome.reason === "nonexistent-path") throw new AnnotationPathDoesNotExist(path);
|
|
251
|
+
throw new Error(`workspace.resolvePath unexpectedly reported not-found for code-intelligence-path-or-directory: ${path}`);
|
|
252
|
+
}
|
|
253
|
+
const resolved: ResolvedWorkspace = { workspaceId: outcome.workspaceId, root: outcome.root };
|
|
254
|
+
resolutionCache.set(key, resolved);
|
|
255
|
+
if (outcome.created) onNewWorkspace?.(outcome.root);
|
|
256
|
+
return resolved;
|
|
257
|
+
}
|
|
258
|
+
|
|
223
259
|
/**
|
|
224
260
|
* The nearest ancestor of an already-resolved project root whose own package.json declares that
|
|
225
261
|
* project as a workspace member via npm/yarn/bun's "workspaces" field -- undefined (no
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OperationInputs, OperationOutputs } from "@danypops/lector";
|
|
2
|
-
import { lectorClient, withWorkspace,
|
|
2
|
+
import { lectorClient, withWorkspace, workspaceForAnnotationPath } from "../lector-client.ts";
|
|
3
3
|
|
|
4
4
|
/** 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
5
|
export interface AnnotationAnchorInput {
|
|
@@ -9,10 +9,12 @@ export interface AnnotationAnchorInput {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Thin wrappers over Lector's annotation operations.
|
|
13
|
-
* via
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Thin wrappers over Lector's annotation operations. Every operation resolves its workspace from
|
|
13
|
+
* its own `path` parameter via workspaceForAnnotationPath -- a real project directory or an
|
|
14
|
+
* existing source file, never dirname()'d unconditionally the way a plain code-intelligence
|
|
15
|
+
* path is. `path` here means "which workspace this call belongs to," not necessarily one of the
|
|
16
|
+
* operation's own anchors (create/refresh's anchors carry their own, separately-validated paths
|
|
17
|
+
* server-side).
|
|
16
18
|
*/
|
|
17
19
|
export interface SymbolAnnotationOperations {
|
|
18
20
|
create(
|
|
@@ -46,7 +48,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
|
|
|
46
48
|
return {
|
|
47
49
|
async create(path, subtype, title, body, anchors) {
|
|
48
50
|
return withWorkspace(
|
|
49
|
-
() =>
|
|
51
|
+
() => workspaceForAnnotationPath(path),
|
|
50
52
|
async ({ workspaceId }) => {
|
|
51
53
|
const client = await lectorClient();
|
|
52
54
|
return client.callOnce("workspace.createAnnotation", { workspaceId, subtype, title, body, anchors });
|
|
@@ -55,7 +57,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
|
|
|
55
57
|
},
|
|
56
58
|
async get(path, id) {
|
|
57
59
|
return withWorkspace(
|
|
58
|
-
() =>
|
|
60
|
+
() => workspaceForAnnotationPath(path),
|
|
59
61
|
async ({ workspaceId }) => {
|
|
60
62
|
const client = await lectorClient();
|
|
61
63
|
return client.call("workspace.getAnnotation", { workspaceId, id });
|
|
@@ -64,7 +66,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
|
|
|
64
66
|
},
|
|
65
67
|
async list(path, options = {}) {
|
|
66
68
|
return withWorkspace(
|
|
67
|
-
() =>
|
|
69
|
+
() => workspaceForAnnotationPath(path),
|
|
68
70
|
async ({ workspaceId }) => {
|
|
69
71
|
const client = await lectorClient();
|
|
70
72
|
return client.call("workspace.listAnnotations", {
|
|
@@ -79,7 +81,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
|
|
|
79
81
|
},
|
|
80
82
|
async refresh(path, id, subtype, title, body, anchors) {
|
|
81
83
|
return withWorkspace(
|
|
82
|
-
() =>
|
|
84
|
+
() => workspaceForAnnotationPath(path),
|
|
83
85
|
async ({ workspaceId }) => {
|
|
84
86
|
const client = await lectorClient();
|
|
85
87
|
return client.callOnce("workspace.refreshAnnotation", { workspaceId, id, subtype, title, body, anchors });
|
|
@@ -88,7 +90,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
|
|
|
88
90
|
},
|
|
89
91
|
async scrub(path, id) {
|
|
90
92
|
return withWorkspace(
|
|
91
|
-
() =>
|
|
93
|
+
() => workspaceForAnnotationPath(path),
|
|
92
94
|
async ({ workspaceId }) => {
|
|
93
95
|
const client = await lectorClient();
|
|
94
96
|
return client.callOnce("workspace.scrubAnnotation", { workspaceId, id });
|
|
@@ -97,7 +99,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
|
|
|
97
99
|
},
|
|
98
100
|
async restore(path, id) {
|
|
99
101
|
return withWorkspace(
|
|
100
|
-
() =>
|
|
102
|
+
() => workspaceForAnnotationPath(path),
|
|
101
103
|
async ({ workspaceId }) => {
|
|
102
104
|
const client = await lectorClient();
|
|
103
105
|
return client.callOnce("workspace.restoreAnnotation", { workspaceId, id });
|
|
@@ -106,7 +108,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
|
|
|
106
108
|
},
|
|
107
109
|
async contain(path, parentId, childId) {
|
|
108
110
|
return withWorkspace(
|
|
109
|
-
() =>
|
|
111
|
+
() => workspaceForAnnotationPath(path),
|
|
110
112
|
async ({ workspaceId }) => {
|
|
111
113
|
const client = await lectorClient();
|
|
112
114
|
return client.callOnce("workspace.containAnnotation", { workspaceId, parentId, childId });
|
|
@@ -115,7 +117,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
|
|
|
115
117
|
},
|
|
116
118
|
async uncontain(path, parentId, childId) {
|
|
117
119
|
return withWorkspace(
|
|
118
|
-
() =>
|
|
120
|
+
() => workspaceForAnnotationPath(path),
|
|
119
121
|
async ({ workspaceId }) => {
|
|
120
122
|
const client = await lectorClient();
|
|
121
123
|
return client.callOnce("workspace.uncontainAnnotation", { workspaceId, parentId, childId });
|
|
@@ -124,7 +126,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
|
|
|
124
126
|
},
|
|
125
127
|
async tree(path, rootId, maxDepth) {
|
|
126
128
|
return withWorkspace(
|
|
127
|
-
() =>
|
|
129
|
+
() => workspaceForAnnotationPath(path),
|
|
128
130
|
async ({ workspaceId }) => {
|
|
129
131
|
const client = await lectorClient();
|
|
130
132
|
return client.call("workspace.annotationTree", { workspaceId, rootId, maxDepth });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-lector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.7",
|
|
4
4
|
"description": "Pi host adapter for Lector: overrides read/write/edit with a daemon-backed, hash-guarded filesystem",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"typebox": "*"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@danypops/vehicle-client": "^0.2
|
|
21
|
+
"@danypops/vehicle-client": "^0.8.2",
|
|
22
22
|
"@danypops/lector": "^0.18.0",
|
|
23
|
-
"malevich-tui-components": "^0.
|
|
23
|
+
"malevich-tui-components": "^0.25.0",
|
|
24
24
|
"picomatch": "^4.0.5"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|