pi-posthorse 0.9.2 → 0.9.3
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/README.md +1 -1
- package/package.json +1 -1
- package/src/files.ts +57 -3
- package/src/notes.ts +4 -4
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ Removal leaves notes and Pi session history in place. See the [installation refe
|
|
|
73
73
|
|
|
74
74
|
## Data and privacy
|
|
75
75
|
|
|
76
|
-
Posthorse makes no network requests. Notes are plaintext files under `.pi/notes/`; linked Git worktrees share them. Add `.pi/notes/` to `.gitignore` if the notes must stay private.
|
|
76
|
+
Posthorse makes no network requests. Notes are plaintext files under `.pi/notes/`; linked Git worktrees share them. Notes for the Git packages Pi installs in `~/.pi/agent/git/` live under `~/.pi/agent/notes/` instead, because `pi update` deletes untracked files in those checkouts. Add `.pi/notes/` to `.gitignore` if the notes must stay private.
|
|
77
77
|
|
|
78
78
|
History can include user messages, assistant thinking, tool arguments, errors, and images. Content you retrieve enters the context of your selected model and provider.
|
|
79
79
|
|
package/package.json
CHANGED
package/src/files.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createReadStream,
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
statSync,
|
|
11
|
+
symlinkSync,
|
|
12
|
+
} from "node:fs";
|
|
3
13
|
import {
|
|
4
14
|
access,
|
|
5
15
|
constants,
|
|
@@ -12,7 +22,8 @@ import {
|
|
|
12
22
|
stat,
|
|
13
23
|
unlink,
|
|
14
24
|
} from "node:fs/promises";
|
|
15
|
-
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
25
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
26
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
16
27
|
import { isRecord } from "./contracts.ts";
|
|
17
28
|
|
|
18
29
|
async function lstatOrMissing(path: string) {
|
|
@@ -171,7 +182,7 @@ function gitRoot(dir: string, marker: string): string {
|
|
|
171
182
|
return basename(common) === ".git" ? dirname(common) : common;
|
|
172
183
|
}
|
|
173
184
|
/** Conventional repos share the primary checkout; separate Git directories own their shared root. */
|
|
174
|
-
|
|
185
|
+
function notesRoot(cwd: string): string {
|
|
175
186
|
for (let dir = cwd; ; dir = dirname(dir)) {
|
|
176
187
|
const marker = join(dir, ".git");
|
|
177
188
|
if (existsSync(marker)) {
|
|
@@ -186,3 +197,46 @@ export function notesRoot(cwd: string): string {
|
|
|
186
197
|
}
|
|
187
198
|
}
|
|
188
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* `pi update` resets the Git packages Pi installs in the agent directory and runs `git clean -fdx`
|
|
202
|
+
* in them, deleting notes kept inside. Their notes live in the agent directory under the same host
|
|
203
|
+
* and path instead.
|
|
204
|
+
* ponytail: packages installed for one project (its `.pi/git/`) still keep notes in the checkout;
|
|
205
|
+
* give them a per-project location if sessions start working inside them.
|
|
206
|
+
* ponytail: notes a package commits under `.pi/notes` return with each update and move into
|
|
207
|
+
* another `from-checkout-*` folder; skip tracked files if a package ever commits its notes.
|
|
208
|
+
*/
|
|
209
|
+
function packageNotes(root: string): string | undefined {
|
|
210
|
+
const agentDir = getAgentDir();
|
|
211
|
+
const installs = join(agentDir, "git");
|
|
212
|
+
const path = relative(existsSync(installs) ? realpathSync(installs) : installs, root);
|
|
213
|
+
const inside =
|
|
214
|
+
path !== "" && path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path);
|
|
215
|
+
// Pi cleans only the checkouts; folders that hold several of them keep their own notes.
|
|
216
|
+
return inside && existsSync(join(root, ".git")) ? join(agentDir, "notes", path) : undefined;
|
|
217
|
+
}
|
|
218
|
+
/** The notes directory for `cwd`, moving any notes found inside a Pi package checkout. */
|
|
219
|
+
export function notesDir(cwd: string): string {
|
|
220
|
+
const root = notesRoot(cwd);
|
|
221
|
+
const local = join(root, ".pi", "notes");
|
|
222
|
+
const moved = packageNotes(root);
|
|
223
|
+
if (moved === undefined) {
|
|
224
|
+
return local;
|
|
225
|
+
}
|
|
226
|
+
if (lstatSync(local, { throwIfNoEntry: false })?.isDirectory() === true) {
|
|
227
|
+
mkdirSync(dirname(moved), { recursive: true });
|
|
228
|
+
// Notes an older Posthorse wrote here after an earlier move keep a folder of their own.
|
|
229
|
+
const target = existsSync(moved) ? join(moved, `from-checkout-${Date.now()}`) : moved;
|
|
230
|
+
try {
|
|
231
|
+
renameSync(local, target);
|
|
232
|
+
// Sessions still running an older Posthorse keep reaching the notes until they reload.
|
|
233
|
+
symlinkSync(moved, local, "junction");
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (lstatSync(local, { throwIfNoEntry: false })?.isDirectory() === true) {
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
// Another session moved them first, or only the compatibility link failed.
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return moved;
|
|
242
|
+
}
|
package/src/notes.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
type PolicyContext,
|
|
24
24
|
type TextResult,
|
|
25
25
|
} from "./contracts.ts";
|
|
26
|
-
import { isBinaryFile,
|
|
26
|
+
import { isBinaryFile, notesDir, publishFile, readText } from "./files.ts";
|
|
27
27
|
import { excerpt, excerptAround } from "./message-text.ts";
|
|
28
28
|
import type { PageAccess } from "./paging.ts";
|
|
29
29
|
|
|
@@ -45,7 +45,7 @@ const SEARCH_OFFSET_LIMIT = 1_000_000;
|
|
|
45
45
|
function safeJoin(dir: string, path: string): string {
|
|
46
46
|
const relative = normalize(path.replace(/^[/\\]+/, ""));
|
|
47
47
|
if (relative === ".." || relative.startsWith(`..${sep}`) || isAbsolute(relative)) {
|
|
48
|
-
throw new Error(`Invalid path "${path}": must stay inside
|
|
48
|
+
throw new Error(`Invalid path "${path}": must stay inside the notes directory.`);
|
|
49
49
|
}
|
|
50
50
|
return join(dir, relative);
|
|
51
51
|
}
|
|
@@ -278,7 +278,7 @@ export function registerNotes(
|
|
|
278
278
|
{ description: "Operation to perform" },
|
|
279
279
|
),
|
|
280
280
|
path: Type.Optional(
|
|
281
|
-
Type.String({ description: "Note or folder path relative to
|
|
281
|
+
Type.String({ description: "Note or folder path relative to the notes directory" }),
|
|
282
282
|
),
|
|
283
283
|
content: Type.Optional(
|
|
284
284
|
Type.String({ description: "Full file content (write) or text to add (append)" }),
|
|
@@ -296,7 +296,7 @@ export function registerNotes(
|
|
|
296
296
|
// Pi's fixed tool contract supplies id, params, signal, updates, and context.
|
|
297
297
|
// oxlint-disable-next-line max-params
|
|
298
298
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
299
|
-
const dir =
|
|
299
|
+
const dir = notesDir(ctx.cwd);
|
|
300
300
|
switch (params.op) {
|
|
301
301
|
case "list":
|
|
302
302
|
return listNotes(dir, params, policy(ctx), pages);
|