pi-goosedump 0.3.5 → 0.5.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 +282 -95
  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,10 +23,11 @@ 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, 5, 0] as const;
27
27
 
28
28
  interface GoosedumpListing {
29
29
  id: string;
30
+ path: string | null;
30
31
  }
31
32
 
32
33
  interface GoosedumpMessage {
@@ -43,6 +44,62 @@ interface GoosedumpContextResult {
43
44
  totalPages: number;
44
45
  }
45
46
 
47
+ interface ListJson {
48
+ sessions: { id: string; path?: string | null }[];
49
+ }
50
+
51
+ interface ToolCallJson {
52
+ name: string;
53
+ arguments: unknown;
54
+ }
55
+
56
+ interface ShowMessageJson {
57
+ entryId: string;
58
+ kind: 'text' | 'assistant' | 'toolResult' | 'bash';
59
+ role?: string;
60
+ text?: string;
61
+ thinking?: string[];
62
+ toolCalls?: ToolCallJson[];
63
+ toolName?: string;
64
+ content?: string;
65
+ isError?: boolean;
66
+ command?: string;
67
+ output?: string;
68
+ }
69
+
70
+ interface ShowJson {
71
+ messages: ShowMessageJson[];
72
+ }
73
+
74
+ interface HitJson {
75
+ entryId: string;
76
+ role: string;
77
+ score: number;
78
+ text: string;
79
+ files: string[];
80
+ terms: string[];
81
+ }
82
+
83
+ interface SearchJson {
84
+ page: number;
85
+ totalPages: number;
86
+ total: number;
87
+ hits: HitJson[];
88
+ }
89
+
90
+ interface GrepJson {
91
+ hits: HitJson[];
92
+ }
93
+
94
+ interface CompactJson {
95
+ goals: string[];
96
+ files: { modified: string[]; created: string[]; read: string[] };
97
+ commits: string[];
98
+ outstanding: string[];
99
+ preferences: string[];
100
+ brief: string;
101
+ }
102
+
46
103
  function resolveGoosedumpBinary(): string {
47
104
  try {
48
105
  return require.resolve('@jarkkojs/goosedump/bin/goosedump.js');
@@ -92,66 +149,76 @@ function hasMinimumVersion(version: string, minimum: readonly [number, number, n
92
149
  return true;
93
150
  }
94
151
 
95
- function parseListings(output: string): GoosedumpListing[] {
96
- const listings: GoosedumpListing[] = [];
97
- const lines = output.split('\n');
152
+ function clip(value: string, max: number): string {
153
+ return value.length > max ? `${value.slice(0, max)}...` : value;
154
+ }
98
155
 
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] });
156
+ function summarizeToolArgs(args: unknown): string {
157
+ if (args && typeof args === 'object' && !Array.isArray(args)) {
158
+ const obj = args as Record<string, unknown>;
159
+ for (const key of ['path', 'file_path', 'filePath', 'file']) {
160
+ if (typeof obj[key] === 'string') return `path=${obj[key] as string}`;
161
+ }
162
+ for (const key of ['command', 'query', 'pattern', 'description']) {
163
+ if (typeof obj[key] === 'string') return `${key}=${clip(obj[key] as string, 160)}`;
104
164
  }
165
+ const keys = Object.keys(obj);
166
+ return keys.length === 0 ? '' : keys.sort().join(', ');
105
167
  }
106
-
107
- return listings;
168
+ if (typeof args === 'string') return clip(args, 160);
169
+ if (args == null) return '';
170
+ return clip(JSON.stringify(args), 160);
108
171
  }
109
172
 
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;
173
+ function showMessageRole(m: ShowMessageJson): string {
174
+ switch (m.kind) {
175
+ case 'assistant':
176
+ return 'assistant';
177
+ case 'toolResult':
178
+ return `tool/${m.toolName ?? ''}${m.isError ? ' ⚠' : ''}`;
179
+ case 'bash':
180
+ return 'bash';
181
+ default:
182
+ return m.role && m.role.length > 0 ? m.role : 'unknown';
183
+ }
184
+ }
118
185
 
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);
186
+ function showMessageContent(m: ShowMessageJson): string {
187
+ switch (m.kind) {
188
+ case 'assistant': {
189
+ const parts: string[] = [];
190
+ if (m.thinking && m.thinking.length > 0) parts.push(m.thinking.join('\n'));
191
+ if (m.text) parts.push(m.text);
192
+ if (m.toolCalls && m.toolCalls.length > 0) {
193
+ parts.push(
194
+ m.toolCalls
195
+ .map((tc) => {
196
+ const summary = summarizeToolArgs(tc.arguments);
197
+ return summary ? `${tc.name}(${summary})` : tc.name;
198
+ })
199
+ .join('\n'),
200
+ );
124
201
  }
125
- currentMsg = {
126
- id: entryMatch[1],
127
- role: entryMatch[2],
128
- content: '',
129
- };
130
- contentLines = [];
131
- continue;
202
+ return parts.join('\n');
132
203
  }
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
- }
204
+ case 'toolResult':
205
+ return m.content ?? '';
206
+ case 'bash': {
207
+ const cmd = m.command ? `$ ${m.command}` : '';
208
+ if (cmd && m.output) return `${cmd}\n${m.output}`;
209
+ return cmd || (m.output ?? '');
146
210
  }
211
+ default:
212
+ return m.text ?? '';
147
213
  }
214
+ }
148
215
 
149
- if (currentMsg) {
150
- currentMsg.content = contentLines.join('\n').trim();
151
- messages.push(currentMsg);
152
- }
216
+ function showMessage(m: ShowMessageJson): GoosedumpMessage {
217
+ return { id: m.entryId, role: showMessageRole(m), content: showMessageContent(m) };
218
+ }
153
219
 
154
- return messages;
220
+ function hitMessage(h: HitJson): GoosedumpMessage {
221
+ return { id: h.entryId, role: h.role, content: h.text, score: h.score };
155
222
  }
156
223
 
157
224
  function formatMessagesCompact(messages: GoosedumpMessage[]): string {
@@ -172,21 +239,12 @@ function runGoosedump(args: string[]): string {
172
239
  });
173
240
  }
174
241
 
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
242
  function goosedumpList(): GoosedumpListing[] {
188
- const output = runGoosedump(['list', 'pi']);
189
- return parseListings(output);
243
+ const json = JSON.parse(runGoosedump(['list', 'pi:*'])) as ListJson;
244
+ return (json.sessions ?? []).map((session) => ({
245
+ id: session.id,
246
+ path: session.path ?? null,
247
+ }));
190
248
  }
191
249
 
192
250
  function goosedumpContext(
@@ -194,7 +252,6 @@ function goosedumpContext(
194
252
  options: {
195
253
  scope?: string;
196
254
  grep?: string;
197
- compact?: boolean;
198
255
  ids?: string;
199
256
  rank?: boolean;
200
257
  page?: number;
@@ -202,33 +259,35 @@ function goosedumpContext(
202
259
  ): GoosedumpContextResult {
203
260
  const scope = options.scope ?? 'lineage';
204
261
  const hasQuery = typeof options.grep === 'string';
262
+ const isSearch = hasQuery && options.rank !== false;
263
+ const target = `pi:${contextId}`;
205
264
  const args = hasQuery
206
- ? options.rank === false
207
- ? ['grep', 'pi', contextId, options.grep ?? '']
208
- : ['search', 'pi', contextId, options.grep ?? '']
209
- : ['show', 'pi', contextId];
265
+ ? isSearch
266
+ ? ['search', target, options.grep ?? '']
267
+ : ['grep', target, options.grep ?? '']
268
+ : ['show', target];
210
269
 
211
- if (options.compact) {
212
- args.push('--clip');
213
- }
214
270
  if (options.ids) {
215
271
  args.push('--ids', options.ids);
216
272
  }
217
- if (hasQuery && options.rank !== false && options.page) {
273
+ if (isSearch && options.page) {
218
274
  args.push('--page', String(options.page));
219
275
  }
220
276
  args.push('--scope', scope);
221
277
 
222
278
  const output = runGoosedump(args);
223
- const messages = parseMessages(output);
224
- const pageInfo = parsePageInfo(output, options.page ?? 1);
225
279
 
226
- return {
227
- messages,
228
- totalCount: messages.length,
229
- page: pageInfo.page,
230
- totalPages: pageInfo.totalPages,
231
- };
280
+ if (hasQuery) {
281
+ const json = JSON.parse(output) as SearchJson | GrepJson;
282
+ const messages = (json.hits ?? []).map(hitMessage);
283
+ const page = isSearch ? ((json as SearchJson).page ?? options.page ?? 1) : 1;
284
+ const totalPages = isSearch ? ((json as SearchJson).totalPages ?? 1) : 1;
285
+ return { messages, totalCount: messages.length, page, totalPages };
286
+ }
287
+
288
+ const json = JSON.parse(output) as ShowJson;
289
+ const messages = (json.messages ?? []).map(showMessage);
290
+ return { messages, totalCount: messages.length, page: options.page ?? 1, totalPages: 1 };
232
291
  }
233
292
 
234
293
  function goosedumpExpand(
@@ -238,16 +297,38 @@ function goosedumpExpand(
238
297
  ): GoosedumpMessage[] {
239
298
  if (entryIds.length === 0) return [];
240
299
 
241
- const output = runGoosedump([
242
- 'show',
243
- 'pi',
244
- contextId,
245
- '--ids',
246
- entryIds.join(','),
247
- '--scope',
248
- scope,
249
- ]);
250
- return parseMessages(output);
300
+ const json = JSON.parse(
301
+ runGoosedump(['show', `pi:${contextId}`, '--ids', entryIds.join(','), '--scope', scope]),
302
+ ) as ShowJson;
303
+ return (json.messages ?? []).map(showMessage);
304
+ }
305
+
306
+ function capList(items: string[], limit: number): string {
307
+ const shown = items.slice(0, limit).join(', ');
308
+ return items.length > limit ? `${shown} (+${items.length - limit} more)` : shown;
309
+ }
310
+
311
+ function bulletSection(title: string, items: string[]): string | null {
312
+ if (items.length === 0) return null;
313
+ return `[${title}]\n${items.map((item) => `- ${item}`).join('\n')}`;
314
+ }
315
+
316
+ function renderCompactSummary(c: CompactJson): string {
317
+ const fileLines: string[] = [];
318
+ if (c.files.modified.length > 0) fileLines.push(`Modified: ${capList(c.files.modified, 10)}`);
319
+ if (c.files.created.length > 0) fileLines.push(`Created: ${capList(c.files.created, 10)}`);
320
+ if (c.files.read.length > 0) fileLines.push(`Read: ${capList(c.files.read, 10)}`);
321
+
322
+ const parts = [
323
+ bulletSection('Session Goal', c.goals),
324
+ bulletSection('Files And Changes', fileLines),
325
+ bulletSection('Commits', c.commits),
326
+ bulletSection('Outstanding Context', c.outstanding),
327
+ bulletSection('User Preferences', c.preferences),
328
+ c.brief.trim() ? c.brief : null,
329
+ ].filter((part): part is string => part !== null);
330
+
331
+ return parts.join('\n\n---\n\n');
251
332
  }
252
333
 
253
334
  function goosedumpCompact(
@@ -260,7 +341,7 @@ function goosedumpCompact(
260
341
  },
261
342
  ): string {
262
343
  const scope = options.scope ?? 'lineage';
263
- const args = ['compact', 'pi', contextId, '--scope', scope];
344
+ const args = ['compact', `pi:${contextId}`, '--scope', scope];
264
345
 
265
346
  if (options.ids && options.ids.length > 0) {
266
347
  args.push('--ids', options.ids.join(','));
@@ -269,7 +350,8 @@ function goosedumpCompact(
269
350
  if (options.until) args.push('--until', options.until);
270
351
  }
271
352
 
272
- return runGoosedump(args).trim();
353
+ const json = JSON.parse(runGoosedump(args)) as CompactJson;
354
+ return renderCompactSummary(json);
273
355
  }
274
356
 
275
357
  export interface GoosedumpIntegrationOptions {
@@ -666,7 +748,6 @@ export function createGoosedumpIntegration(
666
748
  const result = goosedumpContext(contextId, {
667
749
  scope: params.scope ?? 'lineage',
668
750
  grep: params.query,
669
- compact: params.compact ?? true,
670
751
  rank: true,
671
752
  page: params.page ?? 1,
672
753
  });
@@ -748,7 +829,6 @@ export function createGoosedumpIntegration(
748
829
 
749
830
  const result = goosedumpContext(contextId, {
750
831
  scope: params.scope ?? 'lineage',
751
- compact: false,
752
832
  });
753
833
 
754
834
  if (result.messages.length === 0) {
@@ -865,9 +945,17 @@ export function createGoosedumpIntegration(
865
945
  return;
866
946
  }
867
947
 
948
+ let resumePath: string | null | undefined;
949
+
868
950
  await ctx.ui.custom<void>(
869
951
  (tui, theme, _kb, done) => {
870
952
  let selectedIndex = 0;
953
+ let mode: 'list' | 'compact' = 'list';
954
+ let compactLines: string[] = [];
955
+ let compactScroll = 0;
956
+ let compactTitle = '';
957
+
958
+ const COMPACT_VIEWPORT = 20;
871
959
 
872
960
  return {
873
961
  render(width: number): string[] {
@@ -881,6 +969,39 @@ export function createGoosedumpIntegration(
881
969
 
882
970
  const lines: string[] = [];
883
971
 
972
+ if (mode === 'compact') {
973
+ const title = accent(` Compact: ${truncateToWidth(compactTitle, 40)} `);
974
+ const topFill = borderFg(
975
+ '─'.repeat(Math.max(0, width - 4 - visibleWidth(title))),
976
+ );
977
+ lines.push(`${borderFg('╭─')}${title}${topFill}${borderFg('─╮')}`);
978
+
979
+ const total = compactLines.length;
980
+ const maxScroll = Math.max(0, total - COMPACT_VIEWPORT);
981
+ if (compactScroll > maxScroll) compactScroll = maxScroll;
982
+ const window = compactLines.slice(
983
+ compactScroll,
984
+ compactScroll + COMPACT_VIEWPORT,
985
+ );
986
+ for (const row of window) {
987
+ lines.push(makeRow(text(truncateToWidth(row, innerW)), innerW, border));
988
+ }
989
+ for (let i = window.length; i < COMPACT_VIEWPORT; i++) {
990
+ lines.push(makeRow('', innerW, border));
991
+ }
992
+
993
+ const position =
994
+ total > COMPACT_VIEWPORT
995
+ ? `${compactScroll + 1}-${Math.min(compactScroll + COMPACT_VIEWPORT, total)} of ${total}`
996
+ : `${total} line${total === 1 ? '' : 's'}`;
997
+ lines.push(makeRow('', innerW, border));
998
+ lines.push(makeRow(dim(`↑↓ scroll esc back (${position})`), innerW, border));
999
+ lines.push(
1000
+ `${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`,
1001
+ );
1002
+ return lines;
1003
+ }
1004
+
884
1005
  // Top border
885
1006
  const title = accent(' Goosedump Sessions ');
886
1007
  const topFill = borderFg(
@@ -913,7 +1034,13 @@ export function createGoosedumpIntegration(
913
1034
 
914
1035
  // Footer
915
1036
  lines.push(makeRow('', innerW, border));
916
- lines.push(makeRow(dim('↑↓ navigate esc close'), innerW, border));
1037
+ lines.push(
1038
+ makeRow(
1039
+ dim('↑↓ navigate enter compact shift+enter resume esc close'),
1040
+ innerW,
1041
+ border,
1042
+ ),
1043
+ );
917
1044
 
918
1045
  // Bottom border
919
1046
  lines.push(`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`);
@@ -922,6 +1049,35 @@ export function createGoosedumpIntegration(
922
1049
  },
923
1050
 
924
1051
  handleInput(data: string): void {
1052
+ if (mode === 'compact') {
1053
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
1054
+ mode = 'list';
1055
+ tui.requestRender();
1056
+ return;
1057
+ }
1058
+ if (matchesKey(data, Key.up)) {
1059
+ compactScroll = Math.max(0, compactScroll - 1);
1060
+ tui.requestRender();
1061
+ return;
1062
+ }
1063
+ if (matchesKey(data, Key.down)) {
1064
+ compactScroll += 1;
1065
+ tui.requestRender();
1066
+ return;
1067
+ }
1068
+ if (matchesKey(data, Key.pageUp)) {
1069
+ compactScroll = Math.max(0, compactScroll - COMPACT_VIEWPORT);
1070
+ tui.requestRender();
1071
+ return;
1072
+ }
1073
+ if (matchesKey(data, Key.pageDown)) {
1074
+ compactScroll += COMPACT_VIEWPORT;
1075
+ tui.requestRender();
1076
+ return;
1077
+ }
1078
+ return;
1079
+ }
1080
+
925
1081
  if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
926
1082
  done(undefined);
927
1083
  return;
@@ -938,6 +1094,31 @@ export function createGoosedumpIntegration(
938
1094
  tui.requestRender();
939
1095
  return;
940
1096
  }
1097
+
1098
+ if (matchesKey(data, Key.shift(Key.enter))) {
1099
+ resumePath = listings[selectedIndex]?.path;
1100
+ done(undefined);
1101
+ return;
1102
+ }
1103
+
1104
+ if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
1105
+ const listing = listings[selectedIndex];
1106
+ if (!listing) return;
1107
+ compactTitle = listing.id;
1108
+ compactScroll = 0;
1109
+ try {
1110
+ const summary = goosedumpCompact(listing.id, { scope: 'lineage' }).trim();
1111
+ compactLines = summary
1112
+ ? summary.split('\n')
1113
+ : ['No compact summary available for this session.'];
1114
+ } catch (err) {
1115
+ const msg = err instanceof Error ? err.message : String(err);
1116
+ compactLines = [`Failed to compact session: ${msg}`];
1117
+ }
1118
+ mode = 'compact';
1119
+ tui.requestRender();
1120
+ return;
1121
+ }
941
1122
  },
942
1123
 
943
1124
  invalidate(): void {},
@@ -958,6 +1139,12 @@ export function createGoosedumpIntegration(
958
1139
  },
959
1140
  },
960
1141
  );
1142
+
1143
+ if (resumePath) {
1144
+ await ctx.switchSession(resumePath);
1145
+ } else if (resumePath === null) {
1146
+ ctx.ui.notify('Cannot resume: session file path is unavailable.', 'error');
1147
+ }
961
1148
  } catch (err) {
962
1149
  const message = err instanceof Error ? err.message : String(err);
963
1150
  ctx.ui.notify(`goosedump error: ${message}`, 'error');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.3.5",
3
+ "version": "0.5.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.5.0",
33
33
  "@sinclair/typebox": "^0.34.49"
34
34
  },
35
35
  "devDependencies": {