pi-context-recall 0.1.1 → 0.1.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 +0 -1
- package/README.zh-CN.md +2 -1
- package/{history.ts → history.mjs} +91 -41
- package/package.json +2 -2
- package/recall.mjs +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,6 @@ It does not reverse summaries or replace Pi's compaction.
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
14
14
|
Requires Pi, **Node.js ≥22.19.0**, local bash and a saved Pi v3 session.
|
|
15
|
-
The package is not on npm; install it from this repository.
|
|
16
15
|
|
|
17
16
|
```bash
|
|
18
17
|
pi install git:github.com/adlternative/pi-context-recall
|
package/README.zh-CN.md
CHANGED
|
@@ -1,17 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Pure JavaScript on purpose: this file is imported by recall.mjs, and Node refuses to
|
|
3
|
+
* strip types for files under node_modules, which is where `pi install npm:` puts it.
|
|
4
|
+
*
|
|
5
|
+
* @typedef {import("@earendil-works/pi-coding-agent").SessionEntry} SessionEntry
|
|
6
|
+
* @typedef {{ type: string; text?: string; name?: string; arguments?: unknown; mimeType?: string }} ContentBlock
|
|
7
|
+
* @typedef {{ calls: number; results: number; errors: number }} ToolCounts
|
|
8
|
+
*
|
|
9
|
+
* @typedef {object} HistoryParams
|
|
10
|
+
* @property {"overview" | "compactions" | "messages" | "read"} action
|
|
11
|
+
* @property {string} [compactionId]
|
|
12
|
+
* @property {string} [entryId]
|
|
13
|
+
* @property {string} [untilId]
|
|
14
|
+
* @property {string} [query]
|
|
15
|
+
* @property {number} [offset]
|
|
16
|
+
* @property {boolean} [tail]
|
|
17
|
+
* @property {number} [limit]
|
|
18
|
+
*
|
|
19
|
+
* @typedef {ToolCounts & {
|
|
20
|
+
* name: string;
|
|
21
|
+
* }} NamedToolCounts
|
|
22
|
+
*
|
|
23
|
+
* @typedef {object} OverviewRow
|
|
24
|
+
* @property {string} id
|
|
25
|
+
* @property {string} timestamp
|
|
26
|
+
* @property {string} role
|
|
27
|
+
* @property {string} [preview]
|
|
28
|
+
* @property {boolean} [truncated]
|
|
29
|
+
* @property {ToolCounts & {
|
|
30
|
+
* firstEntryId: string; lastEntryId: string; offset: number;
|
|
31
|
+
* firstErrorEntryId: string | null; byName: NamedToolCounts[]; other: ToolCounts;
|
|
32
|
+
* }} [tools]
|
|
33
|
+
*/
|
|
2
34
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
tail?: boolean;
|
|
11
|
-
limit?: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function pageNumber(value: number | undefined, fallback: number, min: number, max: number): number {
|
|
35
|
+
/**
|
|
36
|
+
* @param {number | undefined} value
|
|
37
|
+
* @param {number} fallback
|
|
38
|
+
* @param {number} min
|
|
39
|
+
* @param {number} max
|
|
40
|
+
*/
|
|
41
|
+
function pageNumber(value, fallback, min, max) {
|
|
15
42
|
const n = value ?? fallback;
|
|
16
43
|
if (!Number.isSafeInteger(n) || n < min || n > max) {
|
|
17
44
|
throw new Error(`Expected an integer between ${min} and ${max}.`);
|
|
@@ -19,10 +46,11 @@ function pageNumber(value: number | undefined, fallback: number, min: number, ma
|
|
|
19
46
|
return n;
|
|
20
47
|
}
|
|
21
48
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Never serialize entire entries: they contain binary data, signatures and private extension state.
|
|
51
|
+
* @param {string | readonly ContentBlock[]} content
|
|
52
|
+
*/
|
|
53
|
+
function contentText(content) {
|
|
26
54
|
if (typeof content === "string") return content;
|
|
27
55
|
return content.map((block) => {
|
|
28
56
|
switch (block.type) {
|
|
@@ -35,7 +63,11 @@ function contentText(content: string | readonly {
|
|
|
35
63
|
}).join("\n");
|
|
36
64
|
}
|
|
37
65
|
|
|
38
|
-
|
|
66
|
+
/**
|
|
67
|
+
* @param {SessionEntry} entry
|
|
68
|
+
* @returns {{ role: string; text: string } | undefined}
|
|
69
|
+
*/
|
|
70
|
+
function transcript(entry) {
|
|
39
71
|
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
40
72
|
return { role: entry.type, text: entry.summary };
|
|
41
73
|
}
|
|
@@ -57,7 +89,11 @@ function transcript(entry: SessionEntry): { role: string; text: string } | undef
|
|
|
57
89
|
}
|
|
58
90
|
}
|
|
59
91
|
|
|
60
|
-
|
|
92
|
+
/**
|
|
93
|
+
* @param {readonly SessionEntry[]} branch
|
|
94
|
+
* @param {string} compactionId
|
|
95
|
+
*/
|
|
96
|
+
function archivedPrefix(branch, compactionId) {
|
|
61
97
|
const index = branch.findIndex((e) => e.id === compactionId && e.type === "compaction");
|
|
62
98
|
if (index < 0) throw new Error("Compaction not found on the current branch.");
|
|
63
99
|
const compaction = branch[index];
|
|
@@ -70,7 +106,8 @@ function archivedPrefix(branch: readonly SessionEntry[], compactionId: string):
|
|
|
70
106
|
return branch.slice(0, keptIndex);
|
|
71
107
|
}
|
|
72
108
|
|
|
73
|
-
|
|
109
|
+
/** @param {Extract<SessionEntry, { type: "compaction" }>} entry */
|
|
110
|
+
function checkpointInfo(entry) {
|
|
74
111
|
return {
|
|
75
112
|
id: entry.id, timestamp: entry.timestamp, firstKeptEntryId: entry.firstKeptEntryId,
|
|
76
113
|
tokensBefore: entry.tokensBefore, summaryPreview: entry.summary.slice(0, 300),
|
|
@@ -78,24 +115,25 @@ function checkpointInfo(entry: Extract<SessionEntry, { type: "compaction" }>) {
|
|
|
78
115
|
};
|
|
79
116
|
}
|
|
80
117
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
byName: (ToolCounts & { name: string })[]; other: ToolCounts;
|
|
88
|
-
};
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
// Never split a surrogate pair in a preview; a lone half renders as a broken glyph.
|
|
92
|
-
function clip(text: string, length: number): string {
|
|
118
|
+
/**
|
|
119
|
+
* Never split a surrogate pair in a preview; a lone half renders as a broken glyph.
|
|
120
|
+
* @param {string} text
|
|
121
|
+
* @param {number} length
|
|
122
|
+
*/
|
|
123
|
+
function clip(text, length) {
|
|
93
124
|
const cut = text.slice(0, length);
|
|
94
125
|
return /[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut;
|
|
95
126
|
}
|
|
96
127
|
|
|
97
|
-
|
|
98
|
-
|
|
128
|
+
/**
|
|
129
|
+
* @param {readonly SessionEntry[]} entries
|
|
130
|
+
* @param {number} offset
|
|
131
|
+
* @param {number} limit
|
|
132
|
+
* @param {boolean} [tail]
|
|
133
|
+
*/
|
|
134
|
+
function overview(entries, offset, limit, tail = false) {
|
|
135
|
+
/** @type {OverviewRow[]} */
|
|
136
|
+
const messages = [];
|
|
99
137
|
const cap = tail ? Infinity : limit;
|
|
100
138
|
let cursor = tail ? 0 : Math.min(offset, entries.length);
|
|
101
139
|
for (; cursor < entries.length; cursor++) {
|
|
@@ -116,7 +154,8 @@ function overview(entries: readonly SessionEntry[], offset: number, limit: numbe
|
|
|
116
154
|
}
|
|
117
155
|
if (entry.type !== "message") continue;
|
|
118
156
|
const m = entry.message;
|
|
119
|
-
|
|
157
|
+
/** @type {NamedToolCounts[]} */
|
|
158
|
+
const events = [];
|
|
120
159
|
if (m.role === "user" || m.role === "assistant" || m.role === "custom") {
|
|
121
160
|
const text = typeof m.content === "string" ? m.content
|
|
122
161
|
: contentText(m.content.filter((block) => block.type === "text" || block.type === "image"));
|
|
@@ -155,14 +194,15 @@ function overview(entries: readonly SessionEntry[], offset: number, limit: numbe
|
|
|
155
194
|
};
|
|
156
195
|
tools.lastEntryId = entry.id;
|
|
157
196
|
for (const event of events) {
|
|
158
|
-
|
|
197
|
+
/** @type {ToolCounts | undefined} */
|
|
198
|
+
let counts = tools.byName.find((item) => item.name === event.name);
|
|
159
199
|
if (!counts && tools.byName.length < 5) {
|
|
160
200
|
const named = { name: event.name, calls: 0, results: 0, errors: 0 };
|
|
161
201
|
tools.byName.push(named);
|
|
162
202
|
counts = named;
|
|
163
203
|
}
|
|
164
204
|
counts ??= tools.other;
|
|
165
|
-
for (const key of ["calls", "results", "errors"]
|
|
205
|
+
for (const key of /** @type {const} */ (["calls", "results", "errors"])) {
|
|
166
206
|
tools[key] += event[key];
|
|
167
207
|
counts[key] += event[key];
|
|
168
208
|
}
|
|
@@ -176,16 +216,25 @@ function overview(entries: readonly SessionEntry[], offset: number, limit: numbe
|
|
|
176
216
|
return { messages, nextOffset: cursor < entries.length ? cursor : null };
|
|
177
217
|
}
|
|
178
218
|
|
|
179
|
-
|
|
180
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Newest page first: offset counts rows back from the end, not raw entries.
|
|
221
|
+
* @param {readonly OverviewRow[]} rows
|
|
222
|
+
* @param {number} offset
|
|
223
|
+
* @param {number} limit
|
|
224
|
+
*/
|
|
225
|
+
function tailPage(rows, offset, limit) {
|
|
181
226
|
const end = Math.max(0, rows.length - offset);
|
|
182
227
|
const start = Math.max(0, end - limit);
|
|
183
228
|
const messages = rows.slice(start, end);
|
|
184
229
|
return { messages, nextOffset: start > 0 ? offset + messages.length : null };
|
|
185
230
|
}
|
|
186
231
|
|
|
187
|
-
/**
|
|
188
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Raw persisted transcript, never a reconstruction of provider-hook transformations.
|
|
234
|
+
* @param {readonly SessionEntry[]} branch
|
|
235
|
+
* @param {HistoryParams} params
|
|
236
|
+
*/
|
|
237
|
+
export function queryHistory(branch, params) {
|
|
189
238
|
const offset = pageNumber(params.offset, 0, 0, Number.MAX_SAFE_INTEGER);
|
|
190
239
|
if (params.tail && params.action !== "overview") throw new Error("tail is only supported by overview.");
|
|
191
240
|
if (params.untilId) {
|
|
@@ -236,6 +285,7 @@ export function queryHistory(branch: readonly SessionEntry[], params: HistoryPar
|
|
|
236
285
|
const pattern = params.query === undefined ? undefined
|
|
237
286
|
: new RegExp(params.query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "iu");
|
|
238
287
|
// shortcut: linear scan; add an index only if large sessions make recall slow.
|
|
288
|
+
/** @type {any[]} */
|
|
239
289
|
const messages = [];
|
|
240
290
|
let cursor = Math.min(offset, entries.length);
|
|
241
291
|
for (; cursor < entries.length && messages.length < limit; cursor++) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context-recall",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Recall original current-branch messages via a local CLI, Skill and tool-free Pi context bridge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"index.ts",
|
|
27
|
-
"history.
|
|
27
|
+
"history.mjs",
|
|
28
28
|
"recall.mjs",
|
|
29
29
|
"skills",
|
|
30
30
|
"README.md",
|
package/recall.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
-
import { queryHistory } from "./history.
|
|
4
|
+
import { queryHistory } from "./history.mjs";
|
|
5
5
|
|
|
6
6
|
const usage = `Usage: node recall.mjs <command> [options]
|
|
7
7
|
overview [--compaction-id ID] [--tail] [--until-id ID] [--offset N] [--limit N]
|