@danypops/pi-lector 0.9.4 → 0.9.5

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.
@@ -9,6 +9,7 @@ import type {
9
9
  GithubRepoSearchResult,
10
10
  Hover,
11
11
  IntelligenceProvenance,
12
+ JobSnapshot,
12
13
  LineEdit,
13
14
  LineEditOutcome,
14
15
  MutationHistoryEntry,
@@ -16,12 +17,14 @@ import type {
16
17
  OperationOutputs,
17
18
  PackageEcosystem,
18
19
  PackageSourceOperationResult,
20
+ PopulateSymbolGraphResult,
19
21
  RepoFetchResult,
20
22
  SourcegraphCodeCandidate,
21
23
  SymbolAnnotation,
22
24
  SymbolNode,
23
25
  SymbolSearchResult,
24
26
  TextSearchResult,
27
+ WorkspaceCacheStatus,
25
28
  WorkspaceLocation,
26
29
  WorkspaceMapResult,
27
30
  WorkspaceQueryOutcome,
@@ -125,6 +128,7 @@ import {
125
128
  describeCacheState,
126
129
  monitorWorkspaceCache,
127
130
  } from "./workspace-cache-operations.ts";
131
+ import { formatJobSnapshotResult, formatWorkspaceCacheCall, formatWorkspaceCacheStatusResult } from "./workspace-cache-rendering.ts";
128
132
  import { createLectorWriteOperations } from "./write-operations.ts";
129
133
 
130
134
  function describeIntelligenceSource(provenance: IntelligenceProvenance): string {
@@ -650,7 +654,7 @@ export default function (pi: ExtensionAPI) {
650
654
  "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.",
651
655
  promptSnippet: "Move a file and update every import that references it",
652
656
  promptGuidelines: [
653
- "The workspace's symbol graph auto-populates in the background (default bounds: 500 files, 100 symbols/file) the first time this workspace is touched. If this refuses because the graph isn't populated at the requested maxFiles/maxSymbolsPerFile, run `lector workspace populate-symbol-graph <path> --max-files <n> --max-symbols-per-file <n>` via bash for a larger scan, then retry.",
657
+ "The workspace's symbol graph auto-populates in the background (default bounds: 500 files, 100 symbols/file) the first time this workspace is touched. If this refuses because the graph isn't populated at the requested maxFiles/maxSymbolsPerFile, use workspace_cache(action=populate, maxFiles, maxSymbolsPerFile) for a larger scan, then retry -- no need to shell out to the CLI.",
654
658
  "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.",
655
659
  ],
656
660
  parameters: Type.Object({
@@ -779,10 +783,11 @@ export default function (pi: ExtensionAPI) {
779
783
  name: "symbol_annotations",
780
784
  label: "Symbol Annotations",
781
785
  description:
782
- 'Agent-authored narrative content anchored to one or more symbols in the workspace\'s persisted graph -- e.g. a "user story dataflow" note spanning every symbol touched end-to-end. Every anchor must resolve to a real, currently-known symbol (the workspace\'s symbol graph auto-populates in the background on first touch). get/list/tree live-check staleness against the current graph/workspace on every call and persist a correction before returning, so a returned status never disagrees with reality -- a stale annotation must be refreshed (re-authored and re-anchored) or scrubbed (soft-deleted, restorable) by an explicit decision; Lector never rewrites the narrative itself. contain/uncontain build a reusable, nestable structure on top of plain annotations: a container (e.g. a "data flow") can contain other annotations -- including per-symbol notes shared by more than one container (DRY reuse) or another container one level deeper (nested data flows) -- without duplicating their content. tree reads a whole bounded subtree in one call. Actions: create, get, list, refresh, scrub, restore, contain, uncontain, tree.',
786
+ 'Agent-authored narrative content anchored to one or more symbols in the workspace\'s persisted graph -- e.g. a "user story dataflow" note spanning every symbol touched end-to-end. Every anchor must resolve to a real, currently-known symbol (the workspace\'s symbol graph auto-populates in the background on first touch, bounded to the first 500 files/100 symbols each -- use workspace_cache(action=populate) with larger bounds for a symbol outside that). get/list/tree live-check staleness against the current graph/workspace on every call and persist a correction before returning, so a returned status never disagrees with reality -- a stale annotation must be refreshed (re-authored and re-anchored) or scrubbed (soft-deleted, restorable) by an explicit decision; Lector never rewrites the narrative itself. contain/uncontain build a reusable, nestable structure on top of plain annotations: a container (e.g. a "data flow") can contain other annotations -- including per-symbol notes shared by more than one container (DRY reuse) or another container one level deeper (nested data flows) -- without duplicating their content. tree reads a whole bounded subtree in one call. Actions: create, get, list, refresh, scrub, restore, contain, uncontain, tree.',
783
787
  promptSnippet: "Attach, read, or invalidate narrative annotations on the symbol graph",
784
788
  promptGuidelines: [
785
789
  "Resolve real anchor positions first (find_symbols/document_symbols/go_to_definition) -- an anchor position must match the workspace's own symbol graph's recorded position for that symbol, not just any occurrence of its name.",
790
+ "UnknownAnnotationAnchor on a real, existing symbol usually means the graph's default 500-file auto-scan never reached that file -- check workspace_cache(action=status) and populate with larger bounds before assuming the position itself is wrong.",
786
791
  "A stale annotation's body may no longer describe the code accurately -- read it, decide whether to refresh (re-author) or scrub (remove), never trust it as-is.",
787
792
  "Prefer reusing an existing per-symbol annotation as a shared child of several containers over re-authoring the same explanation in each -- that reuse is the reason contain/uncontain exist.",
788
793
  "contain/uncontain are idempotent (containing an already-contained child, or uncontaining an already-absent relationship, is a no-op, not an error) and reject a cycle up front rather than accepting one.",
@@ -944,10 +949,11 @@ export default function (pi: ExtensionAPI) {
944
949
  name: "reachable_from",
945
950
  label: "Reachable From",
946
951
  description:
947
- "Every symbol reachable from an exact file position by following the workspace's persisted call graph up to maxDepth hops -- transitive callers/reachability that would otherwise require chaining many find_references/call_hierarchy calls by hand. The workspace's symbol graph auto-populates in the background the first time this workspace is touched; if it's still building, this returns an empty result rather than an error -- wait a moment and retry.",
952
+ "Every symbol reachable from an exact file position by following the workspace's persisted call graph up to maxDepth hops -- transitive callers/reachability that would otherwise require chaining many find_references/call_hierarchy calls by hand. The workspace's symbol graph auto-populates in the background the first time this workspace is touched, bounded to the first 500 files/100 symbols each; if the position you need falls outside that, this returns an empty result rather than an error -- use workspace_cache(action=populate) with larger bounds, not just a retry.",
948
953
  promptSnippet: "Find symbols reachable from a position, up to N hops, via the persisted graph",
949
954
  promptGuidelines: [
950
955
  "Use reachable_from for multi-hop questions (does A eventually call C through B); use call_hierarchy (direction=incoming/outgoing) for a single direct hop live against the language server.",
956
+ "An empty result on a real, existing symbol usually means the graph's default 500-file auto-scan never reached that file, not that nothing is reachable -- check with workspace_cache(action=status) and populate with larger bounds if so.",
951
957
  ],
952
958
  parameters: Type.Object({
953
959
  ...positionParameters,
@@ -992,11 +998,11 @@ export default function (pi: ExtensionAPI) {
992
998
  name: "workspace_map",
993
999
  label: "Workspace Map",
994
1000
  description:
995
- "A ranked, budget-bounded summary of the workspace's most structurally central symbols (aider-repomap-shaped) -- signature-only, highest-ranked first by PageRank over the populated call/reference graph, not full file dumps. Use when orienting in an unfamiliar or large codebase instead of reading many files one by one. The workspace's symbol graph auto-populates in the background the first time this workspace is touched; if it's still building, this returns empty rather than an error -- wait a moment and retry.",
1001
+ "A ranked, budget-bounded summary of the workspace's most structurally central symbols (aider-repomap-shaped) -- signature-only, highest-ranked first by PageRank over the populated call/reference graph, not full file dumps. Use when orienting in an unfamiliar or large codebase instead of reading many files one by one. The workspace's symbol graph auto-populates in the background the first time this workspace is touched, bounded to the first 500 files/100 symbols each; a workspace bigger than that needs workspace_cache(action=populate) with larger bounds for full coverage.",
996
1002
  promptSnippet: "Get a ranked, signature-only overview of the workspace's most central symbols",
997
1003
  promptGuidelines: [
998
1004
  "Prefer this over reading many files to get oriented in a large or unfamiliar codebase -- it surfaces the most-referenced symbols first, not an arbitrary file order.",
999
- "A budget-truncated result means real symbols were left out, not that the workspace only has this many -- raise maxEntries/maxBytes for more.",
1005
+ "A budget-truncated result means real symbols were left out, not that the workspace only has this many -- raise maxEntries/maxBytes for more. An empty result instead means the graph itself never covered this workspace -- check workspace_cache(action=status).",
1000
1006
  ],
1001
1007
  parameters: Type.Object({
1002
1008
  path: Type.String({ description: "Absolute or cwd-relative path used to resolve which workspace to map" }),
@@ -1039,6 +1045,80 @@ export default function (pi: ExtensionAPI) {
1039
1045
  },
1040
1046
  });
1041
1047
 
1048
+ interface WorkspaceCacheToolDetails {
1049
+ readonly action: "status" | "populate" | "job_status";
1050
+ readonly status?: WorkspaceCacheStatus;
1051
+ readonly job?: JobSnapshot<PopulateSymbolGraphResult>;
1052
+ }
1053
+
1054
+ pi.registerTool({
1055
+ name: "workspace_cache",
1056
+ label: "Workspace Cache",
1057
+ description:
1058
+ "Checks or drives population of the workspace's persisted symbol graph -- the store reachable_from, symbol_annotations (anchor resolution), reference_based_rename, and workspace_map all read from, separate from the live language-server index find_symbols/hover/go_to_definition use. action=status reports not-cached/caching/partial/cached for the given bounds, without starting any work. action=populate explicitly requests a scan (optionally larger than the default 500-file/100-symbol auto-scan every workspace gets on first touch) and waits up to waitMs for it to finish, returning a job snapshot either way. action=job_status polls a job returned by populate that didn't finish within its own wait.",
1059
+ promptSnippet: "Check or force-populate the workspace's persisted symbol graph",
1060
+ promptGuidelines: [
1061
+ "Use action=populate with a larger maxFiles/maxSymbolsPerFile before relying on reachable_from/symbol_annotations/reference_based_rename against a workspace bigger than the default 500-file auto-scan -- their own errors (empty results, UnknownAnnotationAnchor, ReferenceBasedRenameRequiresFreshGraph) usually mean the graph never reached the files you need, not that population is simply still catching up.",
1062
+ "action=populate returns immediately once its own waitMs elapses even if the job is still running -- check the returned job's status and poll with action=job_status (the same jobId) rather than assuming a non-succeeded result means failure.",
1063
+ ],
1064
+ parameters: Type.Object({
1065
+ action: Type.Union([Type.Literal("status"), Type.Literal("populate"), Type.Literal("job_status")]),
1066
+ directory: Type.Optional(
1067
+ Type.String({ description: "Required for action=status/populate -- absolute or cwd-relative path used to resolve the workspace" }),
1068
+ ),
1069
+ maxFiles: Type.Optional(
1070
+ Type.Number({ description: "action=status/populate only -- defaults to 500, the same bound the automatic first-touch scan uses" }),
1071
+ ),
1072
+ maxSymbolsPerFile: Type.Optional(
1073
+ Type.Number({ description: "action=status/populate only -- defaults to 100, the same bound the automatic first-touch scan uses" }),
1074
+ ),
1075
+ waitMs: Type.Optional(
1076
+ Type.Number({
1077
+ description:
1078
+ "action=populate only -- how long to wait for the job to finish before returning its current snapshot; defaults to 3000, capped by the daemon at 30000",
1079
+ }),
1080
+ ),
1081
+ jobId: Type.Optional(Type.String({ description: "Required for action=job_status -- a jobId returned by a prior action=populate call" })),
1082
+ }),
1083
+ async execute(_toolCallId, params): Promise<AgentToolResult<WorkspaceCacheToolDetails>> {
1084
+ if (params.action === "job_status") {
1085
+ if (!params.jobId) throw new Error("workspace_cache action=job_status requires jobId");
1086
+ const job = await codeIntelligenceOperations.jobStatus(params.jobId);
1087
+ return { content: [{ type: "text", text: JSON.stringify(job) }], details: { action: "job_status", job } };
1088
+ }
1089
+ if (!params.directory) throw new Error(`workspace_cache action=${params.action} requires directory`);
1090
+ const directory = resolve(cwd, params.directory);
1091
+ const maxFiles = params.maxFiles ?? 500;
1092
+ const maxSymbolsPerFile = params.maxSymbolsPerFile ?? 100;
1093
+ if (params.action === "status") {
1094
+ const status = await cacheOperations.status(directory, maxFiles, maxSymbolsPerFile);
1095
+ return { content: [{ type: "text", text: JSON.stringify(status) }], details: { action: "status", status } };
1096
+ }
1097
+ const job = await codeIntelligenceOperations.populateSymbolGraph(directory, maxFiles, maxSymbolsPerFile, params.waitMs ?? 3_000);
1098
+ return { content: [{ type: "text", text: JSON.stringify(job) }], details: { action: "populate", job } };
1099
+ },
1100
+ renderCall(args, theme, context) {
1101
+ const action = args.action === "populate" || args.action === "job_status" ? args.action : "status";
1102
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
1103
+ text.setText(formatWorkspaceCacheCall(action, args, theme));
1104
+ return text;
1105
+ },
1106
+ renderResult(result, { isPartial }, theme, context) {
1107
+ if (isPartial) return new Text(theme.fg("warning", "Checking workspace cache..."), 0, 0);
1108
+ if (context.isError) {
1109
+ const errorText = result.content
1110
+ .filter((block) => block.type === "text")
1111
+ .map((block) => block.text)
1112
+ .join("\n");
1113
+ return new Text(theme.fg("error", errorText || "workspace_cache failed"), 0, 0);
1114
+ }
1115
+ const details = result.details as WorkspaceCacheToolDetails | undefined;
1116
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
1117
+ text.setText(details?.action === "status" ? formatWorkspaceCacheStatusResult(details.status, theme) : formatJobSnapshotResult(details?.job, theme));
1118
+ return text;
1119
+ },
1120
+ });
1121
+
1042
1122
  const gitOperations = createLectorGitOperations();
1043
1123
  pi.registerTool({
1044
1124
  name: "git",
@@ -0,0 +1,45 @@
1
+ import type { JobSnapshot, PopulateSymbolGraphResult, WorkspaceCacheStatus } from "@danypops/lector";
2
+ import type { LectorTheme } from "./lector-tui-theme.ts";
3
+
4
+ type WorkspaceCacheAction = "status" | "populate" | "job_status";
5
+
6
+ export function formatWorkspaceCacheCall(
7
+ action: WorkspaceCacheAction,
8
+ args: { directory?: unknown; maxFiles?: unknown; maxSymbolsPerFile?: unknown; jobId?: unknown },
9
+ theme: LectorTheme,
10
+ ): string {
11
+ const label = theme.fg("toolTitle", theme.bold("workspace_cache"));
12
+ if (action === "job_status") {
13
+ const jobId = typeof args.jobId === "string" ? args.jobId : "";
14
+ return `${label} ${theme.fg("accent", "job_status")} ${theme.fg("dim", jobId)}`;
15
+ }
16
+ const directory = typeof args.directory === "string" ? args.directory : "";
17
+ const maxFiles = typeof args.maxFiles === "number" ? String(args.maxFiles) : "default";
18
+ const maxSymbolsPerFile = typeof args.maxSymbolsPerFile === "number" ? String(args.maxSymbolsPerFile) : "default";
19
+ const bounds =
20
+ action === "populate" && (typeof args.maxFiles === "number" || typeof args.maxSymbolsPerFile === "number")
21
+ ? theme.fg("dim", ` (maxFiles=${maxFiles}, maxSymbolsPerFile=${maxSymbolsPerFile})`)
22
+ : "";
23
+ return `${label} ${theme.fg("accent", action)} ${theme.fg("dim", directory)}${bounds}`;
24
+ }
25
+
26
+ function formatResultCounts(result: PopulateSymbolGraphResult): string {
27
+ const failed = result.filesFailed > 0 ? `, ${result.filesFailed} failed` : "";
28
+ return `${result.filesProcessed}/${result.filesAttempted} files${failed}, ${result.symbolsProcessed} symbols, ${result.nodesAdded} nodes, ${result.edgesAdded} edges`;
29
+ }
30
+
31
+ export function formatWorkspaceCacheStatusResult(status: WorkspaceCacheStatus | undefined, theme: LectorTheme): string {
32
+ if (!status) return theme.fg("dim", "No result.");
33
+ if (status.status === "not-cached") return theme.fg("warning", `not cached (${status.reason})`);
34
+ if (status.status === "caching") return theme.fg("accent", `caching (job ${status.jobId})`);
35
+ if (status.status === "partial") return theme.fg("warning", `partial -- ${formatResultCounts(status.generation.result)}`);
36
+ return theme.fg("success", `cached -- ${formatResultCounts(status.generation.result)}`);
37
+ }
38
+
39
+ export function formatJobSnapshotResult(job: JobSnapshot<PopulateSymbolGraphResult> | undefined, theme: LectorTheme): string {
40
+ if (!job) return theme.fg("dim", "No result.");
41
+ if (job.status === "queued") return theme.fg("dim", `queued (job ${job.id})`);
42
+ if (job.status === "running") return theme.fg("accent", `running (job ${job.id})`);
43
+ if (job.status === "failed") return theme.fg("error", `failed (job ${job.id}): ${job.error.code}: ${job.error.message}`);
44
+ return theme.fg("success", `succeeded (job ${job.id}) -- ${formatResultCounts(job.result)}`);
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-lector",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
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",