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.
- package/README.md +29 -1
- package/index.ts +151 -45
- 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
|
|
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,
|
|
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
|
|
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(
|
|
252
|
-
.map((line) => line.slice(
|
|
255
|
+
.filter((line) => line.startsWith(prefix))
|
|
256
|
+
.map((line) => line.slice(prefix.length));
|
|
253
257
|
|
|
254
|
-
|
|
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
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
324
|
+
target: string,
|
|
320
325
|
options: { scope?: string; ids?: string[] } = {},
|
|
321
326
|
): GoosedumpMessage[] {
|
|
322
327
|
const scope = options.scope ?? 'lineage';
|
|
323
|
-
const args = ['show',
|
|
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
|
|
419
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
947
|
-
|
|
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
|
-
|
|
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
|
|
971
|
-
|
|
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
|
-
|
|
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
|
|
993
|
-
|
|
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
|
-
|
|
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
|
|
1013
|
-
|
|
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(
|
|
1119
|
+
const messages = goosedumpShow(target, { scope: params.scope ?? 'lineage' });
|
|
1016
1120
|
|
|
1017
1121
|
if (messages.length === 0) {
|
|
1018
|
-
return textResult(
|
|
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.
|
|
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.
|
|
32
|
+
"@jarkkojs/goosedump": "^0.6.2",
|
|
33
33
|
"@sinclair/typebox": "^0.34.49"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|