pi-goosedump 0.11.3 → 0.12.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 +22 -18
  2. package/dist/index.ts +417 -53
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -23,19 +23,17 @@ report that they are unavailable.
23
23
  - **`goose_grep`** filters session messages with a glob pattern.
24
24
  - **`goose_recall`** searches memory harvested by earlier compactions. It
25
25
  searches the current working directory by default, can filter decisions,
26
- facts, preferences, procedures, or episodes, and returns governed, typed excerpts
27
- with trust, confidence, scope, derivation, and provenance within a
28
- 4,096-byte/token-safe output budget. Quarantined, expired, superseded, and
29
- contradicted entries are excluded.
26
+ facts, preferences, procedures, or episodes, and returns typed excerpts with
27
+ provenance within a 4,096-byte/token-safe output budget.
30
28
  - **`goose_compact`** starts asynchronous compaction of the current Pi session.
31
29
  Set `keep` to preserve the last N user turns verbatim.
32
- Trained memory is indexed through goosedump's durable, bounded-retry ingestion
33
- ledger; inspect terminal failures with `goosedump ingest review --status failed`.
34
-
35
- The session-history tools accept an optional `provider` (`pi` by default),
36
- `session`, and `scope` (`lineage` by default or `all`). The current Pi session
37
- is used when both provider and session are omitted. Non-Pi providers require an
38
- explicit session ID. Targets sent to goosedump are always provider-qualified.
30
+ - **`goose_sessions`** lists or deletes saved sessions scoped to the current working directory. Deleting the active session is safety-blocked.
31
+ - **`goose_forget`** deletes stored durable memory by content hash or provider:id. Writes durable tombstones to prevent re-learning.
32
+ - **`goose_memory_status`** inspects durable-memory counts and indexing health.
33
+ The session-history tools accept an optional `provider` (`pi` by default),
34
+ `session`, and `scope` (`lineage` by default or `all`). The current Pi session
35
+ is used when both provider and session are omitted. Non-Pi providers require an
36
+ explicit session ID. Targets sent to goosedump are always provider-qualified.
39
37
 
40
38
  Examples:
41
39
 
@@ -47,6 +45,9 @@ goose_grep({ pattern: "*timeout*" })
47
45
  goose_recall({ query: "prior architecture decision" })
48
46
  goose_recall({ query: "database", type: "decision" })
49
47
  goose_compact({ keep: 2 })
48
+ goose_sessions({ action: "list" })
49
+ goose_forget({ hash: "a1b2c3d4..." })
50
+ goose_memory_status({})
50
51
  ```
51
52
 
52
53
  ## Commands
@@ -55,6 +56,9 @@ goose_compact({ keep: 2 })
55
56
  an inline prompt; selecting a result expands it.
56
57
  - **`/goose-compact [keep:N]`** compacts now. With `keep:N`, the last N user
57
58
  turns remain verbatim; without it, Pi chooses the cut point.
59
+ - **`/goose-sessions`** lists or deletes saved sessions scoped to the current working directory.
60
+ - **`/goose-forget [hash|provider:id]`** deletes stored durable memory by hash or session.
61
+ - **`/goose-memory-status`** displays durable-memory storage health and indexing counts.
58
62
  - **`/goose-settings`** edits global and project compaction settings.
59
63
 
60
64
  ## Settings
@@ -74,18 +78,18 @@ Optional settings live under `goosedump` in:
74
78
 
75
79
  `turnsBeforeCompact` is an integer from 0 to 255. `0` disables scheduled
76
80
  compaction. The countdown is restored when a session resumes and appears in the
77
- footer as hexadecimal: `GD: 0A` means ten turns remain, `GD: 00` is ready to
78
- compact, and `GD: --` is disabled. Clearing the project value restores the
81
+ footer with a status dot (green when >25% turns remain, yellow when ≤25%, red when ≤12.5%)
82
+ followed by hexadecimal turn countdown: `● GD: 0A` means ten turns remain, `● GD: 00` is ready
83
+ to compact, and `● GD: --` is disabled. Clearing the project value restores the
79
84
  global setting.
80
85
 
81
86
  ## Compaction behavior
82
87
 
83
88
  The extension replaces Pi's normal summary during manual, scheduled, and
84
- automatic compaction. It passes `--train` so the compacted range is harvested
85
- into persistent memory, preserves the previous compaction summary, and asks
86
- goosedump to summarize only the newly compacted range. A `keep:N` request
87
- instead keeps the last N active user turns; it is cancelled when no earlier
88
- history is available to summarize.
89
+ automatic compaction. It invokes `goosedump session compact --remember`,
90
+ preserves the previous compaction summary, and summarizes only the newly
91
+ compacted range. A `keep:N` request keeps the last N active user turns;
92
+ it is cancelled when no earlier history is available to summarize.
89
93
 
90
94
  Compactions with custom instructions continue to use Pi's normal compactor. If
91
95
  goosedump fails, the extension reports the error and falls back to Pi. Successful
package/dist/index.ts CHANGED
@@ -1,3 +1,6 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) Jarkko Sakkinen 2026
3
+
1
4
  // index.ts
2
5
  import {
3
6
  DEFAULT_COMPACTION_SETTINGS,
@@ -289,7 +292,7 @@ function parsePiMessages(output) {
289
292
  function parseMemoryHits(output) {
290
293
  const value = JSON.parse(output);
291
294
  if (!Array.isArray(value))
292
- throw new Error("goosedump recall returned invalid JSON");
295
+ throw new Error("goosedump memory search returned invalid JSON");
293
296
  return value.filter((item) => isRecord(item) && typeof item.hash === "string" && typeof item.kind === "string" && typeof item.text === "string" && typeof item.score === "number" && typeof item.provider === "string" && typeof item.context_id === "string" && typeof item.entry_id === "string" && typeof item.path === "string" && typeof item.source_path === "string" && ["decision", "fact", "preference", "procedure", "episode"].includes(item.memory_type) && typeof item.trust === "number" && typeof item.confidence === "number" && (item.valid_from === null || typeof item.valid_from === "number") && (item.valid_until === null || typeof item.valid_until === "number") && ["context", "project"].includes(item.scope) && ["observed", "mutation"].includes(item.derivation));
294
297
  }
295
298
  function formatMemoryHit(hit, maxBytes) {
@@ -363,18 +366,6 @@ function goosedumpUnavailableResult() {
363
366
  function compactStartedMessage(keep) {
364
367
  return keep !== null ? `Compacting with goosedump; keeping the last ${keep} user turn${keep === 1 ? "" : "s"}.` : "Compacting with goosedump up to Pi's token-based cut.";
365
368
  }
366
- function runGoosedump(args) {
367
- try {
368
- return execFileSync(process.execPath, [resolveGoosedumpBinary(), ...args], {
369
- encoding: "utf-8",
370
- maxBuffer: 16 * 1024 * 1024
371
- });
372
- } catch (err) {
373
- const stderr = err.stderr?.trim();
374
- const message = stderr ?? (err instanceof Error ? err.message : String(err));
375
- throw new Error(message);
376
- }
377
- }
378
369
  function runGoosedumpAsync(args, signal, executablePath = resolveNativeGoosedumpBinary()) {
379
370
  return new Promise((resolve, reject) => {
380
371
  execFile(executablePath, args, {
@@ -412,26 +403,34 @@ function readFirstLine(path) {
412
403
  function formatTarget(provider, id) {
413
404
  return `${provider}:${id}`;
414
405
  }
415
- function goosedumpSearch(target, query, options = {}) {
416
- const scope = options.scope ?? "lineage";
417
- const args = ["search", target, query, "--as-provider", "pi", "--scope", scope];
406
+ function appendScope(args, scope) {
407
+ if (scope === "all")
408
+ args.push("--all");
409
+ }
410
+ async function goosedumpSearch(target, query, options = {}, signal, executablePath) {
411
+ const args = ["session", "search", target, query, "--as", "pi"];
412
+ appendScope(args, options.scope);
418
413
  if (options.page)
419
- args.push("-p", String(options.page));
420
- return parsePiMessages(runGoosedump(args));
414
+ args.push("--page", String(options.page));
415
+ const output = await runGoosedumpAsync(args, signal, executablePath);
416
+ return parsePiMessages(output.stdout);
421
417
  }
422
- function goosedumpGrep(target, pattern, options = {}) {
423
- const scope = options.scope ?? "lineage";
424
- return parsePiMessages(runGoosedump(["grep", target, pattern, "--as-provider", "pi", "--scope", scope]));
418
+ async function goosedumpGrep(target, pattern, options = {}, signal, executablePath) {
419
+ const args = ["session", "find", target, pattern, "--as", "pi"];
420
+ appendScope(args, options.scope);
421
+ const output = await runGoosedumpAsync(args, signal, executablePath);
422
+ return parsePiMessages(output.stdout);
425
423
  }
426
- function goosedumpShow(target, options = {}) {
427
- const scope = options.scope ?? "lineage";
428
- const args = ["show", target, "--as-provider", "pi", "--scope", scope];
424
+ async function goosedumpShow(target, options = {}, signal, executablePath) {
425
+ const args = ["session", "show", target, "--as", "pi"];
426
+ appendScope(args, options.scope);
429
427
  for (const id of options.ids ?? [])
430
- args.push("-m", id);
431
- return parsePiMessages(runGoosedump(args));
428
+ args.push("--entry", id);
429
+ const output = await runGoosedumpAsync(args, signal, executablePath);
430
+ return parsePiMessages(output.stdout);
432
431
  }
433
432
  async function goosedumpRecall(query, options = {}, signal, executablePath) {
434
- const args = ["recall", query, "--limit", String(options.limit ?? 5)];
433
+ const args = ["memory", "search", query, "--limit", String(options.limit ?? 5)];
435
434
  if (options.provider)
436
435
  args.push("--provider", options.provider);
437
436
  if (options.path)
@@ -441,6 +440,28 @@ async function goosedumpRecall(query, options = {}, signal, executablePath) {
441
440
  const output = await runGoosedumpAsync(args, signal, executablePath);
442
441
  return { hits: parseMemoryHits(output.stdout), warning: output.stderr.trim() };
443
442
  }
443
+ async function goosedumpMemoryForget(target, signal, executablePath) {
444
+ const output = await runGoosedumpAsync(["memory", "forget", target], signal, executablePath);
445
+ return output.stdout.trim() || `Removed memory target: ${target}`;
446
+ }
447
+ async function goosedumpMemoryStatus(signal, executablePath) {
448
+ const output = await runGoosedumpAsync(["memory", "status"], signal, executablePath);
449
+ return output.stdout.trim();
450
+ }
451
+ async function goosedumpListSessions(signal, executablePath) {
452
+ const args = ["session", "list", "--json"];
453
+ const output = await runGoosedumpAsync(args, signal, executablePath);
454
+ try {
455
+ const parsed = JSON.parse(output.stdout);
456
+ return Array.isArray(parsed) ? parsed : [];
457
+ } catch {
458
+ return [];
459
+ }
460
+ }
461
+ async function goosedumpRemoveSession(target, signal, executablePath) {
462
+ const args = ["session", "remove", target, "--yes"];
463
+ await runGoosedumpAsync(args, signal, executablePath);
464
+ }
444
465
  function resolveTarget(ctx, provider, contextId) {
445
466
  if (contextId)
446
467
  return formatTarget(provider, contextId);
@@ -468,13 +489,13 @@ function historyScopeParam() {
468
489
  }));
469
490
  }
470
491
  async function goosedumpCompact(target, options, signal, executablePath) {
471
- const scope = options.scope ?? "lineage";
472
- const args = ["compact", target, "--train", "--scope", scope];
492
+ const args = ["session", "compact", target, "--remember"];
493
+ appendScope(args, options.scope);
473
494
  if (options.from)
474
495
  args.push("--from", options.from);
475
496
  if (options.previousSummary)
476
497
  args.push("--previous-summary", options.previousSummary);
477
- args.push("--until", options.until);
498
+ args.push("--before", options.until);
478
499
  const output = await runGoosedumpAsync(args, signal, executablePath);
479
500
  for (const line of output.stdout.split(`
480
501
  `)) {
@@ -888,9 +909,9 @@ async function runSearchPanel(ctx, query) {
888
909
  let scroll = 0;
889
910
  let detailLines = [];
890
911
  let detailScroll = 0;
891
- function runSearch() {
912
+ async function runSearch() {
892
913
  try {
893
- const first = goosedumpSearch(target, query, { scope: "lineage", page: 1 });
914
+ const first = await goosedumpSearch(target, query, { scope: "lineage", page: 1 });
894
915
  if (first.length === 0) {
895
916
  entries = [];
896
917
  error = `No results for "${query}"`;
@@ -900,7 +921,7 @@ async function runSearchPanel(ctx, query) {
900
921
  }
901
922
  const all = [...first];
902
923
  for (let page = 2;page <= 10; page += 1) {
903
- const result = goosedumpSearch(target, query, { scope: "lineage", page });
924
+ const result = await goosedumpSearch(target, query, { scope: "lineage", page });
904
925
  if (result.length === 0)
905
926
  break;
906
927
  all.push(...result);
@@ -921,9 +942,9 @@ async function runSearchPanel(ctx, query) {
921
942
  } else {
922
943
  runSearch();
923
944
  }
924
- function loadDetail(id) {
945
+ async function loadDetail(id) {
925
946
  try {
926
- const messages = goosedumpShow(target, { ids: [id] });
947
+ const messages = await goosedumpShow(target, { ids: [id] });
927
948
  const content = messages.length > 0 ? messages[0].content : "No content for this entry.";
928
949
  detailLines = wrapTextWithAnsi(content, wrapWidth);
929
950
  detailScroll = 0;
@@ -1063,6 +1084,176 @@ async function runSearchPanel(ctx, query) {
1063
1084
  }
1064
1085
  });
1065
1086
  }
1087
+ async function runSessionsPanel(ctx, executablePath) {
1088
+ const OVERLAY_WIDTH = 84;
1089
+ const VIEWPORT = 18;
1090
+ await ctx.ui.custom((tui, theme, _kb, done) => {
1091
+ const currentTarget = currentSessionTarget(ctx);
1092
+ const borderFg = (s) => theme.fg("border", s);
1093
+ const border = theme.fg("border", "│");
1094
+ const accent = (s) => theme.fg("accent", s);
1095
+ const text = (s) => theme.fg("text", s);
1096
+ const dim = (s) => theme.fg("dim", s);
1097
+ const muted = (s) => theme.fg("muted", s);
1098
+ let sessions = [];
1099
+ let error = null;
1100
+ let selected = 0;
1101
+ let scroll = 0;
1102
+ async function loadSessions() {
1103
+ try {
1104
+ const all = await goosedumpListSessions(undefined, executablePath);
1105
+ const cwdSessions = all.filter((s) => s.cwd === ctx.cwd);
1106
+ cwdSessions.sort((a, b) => {
1107
+ const timeA = a.from ? new Date(a.from).getTime() : 0;
1108
+ const timeB = b.from ? new Date(b.from).getTime() : 0;
1109
+ return timeB - timeA;
1110
+ });
1111
+ sessions = cwdSessions;
1112
+ if (sessions.length === 0) {
1113
+ error = "No sessions found for current directory.";
1114
+ } else {
1115
+ error = null;
1116
+ }
1117
+ selected = Math.min(selected, Math.max(0, sessions.length - 1));
1118
+ scroll = Math.min(scroll, Math.max(0, sessions.length - VIEWPORT));
1119
+ tui.requestRender();
1120
+ } catch (err) {
1121
+ sessions = [];
1122
+ error = err instanceof Error ? err.message : String(err);
1123
+ tui.requestRender();
1124
+ }
1125
+ }
1126
+ loadSessions();
1127
+ async function deleteSelected() {
1128
+ const s = sessions[selected];
1129
+ if (!s)
1130
+ return;
1131
+ const target = formatTarget(s.provider, s.id);
1132
+ if (currentTarget === target) {
1133
+ ctx.ui.notify("Cannot delete the active session.", "error");
1134
+ return;
1135
+ }
1136
+ try {
1137
+ await goosedumpRemoveSession(target, undefined, executablePath);
1138
+ ctx.ui.notify(`Deleted session ${s.id}`, "info");
1139
+ await loadSessions();
1140
+ } catch (err) {
1141
+ const message = err instanceof Error ? err.message : String(err);
1142
+ ctx.ui.notify(`Failed to delete session: ${message}`, "error");
1143
+ }
1144
+ }
1145
+ return {
1146
+ render(width) {
1147
+ const innerW = width - 4;
1148
+ const lines = [];
1149
+ const title = accent(" Goosedump Sessions ");
1150
+ const topFill = borderFg("─".repeat(Math.max(0, width - 4 - visibleWidth(title))));
1151
+ lines.push(`${borderFg("╭─")}${title}${topFill}${borderFg("─╮")}`);
1152
+ if (error) {
1153
+ lines.push(makeRow(muted(error), innerW, border));
1154
+ lines.push(makeRow(dim("esc close"), innerW, border));
1155
+ lines.push(`${borderFg("╰")}${borderFg("─".repeat(width - 2))}${borderFg("╯")}`);
1156
+ return lines;
1157
+ }
1158
+ const maxScroll = Math.max(0, sessions.length - VIEWPORT);
1159
+ const clampScroll = Math.min(scroll, maxScroll);
1160
+ const window = sessions.slice(clampScroll, clampScroll + VIEWPORT);
1161
+ for (let i = 0;i < window.length; i++) {
1162
+ const absolute = clampScroll + i;
1163
+ const s = window[i];
1164
+ const isSelected = absolute === selected;
1165
+ const isActive = currentTarget === formatTarget(s.provider, s.id);
1166
+ const cursor = isSelected ? accent("▶") : " ";
1167
+ const activeMark = isActive ? accent("(active)") : "";
1168
+ const idText = isSelected ? accent(s.id) : text(s.id);
1169
+ const providerTag = muted(`[${s.provider}]`);
1170
+ const countTag = dim(`${s.count} entries`);
1171
+ const dateTag = s.from ? dim(new Date(s.from).toLocaleDateString()) : "";
1172
+ const rowContent = ` ${cursor} ${idText} ${providerTag} ${countTag} ${dateTag} ${activeMark}`.trimEnd();
1173
+ lines.push(makeRow(rowContent, innerW, border));
1174
+ }
1175
+ for (let i = window.length;i < VIEWPORT; i++)
1176
+ lines.push(makeRow("", innerW, border));
1177
+ const count = sessions.length;
1178
+ const position = count > VIEWPORT ? `${clampScroll + 1}-${Math.min(clampScroll + VIEWPORT, count)} of ${count}` : `${count} session${count === 1 ? "" : "s"}`;
1179
+ lines.push(makeRow("", innerW, border));
1180
+ lines.push(makeRow(dim(`↑↓ navigate enter resume d delete esc close (${position})`), innerW, border));
1181
+ lines.push(`${borderFg("╰")}${borderFg("─".repeat(width - 2))}${borderFg("╯")}`);
1182
+ return lines;
1183
+ },
1184
+ handleInput(data) {
1185
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
1186
+ done(undefined);
1187
+ return;
1188
+ }
1189
+ if (error && sessions.length === 0)
1190
+ return;
1191
+ const maxScroll = Math.max(0, sessions.length - VIEWPORT);
1192
+ if (matchesKey(data, Key.up)) {
1193
+ selected = Math.max(0, selected - 1);
1194
+ if (selected < scroll)
1195
+ scroll = selected;
1196
+ tui.requestRender();
1197
+ return;
1198
+ }
1199
+ if (matchesKey(data, Key.down)) {
1200
+ selected = Math.min(Math.max(0, sessions.length - 1), selected + 1);
1201
+ if (selected >= scroll + VIEWPORT)
1202
+ scroll = selected - VIEWPORT + 1;
1203
+ tui.requestRender();
1204
+ return;
1205
+ }
1206
+ if (matchesKey(data, Key.pageUp)) {
1207
+ selected = Math.max(0, selected - VIEWPORT);
1208
+ scroll = Math.max(0, Math.min(scroll, selected));
1209
+ tui.requestRender();
1210
+ return;
1211
+ }
1212
+ if (matchesKey(data, Key.pageDown)) {
1213
+ selected = Math.min(sessions.length - 1, selected + VIEWPORT);
1214
+ scroll = Math.min(maxScroll, Math.max(scroll, selected - VIEWPORT + 1));
1215
+ tui.requestRender();
1216
+ return;
1217
+ }
1218
+ if (data === "d" || data === "D" || matchesKey(data, Key.delete)) {
1219
+ deleteSelected();
1220
+ return;
1221
+ }
1222
+ if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
1223
+ const s = sessions[selected];
1224
+ if (!s)
1225
+ return;
1226
+ if (s.provider !== "pi") {
1227
+ ctx.ui.notify(`Only Pi sessions can be resumed (session ${s.id} is ${s.provider})`, "warning");
1228
+ return;
1229
+ }
1230
+ const sessionDir = ctx.sessionManager.getSessionDir();
1231
+ const sessionPath = join(sessionDir, s.id.endsWith(".jsonl") ? s.id : `${s.id}.jsonl`);
1232
+ if (!existsSync(sessionPath)) {
1233
+ ctx.ui.notify(`Session transcript file not found: ${sessionPath}`, "error");
1234
+ return;
1235
+ }
1236
+ done(undefined);
1237
+ const cmdCtx = ctx;
1238
+ if (typeof cmdCtx.switchSession === "function") {
1239
+ cmdCtx.switchSession(sessionPath);
1240
+ } else {
1241
+ ctx.ui.notify(`Session switched to ${sessionPath}`, "info");
1242
+ }
1243
+ return;
1244
+ }
1245
+ },
1246
+ invalidate() {}
1247
+ };
1248
+ }, {
1249
+ overlay: true,
1250
+ overlayOptions: {
1251
+ anchor: "center",
1252
+ width: OVERLAY_WIDTH,
1253
+ margin: 2
1254
+ }
1255
+ });
1256
+ }
1066
1257
  function createGoosedumpIntegration(options = {}) {
1067
1258
  const shouldRegisterTool = options.registerTool ?? true;
1068
1259
  const shouldOverrideDefaultCompaction = options.overrideDefaultCompaction ?? true;
@@ -1086,8 +1277,19 @@ function createGoosedumpIntegration(options = {}) {
1086
1277
  if (!goosedumpAvailable)
1087
1278
  return;
1088
1279
  const active = shouldOverrideDefaultCompaction && compactSettings.turnsBeforeCompact > 0;
1280
+ let dotColor = "success";
1281
+ if (active) {
1282
+ const remainingRatio = turnsRemaining / compactSettings.turnsBeforeCompact;
1283
+ if (remainingRatio <= 0.125) {
1284
+ dotColor = "error";
1285
+ } else if (remainingRatio <= 0.25) {
1286
+ dotColor = "warning";
1287
+ }
1288
+ }
1289
+ const dot = ctx.ui.theme.fg(dotColor, "●");
1089
1290
  const counter = active ? Math.max(0, turnsRemaining).toString(16).toUpperCase().padStart(2, "0") : "--";
1090
- ctx.ui.setStatus("goosedump", ctx.ui.theme.fg("text", `GD: ${counter}`));
1291
+ const label = ctx.ui.theme.fg("text", `GD: ${counter}`);
1292
+ ctx.ui.setStatus("goosedump", `${dot} ${label}`);
1091
1293
  }
1092
1294
  function noteCompletedTurn(ctx) {
1093
1295
  if (!goosedumpAvailable || !shouldOverrideDefaultCompaction)
@@ -1123,7 +1325,7 @@ function createGoosedumpIntegration(options = {}) {
1123
1325
  pendingKeep = null;
1124
1326
  const message = error instanceof Error ? error.message : String(error);
1125
1327
  if (ctx.hasUI)
1126
- ctx.ui.notify(`goosedump compact failed: ${message}`, "error");
1328
+ ctx.ui.notify(`goosedump compaction failed: ${message}`, "error");
1127
1329
  }
1128
1330
  });
1129
1331
  return "started";
@@ -1178,6 +1380,56 @@ function createGoosedumpIntegration(options = {}) {
1178
1380
  }
1179
1381
  }
1180
1382
  }));
1383
+ pi.registerTool(defineTool({
1384
+ name: "goose_forget",
1385
+ label: "goose_forget",
1386
+ description: "Forget/remove durable context entries from goosedump memory.",
1387
+ promptSnippet: "Remove obsolete entries from durable memory store",
1388
+ promptGuidelines: [
1389
+ "Use goose_forget to delete stale, inaccurate, or obsolete entries from durable memory.",
1390
+ "Requires a target memory ID or key."
1391
+ ],
1392
+ parameters: Type.Object({
1393
+ target: Type.String({
1394
+ description: "Memory target/ID to forget (e.g. entry ID or key pattern)"
1395
+ })
1396
+ }),
1397
+ async execute(_id, params, signal, _onUpdate, _ctx) {
1398
+ if (!goosedumpAvailable)
1399
+ return goosedumpUnavailableResult();
1400
+ if (!params.target || params.target.trim().length === 0) {
1401
+ return textResult("goose_forget error: target is required");
1402
+ }
1403
+ try {
1404
+ const res = await goosedumpMemoryForget(params.target.trim(), signal, nativeExecutablePath);
1405
+ return textResult(res);
1406
+ } catch (err) {
1407
+ const message = err instanceof Error ? err.message : String(err);
1408
+ return textResult(`goosedump error: ${message}`);
1409
+ }
1410
+ }
1411
+ }));
1412
+ pi.registerTool(defineTool({
1413
+ name: "goose_memory_status",
1414
+ label: "goose_memory_status",
1415
+ description: "Show durable memory status, indexing stats, and governance breakdown.",
1416
+ promptSnippet: "Inspect durable memory health and statistics",
1417
+ promptGuidelines: [
1418
+ "Use goose_memory_status to check memory index volume, health, and ingestion queue status."
1419
+ ],
1420
+ parameters: Type.Object({}),
1421
+ async execute(_id, _params, signal, _onUpdate, _ctx) {
1422
+ if (!goosedumpAvailable)
1423
+ return goosedumpUnavailableResult();
1424
+ try {
1425
+ const res = await goosedumpMemoryStatus(signal, nativeExecutablePath);
1426
+ return textResult(res);
1427
+ } catch (err) {
1428
+ const message = err instanceof Error ? err.message : String(err);
1429
+ return textResult(`goosedump error: ${message}`);
1430
+ }
1431
+ }
1432
+ }));
1181
1433
  pi.registerTool(defineTool({
1182
1434
  name: "goose_search",
1183
1435
  label: "goose_search",
@@ -1200,7 +1452,7 @@ function createGoosedumpIntegration(options = {}) {
1200
1452
  description: "Page number for ranked results"
1201
1453
  }))
1202
1454
  }),
1203
- async execute(_id, params, _signal, _onUpdate, ctx) {
1455
+ async execute(_id, params, signal, _onUpdate, ctx) {
1204
1456
  if (!goosedumpAvailable)
1205
1457
  return goosedumpUnavailableResult();
1206
1458
  try {
@@ -1209,10 +1461,7 @@ function createGoosedumpIntegration(options = {}) {
1209
1461
  if (!target)
1210
1462
  return missingSessionId("goose_search", provider);
1211
1463
  const page = params.page ?? 1;
1212
- const messages = goosedumpSearch(target, params.query, {
1213
- scope: params.scope ?? "lineage",
1214
- page
1215
- });
1464
+ const messages = await goosedumpSearch(target, params.query, { scope: params.scope ?? "lineage", page }, signal, nativeExecutablePath);
1216
1465
  if (messages.length === 0) {
1217
1466
  return textResult(`No results for "${params.query}"`);
1218
1467
  }
@@ -1247,7 +1496,7 @@ function createGoosedumpIntegration(options = {}) {
1247
1496
  session: historySessionParam(),
1248
1497
  scope: historyScopeParam()
1249
1498
  }),
1250
- async execute(_id, params, _signal, _onUpdate, ctx) {
1499
+ async execute(_id, params, signal, _onUpdate, ctx) {
1251
1500
  if (!goosedumpAvailable)
1252
1501
  return goosedumpUnavailableResult();
1253
1502
  try {
@@ -1255,10 +1504,7 @@ function createGoosedumpIntegration(options = {}) {
1255
1504
  const target = resolveTarget(ctx, provider, params.session);
1256
1505
  if (!target)
1257
1506
  return missingSessionId("goose_get", provider);
1258
- const messages = goosedumpShow(target, {
1259
- scope: params.scope ?? "lineage",
1260
- ids: params.ids
1261
- });
1507
+ const messages = await goosedumpShow(target, { scope: params.scope ?? "lineage", ids: params.ids }, signal, nativeExecutablePath);
1262
1508
  if (messages.length === 0) {
1263
1509
  return textResult("No entries found for the given IDs.");
1264
1510
  }
@@ -1286,7 +1532,7 @@ function createGoosedumpIntegration(options = {}) {
1286
1532
  session: historySessionParam(),
1287
1533
  scope: historyScopeParam()
1288
1534
  }),
1289
- async execute(_id, params, _signal, _onUpdate, ctx) {
1535
+ async execute(_id, params, signal, _onUpdate, ctx) {
1290
1536
  if (!goosedumpAvailable)
1291
1537
  return goosedumpUnavailableResult();
1292
1538
  try {
@@ -1294,9 +1540,7 @@ function createGoosedumpIntegration(options = {}) {
1294
1540
  const target = resolveTarget(ctx, provider, params.session);
1295
1541
  if (!target)
1296
1542
  return missingSessionId("goose_grep", provider);
1297
- const messages = goosedumpGrep(target, params.pattern, {
1298
- scope: params.scope ?? "lineage"
1299
- });
1543
+ const messages = await goosedumpGrep(target, params.pattern, { scope: params.scope ?? "lineage" }, signal, nativeExecutablePath);
1300
1544
  if (messages.length === 0) {
1301
1545
  return textResult(`No matches for "${params.pattern}"`);
1302
1546
  }
@@ -1336,6 +1580,72 @@ function createGoosedumpIntegration(options = {}) {
1336
1580
  return textResult(compactStartedMessage(keep));
1337
1581
  }
1338
1582
  }));
1583
+ pi.registerTool(defineTool({
1584
+ name: "goose_sessions",
1585
+ label: "goose_sessions",
1586
+ description: "List or delete session IDs for the current working directory.",
1587
+ promptSnippet: "List or delete coding agent sessions for current directory",
1588
+ promptGuidelines: [
1589
+ "Use goose_sessions to discover sessions in the current directory or delete a non-active session.",
1590
+ "Strictly scoped to the current working directory.",
1591
+ "Deleting the currently active session is not allowed."
1592
+ ],
1593
+ parameters: Type.Object({
1594
+ action: Type.Optional(Type.Union([Type.Literal("list"), Type.Literal("delete")], {
1595
+ default: "list",
1596
+ description: "Action: list (default) or delete"
1597
+ })),
1598
+ provider: historyProviderParam("list"),
1599
+ session: Type.Optional(Type.String({
1600
+ description: "Session ID to delete (required for action: delete)"
1601
+ }))
1602
+ }),
1603
+ async execute(_id, params, signal, _onUpdate, ctx) {
1604
+ if (!goosedumpAvailable)
1605
+ return goosedumpUnavailableResult();
1606
+ const action = params.action ?? "list";
1607
+ const provider = params.provider ?? "pi";
1608
+ const currentTarget = currentSessionTarget(ctx);
1609
+ if (action === "delete") {
1610
+ if (!params.session) {
1611
+ return textResult("goose_sessions error: session ID is required for action: delete");
1612
+ }
1613
+ const target = formatTarget(provider, params.session);
1614
+ if (currentTarget && target === currentTarget) {
1615
+ return textResult("Cannot delete the active session.");
1616
+ }
1617
+ try {
1618
+ await goosedumpRemoveSession(target, signal, nativeExecutablePath);
1619
+ return textResult(`Deleted session ${target}`);
1620
+ } catch (err) {
1621
+ const message = err instanceof Error ? err.message : String(err);
1622
+ return textResult(`goosedump error: ${message}`);
1623
+ }
1624
+ }
1625
+ try {
1626
+ const sessions = await goosedumpListSessions(signal, nativeExecutablePath);
1627
+ const cwdSessions = sessions.filter((s) => s.cwd === ctx.cwd && s.provider === provider);
1628
+ if (cwdSessions.length === 0) {
1629
+ return textResult(`No ${provider} sessions found for ${ctx.cwd}`);
1630
+ }
1631
+ const lines = cwdSessions.map((s) => {
1632
+ const target = formatTarget(s.provider, s.id);
1633
+ const isActive = currentTarget === target;
1634
+ const activeMarker = isActive ? " (active)" : "";
1635
+ const dateStr = s.from ? new Date(s.from).toISOString() : "unknown date";
1636
+ const countStr = `${s.count} entries`;
1637
+ return `- ${s.id}${activeMarker} [${countStr}; ${dateStr}]`;
1638
+ });
1639
+ return textResult(`Sessions for ${ctx.cwd} (${provider}):
1640
+
1641
+ ${lines.join(`
1642
+ `)}`);
1643
+ } catch (err) {
1644
+ const message = err instanceof Error ? err.message : String(err);
1645
+ return textResult(`goosedump error: ${message}`);
1646
+ }
1647
+ }
1648
+ }));
1339
1649
  }
1340
1650
  function maybeAutoCompact(ctx, resumeAfter) {
1341
1651
  if (!goosedumpAvailable || !shouldOverrideDefaultCompaction)
@@ -1370,7 +1680,7 @@ function createGoosedumpIntegration(options = {}) {
1370
1680
  updateStatus(ctx);
1371
1681
  const message = error instanceof Error ? error.message : String(error);
1372
1682
  if (ctx.hasUI)
1373
- ctx.ui.notify(`goosedump compact failed: ${message}`, "error");
1683
+ ctx.ui.notify(`goosedump compaction failed: ${message}`, "error");
1374
1684
  }
1375
1685
  });
1376
1686
  }
@@ -1424,7 +1734,7 @@ function createGoosedumpIntegration(options = {}) {
1424
1734
  return { cancel: true };
1425
1735
  if (ctx.hasUI) {
1426
1736
  const message = err instanceof Error ? err.message : String(err);
1427
- ctx.ui.notify(`goosedump compact failed; falling back to Pi compaction: ${message}`, "error");
1737
+ ctx.ui.notify(`goosedump compaction failed; falling back to Pi compaction: ${message}`, "error");
1428
1738
  }
1429
1739
  return;
1430
1740
  } finally {
@@ -1517,6 +1827,60 @@ function createGoosedumpIntegration(options = {}) {
1517
1827
  }
1518
1828
  }
1519
1829
  });
1830
+ maybePi.registerCommand?.("goose-sessions", {
1831
+ description: "Manage and switch between sessions for the current working directory",
1832
+ handler: async (_args, ctx) => {
1833
+ if (!goosedumpAvailable) {
1834
+ ctx.ui.notify(GOOSEDUMP_UNAVAILABLE_MESSAGE, "error");
1835
+ return;
1836
+ }
1837
+ try {
1838
+ await runSessionsPanel(ctx, nativeExecutablePath);
1839
+ } catch (err) {
1840
+ const message = err instanceof Error ? err.message : String(err);
1841
+ ctx.ui.notify(`goosedump error: ${message}`, "error");
1842
+ }
1843
+ }
1844
+ });
1845
+ maybePi.registerCommand?.("goose-forget", {
1846
+ description: "Forget/remove an entry from goosedump durable memory",
1847
+ handler: async (args, ctx) => {
1848
+ if (!goosedumpAvailable) {
1849
+ ctx.ui.notify(GOOSEDUMP_UNAVAILABLE_MESSAGE, "error");
1850
+ return;
1851
+ }
1852
+ let target = typeof args === "string" ? args.trim() : "";
1853
+ if (target.length === 0) {
1854
+ const input = await ctx.ui.input("Memory target/ID to forget:", "");
1855
+ target = (input ?? "").trim();
1856
+ }
1857
+ if (target.length === 0)
1858
+ return;
1859
+ try {
1860
+ const res = await goosedumpMemoryForget(target, undefined, nativeExecutablePath);
1861
+ ctx.ui.notify(res, "info");
1862
+ } catch (err) {
1863
+ const message = err instanceof Error ? err.message : String(err);
1864
+ ctx.ui.notify(`goosedump error: ${message}`, "error");
1865
+ }
1866
+ }
1867
+ });
1868
+ maybePi.registerCommand?.("goose-memory-status", {
1869
+ description: "Show durable memory status and health stats",
1870
+ handler: async (_args, ctx) => {
1871
+ if (!goosedumpAvailable) {
1872
+ ctx.ui.notify(GOOSEDUMP_UNAVAILABLE_MESSAGE, "error");
1873
+ return;
1874
+ }
1875
+ try {
1876
+ const res = await goosedumpMemoryStatus(undefined, nativeExecutablePath);
1877
+ ctx.ui.notify(res, "info");
1878
+ } catch (err) {
1879
+ const message = err instanceof Error ? err.message : String(err);
1880
+ ctx.ui.notify(`goosedump error: ${message}`, "error");
1881
+ }
1882
+ }
1883
+ });
1520
1884
  }
1521
1885
  return { register };
1522
1886
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.11.3",
3
+ "version": "0.12.1",
4
4
  "description": "Pi extension for goosedump-backed session-history search, entry expansion, and compaction",
5
5
  "keywords": [
6
6
  "compaction",
@@ -24,7 +24,7 @@
24
24
  ".": "./dist/index.ts"
25
25
  },
26
26
  "scripts": {
27
- "build": "bun build index.ts --outfile dist/index.ts --target=node --packages=external",
27
+ "build": "bun build index.ts --outfile dist/index.ts --target=node --packages=external --banner='// SPDX-License-Identifier: Apache-2.0\n// Copyright (C) Jarkko Sakkinen 2026\n'",
28
28
  "prepack": "npm run build",
29
29
  "fmt": "oxfmt index.ts tests package.json tsconfig.json .oxfmtrc.json README.md",
30
30
  "lint": "oxlint index.ts tests",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@earendil-works/pi-tui": "^0.78.0",
41
- "@jarkkojs/goosedump": "^0.11.3",
41
+ "@jarkkojs/goosedump": "^0.12.1",
42
42
  "@sinclair/typebox": "^0.34.49"
43
43
  },
44
44
  "devDependencies": {