pi-goosedump 0.12.0 → 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 +16 -7
  2. package/dist/index.ts +394 -38
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -27,11 +27,13 @@ report that they are unavailable.
27
27
  provenance within a 4,096-byte/token-safe output budget.
28
28
  - **`goose_compact`** starts asynchronous compaction of the current Pi session.
29
29
  Set `keep` to preserve the last N user turns verbatim.
30
-
31
- The session-history tools accept an optional `provider` (`pi` by default),
32
- `session`, and `scope` (`lineage` by default or `all`). The current Pi session
33
- is used when both provider and session are omitted. Non-Pi providers require an
34
- 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.
35
37
 
36
38
  Examples:
37
39
 
@@ -43,6 +45,9 @@ goose_grep({ pattern: "*timeout*" })
43
45
  goose_recall({ query: "prior architecture decision" })
44
46
  goose_recall({ query: "database", type: "decision" })
45
47
  goose_compact({ keep: 2 })
48
+ goose_sessions({ action: "list" })
49
+ goose_forget({ hash: "a1b2c3d4..." })
50
+ goose_memory_status({})
46
51
  ```
47
52
 
48
53
  ## Commands
@@ -51,6 +56,9 @@ goose_compact({ keep: 2 })
51
56
  an inline prompt; selecting a result expands it.
52
57
  - **`/goose-compact [keep:N]`** compacts now. With `keep:N`, the last N user
53
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.
54
62
  - **`/goose-settings`** edits global and project compaction settings.
55
63
 
56
64
  ## Settings
@@ -70,8 +78,9 @@ Optional settings live under `goosedump` in:
70
78
 
71
79
  `turnsBeforeCompact` is an integer from 0 to 255. `0` disables scheduled
72
80
  compaction. The countdown is restored when a session resumes and appears in the
73
- footer as hexadecimal: `GD: 0A` means ten turns remain, `GD: 00` is ready to
74
- 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
75
84
  global setting.
76
85
 
77
86
  ## Compaction behavior
package/dist/index.ts CHANGED
@@ -366,18 +366,6 @@ function goosedumpUnavailableResult() {
366
366
  function compactStartedMessage(keep) {
367
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.";
368
368
  }
369
- function runGoosedump(args) {
370
- try {
371
- return execFileSync(process.execPath, [resolveGoosedumpBinary(), ...args], {
372
- encoding: "utf-8",
373
- maxBuffer: 16 * 1024 * 1024
374
- });
375
- } catch (err) {
376
- const stderr = err.stderr?.trim();
377
- const message = stderr ?? (err instanceof Error ? err.message : String(err));
378
- throw new Error(message);
379
- }
380
- }
381
369
  function runGoosedumpAsync(args, signal, executablePath = resolveNativeGoosedumpBinary()) {
382
370
  return new Promise((resolve, reject) => {
383
371
  execFile(executablePath, args, {
@@ -419,24 +407,27 @@ function appendScope(args, scope) {
419
407
  if (scope === "all")
420
408
  args.push("--all");
421
409
  }
422
- function goosedumpSearch(target, query, options = {}) {
410
+ async function goosedumpSearch(target, query, options = {}, signal, executablePath) {
423
411
  const args = ["session", "search", target, query, "--as", "pi"];
424
412
  appendScope(args, options.scope);
425
413
  if (options.page)
426
414
  args.push("--page", String(options.page));
427
- return parsePiMessages(runGoosedump(args));
415
+ const output = await runGoosedumpAsync(args, signal, executablePath);
416
+ return parsePiMessages(output.stdout);
428
417
  }
429
- function goosedumpGrep(target, pattern, options = {}) {
418
+ async function goosedumpGrep(target, pattern, options = {}, signal, executablePath) {
430
419
  const args = ["session", "find", target, pattern, "--as", "pi"];
431
420
  appendScope(args, options.scope);
432
- return parsePiMessages(runGoosedump(args));
421
+ const output = await runGoosedumpAsync(args, signal, executablePath);
422
+ return parsePiMessages(output.stdout);
433
423
  }
434
- function goosedumpShow(target, options = {}) {
424
+ async function goosedumpShow(target, options = {}, signal, executablePath) {
435
425
  const args = ["session", "show", target, "--as", "pi"];
436
426
  appendScope(args, options.scope);
437
427
  for (const id of options.ids ?? [])
438
428
  args.push("--entry", id);
439
- return parsePiMessages(runGoosedump(args));
429
+ const output = await runGoosedumpAsync(args, signal, executablePath);
430
+ return parsePiMessages(output.stdout);
440
431
  }
441
432
  async function goosedumpRecall(query, options = {}, signal, executablePath) {
442
433
  const args = ["memory", "search", query, "--limit", String(options.limit ?? 5)];
@@ -449,6 +440,28 @@ async function goosedumpRecall(query, options = {}, signal, executablePath) {
449
440
  const output = await runGoosedumpAsync(args, signal, executablePath);
450
441
  return { hits: parseMemoryHits(output.stdout), warning: output.stderr.trim() };
451
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
+ }
452
465
  function resolveTarget(ctx, provider, contextId) {
453
466
  if (contextId)
454
467
  return formatTarget(provider, contextId);
@@ -896,9 +909,9 @@ async function runSearchPanel(ctx, query) {
896
909
  let scroll = 0;
897
910
  let detailLines = [];
898
911
  let detailScroll = 0;
899
- function runSearch() {
912
+ async function runSearch() {
900
913
  try {
901
- const first = goosedumpSearch(target, query, { scope: "lineage", page: 1 });
914
+ const first = await goosedumpSearch(target, query, { scope: "lineage", page: 1 });
902
915
  if (first.length === 0) {
903
916
  entries = [];
904
917
  error = `No results for "${query}"`;
@@ -908,7 +921,7 @@ async function runSearchPanel(ctx, query) {
908
921
  }
909
922
  const all = [...first];
910
923
  for (let page = 2;page <= 10; page += 1) {
911
- const result = goosedumpSearch(target, query, { scope: "lineage", page });
924
+ const result = await goosedumpSearch(target, query, { scope: "lineage", page });
912
925
  if (result.length === 0)
913
926
  break;
914
927
  all.push(...result);
@@ -929,9 +942,9 @@ async function runSearchPanel(ctx, query) {
929
942
  } else {
930
943
  runSearch();
931
944
  }
932
- function loadDetail(id) {
945
+ async function loadDetail(id) {
933
946
  try {
934
- const messages = goosedumpShow(target, { ids: [id] });
947
+ const messages = await goosedumpShow(target, { ids: [id] });
935
948
  const content = messages.length > 0 ? messages[0].content : "No content for this entry.";
936
949
  detailLines = wrapTextWithAnsi(content, wrapWidth);
937
950
  detailScroll = 0;
@@ -1071,6 +1084,176 @@ async function runSearchPanel(ctx, query) {
1071
1084
  }
1072
1085
  });
1073
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
+ }
1074
1257
  function createGoosedumpIntegration(options = {}) {
1075
1258
  const shouldRegisterTool = options.registerTool ?? true;
1076
1259
  const shouldOverrideDefaultCompaction = options.overrideDefaultCompaction ?? true;
@@ -1094,8 +1277,19 @@ function createGoosedumpIntegration(options = {}) {
1094
1277
  if (!goosedumpAvailable)
1095
1278
  return;
1096
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, "●");
1097
1290
  const counter = active ? Math.max(0, turnsRemaining).toString(16).toUpperCase().padStart(2, "0") : "--";
1098
- 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}`);
1099
1293
  }
1100
1294
  function noteCompletedTurn(ctx) {
1101
1295
  if (!goosedumpAvailable || !shouldOverrideDefaultCompaction)
@@ -1186,6 +1380,56 @@ function createGoosedumpIntegration(options = {}) {
1186
1380
  }
1187
1381
  }
1188
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
+ }));
1189
1433
  pi.registerTool(defineTool({
1190
1434
  name: "goose_search",
1191
1435
  label: "goose_search",
@@ -1208,7 +1452,7 @@ function createGoosedumpIntegration(options = {}) {
1208
1452
  description: "Page number for ranked results"
1209
1453
  }))
1210
1454
  }),
1211
- async execute(_id, params, _signal, _onUpdate, ctx) {
1455
+ async execute(_id, params, signal, _onUpdate, ctx) {
1212
1456
  if (!goosedumpAvailable)
1213
1457
  return goosedumpUnavailableResult();
1214
1458
  try {
@@ -1217,10 +1461,7 @@ function createGoosedumpIntegration(options = {}) {
1217
1461
  if (!target)
1218
1462
  return missingSessionId("goose_search", provider);
1219
1463
  const page = params.page ?? 1;
1220
- const messages = goosedumpSearch(target, params.query, {
1221
- scope: params.scope ?? "lineage",
1222
- page
1223
- });
1464
+ const messages = await goosedumpSearch(target, params.query, { scope: params.scope ?? "lineage", page }, signal, nativeExecutablePath);
1224
1465
  if (messages.length === 0) {
1225
1466
  return textResult(`No results for "${params.query}"`);
1226
1467
  }
@@ -1255,7 +1496,7 @@ function createGoosedumpIntegration(options = {}) {
1255
1496
  session: historySessionParam(),
1256
1497
  scope: historyScopeParam()
1257
1498
  }),
1258
- async execute(_id, params, _signal, _onUpdate, ctx) {
1499
+ async execute(_id, params, signal, _onUpdate, ctx) {
1259
1500
  if (!goosedumpAvailable)
1260
1501
  return goosedumpUnavailableResult();
1261
1502
  try {
@@ -1263,10 +1504,7 @@ function createGoosedumpIntegration(options = {}) {
1263
1504
  const target = resolveTarget(ctx, provider, params.session);
1264
1505
  if (!target)
1265
1506
  return missingSessionId("goose_get", provider);
1266
- const messages = goosedumpShow(target, {
1267
- scope: params.scope ?? "lineage",
1268
- ids: params.ids
1269
- });
1507
+ const messages = await goosedumpShow(target, { scope: params.scope ?? "lineage", ids: params.ids }, signal, nativeExecutablePath);
1270
1508
  if (messages.length === 0) {
1271
1509
  return textResult("No entries found for the given IDs.");
1272
1510
  }
@@ -1294,7 +1532,7 @@ function createGoosedumpIntegration(options = {}) {
1294
1532
  session: historySessionParam(),
1295
1533
  scope: historyScopeParam()
1296
1534
  }),
1297
- async execute(_id, params, _signal, _onUpdate, ctx) {
1535
+ async execute(_id, params, signal, _onUpdate, ctx) {
1298
1536
  if (!goosedumpAvailable)
1299
1537
  return goosedumpUnavailableResult();
1300
1538
  try {
@@ -1302,9 +1540,7 @@ function createGoosedumpIntegration(options = {}) {
1302
1540
  const target = resolveTarget(ctx, provider, params.session);
1303
1541
  if (!target)
1304
1542
  return missingSessionId("goose_grep", provider);
1305
- const messages = goosedumpGrep(target, params.pattern, {
1306
- scope: params.scope ?? "lineage"
1307
- });
1543
+ const messages = await goosedumpGrep(target, params.pattern, { scope: params.scope ?? "lineage" }, signal, nativeExecutablePath);
1308
1544
  if (messages.length === 0) {
1309
1545
  return textResult(`No matches for "${params.pattern}"`);
1310
1546
  }
@@ -1344,6 +1580,72 @@ function createGoosedumpIntegration(options = {}) {
1344
1580
  return textResult(compactStartedMessage(keep));
1345
1581
  }
1346
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
+ }));
1347
1649
  }
1348
1650
  function maybeAutoCompact(ctx, resumeAfter) {
1349
1651
  if (!goosedumpAvailable || !shouldOverrideDefaultCompaction)
@@ -1525,6 +1827,60 @@ function createGoosedumpIntegration(options = {}) {
1525
1827
  }
1526
1828
  }
1527
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
+ });
1528
1884
  }
1529
1885
  return { register };
1530
1886
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.12.0",
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",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@earendil-works/pi-tui": "^0.78.0",
41
- "@jarkkojs/goosedump": "^0.12.0",
41
+ "@jarkkojs/goosedump": "^0.12.1",
42
42
  "@sinclair/typebox": "^0.34.49"
43
43
  },
44
44
  "devDependencies": {