@danypops/pi-lector 0.6.0 → 0.7.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.
@@ -27,6 +27,7 @@ import type {
27
27
  WorkspaceMapResult,
28
28
  WorkspaceQueryOutcome,
29
29
  } from "@danypops/lector";
30
+ import { DEFAULT_EXTERNAL_SEARCH_MAX_RESULTS } from "@danypops/lector";
30
31
  import {
31
32
  type AgentToolResult,
32
33
  createEditToolDefinition,
@@ -83,7 +84,7 @@ import { setNewWorkspaceObserver } from "./lector-client.ts";
83
84
  import { createLectorLineEditOperations } from "./line-edit-operations.ts";
84
85
  import { formatLineEditCall, formatLineEditResult } from "./line-edit-rendering.ts";
85
86
  import { createMutationHistoryOperations } from "./mutation-history-operations.ts";
86
- import { nearestGitRoot } from "./nearest-workspace-root.ts";
87
+ import { isFilesystemRoot, nearestGitRoot } from "./nearest-workspace-root.ts";
87
88
  import { createLectorPackageSourceOperations } from "./package-source-operations.ts";
88
89
  import { formatPackageSourceCall, formatPackageSourceResult } from "./package-source-rendering.ts";
89
90
  import { createLectorReadOperations } from "./read-operations.ts";
@@ -176,8 +177,16 @@ export default function (pi: ExtensionAPI) {
176
177
  uiContext.ui.setStatus("lector-cache", uiContext.ui.theme.fg(worst, `Lector: ${summary}`));
177
178
  }
178
179
 
179
- /** 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. */
180
+ /**
181
+ * Starts (or restarts, on a stale generation) monitoring one workspace root's cache
182
+ * lifecycle -- shared by session_start's own cwd root and every later root a tool call
183
+ * first touches. Refuses a bare filesystem root outright: workspaceForPath's own
184
+ * intentional fallback for a raw read/write of a file outside any git repo can register
185
+ * exactly this as a "new workspace", and auto-populating it would attempt a full
186
+ * filesystem-wide symbol-graph scan -- confirmed live as a real, previously-shipped bug.
187
+ */
180
188
  function startMonitoringRoot(root: string, ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1]): void {
189
+ if (isFilesystemRoot(root)) return;
181
190
  if (monitoringRoots.has(root)) return;
182
191
  monitoringRoots.add(root);
183
192
  const thisGeneration = sessionGeneration;
@@ -1550,7 +1559,7 @@ export default function (pi: ExtensionAPI) {
1550
1559
  maxResults: Type.Optional(Type.Number({ description: "Maximum candidates to return (default 20)" })),
1551
1560
  }),
1552
1561
  async execute(_toolCallId, params): Promise<AgentToolResult<ExternalSearchToolDetails>> {
1553
- const maxResults = params.maxResults ?? 20;
1562
+ const maxResults = params.maxResults ?? DEFAULT_EXTERNAL_SEARCH_MAX_RESULTS;
1554
1563
  if (params.action === "github_repos") {
1555
1564
  const result = await externalSearchOperations.githubRepos(params.query, maxResults);
1556
1565
  return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "github_repos", result } };
@@ -2,39 +2,60 @@ import { existsSync } from "node:fs";
2
2
  import { dirname, join, parse } from "node:path";
3
3
 
4
4
  /**
5
- * The nearest enclosing git repository root starting from (and including)
6
- * a given directory, or undefined if none is found (e.g. /tmp scratch
7
- * files, dotfiles outside any repo). Callers choose their own fallback --
8
- * see lector-client.ts's workspaceForPath (falls back to the filesystem
9
- * root: any absolute path is fair game for read/write/edit, exactly as
10
- * Pi's built-in tools already allow) vs. workspaceForDirectory (falls back
11
- * to the directory itself: widening a symbol-search scope all the way to
12
- * the entire filesystem when a project isn't a git repo would be absurd).
13
- *
14
- * This -- not a Pi session's original cwd -- is Lector's real workspace
15
- * granularity. A session routinely touches many unrelated repos, sibling
16
- * projects, and scratch paths in one run; pi's built-in read/write/edit
17
- * tools have never restricted which absolute path can be touched, and
18
- * Lector must not either. (Real, shipped bug this fixes: read/write/edit
19
- * hard-locked to whatever directory the session happened to start in,
20
- * refusing every legitimate path outside it with a "Lector-registered
21
- * workspace root" error -- discovered live, in a separate session, working
22
- * against a completely different, unrelated repository.)
5
+ * The bare filesystem root is never a legitimate discovered project root, even if it happens
6
+ * to contain a marker file (a stray `git init /`, a leftover `package.json`) -- matches the
7
+ * same convention already established elsewhere in this house (oculus/survey/rust_scanner.go's
8
+ * findCrateRoot, oculus/locator/match.go's effectiveParent: reaching "/" during a walk-up means
9
+ * "not found", never "found here"). Confirmed live: without this, a Lector daemon registered
10
+ * "/" as a workspace and a background job attempted to symbol-graph the entire filesystem.
11
+ * `exists` is injectable so a test can simulate "a marker exists at the filesystem root"
12
+ * without ever touching the real one.
23
13
  */
24
- function walkUpForMarkers(startDirectory: string, markers: readonly string[]): string | undefined {
14
+ function walkUpForMarkers(startDirectory: string, markers: readonly string[], exists: (path: string) => boolean = existsSync): string | undefined {
25
15
  let dir = startDirectory;
26
16
  const fsRoot = parse(dir).root;
27
17
  while (dir !== fsRoot) {
28
- if (markers.some((marker) => existsSync(join(dir, marker)))) return dir;
18
+ if (markers.some((marker) => exists(join(dir, marker)))) return dir;
29
19
  const parent = dirname(dir);
30
20
  if (parent === dir) break; // defensive: dirname must be strictly ascending
31
21
  dir = parent;
32
22
  }
33
- return markers.some((marker) => existsSync(join(fsRoot, marker))) ? fsRoot : undefined;
23
+ return undefined;
24
+ }
25
+
26
+ /**
27
+ * The nearest enclosing git repository root starting from (and including) a given directory,
28
+ * or undefined if none is found (e.g. /tmp scratch files, dotfiles outside any repo, or the
29
+ * walk reaching the filesystem root without a match). Callers choose their own fallback -- see
30
+ * lector-client.ts's workspaceForPath (falls back to the filesystem root: any absolute path is
31
+ * fair game for read/write/edit, exactly as Pi's built-in tools already allow) vs.
32
+ * workspaceForDirectory (falls back to the directory itself: widening a symbol-search scope all
33
+ * the way to the entire filesystem when a project isn't a git repo would be absurd).
34
+ *
35
+ * This -- not a Pi session's original cwd -- is Lector's real workspace granularity. A session
36
+ * routinely touches many unrelated repos, sibling projects, and scratch paths in one run; Pi's
37
+ * built-in read/write/edit tools have never restricted which absolute path can be touched, and
38
+ * Lector must not either. (Real, shipped bug this fixes: read/write/edit hard-locked to
39
+ * whatever directory the session happened to start in, refusing every legitimate path outside
40
+ * it with a "Lector-registered workspace root" error -- discovered live, in a separate session,
41
+ * working against a completely different, unrelated repository.)
42
+ *
43
+ * `exists` is injectable for tests -- see walkUpForMarkers.
44
+ */
45
+ /**
46
+ * True for the bare filesystem root itself ("/" on Linux/macOS, "C:\\" on Windows) -- the one
47
+ * path a caller must never treat as a real project to auto-index. workspaceForPath's own
48
+ * intentional fallback for a raw read/write of a file outside any git repo can still produce
49
+ * this value; callers that trigger background work (auto-population, cache monitoring) off a
50
+ * newly-registered workspace must check this explicitly rather than assuming
51
+ * nearestGitRoot/nearestProjectRoot are the only paths that can hand them a workspace root.
52
+ */
53
+ export function isFilesystemRoot(path: string): boolean {
54
+ return parse(path).root === path;
34
55
  }
35
56
 
36
- export function nearestGitRoot(startDirectory: string): string | undefined {
37
- return walkUpForMarkers(startDirectory, [".git"]);
57
+ export function nearestGitRoot(startDirectory: string, exists: (path: string) => boolean = existsSync): string | undefined {
58
+ return walkUpForMarkers(startDirectory, [".git"], exists);
38
59
  }
39
60
 
40
61
  /**
@@ -45,6 +66,6 @@ export function nearestGitRoot(startDirectory: string): string | undefined {
45
66
  * misattributes its whole project to the repo root, handing the language server the wrong
46
67
  * rootUri (and, for TypeScript, the wrong tsconfig.json) even though a closer one exists.
47
68
  */
48
- export function nearestProjectRoot(startDirectory: string, rootMarkers: readonly string[]): string | undefined {
49
- return walkUpForMarkers(startDirectory, [...rootMarkers, ".git"]);
69
+ export function nearestProjectRoot(startDirectory: string, rootMarkers: readonly string[], exists: (path: string) => boolean = existsSync): string | undefined {
70
+ return walkUpForMarkers(startDirectory, [...rootMarkers, ".git"], exists);
50
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-lector",
3
- "version": "0.6.0",
3
+ "version": "0.7.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.7.0"
22
+ "@danypops/lector": "^0.8.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@earendil-works/pi-ai": "^0.81.1",