@henryqw/pi-session-recall 2.1.0 → 4.0.1

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
@@ -14,13 +14,13 @@ pi install npm:@henryqw/pi-session-recall
14
14
 
15
15
  ## Use
16
16
 
17
- Start discovery with a distinctive query:
17
+ Start a progressive search with a distinctive keyword query:
18
18
 
19
19
  ```json
20
20
  { "query": "database migration rollback" }
21
21
  ```
22
22
 
23
- `session_search` returns ranked sessions. The top result includes nearby messages and session bookends.
23
+ Discovery returns ranked metadata and snippets. Use a result's `path` and `matchMessageId` to scroll for the relevant messages.
24
24
 
25
25
  Prepare a repository-scoped pattern-mining sample with one call:
26
26
 
@@ -34,12 +34,12 @@ Prepare a repository-scoped pattern-mining sample with one call:
34
34
 
35
35
  Use `scope:"all"` for cross-repository work. It still returns the corpus when repository inventory is unavailable.
36
36
 
37
- Use IDs from a discovery result to ask for more context:
37
+ Use the discovery result's `path` and `matchMessageId` for a follow-up scroll:
38
38
 
39
39
  ```json
40
40
  {
41
- "sessionId": "<returned sessionId>",
42
- "aroundMessageId": "<returned message entryId>",
41
+ "sessionId": "<result path>",
42
+ "aroundMessageId": "<result matchMessageId>",
43
43
  "window": 10
44
44
  }
45
45
  ```
@@ -51,12 +51,12 @@ The follow-up returns up to ten messages before and after that anchor on the sel
51
51
  | `session_search` | tool | Search, inspect, or prepare a bounded mining corpus from past sessions. |
52
52
  | `pi-session-pattern-miner` | skill | Find repeated work and choose the smallest useful automation. |
53
53
 
54
- BM25 is a text-ranking method. Hydrated results include messages read from saved session files.
54
+ BM25 is a text-ranking method. Discovery returns index metadata; READ and SCROLL retrieve messages from saved session files.
55
55
 
56
56
  | Mode | Call | Result |
57
57
  | --- | --- | --- |
58
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. |
59
+ | 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. |
60
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
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
62
  | Browse | no args | Recent sessions with path, name, cwd, started date, and preview. |
@@ -98,6 +98,8 @@ Inspect the current candidate files before assigning ownership. Abstain if targe
98
98
 
99
99
  ## Flow
100
100
 
101
+ ![Flowchart showing progressive keyword discovery returning path and matchMessageId for direct transcript scroll.](./docs/session-search-routing.svg)
102
+
101
103
  Search makes no model calls.
102
104
 
103
105
  ### Query and index
@@ -110,7 +112,7 @@ Search makes no model calls.
110
112
 
111
113
  ### Context and sync
112
114
 
113
- Hits inside the current session's live context are suppressed. Compacted-away or inactive-branch history stays discoverable. Forked sessions collapse into their parent when both match.
115
+ Discovery excludes the current session file when its path is available. Forked sessions collapse into their parent when both match.
114
116
 
115
117
  Before browse, discovery, or pattern preparation, the extension lazily syncs the index from the session tree.
116
118
 
@@ -118,9 +120,9 @@ Pattern preparation reports `sync.walkComplete`, `sync.backlogRemaining`, and `s
118
120
 
119
121
  ### Retrieval safety
120
122
 
121
- Adaptive discovery leaves tool-result messages out of returned context. Use `detail:"full"`, READ, or SCROLL when you explicitly need them.
123
+ 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.
122
124
 
123
- Pattern preparation returns only non-empty user and assistant text. It never returns thinking blocks or tool-result content. Each session keeps citation and lineage metadata even when its hydration fails.
125
+ Pattern preparation returns only non-empty user and assistant text. It never returns thinking blocks or tool-result content. Preparation keeps citation and lineage metadata when a source cannot be read.
124
126
 
125
127
  Historical tool output may contain secrets or other sensitive data. Raw retrieval places that output in active model context.
126
128
 
@@ -137,7 +139,7 @@ The index and transcript reads stay local. Transcripts are read in place. Return
137
139
  Pin the previous release:
138
140
 
139
141
  ```bash
140
- pi install npm:@henryqw/pi-session-recall@2.0.0
142
+ pi install npm:@henryqw/pi-session-recall@2.1.1
141
143
  ```
142
144
 
143
145
  No index migration or cleanup is needed.
@@ -154,7 +156,7 @@ Session directories whose encoded path starts with `--tmp-` or `--private-tmp-`
154
156
 
155
157
  Session files over 32 MiB are excluded from indexing and hydration. Discovery cannot newly find them.
156
158
 
157
- 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.
159
+ READ and SCROLL return an explicit size error. Discovery returns the indexed metadata even when a source file changes afterward.
158
160
 
159
161
  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.
160
162
 
@@ -162,7 +164,7 @@ Repository scope fails outside Git or when repository inventory fails. All scope
162
164
 
163
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.
164
166
 
165
- Preparation rejects `query`, session cursors, `window`, or `detail` in the same call. It also rejects `scope` without the operation.
167
+ Preparation rejects `query`, session cursors, or `window` in the same call. It also rejects `scope` without the operation.
166
168
 
167
169
  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.
168
170
 
@@ -0,0 +1,138 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>session_search routing</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;family=Geist:wght@400;500;600&amp;family=Geist+Mono:wght@400;500;600&amp;display=swap" rel="stylesheet">
8
+ <style>
9
+ html, body { min-height: 100%; margin: 0; background: #f0eee9; }
10
+ body { display: grid; place-items: center; }
11
+ figure { width: min(100%, 1280px); margin: 0; }
12
+ svg { display: block; width: 100%; height: auto; }
13
+ </style>
14
+ </head>
15
+ <body>
16
+ <figure>
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 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
+ <defs>
21
+ <style>
22
+ .eyebrow { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; letter-spacing: .16em; }
23
+ .diagram-title { fill: #101828; font: 400 28px 'Instrument Serif', serif; }
24
+ .zone-label { fill: #4c5665; font: 500 8px 'Geist Mono', monospace; letter-spacing: .12em; }
25
+ .zone-note { fill: #6a7282; font: 400 8px 'Geist Mono', monospace; letter-spacing: .04em; }
26
+ .node-name { fill: #101828; font: 600 12px 'Geist', sans-serif; text-anchor: middle; }
27
+ .technical-name { fill: #101828; font: 500 12px 'Geist Mono', monospace; text-anchor: middle; }
28
+ .node-sub { fill: #4c5665; font: 400 8px 'Geist Mono', monospace; text-anchor: middle; }
29
+ .edge { fill: none; stroke: #4c5665; stroke-width: 1.2; marker-end: url(#session-search-routing-arrow); }
30
+ .edge-label { fill: #4c5665; font: 500 8px 'Geist Mono', monospace; text-anchor: middle; letter-spacing: .08em; }
31
+ </style>
32
+ <marker id="session-search-routing-arrow" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
33
+ <polygon points="0 0,8 4,0 8" fill="#4c5665"/>
34
+ </marker>
35
+ <marker id="session-search-routing-arrow-accent" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
36
+ <polygon points="0 0,8 4,0 8" fill="#1d4ed8"/>
37
+ </marker>
38
+ <marker id="session-search-routing-arrow-link" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
39
+ <polygon points="0 0,8 4,0 8" fill="#1d4ed8"/>
40
+ </marker>
41
+ </defs>
42
+
43
+ <rect width="1280" height="720" fill="#f0eee9"/>
44
+
45
+ <text class="eyebrow" x="40" y="48">PI-SESSION-RECALL · FLOWCHART</text>
46
+ <text class="diagram-title" x="40" y="88">Five routes through session_search</text>
47
+
48
+ <!-- Routing zones are backgrounds, not primary nodes. -->
49
+ <rect x="40" y="352" width="752" height="320" rx="8" fill="#e6e4de" stroke="rgba(16,24,40,0.14)" stroke-width="1"/>
50
+ <text class="zone-label" x="56" y="376">INDEX-BACKED CALLS · LAZY SYNC</text>
51
+ <text class="zone-note" x="56" y="392">PREPARE · DISCOVERY · BROWSE</text>
52
+
53
+ <rect x="816" y="352" width="424" height="320" rx="8" fill="#e6e4de" stroke="rgba(16,24,40,0.14)" stroke-width="1"/>
54
+ <text class="zone-label" x="832" y="376">DIRECT TRANSCRIPT READS</text>
55
+ <text class="zone-note" x="832" y="392">RAW ROLES · TOOL RESULTS</text>
56
+
57
+ <!-- Connectors: all routes are painted before their nodes. -->
58
+ <path class="edge" d="M640 180 V212"/>
59
+ <path class="edge" d="M532 264 H320 Q312 264 312 272 V388 Q312 396 304 396 H184 Q176 396 176 404 V412"/>
60
+ <path class="edge" d="M640 316 V336 Q640 344 632 344 H432 Q424 344 424 352 V392"/>
61
+ <path class="edge" d="M748 264 H792 Q800 264 800 272 V328 Q800 336 808 336 H1016 Q1024 336 1024 344 V392"/>
62
+ <path class="edge" d="M536 448 H576"/>
63
+ <path class="edge" d="M424 504 V544"/>
64
+ <path class="edge" d="M1136 448 V536"/>
65
+ <path class="edge" d="M912 448 H896 Q888 448 888 456 V520 Q888 528 896 528 H912 Q920 528 920 536"/>
66
+
67
+ <!-- Branch labels stay 8px clear of their connectors. -->
68
+ <rect x="384" y="244" width="72" height="12" rx="2" fill="#f0eee9"/>
69
+ <text class="edge-label" x="420" y="252">PREPARE</text>
70
+
71
+ <rect x="488" y="324" width="112" height="12" rx="2" fill="#f0eee9"/>
72
+ <text class="edge-label" x="544" y="332">SEARCH / BROWSE</text>
73
+
74
+ <rect x="808" y="288" width="56" height="12" rx="2" fill="#f0eee9"/>
75
+ <text class="edge-label" x="836" y="296">INSPECT</text>
76
+
77
+ <rect x="544" y="428" width="24" height="12" rx="2" fill="#f0eee9"/>
78
+ <text class="edge-label" x="556" y="436">YES</text>
79
+
80
+ <rect x="432" y="516" width="24" height="12" rx="2" fill="#f0eee9"/>
81
+ <text class="edge-label" x="444" y="524">NO</text>
82
+
83
+ <rect x="1144" y="480" width="24" height="12" rx="2" fill="#f0eee9"/>
84
+ <text class="edge-label" x="1156" y="488">YES</text>
85
+
86
+ <rect x="896" y="480" width="24" height="12" rx="2" fill="#f0eee9"/>
87
+ <text class="edge-label" x="908" y="488">NO</text>
88
+
89
+ <!-- Primary nodes: one start, three decisions, and five terminal modes. -->
90
+ <rect x="540" y="132" width="200" height="48" rx="20" fill="rgba(16,24,40,0.03)" stroke="#4c5665" stroke-width="1"/>
91
+ <text class="technical-name" x="640" y="156">session_search</text>
92
+ <text class="node-sub" x="640" y="172">ONE LOCAL TOOL</text>
93
+
94
+ <polygon points="640,212 748,264 640,316 532,264" fill="rgba(29,78,216,0.08)" stroke="#1d4ed8" stroke-width="1.2"/>
95
+ <text class="node-name" x="640" y="260">Call family?</text>
96
+ <text class="node-sub" x="640" y="280">ROUTE BY ARGUMENTS</text>
97
+
98
+ <rect x="48" y="412" width="240" height="128" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
99
+ <text class="node-name" x="168" y="444">Pattern preparation</text>
100
+ <text class="node-sub" x="168" y="472">operation + scope · lazy sync</text>
101
+ <text class="node-sub" x="168" y="488">bounded user/assistant corpus</text>
102
+ <text class="node-sub" x="168" y="504">+ repository inventory · never tool results</text>
103
+
104
+ <polygon points="424,392 536,448 424,504 312,448" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
105
+ <text class="technical-name" x="424" y="444">query?</text>
106
+ <text class="node-sub" x="424" y="464">PRESENT</text>
107
+
108
+ <rect x="576" y="400" width="200" height="96" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
109
+ <text class="node-name" x="676" y="428">Discovery</text>
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
+
114
+ <rect x="328" y="544" width="192" height="96" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
115
+ <text class="node-name" x="424" y="572">Browse</text>
116
+ <text class="node-sub" x="424" y="596">no args · lazy sync</text>
117
+ <text class="node-sub" x="424" y="612">recent session metadata</text>
118
+
119
+ <polygon points="1024,392 1136,448 1024,504 912,448" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
120
+ <text class="technical-name" x="1024" y="444">aroundMessageId?</text>
121
+ <text class="node-sub" x="1024" y="464">WITH sessionId</text>
122
+
123
+ <rect x="1040" y="536" width="192" height="112" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
124
+ <text class="node-name" x="1136" y="564">Scroll</text>
125
+ <text class="node-sub" x="1136" y="588">sessionId + aroundMessageId</text>
126
+ <text class="node-sub" x="1136" y="604">direct saved JSONL</text>
127
+ <text class="node-sub" x="1136" y="620">raw roles including tool results</text>
128
+ <text class="node-sub" x="1136" y="636">±window with branchTip</text>
129
+
130
+ <rect x="824" y="536" width="192" height="112" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
131
+ <text class="node-name" x="920" y="564">Read</text>
132
+ <text class="node-sub" x="920" y="588">sessionId · direct saved JSONL</text>
133
+ <text class="node-sub" x="920" y="604">raw roles including tool results</text>
134
+ <text class="node-sub" x="920" y="620">head 20 + tail 10</text>
135
+ </svg>
136
+ </figure>
137
+ </body>
138
+ </html>
@@ -0,0 +1,121 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
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 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
+ <defs>
6
+ <style>@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;family=Geist:wght@400;500;600&amp;family=Geist+Mono:wght@400;500;600&amp;display=swap');</style>
7
+ <style>
8
+ .eyebrow { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; letter-spacing: .16em; }
9
+ .diagram-title { fill: #101828; font: 400 28px 'Instrument Serif', serif; }
10
+ .zone-label { fill: #4c5665; font: 500 8px 'Geist Mono', monospace; letter-spacing: .12em; }
11
+ .zone-note { fill: #6a7282; font: 400 8px 'Geist Mono', monospace; letter-spacing: .04em; }
12
+ .node-name { fill: #101828; font: 600 12px 'Geist', sans-serif; text-anchor: middle; }
13
+ .technical-name { fill: #101828; font: 500 12px 'Geist Mono', monospace; text-anchor: middle; }
14
+ .node-sub { fill: #4c5665; font: 400 8px 'Geist Mono', monospace; text-anchor: middle; }
15
+ .edge { fill: none; stroke: #4c5665; stroke-width: 1.2; marker-end: url(#session-search-routing-arrow); }
16
+ .edge-label { fill: #4c5665; font: 500 8px 'Geist Mono', monospace; text-anchor: middle; letter-spacing: .08em; }
17
+ </style>
18
+ <marker id="session-search-routing-arrow" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
19
+ <polygon points="0 0,8 4,0 8" fill="#4c5665"/>
20
+ </marker>
21
+ <marker id="session-search-routing-arrow-accent" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
22
+ <polygon points="0 0,8 4,0 8" fill="#1d4ed8"/>
23
+ </marker>
24
+ <marker id="session-search-routing-arrow-link" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
25
+ <polygon points="0 0,8 4,0 8" fill="#1d4ed8"/>
26
+ </marker>
27
+ </defs>
28
+
29
+ <rect width="1280" height="720" fill="#f0eee9"/>
30
+
31
+ <text class="eyebrow" x="40" y="48">PI-SESSION-RECALL · FLOWCHART</text>
32
+ <text class="diagram-title" x="40" y="88">Five routes through session_search</text>
33
+
34
+ <!-- Routing zones are backgrounds, not primary nodes. -->
35
+ <rect x="40" y="352" width="752" height="320" rx="8" fill="#e6e4de" stroke="#101828" stroke-opacity="0.14" stroke-width="1"/>
36
+ <text class="zone-label" x="56" y="376">INDEX-BACKED CALLS · LAZY SYNC</text>
37
+ <text class="zone-note" x="56" y="392">PREPARE · DISCOVERY · BROWSE</text>
38
+
39
+ <rect x="816" y="352" width="424" height="320" rx="8" fill="#e6e4de" stroke="#101828" stroke-opacity="0.14" stroke-width="1"/>
40
+ <text class="zone-label" x="832" y="376">DIRECT TRANSCRIPT READS</text>
41
+ <text class="zone-note" x="832" y="392">RAW ROLES · TOOL RESULTS</text>
42
+
43
+ <!-- Connectors: all routes are painted before their nodes. -->
44
+ <path class="edge" d="M640 180 V212"/>
45
+ <path class="edge" d="M532 264 H320 Q312 264 312 272 V388 Q312 396 304 396 H184 Q176 396 176 404 V412"/>
46
+ <path class="edge" d="M640 316 V336 Q640 344 632 344 H432 Q424 344 424 352 V392"/>
47
+ <path class="edge" d="M748 264 H792 Q800 264 800 272 V328 Q800 336 808 336 H1016 Q1024 336 1024 344 V392"/>
48
+ <path class="edge" d="M536 448 H576"/>
49
+ <path class="edge" d="M424 504 V544"/>
50
+ <path class="edge" d="M1136 448 V536"/>
51
+ <path class="edge" d="M912 448 H896 Q888 448 888 456 V520 Q888 528 896 528 H912 Q920 528 920 536"/>
52
+
53
+ <!-- Branch labels stay 8px clear of their connectors. -->
54
+ <rect x="384" y="244" width="72" height="12" rx="2" fill="#f0eee9"/>
55
+ <text class="edge-label" x="420" y="252">PREPARE</text>
56
+
57
+ <rect x="488" y="324" width="112" height="12" rx="2" fill="#f0eee9"/>
58
+ <text class="edge-label" x="544" y="332">SEARCH / BROWSE</text>
59
+
60
+ <rect x="808" y="288" width="56" height="12" rx="2" fill="#f0eee9"/>
61
+ <text class="edge-label" x="836" y="296">INSPECT</text>
62
+
63
+ <rect x="544" y="428" width="24" height="12" rx="2" fill="#f0eee9"/>
64
+ <text class="edge-label" x="556" y="436">YES</text>
65
+
66
+ <rect x="432" y="516" width="24" height="12" rx="2" fill="#f0eee9"/>
67
+ <text class="edge-label" x="444" y="524">NO</text>
68
+
69
+ <rect x="1144" y="480" width="24" height="12" rx="2" fill="#f0eee9"/>
70
+ <text class="edge-label" x="1156" y="488">YES</text>
71
+
72
+ <rect x="896" y="480" width="24" height="12" rx="2" fill="#f0eee9"/>
73
+ <text class="edge-label" x="908" y="488">NO</text>
74
+
75
+ <!-- Primary nodes: one start, three decisions, and five terminal modes. -->
76
+ <rect x="540" y="132" width="200" height="48" rx="20" fill="#101828" fill-opacity="0.03" stroke="#4c5665" stroke-width="1"/>
77
+ <text class="technical-name" x="640" y="156">session_search</text>
78
+ <text class="node-sub" x="640" y="172">ONE LOCAL TOOL</text>
79
+
80
+ <polygon points="640,212 748,264 640,316 532,264" fill="#1d4ed8" fill-opacity="0.08" stroke="#1d4ed8" stroke-width="1.2"/>
81
+ <text class="node-name" x="640" y="260">Call family?</text>
82
+ <text class="node-sub" x="640" y="280">ROUTE BY ARGUMENTS</text>
83
+
84
+ <rect x="48" y="412" width="240" height="128" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
85
+ <text class="node-name" x="168" y="444">Pattern preparation</text>
86
+ <text class="node-sub" x="168" y="472">operation + scope · lazy sync</text>
87
+ <text class="node-sub" x="168" y="488">bounded user/assistant corpus</text>
88
+ <text class="node-sub" x="168" y="504">+ repository inventory · never tool results</text>
89
+
90
+ <polygon points="424,392 536,448 424,504 312,448" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
91
+ <text class="technical-name" x="424" y="444">query?</text>
92
+ <text class="node-sub" x="424" y="464">PRESENT</text>
93
+
94
+ <rect x="576" y="400" width="200" height="96" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
95
+ <text class="node-name" x="676" y="428">Discovery</text>
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
+
100
+ <rect x="328" y="544" width="192" height="96" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
101
+ <text class="node-name" x="424" y="572">Browse</text>
102
+ <text class="node-sub" x="424" y="596">no args · lazy sync</text>
103
+ <text class="node-sub" x="424" y="612">recent session metadata</text>
104
+
105
+ <polygon points="1024,392 1136,448 1024,504 912,448" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
106
+ <text class="technical-name" x="1024" y="444">aroundMessageId?</text>
107
+ <text class="node-sub" x="1024" y="464">WITH sessionId</text>
108
+
109
+ <rect x="1040" y="536" width="192" height="112" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
110
+ <text class="node-name" x="1136" y="564">Scroll</text>
111
+ <text class="node-sub" x="1136" y="588">sessionId + aroundMessageId</text>
112
+ <text class="node-sub" x="1136" y="604">direct saved JSONL</text>
113
+ <text class="node-sub" x="1136" y="620">raw roles including tool results</text>
114
+ <text class="node-sub" x="1136" y="636">±window with branchTip</text>
115
+
116
+ <rect x="824" y="536" width="192" height="112" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
117
+ <text class="node-name" x="920" y="564">Read</text>
118
+ <text class="node-sub" x="920" y="588">sessionId · direct saved JSONL</text>
119
+ <text class="node-sub" x="920" y="604">raw roles including tool results</text>
120
+ <text class="node-sub" x="920" y="620">head 20 + tail 10</text>
121
+ </svg>
@@ -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/bookends, READ head/tail.
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; userAssistantTextOnly?: boolean },
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(entriesById, entries, opts.branchTip);
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(entriesById, entries, anchorEntryId);
221
+ tip = deepestDescendant(entries, anchorEntryId);
231
222
  }
232
- const rawMessages = branchMessages(entriesById, tip);
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 user/assistant text message`);
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 live-entry filtering. */
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
- /** Entry ids on the current session's live branch — hits here are skipped. */
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
- // Live-entry suppression happens INSIDE SQL (before ROW_NUMBER/caps) so
529
- // inactive matches on the current file compete only against each other, and
530
- // there is no pre-partition LIMIT: one verbose session cannot starve others.
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 ${LIVE_FILTER_SQL}
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
- // Per-connection TEMP table of (path, entry_id) pairs to suppress —
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 ${LIVE_FILTER_SQL}
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
- // Live-entry and one-hop lineage suppression already happened in SQL,
669
- // before the scan limit, so fork rows cannot starve unrelated matches.
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 stored messages.
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 is adaptive; use \`detail: "full"\` to hydrate every result.`;
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
- detail: Type.Optional(StringEnum(["adaptive", "full"] as const)),
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", "detail"] as const)
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
- // Current-session guard: suppress hits on the live branch.
451
- let liveIds: Set<string> | undefined;
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 → degrade gracefully, no suppression.
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
- // Reserve the complete response envelope and divide remaining space
473
- // across hits so the first hydrated result cannot starve later metadata.
474
- // The same warning-bearing envelope is reused for the final result so the
475
- // reservation matches what is returned (and textResult's trimming keeps
476
- // top-level non-array keys like syncWarning).
477
- const envelope: Record<string, unknown> = {
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 });
@@ -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/discovery window. */
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 before hydration. */
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,56 +1,62 @@
1
1
  {
2
- "name": "@henryqw/pi-session-recall",
3
- "version": "2.1.0",
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
- "README.md"
22
- ],
23
- "scripts": {
24
- "test": "node --test test/*.test.ts",
25
- "typecheck": "tsc --noEmit --allowImportingTsExtensions --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck extensions/*.ts test/*.test.ts",
26
- "pack:check": "npm pack --dry-run"
27
- },
28
- "peerDependencies": {
29
- "@earendil-works/pi-ai": "^0.85.1",
30
- "@earendil-works/pi-coding-agent": "^0.85.1",
31
- "@earendil-works/pi-tui": "^0.85.1",
32
- "typebox": "^1.3.15"
33
- },
34
- "repository": {
35
- "type": "git",
36
- "url": "git+https://github.com/HenryQW/pi-harness.git",
37
- "directory": "extensions/pi-session-recall"
38
- },
39
- "bugs": {
40
- "url": "https://github.com/HenryQW/pi-harness/issues"
41
- },
42
- "publishConfig": {
43
- "access": "public"
44
- },
45
- "pi": {
46
- "extensions": [
47
- "./extensions/session-recall.ts"
48
- ],
49
- "skills": [
50
- "./skills"
51
- ]
52
- },
53
- "dependencies": {
54
- "@henryqw/pi-config-store": "^1.0.0"
55
- }
2
+ "name": "@henryqw/pi-session-recall",
3
+ "version": "4.0.1",
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
+ }
56
62
  }