pi-goosedump 0.5.6 → 0.6.1

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 +151 -45
  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
@@ -13,10 +13,10 @@ import type {
13
13
  import { defineTool } from '@earendil-works/pi-coding-agent';
14
14
 
15
15
  import { execFileSync } from 'node:child_process';
16
- import { type Dirent, existsSync, readdirSync, readFileSync } from 'node:fs';
16
+ import { type Dirent, existsSync, readdirSync } from 'node:fs';
17
17
  import { createRequire } from 'node:module';
18
18
  import { homedir } from 'node:os';
19
- import { basename, join } from 'node:path';
19
+ import { basename, extname, join } from 'node:path';
20
20
 
21
21
  import {
22
22
  Key,
@@ -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;
@@ -234,7 +234,10 @@ function textResult(text: string): AgentToolResult<void> {
234
234
  return { content: [{ type: 'text', text }], details: undefined };
235
235
  }
236
236
 
237
- 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
+ }
238
241
  return textResult(`contextId is required for ${action} when no current session is available`);
239
242
  }
240
243
 
@@ -244,14 +247,16 @@ function runGoosedump(args: string[]): string {
244
247
  });
245
248
  }
246
249
 
247
- function goosedumpList(): GoosedumpListing[] {
248
- const ids = runGoosedump(['list', 'pi:*'])
250
+ function goosedumpList(provider = 'pi'): GoosedumpListing[] {
251
+ const prefix = `${provider}:`;
252
+ const ids = runGoosedump(['list', `${prefix}*`])
249
253
  .split('\n')
250
254
  .map((line) => line.trim())
251
- .filter((line) => line.startsWith('pi:'))
252
- .map((line) => line.slice('pi:'.length));
255
+ .filter((line) => line.startsWith(prefix))
256
+ .map((line) => line.slice(prefix.length));
253
257
 
254
- 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>();
255
260
  return ids.map((id) => ({ id, path: paths.get(id) ?? null }));
256
261
  }
257
262
 
@@ -264,7 +269,7 @@ function piSessionsDir(): string {
264
269
 
265
270
  // goosedump's `list` no longer reports session file paths, so map each pi
266
271
  // session id back to its file by scanning the sessions tree, matching the same
267
- // id goosedump derives from the session header.
272
+ // id goosedump derives from the session file stem.
268
273
  function indexPiSessionPaths(): Map<string, string> {
269
274
  const paths = new Map<string, string>();
270
275
  const walk = (dir: string): void => {
@@ -289,12 +294,12 @@ function indexPiSessionPaths(): Map<string, string> {
289
294
  }
290
295
 
291
296
  function goosedumpSearch(
292
- contextId: string,
297
+ target: string,
293
298
  query: string,
294
299
  options: { scope?: string; page?: number } = {},
295
300
  ): GoosedumpSearchResult {
296
301
  const scope = options.scope ?? 'lineage';
297
- const args = ['search', `pi:${contextId}`, query];
302
+ const args = ['search', target, query];
298
303
  if (options.page) args.push('--page', String(options.page));
299
304
  args.push('--scope', scope);
300
305
 
@@ -304,23 +309,23 @@ function goosedumpSearch(
304
309
  }
305
310
 
306
311
  function goosedumpGrep(
307
- contextId: string,
312
+ target: string,
308
313
  pattern: string,
309
314
  options: { scope?: string } = {},
310
315
  ): GoosedumpMessage[] {
311
316
  const scope = options.scope ?? 'lineage';
312
- const json = JSON.parse(runGoosedump(['grep', `pi:${contextId}`, pattern, '--scope', scope])) as {
317
+ const json = JSON.parse(runGoosedump(['grep', target, pattern, '--scope', scope])) as {
313
318
  hits?: HitJson[];
314
319
  };
315
320
  return (json.hits ?? []).map(hitMessage);
316
321
  }
317
322
 
318
323
  function goosedumpShow(
319
- contextId: string,
324
+ target: string,
320
325
  options: { scope?: string; ids?: string[] } = {},
321
326
  ): GoosedumpMessage[] {
322
327
  const scope = options.scope ?? 'lineage';
323
- const args = ['show', `pi:${contextId}`];
328
+ const args = ['show', target];
324
329
  if (options.ids && options.ids.length > 0) args.push('--ids', options.ids.join(','));
325
330
  args.push('--scope', scope);
326
331
 
@@ -328,6 +333,35 @@ function goosedumpShow(
328
333
  return (json.messages ?? []).map(showMessage);
329
334
  }
330
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
+
331
365
  function capList(items: string[], limit: number): string {
332
366
  const shown = items.slice(0, limit).join(', ');
333
367
  return items.length > limit ? `${shown} (+${items.length - limit} more)` : shown;
@@ -414,19 +448,11 @@ export default function (pi: ExtensionAPI) {
414
448
  function sessionFileContextId(sessionFile: string | null | undefined): string | null {
415
449
  if (!sessionFile) return null;
416
450
 
451
+ // goosedump 0.6+ derives a pi context id from the session file stem (file name
452
+ // without its extension), so match that exactly rather than the inner UUID.
417
453
  const name = basename(sessionFile);
418
- const nameMatch = name.match(
419
- /(?:^|_)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i,
420
- );
421
- if (nameMatch) return nameMatch[1];
422
-
423
- try {
424
- const firstLine = readFileSync(sessionFile, 'utf-8').split(/\r?\n/, 1)[0];
425
- const header = JSON.parse(firstLine) as { id?: unknown };
426
- return typeof header.id === 'string' && header.id.length > 0 ? header.id : null;
427
- } catch {
428
- return null;
429
- }
454
+ const ext = extname(name);
455
+ return ext ? name.slice(0, -ext.length) : name;
430
456
  }
431
457
 
432
458
  function currentSessionContextId(ctx: ExtensionContext): string | null {
@@ -859,14 +885,16 @@ export function createGoosedumpIntegration(
859
885
  name: 'goosedump',
860
886
  label: 'goosedump',
861
887
  description:
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.',
888
+ "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.",
863
889
  promptSnippet:
864
- 'goosedump({ action: "search", query, contextId?, scope?, page? }) - search session history; omit contextId for current session',
890
+ 'goosedump({ action: "search", query, contextId?, provider?, scope?, page? }) - search session history; omit contextId for current Pi session',
865
891
  promptGuidelines: [
866
892
  'When researching past conversation history, use goosedump to find relevant context.',
867
893
  'Start with compact ranked search (compact: true) for quick overview, then expand interesting entries.',
868
894
  'Default scope is "lineage" (current branch); use scope: "all" to include all entries in the session.',
869
895
  'Omit contextId to search, expand, or view the current Pi session; use list first for other sessions.',
896
+ 'Set provider (e.g. "claude", "gemini") with a contextId to browse another tool\'s sessions; results render the same way as Pi sessions.',
897
+ 'Set outputFormat (e.g. "claude", "json") only when you explicitly need the raw native session instead of the rendered view.',
870
898
  ],
871
899
  parameters: Type.Object({
872
900
  action: Type.Union(
@@ -885,7 +913,7 @@ export function createGoosedumpIntegration(
885
913
  contextId: Type.Optional(
886
914
  Type.String({
887
915
  description:
888
- 'Context/session ID (defaults to current session for search, expand, view)',
916
+ 'Context/session ID. Defaults to the current Pi session for search, grep, expand, view; required when provider is not pi.',
889
917
  }),
890
918
  ),
891
919
  query: Type.Optional(
@@ -918,6 +946,42 @@ export function createGoosedumpIntegration(
918
946
  description: 'Page number for ranked results',
919
947
  }),
920
948
  ),
949
+ provider: Type.Optional(
950
+ Type.Union(
951
+ [
952
+ Type.Literal('pi'),
953
+ Type.Literal('claude'),
954
+ Type.Literal('codex'),
955
+ Type.Literal('crush'),
956
+ Type.Literal('gemini'),
957
+ Type.Literal('goose'),
958
+ Type.Literal('opencode'),
959
+ ],
960
+ {
961
+ default: 'pi',
962
+ description:
963
+ '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.',
964
+ },
965
+ ),
966
+ ),
967
+ outputFormat: Type.Optional(
968
+ Type.Union(
969
+ [
970
+ Type.Literal('json'),
971
+ Type.Literal('claude'),
972
+ Type.Literal('codex'),
973
+ Type.Literal('crush'),
974
+ Type.Literal('gemini'),
975
+ Type.Literal('goose'),
976
+ Type.Literal('opencode'),
977
+ Type.Literal('pi'),
978
+ ],
979
+ {
980
+ description:
981
+ "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.",
982
+ },
983
+ ),
984
+ ),
921
985
  }),
922
986
  async execute(_id, params, _signal, _onUpdate, ctx): Promise<AgentToolResult<void>> {
923
987
  if (!goosedumpAvailable) {
@@ -929,7 +993,7 @@ export function createGoosedumpIntegration(
929
993
  try {
930
994
  switch (params.action) {
931
995
  case 'list': {
932
- const listings = goosedumpList();
996
+ const listings = goosedumpList(params.provider ?? 'pi');
933
997
  if (listings.length === 0) {
934
998
  return textResult('No sessions found.');
935
999
  }
@@ -943,12 +1007,23 @@ export function createGoosedumpIntegration(
943
1007
  }
944
1008
 
945
1009
  case 'search': {
946
- const contextId = params.contextId ?? currentSessionContextId(ctx);
947
- if (!contextId) return missingContextId('search');
1010
+ const provider = params.provider ?? 'pi';
1011
+ const target = resolveTarget(ctx, provider, params.contextId);
1012
+ if (!target) return missingContextId('search', provider);
948
1013
 
949
1014
  if (!params.query) return textResult('query is required for search');
950
1015
 
951
- const result = goosedumpSearch(contextId, params.query, {
1016
+ if (params.outputFormat) {
1017
+ return textResult(
1018
+ goosedumpConvert('search', target, params.outputFormat, {
1019
+ positionals: [params.query],
1020
+ scope: params.scope,
1021
+ page: params.page ?? 1,
1022
+ }),
1023
+ );
1024
+ }
1025
+
1026
+ const result = goosedumpSearch(target, params.query, {
952
1027
  scope: params.scope ?? 'lineage',
953
1028
  page: params.page ?? 1,
954
1029
  });
@@ -967,12 +1042,22 @@ export function createGoosedumpIntegration(
967
1042
  }
968
1043
 
969
1044
  case 'grep': {
970
- const contextId = params.contextId ?? currentSessionContextId(ctx);
971
- if (!contextId) return missingContextId('grep');
1045
+ const provider = params.provider ?? 'pi';
1046
+ const target = resolveTarget(ctx, provider, params.contextId);
1047
+ if (!target) return missingContextId('grep', provider);
972
1048
 
973
1049
  if (!params.pattern) return textResult('pattern is required for grep');
974
1050
 
975
- const messages = goosedumpGrep(contextId, params.pattern, {
1051
+ if (params.outputFormat) {
1052
+ return textResult(
1053
+ goosedumpConvert('grep', target, params.outputFormat, {
1054
+ positionals: [params.pattern],
1055
+ scope: params.scope,
1056
+ }),
1057
+ );
1058
+ }
1059
+
1060
+ const messages = goosedumpGrep(target, params.pattern, {
976
1061
  scope: params.scope ?? 'lineage',
977
1062
  });
978
1063
 
@@ -989,14 +1074,24 @@ export function createGoosedumpIntegration(
989
1074
  }
990
1075
 
991
1076
  case 'expand': {
992
- const contextId = params.contextId ?? currentSessionContextId(ctx);
993
- if (!contextId) return missingContextId('expand');
1077
+ const provider = params.provider ?? 'pi';
1078
+ const target = resolveTarget(ctx, provider, params.contextId);
1079
+ if (!target) return missingContextId('expand', provider);
994
1080
 
995
1081
  if (!params.ids || params.ids.length === 0) {
996
1082
  return textResult('ids is required for expand (array of entry IDs)');
997
1083
  }
998
1084
 
999
- const messages = goosedumpShow(contextId, {
1085
+ if (params.outputFormat) {
1086
+ return textResult(
1087
+ goosedumpConvert('show', target, params.outputFormat, {
1088
+ ids: params.ids,
1089
+ scope: params.scope,
1090
+ }),
1091
+ );
1092
+ }
1093
+
1094
+ const messages = goosedumpShow(target, {
1000
1095
  scope: params.scope ?? 'lineage',
1001
1096
  ids: params.ids,
1002
1097
  });
@@ -1009,13 +1104,24 @@ export function createGoosedumpIntegration(
1009
1104
  }
1010
1105
 
1011
1106
  case 'view': {
1012
- const contextId = params.contextId ?? currentSessionContextId(ctx);
1013
- if (!contextId) return missingContextId('view');
1107
+ const provider = params.provider ?? 'pi';
1108
+ const target = resolveTarget(ctx, provider, params.contextId);
1109
+ if (!target) return missingContextId('view', provider);
1110
+
1111
+ if (params.outputFormat) {
1112
+ return textResult(
1113
+ goosedumpConvert('show', target, params.outputFormat, {
1114
+ scope: params.scope,
1115
+ }),
1116
+ );
1117
+ }
1014
1118
 
1015
- const messages = goosedumpShow(contextId, { scope: params.scope ?? 'lineage' });
1119
+ const messages = goosedumpShow(target, { scope: params.scope ?? 'lineage' });
1016
1120
 
1017
1121
  if (messages.length === 0) {
1018
- return textResult(`Session "${contextId}" has no messages.`);
1122
+ return textResult(
1123
+ `Session "${target.slice(provider.length + 1)}" has no messages.`,
1124
+ );
1019
1125
  }
1020
1126
 
1021
1127
  return textResult(formatMessagesFull(messages));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.5.6",
3
+ "version": "0.6.1",
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": {