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.
- package/README.md +22 -18
- package/dist/index.ts +417 -53
- 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
|
|
27
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
|
78
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
|
416
|
-
|
|
417
|
-
|
|
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("
|
|
420
|
-
|
|
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
|
|
424
|
-
|
|
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
|
|
428
|
-
|
|
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("
|
|
431
|
-
|
|
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 = ["
|
|
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
|
|
472
|
-
|
|
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("--
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
41
|
+
"@jarkkojs/goosedump": "^0.12.1",
|
|
42
42
|
"@sinclair/typebox": "^0.34.49"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|