pi-goosedump 0.6.2 → 0.6.4

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 +8 -7
  2. package/index.ts +91 -78
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -21,7 +21,8 @@ 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 Pi session by default or a named session with `contextId`:
24
+ The agent can browse the current Pi session by default, or any session by its
25
+ 8-hex `contextId` (the id shown by `list`):
25
26
 
26
27
  | Action | Description |
27
28
  | -------- | ---------------------------------------- |
@@ -36,7 +37,7 @@ Examples:
36
37
  ```
37
38
  goosedump({ action: "list" })
38
39
  goosedump({ action: "search", query: "bug fix" })
39
- goosedump({ action: "search", contextId: "abc123", query: "bug fix" })
40
+ goosedump({ action: "search", contextId: "0000001f", query: "bug fix" })
40
41
  goosedump({ action: "grep", pattern: "*rand*" })
41
42
  goosedump({ action: "expand", ids: ["entry-a", "entry-b"] })
42
43
  goosedump({ action: "view" })
@@ -52,7 +53,7 @@ Non-pi providers have no current session, so they require an explicit
52
53
 
53
54
  ```
54
55
  goosedump({ action: "list", provider: "claude" })
55
- goosedump({ action: "view", provider: "claude", contextId: "abc123" })
56
+ goosedump({ action: "view", provider: "claude", contextId: "0000001f" })
56
57
  ```
57
58
 
58
59
  #### Raw native output
@@ -76,10 +77,10 @@ Opens an interactive session browser.
76
77
 
77
78
  ### Compaction
78
79
 
79
- pi-goosedump hooks Pi's `/compact` and auto-compaction flow. It uses
80
- `goosedump compact pi:<session>` with Pi's compaction range (`--from`,
81
- `--until`, and `--scope`) so the generated summary matches the entries Pi is
82
- about to replace.
80
+ pi-goosedump hooks Pi's `/compact` and auto-compaction flow. It resolves the
81
+ current session to its goosedump id and runs `goosedump compact <id>` with Pi's
82
+ compaction range (`--from`, `--until`, and `--scope`) so the generated summary
83
+ matches the entries Pi is about to replace.
83
84
 
84
85
  ## License
85
86
 
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 } from 'node:fs';
16
+ import { existsSync } from 'node:fs';
17
17
  import { createRequire } from 'node:module';
18
18
  import { homedir } from 'node:os';
19
- import { basename, extname, join } from 'node:path';
19
+ import { basename, extname, isAbsolute, join, relative } from 'node:path';
20
20
 
21
21
  import {
22
22
  Key,
@@ -29,10 +29,27 @@ import { Type } from '@sinclair/typebox';
29
29
 
30
30
  const require = createRequire(import.meta.url);
31
31
 
32
- const GOOSEDUMP_VERSION = [0, 6, 2] as const;
32
+ const GOOSEDUMP_VERSION = [0, 7, 4] as const;
33
+
34
+ // One element of goosedump 0.7's `list` JSON output.
35
+ interface ListEntryJson {
36
+ id: string;
37
+ parent: string | null;
38
+ provider: string;
39
+ native_id: string;
40
+ cwd: string;
41
+ count: number;
42
+ label: string | null;
43
+ from: string | null;
44
+ until: string | null;
45
+ }
33
46
 
34
47
  interface GoosedumpListing {
35
48
  id: string;
49
+ provider: string;
50
+ nativeId: string;
51
+ cwd: string;
52
+ label: string | null;
36
53
  path: string | null;
37
54
  }
38
55
 
@@ -248,16 +265,18 @@ function runGoosedump(args: string[]): string {
248
265
  }
249
266
 
250
267
  function goosedumpList(provider = 'pi'): GoosedumpListing[] {
251
- const prefix = `${provider}:`;
252
- const ids = runGoosedump(['list', `${prefix}*`])
253
- .split('\n')
254
- .map((line) => line.trim())
255
- .filter((line) => line.startsWith(prefix))
256
- .map((line) => line.slice(prefix.length));
257
-
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>();
260
- return ids.map((id) => ({ id, path: paths.get(id) ?? null }));
268
+ const entries = JSON.parse(runGoosedump(['list'])) as ListEntryJson[];
269
+ return entries
270
+ .filter((entry) => entry.provider === provider)
271
+ .map((entry) => ({
272
+ id: entry.id,
273
+ provider: entry.provider,
274
+ nativeId: entry.native_id,
275
+ cwd: entry.cwd,
276
+ label: entry.label ?? null,
277
+ // Only pi sessions map back to a local file we can resume.
278
+ path: provider === 'pi' ? piSessionFilePath(entry.native_id) : null,
279
+ }));
261
280
  }
262
281
 
263
282
  function piSessionsDir(): string {
@@ -267,30 +286,11 @@ function piSessionsDir(): string {
267
286
  return join(agentDir, 'sessions');
268
287
  }
269
288
 
270
- // goosedump's `list` no longer reports session file paths, so map each pi
271
- // session id back to its file by scanning the sessions tree, matching the same
272
- // id goosedump derives from the session file stem.
273
- function indexPiSessionPaths(): Map<string, string> {
274
- const paths = new Map<string, string>();
275
- const walk = (dir: string): void => {
276
- let entries: Dirent[];
277
- try {
278
- entries = readdirSync(dir, { withFileTypes: true });
279
- } catch {
280
- return;
281
- }
282
- for (const entry of entries) {
283
- const full = join(dir, entry.name);
284
- if (entry.isDirectory()) {
285
- walk(full);
286
- } else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
287
- const id = sessionFileContextId(full);
288
- if (id && !paths.has(id)) paths.set(id, full);
289
- }
290
- }
291
- };
292
- walk(piSessionsDir());
293
- return paths;
289
+ // goosedump derives a pi native id from the session file path relative to the
290
+ // sessions base (without extension), so reconstruct the file path the same way.
291
+ function piSessionFilePath(nativeId: string): string | null {
292
+ const path = join(piSessionsDir(), `${nativeId}.jsonl`);
293
+ return existsSync(path) ? path : null;
294
294
  }
295
295
 
296
296
  function goosedumpSearch(
@@ -326,17 +326,17 @@ function goosedumpShow(
326
326
  ): GoosedumpMessage[] {
327
327
  const scope = options.scope ?? 'lineage';
328
328
  const args = ['show', target];
329
- if (options.ids && options.ids.length > 0) args.push('--ids', options.ids.join(','));
329
+ for (const id of options.ids ?? []) args.push('-m', id);
330
330
  args.push('--scope', scope);
331
331
 
332
332
  const json = JSON.parse(runGoosedump(args)) as ShowJson;
333
333
  return (json.messages ?? []).map(showMessage);
334
334
  }
335
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.
336
+ // goosedump accepts --output-format on show/grep/search to emit a context in a
337
+ // provider's native, round-trippable format. That output is raw native text (not
338
+ // the ShowJson/HitJson shapes), so this bypasses the JSON-parsing wrappers and
339
+ // returns it verbatim. Only used when the caller explicitly sets outputFormat.
340
340
  function goosedumpConvert(
341
341
  subcommand: 'show' | 'grep' | 'search',
342
342
  target: string,
@@ -344,22 +344,23 @@ function goosedumpConvert(
344
344
  options: { positionals?: string[]; scope?: string; ids?: string[]; page?: number } = {},
345
345
  ): string {
346
346
  const args = [subcommand, target, ...(options.positionals ?? [])];
347
- if (options.ids && options.ids.length > 0) args.push('--ids', options.ids.join(','));
347
+ for (const id of options.ids ?? []) args.push('-m', id);
348
348
  if (options.page) args.push('--page', String(options.page));
349
349
  args.push('--scope', options.scope ?? 'lineage', '--output-format', outputFormat);
350
350
  return runGoosedump(args);
351
351
  }
352
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.
353
+ // Resolve a goosedump target id. In 0.7 every context is addressed by its 8-hex
354
+ // index id, so an explicit contextId is already the target. It falls back to the
355
+ // current session only for pi; other providers have no current session, so they
356
+ // require an explicit contextId.
356
357
  function resolveTarget(
357
358
  ctx: ExtensionContext,
358
359
  provider: string,
359
360
  contextId: string | undefined,
360
361
  ): string | null {
361
- const id = contextId ?? (provider === 'pi' ? currentSessionContextId(ctx) : null);
362
- return id ? `${provider}:${id}` : null;
362
+ if (contextId) return contextId;
363
+ return provider === 'pi' ? currentSessionContextId(ctx) : null;
363
364
  }
364
365
 
365
366
  function capList(items: string[], limit: number): string {
@@ -391,7 +392,7 @@ function renderCompactSummary(c: CompactJson): string {
391
392
  }
392
393
 
393
394
  function goosedumpCompact(
394
- contextId: string,
395
+ target: string,
395
396
  options: {
396
397
  scope?: string;
397
398
  from?: string;
@@ -400,10 +401,10 @@ function goosedumpCompact(
400
401
  },
401
402
  ): string {
402
403
  const scope = options.scope ?? 'lineage';
403
- const args = ['compact', `pi:${contextId}`, '--scope', scope];
404
+ const args = ['compact', target, '--scope', scope];
404
405
 
405
406
  if (options.ids && options.ids.length > 0) {
406
- args.push('--ids', options.ids.join(','));
407
+ for (const id of options.ids) args.push('-m', id);
407
408
  } else {
408
409
  if (options.from) args.push('--from', options.from);
409
410
  if (options.until) args.push('--until', options.until);
@@ -445,18 +446,28 @@ export default function (pi: ExtensionAPI) {
445
446
  createGoosedumpIntegration().register(pi);
446
447
  }
447
448
 
448
- function sessionFileContextId(sessionFile: string | null | undefined): string | null {
449
- if (!sessionFile) return null;
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.
453
- const name = basename(sessionFile);
454
- const ext = extname(name);
455
- return ext ? name.slice(0, -ext.length) : name;
449
+ // Compute the pi native id goosedump assigns to a session file: its path relative
450
+ // to the sessions base without the .jsonl extension, or the bare stem when the
451
+ // file lives outside that base (mirroring goosedump's own fallback).
452
+ function sessionFileNativeId(sessionFile: string): string {
453
+ const rel = relative(piSessionsDir(), sessionFile);
454
+ if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {
455
+ const name = basename(sessionFile);
456
+ const ext = extname(name);
457
+ return ext ? name.slice(0, -ext.length) : name;
458
+ }
459
+ const ext = extname(rel);
460
+ return ext ? rel.slice(0, -ext.length) : rel;
456
461
  }
457
462
 
463
+ // Resolve the current pi session to its goosedump 8-hex context id by matching the
464
+ // session file's native id against goosedump's index (via `list`).
458
465
  function currentSessionContextId(ctx: ExtensionContext): string | null {
459
- return sessionFileContextId(ctx.sessionManager.getSessionFile());
466
+ const sessionFile = ctx.sessionManager.getSessionFile();
467
+ if (!sessionFile) return null;
468
+ const nativeId = sessionFileNativeId(sessionFile);
469
+ const listing = goosedumpList('pi').find((entry) => entry.nativeId === nativeId);
470
+ return listing ? listing.id : null;
460
471
  }
461
472
 
462
473
  function isCompactionEntry(entry: SessionEntry): entry is CompactionEntry {
@@ -897,18 +908,21 @@ export function createGoosedumpIntegration(
897
908
  'Set outputFormat (e.g. "claude", "json") only when you explicitly need the raw native session instead of the rendered view.',
898
909
  ],
899
910
  parameters: Type.Object({
900
- action: Type.Union(
901
- [
902
- Type.Literal('list'),
903
- Type.Literal('search'),
904
- Type.Literal('grep'),
905
- Type.Literal('expand'),
906
- Type.Literal('view'),
907
- ],
908
- {
909
- description:
910
- 'Operation: list sessions, search within a session, grep by glob pattern, expand entries, or view full session',
911
- },
911
+ action: Type.Optional(
912
+ Type.Union(
913
+ [
914
+ Type.Literal('list'),
915
+ Type.Literal('search'),
916
+ Type.Literal('grep'),
917
+ Type.Literal('expand'),
918
+ Type.Literal('view'),
919
+ ],
920
+ {
921
+ default: 'list',
922
+ description:
923
+ 'Operation: list sessions, search within a session, grep by glob pattern, expand entries, or view full session. Defaults to list.',
924
+ },
925
+ ),
912
926
  ),
913
927
  contextId: Type.Optional(
914
928
  Type.String({
@@ -991,7 +1005,7 @@ export function createGoosedumpIntegration(
991
1005
  }
992
1006
 
993
1007
  try {
994
- switch (params.action) {
1008
+ switch (params.action ?? 'list') {
995
1009
  case 'list': {
996
1010
  const listings = goosedumpList(params.provider ?? 'pi');
997
1011
  if (listings.length === 0) {
@@ -1000,7 +1014,8 @@ export function createGoosedumpIntegration(
1000
1014
 
1001
1015
  const lines = ['Available sessions:', ''];
1002
1016
  for (const listing of listings) {
1003
- lines.push(` ${listing.id}`);
1017
+ const suffix = listing.label ? `${listing.cwd} ${listing.label}` : listing.cwd;
1018
+ lines.push(` ${listing.id} ${suffix}`);
1004
1019
  }
1005
1020
 
1006
1021
  return textResult(lines.join('\n'));
@@ -1119,16 +1134,14 @@ export function createGoosedumpIntegration(
1119
1134
  const messages = goosedumpShow(target, { scope: params.scope ?? 'lineage' });
1120
1135
 
1121
1136
  if (messages.length === 0) {
1122
- return textResult(
1123
- `Session "${target.slice(provider.length + 1)}" has no messages.`,
1124
- );
1137
+ return textResult(`Session "${target}" has no messages.`);
1125
1138
  }
1126
1139
 
1127
1140
  return textResult(formatMessagesFull(messages));
1128
1141
  }
1129
1142
 
1130
1143
  default:
1131
- return textResult(`Unknown action: ${params.action}`);
1144
+ return textResult(`Unknown action: ${params.action ?? 'list'}`);
1132
1145
  }
1133
1146
  } catch (err) {
1134
1147
  const message = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
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.6.5",
32
+ "@jarkkojs/goosedump": "^0.7.4",
33
33
  "@sinclair/typebox": "^0.34.49"
34
34
  },
35
35
  "devDependencies": {