pi-goosedump 0.5.4 → 0.5.6

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 +2 -0
  2. package/index.ts +83 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -27,6 +27,7 @@ The agent can browse the current session by default or a named session with `con
27
27
  | -------- | ---------------------------------------- |
28
28
  | `list` | List all available sessions |
29
29
  | `search` | Rank messages by query relevance |
30
+ | `grep` | Filter messages by glob pattern |
30
31
  | `expand` | Show full content for specific entry IDs |
31
32
  | `view` | View the full session transcript |
32
33
 
@@ -36,6 +37,7 @@ Examples:
36
37
  goosedump({ action: "list" })
37
38
  goosedump({ action: "search", query: "bug fix" })
38
39
  goosedump({ action: "search", contextId: "abc123", query: "bug fix" })
40
+ goosedump({ action: "grep", pattern: "*rand*" })
39
41
  goosedump({ action: "expand", ids: ["entry-a", "entry-b"] })
40
42
  goosedump({ action: "view" })
41
43
  ```
package/index.ts CHANGED
@@ -18,7 +18,13 @@ import { createRequire } from 'node:module';
18
18
  import { homedir } from 'node:os';
19
19
  import { basename, join } from 'node:path';
20
20
 
21
- import { Key, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
21
+ import {
22
+ Key,
23
+ matchesKey,
24
+ truncateToWidth,
25
+ visibleWidth,
26
+ wrapTextWithAnsi,
27
+ } from '@earendil-works/pi-tui';
22
28
  import { Type } from '@sinclair/typebox';
23
29
 
24
30
  const require = createRequire(import.meta.url);
@@ -109,10 +115,6 @@ function resolveGoosedumpBinary(): string {
109
115
  }
110
116
  }
111
117
 
112
- function binaryPath(): string {
113
- return resolveGoosedumpBinary();
114
- }
115
-
116
118
  function goosedumpVersion(): string | null {
117
119
  try {
118
120
  const pkg = require('@jarkkojs/goosedump/package.json') as { version: string };
@@ -212,12 +214,16 @@ function hitMessage(h: HitJson): GoosedumpMessage {
212
214
  return { id: h.entryId, role: h.role, content: h.text, score: h.score };
213
215
  }
214
216
 
217
+ const ANSI_SGR = new RegExp(String.fromCharCode(27) + '\\[[0-9;]*m', 'g');
218
+
219
+ function compactLine(content: string): string {
220
+ const plain = content.replace(ANSI_SGR, '');
221
+ const collapsed = plain.replace(/\s+/g, ' ').trim();
222
+ return collapsed.length > 80 ? `${collapsed.slice(0, 80)}...` : collapsed;
223
+ }
224
+
215
225
  function formatMessagesCompact(messages: GoosedumpMessage[]): string {
216
- return messages
217
- .map(
218
- (m) => `[${m.id}] ${m.role}: ${m.content.slice(0, 80)}${m.content.length > 80 ? '...' : ''}`,
219
- )
220
- .join('\n');
226
+ return messages.map((m) => `[${m.id}] ${m.role}: ${compactLine(m.content)}`).join('\n');
221
227
  }
222
228
 
223
229
  function formatMessagesFull(messages: GoosedumpMessage[]): string {
@@ -233,7 +239,7 @@ function missingContextId(action: string): AgentToolResult<void> {
233
239
  }
234
240
 
235
241
  function runGoosedump(args: string[]): string {
236
- return execFileSync('node', [binaryPath(), ...args], {
242
+ return execFileSync('node', [resolveGoosedumpBinary(), ...args], {
237
243
  encoding: 'utf-8',
238
244
  });
239
245
  }
@@ -297,6 +303,18 @@ function goosedumpSearch(
297
303
  return { messages, page: json.page ?? options.page ?? 1, totalPages: json.totalPages ?? 1 };
298
304
  }
299
305
 
306
+ function goosedumpGrep(
307
+ contextId: string,
308
+ pattern: string,
309
+ options: { scope?: string } = {},
310
+ ): GoosedumpMessage[] {
311
+ const scope = options.scope ?? 'lineage';
312
+ const json = JSON.parse(runGoosedump(['grep', `pi:${contextId}`, pattern, '--scope', scope])) as {
313
+ hits?: HitJson[];
314
+ };
315
+ return (json.hits ?? []).map(hitMessage);
316
+ }
317
+
300
318
  function goosedumpShow(
301
319
  contextId: string,
302
320
  options: { scope?: string; ids?: string[] } = {},
@@ -605,15 +623,20 @@ async function runSessionBrowser(
605
623
  ): Promise<string | null | undefined> {
606
624
  let resumePath: string | null | undefined;
607
625
 
626
+ const OVERLAY_WIDTH = 72;
627
+
608
628
  await ctx.ui.custom<void>(
609
629
  (tui, theme, _kb, done) => {
610
630
  let selectedIndex = 0;
631
+ let listScroll = 0;
611
632
  let mode: 'list' | 'compact' = 'list';
612
633
  let compactLines: string[] = [];
613
634
  let compactScroll = 0;
614
635
  let compactTitle = '';
615
636
 
616
637
  const COMPACT_VIEWPORT = 20;
638
+ const LIST_VIEWPORT = 20;
639
+ const wrapWidth = OVERLAY_WIDTH - 4;
617
640
 
618
641
  return {
619
642
  render(width: number): string[] {
@@ -633,11 +656,9 @@ async function runSessionBrowser(
633
656
  lines.push(`${borderFg('╭─')}${title}${topFill}${borderFg('─╮')}`);
634
657
 
635
658
  const total = compactLines.length;
636
- const maxScroll = Math.max(0, total - COMPACT_VIEWPORT);
637
- if (compactScroll > maxScroll) compactScroll = maxScroll;
638
659
  const window = compactLines.slice(compactScroll, compactScroll + COMPACT_VIEWPORT);
639
660
  for (const row of window) {
640
- lines.push(makeRow(text(truncateToWidth(row, innerW)), innerW, border));
661
+ lines.push(makeRow(text(row), innerW, border));
641
662
  }
642
663
  for (let i = window.length; i < COMPACT_VIEWPORT; i++) {
643
664
  lines.push(makeRow('', innerW, border));
@@ -665,18 +686,21 @@ async function runSessionBrowser(
665
686
  lines.push(makeRow('', innerW, border));
666
687
 
667
688
  // Session list
668
- const maxItems = Math.min(listings.length, 20);
669
- for (let i = 0; i < maxItems; i++) {
670
- const listing = listings[i];
671
- const isSelected = i === selectedIndex;
689
+ const total = listings.length;
690
+ const window = listings.slice(listScroll, listScroll + LIST_VIEWPORT);
691
+ for (let i = 0; i < window.length; i++) {
692
+ const absolute = listScroll + i;
693
+ const listing = window[i];
694
+ const isSelected = absolute === selectedIndex;
672
695
  const cursor = isSelected ? accent('▶') : ' ';
673
696
  const idText = isSelected ? accent(listing.id) : text(listing.id);
674
697
  const line = ` ${cursor} ${idText}`;
675
- lines.push(makeRow(truncateToWidth(line, innerW), innerW, border));
698
+ lines.push(makeRow(line, innerW, border));
676
699
  }
677
700
 
678
- if (listings.length > 20) {
679
- lines.push(makeRow(dim(` ... and ${listings.length - 20} more`), innerW, border));
701
+ if (total > LIST_VIEWPORT) {
702
+ const position = `${listScroll + 1}-${Math.min(listScroll + LIST_VIEWPORT, total)} of ${total}`;
703
+ lines.push(makeRow(dim(` (${position})`), innerW, border));
680
704
  }
681
705
 
682
706
  // Footer
@@ -697,6 +721,7 @@ async function runSessionBrowser(
697
721
 
698
722
  handleInput(data: string): void {
699
723
  if (mode === 'compact') {
724
+ const maxScroll = Math.max(0, compactLines.length - COMPACT_VIEWPORT);
700
725
  if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
701
726
  mode = 'list';
702
727
  tui.requestRender();
@@ -708,7 +733,7 @@ async function runSessionBrowser(
708
733
  return;
709
734
  }
710
735
  if (matchesKey(data, Key.down)) {
711
- compactScroll += 1;
736
+ compactScroll = Math.min(maxScroll, compactScroll + 1);
712
737
  tui.requestRender();
713
738
  return;
714
739
  }
@@ -718,7 +743,7 @@ async function runSessionBrowser(
718
743
  return;
719
744
  }
720
745
  if (matchesKey(data, Key.pageDown)) {
721
- compactScroll += COMPACT_VIEWPORT;
746
+ compactScroll = Math.min(maxScroll, compactScroll + COMPACT_VIEWPORT);
722
747
  tui.requestRender();
723
748
  return;
724
749
  }
@@ -732,12 +757,16 @@ async function runSessionBrowser(
732
757
 
733
758
  if (matchesKey(data, Key.up)) {
734
759
  selectedIndex = Math.max(0, selectedIndex - 1);
760
+ if (selectedIndex < listScroll) listScroll = selectedIndex;
735
761
  tui.requestRender();
736
762
  return;
737
763
  }
738
764
 
739
765
  if (matchesKey(data, Key.down)) {
740
766
  selectedIndex = Math.min(listings.length - 1, selectedIndex + 1);
767
+ if (selectedIndex >= listScroll + LIST_VIEWPORT) {
768
+ listScroll = selectedIndex - LIST_VIEWPORT + 1;
769
+ }
741
770
  tui.requestRender();
742
771
  return;
743
772
  }
@@ -756,11 +785,11 @@ async function runSessionBrowser(
756
785
  try {
757
786
  const summary = goosedumpCompact(listing.id, { scope: 'lineage' }).trim();
758
787
  compactLines = summary
759
- ? summary.split('\n')
788
+ ? wrapTextWithAnsi(summary, wrapWidth)
760
789
  : ['No compact summary available for this session.'];
761
790
  } catch (err) {
762
791
  const msg = err instanceof Error ? err.message : String(err);
763
- compactLines = [`Failed to compact session: ${msg}`];
792
+ compactLines = wrapTextWithAnsi(`Failed to compact session: ${msg}`, wrapWidth);
764
793
  }
765
794
  mode = 'compact';
766
795
  tui.requestRender();
@@ -781,7 +810,7 @@ async function runSessionBrowser(
781
810
  overlay: true,
782
811
  overlayOptions: {
783
812
  anchor: 'center',
784
- width: 72,
813
+ width: OVERLAY_WIDTH,
785
814
  margin: 2,
786
815
  },
787
816
  },
@@ -830,7 +859,7 @@ export function createGoosedumpIntegration(
830
859
  name: 'goosedump',
831
860
  label: 'goosedump',
832
861
  description:
833
- '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.',
862
+ 'Browse coding agent session history. List all sessions, or view/search/grep the current or a named session. Supports ranked search, glob filtering, compact overview, and result expansion. Default scope is active lineage.',
834
863
  promptSnippet:
835
864
  'goosedump({ action: "search", query, contextId?, scope?, page? }) - search session history; omit contextId for current session',
836
865
  promptGuidelines: [
@@ -844,12 +873,13 @@ export function createGoosedumpIntegration(
844
873
  [
845
874
  Type.Literal('list'),
846
875
  Type.Literal('search'),
876
+ Type.Literal('grep'),
847
877
  Type.Literal('expand'),
848
878
  Type.Literal('view'),
849
879
  ],
850
880
  {
851
881
  description:
852
- 'Operation: list sessions, search within a session, expand entries, or view full session',
882
+ 'Operation: list sessions, search within a session, grep by glob pattern, expand entries, or view full session',
853
883
  },
854
884
  ),
855
885
  contextId: Type.Optional(
@@ -861,6 +891,9 @@ export function createGoosedumpIntegration(
861
891
  query: Type.Optional(
862
892
  Type.String({ description: 'Search query to rank messages by relevance' }),
863
893
  ),
894
+ pattern: Type.Optional(
895
+ Type.String({ description: 'Glob pattern to filter messages (e.g. *rand*)' }),
896
+ ),
864
897
  scope: Type.Optional(
865
898
  Type.Union([Type.Literal('lineage'), Type.Literal('all')], {
866
899
  default: 'lineage',
@@ -933,6 +966,28 @@ export function createGoosedumpIntegration(
933
966
  return textResult(header + text);
934
967
  }
935
968
 
969
+ case 'grep': {
970
+ const contextId = params.contextId ?? currentSessionContextId(ctx);
971
+ if (!contextId) return missingContextId('grep');
972
+
973
+ if (!params.pattern) return textResult('pattern is required for grep');
974
+
975
+ const messages = goosedumpGrep(contextId, params.pattern, {
976
+ scope: params.scope ?? 'lineage',
977
+ });
978
+
979
+ if (messages.length === 0) {
980
+ return textResult(`No matches for "${params.pattern}"`);
981
+ }
982
+
983
+ const useCompact = params.compact ?? true;
984
+ const text = useCompact
985
+ ? formatMessagesCompact(messages)
986
+ : formatMessagesFull(messages);
987
+
988
+ return textResult(text);
989
+ }
990
+
936
991
  case 'expand': {
937
992
  const contextId = params.contextId ?? currentSessionContextId(ctx);
938
993
  if (!contextId) return missingContextId('expand');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Coding agent context data browser plugin for pi",
5
5
  "keywords": [
6
6
  "goosedump",