@henryqw/pi-session-recall 2.1.1 → 4.0.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 +57 -69
- package/docs/session-search-routing.html +5 -5
- package/docs/session-search-routing.svg +5 -5
- package/extensions/hydrate.ts +7 -24
- package/extensions/search-core.ts +12 -33
- package/extensions/session-recall.ts +21 -119
- package/extensions/types.ts +2 -2
- package/package.json +60 -55
package/README.md
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
# `@henryqw/pi-session-recall`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Saved transcripts are not injected on every turn. The active tool registration still adds standing prompt cost through its schema, descriptions, and guideline. Returned content enters active model context.
|
|
6
|
-
|
|
7
|
-
The bundled `pi-session-pattern-miner` skill prepares one bounded sample. The model then finds repeated work that may deserve automation.
|
|
3
|
+
Search past Pi sessions locally to find prior decisions and context without injecting saved transcripts on every turn. The active tool registration still adds standing prompt cost through its schema, descriptions, and guideline. Returned content enters active model context.
|
|
8
4
|
|
|
9
5
|
## Install
|
|
10
6
|
|
|
@@ -12,74 +8,76 @@ The bundled `pi-session-pattern-miner` skill prepares one bounded sample. The mo
|
|
|
12
8
|
pi install npm:@henryqw/pi-session-recall
|
|
13
9
|
```
|
|
14
10
|
|
|
11
|
+
Requires Node.js 22.19.0 or later.
|
|
12
|
+
|
|
15
13
|
## Use
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
Ask the agent to search with distinctive keywords, for example:
|
|
18
16
|
|
|
19
17
|
```json
|
|
20
18
|
{ "query": "database migration rollback" }
|
|
21
19
|
```
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
Discovery returns ranked metadata and snippets. Use a result's `path` and `matchMessageId` to scroll to the relevant messages.
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
| Surface | Type | Purpose |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `session_search` | tool | Agent tool to browse, search, read, scroll, or prepare a bounded pattern-mining corpus. |
|
|
26
|
+
| `/skill:pi-session-pattern-miner` | skill | Agent workflow, invoked by a user, to find repeated work and choose the smallest useful automation. |
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
"operation": "prepare-pattern-miner",
|
|
30
|
-
"scope": "repository",
|
|
31
|
-
"limit": 10
|
|
32
|
-
}
|
|
33
|
-
```
|
|
28
|
+
The tool supports these modes. Agents use `session_search`; users can invoke the skill with `/skill:pi-session-pattern-miner`.
|
|
34
29
|
|
|
35
|
-
|
|
30
|
+
| Mode | Call | Result |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| Pattern preparation | `operation:"prepare-pattern-miner"` + `scope:"repository"` or `scope:"all"` | Up to ten recent lineage-unique sessions plus repository inventory. `limit` defaults to 10 and is clamped to [1,10]. Repository scope includes exact and descendant `cwd` values, filters before the limit, and excludes the current session file. |
|
|
33
|
+
| Discovery | `query` | BM25-ranked metadata and snippets. Each result includes `path` and `matchMessageId` for a follow-up scroll. Start with keywords, then narrow the query before scrolling. `limit` defaults to 3 and is clamped to [1,10]. |
|
|
34
|
+
| Scroll | `sessionId` + `aroundMessageId` | Raw message roles, including tool results, within ±`window` ([1,20], default 5) of the anchor on the selected branch. Re-anchor on the last or first message ID to scroll. Across forks, pass the previous response's `branchTip`; `aroundMessageId` must lie on that branch. |
|
|
35
|
+
| Read | `sessionId` | Raw message roles, including tool results, from the session. Large sessions return head 20 + tail 10. Oversized content is bounded to 50k characters and marked with `contentTruncated`. |
|
|
36
|
+
| Browse | no args | Recent sessions with path, name, cwd, started date, and preview. `limit` defaults to 3 and is clamped to [1,10]. |
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
BM25 is a text-ranking method. Discovery returns index metadata; READ and SCROLL retrieve messages from saved session files.
|
|
39
|
+
|
|
40
|
+
To scroll directly to a discovery hit, pass its `path` and `matchMessageId`:
|
|
38
41
|
|
|
39
42
|
```json
|
|
40
43
|
{
|
|
41
|
-
"sessionId": "<
|
|
42
|
-
"aroundMessageId": "<
|
|
44
|
+
"sessionId": "<result path>",
|
|
45
|
+
"aroundMessageId": "<result matchMessageId>",
|
|
43
46
|
"window": 10
|
|
44
47
|
}
|
|
45
48
|
```
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
This returns up to ten messages before and after the anchor on the selected branch.
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
| --- | --- | --- |
|
|
51
|
-
| `session_search` | tool | Search, inspect, or prepare a bounded mining corpus from past sessions. |
|
|
52
|
-
| `pi-session-pattern-miner` | skill | Find repeated work and choose the smallest useful automation. |
|
|
52
|
+
Start pattern mining with one preparation call:
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"operation": "prepare-pattern-miner",
|
|
57
|
+
"scope": "repository",
|
|
58
|
+
"limit": 10
|
|
59
|
+
}
|
|
60
|
+
```
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
| --- | --- | --- |
|
|
58
|
-
| Pattern preparation | `operation:"prepare-pattern-miner"` + `scope:"repository"` or `scope:"all"` | Up to ten recent lineage-unique sessions plus repository inventory. The default limit is 10. Repository scope includes exact and descendant `cwd` values, filters before the limit, and excludes the current session file. |
|
|
59
|
-
| Discovery | `query` | BM25-ranked top sessions. Adaptive retrieval uses user and assistant text for windows, bookends, anchors, and counts. It omits tool-result messages and sets `toolResultsOmitted:true` when it removes one. Lower hits still include their indexed anchor. Use `detail:"full"` to hydrate every hit with tool-result messages included. |
|
|
60
|
-
| Scroll | `sessionId` + `aroundMessageId` | Raw message roles, including tool results, within ±`window` ([1,20]) of the anchor. Re-anchor on the last or first message ID to scroll. Across forks, pass the previous response's `branchTip`; `aroundMessageId` only centers the window and must lie on that branch. |
|
|
61
|
-
| Read | `sessionId` | Raw message roles, including tool results, from the session. Large sessions return head 20 + tail 10. Oversized content is bounded to 50k characters and marked with `contentTruncated`. |
|
|
62
|
-
| Browse | no args | Recent sessions with path, name, cwd, started date, and preview. |
|
|
62
|
+
Use `scope:"all"` for cross-repository work. It still returns the corpus when repository inventory is unavailable.
|
|
63
63
|
|
|
64
64
|
In the interactive TUI, the collapsed tool block shows the last five visual lines and the earlier-line count. Press `Ctrl+O` to expand the full bounded response. The model always receives the complete tool result.
|
|
65
65
|
|
|
66
|
-
###
|
|
66
|
+
### Pattern-miner skill
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
Invoke `/skill:pi-session-pattern-miner` to find repeated workflows in past sessions. The skill makes exactly one preparation call before interpretation.
|
|
69
69
|
|
|
70
|
-
It treats one lineage as one source
|
|
70
|
+
It treats one lineage as one source and requires two independent examples before recommending automation. A requested topic gets a focused confirmation search even when the prepared sample does not contain it.
|
|
71
71
|
|
|
72
72
|
After clustering, the skill always checks current candidate-relevant package manifests, scripts, skills, and instructions. It abstains if it cannot check them safely.
|
|
73
73
|
|
|
74
74
|
### Repository inventory
|
|
75
75
|
|
|
76
|
-
Repository inventory contains discovery hints. It is never proof that a file owns a workflow
|
|
76
|
+
Repository inventory contains discovery hints. It is never proof that a file owns a workflow and does not verify the current worktree.
|
|
77
77
|
|
|
78
78
|
Package scripts, executable paths, and instruction paths come from stage-0 entries in the Git index. Package content comes from indexed blobs. Git reads the objects locally in one check batch and one content batch. Lazy object fetching and replacement refs are disabled. Inventory never opens working-tree package paths.
|
|
79
79
|
|
|
80
|
-
Executables need index mode `100755`. Instructions need a recognized name and a regular-file index mode.
|
|
81
|
-
|
|
82
|
-
Skills come from Pi's effective command registry. Their canonical source paths must stay inside the repository.
|
|
80
|
+
Executables need index mode `100755`. Instructions need a recognized name and a regular-file index mode. Skills come from Pi's effective command registry, and their canonical source paths must stay inside the repository.
|
|
83
81
|
|
|
84
82
|
Available inventory includes this provenance:
|
|
85
83
|
|
|
@@ -92,13 +90,11 @@ Available inventory includes this provenance:
|
|
|
92
90
|
}
|
|
93
91
|
```
|
|
94
92
|
|
|
95
|
-
It also sets `worktreeVerified:false`. Staged adds, changes, and deletes affect the snapshot. Unstaged changes, deletions, mode changes, symlinks, and untracked files do not.
|
|
96
|
-
|
|
97
|
-
Inspect the current candidate files before assigning ownership. Abstain if targeted current-file checks cannot be done safely.
|
|
93
|
+
It also sets `worktreeVerified:false`. Staged adds, changes, and deletes affect the snapshot. Unstaged changes, deletions, mode changes, symlinks, and untracked files do not. Inspect current candidate files before assigning ownership; abstain if targeted current-file checks cannot be done safely.
|
|
98
94
|
|
|
99
95
|
## Flow
|
|
100
96
|
|
|
101
|
-

|
|
102
98
|
|
|
103
99
|
Search makes no model calls.
|
|
104
100
|
|
|
@@ -112,38 +108,24 @@ Search makes no model calls.
|
|
|
112
108
|
|
|
113
109
|
### Context and sync
|
|
114
110
|
|
|
115
|
-
|
|
111
|
+
Discovery excludes the current session file when its path is available. Forked sessions collapse into their parent when both match.
|
|
116
112
|
|
|
117
|
-
Before browse, discovery, or pattern preparation, the extension lazily syncs the index from the session tree.
|
|
118
|
-
|
|
119
|
-
Pattern preparation reports `sync.walkComplete`, `sync.backlogRemaining`, and `sync.complete`. `sync.complete` is true only after a complete walk with no backlog.
|
|
113
|
+
Before browse, discovery, or pattern preparation, the extension lazily syncs the index from the session tree. Pattern preparation reports `sync.walkComplete`, `sync.backlogRemaining`, and `sync.complete`; `sync.complete` is true only after a complete walk with no backlog.
|
|
120
114
|
|
|
121
115
|
### Retrieval safety
|
|
122
116
|
|
|
123
|
-
|
|
117
|
+
Discovery returns short snippets from indexed user and assistant transcript text. Use a result's `path` and `matchMessageId` with SCROLL for targeted context; READ and SCROLL retrieve raw messages, including tool-result messages when present.
|
|
124
118
|
|
|
125
|
-
Pattern preparation returns only non-empty user and assistant text. It never returns thinking blocks or tool-result content
|
|
119
|
+
Pattern preparation returns only non-empty user and assistant text. It never returns thinking blocks or tool-result content, and preserves citation and lineage metadata when a source cannot be read.
|
|
126
120
|
|
|
127
121
|
Historical tool output may contain secrets or other sensitive data. Raw retrieval places that output in active model context.
|
|
128
122
|
|
|
129
123
|
## State and storage
|
|
130
124
|
|
|
131
|
-
The extension maintains
|
|
132
|
-
|
|
133
|
-
This is derived state. Delete it and it rebuilds from your session files.
|
|
125
|
+
The extension maintains its derived SQLite search index at `~/.pi/agent/config/pi-session-recall/index.db`. Delete this derived state to rebuild it from your session files; no migration or recovery machinery is needed.
|
|
134
126
|
|
|
135
127
|
The index and transcript reads stay local. Transcripts are read in place. Returned content follows the data path of your configured model provider.
|
|
136
128
|
|
|
137
|
-
## Roll back
|
|
138
|
-
|
|
139
|
-
Pin the previous release:
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
pi install npm:@henryqw/pi-session-recall@2.0.0
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
No index migration or cleanup is needed.
|
|
146
|
-
|
|
147
129
|
## Limits and recovery
|
|
148
130
|
|
|
149
131
|
Lazy index sync can fail while walking the session tree. Results still come from the current index and can be partly updated or stale. Files found before failure may have new content, while rows for files the walk did not reach stay stale.
|
|
@@ -154,17 +136,13 @@ Lazy index sync can fail while walking the session tree. Results still come from
|
|
|
154
136
|
|
|
155
137
|
Session directories whose encoded path starts with `--tmp-` or `--private-tmp-` are never indexed. These sessions run from `/tmp` or `/private/tmp`.
|
|
156
138
|
|
|
157
|
-
Session files over 32 MiB are excluded from indexing and hydration
|
|
158
|
-
|
|
159
|
-
READ and SCROLL return an explicit size error. A stale discovery hit from before a file grew returns metadata with empty messages and that error.
|
|
139
|
+
Session files over 32 MiB are excluded from indexing and hydration, so discovery cannot newly find them. READ and SCROLL return an explicit size error. Discovery returns indexed metadata even when a source file changes afterward.
|
|
160
140
|
|
|
161
141
|
Pattern preparation runs one sync pass. A positive backlog or incomplete walk limits the sample and sets `sync.complete:false`. A total sync or required repository-inventory failure returns an explicit tool error.
|
|
162
142
|
|
|
163
|
-
Repository scope fails outside Git or when repository inventory fails. All scope still returns its corpus in both cases.
|
|
164
|
-
|
|
165
|
-
Outside Git, all scope sets `inventory.available:false` with `reason:"not-a-git-repository"`. On a repository inventory error, it uses `reason:"inventory-failed"`. Cancellation always aborts the call instead of returning unavailable inventory.
|
|
143
|
+
Repository scope fails outside Git or when repository inventory fails. All scope still returns its corpus in both cases. Outside Git, all scope sets `inventory.available:false` with `reason:"not-a-git-repository"`. On a repository inventory error, it uses `reason:"inventory-failed"`. Cancellation always aborts the call instead of returning unavailable inventory.
|
|
166
144
|
|
|
167
|
-
Preparation rejects `query`, session cursors,
|
|
145
|
+
Preparation rejects `query`, session cursors, or `window` in the same call. It also rejects `scope` without the operation.
|
|
168
146
|
|
|
169
147
|
Preparation output stays within 50,000 serialized characters. Inventory uses at most 10,000 characters. Its `omittedCounts` report only omitted collection entries, and `inventory.truncated` reports those omissions.
|
|
170
148
|
|
|
@@ -173,3 +151,13 @@ Inventory fails if the Git root output exceeds 4 KiB or the raw index listing ex
|
|
|
173
151
|
Inventory accepts at most 512 package manifests. Each indexed manifest can be at most 1 MiB, and their declared sizes can total at most 16 MiB. One bounded batch checks all blob sizes before one bounded batch reads their content. Blob order, type, size, UTF-8, and exact output framing are checked before manifest data is parsed.
|
|
174
152
|
|
|
175
153
|
Session and top-level `contentTruncated` report transcript budget trimming only. Session `truncated` reports omitted middle messages.
|
|
154
|
+
|
|
155
|
+
### Rollback
|
|
156
|
+
|
|
157
|
+
To return to the older 2.1.1 behavior, pin that release:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pi install npm:@henryqw/pi-session-recall@2.1.1
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
No index migration or cleanup is needed.
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
<body>
|
|
16
16
|
<figure>
|
|
17
17
|
<svg width="1280" height="720" viewBox="0 0 1280 720" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="session-search-routing-title session-search-routing-desc">
|
|
18
|
-
<title id="session-search-routing-title">session_search
|
|
19
|
-
<desc id="session-search-routing-desc">Flowchart routing
|
|
18
|
+
<title id="session-search-routing-title">session_search progressive search and scroll</title>
|
|
19
|
+
<desc id="session-search-routing-desc">Flowchart routing progressive keyword discovery to metadata results, then using each result path and matchMessageId for direct transcript scroll.</desc>
|
|
20
20
|
<defs>
|
|
21
21
|
<style>
|
|
22
22
|
.eyebrow { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; letter-spacing: .16em; }
|
|
@@ -107,9 +107,9 @@
|
|
|
107
107
|
|
|
108
108
|
<rect x="576" y="400" width="200" height="96" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
|
|
109
109
|
<text class="node-name" x="676" y="428">Discovery</text>
|
|
110
|
-
<text class="node-sub" x="676" y="452">query · lazy sync + FTS5</text>
|
|
111
|
-
<text class="node-sub" x="676" y="468">
|
|
112
|
-
<text class="node-sub" x="676" y="484">
|
|
110
|
+
<text class="node-sub" x="676" y="452">keyword query · lazy sync + FTS5</text>
|
|
111
|
+
<text class="node-sub" x="676" y="468">metadata + snippets only</text>
|
|
112
|
+
<text class="node-sub" x="676" y="484">path + matchMessageId → SCROLL</text>
|
|
113
113
|
|
|
114
114
|
<rect x="328" y="544" width="192" height="96" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
|
|
115
115
|
<text class="node-name" x="424" y="572">Browse</text>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
2
|
<svg width="1280" height="720" viewBox="0 0 1280 720" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="session-search-routing-title session-search-routing-desc">
|
|
3
|
-
<title id="session-search-routing-title">session_search
|
|
4
|
-
<desc id="session-search-routing-desc">Flowchart routing
|
|
3
|
+
<title id="session-search-routing-title">session_search progressive search and scroll</title>
|
|
4
|
+
<desc id="session-search-routing-desc">Flowchart routing progressive keyword discovery to metadata results, then using each result path and matchMessageId for direct transcript scroll.</desc>
|
|
5
5
|
<defs>
|
|
6
6
|
<style>@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap');</style>
|
|
7
7
|
<style>
|
|
@@ -93,9 +93,9 @@
|
|
|
93
93
|
|
|
94
94
|
<rect x="576" y="400" width="200" height="96" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
|
|
95
95
|
<text class="node-name" x="676" y="428">Discovery</text>
|
|
96
|
-
<text class="node-sub" x="676" y="452">query · lazy sync + FTS5</text>
|
|
97
|
-
<text class="node-sub" x="676" y="468">
|
|
98
|
-
<text class="node-sub" x="676" y="484">
|
|
96
|
+
<text class="node-sub" x="676" y="452">keyword query · lazy sync + FTS5</text>
|
|
97
|
+
<text class="node-sub" x="676" y="468">metadata + snippets only</text>
|
|
98
|
+
<text class="node-sub" x="676" y="484">path + matchMessageId → SCROLL</text>
|
|
99
99
|
|
|
100
100
|
<rect x="328" y="544" width="192" height="96" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
|
|
101
101
|
<text class="node-name" x="424" y="572">Browse</text>
|
package/extensions/hydrate.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hydration: direct JSONL parsing of Pi session files (never SessionManager.open —
|
|
3
|
-
* it rewrites legacy files), branch-aware windows
|
|
3
|
+
* it rewrites legacy files), branch-aware windows, READ head/tail.
|
|
4
4
|
* Pure functions over file paths; no SQLite access.
|
|
5
5
|
*/
|
|
6
6
|
/// <reference types="node" />
|
|
@@ -9,13 +9,8 @@ import type { WindowMessage } from "./types.ts";
|
|
|
9
9
|
|
|
10
10
|
export interface WindowResult {
|
|
11
11
|
messages: WindowMessage[];
|
|
12
|
-
/** Every message on the resolved branch, root→tip chronological. Callers
|
|
13
|
-
* derive bookends from this instead of re-parsing the transcript. */
|
|
14
|
-
branchMessages: WindowMessage[];
|
|
15
12
|
messagesBefore: number;
|
|
16
13
|
messagesAfter: number;
|
|
17
|
-
/** Present only when discovery filtering removed at least one tool result. */
|
|
18
|
-
toolResultsOmitted?: true;
|
|
19
14
|
/** Tip of the branch the window was resolved on — pass back as branchTip to
|
|
20
15
|
* keep scrolling on this branch across forks. May be a non-message entry id. */
|
|
21
16
|
branchTip: string;
|
|
@@ -139,11 +134,7 @@ function branch(entriesById: Map<string, Entry>, entryId: string): Entry[] {
|
|
|
139
134
|
}
|
|
140
135
|
|
|
141
136
|
/** Deepest entry in file order whose ancestry contains tipId (tipId itself if none). */
|
|
142
|
-
function deepestDescendant(
|
|
143
|
-
entriesById: Map<string, Entry>,
|
|
144
|
-
entries: Entry[],
|
|
145
|
-
anchorId: string,
|
|
146
|
-
): string {
|
|
137
|
+
function deepestDescendant(entries: Entry[], anchorId: string): string {
|
|
147
138
|
const onBranch = new Set([anchorId]);
|
|
148
139
|
let tip = anchorId;
|
|
149
140
|
for (const e of entries) {
|
|
@@ -207,7 +198,7 @@ export function getWindow(
|
|
|
207
198
|
sessionPath: string,
|
|
208
199
|
anchorEntryId: string,
|
|
209
200
|
windowN: number,
|
|
210
|
-
opts?: { branchTip?: string
|
|
201
|
+
opts?: { branchTip?: string },
|
|
211
202
|
): WindowResult {
|
|
212
203
|
const n = Math.max(0, Math.min(50, windowN));
|
|
213
204
|
const entries = parseSessionEntries(sessionPath);
|
|
@@ -222,32 +213,24 @@ export function getWindow(
|
|
|
222
213
|
// Explicit branch selection: resolve the branch from its tip (deepest
|
|
223
214
|
// descendant), independent of where the window centers.
|
|
224
215
|
resolveMessageCursor(entriesById, opts.branchTip); // unknown tip → throw
|
|
225
|
-
tip = deepestDescendant(
|
|
216
|
+
tip = deepestDescendant(entries, opts.branchTip);
|
|
226
217
|
if (!branch(entriesById, tip).some((e) => e.id === messageAnchor.id)) {
|
|
227
218
|
throw new Error(`anchor entry ${anchorEntryId} is not on branch ${opts.branchTip}`);
|
|
228
219
|
}
|
|
229
220
|
} else {
|
|
230
|
-
tip = deepestDescendant(
|
|
221
|
+
tip = deepestDescendant(entries, anchorEntryId);
|
|
231
222
|
}
|
|
232
|
-
const
|
|
233
|
-
const toolResultsOmitted = opts?.userAssistantTextOnly && rawMessages.some((m) => m.role === "toolResult")
|
|
234
|
-
? true
|
|
235
|
-
: undefined;
|
|
236
|
-
const msgs = opts?.userAssistantTextOnly
|
|
237
|
-
? rawMessages.filter((m) => (m.role === "user" || m.role === "assistant") && m.content.length > 0)
|
|
238
|
-
: rawMessages;
|
|
223
|
+
const msgs = branchMessages(entriesById, tip);
|
|
239
224
|
const idx = msgs.findIndex((m) => m.entryId === anchorEntryIdMsg);
|
|
240
|
-
if (idx < 0) throw new Error(`anchor entry ${anchorEntryId} is not a
|
|
225
|
+
if (idx < 0) throw new Error(`anchor entry ${anchorEntryId} is not a message`);
|
|
241
226
|
const start = Math.max(0, idx - n);
|
|
242
227
|
const end = Math.min(msgs.length - 1, idx + n);
|
|
243
228
|
return {
|
|
244
|
-
branchMessages: msgs,
|
|
245
229
|
messages: msgs.slice(start, end + 1).map((m) =>
|
|
246
230
|
m.entryId === anchorEntryIdMsg ? { ...m, anchor: true } : m,
|
|
247
231
|
),
|
|
248
232
|
messagesBefore: idx,
|
|
249
233
|
messagesAfter: msgs.length - 1 - idx,
|
|
250
|
-
...(toolResultsOmitted ? { toolResultsOmitted } : {}),
|
|
251
234
|
branchTip: tip,
|
|
252
235
|
};
|
|
253
236
|
}
|
|
@@ -15,7 +15,7 @@ export const DEFAULT_SYNC_CAP = 50;
|
|
|
15
15
|
const MAX_SYNC_CAP = DEFAULT_SYNC_CAP * 10;
|
|
16
16
|
const MAX_TEXT_CHARS = 20000;
|
|
17
17
|
const SCAN_LIMIT = 300;
|
|
18
|
-
/** Best-ranked candidate retained per session after
|
|
18
|
+
/** Best-ranked candidate retained per session after SQL prefiltering. */
|
|
19
19
|
const ROWS_PER_FILE = 1;
|
|
20
20
|
|
|
21
21
|
const SCHEMA_SQL = `
|
|
@@ -519,19 +519,13 @@ function getBacklog(db: DatabaseSync): number {
|
|
|
519
519
|
|
|
520
520
|
export interface SearchOptions {
|
|
521
521
|
limit?: number;
|
|
522
|
-
/**
|
|
523
|
-
currentLiveEntryIds?: Set<string>;
|
|
524
|
-
/** Path of the current session file. */
|
|
522
|
+
/** Path of the current session file — the whole file is skipped. */
|
|
525
523
|
currentSessionPath?: string;
|
|
526
524
|
}
|
|
527
525
|
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
// live_filter is a per-connection TEMP table populated from SearchOptions.
|
|
532
|
-
const LIVE_FILTER_SQL = `NOT EXISTS (
|
|
533
|
-
SELECT 1 FROM live_filter lf WHERE lf.path = m.path AND lf.entry_id = m.entry_id
|
|
534
|
-
)`;
|
|
526
|
+
// Current-session exclusion happens INSIDE SQL (before ROW_NUMBER/caps), so
|
|
527
|
+
// the excluded file cannot consume the per-file rank, lineage, or scan limit.
|
|
528
|
+
const CURRENT_SESSION_FILTER_SQL = "(? IS NULL OR m.path <> ?)";
|
|
535
529
|
const LINEAGE_FILTER_SQL = `NOT EXISTS (
|
|
536
530
|
SELECT 1 FROM ranked parent
|
|
537
531
|
WHERE parent.path = r.parent_session
|
|
@@ -549,7 +543,7 @@ WITH matches AS (
|
|
|
549
543
|
bm25(session_fts) AS score
|
|
550
544
|
FROM session_fts JOIN messages m ON m.rowid = session_fts.rowid
|
|
551
545
|
LEFT JOIN sessions s ON s.path = m.path
|
|
552
|
-
WHERE session_fts MATCH ? AND ${
|
|
546
|
+
WHERE session_fts MATCH ? AND ${CURRENT_SESSION_FILTER_SQL}
|
|
553
547
|
), ranked AS (
|
|
554
548
|
SELECT *, ROW_NUMBER() OVER (PARTITION BY path ORDER BY score, rid) AS rn
|
|
555
549
|
FROM matches
|
|
@@ -587,22 +581,7 @@ export function searchIndex(
|
|
|
587
581
|
const limit = opts?.limit ?? 3;
|
|
588
582
|
const db = openDb(dbPath);
|
|
589
583
|
try {
|
|
590
|
-
|
|
591
|
-
// parameterized, immune to SQLite variable limits. Primary key indexes
|
|
592
|
-
// the correlated NOT EXISTS probe and dedupes inserts.
|
|
593
|
-
db.exec("CREATE TEMP TABLE IF NOT EXISTS live_filter (path TEXT NOT NULL, entry_id TEXT NOT NULL, PRIMARY KEY (path, entry_id))");
|
|
594
|
-
if (opts?.currentSessionPath && opts?.currentLiveEntryIds?.size) {
|
|
595
|
-
const ins = db.prepare("INSERT INTO live_filter(path, entry_id) VALUES (?, ?)");
|
|
596
|
-
db.exec("BEGIN");
|
|
597
|
-
try {
|
|
598
|
-
for (const id of opts.currentLiveEntryIds!) ins.run(opts.currentSessionPath!, id);
|
|
599
|
-
db.exec("COMMIT");
|
|
600
|
-
} catch (err) {
|
|
601
|
-
db.exec("ROLLBACK");
|
|
602
|
-
throw err;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
584
|
+
const currentSessionPath = opts?.currentSessionPath ?? null;
|
|
606
585
|
const plan = buildFtsQueryPlan(query);
|
|
607
586
|
if (plan.ftsCandidates.length === 0 && !plan.forceLike) {
|
|
608
587
|
return { hits: [], backlogRemaining: getBacklog(db) };
|
|
@@ -628,7 +607,7 @@ export function searchIndex(
|
|
|
628
607
|
let parseFailed = false;
|
|
629
608
|
for (const cand of plan.ftsCandidates) {
|
|
630
609
|
try {
|
|
631
|
-
rows = db.prepare(BASE_SELECT).all(cand) as any;
|
|
610
|
+
rows = db.prepare(BASE_SELECT).all(cand, currentSessionPath, currentSessionPath) as any;
|
|
632
611
|
// First/raw success defines the result even with zero rows. After a
|
|
633
612
|
// parse error, an empty recovery candidate keeps trying later ones.
|
|
634
613
|
if (!parseFailed || rows.length > 0) {
|
|
@@ -652,21 +631,21 @@ export function searchIndex(
|
|
|
652
631
|
ROW_NUMBER() OVER (PARTITION BY m.path ORDER BY m.rowid DESC) AS rn,
|
|
653
632
|
COUNT(*) OVER (PARTITION BY m.path) AS matches
|
|
654
633
|
FROM messages m LEFT JOIN sessions s ON s.path = m.path
|
|
655
|
-
WHERE ${likePlan.where} AND ${
|
|
634
|
+
WHERE ${likePlan.where} AND ${CURRENT_SESSION_FILTER_SQL}
|
|
656
635
|
)
|
|
657
636
|
SELECT path, entry_id, role, timestamp, head, tail, cwd, name, started_at
|
|
658
637
|
FROM ranked r
|
|
659
638
|
WHERE rn <= ${ROWS_PER_FILE}
|
|
660
639
|
AND ${LINEAGE_FILTER_SQL}
|
|
661
640
|
ORDER BY matches DESC, started_at DESC, path
|
|
662
|
-
LIMIT ${SCAN_LIMIT}`).all(...likePlan.params) as any;
|
|
641
|
+
LIMIT ${SCAN_LIMIT}`).all(...likePlan.params, currentSessionPath, currentSessionPath) as any;
|
|
663
642
|
// Snippets anchor on operand terms only — operator words like OR would
|
|
664
643
|
// otherwise match common substrings and hide the real match.
|
|
665
644
|
for (const r of rows as any[]) r.snip = likeSnippet((r as any).head ?? "", (r as any).tail ?? "", likePlan.terms);
|
|
666
645
|
}
|
|
667
646
|
|
|
668
|
-
//
|
|
669
|
-
// before the scan limit, so fork rows cannot starve
|
|
647
|
+
// Current-file and one-hop lineage suppression already happened in SQL,
|
|
648
|
+
// before the scan limit, so excluded or fork rows cannot starve matches.
|
|
670
649
|
const seenFiles = new Set<string>();
|
|
671
650
|
const hits: SearchHit[] = [];
|
|
672
651
|
let rankCounter = 0;
|
|
@@ -266,16 +266,15 @@ interface ToolParams {
|
|
|
266
266
|
branchTip?: string;
|
|
267
267
|
window?: number;
|
|
268
268
|
limit?: number;
|
|
269
|
-
detail?: "adaptive" | "full";
|
|
270
269
|
}
|
|
271
270
|
|
|
272
|
-
const DESCRIPTION = `Search past Pi sessions locally with FTS5; returns
|
|
271
|
+
const DESCRIPTION = `Search past Pi sessions locally with FTS5; returns indexed metadata and snippets.
|
|
273
272
|
|
|
274
273
|
- \`operation: "prepare-pattern-miner"\` + \`scope\`: prepare one bounded corpus and repository inventory.
|
|
275
274
|
- \`query\`: discover matches. Prefer distinctive identifiers or uncommon terms; multi-word queries are AND. Use \`OR\`/\`NOT\` for Boolean queries and quotes only when exact wording is known.
|
|
276
275
|
- \`sessionId\` + \`aroundMessageId\`: scroll ±\`window\`; retain \`branchTip\` across forks.
|
|
277
276
|
- \`sessionId\` alone: read; no args: browse recent sessions.
|
|
278
|
-
- Discovery
|
|
277
|
+
- Discovery returns metadata and snippets. Use a result's \`path\` and \`matchMessageId\` for a follow-up scroll.`;
|
|
279
278
|
|
|
280
279
|
export default function (pi: ExtensionAPI): void {
|
|
281
280
|
// Best-effort sync at startup, deferred so the synchronous walk + SQLite
|
|
@@ -307,8 +306,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
307
306
|
branchTip: Type.Optional(Type.String({ description: "Branch tip entry id from a previous response — selects which branch of a forked session to scroll; aroundMessageId must lie on it." })),
|
|
308
307
|
window: Type.Optional(Type.Number({ description: "Scroll window radius, [1,20], default 5." })),
|
|
309
308
|
limit: Type.Optional(Type.Number({ description: "Max results, [1,10]. Defaults to 10 for preparation and 3 otherwise." })),
|
|
310
|
-
|
|
311
|
-
}),
|
|
309
|
+
}, { additionalProperties: false }),
|
|
312
310
|
renderResult(result, { expanded }, theme) {
|
|
313
311
|
const output = result.content.find((part) => part.type === "text")?.text ?? "";
|
|
314
312
|
const styledOutput = theme.fg("toolOutput", output);
|
|
@@ -333,7 +331,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
333
331
|
throw new Error("scope requires operation: prepare-pattern-miner.");
|
|
334
332
|
}
|
|
335
333
|
if (rawParams.operation === "prepare-pattern-miner") {
|
|
336
|
-
const incompatible = (["query", "sessionId", "aroundMessageId", "branchTip", "window"
|
|
334
|
+
const incompatible = (["query", "sessionId", "aroundMessageId", "branchTip", "window"] as const)
|
|
337
335
|
.filter((key) => rawParams[key] !== undefined);
|
|
338
336
|
if (incompatible.length > 0) {
|
|
339
337
|
throw new Error(`prepare-pattern-miner does not accept: ${incompatible.join(", ")}.`);
|
|
@@ -365,7 +363,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
365
363
|
branchTip: rawParams.branchTip != null ? String(rawParams.branchTip) : undefined,
|
|
366
364
|
window: rawParams.window,
|
|
367
365
|
limit: rawParams.limit,
|
|
368
|
-
detail: rawParams.detail,
|
|
369
366
|
};
|
|
370
367
|
let sessionId = params.sessionId?.trim() || undefined;
|
|
371
368
|
const anchor = params.aroundMessageId?.trim() || undefined;
|
|
@@ -445,135 +442,40 @@ export default function (pi: ExtensionAPI): void {
|
|
|
445
442
|
|
|
446
443
|
// --- DISCOVERY ---
|
|
447
444
|
const limit = clamp(params.limit, 1, 10, 3);
|
|
448
|
-
const full = params.detail === "full";
|
|
449
445
|
|
|
450
|
-
//
|
|
451
|
-
|
|
446
|
+
// Exclude the whole current file when the session manager provides it.
|
|
447
|
+
// A missing or failing manager leaves discovery usable without exclusion.
|
|
452
448
|
let currentSessionPath: string | undefined;
|
|
453
449
|
try {
|
|
454
|
-
currentSessionPath = ctx.sessionManager.getSessionFile();
|
|
455
|
-
liveIds = new Set(
|
|
456
|
-
ctx.sessionManager
|
|
457
|
-
.buildContextEntries()
|
|
458
|
-
.filter((e) => e.type === "message")
|
|
459
|
-
.map((e) => e.id),
|
|
460
|
-
);
|
|
450
|
+
currentSessionPath = ctx.sessionManager.getSessionFile() ?? undefined;
|
|
461
451
|
} catch {
|
|
462
|
-
// Guard unavailable →
|
|
452
|
+
// Guard unavailable → continue without exclusion.
|
|
463
453
|
}
|
|
464
454
|
|
|
465
455
|
const { hits, backlogRemaining } = searchIndex(dbPath(), params.query, {
|
|
466
456
|
limit,
|
|
467
|
-
currentLiveEntryIds: liveIds,
|
|
468
457
|
currentSessionPath,
|
|
469
458
|
});
|
|
470
459
|
|
|
471
460
|
const resultQuery = params.query!.trim().slice(0, MAX_QUERY_CHARS);
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
461
|
+
const results = hits.map((hit) => ({
|
|
462
|
+
path: hit.path,
|
|
463
|
+
snippet: hit.snippet,
|
|
464
|
+
rank: hit.rank,
|
|
465
|
+
matchMessageId: hit.entryId,
|
|
466
|
+
role: hit.role,
|
|
467
|
+
timestamp: hit.timestamp,
|
|
468
|
+
cwd: hit.cwd,
|
|
469
|
+
name: hit.name,
|
|
470
|
+
startedAt: hit.startedAt,
|
|
471
|
+
}));
|
|
472
|
+
return textResult({
|
|
478
473
|
mode: "discovery",
|
|
479
474
|
query: resultQuery,
|
|
480
|
-
results
|
|
475
|
+
results,
|
|
481
476
|
backlogRemaining,
|
|
482
477
|
...(syncWarning ? { syncWarning } : {}),
|
|
483
|
-
};
|
|
484
|
-
let used = JSON.stringify(envelope).length + Math.max(0, hits.length - 1);
|
|
485
|
-
const results = hits.map((hit, index) => {
|
|
486
|
-
const remaining = Math.floor((OUTPUT_CHAR_BUDGET - used) / (hits.length - index));
|
|
487
|
-
const meta = {
|
|
488
|
-
path: hit.path,
|
|
489
|
-
snippet: hit.snippet,
|
|
490
|
-
rank: hit.rank,
|
|
491
|
-
matchMessageId: hit.entryId,
|
|
492
|
-
role: hit.role,
|
|
493
|
-
timestamp: hit.timestamp,
|
|
494
|
-
cwd: hit.cwd,
|
|
495
|
-
name: hit.name,
|
|
496
|
-
startedAt: hit.startedAt,
|
|
497
|
-
};
|
|
498
|
-
const hydrateFull = full || hit.rank === 0;
|
|
499
|
-
// Every hit is sized against the cumulative remaining budget: keep
|
|
500
|
-
// as-is when it fits, else truncate to the largest uniform cap that
|
|
501
|
-
// fits across messages and bookends, else metadata-only.
|
|
502
|
-
// contentTruncated signals either case.
|
|
503
|
-
const fitOrTruncate = (
|
|
504
|
-
hitObj: Record<string, unknown>,
|
|
505
|
-
messages: WindowMessage[],
|
|
506
|
-
bookends?: { start: WindowMessage[]; end: WindowMessage[] },
|
|
507
|
-
): Record<string, unknown> => {
|
|
508
|
-
const out: Record<string, unknown> = { ...hitObj };
|
|
509
|
-
if (JSON.stringify(out).length > remaining) {
|
|
510
|
-
const pools = bookends ? [messages, bookends.start, bookends.end] : [messages];
|
|
511
|
-
const maxLen = Math.max(...pools.flatMap((a) => a.map((m) => m.content.length)), 0);
|
|
512
|
-
Object.assign(
|
|
513
|
-
out,
|
|
514
|
-
boundContent(
|
|
515
|
-
(cap) => ({
|
|
516
|
-
...hitObj,
|
|
517
|
-
messages: cap === null ? [] : truncateContent(messages, cap),
|
|
518
|
-
...(bookends && cap !== null
|
|
519
|
-
? { bookends: { start: truncateContent(bookends.start, cap), end: truncateContent(bookends.end, cap) } }
|
|
520
|
-
: bookends
|
|
521
|
-
? { bookends: { start: [], end: [] } }
|
|
522
|
-
: {}),
|
|
523
|
-
contentTruncated: true,
|
|
524
|
-
}),
|
|
525
|
-
maxLen,
|
|
526
|
-
remaining,
|
|
527
|
-
),
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
used += JSON.stringify(out).length;
|
|
531
|
-
return out;
|
|
532
|
-
};
|
|
533
|
-
const hydrationFallback = (error: unknown) =>
|
|
534
|
-
fitOrTruncate(
|
|
535
|
-
{ ...meta, detail: hydrateFull ? "full" : "compact", messages: [], bookends: { start: [], end: [] }, messagesBefore: 0, messagesAfter: 0, error: (error instanceof Error ? error.message : String(error)).slice(0, 512) },
|
|
536
|
-
[],
|
|
537
|
-
);
|
|
538
|
-
if (!hydrateFull) {
|
|
539
|
-
// Compact hits still carry the matched anchor message.
|
|
540
|
-
try {
|
|
541
|
-
const win = getWindow(hit.path, hit.entryId, 0, { userAssistantTextOnly: true });
|
|
542
|
-
// Mark when the fixed compact cap already removed content, so a
|
|
543
|
-
// hit that still fits the budget isn't mistaken for complete.
|
|
544
|
-
const overCompactCap = win.messages.some((m) => m.content.length > 2000);
|
|
545
|
-
return fitOrTruncate({ ...meta, detail: "compact", ...(overCompactCap ? { contentTruncated: true } : {}), ...(win.toolResultsOmitted ? { toolResultsOmitted: true } : {}), messages: truncateContent(win.messages, 2000), bookends: { start: [], end: [] }, messagesBefore: win.messagesBefore, messagesAfter: win.messagesAfter }, win.messages);
|
|
546
|
-
} catch (error) {
|
|
547
|
-
return hydrationFallback(error);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
try {
|
|
551
|
-
// One bounded snapshot feeds both window and branch bookends.
|
|
552
|
-
const win = getWindow(hit.path, hit.entryId, 5, full ? undefined : { userAssistantTextOnly: true });
|
|
553
|
-
// Same branch as the anchor — following the file's final leaf
|
|
554
|
-
// would attach unrelated sibling messages.
|
|
555
|
-
const bookends = { start: win.branchMessages.slice(0, 3), end: win.branchMessages.slice(-3) };
|
|
556
|
-
return fitOrTruncate(
|
|
557
|
-
{
|
|
558
|
-
...meta,
|
|
559
|
-
detail: "full" as const,
|
|
560
|
-
...(win.toolResultsOmitted ? { toolResultsOmitted: true } : {}),
|
|
561
|
-
messages: win.messages,
|
|
562
|
-
bookends,
|
|
563
|
-
messagesBefore: win.messagesBefore,
|
|
564
|
-
messagesAfter: win.messagesAfter,
|
|
565
|
-
},
|
|
566
|
-
win.messages,
|
|
567
|
-
bookends,
|
|
568
|
-
);
|
|
569
|
-
} catch (error) {
|
|
570
|
-
// Session file unreadable/moved since indexing → anchor-only.
|
|
571
|
-
return hydrationFallback(error);
|
|
572
|
-
}
|
|
573
478
|
});
|
|
574
|
-
|
|
575
|
-
const result: Record<string, unknown> = { ...envelope, results };
|
|
576
|
-
return textResult(result);
|
|
577
479
|
} catch (error) {
|
|
578
480
|
const message = error instanceof Error ? error.message : String(error);
|
|
579
481
|
return textResult({ success: false, error: message });
|
package/extensions/types.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface WindowMessage {
|
|
|
9
9
|
role: string;
|
|
10
10
|
content: string;
|
|
11
11
|
timestamp: string;
|
|
12
|
-
/** True when this message is the anchor of a scroll
|
|
12
|
+
/** True when this message is the anchor of a scroll window. */
|
|
13
13
|
anchor?: boolean;
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -28,7 +28,7 @@ export interface PreparationSessionRow extends SessionRow {
|
|
|
28
28
|
lineageId: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/** One FTS discovery hit
|
|
31
|
+
/** One metadata-only FTS discovery hit. */
|
|
32
32
|
export interface SearchHit {
|
|
33
33
|
path: string;
|
|
34
34
|
entryId: string;
|
package/package.json
CHANGED
|
@@ -1,57 +1,62 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
2
|
+
"name": "@henryqw/pi-session-recall",
|
|
3
|
+
"version": "4.0.2",
|
|
4
|
+
"description": "Local FTS5 search over past Pi sessions plus a skill for turning recurring work into deterministic automation.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"session-recall",
|
|
9
|
+
"fts5",
|
|
10
|
+
"recall"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22.19.0"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"files": [
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"extensions",
|
|
20
|
+
"skills",
|
|
21
|
+
"docs",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node --test test/*.test.ts",
|
|
26
|
+
"typecheck": "tsc --noEmit --allowImportingTsExtensions --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck extensions/*.ts test/*.test.ts",
|
|
27
|
+
"pack:check": "npm pack --dry-run"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@earendil-works/pi-ai": ">=0.86.0 <0.88.0",
|
|
31
|
+
"@earendil-works/pi-coding-agent": ">=0.86.0 <0.88.0",
|
|
32
|
+
"@earendil-works/pi-tui": ">=0.86.0 <0.88.0",
|
|
33
|
+
"typebox": "^1.3.15"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/HenryQW/pi-harness.git",
|
|
38
|
+
"directory": "extensions/pi-session-recall"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/HenryQW/pi-harness/issues"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"pi": {
|
|
47
|
+
"extensions": [
|
|
48
|
+
"./extensions/session-recall.ts"
|
|
49
|
+
],
|
|
50
|
+
"skills": [
|
|
51
|
+
"./skills"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@henryqw/pi-config-store": "^2.0.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@earendil-works/pi-ai": "0.87.0",
|
|
59
|
+
"@earendil-works/pi-coding-agent": "0.87.0",
|
|
60
|
+
"@earendil-works/pi-tui": "0.87.0"
|
|
61
|
+
}
|
|
57
62
|
}
|