pi-goosedump 0.3.4 → 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 +5 -4
  2. package/index.ts +204 -110
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -21,7 +21,7 @@ Once installed, pi-goosedump registers a tool and a slash command:
21
21
 
22
22
  ### Tool: `goosedump`
23
23
 
24
- The agent can browse session history with:
24
+ The agent can browse the current session by default or a named session with `contextId`:
25
25
 
26
26
  | Action | Description |
27
27
  | -------- | ---------------------------------------- |
@@ -34,9 +34,10 @@ Examples:
34
34
 
35
35
  ```
36
36
  goosedump({ action: "list" })
37
+ goosedump({ action: "search", query: "bug fix" })
37
38
  goosedump({ action: "search", contextId: "abc123", query: "bug fix" })
38
- goosedump({ action: "expand", contextId: "abc123", ids: ["entry-a", "entry-b"] })
39
- goosedump({ action: "view", contextId: "abc123" })
39
+ goosedump({ action: "expand", ids: ["entry-a", "entry-b"] })
40
+ goosedump({ action: "view" })
40
41
  ```
41
42
 
42
43
  ### Command: `/goosedump`
@@ -46,7 +47,7 @@ Opens an interactive session browser.
46
47
  ### Compaction
47
48
 
48
49
  pi-goosedump hooks Pi's `/compact` and auto-compaction flow. It uses
49
- `goosedump compact pi <session>` with Pi's compaction range (`--from`,
50
+ `goosedump compact pi:<session>` with Pi's compaction range (`--from`,
50
51
  `--until`, and `--scope`) so the generated summary matches the entries Pi is
51
52
  about to replace.
52
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 {
@@ -551,13 +629,14 @@ export function createGoosedumpIntegration(
551
629
  name: 'goosedump',
552
630
  label: 'goosedump',
553
631
  description:
554
- '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.',
632
+ 'Browse coding agent session history. List all sessions, or view/search the current or a named session. Supports ranked search, compact overview, and result expansion. Default scope is active lineage.',
555
633
  promptSnippet:
556
- 'goosedump({ action: "search", contextId, query, scope?, page? }) - search session history',
634
+ 'goosedump({ action: "search", query, contextId?, scope?, page? }) - search session history; omit contextId for current session',
557
635
  promptGuidelines: [
558
636
  'When researching past conversation history, use goosedump to find relevant context.',
559
637
  'Start with compact ranked search (compact: true) for quick overview, then expand interesting entries.',
560
638
  'Default scope is "lineage" (current branch); use scope: "all" to include all entries in the session.',
639
+ 'Omit contextId to search, expand, or view the current Pi session; use list first for other sessions.',
561
640
  ],
562
641
  parameters: Type.Object({
563
642
  action: Type.Union(
@@ -574,7 +653,8 @@ export function createGoosedumpIntegration(
574
653
  ),
575
654
  contextId: Type.Optional(
576
655
  Type.String({
577
- description: 'Context/session ID (required for search, expand, view)',
656
+ description:
657
+ 'Context/session ID (defaults to current session for search, expand, view)',
578
658
  }),
579
659
  ),
580
660
  query: Type.Optional(
@@ -605,7 +685,7 @@ export function createGoosedumpIntegration(
605
685
  }),
606
686
  ),
607
687
  }),
608
- async execute(_id, params, _signal, _onUpdate, _ctx): Promise<AgentToolResult<void>> {
688
+ async execute(_id, params, _signal, _onUpdate, ctx): Promise<AgentToolResult<void>> {
609
689
  if (!goosedumpEnabled || !goosedumpReady) {
610
690
  return {
611
691
  content: [
@@ -641,9 +721,15 @@ export function createGoosedumpIntegration(
641
721
  }
642
722
 
643
723
  case 'search': {
644
- if (!params.contextId) {
724
+ const contextId = params.contextId ?? currentSessionContextId(ctx);
725
+ if (!contextId) {
645
726
  return {
646
- content: [{ type: 'text', text: 'contextId is required for search' }],
727
+ content: [
728
+ {
729
+ type: 'text',
730
+ text: 'contextId is required for search when no current session is available',
731
+ },
732
+ ],
647
733
  details: undefined,
648
734
  };
649
735
  }
@@ -655,10 +741,9 @@ export function createGoosedumpIntegration(
655
741
  };
656
742
  }
657
743
 
658
- const result = goosedumpContext(params.contextId, {
744
+ const result = goosedumpContext(contextId, {
659
745
  scope: params.scope ?? 'lineage',
660
746
  grep: params.query,
661
- compact: params.compact ?? true,
662
747
  rank: true,
663
748
  page: params.page ?? 1,
664
749
  });
@@ -683,9 +768,15 @@ export function createGoosedumpIntegration(
683
768
  }
684
769
 
685
770
  case 'expand': {
686
- if (!params.contextId) {
771
+ const contextId = params.contextId ?? currentSessionContextId(ctx);
772
+ if (!contextId) {
687
773
  return {
688
- content: [{ type: 'text', text: 'contextId is required for expand' }],
774
+ content: [
775
+ {
776
+ type: 'text',
777
+ text: 'contextId is required for expand when no current session is available',
778
+ },
779
+ ],
689
780
  details: undefined,
690
781
  };
691
782
  }
@@ -700,7 +791,7 @@ export function createGoosedumpIntegration(
700
791
  }
701
792
 
702
793
  const messages = goosedumpExpand(
703
- params.contextId,
794
+ contextId,
704
795
  params.ids,
705
796
  params.scope ?? 'lineage',
706
797
  );
@@ -719,23 +810,26 @@ export function createGoosedumpIntegration(
719
810
  }
720
811
 
721
812
  case 'view': {
722
- if (!params.contextId) {
813
+ const contextId = params.contextId ?? currentSessionContextId(ctx);
814
+ if (!contextId) {
723
815
  return {
724
- content: [{ type: 'text', text: 'contextId is required for view' }],
816
+ content: [
817
+ {
818
+ type: 'text',
819
+ text: 'contextId is required for view when no current session is available',
820
+ },
821
+ ],
725
822
  details: undefined,
726
823
  };
727
824
  }
728
825
 
729
- const result = goosedumpContext(params.contextId, {
826
+ const result = goosedumpContext(contextId, {
730
827
  scope: params.scope ?? 'lineage',
731
- compact: false,
732
828
  });
733
829
 
734
830
  if (result.messages.length === 0) {
735
831
  return {
736
- content: [
737
- { type: 'text', text: `Session "${params.contextId}" has no messages.` },
738
- ],
832
+ content: [{ type: 'text', text: `Session "${contextId}" has no messages.` }],
739
833
  details: undefined,
740
834
  };
741
835
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.3.4",
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": {