pi-goosedump 0.11.2 → 0.12.0
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 +6 -11
- package/dist/index.ts +1535 -0
- package/package.json +10 -5
- package/index.ts +0 -2065
package/dist/index.ts
ADDED
|
@@ -0,0 +1,1535 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) Jarkko Sakkinen 2026
|
|
3
|
+
|
|
4
|
+
// index.ts
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_COMPACTION_SETTINGS,
|
|
7
|
+
defineTool,
|
|
8
|
+
findCutPoint,
|
|
9
|
+
getSettingsListTheme
|
|
10
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import {
|
|
14
|
+
closeSync,
|
|
15
|
+
existsSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
openSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
readSync,
|
|
20
|
+
renameSync,
|
|
21
|
+
unlinkSync,
|
|
22
|
+
writeFileSync
|
|
23
|
+
} from "node:fs";
|
|
24
|
+
import { createRequire } from "node:module";
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
27
|
+
import {
|
|
28
|
+
Input,
|
|
29
|
+
Key,
|
|
30
|
+
SettingsList,
|
|
31
|
+
decodeKittyPrintable,
|
|
32
|
+
getKeybindings,
|
|
33
|
+
matchesKey,
|
|
34
|
+
truncateToWidth,
|
|
35
|
+
visibleWidth,
|
|
36
|
+
wrapTextWithAnsi
|
|
37
|
+
} from "@earendil-works/pi-tui";
|
|
38
|
+
import { Type } from "@sinclair/typebox";
|
|
39
|
+
var require2 = createRequire(import.meta.url);
|
|
40
|
+
var BRANCH_SUMMARY_PREFIX = `The following is a summary of a branch that this conversation came back from:
|
|
41
|
+
|
|
42
|
+
<summary>
|
|
43
|
+
`;
|
|
44
|
+
var BRANCH_SUMMARY_SUFFIX = "</summary>";
|
|
45
|
+
var RECALL_TOKEN_BUDGET = 4096;
|
|
46
|
+
var RECALL_MAX_BYTES = RECALL_TOKEN_BUDGET;
|
|
47
|
+
var MEMORY_HIT_SEPARATOR = `
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
`;
|
|
52
|
+
var MEMORY_TEXT_TRUNCATED = `
|
|
53
|
+
[Entry text truncated]`;
|
|
54
|
+
var DEFAULT_GOOSEDUMP_SETTINGS = {
|
|
55
|
+
turnsBeforeCompact: 0
|
|
56
|
+
};
|
|
57
|
+
var TURNS_BEFORE_COMPACT_MIN = 0;
|
|
58
|
+
var TURNS_BEFORE_COMPACT_MAX = 255;
|
|
59
|
+
function isTurnsBeforeCompact(value) {
|
|
60
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= TURNS_BEFORE_COMPACT_MIN && value <= TURNS_BEFORE_COMPACT_MAX;
|
|
61
|
+
}
|
|
62
|
+
function piAgentDir() {
|
|
63
|
+
return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
64
|
+
}
|
|
65
|
+
function globalSettingsPath() {
|
|
66
|
+
return join(piAgentDir(), "settings.json");
|
|
67
|
+
}
|
|
68
|
+
function projectSettingsPath(cwd) {
|
|
69
|
+
return join(cwd, ".pi", "settings.json");
|
|
70
|
+
}
|
|
71
|
+
function isRecord(value) {
|
|
72
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
73
|
+
}
|
|
74
|
+
function readJsonObject(path) {
|
|
75
|
+
if (!existsSync(path))
|
|
76
|
+
return {};
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
79
|
+
return isRecord(parsed) ? parsed : {};
|
|
80
|
+
} catch {
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function readJsonObjectStrict(path) {
|
|
85
|
+
if (!existsSync(path))
|
|
86
|
+
return {};
|
|
87
|
+
let parsed;
|
|
88
|
+
try {
|
|
89
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
92
|
+
throw new Error(`${path}: ${message}`);
|
|
93
|
+
}
|
|
94
|
+
if (!isRecord(parsed))
|
|
95
|
+
throw new Error(`${path} must contain a JSON object`);
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
function writeJsonObjectAtomically(path, value) {
|
|
99
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
100
|
+
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
101
|
+
try {
|
|
102
|
+
writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}
|
|
103
|
+
`, { flag: "wx" });
|
|
104
|
+
renameSync(temporaryPath, path);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
try {
|
|
107
|
+
unlinkSync(temporaryPath);
|
|
108
|
+
} catch {}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function readGoosedumpSettingsFrom(path) {
|
|
113
|
+
const raw = readJsonObject(path).goosedump;
|
|
114
|
+
if (!isRecord(raw))
|
|
115
|
+
return {};
|
|
116
|
+
const settings = {};
|
|
117
|
+
const value = raw.turnsBeforeCompact;
|
|
118
|
+
if (isTurnsBeforeCompact(value))
|
|
119
|
+
settings.turnsBeforeCompact = value;
|
|
120
|
+
return settings;
|
|
121
|
+
}
|
|
122
|
+
function validateGoosedumpSettings(path) {
|
|
123
|
+
const root = readJsonObjectStrict(path);
|
|
124
|
+
if (root.goosedump === undefined)
|
|
125
|
+
return;
|
|
126
|
+
if (!isRecord(root.goosedump)) {
|
|
127
|
+
throw new Error(`${path}: goosedump must be a JSON object`);
|
|
128
|
+
}
|
|
129
|
+
const value = root.goosedump.turnsBeforeCompact;
|
|
130
|
+
if (value !== undefined && !isTurnsBeforeCompact(value)) {
|
|
131
|
+
throw new Error(`${path}: goosedump.turnsBeforeCompact must be an integer from ` + `${TURNS_BEFORE_COMPACT_MIN} to ${TURNS_BEFORE_COMPACT_MAX}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function resolveGoosedumpSettings(cwd) {
|
|
135
|
+
return {
|
|
136
|
+
...DEFAULT_GOOSEDUMP_SETTINGS,
|
|
137
|
+
...readGoosedumpSettingsFrom(globalSettingsPath()),
|
|
138
|
+
...readGoosedumpSettingsFrom(projectSettingsPath(cwd))
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function notifyInvalidSettings(ctx) {
|
|
142
|
+
for (const path of [globalSettingsPath(), projectSettingsPath(ctx.cwd)]) {
|
|
143
|
+
try {
|
|
144
|
+
validateGoosedumpSettings(path);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
147
|
+
if (ctx.hasUI)
|
|
148
|
+
ctx.ui.notify(`goosedump settings error: ${message}`, "error");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function readScopedGoosedumpSettings(cwd, scope) {
|
|
153
|
+
const path = scope === "global" ? globalSettingsPath() : projectSettingsPath(cwd);
|
|
154
|
+
const raw = readJsonObjectStrict(path).goosedump;
|
|
155
|
+
if (!isRecord(raw))
|
|
156
|
+
return { ...DEFAULT_GOOSEDUMP_SETTINGS };
|
|
157
|
+
const settings = {};
|
|
158
|
+
const value = raw.turnsBeforeCompact;
|
|
159
|
+
if (isTurnsBeforeCompact(value))
|
|
160
|
+
settings.turnsBeforeCompact = value;
|
|
161
|
+
return { ...DEFAULT_GOOSEDUMP_SETTINGS, ...settings };
|
|
162
|
+
}
|
|
163
|
+
function writeScopedGoosedumpSetting(cwd, scope, key, value) {
|
|
164
|
+
const path = scope === "global" ? globalSettingsPath() : projectSettingsPath(cwd);
|
|
165
|
+
const root = readJsonObjectStrict(path);
|
|
166
|
+
const goosedump = isRecord(root.goosedump) ? root.goosedump : {};
|
|
167
|
+
root.goosedump = { ...goosedump, [key]: value };
|
|
168
|
+
writeJsonObjectAtomically(path, root);
|
|
169
|
+
}
|
|
170
|
+
function clearScopedGoosedumpSetting(cwd, scope, key) {
|
|
171
|
+
const path = scope === "global" ? globalSettingsPath() : projectSettingsPath(cwd);
|
|
172
|
+
const root = readJsonObjectStrict(path);
|
|
173
|
+
if (!isRecord(root.goosedump))
|
|
174
|
+
return;
|
|
175
|
+
const goosedump = { ...root.goosedump };
|
|
176
|
+
delete goosedump[key];
|
|
177
|
+
if (Object.keys(goosedump).length === 0)
|
|
178
|
+
delete root.goosedump;
|
|
179
|
+
else
|
|
180
|
+
root.goosedump = goosedump;
|
|
181
|
+
writeJsonObjectAtomically(path, root);
|
|
182
|
+
}
|
|
183
|
+
function formatCompactSetting(value) {
|
|
184
|
+
return value === 0 ? "disabled" : String(value);
|
|
185
|
+
}
|
|
186
|
+
function resolveGoosedumpBinary() {
|
|
187
|
+
return require2.resolve("@jarkkojs/goosedump/bin/goosedump.js");
|
|
188
|
+
}
|
|
189
|
+
function resolveNativeGoosedumpBinary() {
|
|
190
|
+
const target = `${process.platform}-${process.arch}`;
|
|
191
|
+
let packageName;
|
|
192
|
+
let binaryName = "goosedump";
|
|
193
|
+
switch (target) {
|
|
194
|
+
case "linux-x64":
|
|
195
|
+
packageName = "@jarkkojs/goosedump-linux-x64";
|
|
196
|
+
break;
|
|
197
|
+
case "darwin-arm64":
|
|
198
|
+
packageName = "@jarkkojs/goosedump-darwin-arm64";
|
|
199
|
+
break;
|
|
200
|
+
case "win32-x64":
|
|
201
|
+
packageName = "@jarkkojs/goosedump-win32-x64";
|
|
202
|
+
binaryName = "goosedump.exe";
|
|
203
|
+
break;
|
|
204
|
+
default:
|
|
205
|
+
throw new Error(`unsupported platform architecture: ${target}`);
|
|
206
|
+
}
|
|
207
|
+
const wrapperRequire = createRequire(resolveGoosedumpBinary());
|
|
208
|
+
const packageDir = dirname(wrapperRequire.resolve(`${packageName}/package.json`));
|
|
209
|
+
const binaryPath = join(packageDir, "bin", binaryName);
|
|
210
|
+
if (!existsSync(binaryPath))
|
|
211
|
+
throw new Error(`binary not found: ${binaryPath}`);
|
|
212
|
+
return binaryPath;
|
|
213
|
+
}
|
|
214
|
+
function clip(value, max) {
|
|
215
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
216
|
+
}
|
|
217
|
+
function summarizeToolArgs(args) {
|
|
218
|
+
if (args && typeof args === "object" && !Array.isArray(args)) {
|
|
219
|
+
const obj = args;
|
|
220
|
+
for (const key of ["path", "file_path", "filePath", "file"]) {
|
|
221
|
+
if (typeof obj[key] === "string")
|
|
222
|
+
return `path=${obj[key]}`;
|
|
223
|
+
}
|
|
224
|
+
for (const key of ["command", "query", "pattern", "description"]) {
|
|
225
|
+
if (typeof obj[key] === "string")
|
|
226
|
+
return `${key}=${clip(obj[key], 160)}`;
|
|
227
|
+
}
|
|
228
|
+
const keys = Object.keys(obj);
|
|
229
|
+
return keys.length === 0 ? "" : keys.sort().join(", ");
|
|
230
|
+
}
|
|
231
|
+
if (typeof args === "string")
|
|
232
|
+
return clip(args, 160);
|
|
233
|
+
if (args == null)
|
|
234
|
+
return "";
|
|
235
|
+
return clip(JSON.stringify(args), 160);
|
|
236
|
+
}
|
|
237
|
+
function flattenPiPart(part) {
|
|
238
|
+
switch (part.type) {
|
|
239
|
+
case "text":
|
|
240
|
+
return part.text ?? "";
|
|
241
|
+
case "thinking":
|
|
242
|
+
return part.thinking ?? "";
|
|
243
|
+
case "toolCall": {
|
|
244
|
+
const summary = summarizeToolArgs(part.arguments);
|
|
245
|
+
return summary ? `${part.name ?? "tool"}(${summary})` : part.name ?? "tool";
|
|
246
|
+
}
|
|
247
|
+
case "image":
|
|
248
|
+
return part.mimeType ? `[image ${part.mimeType}]` : "[image]";
|
|
249
|
+
default:
|
|
250
|
+
return clip(JSON.stringify(part), 160);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function flattenPiContent(content) {
|
|
254
|
+
if (typeof content === "string")
|
|
255
|
+
return content;
|
|
256
|
+
if (!Array.isArray(content))
|
|
257
|
+
return "";
|
|
258
|
+
return content.map(flattenPiPart).filter(Boolean).join(`
|
|
259
|
+
`);
|
|
260
|
+
}
|
|
261
|
+
function parsePiMessages(output) {
|
|
262
|
+
const messages = [];
|
|
263
|
+
for (const line of output.split(`
|
|
264
|
+
`)) {
|
|
265
|
+
if (!line)
|
|
266
|
+
continue;
|
|
267
|
+
let entry;
|
|
268
|
+
try {
|
|
269
|
+
entry = JSON.parse(line);
|
|
270
|
+
} catch {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (entry.type === "message" && entry.message) {
|
|
274
|
+
const content = flattenPiContent(entry.message.content);
|
|
275
|
+
messages.push({ id: entry.id ?? "", role: entry.message.role ?? "unknown", content });
|
|
276
|
+
} else if (entry.type === "custom_message") {
|
|
277
|
+
messages.push({
|
|
278
|
+
id: entry.id ?? "",
|
|
279
|
+
role: "user",
|
|
280
|
+
content: flattenPiContent(entry.content)
|
|
281
|
+
});
|
|
282
|
+
} else if (entry.type === "branch_summary") {
|
|
283
|
+
messages.push({
|
|
284
|
+
id: entry.id ?? "",
|
|
285
|
+
role: "user",
|
|
286
|
+
content: `${BRANCH_SUMMARY_PREFIX}${entry.summary ?? ""}${BRANCH_SUMMARY_SUFFIX}`
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return messages;
|
|
291
|
+
}
|
|
292
|
+
function parseMemoryHits(output) {
|
|
293
|
+
const value = JSON.parse(output);
|
|
294
|
+
if (!Array.isArray(value))
|
|
295
|
+
throw new Error("goosedump memory search returned invalid JSON");
|
|
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));
|
|
297
|
+
}
|
|
298
|
+
function formatMemoryHit(hit, maxBytes) {
|
|
299
|
+
const provenance = `[${hit.hash}] ${hit.memory_type} (${hit.kind}; ${hit.scope}/${hit.derivation}; ` + `trust ${hit.trust.toFixed(2)}, confidence ${hit.confidence.toFixed(2)}; ` + `${hit.provider}:${hit.context_id}/${hit.entry_id})
|
|
300
|
+
` + `Working directory: ${hit.path}
|
|
301
|
+
Source: ${hit.source_path}
|
|
302
|
+
`;
|
|
303
|
+
const full = `${provenance}${hit.text}`;
|
|
304
|
+
if (Buffer.byteLength(full) <= maxBytes)
|
|
305
|
+
return full;
|
|
306
|
+
const provenanceBytes = Buffer.byteLength(provenance);
|
|
307
|
+
const markerBytes = Buffer.byteLength(MEMORY_TEXT_TRUNCATED);
|
|
308
|
+
if (provenanceBytes + markerBytes >= maxBytes)
|
|
309
|
+
return utf8Prefix(provenance, maxBytes);
|
|
310
|
+
const textBytes = maxBytes - provenanceBytes - markerBytes;
|
|
311
|
+
return `${provenance}${utf8Prefix(hit.text, textBytes)}${MEMORY_TEXT_TRUNCATED}`;
|
|
312
|
+
}
|
|
313
|
+
function formatMemoryHits(hits, maxBytes = RECALL_MAX_BYTES) {
|
|
314
|
+
const formatted = [];
|
|
315
|
+
let remainingBytes = maxBytes;
|
|
316
|
+
const separatorBytes = Buffer.byteLength(MEMORY_HIT_SEPARATOR);
|
|
317
|
+
for (let index = 0;index < hits.length; index += 1) {
|
|
318
|
+
if (index > 0) {
|
|
319
|
+
if (remainingBytes < separatorBytes)
|
|
320
|
+
break;
|
|
321
|
+
formatted.push(MEMORY_HIT_SEPARATOR);
|
|
322
|
+
remainingBytes -= separatorBytes;
|
|
323
|
+
}
|
|
324
|
+
const remainingHits = hits.length - index;
|
|
325
|
+
const futureSeparators = separatorBytes * (remainingHits - 1);
|
|
326
|
+
const hitBytes = Math.floor((remainingBytes - futureSeparators) / remainingHits);
|
|
327
|
+
if (hitBytes <= 0)
|
|
328
|
+
break;
|
|
329
|
+
const hit = formatMemoryHit(hits[index], hitBytes);
|
|
330
|
+
formatted.push(hit);
|
|
331
|
+
remainingBytes -= Buffer.byteLength(hit);
|
|
332
|
+
}
|
|
333
|
+
return formatted.join("");
|
|
334
|
+
}
|
|
335
|
+
var ANSI_SGR = new RegExp(String.fromCharCode(27) + "\\[[0-9;]*m", "g");
|
|
336
|
+
function compactLine(content) {
|
|
337
|
+
const plain = content.replace(ANSI_SGR, "");
|
|
338
|
+
const collapsed = plain.replace(/\s+/g, " ").trim();
|
|
339
|
+
return collapsed.length > 80 ? `${collapsed.slice(0, 80)}...` : collapsed;
|
|
340
|
+
}
|
|
341
|
+
function formatMessagesCompact(messages) {
|
|
342
|
+
return messages.map((m) => `[${m.id}] ${m.role}: ${compactLine(m.content)}`).join(`
|
|
343
|
+
`);
|
|
344
|
+
}
|
|
345
|
+
function formatMessagesFull(messages) {
|
|
346
|
+
return messages.map((m) => `[${m.id}] ${m.role}:
|
|
347
|
+
${m.content}`).join(`
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
`);
|
|
352
|
+
}
|
|
353
|
+
function textResult(text) {
|
|
354
|
+
return { content: [{ type: "text", text }], details: undefined };
|
|
355
|
+
}
|
|
356
|
+
function missingSessionId(tool, provider = "pi") {
|
|
357
|
+
if (provider !== "pi") {
|
|
358
|
+
return textResult(`session is required for ${tool} when provider is "${provider}"`);
|
|
359
|
+
}
|
|
360
|
+
return textResult(`session is required for ${tool} when no current session is available`);
|
|
361
|
+
}
|
|
362
|
+
var GOOSEDUMP_UNAVAILABLE_MESSAGE = "goosedump is not available. Install with: npm install @jarkkojs/goosedump";
|
|
363
|
+
function goosedumpUnavailableResult() {
|
|
364
|
+
return textResult(GOOSEDUMP_UNAVAILABLE_MESSAGE);
|
|
365
|
+
}
|
|
366
|
+
function compactStartedMessage(keep) {
|
|
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
|
+
}
|
|
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
|
+
function runGoosedumpAsync(args, signal, executablePath = resolveNativeGoosedumpBinary()) {
|
|
382
|
+
return new Promise((resolve, reject) => {
|
|
383
|
+
execFile(executablePath, args, {
|
|
384
|
+
encoding: "utf-8",
|
|
385
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
386
|
+
signal
|
|
387
|
+
}, (err, stdout, stderr) => {
|
|
388
|
+
if (err) {
|
|
389
|
+
const message = stderr.trim() || err.message;
|
|
390
|
+
reject(new Error(message));
|
|
391
|
+
} else {
|
|
392
|
+
resolve({ stdout, stderr });
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
function readFirstLine(path) {
|
|
398
|
+
let fd;
|
|
399
|
+
try {
|
|
400
|
+
fd = openSync(path, "r");
|
|
401
|
+
const buf = Buffer.alloc(8192);
|
|
402
|
+
const bytesRead = readSync(fd, buf, 0, buf.length, 0);
|
|
403
|
+
if (bytesRead < 0)
|
|
404
|
+
return null;
|
|
405
|
+
const slice = buf.subarray(0, bytesRead);
|
|
406
|
+
const newline = slice.indexOf(10);
|
|
407
|
+
return (newline < 0 ? slice : slice.subarray(0, newline)).toString("utf8");
|
|
408
|
+
} catch {
|
|
409
|
+
return null;
|
|
410
|
+
} finally {
|
|
411
|
+
if (fd !== undefined)
|
|
412
|
+
closeSync(fd);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function formatTarget(provider, id) {
|
|
416
|
+
return `${provider}:${id}`;
|
|
417
|
+
}
|
|
418
|
+
function appendScope(args, scope) {
|
|
419
|
+
if (scope === "all")
|
|
420
|
+
args.push("--all");
|
|
421
|
+
}
|
|
422
|
+
function goosedumpSearch(target, query, options = {}) {
|
|
423
|
+
const args = ["session", "search", target, query, "--as", "pi"];
|
|
424
|
+
appendScope(args, options.scope);
|
|
425
|
+
if (options.page)
|
|
426
|
+
args.push("--page", String(options.page));
|
|
427
|
+
return parsePiMessages(runGoosedump(args));
|
|
428
|
+
}
|
|
429
|
+
function goosedumpGrep(target, pattern, options = {}) {
|
|
430
|
+
const args = ["session", "find", target, pattern, "--as", "pi"];
|
|
431
|
+
appendScope(args, options.scope);
|
|
432
|
+
return parsePiMessages(runGoosedump(args));
|
|
433
|
+
}
|
|
434
|
+
function goosedumpShow(target, options = {}) {
|
|
435
|
+
const args = ["session", "show", target, "--as", "pi"];
|
|
436
|
+
appendScope(args, options.scope);
|
|
437
|
+
for (const id of options.ids ?? [])
|
|
438
|
+
args.push("--entry", id);
|
|
439
|
+
return parsePiMessages(runGoosedump(args));
|
|
440
|
+
}
|
|
441
|
+
async function goosedumpRecall(query, options = {}, signal, executablePath) {
|
|
442
|
+
const args = ["memory", "search", query, "--limit", String(options.limit ?? 5)];
|
|
443
|
+
if (options.provider)
|
|
444
|
+
args.push("--provider", options.provider);
|
|
445
|
+
if (options.path)
|
|
446
|
+
args.push("--path", options.path);
|
|
447
|
+
if (options.type)
|
|
448
|
+
args.push("--type", options.type);
|
|
449
|
+
const output = await runGoosedumpAsync(args, signal, executablePath);
|
|
450
|
+
return { hits: parseMemoryHits(output.stdout), warning: output.stderr.trim() };
|
|
451
|
+
}
|
|
452
|
+
function resolveTarget(ctx, provider, contextId) {
|
|
453
|
+
if (contextId)
|
|
454
|
+
return formatTarget(provider, contextId);
|
|
455
|
+
if (provider === "pi")
|
|
456
|
+
return currentSessionTarget(ctx);
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
var GOOSE_PROVIDERS = ["pi", "claude", "codex", "crush", "gemini", "goose", "opencode"];
|
|
460
|
+
var HISTORY_TARGET_GUIDELINE = "Use the current Pi session by default; non-Pi providers require an explicit session ID.";
|
|
461
|
+
function historyProviderParam(operation) {
|
|
462
|
+
return Type.Optional(Type.Union(GOOSE_PROVIDERS.map((provider) => Type.Literal(provider)), {
|
|
463
|
+
default: "pi",
|
|
464
|
+
description: `Source provider whose sessions to ${operation}. Defaults to pi. Non-pi providers have no current session, so they require an explicit session id.`
|
|
465
|
+
}));
|
|
466
|
+
}
|
|
467
|
+
function historySessionParam() {
|
|
468
|
+
return Type.Optional(Type.String({
|
|
469
|
+
description: "Context/session ID. Defaults to the current Pi session for pi; required when provider is not pi."
|
|
470
|
+
}));
|
|
471
|
+
}
|
|
472
|
+
function historyScopeParam() {
|
|
473
|
+
return Type.Optional(Type.Union([Type.Literal("lineage"), Type.Literal("all")], {
|
|
474
|
+
default: "lineage",
|
|
475
|
+
description: "Scope: lineage (current branch) or all entries in the session"
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
async function goosedumpCompact(target, options, signal, executablePath) {
|
|
479
|
+
const args = ["session", "compact", target, "--remember"];
|
|
480
|
+
appendScope(args, options.scope);
|
|
481
|
+
if (options.from)
|
|
482
|
+
args.push("--from", options.from);
|
|
483
|
+
if (options.previousSummary)
|
|
484
|
+
args.push("--previous-summary", options.previousSummary);
|
|
485
|
+
args.push("--before", options.until);
|
|
486
|
+
const output = await runGoosedumpAsync(args, signal, executablePath);
|
|
487
|
+
for (const line of output.stdout.split(`
|
|
488
|
+
`)) {
|
|
489
|
+
if (!line)
|
|
490
|
+
continue;
|
|
491
|
+
try {
|
|
492
|
+
const entry = JSON.parse(line);
|
|
493
|
+
if (entry.type === "compaction" && typeof entry.summary === "string") {
|
|
494
|
+
return { summary: entry.summary, warning: output.stderr.trim() };
|
|
495
|
+
}
|
|
496
|
+
} catch {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return { summary: output.stdout.trim(), warning: output.stderr.trim() };
|
|
501
|
+
}
|
|
502
|
+
function pi_goosedump_default(pi) {
|
|
503
|
+
createGoosedumpIntegration().register(pi);
|
|
504
|
+
}
|
|
505
|
+
function currentSessionTarget(ctx) {
|
|
506
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
507
|
+
if (!sessionFile)
|
|
508
|
+
return null;
|
|
509
|
+
const line = readFirstLine(sessionFile);
|
|
510
|
+
if (!line)
|
|
511
|
+
return null;
|
|
512
|
+
try {
|
|
513
|
+
const entry = JSON.parse(line);
|
|
514
|
+
if (entry.type === "session" && typeof entry.id === "string") {
|
|
515
|
+
return formatTarget("pi", entry.id);
|
|
516
|
+
}
|
|
517
|
+
} catch {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
function isCompactionEntry(entry) {
|
|
523
|
+
return entry.type === "compaction";
|
|
524
|
+
}
|
|
525
|
+
function isMessageEntry(entry) {
|
|
526
|
+
return entry.type === "message";
|
|
527
|
+
}
|
|
528
|
+
function isCompletedAssistant(message) {
|
|
529
|
+
return message.role === "assistant" && message.stopReason !== "error" && message.stopReason !== "aborted";
|
|
530
|
+
}
|
|
531
|
+
var SUMMARIZABLE_ENTRY_TYPES = new Set(["message", "custom_message", "branch_summary"]);
|
|
532
|
+
function piKeepRecentTokens(cwd) {
|
|
533
|
+
for (const path of [projectSettingsPath(cwd), globalSettingsPath()]) {
|
|
534
|
+
const compaction = readJsonObject(path).compaction;
|
|
535
|
+
if (isRecord(compaction) && typeof compaction.keepRecentTokens === "number") {
|
|
536
|
+
return compaction.keepRecentTokens;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return DEFAULT_COMPACTION_SETTINGS.keepRecentTokens;
|
|
540
|
+
}
|
|
541
|
+
function piCompactionEnabled(cwd) {
|
|
542
|
+
for (const path of [projectSettingsPath(cwd), globalSettingsPath()]) {
|
|
543
|
+
const compaction = readJsonObject(path).compaction;
|
|
544
|
+
if (isRecord(compaction) && typeof compaction.enabled === "boolean") {
|
|
545
|
+
return compaction.enabled;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return DEFAULT_COMPACTION_SETTINGS.enabled;
|
|
549
|
+
}
|
|
550
|
+
function piReserveTokens(cwd) {
|
|
551
|
+
for (const path of [projectSettingsPath(cwd), globalSettingsPath()]) {
|
|
552
|
+
const compaction = readJsonObject(path).compaction;
|
|
553
|
+
if (isRecord(compaction) && typeof compaction.reserveTokens === "number") {
|
|
554
|
+
return compaction.reserveTokens;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return DEFAULT_COMPACTION_SETTINGS.reserveTokens;
|
|
558
|
+
}
|
|
559
|
+
function piAutoCompactionPending(ctx) {
|
|
560
|
+
const usage = ctx.getContextUsage();
|
|
561
|
+
if (!usage || usage.tokens === null)
|
|
562
|
+
return false;
|
|
563
|
+
return piCompactionEnabled(ctx.cwd) && usage.tokens > usage.contextWindow - piReserveTokens(ctx.cwd);
|
|
564
|
+
}
|
|
565
|
+
function hasCompactableHistory(ctx) {
|
|
566
|
+
const branch = ctx.sessionManager.getBranch();
|
|
567
|
+
if (branch.length === 0)
|
|
568
|
+
return false;
|
|
569
|
+
if (branch[branch.length - 1].type === "compaction")
|
|
570
|
+
return false;
|
|
571
|
+
const boundaryStart = activeHistoryStartIndex(branch);
|
|
572
|
+
const cut = findCutPoint(branch, boundaryStart, branch.length, piKeepRecentTokens(ctx.cwd));
|
|
573
|
+
if (!branch[cut.firstKeptEntryIndex]?.id)
|
|
574
|
+
return false;
|
|
575
|
+
const historyEnd = cut.isSplitTurn ? cut.turnStartIndex : cut.firstKeptEntryIndex;
|
|
576
|
+
for (let i = boundaryStart;i < historyEnd; i += 1) {
|
|
577
|
+
if (SUMMARIZABLE_ENTRY_TYPES.has(branch[i].type))
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
if (cut.isSplitTurn) {
|
|
581
|
+
for (let i = cut.turnStartIndex;i < cut.firstKeptEntryIndex; i += 1) {
|
|
582
|
+
if (SUMMARIZABLE_ENTRY_TYPES.has(branch[i].type))
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
function latestCompaction(branchEntries) {
|
|
589
|
+
for (let i = branchEntries.length - 1;i >= 0; i -= 1) {
|
|
590
|
+
const entry = branchEntries[i];
|
|
591
|
+
if (isCompactionEntry(entry))
|
|
592
|
+
return entry;
|
|
593
|
+
}
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
function compactionBoundaryStartIndex(branchEntries, compaction) {
|
|
597
|
+
const keptIndex = branchEntries.findIndex((entry) => entry.id === compaction.firstKeptEntryId);
|
|
598
|
+
if (keptIndex >= 0)
|
|
599
|
+
return keptIndex;
|
|
600
|
+
const compactionIndex = branchEntries.findIndex((entry) => entry.id === compaction.id);
|
|
601
|
+
return compactionIndex >= 0 ? compactionIndex + 1 : 0;
|
|
602
|
+
}
|
|
603
|
+
function activeHistoryStartIndex(branchEntries) {
|
|
604
|
+
const compaction = latestCompaction(branchEntries);
|
|
605
|
+
return compaction ? compactionBoundaryStartIndex(branchEntries, compaction) : 0;
|
|
606
|
+
}
|
|
607
|
+
function completedTurnsSinceCompaction(branchEntries) {
|
|
608
|
+
let start = 0;
|
|
609
|
+
for (let i = branchEntries.length - 1;i >= 0; i -= 1) {
|
|
610
|
+
if (isCompactionEntry(branchEntries[i])) {
|
|
611
|
+
start = i + 1;
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
let completed = 0;
|
|
616
|
+
for (let i = start;i < branchEntries.length; i += 1) {
|
|
617
|
+
const entry = branchEntries[i];
|
|
618
|
+
if (isMessageEntry(entry) && isCompletedAssistant(entry.message))
|
|
619
|
+
completed += 1;
|
|
620
|
+
}
|
|
621
|
+
return completed;
|
|
622
|
+
}
|
|
623
|
+
function keepBoundaryId(branchEntries, keep) {
|
|
624
|
+
const historyStart = activeHistoryStartIndex(branchEntries);
|
|
625
|
+
let seen = 0;
|
|
626
|
+
for (let i = branchEntries.length - 1;i >= historyStart; i -= 1) {
|
|
627
|
+
const entry = branchEntries[i];
|
|
628
|
+
if (isMessageEntry(entry) && entry.message.role === "user") {
|
|
629
|
+
seen += 1;
|
|
630
|
+
if (seen === keep)
|
|
631
|
+
return i === historyStart ? undefined : entry.id;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
function latestCompactionForRange(branchEntries, untilIndex) {
|
|
637
|
+
for (let i = branchEntries.length - 1;i >= 0; i -= 1) {
|
|
638
|
+
const entry = branchEntries[i];
|
|
639
|
+
if (!isCompactionEntry(entry))
|
|
640
|
+
continue;
|
|
641
|
+
if (compactionBoundaryStartIndex(branchEntries, entry) < untilIndex)
|
|
642
|
+
return entry;
|
|
643
|
+
}
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
function compactionStartId(branchEntries, previousCompaction, untilIndex) {
|
|
647
|
+
const boundaryStart = compactionBoundaryStartIndex(branchEntries, previousCompaction);
|
|
648
|
+
return boundaryStart < untilIndex ? branchEntries[boundaryStart]?.id : undefined;
|
|
649
|
+
}
|
|
650
|
+
function buildCompactRange(branchEntries, firstKeptEntryId) {
|
|
651
|
+
const untilIndex = branchEntries.findIndex((entry) => entry.id === firstKeptEntryId);
|
|
652
|
+
if (untilIndex < 0)
|
|
653
|
+
return;
|
|
654
|
+
const previousCompaction = latestCompactionForRange(branchEntries, untilIndex);
|
|
655
|
+
const from = previousCompaction ? compactionStartId(branchEntries, previousCompaction, untilIndex) : undefined;
|
|
656
|
+
return from && from !== firstKeptEntryId ? { scope: "lineage", from, until: firstKeptEntryId } : { scope: "lineage", until: firstKeptEntryId };
|
|
657
|
+
}
|
|
658
|
+
function utf8Prefix(text, maxBytes) {
|
|
659
|
+
const prefix = [];
|
|
660
|
+
let bytes = 0;
|
|
661
|
+
for (const char of text) {
|
|
662
|
+
const charBytes = Buffer.byteLength(char);
|
|
663
|
+
if (bytes + charBytes > maxBytes)
|
|
664
|
+
break;
|
|
665
|
+
prefix.push(char);
|
|
666
|
+
bytes += charBytes;
|
|
667
|
+
}
|
|
668
|
+
return prefix.join("");
|
|
669
|
+
}
|
|
670
|
+
async function buildGoosedumpCompaction(target, preparation, branchEntries, keep, signal, executablePath) {
|
|
671
|
+
let firstKeptEntryId = preparation.firstKeptEntryId;
|
|
672
|
+
if (keep !== undefined && keep > 0) {
|
|
673
|
+
const id = keepBoundaryId(branchEntries, keep);
|
|
674
|
+
if (!id) {
|
|
675
|
+
return { cancel: true, reason: `fewer than ${keep} user turns; nothing to compact` };
|
|
676
|
+
}
|
|
677
|
+
firstKeptEntryId = id;
|
|
678
|
+
}
|
|
679
|
+
const range = buildCompactRange(branchEntries, firstKeptEntryId);
|
|
680
|
+
if (!range)
|
|
681
|
+
return;
|
|
682
|
+
const generated = await goosedumpCompact(target, { ...range, previousSummary: preparation.previousSummary }, signal, executablePath);
|
|
683
|
+
if (!generated.summary)
|
|
684
|
+
return;
|
|
685
|
+
const summary = generated.summary;
|
|
686
|
+
return {
|
|
687
|
+
summary,
|
|
688
|
+
firstKeptEntryId,
|
|
689
|
+
tokensBefore: preparation.tokensBefore,
|
|
690
|
+
details: {
|
|
691
|
+
source: "goosedump",
|
|
692
|
+
target,
|
|
693
|
+
scope: range.scope,
|
|
694
|
+
from: range.from,
|
|
695
|
+
until: range.until,
|
|
696
|
+
fileOps: preparation.fileOps,
|
|
697
|
+
warning: generated.warning || undefined
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
function isControlChar(char) {
|
|
702
|
+
const code = char.charCodeAt(0);
|
|
703
|
+
return code < 32 || code === 127 || code >= 128 && code <= 159;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
class NumberInputSubmenu {
|
|
707
|
+
input = new Input;
|
|
708
|
+
hint;
|
|
709
|
+
onSubmitValue;
|
|
710
|
+
onCancelEdit;
|
|
711
|
+
onClearValue;
|
|
712
|
+
onInvalidValue;
|
|
713
|
+
title;
|
|
714
|
+
currentValue;
|
|
715
|
+
min;
|
|
716
|
+
max;
|
|
717
|
+
constructor(title, currentValue, onSubmit, onCancel, options) {
|
|
718
|
+
this.title = title;
|
|
719
|
+
this.currentValue = currentValue;
|
|
720
|
+
this.onSubmitValue = onSubmit;
|
|
721
|
+
this.onCancelEdit = onCancel;
|
|
722
|
+
this.onClearValue = options?.onClear;
|
|
723
|
+
this.onInvalidValue = options?.onInvalid ?? (() => {});
|
|
724
|
+
this.min = options?.min ?? 0;
|
|
725
|
+
this.max = options?.max ?? 255;
|
|
726
|
+
this.hint = getSettingsListTheme().hint;
|
|
727
|
+
}
|
|
728
|
+
handleInput(data) {
|
|
729
|
+
const kb = getKeybindings();
|
|
730
|
+
if (kb.matches(data, "tui.select.confirm") || data === `
|
|
731
|
+
`) {
|
|
732
|
+
const value = this.input.getValue();
|
|
733
|
+
if (value === "") {
|
|
734
|
+
if (this.onClearValue)
|
|
735
|
+
this.onClearValue();
|
|
736
|
+
else
|
|
737
|
+
this.onCancelEdit();
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (!/^\d+$/.test(value)) {
|
|
741
|
+
this.onInvalidValue(`Enter a whole number between ${this.min} and ${this.max}`);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const parsed = Number(value);
|
|
745
|
+
if (!Number.isSafeInteger(parsed) || parsed < this.min || parsed > this.max) {
|
|
746
|
+
this.onInvalidValue(`Enter a whole number between ${this.min} and ${this.max}`);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
this.onSubmitValue(parsed);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (kb.matches(data, "tui.select.cancel")) {
|
|
753
|
+
this.onCancelEdit();
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const kittyChar = decodeKittyPrintable(data);
|
|
757
|
+
const printable = kittyChar ?? (data.length === 1 && !isControlChar(data) ? data : undefined);
|
|
758
|
+
if (printable !== undefined && !/^[0-9]$/.test(printable))
|
|
759
|
+
return;
|
|
760
|
+
this.input.handleInput(data);
|
|
761
|
+
}
|
|
762
|
+
invalidate() {
|
|
763
|
+
this.input.invalidate();
|
|
764
|
+
}
|
|
765
|
+
render(width) {
|
|
766
|
+
return [
|
|
767
|
+
this.hint(` ${this.title} · current ${this.currentValue ?? "inherit"} · ${this.min}-${this.max}`),
|
|
768
|
+
"",
|
|
769
|
+
...this.input.render(width),
|
|
770
|
+
"",
|
|
771
|
+
this.hint(this.onClearValue ? " empty inherit · enter submit · esc cancel" : " enter submit · esc cancel")
|
|
772
|
+
];
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function makeRow(content, innerW, border) {
|
|
776
|
+
const line = truncateToWidth(content, innerW);
|
|
777
|
+
const pad = Math.max(0, innerW - visibleWidth(line));
|
|
778
|
+
return `${border} ${line}${" ".repeat(pad)} ${border}`;
|
|
779
|
+
}
|
|
780
|
+
async function runSettingsEditor(ctx) {
|
|
781
|
+
const OVERLAY_WIDTH = 84;
|
|
782
|
+
const location = ctx.cwd;
|
|
783
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
784
|
+
let settingsList;
|
|
785
|
+
function settingLabel(scope, key) {
|
|
786
|
+
const scopeLabel = scope === "global" ? "Global" : "Project";
|
|
787
|
+
return `${scopeLabel} ${key}`;
|
|
788
|
+
}
|
|
789
|
+
function buildSettingItems() {
|
|
790
|
+
const global = readScopedGoosedumpSettings(location, "global");
|
|
791
|
+
const projectPath = projectSettingsPath(location);
|
|
792
|
+
const projectRaw = readJsonObjectStrict(projectPath).goosedump;
|
|
793
|
+
const projectValue = isRecord(projectRaw) && isTurnsBeforeCompact(projectRaw.turnsBeforeCompact) ? projectRaw.turnsBeforeCompact : undefined;
|
|
794
|
+
return ["global", "project"].map((scope) => ({
|
|
795
|
+
id: `${scope}:turnsBeforeCompact`,
|
|
796
|
+
label: settingLabel(scope, "turnsBeforeCompact"),
|
|
797
|
+
description: scope === "project" ? "LLM turns before compaction; empty inherits Global." : "LLM turns before compaction; 0 disables.",
|
|
798
|
+
currentValue: String(scope === "project" ? projectValue ?? "" : global.turnsBeforeCompact),
|
|
799
|
+
submenu: (currentValue, subDone) => new NumberInputSubmenu(settingLabel(scope, "turnsBeforeCompact"), currentValue === "" ? null : Number(currentValue), (value) => subDone(String(value)), () => subDone(), {
|
|
800
|
+
min: TURNS_BEFORE_COMPACT_MIN,
|
|
801
|
+
max: TURNS_BEFORE_COMPACT_MAX,
|
|
802
|
+
onClear: scope === "project" ? () => subDone("") : undefined,
|
|
803
|
+
onInvalid: (message) => ctx.ui.notify(message, "error")
|
|
804
|
+
})
|
|
805
|
+
}));
|
|
806
|
+
}
|
|
807
|
+
function rebuildSettingsList() {
|
|
808
|
+
settingsList = new SettingsList(buildSettingItems(), 10, getSettingsListTheme(), (id, newValue) => {
|
|
809
|
+
const [scope, key] = id.split(":");
|
|
810
|
+
if (scope === "project" && newValue === "") {
|
|
811
|
+
try {
|
|
812
|
+
clearScopedGoosedumpSetting(location, scope, key);
|
|
813
|
+
settingsList.updateValue(id, "");
|
|
814
|
+
ctx.ui.notify(`${settingLabel(scope, key)} inherits Global`, "info");
|
|
815
|
+
tui.requestRender();
|
|
816
|
+
} catch (error) {
|
|
817
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
818
|
+
ctx.ui.notify(`goosedump settings error: ${message}`, "error");
|
|
819
|
+
}
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const parsed = Number(newValue);
|
|
823
|
+
if (!isTurnsBeforeCompact(parsed))
|
|
824
|
+
return;
|
|
825
|
+
try {
|
|
826
|
+
writeScopedGoosedumpSetting(location, scope, key, parsed);
|
|
827
|
+
settingsList.updateValue(id, newValue);
|
|
828
|
+
ctx.ui.notify(`${settingLabel(scope, key)} = ${formatCompactSetting(parsed)}`, "info");
|
|
829
|
+
tui.requestRender();
|
|
830
|
+
} catch (error) {
|
|
831
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
832
|
+
ctx.ui.notify(`goosedump settings error: ${message}`, "error");
|
|
833
|
+
}
|
|
834
|
+
tui.requestRender();
|
|
835
|
+
}, () => {
|
|
836
|
+
done(undefined);
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
rebuildSettingsList();
|
|
840
|
+
return {
|
|
841
|
+
render(width) {
|
|
842
|
+
const innerW = width - 4;
|
|
843
|
+
const border = theme.fg("border", "│");
|
|
844
|
+
const borderFg = (s) => theme.fg("border", s);
|
|
845
|
+
const accent = (s) => theme.fg("accent", s);
|
|
846
|
+
const dim = (s) => theme.fg("dim", s);
|
|
847
|
+
const lines = [];
|
|
848
|
+
const title = accent(" Goosedump Settings ");
|
|
849
|
+
const topFill = borderFg("─".repeat(Math.max(0, width - 4 - visibleWidth(title))));
|
|
850
|
+
lines.push(`${borderFg("╭─")}${title}${topFill}${borderFg("─╮")}`);
|
|
851
|
+
for (const line of settingsList.render(innerW))
|
|
852
|
+
lines.push(makeRow(line, innerW, border));
|
|
853
|
+
lines.push(makeRow("", innerW, border));
|
|
854
|
+
const effective = resolveGoosedumpSettings(location);
|
|
855
|
+
lines.push(makeRow(dim(`Effective: turns=${formatCompactSetting(effective.turnsBeforeCompact)}`), innerW, border));
|
|
856
|
+
lines.push(makeRow(dim("↑↓ select enter/space edit esc close"), innerW, border));
|
|
857
|
+
lines.push(`${borderFg("╰")}${borderFg("─".repeat(width - 2))}${borderFg("╯")}`);
|
|
858
|
+
return lines;
|
|
859
|
+
},
|
|
860
|
+
handleInput(data) {
|
|
861
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
862
|
+
done(undefined);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
settingsList.handleInput(data);
|
|
866
|
+
tui.requestRender();
|
|
867
|
+
},
|
|
868
|
+
invalidate() {}
|
|
869
|
+
};
|
|
870
|
+
}, {
|
|
871
|
+
overlay: true,
|
|
872
|
+
overlayOptions: {
|
|
873
|
+
anchor: "center",
|
|
874
|
+
width: OVERLAY_WIDTH,
|
|
875
|
+
margin: 2
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
async function runSearchPanel(ctx, query) {
|
|
880
|
+
const OVERLAY_WIDTH = 84;
|
|
881
|
+
const VIEWPORT = 18;
|
|
882
|
+
const PAGE_SIZE = 5;
|
|
883
|
+
const wrapWidth = OVERLAY_WIDTH - 4;
|
|
884
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
885
|
+
const target = currentSessionTarget(ctx);
|
|
886
|
+
const borderFg = (s) => theme.fg("border", s);
|
|
887
|
+
const border = theme.fg("border", "│");
|
|
888
|
+
const accent = (s) => theme.fg("accent", s);
|
|
889
|
+
const text = (s) => theme.fg("text", s);
|
|
890
|
+
const dim = (s) => theme.fg("dim", s);
|
|
891
|
+
const muted = (s) => theme.fg("muted", s);
|
|
892
|
+
let entries = [];
|
|
893
|
+
let error = null;
|
|
894
|
+
let mode = "results";
|
|
895
|
+
let selected = 0;
|
|
896
|
+
let scroll = 0;
|
|
897
|
+
let detailLines = [];
|
|
898
|
+
let detailScroll = 0;
|
|
899
|
+
function runSearch() {
|
|
900
|
+
try {
|
|
901
|
+
const first = goosedumpSearch(target, query, { scope: "lineage", page: 1 });
|
|
902
|
+
if (first.length === 0) {
|
|
903
|
+
entries = [];
|
|
904
|
+
error = `No results for "${query}"`;
|
|
905
|
+
selected = 0;
|
|
906
|
+
scroll = 0;
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const all = [...first];
|
|
910
|
+
for (let page = 2;page <= 10; page += 1) {
|
|
911
|
+
const result = goosedumpSearch(target, query, { scope: "lineage", page });
|
|
912
|
+
if (result.length === 0)
|
|
913
|
+
break;
|
|
914
|
+
all.push(...result);
|
|
915
|
+
if (result.length < PAGE_SIZE)
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
entries = all;
|
|
919
|
+
error = null;
|
|
920
|
+
selected = 0;
|
|
921
|
+
scroll = 0;
|
|
922
|
+
} catch (err) {
|
|
923
|
+
entries = [];
|
|
924
|
+
error = err instanceof Error ? err.message : String(err);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (!target) {
|
|
928
|
+
error = "No current Pi session available.";
|
|
929
|
+
} else {
|
|
930
|
+
runSearch();
|
|
931
|
+
}
|
|
932
|
+
function loadDetail(id) {
|
|
933
|
+
try {
|
|
934
|
+
const messages = goosedumpShow(target, { ids: [id] });
|
|
935
|
+
const content = messages.length > 0 ? messages[0].content : "No content for this entry.";
|
|
936
|
+
detailLines = wrapTextWithAnsi(content, wrapWidth);
|
|
937
|
+
detailScroll = 0;
|
|
938
|
+
mode = "detail";
|
|
939
|
+
} catch (err) {
|
|
940
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
941
|
+
ctx.ui.notify(`goosedump error: ${message}`, "error");
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
render(width) {
|
|
946
|
+
const innerW = width - 4;
|
|
947
|
+
const lines = [];
|
|
948
|
+
const title = accent(` Goosedump search: ${truncateToWidth(query, 44)} `);
|
|
949
|
+
const topFill = borderFg("─".repeat(Math.max(0, width - 4 - visibleWidth(title))));
|
|
950
|
+
lines.push(`${borderFg("╭─")}${title}${topFill}${borderFg("─╮")}`);
|
|
951
|
+
if (error) {
|
|
952
|
+
lines.push(makeRow(muted(error), innerW, border));
|
|
953
|
+
lines.push(makeRow(dim("esc close"), innerW, border));
|
|
954
|
+
lines.push(`${borderFg("╰")}${borderFg("─".repeat(width - 2))}${borderFg("╯")}`);
|
|
955
|
+
return lines;
|
|
956
|
+
}
|
|
957
|
+
if (mode === "detail") {
|
|
958
|
+
const total = detailLines.length;
|
|
959
|
+
const window2 = detailLines.slice(detailScroll, detailScroll + VIEWPORT);
|
|
960
|
+
for (const row of window2)
|
|
961
|
+
lines.push(makeRow(text(row), innerW, border));
|
|
962
|
+
for (let i = window2.length;i < VIEWPORT; i++)
|
|
963
|
+
lines.push(makeRow("", innerW, border));
|
|
964
|
+
const position2 = total > VIEWPORT ? `${detailScroll + 1}-${Math.min(detailScroll + VIEWPORT, total)} of ${total}` : `${total} line${total === 1 ? "" : "s"}`;
|
|
965
|
+
lines.push(makeRow("", innerW, border));
|
|
966
|
+
lines.push(makeRow(dim(`esc back ↑↓ scroll (${position2})`), innerW, border));
|
|
967
|
+
lines.push(`${borderFg("╰")}${borderFg("─".repeat(width - 2))}${borderFg("╯")}`);
|
|
968
|
+
return lines;
|
|
969
|
+
}
|
|
970
|
+
const maxScroll = Math.max(0, entries.length - VIEWPORT);
|
|
971
|
+
const clampScroll = Math.min(scroll, maxScroll);
|
|
972
|
+
const window = entries.slice(clampScroll, clampScroll + VIEWPORT);
|
|
973
|
+
for (let i = 0;i < window.length; i++) {
|
|
974
|
+
const absolute = clampScroll + i;
|
|
975
|
+
const entry = window[i];
|
|
976
|
+
const isSelected = absolute === selected;
|
|
977
|
+
const cursor = isSelected ? accent("▶") : " ";
|
|
978
|
+
const idText = isSelected ? accent(entry.id) : text(entry.id);
|
|
979
|
+
const snippet = compactLine(entry.content);
|
|
980
|
+
lines.push(makeRow(` ${cursor} ${idText} ${muted(entry.role)} ${dim(snippet)}`, innerW, border));
|
|
981
|
+
}
|
|
982
|
+
for (let i = window.length;i < VIEWPORT; i++)
|
|
983
|
+
lines.push(makeRow("", innerW, border));
|
|
984
|
+
const count = entries.length;
|
|
985
|
+
const position = count > VIEWPORT ? `${clampScroll + 1}-${Math.min(clampScroll + VIEWPORT, count)} of ${count}` : `${count} entr${count === 1 ? "y" : "ies"}`;
|
|
986
|
+
lines.push(makeRow("", innerW, border));
|
|
987
|
+
lines.push(makeRow(dim(`↑↓ navigate enter expand esc close (${position})`), innerW, border));
|
|
988
|
+
lines.push(`${borderFg("╰")}${borderFg("─".repeat(width - 2))}${borderFg("╯")}`);
|
|
989
|
+
return lines;
|
|
990
|
+
},
|
|
991
|
+
handleInput(data) {
|
|
992
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
993
|
+
if (mode === "detail") {
|
|
994
|
+
mode = "results";
|
|
995
|
+
tui.requestRender();
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
done(undefined);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (error)
|
|
1002
|
+
return;
|
|
1003
|
+
if (mode === "detail") {
|
|
1004
|
+
const maxScroll2 = Math.max(0, detailLines.length - VIEWPORT);
|
|
1005
|
+
if (matchesKey(data, Key.up)) {
|
|
1006
|
+
detailScroll = Math.max(0, detailScroll - 1);
|
|
1007
|
+
tui.requestRender();
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (matchesKey(data, Key.down)) {
|
|
1011
|
+
detailScroll = Math.min(maxScroll2, detailScroll + 1);
|
|
1012
|
+
tui.requestRender();
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (matchesKey(data, Key.pageUp)) {
|
|
1016
|
+
detailScroll = Math.max(0, detailScroll - VIEWPORT);
|
|
1017
|
+
tui.requestRender();
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (matchesKey(data, Key.pageDown)) {
|
|
1021
|
+
detailScroll = Math.min(maxScroll2, detailScroll + VIEWPORT);
|
|
1022
|
+
tui.requestRender();
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
const maxScroll = Math.max(0, entries.length - VIEWPORT);
|
|
1028
|
+
if (matchesKey(data, Key.up)) {
|
|
1029
|
+
selected = Math.max(0, selected - 1);
|
|
1030
|
+
if (selected < scroll)
|
|
1031
|
+
scroll = selected;
|
|
1032
|
+
tui.requestRender();
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
if (matchesKey(data, Key.down)) {
|
|
1036
|
+
selected = Math.min(Math.max(0, entries.length - 1), selected + 1);
|
|
1037
|
+
if (selected >= scroll + VIEWPORT)
|
|
1038
|
+
scroll = selected - VIEWPORT + 1;
|
|
1039
|
+
tui.requestRender();
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
if (matchesKey(data, Key.pageUp)) {
|
|
1043
|
+
selected = Math.max(0, selected - VIEWPORT);
|
|
1044
|
+
scroll = Math.max(0, Math.min(scroll, selected));
|
|
1045
|
+
tui.requestRender();
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (matchesKey(data, Key.pageDown)) {
|
|
1049
|
+
selected = Math.min(entries.length - 1, selected + VIEWPORT);
|
|
1050
|
+
scroll = Math.min(maxScroll, Math.max(scroll, selected - VIEWPORT + 1));
|
|
1051
|
+
tui.requestRender();
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
|
|
1055
|
+
const entry = entries[selected];
|
|
1056
|
+
if (!entry)
|
|
1057
|
+
return;
|
|
1058
|
+
loadDetail(entry.id);
|
|
1059
|
+
tui.requestRender();
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
},
|
|
1063
|
+
invalidate() {}
|
|
1064
|
+
};
|
|
1065
|
+
}, {
|
|
1066
|
+
overlay: true,
|
|
1067
|
+
overlayOptions: {
|
|
1068
|
+
anchor: "center",
|
|
1069
|
+
width: OVERLAY_WIDTH,
|
|
1070
|
+
margin: 2
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
function createGoosedumpIntegration(options = {}) {
|
|
1075
|
+
const shouldRegisterTool = options.registerTool ?? true;
|
|
1076
|
+
const shouldOverrideDefaultCompaction = options.overrideDefaultCompaction ?? true;
|
|
1077
|
+
const nativeExecutablePath = options.nativeExecutablePath;
|
|
1078
|
+
let goosedumpAvailable = false;
|
|
1079
|
+
let compactSettings = DEFAULT_GOOSEDUMP_SETTINGS;
|
|
1080
|
+
let turnsRemaining = 0;
|
|
1081
|
+
let counterTriggered = false;
|
|
1082
|
+
let compactionQueued = false;
|
|
1083
|
+
let compactionHookActive = false;
|
|
1084
|
+
let pendingKeep = null;
|
|
1085
|
+
function syncCompactCounters(ctx, clearPending = false) {
|
|
1086
|
+
compactSettings = resolveGoosedumpSettings(ctx.cwd);
|
|
1087
|
+
const completedTurns = completedTurnsSinceCompaction(ctx.sessionManager.getBranch());
|
|
1088
|
+
turnsRemaining = Math.max(0, compactSettings.turnsBeforeCompact - completedTurns);
|
|
1089
|
+
counterTriggered = compactSettings.turnsBeforeCompact > 0 && completedTurns >= compactSettings.turnsBeforeCompact;
|
|
1090
|
+
if (clearPending)
|
|
1091
|
+
pendingKeep = null;
|
|
1092
|
+
}
|
|
1093
|
+
function updateStatus(ctx) {
|
|
1094
|
+
if (!goosedumpAvailable)
|
|
1095
|
+
return;
|
|
1096
|
+
const active = shouldOverrideDefaultCompaction && compactSettings.turnsBeforeCompact > 0;
|
|
1097
|
+
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}`));
|
|
1099
|
+
}
|
|
1100
|
+
function noteCompletedTurn(ctx) {
|
|
1101
|
+
if (!goosedumpAvailable || !shouldOverrideDefaultCompaction)
|
|
1102
|
+
return;
|
|
1103
|
+
syncCompactCounters(ctx);
|
|
1104
|
+
updateStatus(ctx);
|
|
1105
|
+
}
|
|
1106
|
+
function checkGoosedump(ctx) {
|
|
1107
|
+
try {
|
|
1108
|
+
execFileSync(process.execPath, [resolveGoosedumpBinary(), "--version"], { stdio: "ignore" });
|
|
1109
|
+
return true;
|
|
1110
|
+
} catch {
|
|
1111
|
+
if (ctx) {
|
|
1112
|
+
ctx.ui.notify("goosedump was not found. Install with: npm install @jarkkojs/goosedump", "error");
|
|
1113
|
+
}
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function requestCompaction(ctx, keep) {
|
|
1118
|
+
if (compactionQueued || compactionHookActive)
|
|
1119
|
+
return "busy";
|
|
1120
|
+
if (keep === null && !hasCompactableHistory(ctx))
|
|
1121
|
+
return "nothing-to-compact";
|
|
1122
|
+
compactionQueued = true;
|
|
1123
|
+
pendingKeep = keep;
|
|
1124
|
+
ctx.compact({
|
|
1125
|
+
onComplete: () => {
|
|
1126
|
+
compactionQueued = false;
|
|
1127
|
+
pendingKeep = null;
|
|
1128
|
+
},
|
|
1129
|
+
onError: (error) => {
|
|
1130
|
+
compactionQueued = false;
|
|
1131
|
+
pendingKeep = null;
|
|
1132
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1133
|
+
if (ctx.hasUI)
|
|
1134
|
+
ctx.ui.notify(`goosedump compaction failed: ${message}`, "error");
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
return "started";
|
|
1138
|
+
}
|
|
1139
|
+
function register(pi) {
|
|
1140
|
+
if (shouldRegisterTool) {
|
|
1141
|
+
pi.registerTool(defineTool({
|
|
1142
|
+
name: "goose_recall",
|
|
1143
|
+
label: "goose_recall",
|
|
1144
|
+
description: "Recall durable context harvested from earlier compactions. Searches the current project by default and returns bounded remembered excerpts with provenance.",
|
|
1145
|
+
promptSnippet: "Recall durable context from earlier sessions",
|
|
1146
|
+
promptGuidelines: [
|
|
1147
|
+
"Use goose_recall for decisions, evidence, and context that may have disappeared from the current session.",
|
|
1148
|
+
"Results include their original provider, session, entry, and working-directory provenance.",
|
|
1149
|
+
"The current project is searched by default; set allProjects only when cross-project context is appropriate."
|
|
1150
|
+
],
|
|
1151
|
+
parameters: Type.Object({
|
|
1152
|
+
query: Type.String({ description: "Persistent-memory search query" }),
|
|
1153
|
+
provider: Type.Optional(Type.Union(GOOSE_PROVIDERS.map((provider) => Type.Literal(provider)), {
|
|
1154
|
+
description: "Optional source-provider filter"
|
|
1155
|
+
})),
|
|
1156
|
+
type: Type.Optional(Type.Union(["decision", "fact", "preference", "procedure", "episode"].map((type) => Type.Literal(type)), { description: "Optional memory-type filter" })),
|
|
1157
|
+
allProjects: Type.Optional(Type.Boolean({
|
|
1158
|
+
default: false,
|
|
1159
|
+
description: "Search memory from every working directory"
|
|
1160
|
+
})),
|
|
1161
|
+
limit: Type.Optional(Type.Integer({
|
|
1162
|
+
minimum: 1,
|
|
1163
|
+
maximum: 20,
|
|
1164
|
+
default: 5,
|
|
1165
|
+
description: "Maximum remembered entries to return"
|
|
1166
|
+
}))
|
|
1167
|
+
}),
|
|
1168
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1169
|
+
if (!goosedumpAvailable)
|
|
1170
|
+
return goosedumpUnavailableResult();
|
|
1171
|
+
try {
|
|
1172
|
+
const { hits, warning } = await goosedumpRecall(params.query, {
|
|
1173
|
+
provider: params.provider,
|
|
1174
|
+
path: params.allProjects ? undefined : ctx.cwd,
|
|
1175
|
+
type: params.type,
|
|
1176
|
+
limit: params.limit
|
|
1177
|
+
}, signal, nativeExecutablePath);
|
|
1178
|
+
if (warning && ctx.hasUI)
|
|
1179
|
+
ctx.ui.notify(warning, "warning");
|
|
1180
|
+
if (hits.length === 0)
|
|
1181
|
+
return textResult(`No durable memory for "${params.query}"`);
|
|
1182
|
+
return textResult(formatMemoryHits(hits));
|
|
1183
|
+
} catch (err) {
|
|
1184
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1185
|
+
return textResult(`goosedump error: ${message}`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}));
|
|
1189
|
+
pi.registerTool(defineTool({
|
|
1190
|
+
name: "goose_search",
|
|
1191
|
+
label: "goose_search",
|
|
1192
|
+
description: "Search coding agent session history by ranked relevance. Returns compact entry overviews with entry IDs; use goose_get to expand specific IDs to full content. Defaults to the current Pi session.",
|
|
1193
|
+
promptSnippet: "Search session history by ranked relevance",
|
|
1194
|
+
promptGuidelines: [
|
|
1195
|
+
"Use goose_search to find relevant context in this or another coding agent's session history.",
|
|
1196
|
+
"Start with compact ranked results, then use goose_get to expand interesting entry IDs to full content.",
|
|
1197
|
+
'Default scope is "lineage" (current branch); use scope: "all" to include all entries in the session.',
|
|
1198
|
+
HISTORY_TARGET_GUIDELINE
|
|
1199
|
+
],
|
|
1200
|
+
parameters: Type.Object({
|
|
1201
|
+
query: Type.String({ description: "Search query to rank messages by relevance" }),
|
|
1202
|
+
provider: historyProviderParam("search"),
|
|
1203
|
+
session: historySessionParam(),
|
|
1204
|
+
scope: historyScopeParam(),
|
|
1205
|
+
page: Type.Optional(Type.Integer({
|
|
1206
|
+
minimum: 1,
|
|
1207
|
+
default: 1,
|
|
1208
|
+
description: "Page number for ranked results"
|
|
1209
|
+
}))
|
|
1210
|
+
}),
|
|
1211
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1212
|
+
if (!goosedumpAvailable)
|
|
1213
|
+
return goosedumpUnavailableResult();
|
|
1214
|
+
try {
|
|
1215
|
+
const provider = params.provider ?? "pi";
|
|
1216
|
+
const target = resolveTarget(ctx, provider, params.session);
|
|
1217
|
+
if (!target)
|
|
1218
|
+
return missingSessionId("goose_search", provider);
|
|
1219
|
+
const page = params.page ?? 1;
|
|
1220
|
+
const messages = goosedumpSearch(target, params.query, {
|
|
1221
|
+
scope: params.scope ?? "lineage",
|
|
1222
|
+
page
|
|
1223
|
+
});
|
|
1224
|
+
if (messages.length === 0) {
|
|
1225
|
+
return textResult(`No results for "${params.query}"`);
|
|
1226
|
+
}
|
|
1227
|
+
const text = formatMessagesCompact(messages);
|
|
1228
|
+
const header = `Results for "${params.query}" (page ${page}):
|
|
1229
|
+
|
|
1230
|
+
`;
|
|
1231
|
+
return textResult(header + text);
|
|
1232
|
+
} catch (err) {
|
|
1233
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1234
|
+
return textResult(`goosedump error: ${message}`);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}));
|
|
1238
|
+
pi.registerTool(defineTool({
|
|
1239
|
+
name: "goose_get",
|
|
1240
|
+
label: "goose_get",
|
|
1241
|
+
description: "Expand specific entry IDs (from goose_search or goose_grep) to their full content. Defaults to the current Pi session.",
|
|
1242
|
+
promptSnippet: "Expand session entries by ID to full content",
|
|
1243
|
+
promptGuidelines: [
|
|
1244
|
+
"Use goose_get to expand entry IDs returned by goose_search or goose_grep into their full content.",
|
|
1245
|
+
"Pass only the specific IDs you need; full transcripts are expensive.",
|
|
1246
|
+
'Default scope is "lineage" (current branch); use scope: "all" to retrieve entries from all session branches.',
|
|
1247
|
+
HISTORY_TARGET_GUIDELINE
|
|
1248
|
+
],
|
|
1249
|
+
parameters: Type.Object({
|
|
1250
|
+
ids: Type.Array(Type.String(), {
|
|
1251
|
+
minItems: 1,
|
|
1252
|
+
description: "Entry IDs to expand (show full content)"
|
|
1253
|
+
}),
|
|
1254
|
+
provider: historyProviderParam("read"),
|
|
1255
|
+
session: historySessionParam(),
|
|
1256
|
+
scope: historyScopeParam()
|
|
1257
|
+
}),
|
|
1258
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1259
|
+
if (!goosedumpAvailable)
|
|
1260
|
+
return goosedumpUnavailableResult();
|
|
1261
|
+
try {
|
|
1262
|
+
const provider = params.provider ?? "pi";
|
|
1263
|
+
const target = resolveTarget(ctx, provider, params.session);
|
|
1264
|
+
if (!target)
|
|
1265
|
+
return missingSessionId("goose_get", provider);
|
|
1266
|
+
const messages = goosedumpShow(target, {
|
|
1267
|
+
scope: params.scope ?? "lineage",
|
|
1268
|
+
ids: params.ids
|
|
1269
|
+
});
|
|
1270
|
+
if (messages.length === 0) {
|
|
1271
|
+
return textResult("No entries found for the given IDs.");
|
|
1272
|
+
}
|
|
1273
|
+
return textResult(formatMessagesFull(messages));
|
|
1274
|
+
} catch (err) {
|
|
1275
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1276
|
+
return textResult(`goosedump error: ${message}`);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}));
|
|
1280
|
+
pi.registerTool(defineTool({
|
|
1281
|
+
name: "goose_grep",
|
|
1282
|
+
label: "goose_grep",
|
|
1283
|
+
description: "Filter session messages by glob pattern (e.g. *rand*). Returns compact entry overviews with entry IDs; use goose_get to expand. Defaults to the current Pi session.",
|
|
1284
|
+
promptSnippet: "Filter session history by glob pattern",
|
|
1285
|
+
promptGuidelines: [
|
|
1286
|
+
"Use goose_grep to filter session messages by glob pattern when you need exact substring/glob matching instead of ranked relevance.",
|
|
1287
|
+
"Expand interesting entry IDs with goose_get.",
|
|
1288
|
+
'Default scope is "lineage" (current branch); use scope: "all" to include all entries in the session.',
|
|
1289
|
+
HISTORY_TARGET_GUIDELINE
|
|
1290
|
+
],
|
|
1291
|
+
parameters: Type.Object({
|
|
1292
|
+
pattern: Type.String({ description: "Glob pattern to filter messages (e.g. *rand*)" }),
|
|
1293
|
+
provider: historyProviderParam("grep"),
|
|
1294
|
+
session: historySessionParam(),
|
|
1295
|
+
scope: historyScopeParam()
|
|
1296
|
+
}),
|
|
1297
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1298
|
+
if (!goosedumpAvailable)
|
|
1299
|
+
return goosedumpUnavailableResult();
|
|
1300
|
+
try {
|
|
1301
|
+
const provider = params.provider ?? "pi";
|
|
1302
|
+
const target = resolveTarget(ctx, provider, params.session);
|
|
1303
|
+
if (!target)
|
|
1304
|
+
return missingSessionId("goose_grep", provider);
|
|
1305
|
+
const messages = goosedumpGrep(target, params.pattern, {
|
|
1306
|
+
scope: params.scope ?? "lineage"
|
|
1307
|
+
});
|
|
1308
|
+
if (messages.length === 0) {
|
|
1309
|
+
return textResult(`No matches for "${params.pattern}"`);
|
|
1310
|
+
}
|
|
1311
|
+
return textResult(formatMessagesCompact(messages));
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1314
|
+
return textResult(`goosedump error: ${message}`);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}));
|
|
1318
|
+
pi.registerTool(defineTool({
|
|
1319
|
+
name: "goose_compact",
|
|
1320
|
+
label: "goose_compact",
|
|
1321
|
+
description: "Trigger a goosedump compaction of the current session now, optionally keeping the last N user turns verbatim while summarizing everything before. Compaction runs asynchronously via Pi's compaction flow.",
|
|
1322
|
+
promptSnippet: "Compact session history; optionally keep recent user turns",
|
|
1323
|
+
promptGuidelines: [
|
|
1324
|
+
"Use goose_compact to compact the session proactively with goosedump when context is large or nearing limits.",
|
|
1325
|
+
"Pass keep:N (N >= 1) to keep the last N user turns verbatim and summarize everything before them; omit to compact up to Pi's token-based cut.",
|
|
1326
|
+
"Compaction is asynchronous; the session is notified when it completes."
|
|
1327
|
+
],
|
|
1328
|
+
parameters: Type.Object({
|
|
1329
|
+
keep: Type.Optional(Type.Integer({
|
|
1330
|
+
minimum: 1,
|
|
1331
|
+
description: "User turns to keep verbatim; summarize everything before them. Omit to compact up to Pi's token-based cut."
|
|
1332
|
+
}))
|
|
1333
|
+
}),
|
|
1334
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1335
|
+
if (!goosedumpAvailable)
|
|
1336
|
+
return goosedumpUnavailableResult();
|
|
1337
|
+
const keep = params.keep ?? null;
|
|
1338
|
+
const result = requestCompaction(ctx, keep);
|
|
1339
|
+
if (result === "busy")
|
|
1340
|
+
return textResult("Compaction already in progress.");
|
|
1341
|
+
if (result === "nothing-to-compact") {
|
|
1342
|
+
return textResult("Nothing to compact (session too small).");
|
|
1343
|
+
}
|
|
1344
|
+
return textResult(compactStartedMessage(keep));
|
|
1345
|
+
}
|
|
1346
|
+
}));
|
|
1347
|
+
}
|
|
1348
|
+
function maybeAutoCompact(ctx, resumeAfter) {
|
|
1349
|
+
if (!goosedumpAvailable || !shouldOverrideDefaultCompaction)
|
|
1350
|
+
return;
|
|
1351
|
+
if (!counterTriggered || compactionQueued)
|
|
1352
|
+
return;
|
|
1353
|
+
if (ctx.hasPendingMessages())
|
|
1354
|
+
return;
|
|
1355
|
+
if (!hasCompactableHistory(ctx))
|
|
1356
|
+
return;
|
|
1357
|
+
if (!resumeAfter && piAutoCompactionPending(ctx))
|
|
1358
|
+
return;
|
|
1359
|
+
syncCompactCounters(ctx, true);
|
|
1360
|
+
updateStatus(ctx);
|
|
1361
|
+
compactionQueued = true;
|
|
1362
|
+
ctx.compact({
|
|
1363
|
+
onComplete: () => {
|
|
1364
|
+
compactionQueued = false;
|
|
1365
|
+
syncCompactCounters(ctx, true);
|
|
1366
|
+
updateStatus(ctx);
|
|
1367
|
+
if (resumeAfter && !ctx.hasPendingMessages()) {
|
|
1368
|
+
pi.sendMessage({
|
|
1369
|
+
customType: "goosedump-autocompact-resume",
|
|
1370
|
+
content: "The session was compacted to reduce context size. Continue the task you were working on from where you left off.",
|
|
1371
|
+
display: false
|
|
1372
|
+
}, { triggerTurn: true });
|
|
1373
|
+
}
|
|
1374
|
+
},
|
|
1375
|
+
onError: (error) => {
|
|
1376
|
+
compactionQueued = false;
|
|
1377
|
+
syncCompactCounters(ctx, true);
|
|
1378
|
+
updateStatus(ctx);
|
|
1379
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1380
|
+
if (ctx.hasUI)
|
|
1381
|
+
ctx.ui.notify(`goosedump compaction failed: ${message}`, "error");
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
1386
|
+
goosedumpAvailable = checkGoosedump(ctx);
|
|
1387
|
+
notifyInvalidSettings(ctx);
|
|
1388
|
+
syncCompactCounters(ctx, true);
|
|
1389
|
+
updateStatus(ctx);
|
|
1390
|
+
});
|
|
1391
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
1392
|
+
if (isCompletedAssistant(event.message))
|
|
1393
|
+
noteCompletedTurn(ctx);
|
|
1394
|
+
if (event.toolResults.length > 0)
|
|
1395
|
+
maybeAutoCompact(ctx, true);
|
|
1396
|
+
});
|
|
1397
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
1398
|
+
maybeAutoCompact(ctx, false);
|
|
1399
|
+
});
|
|
1400
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
1401
|
+
if (!shouldOverrideDefaultCompaction || !goosedumpAvailable)
|
|
1402
|
+
return;
|
|
1403
|
+
if (event.customInstructions?.trim())
|
|
1404
|
+
return;
|
|
1405
|
+
if (event.signal.aborted)
|
|
1406
|
+
return;
|
|
1407
|
+
const { messagesToSummarize, turnPrefixMessages } = event.preparation;
|
|
1408
|
+
if (pendingKeep === null && messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) {
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
const target = currentSessionTarget(ctx);
|
|
1412
|
+
if (!target)
|
|
1413
|
+
return;
|
|
1414
|
+
if (compactionHookActive)
|
|
1415
|
+
return { cancel: true };
|
|
1416
|
+
compactionHookActive = true;
|
|
1417
|
+
try {
|
|
1418
|
+
const keep = pendingKeep ?? undefined;
|
|
1419
|
+
const compaction = await buildGoosedumpCompaction(target, event.preparation, event.branchEntries, keep, event.signal, nativeExecutablePath);
|
|
1420
|
+
if (compaction && "cancel" in compaction) {
|
|
1421
|
+
if (ctx.hasUI)
|
|
1422
|
+
ctx.ui.notify(compaction.reason, "info");
|
|
1423
|
+
return { cancel: true };
|
|
1424
|
+
}
|
|
1425
|
+
const warning = compaction?.details?.warning;
|
|
1426
|
+
if (warning && ctx.hasUI) {
|
|
1427
|
+
ctx.ui.notify(warning, "warning");
|
|
1428
|
+
}
|
|
1429
|
+
return compaction ? { compaction } : undefined;
|
|
1430
|
+
} catch (err) {
|
|
1431
|
+
if (event.signal.aborted)
|
|
1432
|
+
return { cancel: true };
|
|
1433
|
+
if (ctx.hasUI) {
|
|
1434
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1435
|
+
ctx.ui.notify(`goosedump compaction failed; falling back to Pi compaction: ${message}`, "error");
|
|
1436
|
+
}
|
|
1437
|
+
return;
|
|
1438
|
+
} finally {
|
|
1439
|
+
compactionHookActive = false;
|
|
1440
|
+
pendingKeep = null;
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
pi.on("session_compact", async (event, ctx) => {
|
|
1444
|
+
if (!goosedumpAvailable || !shouldOverrideDefaultCompaction)
|
|
1445
|
+
return;
|
|
1446
|
+
syncCompactCounters(ctx, true);
|
|
1447
|
+
updateStatus(ctx);
|
|
1448
|
+
const compactionEntry = event.compactionEntry;
|
|
1449
|
+
if (!event.fromExtension || !compactionEntry || !isRecord(compactionEntry.details) || compactionEntry.details.source !== "goosedump") {
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
const tokensBefore = compactionEntry.tokensBefore;
|
|
1453
|
+
const summaryLen = compactionEntry.summary.length;
|
|
1454
|
+
const tokenEstimate = Math.ceil(summaryLen / 4);
|
|
1455
|
+
const tokensSaved = tokensBefore > 0 ? Math.round((1 - tokenEstimate / tokensBefore) * 100) : 0;
|
|
1456
|
+
ctx.ui.notify(`Goosedump compacted session: ~${tokensSaved}% token reduction (${tokensBefore} → ~${tokenEstimate} tokens)`, "info");
|
|
1457
|
+
});
|
|
1458
|
+
const maybePi = pi;
|
|
1459
|
+
maybePi.registerCommand?.("goose-settings", {
|
|
1460
|
+
description: "Edit goosedump compaction settings",
|
|
1461
|
+
handler: async (_args, ctx) => {
|
|
1462
|
+
if (!goosedumpAvailable) {
|
|
1463
|
+
ctx.ui.notify(GOOSEDUMP_UNAVAILABLE_MESSAGE, "error");
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
try {
|
|
1467
|
+
await runSettingsEditor(ctx);
|
|
1468
|
+
syncCompactCounters(ctx);
|
|
1469
|
+
updateStatus(ctx);
|
|
1470
|
+
} catch (err) {
|
|
1471
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1472
|
+
ctx.ui.notify(`goosedump error: ${message}`, "error");
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
maybePi.registerCommand?.("goose-compact", {
|
|
1477
|
+
description: "Compact now with goosedump; optionally keep the last N user turns",
|
|
1478
|
+
handler: async (args, ctx) => {
|
|
1479
|
+
if (!goosedumpAvailable) {
|
|
1480
|
+
ctx.ui.notify(GOOSEDUMP_UNAVAILABLE_MESSAGE, "error");
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
let keep = null;
|
|
1484
|
+
if (typeof args === "string" && args.trim()) {
|
|
1485
|
+
const match = args.trim().match(/^keep:(\d+)$/);
|
|
1486
|
+
if (!match) {
|
|
1487
|
+
ctx.ui.notify("Usage: /goose-compact [keep:N], where N >= 1", "error");
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
keep = Number(match[1]);
|
|
1491
|
+
if (!Number.isSafeInteger(keep) || keep < 1) {
|
|
1492
|
+
ctx.ui.notify("keep:N requires a safe integer N >= 1", "error");
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
const result = requestCompaction(ctx, keep);
|
|
1497
|
+
if (result === "busy") {
|
|
1498
|
+
ctx.ui.notify("Compaction already in progress", "info");
|
|
1499
|
+
} else if (result === "nothing-to-compact") {
|
|
1500
|
+
ctx.ui.notify("Nothing to compact (session too small)", "info");
|
|
1501
|
+
} else {
|
|
1502
|
+
ctx.ui.notify(compactStartedMessage(keep), "info");
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
maybePi.registerCommand?.("goose-search", {
|
|
1507
|
+
description: "Search the current session history",
|
|
1508
|
+
handler: async (args, ctx) => {
|
|
1509
|
+
if (!goosedumpAvailable) {
|
|
1510
|
+
ctx.ui.notify(GOOSEDUMP_UNAVAILABLE_MESSAGE, "error");
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
let query = typeof args === "string" ? args.trim() : "";
|
|
1514
|
+
if (query.length === 0) {
|
|
1515
|
+
const input = await ctx.ui.input("Search query:", "");
|
|
1516
|
+
query = (input ?? "").trim();
|
|
1517
|
+
}
|
|
1518
|
+
if (query.length === 0)
|
|
1519
|
+
return;
|
|
1520
|
+
try {
|
|
1521
|
+
await runSearchPanel(ctx, query);
|
|
1522
|
+
} catch (err) {
|
|
1523
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1524
|
+
ctx.ui.notify(`goosedump error: ${message}`, "error");
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
return { register };
|
|
1530
|
+
}
|
|
1531
|
+
export {
|
|
1532
|
+
parsePiMessages,
|
|
1533
|
+
pi_goosedump_default as default,
|
|
1534
|
+
createGoosedumpIntegration
|
|
1535
|
+
};
|