pi-goosedump 0.1.3 → 0.2.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 +14 -7
- package/index.ts +284 -237
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -20,19 +20,19 @@ Once installed, pi-goosedump registers a tool and a slash command:
|
|
|
20
20
|
|
|
21
21
|
The agent can browse session history with:
|
|
22
22
|
|
|
23
|
-
| Action | Description
|
|
24
|
-
| -------- |
|
|
25
|
-
| `list` | List all available sessions
|
|
26
|
-
| `search` |
|
|
27
|
-
| `expand` | Show full content for specific entry IDs
|
|
28
|
-
| `view` | View the full session transcript
|
|
23
|
+
| Action | Description |
|
|
24
|
+
| -------- | ---------------------------------------- |
|
|
25
|
+
| `list` | List all available sessions |
|
|
26
|
+
| `search` | Rank messages by query relevance |
|
|
27
|
+
| `expand` | Show full content for specific entry IDs |
|
|
28
|
+
| `view` | View the full session transcript |
|
|
29
29
|
|
|
30
30
|
Examples:
|
|
31
31
|
|
|
32
32
|
```
|
|
33
33
|
goosedump({ action: "list" })
|
|
34
34
|
goosedump({ action: "search", contextId: "abc123", query: "bug fix" })
|
|
35
|
-
goosedump({ action: "expand", contextId: "abc123", ids: [
|
|
35
|
+
goosedump({ action: "expand", contextId: "abc123", ids: ["entry-a", "entry-b"] })
|
|
36
36
|
goosedump({ action: "view", contextId: "abc123" })
|
|
37
37
|
```
|
|
38
38
|
|
|
@@ -40,6 +40,13 @@ goosedump({ action: "view", contextId: "abc123" })
|
|
|
40
40
|
|
|
41
41
|
Opens an interactive session browser.
|
|
42
42
|
|
|
43
|
+
### Compaction
|
|
44
|
+
|
|
45
|
+
pi-goosedump hooks Pi's `/compact` and auto-compaction flow. It uses
|
|
46
|
+
`goosedump compact pi <session>` with Pi's compaction range (`--from`,
|
|
47
|
+
`--until`, and `--scope`) so the generated summary matches the entries Pi is
|
|
48
|
+
about to replace.
|
|
49
|
+
|
|
43
50
|
## License
|
|
44
51
|
|
|
45
52
|
`pi-goosedump` is licensed under `MIT`. See [LICENSE](LICENSE) for more
|
package/index.ts
CHANGED
|
@@ -3,34 +3,37 @@
|
|
|
3
3
|
|
|
4
4
|
import type {
|
|
5
5
|
AgentToolResult,
|
|
6
|
+
CompactionEntry,
|
|
7
|
+
CompactionResult,
|
|
6
8
|
ExtensionAPI,
|
|
7
9
|
ExtensionContext,
|
|
10
|
+
SessionEntry,
|
|
8
11
|
} from '@earendil-works/pi-coding-agent';
|
|
9
12
|
|
|
10
13
|
import { defineTool } from '@earendil-works/pi-coding-agent';
|
|
11
14
|
|
|
12
15
|
import { execFileSync } from 'node:child_process';
|
|
13
|
-
import { existsSync } from 'node:fs';
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
14
18
|
import { homedir } from 'node:os';
|
|
15
|
-
import { join } from 'node:path';
|
|
19
|
+
import { basename, join } from 'node:path';
|
|
16
20
|
|
|
17
21
|
import { Key, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
18
22
|
import { Type } from '@sinclair/typebox';
|
|
19
23
|
|
|
20
|
-
const
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
|
|
26
|
+
const GOOSEDUMP_VERSION = [0, 2, 1] as const;
|
|
21
27
|
|
|
22
28
|
interface GoosedumpListing {
|
|
23
29
|
id: string;
|
|
24
|
-
created: string;
|
|
25
|
-
modified: string;
|
|
26
|
-
entryCount: number;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
interface GoosedumpMessage {
|
|
30
33
|
id: string;
|
|
31
34
|
role: string;
|
|
32
35
|
content: string;
|
|
33
|
-
|
|
36
|
+
score?: number;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
interface GoosedumpContextResult {
|
|
@@ -91,17 +94,14 @@ function hasMinimumVersion(version: string, minimum: readonly [number, number, n
|
|
|
91
94
|
|
|
92
95
|
function parseListings(output: string): GoosedumpListing[] {
|
|
93
96
|
const listings: GoosedumpListing[] = [];
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
id: match[1]
|
|
101
|
-
|
|
102
|
-
entryCount: parseInt(match[3], 10),
|
|
103
|
-
modified: match[4],
|
|
104
|
-
});
|
|
97
|
+
const lines = output.split('\n');
|
|
98
|
+
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
if (line === 'Sessions:' || line.trim() === '') continue;
|
|
101
|
+
const match = line.match(/^ (\S+)/);
|
|
102
|
+
if (match) {
|
|
103
|
+
listings.push({ id: match[1] });
|
|
104
|
+
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
return listings;
|
|
@@ -109,41 +109,46 @@ function parseListings(output: string): GoosedumpListing[] {
|
|
|
109
109
|
|
|
110
110
|
function parseMessages(output: string): GoosedumpMessage[] {
|
|
111
111
|
const messages: GoosedumpMessage[] = [];
|
|
112
|
-
// Format from goosedump plain output: [id] role: content
|
|
113
112
|
const lines = output.split('\n');
|
|
114
|
-
let
|
|
115
|
-
let currentRole = '';
|
|
113
|
+
let currentMsg: GoosedumpMessage | null = null;
|
|
116
114
|
let contentLines: string[] = [];
|
|
117
|
-
let currentTimestamp = '';
|
|
118
115
|
|
|
119
116
|
for (const line of lines) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
117
|
+
if (line.startsWith('results for ')) continue;
|
|
118
|
+
|
|
119
|
+
const entryMatch = line.match(/^entryId:\s+(\S+)\s+\((.+)\)$/);
|
|
120
|
+
if (entryMatch) {
|
|
121
|
+
if (currentMsg) {
|
|
122
|
+
currentMsg.content = contentLines.join('\n').trim();
|
|
123
|
+
messages.push(currentMsg);
|
|
124
|
+
}
|
|
125
|
+
currentMsg = {
|
|
126
|
+
id: entryMatch[1],
|
|
127
|
+
role: entryMatch[2],
|
|
128
|
+
content: '',
|
|
129
|
+
};
|
|
130
|
+
contentLines = [];
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (currentMsg) {
|
|
135
|
+
const indentedMatch = line.match(/^ (.+)$/);
|
|
136
|
+
if (indentedMatch) {
|
|
137
|
+
const content = indentedMatch[1];
|
|
138
|
+
if (content.startsWith('score: ')) {
|
|
139
|
+
const scoreVal = parseFloat(content.slice(7));
|
|
140
|
+
if (!isNaN(scoreVal)) currentMsg.score = scoreVal;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (/^\.\.\.\(\d+ lines (?:above|below)\)$/.test(content)) continue;
|
|
144
|
+
contentLines.push(content);
|
|
129
145
|
}
|
|
130
|
-
currentId = headerMatch[1];
|
|
131
|
-
currentRole = headerMatch[2];
|
|
132
|
-
currentTimestamp = headerMatch[3]?.replace(/[()]/g, '') ?? '';
|
|
133
|
-
contentLines = [line.slice(headerMatch[0].length).trim()];
|
|
134
|
-
} else if (currentId) {
|
|
135
|
-
if (line.startsWith('---') && messages.length > 0) continue;
|
|
136
|
-
contentLines.push(line);
|
|
137
146
|
}
|
|
138
147
|
}
|
|
139
148
|
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
role: currentRole,
|
|
144
|
-
content: contentLines.join('\n').trim(),
|
|
145
|
-
timestamp: currentTimestamp,
|
|
146
|
-
});
|
|
149
|
+
if (currentMsg) {
|
|
150
|
+
currentMsg.content = contentLines.join('\n').trim();
|
|
151
|
+
messages.push(currentMsg);
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
return messages;
|
|
@@ -161,10 +166,26 @@ function formatMessagesFull(messages: GoosedumpMessage[]): string {
|
|
|
161
166
|
return messages.map((m) => `[${m.id}] ${m.role}:\n${m.content}`).join('\n\n---\n\n');
|
|
162
167
|
}
|
|
163
168
|
|
|
164
|
-
function
|
|
165
|
-
|
|
169
|
+
function runGoosedump(args: string[]): string {
|
|
170
|
+
return execFileSync(process.execPath, [binaryPath(), ...args], {
|
|
166
171
|
encoding: 'utf-8',
|
|
167
172
|
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parsePageInfo(output: string, fallbackPage: number): { page: number; totalPages: number } {
|
|
176
|
+
const match = output.match(/\(page\s+(\d+)\s+of\s+(\d+)\):/i);
|
|
177
|
+
if (!match) {
|
|
178
|
+
return { page: fallbackPage, totalPages: 1 };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
page: Number(match[1]),
|
|
183
|
+
totalPages: Number(match[2]),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function goosedumpList(): GoosedumpListing[] {
|
|
188
|
+
const output = runGoosedump(['list', 'pi']);
|
|
168
189
|
return parseListings(output);
|
|
169
190
|
}
|
|
170
191
|
|
|
@@ -179,56 +200,76 @@ function goosedumpContext(
|
|
|
179
200
|
page?: number;
|
|
180
201
|
} = {},
|
|
181
202
|
): GoosedumpContextResult {
|
|
182
|
-
const
|
|
203
|
+
const scope = options.scope ?? 'lineage';
|
|
204
|
+
const hasQuery = typeof options.grep === 'string';
|
|
205
|
+
const args = hasQuery
|
|
206
|
+
? options.rank === false
|
|
207
|
+
? ['grep', 'pi', contextId, options.grep ?? '']
|
|
208
|
+
: ['search', 'pi', contextId, options.grep ?? '']
|
|
209
|
+
: ['show', 'pi', contextId];
|
|
183
210
|
|
|
184
|
-
if (options.scope && options.scope !== 'lineage') {
|
|
185
|
-
args.push('--scope', options.scope);
|
|
186
|
-
}
|
|
187
|
-
if (options.grep) {
|
|
188
|
-
args.push('--grep', options.grep);
|
|
189
|
-
}
|
|
190
211
|
if (options.compact) {
|
|
191
|
-
args.push('--
|
|
212
|
+
args.push('--clip');
|
|
192
213
|
}
|
|
193
214
|
if (options.ids) {
|
|
194
215
|
args.push('--ids', options.ids);
|
|
195
216
|
}
|
|
196
|
-
if (options.rank && options.
|
|
197
|
-
args.push('--
|
|
198
|
-
if (options.page && options.page > 1) {
|
|
199
|
-
args.push('--page', String(options.page));
|
|
200
|
-
}
|
|
217
|
+
if (hasQuery && options.rank !== false && options.page) {
|
|
218
|
+
args.push('--page', String(options.page));
|
|
201
219
|
}
|
|
220
|
+
args.push('--scope', scope);
|
|
202
221
|
|
|
203
|
-
const output =
|
|
204
|
-
encoding: 'utf-8',
|
|
205
|
-
});
|
|
206
|
-
|
|
222
|
+
const output = runGoosedump(args);
|
|
207
223
|
const messages = parseMessages(output);
|
|
224
|
+
const pageInfo = parsePageInfo(output, options.page ?? 1);
|
|
225
|
+
|
|
208
226
|
return {
|
|
209
227
|
messages,
|
|
210
228
|
totalCount: messages.length,
|
|
211
|
-
page:
|
|
212
|
-
totalPages:
|
|
229
|
+
page: pageInfo.page,
|
|
230
|
+
totalPages: pageInfo.totalPages,
|
|
213
231
|
};
|
|
214
232
|
}
|
|
215
233
|
|
|
216
234
|
function goosedumpExpand(
|
|
217
235
|
contextId: string,
|
|
218
|
-
entryIds:
|
|
236
|
+
entryIds: string[],
|
|
219
237
|
scope: string = 'lineage',
|
|
220
238
|
): GoosedumpMessage[] {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
239
|
+
if (entryIds.length === 0) return [];
|
|
240
|
+
|
|
241
|
+
const output = runGoosedump([
|
|
242
|
+
'show',
|
|
243
|
+
'pi',
|
|
244
|
+
contextId,
|
|
245
|
+
'--ids',
|
|
246
|
+
entryIds.join(','),
|
|
247
|
+
'--scope',
|
|
248
|
+
scope,
|
|
249
|
+
]);
|
|
250
|
+
return parseMessages(output);
|
|
251
|
+
}
|
|
226
252
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
253
|
+
function goosedumpCompact(
|
|
254
|
+
contextId: string,
|
|
255
|
+
options: {
|
|
256
|
+
scope?: string;
|
|
257
|
+
from?: string;
|
|
258
|
+
until?: string;
|
|
259
|
+
ids?: string[];
|
|
260
|
+
},
|
|
261
|
+
): string {
|
|
262
|
+
const scope = options.scope ?? 'lineage';
|
|
263
|
+
const args = ['compact', 'pi', contextId, '--scope', scope];
|
|
264
|
+
|
|
265
|
+
if (options.ids && options.ids.length > 0) {
|
|
266
|
+
args.push('--ids', options.ids.join(','));
|
|
267
|
+
} else {
|
|
268
|
+
if (options.from) args.push('--from', options.from);
|
|
269
|
+
if (options.until) args.push('--until', options.until);
|
|
270
|
+
}
|
|
230
271
|
|
|
231
|
-
return
|
|
272
|
+
return runGoosedump(args).trim();
|
|
232
273
|
}
|
|
233
274
|
|
|
234
275
|
export interface GoosedumpIntegrationOptions {
|
|
@@ -236,18 +277,23 @@ export interface GoosedumpIntegrationOptions {
|
|
|
236
277
|
overrideDefaultCompaction?: boolean;
|
|
237
278
|
}
|
|
238
279
|
|
|
239
|
-
interface
|
|
240
|
-
|
|
241
|
-
|
|
280
|
+
interface GoosedumpCompactRange {
|
|
281
|
+
scope: string;
|
|
282
|
+
from?: string;
|
|
283
|
+
until: string;
|
|
284
|
+
ids?: string[];
|
|
242
285
|
}
|
|
243
286
|
|
|
244
|
-
interface
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
287
|
+
interface GoosedumpCompactionDetails {
|
|
288
|
+
source: 'goosedump';
|
|
289
|
+
goosedumpVersion: string | null;
|
|
290
|
+
contextId: string;
|
|
291
|
+
scope: string;
|
|
292
|
+
from?: string;
|
|
293
|
+
until?: string;
|
|
294
|
+
ids?: string[];
|
|
295
|
+
previousSummaryIncluded: boolean;
|
|
296
|
+
fileOps: unknown;
|
|
251
297
|
}
|
|
252
298
|
|
|
253
299
|
export interface GoosedumpIntegration {
|
|
@@ -258,143 +304,138 @@ export default function (pi: ExtensionAPI) {
|
|
|
258
304
|
createGoosedumpIntegration().register(pi);
|
|
259
305
|
}
|
|
260
306
|
|
|
261
|
-
function
|
|
262
|
-
if (
|
|
263
|
-
if (Array.isArray(m.content)) {
|
|
264
|
-
return m.content
|
|
265
|
-
.filter((c) => c.type === 'text' && typeof c.text === 'string')
|
|
266
|
-
.map((c) => c.text!)
|
|
267
|
-
.join(' ');
|
|
268
|
-
}
|
|
269
|
-
return '';
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function extractContextFromMessages(messages: MessageLike[]): ExtractedContext {
|
|
273
|
-
const goalPatterns = [
|
|
274
|
-
/(?:goal|objective|task|aim|purpose)[\s:]*([^.\n]+)/gi,
|
|
275
|
-
/(?:working on|implementing|building|creating|fixing|adding)\s+([^.\n]+)/gi,
|
|
276
|
-
/#\s*(?:Goal|Task|Objective)[\s:]*([^\n]+)/gi,
|
|
277
|
-
];
|
|
278
|
-
|
|
279
|
-
const referencePatterns = [
|
|
280
|
-
/(?:https?:\/\/[^\s]+)/gi,
|
|
281
|
-
/(?:[\w./-]+\/[@\w.-]+(?:#\S+)?)/g,
|
|
282
|
-
/(?:`[^`]+\.[a-z]{1,6}`)/g,
|
|
283
|
-
/(?:v?\d+\.\d+(?:\.\d+)?(?:-[\w.]+)?)/g,
|
|
284
|
-
/(?:#[1-9]\d*)/g,
|
|
285
|
-
];
|
|
286
|
-
|
|
287
|
-
const decisionPatterns = [
|
|
288
|
-
/(?:decided|decision|chosen?|opted|settled|agreed)\s+(?:to\s+)?([^.\n]+)/gi,
|
|
289
|
-
/(?:will|should|must|going to)\s+(?:use|switch|change|keep|stay|go with)\s+([^.\n]+)/gi,
|
|
290
|
-
/(?:let's|lets|we'll)\s+([^.\n]+)/gi,
|
|
291
|
-
];
|
|
292
|
-
|
|
293
|
-
const constraintPatterns = [
|
|
294
|
-
/(?:cannot|can't|must not|should not|don't|do not|never)\s+([^.\n]+)/gi,
|
|
295
|
-
/(?:constraint|restriction|limitation|requirement)[\s:]*([^.\n]+)/gi,
|
|
296
|
-
/(?:need to|have to|required to)\s+(?:keep|maintain|preserve|ensure|avoid)\s+([^.\n]+)/gi,
|
|
297
|
-
];
|
|
298
|
-
|
|
299
|
-
const statusPatterns = [
|
|
300
|
-
/(?:status|state|progress)[\s:]*([^.\n]+)/gi,
|
|
301
|
-
/(?:done|completed|finished|resolved|fixed)\s+([^.\n]+)/gi,
|
|
302
|
-
/(?:todo|pending|remaining|left|in progress|wip)\s*:?\s*([^.\n]+)/gi,
|
|
303
|
-
];
|
|
304
|
-
|
|
305
|
-
const allText = messages.map((m) => getMessageText(m)).join('\n');
|
|
306
|
-
|
|
307
|
-
const recentText = messages
|
|
308
|
-
.slice(-30)
|
|
309
|
-
.map((m) => getMessageText(m))
|
|
310
|
-
.join('\n');
|
|
311
|
-
|
|
312
|
-
const extractMatches = (text: string, patterns: RegExp[]): Set<string> => {
|
|
313
|
-
const matches = new Set<string>();
|
|
314
|
-
for (const pattern of patterns) {
|
|
315
|
-
pattern.lastIndex = 0;
|
|
316
|
-
let match: RegExpExecArray | null;
|
|
317
|
-
while ((match = pattern.exec(text)) !== null) {
|
|
318
|
-
const extracted = (match[1] ?? '').trim().slice(0, 200);
|
|
319
|
-
if (extracted.length > 3) matches.add(extracted);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
return matches;
|
|
323
|
-
};
|
|
307
|
+
function sessionFileContextId(sessionFile: string | null | undefined): string | null {
|
|
308
|
+
if (!sessionFile) return null;
|
|
324
309
|
|
|
325
|
-
const
|
|
326
|
-
const
|
|
310
|
+
const name = basename(sessionFile);
|
|
311
|
+
const nameMatch = name.match(
|
|
312
|
+
/(?:^|_)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i,
|
|
313
|
+
);
|
|
314
|
+
if (nameMatch) return nameMatch[1];
|
|
327
315
|
|
|
328
|
-
|
|
329
|
-
.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const lines = content.split('\n').filter((l: string) => l.trim().length > 10);
|
|
337
|
-
return lines.length > 0 ? lines[0].trim().slice(0, 120) : null;
|
|
338
|
-
})
|
|
339
|
-
.filter((l: string | null): l is string => l !== null)
|
|
340
|
-
.slice(0, 8);
|
|
316
|
+
try {
|
|
317
|
+
const firstLine = readFileSync(sessionFile, 'utf-8').split(/\r?\n/, 1)[0];
|
|
318
|
+
const header = JSON.parse(firstLine) as { id?: unknown };
|
|
319
|
+
return typeof header.id === 'string' && header.id.length > 0 ? header.id : null;
|
|
320
|
+
} catch {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
341
324
|
|
|
342
|
-
|
|
343
|
-
|
|
325
|
+
function currentSessionContextId(ctx: ExtensionContext): string | null {
|
|
326
|
+
return sessionFileContextId(ctx.sessionManager.getSessionFile());
|
|
327
|
+
}
|
|
344
328
|
|
|
345
|
-
|
|
346
|
-
|
|
329
|
+
function isCompactionEntry(entry: SessionEntry): entry is CompactionEntry {
|
|
330
|
+
return entry.type === 'compaction';
|
|
331
|
+
}
|
|
347
332
|
|
|
348
|
-
|
|
349
|
-
|
|
333
|
+
function latestCompactionBefore(
|
|
334
|
+
branchEntries: SessionEntry[],
|
|
335
|
+
untilIndex: number,
|
|
336
|
+
): CompactionEntry | undefined {
|
|
337
|
+
for (let i = untilIndex - 1; i >= 0; i -= 1) {
|
|
338
|
+
const entry = branchEntries[i];
|
|
339
|
+
if (isCompactionEntry(entry)) return entry;
|
|
340
|
+
}
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
350
343
|
|
|
351
|
-
|
|
352
|
-
|
|
344
|
+
function compactionStartId(
|
|
345
|
+
branchEntries: SessionEntry[],
|
|
346
|
+
previousCompaction: CompactionEntry,
|
|
347
|
+
untilIndex: number,
|
|
348
|
+
): string | undefined {
|
|
349
|
+
const keptIndex = branchEntries.findIndex(
|
|
350
|
+
(entry) => entry.id === previousCompaction.firstKeptEntryId,
|
|
351
|
+
);
|
|
352
|
+
if (keptIndex >= 0 && keptIndex < untilIndex) return previousCompaction.firstKeptEntryId;
|
|
353
|
+
|
|
354
|
+
const compactionIndex = branchEntries.findIndex((entry) => entry.id === previousCompaction.id);
|
|
355
|
+
if (compactionIndex >= 0 && compactionIndex + 1 < untilIndex) {
|
|
356
|
+
return branchEntries[compactionIndex + 1].id;
|
|
357
|
+
}
|
|
353
358
|
|
|
354
|
-
return
|
|
355
|
-
goal,
|
|
356
|
-
outstanding:
|
|
357
|
-
outstandingItems.length > 0 ? outstandingItems : ['No outstanding items identified'],
|
|
358
|
-
references,
|
|
359
|
-
decisions,
|
|
360
|
-
constraints: constraints.length > 0 ? constraints : ['No specific constraints identified'],
|
|
361
|
-
status,
|
|
362
|
-
};
|
|
359
|
+
return undefined;
|
|
363
360
|
}
|
|
364
361
|
|
|
365
|
-
function
|
|
366
|
-
|
|
362
|
+
function buildCompactRange(
|
|
363
|
+
branchEntries: SessionEntry[],
|
|
364
|
+
firstKeptEntryId: string,
|
|
365
|
+
): GoosedumpCompactRange | undefined {
|
|
366
|
+
const untilIndex = branchEntries.findIndex((entry) => entry.id === firstKeptEntryId);
|
|
367
|
+
if (untilIndex < 0) return undefined;
|
|
368
|
+
|
|
369
|
+
const previousCompaction = latestCompactionBefore(branchEntries, untilIndex);
|
|
370
|
+
const from = previousCompaction
|
|
371
|
+
? compactionStartId(branchEntries, previousCompaction, untilIndex)
|
|
372
|
+
: undefined;
|
|
373
|
+
|
|
374
|
+
return from && from !== firstKeptEntryId
|
|
375
|
+
? { scope: 'lineage', from, until: firstKeptEntryId }
|
|
376
|
+
: { scope: 'lineage', until: firstKeptEntryId };
|
|
377
|
+
}
|
|
367
378
|
|
|
368
|
-
|
|
379
|
+
function mergeWithPreviousSummary(
|
|
380
|
+
summary: string,
|
|
381
|
+
previousSummary: string | undefined,
|
|
382
|
+
): { summary: string; previousSummaryIncluded: boolean } {
|
|
383
|
+
const nextSummary = summary.trim();
|
|
384
|
+
const priorSummary = previousSummary?.trim();
|
|
369
385
|
|
|
370
|
-
if (
|
|
371
|
-
|
|
386
|
+
if (!priorSummary) {
|
|
387
|
+
return { summary: nextSummary, previousSummaryIncluded: false };
|
|
372
388
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
sections.push(`## Outstanding Context\n${ctx.outstanding.map((o) => `- ${o}`).join('\n')}`);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (ctx.decisions.length > 0) {
|
|
379
|
-
sections.push(`## Key Decisions\n${ctx.decisions.map((d) => `- ${d}`).join('\n')}`);
|
|
389
|
+
if (!nextSummary) {
|
|
390
|
+
return { summary: priorSummary, previousSummaryIncluded: true };
|
|
380
391
|
}
|
|
381
392
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
393
|
+
return {
|
|
394
|
+
summary: `${priorSummary}\n\n## Recent Compaction Update\n${nextSummary}`,
|
|
395
|
+
previousSummaryIncluded: true,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
385
398
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
399
|
+
function buildGoosedumpCompaction(
|
|
400
|
+
contextId: string,
|
|
401
|
+
preparation: {
|
|
402
|
+
firstKeptEntryId: string;
|
|
403
|
+
tokensBefore: number;
|
|
404
|
+
previousSummary?: string;
|
|
405
|
+
fileOps: unknown;
|
|
406
|
+
},
|
|
407
|
+
branchEntries: SessionEntry[],
|
|
408
|
+
): CompactionResult<GoosedumpCompactionDetails> | undefined {
|
|
409
|
+
const range = buildCompactRange(branchEntries, preparation.firstKeptEntryId);
|
|
410
|
+
if (!range) return undefined;
|
|
411
|
+
|
|
412
|
+
const summary = goosedumpCompact(contextId, range);
|
|
413
|
+
const merged = mergeWithPreviousSummary(summary, preparation.previousSummary);
|
|
414
|
+
if (!merged.summary) return undefined;
|
|
389
415
|
|
|
390
|
-
return
|
|
416
|
+
return {
|
|
417
|
+
summary: merged.summary,
|
|
418
|
+
firstKeptEntryId: preparation.firstKeptEntryId,
|
|
419
|
+
tokensBefore: preparation.tokensBefore,
|
|
420
|
+
details: {
|
|
421
|
+
source: 'goosedump',
|
|
422
|
+
goosedumpVersion: goosedumpVersion(),
|
|
423
|
+
contextId,
|
|
424
|
+
scope: range.scope,
|
|
425
|
+
from: range.from,
|
|
426
|
+
until: range.until,
|
|
427
|
+
ids: range.ids,
|
|
428
|
+
previousSummaryIncluded: merged.previousSummaryIncluded,
|
|
429
|
+
fileOps: preparation.fileOps,
|
|
430
|
+
},
|
|
431
|
+
};
|
|
391
432
|
}
|
|
392
433
|
|
|
393
434
|
export function createGoosedumpIntegration(
|
|
394
435
|
options: GoosedumpIntegrationOptions = {},
|
|
395
436
|
): GoosedumpIntegration {
|
|
396
437
|
const shouldRegisterTool = options.registerTool ?? true;
|
|
397
|
-
const
|
|
438
|
+
const shouldOverrideDefaultCompaction = options.overrideDefaultCompaction ?? true;
|
|
398
439
|
|
|
399
440
|
let goosedumpReady = false;
|
|
400
441
|
let goosedumpEnabled = false;
|
|
@@ -431,12 +472,13 @@ export function createGoosedumpIntegration(
|
|
|
431
472
|
name: 'goosedump',
|
|
432
473
|
label: 'goosedump',
|
|
433
474
|
description:
|
|
434
|
-
'Browse coding agent session history. List all sessions, or view/search within a session. Supports
|
|
435
|
-
promptSnippet:
|
|
475
|
+
'Browse coding agent session history. List all sessions, or view/search within a session. Supports ranked search, compact overview, and result expansion. Default scope is active lineage.',
|
|
476
|
+
promptSnippet:
|
|
477
|
+
'goosedump({ action: "search", contextId, query, scope?, page? }) - search session history',
|
|
436
478
|
promptGuidelines: [
|
|
437
479
|
'When researching past conversation history, use goosedump to find relevant context.',
|
|
438
|
-
'Start with compact search (compact: true) for quick overview, then expand interesting entries.',
|
|
439
|
-
'Default scope is "lineage" (current branch); use scope: "all" to
|
|
480
|
+
'Start with compact ranked search (compact: true) for quick overview, then expand interesting entries.',
|
|
481
|
+
'Default scope is "lineage" (current branch); use scope: "all" to include all entries in the session.',
|
|
440
482
|
],
|
|
441
483
|
parameters: Type.Object({
|
|
442
484
|
action: Type.Union(
|
|
@@ -457,12 +499,12 @@ export function createGoosedumpIntegration(
|
|
|
457
499
|
}),
|
|
458
500
|
),
|
|
459
501
|
query: Type.Optional(
|
|
460
|
-
Type.String({ description: '
|
|
502
|
+
Type.String({ description: 'Search query to rank messages by relevance' }),
|
|
461
503
|
),
|
|
462
504
|
scope: Type.Optional(
|
|
463
505
|
Type.Union([Type.Literal('lineage'), Type.Literal('all')], {
|
|
464
506
|
default: 'lineage',
|
|
465
|
-
description: 'Scope: lineage (current branch) or all
|
|
507
|
+
description: 'Scope: lineage (current branch) or all entries in the session',
|
|
466
508
|
}),
|
|
467
509
|
),
|
|
468
510
|
compact: Type.Optional(
|
|
@@ -472,7 +514,7 @@ export function createGoosedumpIntegration(
|
|
|
472
514
|
}),
|
|
473
515
|
),
|
|
474
516
|
ids: Type.Optional(
|
|
475
|
-
Type.Array(Type.
|
|
517
|
+
Type.Array(Type.String(), {
|
|
476
518
|
description: 'Entry IDs to expand (show full content)',
|
|
477
519
|
}),
|
|
478
520
|
),
|
|
@@ -510,9 +552,7 @@ export function createGoosedumpIntegration(
|
|
|
510
552
|
|
|
511
553
|
const lines = ['Available sessions:', ''];
|
|
512
554
|
for (const listing of listings) {
|
|
513
|
-
lines.push(
|
|
514
|
-
` ${listing.id} — ${listing.entryCount} entries (modified ${listing.modified})`,
|
|
515
|
-
);
|
|
555
|
+
lines.push(` ${listing.id}`);
|
|
516
556
|
}
|
|
517
557
|
|
|
518
558
|
return {
|
|
@@ -551,7 +591,8 @@ export function createGoosedumpIntegration(
|
|
|
551
591
|
};
|
|
552
592
|
}
|
|
553
593
|
|
|
554
|
-
const
|
|
594
|
+
const useCompact = params.compact ?? true;
|
|
595
|
+
const text = useCompact
|
|
555
596
|
? formatMessagesCompact(result.messages)
|
|
556
597
|
: formatMessagesFull(result.messages);
|
|
557
598
|
|
|
@@ -656,34 +697,41 @@ export function createGoosedumpIntegration(
|
|
|
656
697
|
}
|
|
657
698
|
});
|
|
658
699
|
|
|
659
|
-
pi.on('session_before_compact', async (event,
|
|
660
|
-
if (!goosedumpEnabled || !goosedumpReady) return;
|
|
700
|
+
pi.on('session_before_compact', async (event, ctx) => {
|
|
701
|
+
if (!shouldOverrideDefaultCompaction || !goosedumpEnabled || !goosedumpReady) return;
|
|
702
|
+
if (event.customInstructions?.trim()) return;
|
|
703
|
+
if (event.signal.aborted) return;
|
|
661
704
|
|
|
662
|
-
const { messagesToSummarize,
|
|
705
|
+
const { messagesToSummarize, turnPrefixMessages } = event.preparation;
|
|
706
|
+
if (messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) return;
|
|
663
707
|
|
|
664
|
-
|
|
708
|
+
const contextId = currentSessionContextId(ctx);
|
|
709
|
+
if (!contextId) return;
|
|
665
710
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
711
|
+
try {
|
|
712
|
+
const compaction = buildGoosedumpCompaction(
|
|
713
|
+
contextId,
|
|
714
|
+
event.preparation,
|
|
715
|
+
event.branchEntries,
|
|
716
|
+
);
|
|
717
|
+
return compaction ? { compaction } : undefined;
|
|
718
|
+
} catch (err) {
|
|
719
|
+
if (ctx.hasUI) {
|
|
720
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
721
|
+
ctx.ui.notify(
|
|
722
|
+
`goosedump compact failed; falling back to Pi compaction: ${message}`,
|
|
723
|
+
'error',
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
return undefined;
|
|
727
|
+
}
|
|
680
728
|
});
|
|
681
729
|
|
|
682
|
-
pi.on('session_compact', async (event,
|
|
683
|
-
if (!goosedumpEnabled || !goosedumpReady) return;
|
|
730
|
+
pi.on('session_compact', async (event, ctx) => {
|
|
731
|
+
if (!goosedumpEnabled || !goosedumpReady || !event.fromExtension) return;
|
|
684
732
|
|
|
685
733
|
const compactionEntry = event.compactionEntry;
|
|
686
|
-
if (!compactionEntry
|
|
734
|
+
if (!compactionEntry) return;
|
|
687
735
|
|
|
688
736
|
const tokensBefore = compactionEntry.tokensBefore;
|
|
689
737
|
const summaryLen = compactionEntry.summary.length;
|
|
@@ -691,7 +739,7 @@ export function createGoosedumpIntegration(
|
|
|
691
739
|
const tokensSaved =
|
|
692
740
|
tokensBefore > 0 ? Math.round((1 - tokenEstimate / tokensBefore) * 100) : 0;
|
|
693
741
|
|
|
694
|
-
|
|
742
|
+
ctx.ui.notify(
|
|
695
743
|
`Goosedump compacted session: ~${tokensSaved}% token reduction (${tokensBefore} → ~${tokenEstimate} tokens)`,
|
|
696
744
|
'info',
|
|
697
745
|
);
|
|
@@ -755,9 +803,8 @@ export function createGoosedumpIntegration(
|
|
|
755
803
|
const listing = listings[i];
|
|
756
804
|
const isSelected = i === selectedIndex;
|
|
757
805
|
const cursor = isSelected ? accent('▶') : ' ';
|
|
758
|
-
const
|
|
759
|
-
const
|
|
760
|
-
const line = ` ${cursor} ${idText} ${entryInfo}`;
|
|
806
|
+
const idText = isSelected ? accent(listing.id) : text(listing.id);
|
|
807
|
+
const line = ` ${cursor} ${idText}`;
|
|
761
808
|
lines.push(makeRow(truncateToWidth(line, innerW), innerW, border));
|
|
762
809
|
}
|
|
763
810
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goosedump",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Coding agent context data browser plugin for pi",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"goosedump",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@earendil-works/pi-tui": "^0.78.0",
|
|
32
|
-
"@jarkkojs/goosedump": "^0.1
|
|
32
|
+
"@jarkkojs/goosedump": "^0.2.1",
|
|
33
33
|
"@sinclair/typebox": "^0.34.49"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|