pi-posthorse 0.10.2 → 0.10.4
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 +7 -2
- package/index.ts +13 -3
- package/package.json +24 -21
- package/src/context-tools.ts +2 -2
- package/src/contracts.ts +39 -32
- package/src/files.ts +0 -5
- package/src/guidance.ts +41 -0
- package/src/history-entries.ts +16 -16
- package/src/history-pages.ts +1 -1
- package/src/history-search.ts +6 -6
- package/src/history.ts +5 -2
- package/src/note-discovery.ts +291 -0
- package/src/note-evidence.ts +179 -0
- package/src/note-guards.ts +203 -0
- package/src/note-inventory.ts +111 -0
- package/src/note-provenance.ts +299 -0
- package/src/note-scopes.ts +293 -0
- package/src/note-snapshots.ts +185 -0
- package/src/note-store.ts +278 -0
- package/src/note-tool-effects.ts +190 -0
- package/src/notes.ts +118 -137
- package/src/paging.ts +2 -2
- package/src/recovery-note-touches.ts +389 -0
- package/src/recovery-notes.ts +54 -503
- package/src/recovery-records.ts +90 -0
- package/src/recovery-results.ts +7 -6
- package/src/recovery.ts +24 -108
- package/src/reminders.ts +3 -3
- package/src/retention.ts +23 -17
- package/src/rollover.ts +66 -88
- package/src/session-files.ts +29 -20
- package/src/session-lines.ts +1 -2
- package/src/ui/cards.ts +3 -2
- package/src/ui/display.ts +2 -2
- package/src/ui/text.ts +0 -1
package/src/session-files.ts
CHANGED
|
@@ -56,7 +56,7 @@ const LABEL_SOURCES: readonly (readonly [label: string, needles: readonly string
|
|
|
56
56
|
*/
|
|
57
57
|
export function prefilterNeedles(query: string): string[] | undefined {
|
|
58
58
|
// A match contains every fragment between characters that JSON escapes or labels may add.
|
|
59
|
-
const [fragment] = query
|
|
59
|
+
const [fragment = ""] = query
|
|
60
60
|
.split(/["\\[\](){}:$\p{Cc}\p{Cs}]/u)
|
|
61
61
|
.map((part) => part.trim())
|
|
62
62
|
.sort((a, b) => b.length - a.length);
|
|
@@ -143,7 +143,6 @@ function ripgrepFilter(
|
|
|
143
143
|
for (const binary of binaries) {
|
|
144
144
|
try {
|
|
145
145
|
// Try the host-managed binary first; PATH is a fallback only when it is unavailable.
|
|
146
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
147
146
|
return (await execFileText(binary, [...flags, ...paths], dir, signal)).split("\0");
|
|
148
147
|
} catch (error) {
|
|
149
148
|
const code = isRecord(error) ? error.code : undefined;
|
|
@@ -162,7 +161,7 @@ function ripgrepFilter(
|
|
|
162
161
|
return async (files) => {
|
|
163
162
|
const paths = files.map((file) => relative(dir, file));
|
|
164
163
|
const found = new Set((await Promise.all(commandGroups(paths).map(matching))).flat());
|
|
165
|
-
return files.filter((
|
|
164
|
+
return files.filter((file) => found.has(relative(dir, file)));
|
|
166
165
|
};
|
|
167
166
|
}
|
|
168
167
|
function messageFrom(value: unknown): MessageLike | undefined {
|
|
@@ -178,13 +177,15 @@ function messageFrom(value: unknown): MessageLike | undefined {
|
|
|
178
177
|
toolName: optionalString(value.toolName),
|
|
179
178
|
namespace: optionalString(value.namespace),
|
|
180
179
|
toolCallId: optionalString(value.toolCallId),
|
|
180
|
+
details: value.details,
|
|
181
|
+
nestedCalls: value.nestedCalls,
|
|
181
182
|
isError: value.isError === true,
|
|
182
183
|
command: optionalString(value.command),
|
|
183
184
|
output: optionalString(value.output),
|
|
184
185
|
excludeFromContext: value.excludeFromContext === true,
|
|
185
186
|
};
|
|
186
187
|
}
|
|
187
|
-
function
|
|
188
|
+
export function parseSessionLine(line: string): EntryLike | undefined {
|
|
188
189
|
try {
|
|
189
190
|
const value: unknown = JSON.parse(line);
|
|
190
191
|
if (!isRecord(value)) {
|
|
@@ -203,6 +204,9 @@ function parsedEntry(line: string): EntryLike | undefined {
|
|
|
203
204
|
customType: optionalString(value.customType),
|
|
204
205
|
content: value.content,
|
|
205
206
|
details: value.details,
|
|
207
|
+
data: value.data,
|
|
208
|
+
cwd: optionalString(value.cwd),
|
|
209
|
+
parentSession: optionalString(value.parentSession),
|
|
206
210
|
display: value.display === true,
|
|
207
211
|
handoff: optionalString(value.handoff),
|
|
208
212
|
firstKeptEntryId: optionalString(value.firstKeptEntryId),
|
|
@@ -222,7 +226,7 @@ function entryHead(prefix: string): EntryLike | undefined {
|
|
|
222
226
|
return {
|
|
223
227
|
type: head[1],
|
|
224
228
|
id: head[2],
|
|
225
|
-
parentId: head[3] === "null" ? null : head[3]
|
|
229
|
+
parentId: head[3] === "null" ? null : head[3]?.slice(1, -1),
|
|
226
230
|
};
|
|
227
231
|
}
|
|
228
232
|
/** Whether a caller may need a line; other lines only advance window ancestry. */
|
|
@@ -235,7 +239,7 @@ async function readEntry(
|
|
|
235
239
|
if (head !== undefined && (!line.matches || !wanted(head))) {
|
|
236
240
|
return { entry: head, selected: false };
|
|
237
241
|
}
|
|
238
|
-
const entry =
|
|
242
|
+
const entry = parseSessionLine(await line.text());
|
|
239
243
|
return entry === undefined
|
|
240
244
|
? undefined
|
|
241
245
|
: {
|
|
@@ -271,7 +275,6 @@ async function sessionFiles(dir: string): Promise<string[]> {
|
|
|
271
275
|
let level = existsSync(dir) ? [dir] : [];
|
|
272
276
|
while (level.length > 0) {
|
|
273
277
|
// Each level lists the directories the previous level found.
|
|
274
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
275
278
|
const listings = await Promise.all(
|
|
276
279
|
level.map((path) => readdir(path, { withFileTypes: true })),
|
|
277
280
|
);
|
|
@@ -293,16 +296,24 @@ async function sessionFiles(dir: string): Promise<string[]> {
|
|
|
293
296
|
}
|
|
294
297
|
type SessionFile = { readonly file: string; readonly size: number; readonly mtime: number };
|
|
295
298
|
async function newestFirst(files: readonly string[]): Promise<SessionFile[]> {
|
|
296
|
-
const stats = await Promise.all(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
299
|
+
const stats = await Promise.all(
|
|
300
|
+
files.map(async (file) => {
|
|
301
|
+
const info = await stat(file);
|
|
302
|
+
return { file, size: info.size, mtime: info.mtimeMs };
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
return stats.sort((a, b) => b.mtime - a.mtime);
|
|
300
306
|
}
|
|
301
307
|
/** The newest files up to FIRST_SEARCH_BYTES, then the rest. */
|
|
302
308
|
function searchPhases(files: readonly SessionFile[]): readonly (readonly string[])[] {
|
|
303
309
|
let count = 0;
|
|
304
|
-
|
|
305
|
-
|
|
310
|
+
let bytes = 0;
|
|
311
|
+
for (const file of files) {
|
|
312
|
+
if (bytes >= FIRST_SEARCH_BYTES) {
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
bytes += file.size;
|
|
316
|
+
count++;
|
|
306
317
|
}
|
|
307
318
|
const paths = files.map(({ file }) => file);
|
|
308
319
|
return [paths.slice(0, count), paths.slice(count)];
|
|
@@ -310,7 +321,7 @@ function searchPhases(files: readonly SessionFile[]): readonly (readonly string[
|
|
|
310
321
|
async function sessionHeader(
|
|
311
322
|
file: string,
|
|
312
323
|
signal?: AbortSignal,
|
|
313
|
-
): Promise<{ readonly cwd?: string } | undefined> {
|
|
324
|
+
): Promise<{ readonly cwd?: string | undefined } | undefined> {
|
|
314
325
|
for await (const line of lfLines(file, signal)) {
|
|
315
326
|
try {
|
|
316
327
|
const value: unknown = JSON.parse(line.toString("utf8"));
|
|
@@ -343,11 +354,11 @@ function parentSession(
|
|
|
343
354
|
export type SessionScope = {
|
|
344
355
|
readonly dir: string;
|
|
345
356
|
readonly cwd: string;
|
|
346
|
-
readonly currentFile?: string;
|
|
347
|
-
readonly signal?: AbortSignal;
|
|
348
|
-
readonly fileKey?: string;
|
|
357
|
+
readonly currentFile?: string | undefined;
|
|
358
|
+
readonly signal?: AbortSignal | undefined;
|
|
359
|
+
readonly fileKey?: string | undefined;
|
|
349
360
|
/** Raw-text needles; files without any of them are skipped. */
|
|
350
|
-
readonly needles?: readonly string[];
|
|
361
|
+
readonly needles?: readonly string[] | undefined;
|
|
351
362
|
};
|
|
352
363
|
/** Scope memoization is owned by one enumeration; nested runs inherit their parent's project. */
|
|
353
364
|
function membership(
|
|
@@ -393,10 +404,8 @@ export async function* scopedSessionFiles(scope: SessionScope): AsyncGenerator<s
|
|
|
393
404
|
: ripgrepFilter(scope.dir, scope.needles, scope.signal);
|
|
394
405
|
for (const phase of searchPhases(selected)) {
|
|
395
406
|
// Older files are searched only when the newest ones did not satisfy the caller.
|
|
396
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
397
407
|
for (const file of prefilter === undefined ? phase : await prefilter(phase)) {
|
|
398
408
|
// Preserve newest-first traversal while closing each header stream before opening another.
|
|
399
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
400
409
|
if (await belongs(file)) {
|
|
401
410
|
yield file;
|
|
402
411
|
}
|
package/src/session-lines.ts
CHANGED
|
@@ -18,7 +18,7 @@ type LineRange = {
|
|
|
18
18
|
readonly end: number;
|
|
19
19
|
readonly head: string;
|
|
20
20
|
readonly matches: boolean;
|
|
21
|
-
readonly bytes?: Readonly<Pick<Buffer, "toString"
|
|
21
|
+
readonly bytes?: Readonly<Pick<Buffer, "toString">> | undefined;
|
|
22
22
|
};
|
|
23
23
|
async function* lineRanges(
|
|
24
24
|
file: string,
|
|
@@ -73,7 +73,6 @@ async function rangeText(
|
|
|
73
73
|
while (offset < buffer.length) {
|
|
74
74
|
signal?.throwIfAborted();
|
|
75
75
|
// Positioned reads may be short; each continues the same selected record.
|
|
76
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
77
76
|
const { bytesRead } = await handle.read(
|
|
78
77
|
buffer,
|
|
79
78
|
offset,
|
package/src/ui/cards.ts
CHANGED
|
@@ -61,11 +61,12 @@ function callDetails(name: ToolName, args: Readonly<Record<string, unknown>>): s
|
|
|
61
61
|
name === "history" && args.op === "search"
|
|
62
62
|
? `Scope: ${args.all === true ? "all project sessions" : "current branch"}`
|
|
63
63
|
: "";
|
|
64
|
-
const
|
|
64
|
+
const labels: readonly (readonly [string, string])[] = [
|
|
65
65
|
["offset", "Offset"],
|
|
66
66
|
["imageOffset", "Image offset"],
|
|
67
67
|
["limit", "Limit"],
|
|
68
|
-
]
|
|
68
|
+
];
|
|
69
|
+
const offsets = labels.map(([key, label]) => {
|
|
69
70
|
const value = args[key];
|
|
70
71
|
return value === undefined
|
|
71
72
|
? ""
|
package/src/ui/display.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type PosthorseDisplay =
|
|
|
21
21
|
readonly percent: number | null;
|
|
22
22
|
};
|
|
23
23
|
readonly rollover?: "enabled" | "disabled" | "unsupported";
|
|
24
|
-
readonly rolloverAt?: number;
|
|
24
|
+
readonly rolloverAt?: number | undefined;
|
|
25
25
|
}
|
|
26
26
|
| {
|
|
27
27
|
readonly kind: "notes-list" | "notes-search";
|
|
@@ -241,7 +241,7 @@ function historyReadSummary(
|
|
|
241
241
|
) {
|
|
242
242
|
return pageSummary(display);
|
|
243
243
|
}
|
|
244
|
-
const summary = `${pageSummary(display).split("\n")
|
|
244
|
+
const summary = `${pageSummary(display).split("\n", 1).join("")}\nImages ${n(display.imageOffset ?? 0)}–${n(display.imageEnd)} of ${n(display.imageTotal)}`;
|
|
245
245
|
return display.end < display.total || display.imageEnd < display.imageTotal
|
|
246
246
|
? `Next offset ${n(display.end)}\nimageOffset ${n(display.imageEnd)}\n${summary}`
|
|
247
247
|
: summary;
|
package/src/ui/text.ts
CHANGED
|
@@ -5,7 +5,6 @@ export type OutputColor = "toolOutput" | "muted" | "dim" | "error" | "warning";
|
|
|
5
5
|
|
|
6
6
|
export function clean(value: string): string {
|
|
7
7
|
// Tool previews must not execute terminal controls or Unicode interlinear annotations.
|
|
8
|
-
// oxlint-disable-next-line no-control-regex
|
|
9
8
|
return stripTerminalSequences(value).replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f\ufff9-\ufffb]/g, "");
|
|
10
9
|
}
|
|
11
10
|
|