pi-context-recall 0.1.1 → 0.1.2

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 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
@@ -11,7 +11,8 @@ CLI 检索 Pi 已保存的原文,Skill 教 agent 通过已有 `bash` 使用,
11
11
  ## 安装
12
12
 
13
13
  需要 Pi、**Node.js ≥22.19.0**、本地 bash 和已保存的 Pi v3 会话。
14
- 包尚未发布到 npm,从本仓库安装:
14
+
15
+ 从本仓库安装:
15
16
 
16
17
  ```bash
17
18
  pi install git:github.com/adlternative/pi-context-recall
@@ -1,17 +1,44 @@
1
- import type { SessionEntry } from "@earendil-works/pi-coding-agent";
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
- export interface HistoryParams {
4
- action: "overview" | "compactions" | "messages" | "read";
5
- compactionId?: string;
6
- entryId?: string;
7
- untilId?: string;
8
- query?: string;
9
- offset?: number;
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
- // Never serialize entire entries: they contain binary data, signatures and private extension state.
23
- function contentText(content: string | readonly {
24
- type: string; text?: string; name?: string; arguments?: unknown; mimeType?: string;
25
- }[]): string {
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
- function transcript(entry: SessionEntry): { role: string; text: string } | undefined {
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
- function archivedPrefix(branch: readonly SessionEntry[], compactionId: string): readonly SessionEntry[] {
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
- function checkpointInfo(entry: Extract<SessionEntry, { type: "compaction" }>) {
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
- type ToolCounts = { calls: number; results: number; errors: number };
82
- type OverviewRow = {
83
- id: string; timestamp: string; role: string;
84
- preview?: string; truncated?: boolean;
85
- tools?: ToolCounts & {
86
- firstEntryId: string; lastEntryId: string; offset: number; firstErrorEntryId: string | null;
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
- function overview(entries: readonly SessionEntry[], offset: number, limit: number, tail = false) {
98
- const messages: OverviewRow[] = [];
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
- const events: (ToolCounts & { name: string })[] = [];
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
- let counts: ToolCounts | undefined = tools.byName.find((item) => item.name === event.name);
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"] as const) {
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
- // Newest page first: offset counts rows back from the end, not raw entries.
180
- function tailPage(rows: readonly OverviewRow[], offset: number, limit: number) {
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
- /** Raw persisted transcript, never a reconstruction of provider-hook transformations. */
188
- export function queryHistory(branch: readonly SessionEntry[], params: HistoryParams) {
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.1",
3
+ "version": "0.1.2",
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.ts",
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.ts";
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]