hippo-memory 1.68.0 → 1.69.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 +1 -1
- package/dist/agent-memories/copilot.d.ts +3 -1
- package/dist/agent-memories/copilot.js +2 -2
- package/dist/api/context-select.d.ts +10 -0
- package/dist/api/context-select.js +16 -18
- package/dist/api/context.js +101 -60
- package/dist/api/forget.d.ts +2 -2
- package/dist/api/forget.js +11 -0
- package/dist/api/outcome.d.ts +4 -4
- package/dist/api/outcome.js +29 -6
- package/dist/api/promote.d.ts +3 -3
- package/dist/api/promote.js +45 -90
- package/dist/api/remember.d.ts +3 -2
- package/dist/api/remember.js +38 -11
- package/dist/capture/compact.js +21 -2
- package/dist/capture/copilot-transcript.d.ts +3 -0
- package/dist/capture/copilot-transcript.js +8 -1
- package/dist/capture/session-worker.d.ts +13 -0
- package/dist/capture/session-worker.js +194 -0
- package/dist/capture-contract.d.ts +3 -1
- package/dist/capture-contract.js +36 -0
- package/dist/cli/session-hooks.js +90 -22
- package/dist/cli/setup.js +70 -24
- package/dist/cli/usage.js +4 -1
- package/dist/cli.js +1 -1
- package/dist/connectors/github/ingest.js +2 -1
- package/dist/connectors/slack/deletion.js +2 -1
- package/dist/connectors/slack/ingest.js +2 -1
- package/dist/env.d.ts +2 -0
- package/dist/env.js +2 -0
- package/dist/hooks/copilot.d.ts +50 -8
- package/dist/hooks/copilot.js +116 -30
- package/dist/hooks/json-hooks.d.ts +2 -0
- package/dist/hooks/json-hooks.js +11 -2
- package/dist/hooks/shared.d.ts +7 -1
- package/dist/hooks/shared.js +18 -3
- package/dist/mcp/memory-tools.d.ts +2 -2
- package/dist/mcp/memory-tools.js +9 -8
- package/dist/mcp/request.js +3 -3
- package/dist/memory.d.ts +2 -0
- package/dist/memory.js +7 -0
- package/dist/prompt-recall.d.ts +5 -0
- package/dist/prompt-recall.js +13 -0
- package/dist/raw-archive.d.ts +2 -2
- package/dist/raw-archive.js +5 -3
- package/dist/recall-scope.d.ts +2 -5
- package/dist/recall-scope.js +5 -6
- package/dist/server/routes/memories.js +9 -6
- package/dist/server-detect.js +3 -2
- package/dist/server.d.ts +14 -1
- package/dist/server.js +16 -6
- package/dist/store/audit-event.d.ts +1 -1
- package/dist/store/audit-event.js +1 -1
- package/dist/store/entry-writes-group.d.ts +9 -0
- package/dist/store/entry-writes-group.js +157 -0
- package/dist/store/handoffs.d.ts +5 -2
- package/dist/store/handoffs.js +35 -5
- package/dist/store/open.d.ts +2 -0
- package/dist/store/open.js +10 -0
- package/dist/store/search-rows.js +3 -9
- package/dist/store/sessions.d.ts +5 -0
- package/dist/store/sessions.js +4 -1
- package/dist/store-port.d.ts +71 -0
- package/dist/store-port.js +23 -11
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -21,11 +21,15 @@ import { cmdPreCompact, cmdPostCompact } from '../capture/compact.js';
|
|
|
21
21
|
import { transcriptWorkingState } from '../capture/working-state.js';
|
|
22
22
|
import { collectHandoffEvidence } from '../handoff-evidence.js';
|
|
23
23
|
import { resolveLastSessionTranscript } from '../capture/transcript.js';
|
|
24
|
-
import { copilotTranscriptFor } from '../capture/copilot-transcript.js';
|
|
24
|
+
import { copilotTranscriptFor, SESSION_ID_RE } from '../capture/copilot-transcript.js';
|
|
25
|
+
import { loadTurnPosition, runSessionWorker, saveTurnPosition, turnsAfter } from '../capture/session-worker.js';
|
|
26
|
+
import { isStringValue, readVscodeStop } from '../capture-contract.js';
|
|
27
|
+
import { loadConfig } from '../config.js';
|
|
28
|
+
import { countCreatedSinceLastSleep } from '../store/index-and-stats.js';
|
|
25
29
|
import { truncateCodePointSafe } from '../transcript-tail.js';
|
|
26
30
|
import { COMPACTION_DB_WAIT_MS } from '../compaction-record.js';
|
|
27
31
|
import { COMPACT_RESUME_EVENT_CONTENT_CAP, COMPACT_RESUME_MAX_AGE_MS, compactResumeText } from '../context-render.js';
|
|
28
|
-
import { readHookStdin } from '../stdin.js';
|
|
32
|
+
import { normaliseHookPayload, readHookStdin, readStdinBounded } from '../stdin.js';
|
|
29
33
|
import { resolveTenantId } from '../tenant.js';
|
|
30
34
|
import { errorMessage, log } from '../log.js';
|
|
31
35
|
import { withLedgerDb } from '../ledger-db.js';
|
|
@@ -143,11 +147,16 @@ function restoreCompactSnapshot(hippoRoot, payloadSessionId) {
|
|
|
143
147
|
*/
|
|
144
148
|
export async function cmdSessionEnd(hippoRoot, flags) {
|
|
145
149
|
const runtime = hookRuntime(flags);
|
|
150
|
+
const turn = flags['turn'] === true;
|
|
146
151
|
// Copilot's hook command carries no path, since one quoted into it would need escaping for each shell; the log goes where the hook table used to point.
|
|
147
152
|
const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : runtime === 'copilot' ? resolveJsonHookPaths('copilot').logFile : null;
|
|
148
153
|
// Bounded read: extracts transcript_path + session_id for the detached worker's argv.
|
|
149
154
|
let sessionId = null;
|
|
150
|
-
const
|
|
155
|
+
const raw = await readStdinBounded();
|
|
156
|
+
// Read before normalising: the Copilot CLI's agentStop shares this hook line, and camelCase keys would pass as VS Code's.
|
|
157
|
+
if (turn && !isVscodeStopPayload(raw))
|
|
158
|
+
return;
|
|
159
|
+
const stdinText = normaliseHookPayload(raw.text);
|
|
151
160
|
// Before the spawn, since the worker finds its store from the folder it inherits.
|
|
152
161
|
const root = payloadCwdRoot(hippoRoot, stdinText, runtime);
|
|
153
162
|
try {
|
|
@@ -172,6 +181,8 @@ export async function cmdSessionEnd(hippoRoot, flags) {
|
|
|
172
181
|
workerArgs.push('--transcript', transcriptPath);
|
|
173
182
|
if (sessionId)
|
|
174
183
|
workerArgs.push('--session-id', sessionId);
|
|
184
|
+
if (turn)
|
|
185
|
+
workerArgs.push('--turn');
|
|
175
186
|
try {
|
|
176
187
|
const child = spawn(process.execPath, workerArgs, {
|
|
177
188
|
detached: true,
|
|
@@ -194,7 +205,16 @@ export async function cmdSessionEnd(hippoRoot, flags) {
|
|
|
194
205
|
return;
|
|
195
206
|
}
|
|
196
207
|
}
|
|
208
|
+
/** A VS Code Stop payload with a session id that can name the lock file; anything else makes turn mode a silent no-op. */
|
|
209
|
+
function isVscodeStopPayload(raw) {
|
|
210
|
+
const receipt = readVscodeStop(raw.text, raw.timedOut);
|
|
211
|
+
return receipt.status === 'received' && receipt.input.sessionId !== null && SESSION_ID_RE.test(receipt.input.sessionId);
|
|
212
|
+
}
|
|
197
213
|
export async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
214
|
+
const sessionId = flags['session-id'];
|
|
215
|
+
await runSessionWorker(isStringValue(sessionId) ? sessionId : null, flags['turn'] === true ? 'turn' : 'full', (mode) => sessionEndWork(hippoRoot, flags, mode));
|
|
216
|
+
}
|
|
217
|
+
async function sessionEndWork(hippoRoot, flags, mode) {
|
|
198
218
|
const transcriptPath = typeof flags['transcript'] === 'string' ? flags['transcript'] : undefined;
|
|
199
219
|
const closeLogFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : null;
|
|
200
220
|
const closeSessionId = typeof flags['session-id'] === 'string' ? flags['session-id'] : null;
|
|
@@ -211,11 +231,18 @@ export async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
211
231
|
flushRereadLog();
|
|
212
232
|
return;
|
|
213
233
|
}
|
|
214
|
-
|
|
234
|
+
if (mode === 'turn' && closeSessionId)
|
|
235
|
+
await sleepIfDue(hippoRoot, flags, closeLogFile, transcriptPath, closeSessionId);
|
|
236
|
+
else
|
|
237
|
+
await sleepProjectStore(hippoRoot, flags, closeLogFile, transcriptPath);
|
|
215
238
|
flushRereadLog();
|
|
216
239
|
const digestLog = (message) => appendSessionEndCloseLog(closeLogFile, message);
|
|
217
240
|
const scan = transcriptPath ? readSessionScan(transcriptPath, digestLog) : null;
|
|
218
|
-
captureEndedSession(hippoRoot, store, flags, transcriptPath,
|
|
241
|
+
const capture = (turns) => captureEndedSession(hippoRoot, store, flags, transcriptPath, turns);
|
|
242
|
+
if (mode === 'turn')
|
|
243
|
+
captureNewTurns(transcriptPath, closeSessionId, scan, capture, digestLog);
|
|
244
|
+
else
|
|
245
|
+
capture(scan?.turns);
|
|
219
246
|
recordSessionDigest(hippoRoot, scan, {
|
|
220
247
|
key: closeSessionId || path.basename(transcriptPath ?? '', '.jsonl'),
|
|
221
248
|
tenantId: resolveTenantId({}),
|
|
@@ -225,7 +252,12 @@ export async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
225
252
|
// session-end may never fire (crash, kill -9) the freshness bound in loadFreshActiveTaskSnapshot is the backstop.
|
|
226
253
|
// The handoff is written first, while the snapshot writeSessionEndHandoff reads is still active.
|
|
227
254
|
if (closeSessionId)
|
|
228
|
-
writeEndHandoff(store, closeSessionId, transcriptPath, closeLogFile);
|
|
255
|
+
writeEndHandoff(store, closeSessionId, transcriptPath, closeLogFile, mode === 'turn');
|
|
256
|
+
if (mode === 'turn') {
|
|
257
|
+
// The chat goes on after a reply, so its snapshot stays for the next compaction to restore.
|
|
258
|
+
appendSessionEndCloseLog(closeLogFile, 'skip snapshot close: turn mode');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
229
261
|
try {
|
|
230
262
|
if (closeSessionId) {
|
|
231
263
|
const closed = closeTaskSnapshotsForSession(store, resolveTenantId({}), closeSessionId);
|
|
@@ -255,32 +287,68 @@ async function sleepProjectStore(hippoRoot, flags, closeLogFile, transcriptPath)
|
|
|
255
287
|
logSessionEndImport(closeLogFile, transcriptPath);
|
|
256
288
|
}
|
|
257
289
|
}
|
|
258
|
-
|
|
290
|
+
/** A close after every reply sleeps only at the MCP server's auto-sleep threshold; each line starts the log afresh, as a sleep does. */
|
|
291
|
+
async function sleepIfDue(hippoRoot, flags, closeLogFile, transcriptPath, sessionId) {
|
|
292
|
+
if (!isInitialized(hippoRoot))
|
|
293
|
+
return sleepProjectStore(hippoRoot, flags, closeLogFile, transcriptPath);
|
|
294
|
+
const prefix = `turn close, session ${sessionId}`;
|
|
295
|
+
try {
|
|
296
|
+
const { enabled, threshold } = loadConfig(hippoRoot).autoSleep;
|
|
297
|
+
const count = enabled ? countCreatedSinceLastSleep(hippoRoot, resolveTenantId({})) : 0;
|
|
298
|
+
if (!enabled || count < threshold) {
|
|
299
|
+
const why = enabled ? `${count} new memories, threshold ${threshold}` : 'auto-sleep is off';
|
|
300
|
+
appendSessionEndCloseLog(closeLogFile, `${prefix}: skip sleep, ${why}`, { startFresh: true });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
await sleepProjectStore(hippoRoot, flags, closeLogFile, transcriptPath);
|
|
304
|
+
appendSessionEndCloseLog(closeLogFile, `${prefix}: ran sleep at ${count} new memories (threshold ${threshold})`);
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
appendSessionEndCloseLog(closeLogFile, `${prefix}: sleep check failed: ${errorMessage(err)}`, { startFresh: true });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/** Captures only the turns after this session's cursor, then moves the cursor, so each reply is extracted once. */
|
|
311
|
+
function captureNewTurns(transcriptPath, sessionId, scan, capture, log) {
|
|
312
|
+
if (!transcriptPath || !sessionId || !scan) {
|
|
313
|
+
log('skip capture: no readable transcript for this session');
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const fresh = turnsAfter(scan.turns, loadTurnPosition(sessionId, transcriptPath));
|
|
317
|
+
if (fresh.length === 0) {
|
|
318
|
+
log('skip capture: no new turns since the last reply');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (capture(fresh))
|
|
322
|
+
saveTurnPosition(sessionId, transcriptPath, scan.turns, log);
|
|
323
|
+
}
|
|
324
|
+
/** True when capture ran to the end, so a turn close may move its cursor past these turns. */
|
|
325
|
+
function captureEndedSession(hippoRoot, store, flags, transcriptPath, turns) {
|
|
259
326
|
try {
|
|
260
327
|
const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined;
|
|
261
328
|
// With no stdin of its own, capture would read this as a manual run and scan every project.
|
|
262
329
|
if (!transcriptPath) {
|
|
263
330
|
appendSessionEndCloseLog(logFile ?? null, 'skip capture: no transcript for this session');
|
|
331
|
+
return false;
|
|
264
332
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
333
|
+
cmdCapture(store, {
|
|
334
|
+
source: 'last-session',
|
|
335
|
+
transcriptPath,
|
|
336
|
+
logFile,
|
|
337
|
+
dryRun: false,
|
|
338
|
+
global: false,
|
|
339
|
+
tenantId: resolveTenantId({}),
|
|
340
|
+
// In the global store, rows would otherwise read as user-global and show up in every project.
|
|
341
|
+
originProject: store === hippoRoot ? undefined : resolveProjectIdentity(process.cwd()),
|
|
342
|
+
sessionTurns: turns,
|
|
343
|
+
});
|
|
344
|
+
return true;
|
|
278
345
|
}
|
|
279
346
|
catch {
|
|
280
347
|
// Same treatment — the failure line is already in the log.
|
|
348
|
+
return false;
|
|
281
349
|
}
|
|
282
350
|
}
|
|
283
|
-
function writeEndHandoff(store, closeSessionId, transcriptPath, closeLogFile) {
|
|
351
|
+
function writeEndHandoff(store, closeSessionId, transcriptPath, closeLogFile, inPlace = false) {
|
|
284
352
|
try {
|
|
285
353
|
const tenantId = resolveTenantId({});
|
|
286
354
|
const ownSnapshot = loadActiveTaskSnapshot(store, tenantId)?.session_id === closeSessionId;
|
|
@@ -293,7 +361,7 @@ function writeEndHandoff(store, closeSessionId, transcriptPath, closeLogFile) {
|
|
|
293
361
|
}
|
|
294
362
|
else {
|
|
295
363
|
const evidence = collectHandoffEvidence(process.cwd(), 'unknown');
|
|
296
|
-
const handoff = writeSessionEndHandoff(store, tenantId, closeSessionId, evidence, derived);
|
|
364
|
+
const handoff = writeSessionEndHandoff(store, tenantId, closeSessionId, evidence, derived, undefined, { inPlace });
|
|
297
365
|
appendSessionEndCloseLog(closeLogFile, handoff ? `wrote handoff for session ${closeSessionId}` : `skip: kept the existing handoff for session ${closeSessionId}`);
|
|
298
366
|
}
|
|
299
367
|
}
|
package/dist/cli/setup.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import { execFileSync } from 'child_process';
|
|
5
|
-
import { installJsonHooks, uninstallJsonHooks, checkUninstallable, resolveJsonHookPaths } from '../hooks/json-hooks.js';
|
|
5
|
+
import { installJsonHooks, uninstallJsonHooks, checkUninstallable, copilotHookEvents, resolveJsonHookPaths } from '../hooks/json-hooks.js';
|
|
6
6
|
import { detectInstalledTools } from '../hooks/shared.js';
|
|
7
7
|
import { ensureCodexWrapperInstalled, installCodexWrapper, detectRealCodexPath, uninstallCodexWrapper, } from '../hooks/codex-wrapper.js';
|
|
8
8
|
import { installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath } from '../hooks/opencode.js';
|
|
9
|
-
import { copilotMcpSnippet, copilotPaths, installCopilot, isMcpFailure, uninstallCopilot } from '../hooks/copilot.js';
|
|
9
|
+
import { COPILOT_CLI_MCP, VSCODE_MCP, copilotMcpSnippet, copilotPaths, installCopilot, isCopilotCliPresent, isMcpFailure, uninstallCopilot, } from '../hooks/copilot.js';
|
|
10
10
|
import { isInitialized } from '../store/open.js';
|
|
11
11
|
import { currentMachine, importUserMemories } from '../agent-memories/sync.js';
|
|
12
12
|
import { getGlobalRoot } from '../shared.js';
|
|
@@ -42,7 +42,7 @@ function hookList() {
|
|
|
42
42
|
for (const [name, hook] of Object.entries(HOOKS)) {
|
|
43
43
|
console.log(` ${name.padEnd(15)} -> ${hook.file} (${hook.description})`);
|
|
44
44
|
}
|
|
45
|
-
console.log(` ${'copilot'.padEnd(15)} -> $COPILOT_HOME or ~/.copilot (GitHub Copilot: hooks, MCP server and instructions)`);
|
|
45
|
+
console.log(` ${'copilot'.padEnd(15)} -> $COPILOT_HOME or ~/.copilot, and each VS Code User folder (GitHub Copilot: hooks, MCP server and instructions)`);
|
|
46
46
|
console.log('\nUsage: hippo hook install <name>');
|
|
47
47
|
console.log(' hippo hook uninstall <name>');
|
|
48
48
|
}
|
|
@@ -155,11 +155,12 @@ function installOpencodeHook() {
|
|
|
155
155
|
console.log(`WARNING: opencode.json is unparseable; legacy hooks block could not be auto-removed. Fix the file manually.`);
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
+
const addByHand = (host) => `add this under "${host.key}" by hand: ${copilotMcpSnippet(host)}`;
|
|
158
159
|
const MCP_MERGE_LINES = {
|
|
159
160
|
added: (file) => `Added the "hippo" MCP server -> ${file}`,
|
|
160
161
|
present: (file) => `The "hippo" MCP server is already in ${file}`,
|
|
161
162
|
'user-owned': (file) => `Left the "hippo" MCP server in ${file} as it is: hippo did not write it`,
|
|
162
|
-
unreadable: (file) => `WARNING: ${file} has comments or is not a JSON object hippo can merge into, so it was left unchanged;
|
|
163
|
+
unreadable: (file, host) => `WARNING: ${file} has comments or is not a JSON object hippo can merge into, so it was left unchanged; ${addByHand(host)}`,
|
|
163
164
|
};
|
|
164
165
|
const mcpFailureLine = (file, failure, fix) => `WARNING: hippo could not use ${file} (${failure.failed}), so it was left unchanged; ${fix}`;
|
|
165
166
|
const unclosedLine = (file) => `WARNING: ${file} has ${HOOK_MARKERS.start} with no ${HOOK_MARKERS.end}, so hippo left it unchanged; fix the markers by hand`;
|
|
@@ -175,32 +176,74 @@ const INSTRUCTIONS_REMOVE_LINES = {
|
|
|
175
176
|
kept: (file) => `Left the hippo block in ${file}: it is not the Copilot text hippo wrote. Delete it by hand if no agent needs it.`,
|
|
176
177
|
unclosed: unclosedLine,
|
|
177
178
|
};
|
|
179
|
+
const VSCODE_INSTRUCTIONS_INSTALL_LINES = {
|
|
180
|
+
written: (file) => `Wrote the hippo instructions file -> ${file}`,
|
|
181
|
+
present: (file) => `The hippo instructions file is already at ${file}`,
|
|
182
|
+
kept: (file) => `Kept ${file} as it is: it is not the text hippo writes, so it was edited or is someone else's`,
|
|
183
|
+
};
|
|
184
|
+
const VSCODE_INSTRUCTIONS_REMOVE_LINES = {
|
|
185
|
+
removed: (file) => `Removed the hippo instructions file ${file}`,
|
|
186
|
+
absent: () => '',
|
|
187
|
+
kept: (file) => `Left ${file}: it is not the text hippo wrote. Delete it by hand if no agent needs it.`,
|
|
188
|
+
};
|
|
189
|
+
const VSCODE_HOOKS_LINE = 'VS Code: hooks need VS Code 1.109.3 or later with chat.useHooks on (the default); older versions get the MCP server and the instructions file only.';
|
|
190
|
+
function mcpInstallLine(file, mcp, host) {
|
|
191
|
+
return isMcpFailure(mcp) ? mcpFailureLine(file, mcp, addByHand(host)) : MCP_MERGE_LINES[mcp](file, host);
|
|
192
|
+
}
|
|
193
|
+
function printVscodeInstall(result, indent) {
|
|
194
|
+
const { paths } = result;
|
|
195
|
+
console.log(`${indent}${mcpInstallLine(paths.mcpConfig, result.mcp, VSCODE_MCP)}`);
|
|
196
|
+
console.log(`${indent}${VSCODE_INSTRUCTIONS_INSTALL_LINES[result.instructions](paths.instructions)}`);
|
|
197
|
+
if (paths.profiles.length > 0) {
|
|
198
|
+
const names = paths.profiles.map((dir) => path.basename(dir)).join(', ');
|
|
199
|
+
console.log(`${indent}Found VS Code profiles (${names}) under ${path.join(paths.userDir, 'profiles')}: hippo set up the default profile only; copy the "hippo" server and prompts/hippo.instructions.md into a profile to use hippo there.`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
178
202
|
function printCopilotInstall(result, indent) {
|
|
179
|
-
const { paths
|
|
180
|
-
const hooks = result.hooks ?
|
|
203
|
+
const { paths } = result;
|
|
204
|
+
const hooks = result.hooks ? `Installed hippo's Copilot hooks (${copilotHookEvents().join(', ')}) ->` : "hippo's Copilot hooks are already in";
|
|
181
205
|
console.log(`${indent}${hooks} ${paths.hooks}`);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
206
|
+
if (result.mcp === null || result.instructions === null) {
|
|
207
|
+
console.log(`${indent}No Copilot CLI files in ${path.dirname(paths.mcpConfig)}, so hippo skipped mcp-config.json and copilot-instructions.md, which only the Copilot CLI reads`);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
console.log(`${indent}${mcpInstallLine(paths.mcpConfig, result.mcp, COPILOT_CLI_MCP)}`);
|
|
211
|
+
console.log(`${indent}${INSTRUCTIONS_INSTALL_LINES[result.instructions](paths.instructions)}`);
|
|
212
|
+
}
|
|
213
|
+
for (const vscode of result.vscode)
|
|
214
|
+
printVscodeInstall(vscode, indent);
|
|
215
|
+
if (result.vscode.length > 0)
|
|
216
|
+
console.log(`${indent}${VSCODE_HOOKS_LINE}`);
|
|
185
217
|
console.log(`${indent}VS Code Copilot runs new hooks in a new chat session.`);
|
|
186
218
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (result.hooks)
|
|
190
|
-
console.log(`Removed hippo's Copilot hooks file ${paths.hooks}`);
|
|
219
|
+
/** The lines for one MCP config file; empty when hippo's server was not there. */
|
|
220
|
+
function mcpRemoveLines(file, mcp) {
|
|
191
221
|
if (isMcpFailure(mcp))
|
|
192
|
-
|
|
222
|
+
return [mcpFailureLine(file, mcp, 'remove the "hippo" server by hand if hippo added it')];
|
|
193
223
|
if (mcp === 'removed')
|
|
194
|
-
|
|
224
|
+
return [`Removed the "hippo" MCP server from ${file}`];
|
|
195
225
|
if (mcp === 'user-owned')
|
|
196
|
-
|
|
226
|
+
return [`Left the "hippo" MCP server in ${file}: hippo did not write it`];
|
|
197
227
|
if (mcp === 'unreadable')
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
228
|
+
return [`WARNING: ${file} has comments or is not a JSON object hippo can edit, so it was left unchanged; remove the "hippo" server by hand if hippo added it`];
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
function printCopilotUninstall(result) {
|
|
232
|
+
const { paths, mcp } = result;
|
|
233
|
+
const lines = [];
|
|
234
|
+
if (result.hooks)
|
|
235
|
+
lines.push(`Removed hippo's Copilot hooks file ${paths.hooks}`);
|
|
236
|
+
lines.push(...mcpRemoveLines(paths.mcpConfig, mcp));
|
|
237
|
+
lines.push(INSTRUCTIONS_REMOVE_LINES[result.instructions](paths.instructions));
|
|
238
|
+
for (const vscode of result.vscode) {
|
|
239
|
+
lines.push(...mcpRemoveLines(vscode.paths.mcpConfig, vscode.mcp));
|
|
240
|
+
lines.push(VSCODE_INSTRUCTIONS_REMOVE_LINES[vscode.instructions](vscode.paths.instructions));
|
|
241
|
+
}
|
|
242
|
+
const printed = lines.filter((line) => line !== '');
|
|
243
|
+
for (const line of printed)
|
|
244
|
+
console.log(line);
|
|
245
|
+
if (printed.length === 0)
|
|
246
|
+
console.log('No hippo Copilot hooks, MCP server or instructions found.');
|
|
204
247
|
}
|
|
205
248
|
function hookUninstall(target) {
|
|
206
249
|
if (target === 'copilot')
|
|
@@ -330,8 +373,11 @@ function setupCopilot(dryRun) {
|
|
|
330
373
|
const indent = ` ${'copilot'.padEnd(14)} `;
|
|
331
374
|
if (!dryRun)
|
|
332
375
|
return printCopilotInstall(installCopilot(), indent);
|
|
333
|
-
const { hooks, mcpConfig, instructions } = copilotPaths();
|
|
334
|
-
|
|
376
|
+
const { hooks, mcpConfig, instructions, vscode } = copilotPaths();
|
|
377
|
+
const cliFiles = isCopilotCliPresent() ? `, the MCP server in ${mcpConfig} and the hippo block in ${instructions}` : '';
|
|
378
|
+
console.log(`[dry-run] would install hooks in ${hooks}${cliFiles}`);
|
|
379
|
+
for (const dir of vscode)
|
|
380
|
+
console.log(`[dry-run] would add the MCP server to ${dir.mcpConfig} and write ${dir.instructions}`);
|
|
335
381
|
}
|
|
336
382
|
function setupJsonTool(tool, dryRun) {
|
|
337
383
|
if (tool.name === 'copilot')
|
package/dist/cli/usage.js
CHANGED
|
@@ -258,7 +258,10 @@ export const VERB_USAGE = {
|
|
|
258
258
|
--log-file <path> Tee the worker's output to a log file (paired with 'hippo last-sleep')
|
|
259
259
|
--runtime copilot The payload came from a Copilot hook: use the store of its cwd and find
|
|
260
260
|
the Copilot CLI session log by session id; with no --log-file, log to
|
|
261
|
-
~/.hippo/logs/copilot-sleep.log
|
|
261
|
+
~/.hippo/logs/copilot-sleep.log
|
|
262
|
+
--turn VS Code Stop hook, after each reply: capture only the new turns, sleep
|
|
263
|
+
only at the auto-sleep threshold, and keep the session's snapshot; does
|
|
264
|
+
nothing for any other payload`],
|
|
262
265
|
'pre-compact': [`
|
|
263
266
|
pre-compact PreCompact hook: record the compaction, save a working-state snapshot, and
|
|
264
267
|
ask the summariser to end with a "Memories for hippo" list
|
package/dist/cli.js
CHANGED
|
@@ -57,7 +57,7 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
57
57
|
'no-learn', 'no-mmr', 'no-propagate', 'no-schedule', 'no-share', 'no-summarize-older',
|
|
58
58
|
'observed', 'open', 'physics', 'pin', 'pinned-only', 'reject-loser', 'rerank-utility',
|
|
59
59
|
'reset-physics', 'save-baseline', 'show-cases', 'stats', 'stdin',
|
|
60
|
-
'strict', 'suite', 'value-aware', 'verified', 'version', 'why',
|
|
60
|
+
'strict', 'suite', 'turn', 'value-aware', 'verified', 'version', 'why',
|
|
61
61
|
]);
|
|
62
62
|
// Every flag some command reads. Anything else is a typo that no command would act on.
|
|
63
63
|
export const KNOWN_FLAGS = new Set([
|
|
@@ -132,7 +132,8 @@ function markKeySeenWithoutMemory(hippoRoot, idempotencyKey, input) {
|
|
|
132
132
|
}
|
|
133
133
|
function rememberWithEventLog(ctx, input, idempotencyKey, opts) {
|
|
134
134
|
// No `|| 'connector:github'` fallback (see rememberWithEventLog in slack/ingest.ts for rationale).
|
|
135
|
-
const result = remember(ctx,
|
|
135
|
+
const result = remember({ ...ctx, store: undefined }, // the event log row commits with the memory on hippo.db's own handle, never through a store
|
|
136
|
+
{
|
|
136
137
|
...opts,
|
|
137
138
|
untrusted: true,
|
|
138
139
|
afterWrite: (innerDb, memoryId) => {
|
|
@@ -44,7 +44,8 @@ export function handleMessageDeleted(ctx, input) {
|
|
|
44
44
|
// Archive + event-log mark commit together via afterArchive. The hook
|
|
45
45
|
// receives the same db handle the archive is using, so the INSERT lives
|
|
46
46
|
// inside the SAVEPOINT.
|
|
47
|
-
archiveRaw(ctx,
|
|
47
|
+
archiveRaw({ ...ctx, store: undefined }, // afterArchive runs on hippo.db's own handle, never through a store
|
|
48
|
+
memoryId, `source_deleted:slack:${input.teamId}:${input.channelId}:${input.deletedTs}`, {
|
|
48
49
|
afterArchive: (sameDb, archivedId) => {
|
|
49
50
|
markEventSeen(sameDb, input.eventId, archivedId);
|
|
50
51
|
},
|
|
@@ -63,7 +63,8 @@ export function ingestMessage(ctx, input) {
|
|
|
63
63
|
function rememberWithEventLog(ctx, eventId, opts) {
|
|
64
64
|
// No `|| 'connector:slack'` fallback: the caller always builds ctx with the connector subject, and with
|
|
65
65
|
// an object-shaped Context.actor an OR-fallback would never fire anyway.
|
|
66
|
-
const result = remember(ctx,
|
|
66
|
+
const result = remember({ ...ctx, store: undefined }, // the event log row commits with the memory on hippo.db's own handle, never through a store
|
|
67
|
+
{
|
|
67
68
|
...opts,
|
|
68
69
|
untrusted: true,
|
|
69
70
|
afterWrite: (innerDb, memoryId) => {
|
package/dist/env.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export declare function envRequireSessionScopedFreshTail(): boolean;
|
|
|
41
41
|
export declare function envStdinWaitMs(): number | undefined;
|
|
42
42
|
export declare function envLogLevel(): string;
|
|
43
43
|
export declare function envPort(): string | undefined;
|
|
44
|
+
/** How long a client waits for a running server's /health before writing to the store directly; server-detect.ts holds the default. */
|
|
45
|
+
export declare function envHealthProbeMs(): number | undefined;
|
|
44
46
|
export declare function envRequireAuth(): boolean;
|
|
45
47
|
export declare function envV1Rps(): string | undefined;
|
|
46
48
|
export declare function envApiKey(): string | undefined;
|
package/dist/env.js
CHANGED
|
@@ -83,6 +83,8 @@ export function envStdinWaitMs() { return positiveInt('HIPPO_STDIN_WAIT_MS'); }
|
|
|
83
83
|
export function envLogLevel() { return raw('HIPPO_LOG')?.trim().toLowerCase() ?? ''; }
|
|
84
84
|
// Server.
|
|
85
85
|
export function envPort() { return raw('HIPPO_PORT'); }
|
|
86
|
+
/** How long a client waits for a running server's /health before writing to the store directly; server-detect.ts holds the default. */
|
|
87
|
+
export function envHealthProbeMs() { return positiveInt('HIPPO_HEALTH_PROBE_MS'); }
|
|
86
88
|
export function envRequireAuth() { return isOne('HIPPO_REQUIRE_AUTH'); }
|
|
87
89
|
export function envV1Rps() { return raw('HIPPO_V1_RPS'); }
|
|
88
90
|
export function envApiKey() { return raw('HIPPO_API_KEY'); }
|
package/dist/hooks/copilot.d.ts
CHANGED
|
@@ -1,21 +1,41 @@
|
|
|
1
|
+
import type { JsonObject } from '../working-memory.js';
|
|
1
2
|
/** Copilot drops what a per-prompt hook prints, so per-task recall goes through the MCP tool the block names. */
|
|
2
3
|
export declare const COPILOT_INSTRUCTIONS: string;
|
|
3
|
-
|
|
4
|
+
export declare const VSCODE_INSTRUCTIONS: string;
|
|
5
|
+
/** The files hippo writes in one VS Code User folder. */
|
|
6
|
+
export interface VscodePaths {
|
|
7
|
+
readonly userDir: string;
|
|
8
|
+
readonly mcpConfig: string;
|
|
9
|
+
readonly instructions: string;
|
|
10
|
+
/** Profile folders, which keep their own mcp.json and prompts; hippo installs into the default profile only. */
|
|
11
|
+
readonly profiles: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
/** The three files hippo writes for Copilot, plus the two it writes in each VS Code User folder. */
|
|
4
14
|
export interface CopilotPaths {
|
|
5
15
|
readonly hooks: string;
|
|
6
16
|
readonly mcpConfig: string;
|
|
7
17
|
readonly instructions: string;
|
|
18
|
+
readonly vscode: readonly VscodePaths[];
|
|
8
19
|
}
|
|
9
20
|
export declare function copilotPaths(): CopilotPaths;
|
|
10
|
-
/**
|
|
11
|
-
export
|
|
21
|
+
/** The app that reads an MCP config: where it keeps its servers, and hippo's entry there. */
|
|
22
|
+
export interface McpHost {
|
|
23
|
+
readonly key: 'mcpServers' | 'servers';
|
|
24
|
+
server(): JsonObject;
|
|
25
|
+
}
|
|
26
|
+
/** The Copilot CLI's mcp-config.json keeps servers under "mcpServers", each with an env and a tool list. */
|
|
27
|
+
export declare const COPILOT_CLI_MCP: McpHost;
|
|
28
|
+
/** VS Code's mcp.json keeps stdio servers under "servers"; it has no "mcpServers" key. */
|
|
29
|
+
export declare const VSCODE_MCP: McpHost;
|
|
30
|
+
/** What to paste under the host's key when hippo cannot edit the file itself. */
|
|
31
|
+
export declare function copilotMcpSnippet(host?: McpHost): string;
|
|
12
32
|
export type McpMergeStatus = 'added' | 'present' | 'user-owned' | 'unreadable';
|
|
13
33
|
/** Adds hippo's server and keeps every other key; a file hippo cannot parse, or a "hippo" key it did not write, stays as it is. */
|
|
14
|
-
export declare function mergeMcpServer(file: string): McpMergeStatus;
|
|
34
|
+
export declare function mergeMcpServer(file: string, host?: McpHost): McpMergeStatus;
|
|
15
35
|
export type McpRemoveStatus = 'removed' | 'absent' | 'user-owned' | 'unreadable';
|
|
16
36
|
/** Removes the "hippo" key only when hippo wrote it. */
|
|
17
|
-
export declare function removeMcpServer(file: string): McpRemoveStatus;
|
|
18
|
-
/** A filesystem error on
|
|
37
|
+
export declare function removeMcpServer(file: string, host?: McpHost): McpRemoveStatus;
|
|
38
|
+
/** A filesystem error on an MCP config file (EACCES, EISDIR, EBUSY), kept as its own message so setup can report it and go on. */
|
|
19
39
|
export interface McpFailure {
|
|
20
40
|
readonly failed: string;
|
|
21
41
|
}
|
|
@@ -27,19 +47,41 @@ export declare function ensureInstructionsBlock(file: string): InstructionsInsta
|
|
|
27
47
|
export type InstructionsRemoveStatus = 'removed' | 'absent' | 'kept' | 'unclosed';
|
|
28
48
|
/** Splices out only hippo's own Copilot block and keeps every other byte; a file left with nothing but whitespace goes. */
|
|
29
49
|
export declare function removeInstructionsBlock(file: string): InstructionsRemoveStatus;
|
|
50
|
+
/** `kept`: the file is there but is not the text hippo writes, so the user edited it or it is someone else's. */
|
|
51
|
+
export type VscodeInstructionsInstallStatus = 'written' | 'present' | 'kept';
|
|
52
|
+
/** The file is hippo's alone, so it is written whole; an earlier hippo text is upgraded, and any other text already there stays. */
|
|
53
|
+
export declare function ensureVscodeInstructions(file: string): VscodeInstructionsInstallStatus;
|
|
54
|
+
export type VscodeInstructionsRemoveStatus = 'removed' | 'absent' | 'kept';
|
|
55
|
+
/** Deletes the file only when it still holds exactly what a hippo release wrote. */
|
|
56
|
+
export declare function removeVscodeInstructions(file: string): VscodeInstructionsRemoveStatus;
|
|
57
|
+
export interface VscodeInstallResult {
|
|
58
|
+
readonly paths: VscodePaths;
|
|
59
|
+
readonly mcp: McpMergeStatus | McpFailure;
|
|
60
|
+
readonly instructions: VscodeInstructionsInstallStatus;
|
|
61
|
+
}
|
|
30
62
|
export interface CopilotInstallResult {
|
|
31
63
|
readonly paths: CopilotPaths;
|
|
32
64
|
/** True when the hooks file was written; false when it already held hippo's current table. */
|
|
33
65
|
readonly hooks: boolean;
|
|
34
|
-
|
|
35
|
-
readonly
|
|
66
|
+
/** Null, like `instructions`, when the Copilot CLI is not installed, since only it reads these two files. */
|
|
67
|
+
readonly mcp: McpMergeStatus | McpFailure | null;
|
|
68
|
+
readonly instructions: InstructionsInstallStatus | null;
|
|
69
|
+
readonly vscode: readonly VscodeInstallResult[];
|
|
36
70
|
}
|
|
71
|
+
/** The CLI's folder holds more than the hooks folder setup makes for VS Code, so a rerun on a VS Code-only machine still finds no CLI. */
|
|
72
|
+
export declare function isCopilotCliPresent(home?: string): boolean;
|
|
37
73
|
export declare function installCopilot(): CopilotInstallResult;
|
|
74
|
+
export interface VscodeUninstallResult {
|
|
75
|
+
readonly paths: VscodePaths;
|
|
76
|
+
readonly mcp: McpRemoveStatus | McpFailure;
|
|
77
|
+
readonly instructions: VscodeInstructionsRemoveStatus;
|
|
78
|
+
}
|
|
38
79
|
export interface CopilotUninstallResult {
|
|
39
80
|
readonly paths: CopilotPaths;
|
|
40
81
|
readonly hooks: boolean;
|
|
41
82
|
readonly mcp: McpRemoveStatus | McpFailure;
|
|
42
83
|
readonly instructions: InstructionsRemoveStatus;
|
|
84
|
+
readonly vscode: readonly VscodeUninstallResult[];
|
|
43
85
|
}
|
|
44
86
|
export declare function uninstallCopilot(): CopilotUninstallResult;
|
|
45
87
|
//# sourceMappingURL=copilot.d.ts.map
|