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 +10 -0
- package/package.json +2 -2
- package/src/readseek-client.ts +19 -2
- package/src/readseek-command.ts +170 -0
- package/src/readseek-repo.ts +50 -0
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.
|
|
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.
|
|
42
|
+
"@jarkkojs/readseek": "^0.3.0",
|
|
43
43
|
"diff": "^8.0.3",
|
|
44
44
|
"ignore": "^7.0.5",
|
|
45
45
|
"picomatch": "^4.0.4",
|
package/src/readseek-client.ts
CHANGED
|
@@ -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
|
|
170
|
-
|
|
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
|
+
}
|