pi-goosedump 0.3.5 → 0.4.0

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/index.ts +170 -94
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -47,7 +47,7 @@ Opens an interactive session browser.
47
47
  ### Compaction
48
48
 
49
49
  pi-goosedump hooks Pi's `/compact` and auto-compaction flow. It uses
50
- `goosedump compact pi <session>` with Pi's compaction range (`--from`,
50
+ `goosedump compact pi:<session>` with Pi's compaction range (`--from`,
51
51
  `--until`, and `--scope`) so the generated summary matches the entries Pi is
52
52
  about to replace.
53
53
 
package/index.ts CHANGED
@@ -23,7 +23,7 @@ import { Type } from '@sinclair/typebox';
23
23
 
24
24
  const require = createRequire(import.meta.url);
25
25
 
26
- const GOOSEDUMP_VERSION = [0, 3, 3] as const;
26
+ const GOOSEDUMP_VERSION = [0, 4, 2] as const;
27
27
 
28
28
  interface GoosedumpListing {
29
29
  id: string;
@@ -43,6 +43,62 @@ interface GoosedumpContextResult {
43
43
  totalPages: number;
44
44
  }
45
45
 
46
+ interface ListJson {
47
+ sessions: { id: string }[];
48
+ }
49
+
50
+ interface ToolCallJson {
51
+ name: string;
52
+ arguments: unknown;
53
+ }
54
+
55
+ interface ShowMessageJson {
56
+ entryId: string;
57
+ kind: 'text' | 'assistant' | 'toolResult' | 'bash';
58
+ role?: string;
59
+ text?: string;
60
+ thinking?: string[];
61
+ toolCalls?: ToolCallJson[];
62
+ toolName?: string;
63
+ content?: string;
64
+ isError?: boolean;
65
+ command?: string;
66
+ output?: string;
67
+ }
68
+
69
+ interface ShowJson {
70
+ messages: ShowMessageJson[];
71
+ }
72
+
73
+ interface HitJson {
74
+ entryId: string;
75
+ role: string;
76
+ score: number;
77
+ text: string;
78
+ files: string[];
79
+ terms: string[];
80
+ }
81
+
82
+ interface SearchJson {
83
+ page: number;
84
+ totalPages: number;
85
+ total: number;
86
+ hits: HitJson[];
87
+ }
88
+
89
+ interface GrepJson {
90
+ hits: HitJson[];
91
+ }
92
+
93
+ interface CompactJson {
94
+ goals: string[];
95
+ files: { modified: string[]; created: string[]; read: string[] };
96
+ commits: string[];
97
+ outstanding: string[];
98
+ preferences: string[];
99
+ brief: string;
100
+ }
101
+
46
102
  function resolveGoosedumpBinary(): string {
47
103
  try {
48
104
  return require.resolve('@jarkkojs/goosedump/bin/goosedump.js');
@@ -92,66 +148,76 @@ function hasMinimumVersion(version: string, minimum: readonly [number, number, n
92
148
  return true;
93
149
  }
94
150
 
95
- function parseListings(output: string): GoosedumpListing[] {
96
- const listings: GoosedumpListing[] = [];
97
- const lines = output.split('\n');
151
+ function clip(value: string, max: number): string {
152
+ return value.length > max ? `${value.slice(0, max)}...` : value;
153
+ }
98
154
 
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] });
155
+ function summarizeToolArgs(args: unknown): string {
156
+ if (args && typeof args === 'object' && !Array.isArray(args)) {
157
+ const obj = args as Record<string, unknown>;
158
+ for (const key of ['path', 'file_path', 'filePath', 'file']) {
159
+ if (typeof obj[key] === 'string') return `path=${obj[key] as string}`;
160
+ }
161
+ for (const key of ['command', 'query', 'pattern', 'description']) {
162
+ if (typeof obj[key] === 'string') return `${key}=${clip(obj[key] as string, 160)}`;
104
163
  }
164
+ const keys = Object.keys(obj);
165
+ return keys.length === 0 ? '' : keys.sort().join(', ');
105
166
  }
106
-
107
- return listings;
167
+ if (typeof args === 'string') return clip(args, 160);
168
+ if (args == null) return '';
169
+ return clip(JSON.stringify(args), 160);
108
170
  }
109
171
 
110
- function parseMessages(output: string): GoosedumpMessage[] {
111
- const messages: GoosedumpMessage[] = [];
112
- const lines = output.split('\n');
113
- let currentMsg: GoosedumpMessage | null = null;
114
- let contentLines: string[] = [];
115
-
116
- for (const line of lines) {
117
- if (line.startsWith('results for ')) continue;
172
+ function showMessageRole(m: ShowMessageJson): string {
173
+ switch (m.kind) {
174
+ case 'assistant':
175
+ return 'assistant';
176
+ case 'toolResult':
177
+ return `tool/${m.toolName ?? ''}${m.isError ? ' ⚠' : ''}`;
178
+ case 'bash':
179
+ return 'bash';
180
+ default:
181
+ return m.role && m.role.length > 0 ? m.role : 'unknown';
182
+ }
183
+ }
118
184
 
119
- const entryMatch = line.match(/^entryId:\s+(\S+)\s+\((.+?)\)(?:\s+\[[^\]]+\])?$/);
120
- if (entryMatch) {
121
- if (currentMsg) {
122
- currentMsg.content = contentLines.join('\n').trim();
123
- messages.push(currentMsg);
185
+ function showMessageContent(m: ShowMessageJson): string {
186
+ switch (m.kind) {
187
+ case 'assistant': {
188
+ const parts: string[] = [];
189
+ if (m.thinking && m.thinking.length > 0) parts.push(m.thinking.join('\n'));
190
+ if (m.text) parts.push(m.text);
191
+ if (m.toolCalls && m.toolCalls.length > 0) {
192
+ parts.push(
193
+ m.toolCalls
194
+ .map((tc) => {
195
+ const summary = summarizeToolArgs(tc.arguments);
196
+ return summary ? `${tc.name}(${summary})` : tc.name;
197
+ })
198
+ .join('\n'),
199
+ );
124
200
  }
125
- currentMsg = {
126
- id: entryMatch[1],
127
- role: entryMatch[2],
128
- content: '',
129
- };
130
- contentLines = [];
131
- continue;
201
+ return parts.join('\n');
132
202
  }
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);
145
- }
203
+ case 'toolResult':
204
+ return m.content ?? '';
205
+ case 'bash': {
206
+ const cmd = m.command ? `$ ${m.command}` : '';
207
+ if (cmd && m.output) return `${cmd}\n${m.output}`;
208
+ return cmd || (m.output ?? '');
146
209
  }
210
+ default:
211
+ return m.text ?? '';
147
212
  }
213
+ }
148
214
 
149
- if (currentMsg) {
150
- currentMsg.content = contentLines.join('\n').trim();
151
- messages.push(currentMsg);
152
- }
215
+ function showMessage(m: ShowMessageJson): GoosedumpMessage {
216
+ return { id: m.entryId, role: showMessageRole(m), content: showMessageContent(m) };
217
+ }
153
218
 
154
- return messages;
219
+ function hitMessage(h: HitJson): GoosedumpMessage {
220
+ return { id: h.entryId, role: h.role, content: h.text, score: h.score };
155
221
  }
156
222
 
157
223
  function formatMessagesCompact(messages: GoosedumpMessage[]): string {
@@ -172,21 +238,9 @@ function runGoosedump(args: string[]): string {
172
238
  });
173
239
  }
174
240
 
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
241
  function goosedumpList(): GoosedumpListing[] {
188
- const output = runGoosedump(['list', 'pi']);
189
- return parseListings(output);
242
+ const json = JSON.parse(runGoosedump(['list', 'pi:*'])) as ListJson;
243
+ return (json.sessions ?? []).map((session) => ({ id: session.id }));
190
244
  }
191
245
 
192
246
  function goosedumpContext(
@@ -194,7 +248,6 @@ function goosedumpContext(
194
248
  options: {
195
249
  scope?: string;
196
250
  grep?: string;
197
- compact?: boolean;
198
251
  ids?: string;
199
252
  rank?: boolean;
200
253
  page?: number;
@@ -202,33 +255,35 @@ function goosedumpContext(
202
255
  ): GoosedumpContextResult {
203
256
  const scope = options.scope ?? 'lineage';
204
257
  const hasQuery = typeof options.grep === 'string';
258
+ const isSearch = hasQuery && options.rank !== false;
259
+ const target = `pi:${contextId}`;
205
260
  const args = hasQuery
206
- ? options.rank === false
207
- ? ['grep', 'pi', contextId, options.grep ?? '']
208
- : ['search', 'pi', contextId, options.grep ?? '']
209
- : ['show', 'pi', contextId];
261
+ ? isSearch
262
+ ? ['search', target, options.grep ?? '']
263
+ : ['grep', target, options.grep ?? '']
264
+ : ['show', target];
210
265
 
211
- if (options.compact) {
212
- args.push('--clip');
213
- }
214
266
  if (options.ids) {
215
267
  args.push('--ids', options.ids);
216
268
  }
217
- if (hasQuery && options.rank !== false && options.page) {
269
+ if (isSearch && options.page) {
218
270
  args.push('--page', String(options.page));
219
271
  }
220
272
  args.push('--scope', scope);
221
273
 
222
274
  const output = runGoosedump(args);
223
- const messages = parseMessages(output);
224
- const pageInfo = parsePageInfo(output, options.page ?? 1);
225
275
 
226
- return {
227
- messages,
228
- totalCount: messages.length,
229
- page: pageInfo.page,
230
- totalPages: pageInfo.totalPages,
231
- };
276
+ if (hasQuery) {
277
+ const json = JSON.parse(output) as SearchJson | GrepJson;
278
+ const messages = (json.hits ?? []).map(hitMessage);
279
+ const page = isSearch ? ((json as SearchJson).page ?? options.page ?? 1) : 1;
280
+ const totalPages = isSearch ? ((json as SearchJson).totalPages ?? 1) : 1;
281
+ return { messages, totalCount: messages.length, page, totalPages };
282
+ }
283
+
284
+ const json = JSON.parse(output) as ShowJson;
285
+ const messages = (json.messages ?? []).map(showMessage);
286
+ return { messages, totalCount: messages.length, page: options.page ?? 1, totalPages: 1 };
232
287
  }
233
288
 
234
289
  function goosedumpExpand(
@@ -238,16 +293,38 @@ function goosedumpExpand(
238
293
  ): GoosedumpMessage[] {
239
294
  if (entryIds.length === 0) return [];
240
295
 
241
- const output = runGoosedump([
242
- 'show',
243
- 'pi',
244
- contextId,
245
- '--ids',
246
- entryIds.join(','),
247
- '--scope',
248
- scope,
249
- ]);
250
- return parseMessages(output);
296
+ const json = JSON.parse(
297
+ runGoosedump(['show', `pi:${contextId}`, '--ids', entryIds.join(','), '--scope', scope]),
298
+ ) as ShowJson;
299
+ return (json.messages ?? []).map(showMessage);
300
+ }
301
+
302
+ function capList(items: string[], limit: number): string {
303
+ const shown = items.slice(0, limit).join(', ');
304
+ return items.length > limit ? `${shown} (+${items.length - limit} more)` : shown;
305
+ }
306
+
307
+ function bulletSection(title: string, items: string[]): string | null {
308
+ if (items.length === 0) return null;
309
+ return `[${title}]\n${items.map((item) => `- ${item}`).join('\n')}`;
310
+ }
311
+
312
+ function renderCompactSummary(c: CompactJson): string {
313
+ const fileLines: string[] = [];
314
+ if (c.files.modified.length > 0) fileLines.push(`Modified: ${capList(c.files.modified, 10)}`);
315
+ if (c.files.created.length > 0) fileLines.push(`Created: ${capList(c.files.created, 10)}`);
316
+ if (c.files.read.length > 0) fileLines.push(`Read: ${capList(c.files.read, 10)}`);
317
+
318
+ const parts = [
319
+ bulletSection('Session Goal', c.goals),
320
+ bulletSection('Files And Changes', fileLines),
321
+ bulletSection('Commits', c.commits),
322
+ bulletSection('Outstanding Context', c.outstanding),
323
+ bulletSection('User Preferences', c.preferences),
324
+ c.brief.trim() ? c.brief : null,
325
+ ].filter((part): part is string => part !== null);
326
+
327
+ return parts.join('\n\n---\n\n');
251
328
  }
252
329
 
253
330
  function goosedumpCompact(
@@ -260,7 +337,7 @@ function goosedumpCompact(
260
337
  },
261
338
  ): string {
262
339
  const scope = options.scope ?? 'lineage';
263
- const args = ['compact', 'pi', contextId, '--scope', scope];
340
+ const args = ['compact', `pi:${contextId}`, '--scope', scope];
264
341
 
265
342
  if (options.ids && options.ids.length > 0) {
266
343
  args.push('--ids', options.ids.join(','));
@@ -269,7 +346,8 @@ function goosedumpCompact(
269
346
  if (options.until) args.push('--until', options.until);
270
347
  }
271
348
 
272
- return runGoosedump(args).trim();
349
+ const json = JSON.parse(runGoosedump(args)) as CompactJson;
350
+ return renderCompactSummary(json);
273
351
  }
274
352
 
275
353
  export interface GoosedumpIntegrationOptions {
@@ -666,7 +744,6 @@ export function createGoosedumpIntegration(
666
744
  const result = goosedumpContext(contextId, {
667
745
  scope: params.scope ?? 'lineage',
668
746
  grep: params.query,
669
- compact: params.compact ?? true,
670
747
  rank: true,
671
748
  page: params.page ?? 1,
672
749
  });
@@ -748,7 +825,6 @@ export function createGoosedumpIntegration(
748
825
 
749
826
  const result = goosedumpContext(contextId, {
750
827
  scope: params.scope ?? 'lineage',
751
- compact: false,
752
828
  });
753
829
 
754
830
  if (result.messages.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
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.3.3",
32
+ "@jarkkojs/goosedump": "^0.4.2",
33
33
  "@sinclair/typebox": "^0.34.49"
34
34
  },
35
35
  "devDependencies": {