pi-goosedump 0.5.5 → 0.6.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 +29 -1
  2. package/index.ts +152 -39
  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 the current session by default or a named session with `contextId`:
24
+ The agent can browse the current Pi session by default or a named session with `contextId`:
25
25
 
26
26
  | Action | Description |
27
27
  | -------- | ---------------------------------------- |
@@ -42,6 +42,34 @@ goosedump({ action: "expand", ids: ["entry-a", "entry-b"] })
42
42
  goosedump({ action: "view" })
43
43
  ```
44
44
 
45
+ #### Other providers
46
+
47
+ By default the tool browses Pi sessions. Set `provider` (one of `claude`,
48
+ `codex`, `crush`, `gemini`, `goose`, `opencode`, `pi`) with a `contextId` to
49
+ browse another tool's sessions; results render the same way as Pi sessions.
50
+ Non-pi providers have no current session, so they require an explicit
51
+ `contextId`.
52
+
53
+ ```
54
+ goosedump({ action: "list", provider: "claude" })
55
+ goosedump({ action: "view", provider: "claude", contextId: "abc123" })
56
+ ```
57
+
58
+ #### Raw native output
59
+
60
+ By default every action renders through the normal structured view. Set
61
+ `outputFormat` to emit the raw native session instead — one of `claude`,
62
+ `codex`, `crush`, `gemini`, `goose`, `opencode`, `pi`, or `json`. JSONL providers
63
+ (`claude`, `codex`, `pi`, `gemini`) emit the raw session file; SQLite providers
64
+ (`crush`, `goose`, `opencode`) emit a row-oriented JSON projection of their
65
+ tables; `json` emits goosedump's structured JSON. This is round-trippable and
66
+ applies to `view`, `expand`, `grep`, and `search`.
67
+
68
+ ```
69
+ goosedump({ action: "view", outputFormat: "goose" })
70
+ goosedump({ action: "grep", pattern: "*rand*", outputFormat: "claude" })
71
+ ```
72
+
45
73
  ### Command: `/goosedump`
46
74
 
47
75
  Opens an interactive session browser.
package/index.ts CHANGED
@@ -29,7 +29,7 @@ import { Type } from '@sinclair/typebox';
29
29
 
30
30
  const require = createRequire(import.meta.url);
31
31
 
32
- const GOOSEDUMP_VERSION = [0, 5, 1] as const;
32
+ const GOOSEDUMP_VERSION = [0, 6, 2] as const;
33
33
 
34
34
  interface GoosedumpListing {
35
35
  id: string;
@@ -115,10 +115,6 @@ function resolveGoosedumpBinary(): string {
115
115
  }
116
116
  }
117
117
 
118
- function binaryPath(): string {
119
- return resolveGoosedumpBinary();
120
- }
121
-
122
118
  function goosedumpVersion(): string | null {
123
119
  try {
124
120
  const pkg = require('@jarkkojs/goosedump/package.json') as { version: string };
@@ -218,9 +214,12 @@ function hitMessage(h: HitJson): GoosedumpMessage {
218
214
  return { id: h.entryId, role: h.role, content: h.text, score: h.score };
219
215
  }
220
216
 
217
+ const ANSI_SGR = new RegExp(String.fromCharCode(27) + '\\[[0-9;]*m', 'g');
218
+
221
219
  function compactLine(content: string): string {
222
- const collapsed = content.replace(/\s+/g, ' ').trim();
223
- return truncateToWidth(collapsed, 80).replaceAll(String.fromCharCode(27) + '[0m', '');
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;
224
223
  }
225
224
 
226
225
  function formatMessagesCompact(messages: GoosedumpMessage[]): string {
@@ -235,24 +234,29 @@ function textResult(text: string): AgentToolResult<void> {
235
234
  return { content: [{ type: 'text', text }], details: undefined };
236
235
  }
237
236
 
238
- function missingContextId(action: string): AgentToolResult<void> {
237
+ function missingContextId(action: string, provider = 'pi'): AgentToolResult<void> {
238
+ if (provider !== 'pi') {
239
+ return textResult(`contextId is required for ${action} when provider is "${provider}"`);
240
+ }
239
241
  return textResult(`contextId is required for ${action} when no current session is available`);
240
242
  }
241
243
 
242
244
  function runGoosedump(args: string[]): string {
243
- return execFileSync('node', [binaryPath(), ...args], {
245
+ return execFileSync('node', [resolveGoosedumpBinary(), ...args], {
244
246
  encoding: 'utf-8',
245
247
  });
246
248
  }
247
249
 
248
- function goosedumpList(): GoosedumpListing[] {
249
- const ids = runGoosedump(['list', 'pi:*'])
250
+ function goosedumpList(provider = 'pi'): GoosedumpListing[] {
251
+ const prefix = `${provider}:`;
252
+ const ids = runGoosedump(['list', `${prefix}*`])
250
253
  .split('\n')
251
254
  .map((line) => line.trim())
252
- .filter((line) => line.startsWith('pi:'))
253
- .map((line) => line.slice('pi:'.length));
255
+ .filter((line) => line.startsWith(prefix))
256
+ .map((line) => line.slice(prefix.length));
254
257
 
255
- const paths = indexPiSessionPaths();
258
+ // Only pi sessions live in the local sessions tree we can map back to files.
259
+ const paths = provider === 'pi' ? indexPiSessionPaths() : new Map<string, string>();
256
260
  return ids.map((id) => ({ id, path: paths.get(id) ?? null }));
257
261
  }
258
262
 
@@ -290,12 +294,12 @@ function indexPiSessionPaths(): Map<string, string> {
290
294
  }
291
295
 
292
296
  function goosedumpSearch(
293
- contextId: string,
297
+ target: string,
294
298
  query: string,
295
299
  options: { scope?: string; page?: number } = {},
296
300
  ): GoosedumpSearchResult {
297
301
  const scope = options.scope ?? 'lineage';
298
- const args = ['search', `pi:${contextId}`, query];
302
+ const args = ['search', target, query];
299
303
  if (options.page) args.push('--page', String(options.page));
300
304
  args.push('--scope', scope);
301
305
 
@@ -305,23 +309,23 @@ function goosedumpSearch(
305
309
  }
306
310
 
307
311
  function goosedumpGrep(
308
- contextId: string,
312
+ target: string,
309
313
  pattern: string,
310
314
  options: { scope?: string } = {},
311
315
  ): GoosedumpMessage[] {
312
316
  const scope = options.scope ?? 'lineage';
313
- const json = JSON.parse(runGoosedump(['grep', `pi:${contextId}`, pattern, '--scope', scope])) as {
317
+ const json = JSON.parse(runGoosedump(['grep', target, pattern, '--scope', scope])) as {
314
318
  hits?: HitJson[];
315
319
  };
316
320
  return (json.hits ?? []).map(hitMessage);
317
321
  }
318
322
 
319
323
  function goosedumpShow(
320
- contextId: string,
324
+ target: string,
321
325
  options: { scope?: string; ids?: string[] } = {},
322
326
  ): GoosedumpMessage[] {
323
327
  const scope = options.scope ?? 'lineage';
324
- const args = ['show', `pi:${contextId}`];
328
+ const args = ['show', target];
325
329
  if (options.ids && options.ids.length > 0) args.push('--ids', options.ids.join(','));
326
330
  args.push('--scope', scope);
327
331
 
@@ -329,6 +333,35 @@ function goosedumpShow(
329
333
  return (json.messages ?? []).map(showMessage);
330
334
  }
331
335
 
336
+ // goosedump 0.6.2+ accepts --output-format on show/grep/search to emit a context
337
+ // in a provider's native, round-trippable format. That output is raw native text
338
+ // (not the ShowJson/HitJson shapes), so this bypasses the JSON-parsing wrappers
339
+ // and returns it verbatim. Only used when the caller explicitly sets outputFormat.
340
+ function goosedumpConvert(
341
+ subcommand: 'show' | 'grep' | 'search',
342
+ target: string,
343
+ outputFormat: string,
344
+ options: { positionals?: string[]; scope?: string; ids?: string[]; page?: number } = {},
345
+ ): string {
346
+ const args = [subcommand, target, ...(options.positionals ?? [])];
347
+ if (options.ids && options.ids.length > 0) args.push('--ids', options.ids.join(','));
348
+ if (options.page) args.push('--page', String(options.page));
349
+ args.push('--scope', options.scope ?? 'lineage', '--output-format', outputFormat);
350
+ return runGoosedump(args);
351
+ }
352
+
353
+ // Resolve a <provider>:<contextId> target. contextId falls back to the current
354
+ // session only for pi; other providers have no current session, so they require
355
+ // an explicit contextId.
356
+ function resolveTarget(
357
+ ctx: ExtensionContext,
358
+ provider: string,
359
+ contextId: string | undefined,
360
+ ): string | null {
361
+ const id = contextId ?? (provider === 'pi' ? currentSessionContextId(ctx) : null);
362
+ return id ? `${provider}:${id}` : null;
363
+ }
364
+
332
365
  function capList(items: string[], limit: number): string {
333
366
  const shown = items.slice(0, limit).join(', ');
334
367
  return items.length > limit ? `${shown} (+${items.length - limit} more)` : shown;
@@ -659,7 +692,7 @@ async function runSessionBrowser(
659
692
  const total = compactLines.length;
660
693
  const window = compactLines.slice(compactScroll, compactScroll + COMPACT_VIEWPORT);
661
694
  for (const row of window) {
662
- lines.push(makeRow(text(truncateToWidth(row, innerW)), innerW, border));
695
+ lines.push(makeRow(text(row), innerW, border));
663
696
  }
664
697
  for (let i = window.length; i < COMPACT_VIEWPORT; i++) {
665
698
  lines.push(makeRow('', innerW, border));
@@ -696,7 +729,7 @@ async function runSessionBrowser(
696
729
  const cursor = isSelected ? accent('▶') : ' ';
697
730
  const idText = isSelected ? accent(listing.id) : text(listing.id);
698
731
  const line = ` ${cursor} ${idText}`;
699
- lines.push(makeRow(truncateToWidth(line, innerW), innerW, border));
732
+ lines.push(makeRow(line, innerW, border));
700
733
  }
701
734
 
702
735
  if (total > LIST_VIEWPORT) {
@@ -860,14 +893,16 @@ export function createGoosedumpIntegration(
860
893
  name: 'goosedump',
861
894
  label: 'goosedump',
862
895
  description:
863
- '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.',
896
+ "Browse coding agent session history across providers. List sessions, or view/search/grep the current Pi session or a named session. Set provider to browse another tool's sessions (claude, codex, gemini, ...); set outputFormat for raw native output. Supports ranked search, glob filtering, compact overview, and result expansion. Default scope is active lineage.",
864
897
  promptSnippet:
865
- 'goosedump({ action: "search", query, contextId?, scope?, page? }) - search session history; omit contextId for current session',
898
+ 'goosedump({ action: "search", query, contextId?, provider?, scope?, page? }) - search session history; omit contextId for current Pi session',
866
899
  promptGuidelines: [
867
900
  'When researching past conversation history, use goosedump to find relevant context.',
868
901
  'Start with compact ranked search (compact: true) for quick overview, then expand interesting entries.',
869
902
  'Default scope is "lineage" (current branch); use scope: "all" to include all entries in the session.',
870
903
  'Omit contextId to search, expand, or view the current Pi session; use list first for other sessions.',
904
+ 'Set provider (e.g. "claude", "gemini") with a contextId to browse another tool\'s sessions; results render the same way as Pi sessions.',
905
+ 'Set outputFormat (e.g. "claude", "json") only when you explicitly need the raw native session instead of the rendered view.',
871
906
  ],
872
907
  parameters: Type.Object({
873
908
  action: Type.Union(
@@ -886,7 +921,7 @@ export function createGoosedumpIntegration(
886
921
  contextId: Type.Optional(
887
922
  Type.String({
888
923
  description:
889
- 'Context/session ID (defaults to current session for search, expand, view)',
924
+ 'Context/session ID. Defaults to the current Pi session for search, grep, expand, view; required when provider is not pi.',
890
925
  }),
891
926
  ),
892
927
  query: Type.Optional(
@@ -919,6 +954,42 @@ export function createGoosedumpIntegration(
919
954
  description: 'Page number for ranked results',
920
955
  }),
921
956
  ),
957
+ provider: Type.Optional(
958
+ Type.Union(
959
+ [
960
+ Type.Literal('pi'),
961
+ Type.Literal('claude'),
962
+ Type.Literal('codex'),
963
+ Type.Literal('crush'),
964
+ Type.Literal('gemini'),
965
+ Type.Literal('goose'),
966
+ Type.Literal('opencode'),
967
+ ],
968
+ {
969
+ default: 'pi',
970
+ description:
971
+ 'Source provider whose sessions to browse (list, view, search, grep, expand). Defaults to pi. Non-pi providers have no current session, so they require an explicit contextId. Results render through the normal structured view regardless of provider.',
972
+ },
973
+ ),
974
+ ),
975
+ outputFormat: Type.Optional(
976
+ Type.Union(
977
+ [
978
+ Type.Literal('json'),
979
+ Type.Literal('claude'),
980
+ Type.Literal('codex'),
981
+ Type.Literal('crush'),
982
+ Type.Literal('gemini'),
983
+ Type.Literal('goose'),
984
+ Type.Literal('opencode'),
985
+ Type.Literal('pi'),
986
+ ],
987
+ {
988
+ description:
989
+ "Explicitly emit raw native output instead of the normal rendered view (view, expand, grep, search). JSONL providers (claude, codex, pi, gemini) emit raw session JSONL; SQLite providers (crush, goose, opencode) emit a JSON table projection; json emits goosedump's structured JSON. Omit to render normally.",
990
+ },
991
+ ),
992
+ ),
922
993
  }),
923
994
  async execute(_id, params, _signal, _onUpdate, ctx): Promise<AgentToolResult<void>> {
924
995
  if (!goosedumpAvailable) {
@@ -930,7 +1001,7 @@ export function createGoosedumpIntegration(
930
1001
  try {
931
1002
  switch (params.action) {
932
1003
  case 'list': {
933
- const listings = goosedumpList();
1004
+ const listings = goosedumpList(params.provider ?? 'pi');
934
1005
  if (listings.length === 0) {
935
1006
  return textResult('No sessions found.');
936
1007
  }
@@ -944,12 +1015,23 @@ export function createGoosedumpIntegration(
944
1015
  }
945
1016
 
946
1017
  case 'search': {
947
- const contextId = params.contextId ?? currentSessionContextId(ctx);
948
- if (!contextId) return missingContextId('search');
1018
+ const provider = params.provider ?? 'pi';
1019
+ const target = resolveTarget(ctx, provider, params.contextId);
1020
+ if (!target) return missingContextId('search', provider);
949
1021
 
950
1022
  if (!params.query) return textResult('query is required for search');
951
1023
 
952
- const result = goosedumpSearch(contextId, params.query, {
1024
+ if (params.outputFormat) {
1025
+ return textResult(
1026
+ goosedumpConvert('search', target, params.outputFormat, {
1027
+ positionals: [params.query],
1028
+ scope: params.scope,
1029
+ page: params.page ?? 1,
1030
+ }),
1031
+ );
1032
+ }
1033
+
1034
+ const result = goosedumpSearch(target, params.query, {
953
1035
  scope: params.scope ?? 'lineage',
954
1036
  page: params.page ?? 1,
955
1037
  });
@@ -968,12 +1050,22 @@ export function createGoosedumpIntegration(
968
1050
  }
969
1051
 
970
1052
  case 'grep': {
971
- const contextId = params.contextId ?? currentSessionContextId(ctx);
972
- if (!contextId) return missingContextId('grep');
1053
+ const provider = params.provider ?? 'pi';
1054
+ const target = resolveTarget(ctx, provider, params.contextId);
1055
+ if (!target) return missingContextId('grep', provider);
973
1056
 
974
1057
  if (!params.pattern) return textResult('pattern is required for grep');
975
1058
 
976
- const messages = goosedumpGrep(contextId, params.pattern, {
1059
+ if (params.outputFormat) {
1060
+ return textResult(
1061
+ goosedumpConvert('grep', target, params.outputFormat, {
1062
+ positionals: [params.pattern],
1063
+ scope: params.scope,
1064
+ }),
1065
+ );
1066
+ }
1067
+
1068
+ const messages = goosedumpGrep(target, params.pattern, {
977
1069
  scope: params.scope ?? 'lineage',
978
1070
  });
979
1071
 
@@ -990,14 +1082,24 @@ export function createGoosedumpIntegration(
990
1082
  }
991
1083
 
992
1084
  case 'expand': {
993
- const contextId = params.contextId ?? currentSessionContextId(ctx);
994
- if (!contextId) return missingContextId('expand');
1085
+ const provider = params.provider ?? 'pi';
1086
+ const target = resolveTarget(ctx, provider, params.contextId);
1087
+ if (!target) return missingContextId('expand', provider);
995
1088
 
996
1089
  if (!params.ids || params.ids.length === 0) {
997
1090
  return textResult('ids is required for expand (array of entry IDs)');
998
1091
  }
999
1092
 
1000
- const messages = goosedumpShow(contextId, {
1093
+ if (params.outputFormat) {
1094
+ return textResult(
1095
+ goosedumpConvert('show', target, params.outputFormat, {
1096
+ ids: params.ids,
1097
+ scope: params.scope,
1098
+ }),
1099
+ );
1100
+ }
1101
+
1102
+ const messages = goosedumpShow(target, {
1001
1103
  scope: params.scope ?? 'lineage',
1002
1104
  ids: params.ids,
1003
1105
  });
@@ -1010,13 +1112,24 @@ export function createGoosedumpIntegration(
1010
1112
  }
1011
1113
 
1012
1114
  case 'view': {
1013
- const contextId = params.contextId ?? currentSessionContextId(ctx);
1014
- if (!contextId) return missingContextId('view');
1115
+ const provider = params.provider ?? 'pi';
1116
+ const target = resolveTarget(ctx, provider, params.contextId);
1117
+ if (!target) return missingContextId('view', provider);
1118
+
1119
+ if (params.outputFormat) {
1120
+ return textResult(
1121
+ goosedumpConvert('show', target, params.outputFormat, {
1122
+ scope: params.scope,
1123
+ }),
1124
+ );
1125
+ }
1015
1126
 
1016
- const messages = goosedumpShow(contextId, { scope: params.scope ?? 'lineage' });
1127
+ const messages = goosedumpShow(target, { scope: params.scope ?? 'lineage' });
1017
1128
 
1018
1129
  if (messages.length === 0) {
1019
- return textResult(`Session "${contextId}" has no messages.`);
1130
+ return textResult(
1131
+ `Session "${target.slice(provider.length + 1)}" has no messages.`,
1132
+ );
1020
1133
  }
1021
1134
 
1022
1135
  return textResult(formatMessagesFull(messages));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.5.5",
3
+ "version": "0.6.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.5.1",
32
+ "@jarkkojs/goosedump": "^0.6.2",
33
33
  "@sinclair/typebox": "^0.34.49"
34
34
  },
35
35
  "devDependencies": {