pi-readseek 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -6,6 +6,9 @@ import { registerSgTool, isSgAvailable } from "./src/sg.js";
6
6
  import { registerWriteTool } from "./src/write.js";
7
7
  import { registerLsTool } from "./src/ls.js";
8
8
  import { registerFindTool } from "./src/find.js";
9
+ import { registerReadseekCommand } from "./src/readseek-command.js";
10
+ import { hasReadseekDir } from "./src/readseek-repo.js";
11
+ import { readseekUpdate } from "./src/readseek-client.js";
9
12
  import { applyContextHygieneStaleContext } from "./src/context-application.js";
10
13
  import {
11
14
  createContextHygieneTracker,
@@ -83,6 +86,13 @@ export default function piReadseekExtension(pi: ExtensionAPI): void {
83
86
  registerWriteTool(pi, { onFileAnchored: noteRead });
84
87
  registerLsTool(pi);
85
88
  registerFindTool(pi);
89
+ registerReadseekCommand(pi);
90
+
91
+ pi.on("session_start", async (_event, ctx) => {
92
+ if (hasReadseekDir(ctx.cwd)) {
93
+ readseekUpdate(ctx.cwd).catch(() => {});
94
+ }
95
+ });
86
96
 
87
97
  pi.on("tool_call", (event: any) => {
88
98
  recordToolCall(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-readseek",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "Pi extension for readseek-backed hash-anchored read/edit/grep, structural code maps, structural search, and file exploration",
5
5
  "type": "module",
6
6
  "exports": {
@@ -39,7 +39,7 @@
39
39
  "node": ">=20.0.0"
40
40
  },
41
41
  "dependencies": {
42
- "@jarkkojs/readseek": "^0.2.9",
42
+ "@jarkkojs/readseek": "^0.3.0",
43
43
  "diff": "^8.0.3",
44
44
  "ignore": "^7.0.5",
45
45
  "picomatch": "^4.0.4",
@@ -135,6 +135,8 @@ function readseekPackageDir(): string {
135
135
  }
136
136
 
137
137
  export function readseekBinaryPath(): string {
138
+ if (process.env.READSEEK_BIN) return process.env.READSEEK_BIN;
139
+
138
140
  const platformPackage = (() => {
139
141
  switch (process.platform) {
140
142
  case "darwin":
@@ -166,8 +168,8 @@ interface RunReadseekOptions {
166
168
  stdin?: string;
167
169
  }
168
170
 
169
- async function runReadseek(args: string[], options: RunReadseekOptions = {}): Promise<unknown> {
170
- const stdout = await new Promise<string>((resolve, reject) => {
171
+ async function runReadseekRaw(args: string[], options: RunReadseekOptions = {}): Promise<string> {
172
+ return new Promise<string>((resolve, reject) => {
171
173
  const stdin = options.stdin;
172
174
  const stdio: StdioOptions = [stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"];
173
175
  const child = spawn(readseekBinaryPath(), args, { stdio, signal: options.signal });
@@ -212,6 +214,10 @@ async function runReadseek(args: string[], options: RunReadseekOptions = {}): Pr
212
214
  else reject(new Error((stderr || `readseek exited with status ${code}`).replace(/^error:\s*/i, "")));
213
215
  });
214
216
  });
217
+ }
218
+
219
+ async function runReadseek(args: string[], options: RunReadseekOptions = {}): Promise<unknown> {
220
+ const stdout = await runReadseekRaw(args, options);
215
221
  return JSON.parse(stdout) as unknown;
216
222
  }
217
223
 
@@ -383,3 +389,14 @@ export async function readseekMapContent(
383
389
  );
384
390
  return fileMapFromReadseekOutput(output, filePath, Buffer.byteLength(content, "utf8"));
385
391
  }
392
+
393
+ export interface ReadseekUpdateStats {
394
+ created: number;
395
+ removed: number;
396
+ unchanged: number;
397
+ }
398
+
399
+ export async function readseekUpdate(cwd: string): Promise<ReadseekUpdateStats> {
400
+ const stdout = await runReadseekRaw(["update", cwd]);
401
+ return JSON.parse(stdout) as ReadseekUpdateStats;
402
+ }
@@ -0,0 +1,170 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, relative } from "node:path";
5
+ import { findReadseekDir, initReadseekDir } from "./readseek-repo.js";
6
+ import { readseekUpdate } from "./readseek-client.js";
7
+
8
+ type ReadseekAction = "init" | "deinit" | "update" | null;
9
+
10
+ function stripFromGitignore(dir: string): void {
11
+ const gitignorePath = join(dir, ".gitignore");
12
+ if (!existsSync(gitignorePath)) return;
13
+ const lines = readFileSync(gitignorePath, "utf-8").split("\n");
14
+ const filtered = lines.filter((line) => line.trim() !== "/.readseek");
15
+ writeFileSync(gitignorePath, filtered.join("\n"), "utf-8");
16
+ }
17
+
18
+ async function deinit(ctx: ExtensionContext): Promise<void> {
19
+ const dir = findReadseekDir(ctx.cwd);
20
+ if (!dir) {
21
+ ctx.ui.notify("No .readseek directory found", "info");
22
+ return;
23
+ }
24
+ const projectDir = dirname(dir);
25
+ rmSync(dir, { recursive: true, force: true });
26
+ stripFromGitignore(projectDir);
27
+ ctx.ui.notify("Removed .readseek/", "info");
28
+ }
29
+
30
+ async function init(ctx: ExtensionContext): Promise<void> {
31
+ const projectDir = ctx.cwd;
32
+ if (findReadseekDir(projectDir)) {
33
+ ctx.ui.notify(".readseek/ already exists", "info");
34
+ return;
35
+ }
36
+ initReadseekDir(projectDir);
37
+ ctx.ui.notify("Initialized .readseek/", "info");
38
+ await readseekUpdate(projectDir);
39
+ }
40
+
41
+ export function registerReadseekCommand(pi: ExtensionAPI): void {
42
+ const maybePi = pi as ExtensionAPI & {
43
+ registerCommand?: ExtensionAPI["registerCommand"];
44
+ };
45
+
46
+ maybePi.registerCommand?.("readseek", {
47
+ description: "Manage .readseek/ map cache",
48
+ handler: async (_args, ctx) => {
49
+ if (!ctx.hasUI) return;
50
+
51
+ const action = await new Promise<ReadseekAction>((resolve) => {
52
+ const initialized = findReadseekDir(ctx.cwd) !== null;
53
+
54
+ void ctx.ui.custom<ReadseekAction>(
55
+ (tui, theme, _kb, done) => {
56
+ return {
57
+ render(width: number): string[] {
58
+ const innerW = Math.max(1, width - 4);
59
+ const border = theme.fg("border", "│");
60
+ const dim = (s: string) => theme.fg("dim", s);
61
+ const accent = (s: string) => theme.fg("accent", s);
62
+ const borderFg = (s: string) => theme.fg("border", s);
63
+
64
+ function row(content: string): string {
65
+ const line = truncateToWidth(content, innerW);
66
+ const pad = Math.max(0, innerW - visibleWidth(line));
67
+ return `${border} ${line}${" ".repeat(pad)} ${border}`;
68
+ }
69
+
70
+ const lines: string[] = [];
71
+
72
+ const label = " Readseek ";
73
+ const topFill = borderFg(
74
+ "─".repeat(Math.max(0, width - 4 - visibleWidth(label))),
75
+ );
76
+ lines.push(
77
+ `${borderFg("╭─")}${accent(label)}${topFill}${borderFg("─╮")}`,
78
+ );
79
+
80
+ lines.push(row(""));
81
+
82
+ const readseekDir = findReadseekDir(ctx.cwd);
83
+ const statusDot = initialized
84
+ ? theme.fg("success", "●")
85
+ : theme.fg("warning", "●");
86
+ const statusLabel = initialized ? "Initialized" : "Not initialized";
87
+ const statusColor = initialized
88
+ ? theme.fg("success", statusLabel)
89
+ : theme.fg("warning", statusLabel);
90
+ const pathText = readseekDir
91
+ ? dim(relative(ctx.cwd, readseekDir) || readseekDir)
92
+ : dim(".readseek");
93
+ lines.push(
94
+ row(`${statusDot} ${statusColor} ${dim("·")} ${pathText}`),
95
+ );
96
+
97
+ lines.push(row(""));
98
+
99
+ if (initialized) {
100
+ lines.push(
101
+ row(
102
+ `${accent("▶")} ${dim("[u]")} Update ${dim("refresh map cache")}`,
103
+ ),
104
+ );
105
+ lines.push(
106
+ row(` ${dim("[d]")} Deinit ${dim("remove .readseek/")}`),
107
+ );
108
+ } else {
109
+ lines.push(
110
+ row(
111
+ `${accent("▶")} ${dim("[i]")} Init ${dim("create .readseek/ map cache")}`,
112
+ ),
113
+ );
114
+ }
115
+
116
+ lines.push(row(""));
117
+ lines.push(row(dim("esc close")));
118
+
119
+ lines.push(
120
+ `${borderFg("╰")}${borderFg("─".repeat(Math.max(0, width - 2)))}${borderFg("╯")}`,
121
+ );
122
+
123
+ return lines;
124
+ },
125
+
126
+ handleInput(data: string): void {
127
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
128
+ done(null);
129
+ return;
130
+ }
131
+
132
+ if (!initialized && (data === "i" || data === "I")) {
133
+ done("init");
134
+ return;
135
+ }
136
+ if (initialized && (data === "d" || data === "D")) {
137
+ done("deinit");
138
+ return;
139
+ }
140
+ if (initialized && (data === "u" || data === "U")) {
141
+ done("update");
142
+ return;
143
+ }
144
+ },
145
+
146
+ invalidate(): void {},
147
+ };
148
+ },
149
+ {
150
+ overlay: true,
151
+ overlayOptions: {
152
+ anchor: "center",
153
+ width: 60,
154
+ margin: 2,
155
+ },
156
+ },
157
+ ).then((result) => {
158
+ resolve(result ?? null);
159
+ });
160
+ });
161
+
162
+ if (action === "init") await init(ctx);
163
+ else if (action === "deinit") await deinit(ctx);
164
+ else if (action === "update") {
165
+ await readseekUpdate(ctx.cwd);
166
+ ctx.ui.notify("Map cache updated", "info");
167
+ }
168
+ },
169
+ });
170
+ }
@@ -0,0 +1,50 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+
4
+ export function findReadseekDir(cwd: string): string | null {
5
+ const abs = resolve(cwd);
6
+ for (let dir = abs; ; dir = dirname(dir)) {
7
+ const candidate = join(dir, ".readseek");
8
+ if (existsSync(candidate)) return candidate;
9
+ const parent = dirname(dir);
10
+ if (parent === dir) break;
11
+ }
12
+ return null;
13
+ }
14
+
15
+ export function hasReadseekDir(cwd: string): boolean {
16
+ return findReadseekDir(cwd) !== null;
17
+ }
18
+
19
+ export function initReadseekDir(cwd: string): string {
20
+ const abs = resolve(cwd);
21
+ const readseekDir = join(abs, ".readseek");
22
+ const mapsDir = join(readseekDir, "maps");
23
+
24
+ if (existsSync(readseekDir)) {
25
+ throw new Error(`.readseek/ already exists in ${abs}`);
26
+ }
27
+
28
+ mkdirSync(mapsDir, { recursive: true });
29
+
30
+ const gitignore = join(abs, ".gitignore");
31
+ const entry = "/.readseek";
32
+ const needsAppend = (() => {
33
+ if (!existsSync(gitignore)) return true;
34
+ const contents = readFileSync(gitignore, "utf-8");
35
+ return !contents.split("\n").some((line) => line.trim() === entry);
36
+ })();
37
+
38
+ if (needsAppend) {
39
+ let prefix = "";
40
+ if (existsSync(gitignore)) {
41
+ const contents = readFileSync(gitignore, "utf-8");
42
+ if (contents.length > 0 && !contents.endsWith("\n")) {
43
+ prefix = "\n";
44
+ }
45
+ }
46
+ appendFileSync(gitignore, `${prefix}${entry}\n`, "utf-8");
47
+ }
48
+
49
+ return readseekDir;
50
+ }