hippo-memory 1.65.0 → 1.67.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 +2 -0
- package/dist/api/auth.d.ts +11 -2
- package/dist/api/auth.js +25 -2
- package/dist/api/recall-types.d.ts +1 -0
- package/dist/api/recall.js +5 -5
- package/dist/auth.d.ts +3 -1
- package/dist/auth.js +6 -2
- package/dist/capture/compact.d.ts +2 -0
- package/dist/capture/compact.js +12 -6
- package/dist/capture/copilot-transcript.d.ts +15 -0
- package/dist/capture/copilot-transcript.js +40 -0
- package/dist/capture/failure-reading.js +5 -2
- package/dist/capture/transcript.js +7 -1
- package/dist/capture/working-state.js +14 -3
- package/dist/capture-contract.d.ts +4 -2
- package/dist/capture-contract.js +3 -3
- package/dist/cli/context.js +11 -8
- package/dist/cli/hook-blocks.d.ts +2 -0
- package/dist/cli/hook-blocks.js +6 -0
- package/dist/cli/session-hooks.d.ts +1 -1
- package/dist/cli/session-hooks.js +28 -16
- package/dist/cli/setup.js +66 -7
- package/dist/cli/shared.d.ts +5 -0
- package/dist/cli/shared.js +22 -3
- package/dist/cli/usage.js +20 -9
- package/dist/db/open.d.ts +5 -1
- package/dist/db/open.js +28 -7
- package/dist/db/sqlite-blocked.d.ts +11 -1
- package/dist/db/sqlite-blocked.js +20 -2
- package/dist/db.d.ts +2 -2
- package/dist/db.js +2 -2
- package/dist/delivery-recorder.d.ts +3 -1
- package/dist/delivery-recorder.js +1 -1
- package/dist/embeddings.d.ts +11 -2
- package/dist/embeddings.js +20 -11
- package/dist/hooks/copilot.d.ts +45 -0
- package/dist/hooks/copilot.js +187 -0
- package/dist/hooks/json-hooks.d.ts +1 -1
- package/dist/hooks/json-hooks.js +41 -2
- package/dist/hooks/shared.d.ts +5 -1
- package/dist/hooks/shared.js +9 -0
- package/dist/mcp/request.d.ts +0 -2
- package/dist/mcp/request.js +16 -11
- package/dist/prompt-hook.d.ts +6 -0
- package/dist/prompt-hook.js +15 -7
- package/dist/quality-repair.js +3 -0
- package/dist/search/hybrid.d.ts +3 -0
- package/dist/search/physics-search.d.ts +3 -0
- package/dist/search/physics-search.js +18 -19
- package/dist/search/vector.d.ts +7 -2
- package/dist/search/vector.js +24 -10
- package/dist/server/routes/admin.js +1 -1
- package/dist/server/routes/recall.js +6 -9
- package/dist/server/types.d.ts +5 -4
- package/dist/server.d.ts +11 -6
- package/dist/server.js +18 -14
- package/dist/stdin.d.ts +5 -0
- package/dist/stdin.js +62 -0
- package/dist/store/key-audit.d.ts +6 -0
- package/dist/store/key-audit.js +25 -0
- package/dist/store/open.js +2 -1
- package/dist/store-port.d.ts +42 -4
- package/dist/store-port.js +46 -2
- package/dist/vector-store.d.ts +8 -0
- package/dist/vector-store.js +31 -19
- 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
package/dist/embeddings.js
CHANGED
|
@@ -101,16 +101,29 @@ export function saveStoredEmbeddingModel(hippoRoot, model) {
|
|
|
101
101
|
closeHippoDb(db);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
/** The model the index was built by: the stored identity, else the default model when vectors exist. */
|
|
105
|
+
export function indexedModel(state) {
|
|
106
|
+
return state.storedModel ?? (state.hasVectors ? DEFAULT_EMBEDDING_MODEL : null);
|
|
107
|
+
}
|
|
108
|
+
/** The one rebuild rule every store applies to its indexed model. */
|
|
109
|
+
export function indexNeedsRebuild(indexed, providerId) {
|
|
110
|
+
return indexed !== null && indexed !== embeddingIndexIdentity(providerId);
|
|
111
|
+
}
|
|
104
112
|
export function resolveIndexedEmbeddingModel(hippoRoot, index) {
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
return hasVectors ? DEFAULT_EMBEDDING_MODEL : null;
|
|
113
|
+
const storedModel = loadStoredEmbeddingModel(hippoRoot);
|
|
114
|
+
if (storedModel)
|
|
115
|
+
return storedModel;
|
|
116
|
+
return indexedModel({ storedModel, hasVectors: index ? Object.keys(index).length > 0 : withVectorDb(hippoRoot, hasStoredVectors) });
|
|
110
117
|
}
|
|
111
118
|
export function embeddingModelRequiresReindex(hippoRoot, model, index) {
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
return indexNeedsRebuild(resolveIndexedEmbeddingModel(hippoRoot, index), model);
|
|
120
|
+
}
|
|
121
|
+
/** hippo.db's index state, the meta row and the EXISTS on one handle. */
|
|
122
|
+
export function embeddingIndexStateAt(hippoRoot) {
|
|
123
|
+
return withVectorDb(hippoRoot, (db) => ({
|
|
124
|
+
storedModel: getMeta(db, EMBEDDING_MODEL_META_KEY, '').trim() || null,
|
|
125
|
+
hasVectors: hasStoredVectors(db),
|
|
126
|
+
}));
|
|
114
127
|
}
|
|
115
128
|
async function rebuildEmbeddingIndex(entries, provider) {
|
|
116
129
|
const rebuilt = {};
|
|
@@ -189,10 +202,6 @@ export function loadEmbeddingIndex(hippoRoot) {
|
|
|
189
202
|
export function loadStoredVectors(hippoRoot, ids) {
|
|
190
203
|
return ids.length === 0 ? new Map() : withVectorDb(hippoRoot, (db) => loadVectors(db, ids));
|
|
191
204
|
}
|
|
192
|
-
/** Whether the store holds any vector at all. */
|
|
193
|
-
export function hasEmbeddings(hippoRoot) {
|
|
194
|
-
return withVectorDb(hippoRoot, hasStoredVectors);
|
|
195
|
-
}
|
|
196
205
|
/** Replace every stored vector with `index`; `model` defaults to the stored index identity. */
|
|
197
206
|
export function saveEmbeddingIndex(hippoRoot, index, model) {
|
|
198
207
|
withVectorDb(hippoRoot, (db) => replaceAllVectors(db, index, model ?? getMeta(db, EMBEDDING_MODEL_META_KEY, '')));
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Copilot drops what a per-prompt hook prints, so per-task recall goes through the MCP tool the block names. */
|
|
2
|
+
export declare const COPILOT_INSTRUCTIONS: string;
|
|
3
|
+
/** The three files hippo writes for Copilot. */
|
|
4
|
+
export interface CopilotPaths {
|
|
5
|
+
readonly hooks: string;
|
|
6
|
+
readonly mcpConfig: string;
|
|
7
|
+
readonly instructions: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function copilotPaths(): CopilotPaths;
|
|
10
|
+
/** What to paste under "mcpServers" when hippo cannot edit the file itself. */
|
|
11
|
+
export declare function copilotMcpSnippet(): string;
|
|
12
|
+
export type McpMergeStatus = 'added' | 'present' | 'user-owned' | 'unreadable';
|
|
13
|
+
/** 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;
|
|
15
|
+
export type McpRemoveStatus = 'removed' | 'absent' | 'user-owned' | 'unreadable';
|
|
16
|
+
/** Removes the "hippo" key only when hippo wrote it. */
|
|
17
|
+
export declare function removeMcpServer(file: string): McpRemoveStatus;
|
|
18
|
+
/** A filesystem error on mcp-config.json (EACCES, EISDIR, EBUSY), kept as its own message so setup can report it and go on. */
|
|
19
|
+
export interface McpFailure {
|
|
20
|
+
readonly failed: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function isMcpFailure(status: string | McpFailure): status is McpFailure;
|
|
23
|
+
/** `kept`: a hippo block that is not the Copilot text (edited, or another agent's in a shared file); `unclosed`: a start marker with no end. */
|
|
24
|
+
export type InstructionsInstallStatus = 'written' | 'present' | 'kept' | 'unclosed';
|
|
25
|
+
/** Creates the file when missing; any block but hippo's own Copilot one, and a start marker with no end, leave the file as it is. */
|
|
26
|
+
export declare function ensureInstructionsBlock(file: string): InstructionsInstallStatus;
|
|
27
|
+
export type InstructionsRemoveStatus = 'removed' | 'absent' | 'kept' | 'unclosed';
|
|
28
|
+
/** Splices out only hippo's own Copilot block and keeps every other byte; a file left with nothing but whitespace goes. */
|
|
29
|
+
export declare function removeInstructionsBlock(file: string): InstructionsRemoveStatus;
|
|
30
|
+
export interface CopilotInstallResult {
|
|
31
|
+
readonly paths: CopilotPaths;
|
|
32
|
+
/** True when the hooks file was written; false when it already held hippo's current table. */
|
|
33
|
+
readonly hooks: boolean;
|
|
34
|
+
readonly mcp: McpMergeStatus | McpFailure;
|
|
35
|
+
readonly instructions: InstructionsInstallStatus;
|
|
36
|
+
}
|
|
37
|
+
export declare function installCopilot(): CopilotInstallResult;
|
|
38
|
+
export interface CopilotUninstallResult {
|
|
39
|
+
readonly paths: CopilotPaths;
|
|
40
|
+
readonly hooks: boolean;
|
|
41
|
+
readonly mcp: McpRemoveStatus | McpFailure;
|
|
42
|
+
readonly instructions: InstructionsRemoveStatus;
|
|
43
|
+
}
|
|
44
|
+
export declare function uninstallCopilot(): CopilotUninstallResult;
|
|
45
|
+
//# sourceMappingURL=copilot.d.ts.map
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// GitHub Copilot: hippo's hooks file, one MCP server key and an instructions block, all under the Copilot home folder.
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
5
|
+
import { readJsonFile } from '../json.js';
|
|
6
|
+
import { HOOK_MARKERS, hippoBlock } from '../cli/hook-blocks.js';
|
|
7
|
+
import { copilotHomeDir, isJsonObject } from './shared.js';
|
|
8
|
+
import { installJsonHooks, resolveJsonHookPaths, uninstallJsonHooks, writeSettingsFile } from './json-hooks.js';
|
|
9
|
+
const MCP_KEY = 'hippo';
|
|
10
|
+
/** Copilot drops what a per-prompt hook prints, so per-task recall goes through the MCP tool the block names. */
|
|
11
|
+
export const COPILOT_INSTRUCTIONS = `
|
|
12
|
+
## Project Memory (Hippo)
|
|
13
|
+
|
|
14
|
+
At the start of each task, call the \`hippo_recall\` tool with a short query that
|
|
15
|
+
names the task. Read the memories it returns before you write any code.
|
|
16
|
+
|
|
17
|
+
When you learn something that should outlive this session (a decision and its
|
|
18
|
+
reason, a user preference, why something failed), call the \`hippo_remember\`
|
|
19
|
+
tool right then, while you work, never as a closing step. Set \`error\` to true
|
|
20
|
+
for a failure. Leave out secrets and personal details.
|
|
21
|
+
|
|
22
|
+
The installed hooks load pinned memories when a session starts, store failed
|
|
23
|
+
tool calls and capture the session when it ends, so there is nothing to run
|
|
24
|
+
before you finish.
|
|
25
|
+
`.trim();
|
|
26
|
+
export function copilotPaths() {
|
|
27
|
+
const home = copilotHomeDir();
|
|
28
|
+
return {
|
|
29
|
+
hooks: resolveJsonHookPaths('copilot').settings,
|
|
30
|
+
mcpConfig: path.join(home, 'mcp-config.json'),
|
|
31
|
+
instructions: path.join(home, 'copilot-instructions.md'),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Windows gets npm's .cmd shim, as the Codex hooks do, since a PowerShell execution policy can block hippo.ps1. */
|
|
35
|
+
function hippoMcpServer() {
|
|
36
|
+
return { type: 'local', command: process.platform === 'win32' ? 'hippo.cmd' : 'hippo', args: ['mcp'], env: {}, tools: ['*'] };
|
|
37
|
+
}
|
|
38
|
+
/** Only a server hippo wrote counts as hippo's: a user's own "hippo" key with another command or args is never touched. */
|
|
39
|
+
function isHipposServer(entry) {
|
|
40
|
+
return isJsonObject(entry) && (entry.command === 'hippo' || entry.command === 'hippo.cmd') && isDeepStrictEqual(entry.args, ['mcp']);
|
|
41
|
+
}
|
|
42
|
+
/** What to paste under "mcpServers" when hippo cannot edit the file itself. */
|
|
43
|
+
export function copilotMcpSnippet() {
|
|
44
|
+
return `"${MCP_KEY}": ${JSON.stringify(hippoMcpServer())}`;
|
|
45
|
+
}
|
|
46
|
+
/** Null for a file with comments, one that is not JSON, or one whose shape a merge would overwrite; a missing file reads as empty. */
|
|
47
|
+
function readMcpConfig(file) {
|
|
48
|
+
let config = {};
|
|
49
|
+
if (fs.existsSync(file)) {
|
|
50
|
+
try {
|
|
51
|
+
config = readJsonFile(file);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
// Only a parse failure means the text is not JSON hippo can merge; EACCES or EISDIR is thrown with its own message.
|
|
55
|
+
if (err instanceof SyntaxError)
|
|
56
|
+
return null;
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!isJsonObject(config))
|
|
61
|
+
return null;
|
|
62
|
+
if (config.mcpServers === undefined)
|
|
63
|
+
config.mcpServers = {};
|
|
64
|
+
const servers = config.mcpServers;
|
|
65
|
+
return isJsonObject(servers) ? { config, servers } : null;
|
|
66
|
+
}
|
|
67
|
+
/** 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. */
|
|
68
|
+
export function mergeMcpServer(file) {
|
|
69
|
+
const read = readMcpConfig(file);
|
|
70
|
+
if (read === null)
|
|
71
|
+
return 'unreadable';
|
|
72
|
+
const existing = read.servers[MCP_KEY];
|
|
73
|
+
if (existing !== undefined)
|
|
74
|
+
return isHipposServer(existing) ? 'present' : 'user-owned';
|
|
75
|
+
read.servers[MCP_KEY] = hippoMcpServer();
|
|
76
|
+
writeSettingsFile(file, read.config);
|
|
77
|
+
return 'added';
|
|
78
|
+
}
|
|
79
|
+
/** Removes the "hippo" key only when hippo wrote it. */
|
|
80
|
+
export function removeMcpServer(file) {
|
|
81
|
+
if (!fs.existsSync(file))
|
|
82
|
+
return 'absent';
|
|
83
|
+
const read = readMcpConfig(file);
|
|
84
|
+
if (read === null)
|
|
85
|
+
return 'unreadable';
|
|
86
|
+
const existing = read.servers[MCP_KEY];
|
|
87
|
+
if (existing === undefined)
|
|
88
|
+
return 'absent';
|
|
89
|
+
if (!isHipposServer(existing))
|
|
90
|
+
return 'user-owned';
|
|
91
|
+
delete read.servers[MCP_KEY];
|
|
92
|
+
if (Object.keys(read.servers).length === 0)
|
|
93
|
+
delete read.config.mcpServers;
|
|
94
|
+
writeSettingsFile(file, read.config);
|
|
95
|
+
return 'removed';
|
|
96
|
+
}
|
|
97
|
+
export function isMcpFailure(status) {
|
|
98
|
+
return typeof status !== 'string';
|
|
99
|
+
}
|
|
100
|
+
function hasErrnoCode(err) {
|
|
101
|
+
return 'code' in err && typeof err.code === 'string';
|
|
102
|
+
}
|
|
103
|
+
/** One mcp-config.json step; a filesystem error comes back as a failure, so the hooks file and the block still install. */
|
|
104
|
+
function mcpStep(step) {
|
|
105
|
+
try {
|
|
106
|
+
return step();
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
if (err instanceof Error && hasErrnoCode(err))
|
|
110
|
+
return { failed: err.message };
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// A later release adds the hash of each earlier Copilot text here, as SHIPPED_HOOK_HASHES does for the other agents.
|
|
115
|
+
function isCopilotBlock(inner) {
|
|
116
|
+
return inner === COPILOT_INSTRUCTIONS;
|
|
117
|
+
}
|
|
118
|
+
/** Replaces only the text between the markers, in the file's own line ending, or appends a block after the user's text. */
|
|
119
|
+
function withCopilotBlock(text, found) {
|
|
120
|
+
if (found !== null) {
|
|
121
|
+
const { start, end, eol } = found;
|
|
122
|
+
return `${text.slice(0, start)}${eol}${COPILOT_INSTRUCTIONS.replace(/\n/g, eol)}${eol}${text.slice(end)}`;
|
|
123
|
+
}
|
|
124
|
+
const block = `${HOOK_MARKERS.start}\n${COPILOT_INSTRUCTIONS}\n${HOOK_MARKERS.end}\n`;
|
|
125
|
+
if (text === '')
|
|
126
|
+
return block;
|
|
127
|
+
return `${text}${text.endsWith('\n') ? '\n' : '\n\n'}${block}`;
|
|
128
|
+
}
|
|
129
|
+
/** Creates the file when missing; any block but hippo's own Copilot one, and a start marker with no end, leave the file as it is. */
|
|
130
|
+
export function ensureInstructionsBlock(file) {
|
|
131
|
+
const old = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
132
|
+
const found = hippoBlock(old);
|
|
133
|
+
if (found === null && old.includes(HOOK_MARKERS.start))
|
|
134
|
+
return 'unclosed';
|
|
135
|
+
if (found !== null && !isCopilotBlock(found.inner))
|
|
136
|
+
return 'kept';
|
|
137
|
+
const next = withCopilotBlock(old, found);
|
|
138
|
+
if (next === old)
|
|
139
|
+
return 'present';
|
|
140
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
141
|
+
fs.writeFileSync(file, next, 'utf8');
|
|
142
|
+
return 'written';
|
|
143
|
+
}
|
|
144
|
+
/** `text` without the block and its markers, plus the line break after the end marker, else the one before the start marker. */
|
|
145
|
+
function withoutCopilotBlock(text, found) {
|
|
146
|
+
const from = found.start - HOOK_MARKERS.start.length;
|
|
147
|
+
const to = found.end + HOOK_MARKERS.end.length;
|
|
148
|
+
const after = text.startsWith('\r\n', to) ? 2 : text.startsWith('\n', to) ? 1 : 0;
|
|
149
|
+
const before = after > 0 ? 0 : text.endsWith('\r\n', from) ? 2 : text.endsWith('\n', from) ? 1 : 0;
|
|
150
|
+
return text.slice(0, from - before) + text.slice(to + after);
|
|
151
|
+
}
|
|
152
|
+
/** Splices out only hippo's own Copilot block and keeps every other byte; a file left with nothing but whitespace goes. */
|
|
153
|
+
export function removeInstructionsBlock(file) {
|
|
154
|
+
if (!fs.existsSync(file))
|
|
155
|
+
return 'absent';
|
|
156
|
+
const old = fs.readFileSync(file, 'utf8');
|
|
157
|
+
const found = hippoBlock(old);
|
|
158
|
+
if (found === null)
|
|
159
|
+
return old.includes(HOOK_MARKERS.start) ? 'unclosed' : 'absent';
|
|
160
|
+
if (!isCopilotBlock(found.inner))
|
|
161
|
+
return 'kept';
|
|
162
|
+
const left = withoutCopilotBlock(old, found);
|
|
163
|
+
if (left.trim() === '')
|
|
164
|
+
fs.rmSync(file);
|
|
165
|
+
else
|
|
166
|
+
fs.writeFileSync(file, left, 'utf8');
|
|
167
|
+
return 'removed';
|
|
168
|
+
}
|
|
169
|
+
export function installCopilot() {
|
|
170
|
+
const paths = copilotPaths();
|
|
171
|
+
return {
|
|
172
|
+
paths,
|
|
173
|
+
hooks: installJsonHooks('copilot').installedSessionStart,
|
|
174
|
+
mcp: mcpStep(() => mergeMcpServer(paths.mcpConfig)),
|
|
175
|
+
instructions: ensureInstructionsBlock(paths.instructions),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export function uninstallCopilot() {
|
|
179
|
+
const paths = copilotPaths();
|
|
180
|
+
return {
|
|
181
|
+
paths,
|
|
182
|
+
hooks: uninstallJsonHooks('copilot'),
|
|
183
|
+
mcp: mcpStep(() => removeMcpServer(paths.mcpConfig)),
|
|
184
|
+
instructions: removeInstructionsBlock(paths.instructions),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=copilot.js.map
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* PreCompact and PostCompact entries go in too: the first records the compaction and
|
|
21
21
|
* asks the summariser for a "Memories for hippo" list, the second saves that list.
|
|
22
22
|
* Codex's hooks.json gets only two groups (per-prompt memory and
|
|
23
|
-
* compact-resume); see installCodexHooks.
|
|
23
|
+
* compact-resume); see installCodexHooks. Copilot gets a file of its own; see installCopilotHooks.
|
|
24
24
|
*
|
|
25
25
|
* 2. Plugin install (OpenCode only). OpenCode does NOT share Claude Code's
|
|
26
26
|
* JSON-hook schema — its config has `additionalProperties: false` and no
|
package/dist/hooks/json-hooks.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* PreCompact and PostCompact entries go in too: the first records the compaction and
|
|
21
21
|
* asks the summariser for a "Memories for hippo" list, the second saves that list.
|
|
22
22
|
* Codex's hooks.json gets only two groups (per-prompt memory and
|
|
23
|
-
* compact-resume); see installCodexHooks.
|
|
23
|
+
* compact-resume); see installCodexHooks. Copilot gets a file of its own; see installCopilotHooks.
|
|
24
24
|
*
|
|
25
25
|
* 2. Plugin install (OpenCode only). OpenCode does NOT share Claude Code's
|
|
26
26
|
* JSON-hook schema — its config has `additionalProperties: false` and no
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
import * as fs from 'fs';
|
|
37
37
|
import * as path from 'path';
|
|
38
|
-
import { isJsonObject, HIPPO_SLEEP_MARKER, HIPPO_LAST_SLEEP_MARKER, HIPPO_CAPTURE_MARKER, HIPPO_SESSION_END_MARKER, HIPPO_PINNED_INJECT_MARKER, HIPPO_PINNED_INJECT_COMMAND, HIPPO_PRE_COMPACT_MARKER, HIPPO_COMPACT_RESUME_MARKER, HIPPO_CAPTURE_ERROR_MARKER, HIPPO_POST_COMPACT_MARKER, homeDir, claudeConfigDir, codexHomeDir, defaultPreCompactLogPath } from './shared.js';
|
|
38
|
+
import { isJsonObject, HIPPO_SLEEP_MARKER, HIPPO_LAST_SLEEP_MARKER, HIPPO_CAPTURE_MARKER, HIPPO_SESSION_END_MARKER, HIPPO_PINNED_INJECT_MARKER, HIPPO_PINNED_INJECT_COMMAND, HIPPO_PRE_COMPACT_MARKER, HIPPO_COMPACT_RESUME_MARKER, HIPPO_CAPTURE_ERROR_MARKER, HIPPO_POST_COMPACT_MARKER, homeDir, claudeConfigDir, codexHomeDir, copilotHomeDir, defaultPreCompactLogPath } from './shared.js';
|
|
39
39
|
import { isJsonString, readJsonFile } from '../json.js';
|
|
40
40
|
import { escapeRegex } from '../escape.js';
|
|
41
41
|
import { errorMessage, log } from '../log.js';
|
|
@@ -56,6 +56,12 @@ export function resolveJsonHookPaths(target) {
|
|
|
56
56
|
logFile: path.join(logsDir, 'codex-sleep.log'),
|
|
57
57
|
display: 'Codex',
|
|
58
58
|
};
|
|
59
|
+
case 'copilot':
|
|
60
|
+
return {
|
|
61
|
+
settings: path.join(copilotHomeDir(), 'hooks', 'hippo.json'),
|
|
62
|
+
logFile: path.join(logsDir, 'copilot-sleep.log'),
|
|
63
|
+
display: 'Copilot',
|
|
64
|
+
};
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
// An unterminated quote runs to the end, as in a shell, which also keeps the scan linear.
|
|
@@ -196,12 +202,40 @@ function installCodexHooks(settingsPath, settings) {
|
|
|
196
202
|
writeSettingsFile(settingsPath, settings);
|
|
197
203
|
return { ...result, installedUserPromptSubmit, installedCompactResume };
|
|
198
204
|
}
|
|
205
|
+
/** One Copilot entry with a PowerShell twin: VS Code runs only `powershell` on Windows, where the execution policy can block npm's hippo.ps1. */
|
|
206
|
+
function copilotCommandHook(args, timeoutSec) {
|
|
207
|
+
return [{ type: 'command', bash: `hippo ${args}`, powershell: `hippo.cmd ${args}`, timeoutSec }];
|
|
208
|
+
}
|
|
209
|
+
/** No command names a path, so nothing has to be quoted for bash and PowerShell; session-end --runtime copilot picks its own log file. */
|
|
210
|
+
function copilotHooksTable() {
|
|
211
|
+
return {
|
|
212
|
+
version: 1,
|
|
213
|
+
hooks: {
|
|
214
|
+
sessionStart: copilotCommandHook('context --pinned-only --include-recent 5 --format copilot', 10),
|
|
215
|
+
postToolUseFailure: copilotCommandHook('capture-error --runtime copilot', 10),
|
|
216
|
+
preCompact: copilotCommandHook('pre-compact --runtime copilot', 30),
|
|
217
|
+
sessionEnd: copilotCommandHook('session-end --runtime copilot', 30),
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/** Hippo owns this whole file, so install writes the full table and a second install changes no byte. */
|
|
222
|
+
function installCopilotHooks(settingsPath) {
|
|
223
|
+
const result = nothingInstalled('copilot', settingsPath);
|
|
224
|
+
const table = copilotHooksTable();
|
|
225
|
+
const current = fs.existsSync(settingsPath) ? fs.readFileSync(settingsPath, 'utf8') : null;
|
|
226
|
+
if (current === JSON.stringify(table, null, 2) + '\n')
|
|
227
|
+
return result;
|
|
228
|
+
writeSettingsFile(settingsPath, table);
|
|
229
|
+
return { ...result, installedSessionStart: true, installedCaptureError: true, installedPreCompact: true, installedSessionEnd: true };
|
|
230
|
+
}
|
|
199
231
|
/** Adds hippo's hooks to `target`'s settings file and migrates older hippo entries; a file that is not JSON stays untouched. */
|
|
200
232
|
export function installJsonHooks(target) {
|
|
201
233
|
const { settings: settingsPath, logFile } = resolveJsonHookPaths(target);
|
|
202
234
|
const dir = path.dirname(settingsPath);
|
|
203
235
|
if (!fs.existsSync(dir))
|
|
204
236
|
fs.mkdirSync(dir, { recursive: true });
|
|
237
|
+
if (target === 'copilot')
|
|
238
|
+
return installCopilotHooks(settingsPath);
|
|
205
239
|
let settings = {};
|
|
206
240
|
if (fs.existsSync(settingsPath)) {
|
|
207
241
|
try {
|
|
@@ -443,6 +477,11 @@ export function uninstallJsonHooks(target) {
|
|
|
443
477
|
const { settings: settingsPath } = resolveJsonHookPaths(target);
|
|
444
478
|
if (!fs.existsSync(settingsPath))
|
|
445
479
|
return false;
|
|
480
|
+
if (target === 'copilot') {
|
|
481
|
+
// Copilot reads every file in its hooks folder, so hippo's own file goes whole and the user's files stay.
|
|
482
|
+
fs.rmSync(settingsPath, { force: true });
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
446
485
|
let settings;
|
|
447
486
|
try {
|
|
448
487
|
settings = readJsonFile(settingsPath);
|
package/dist/hooks/shared.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { JsonObject } from '../working-memory.js';
|
|
|
2
2
|
import type { JsonValue } from '../json.js';
|
|
3
3
|
/** JSON-value plain-object check (excludes arrays and null); `constructor` avoids the `typeof` tag banned by anti-slop/no-runtime-typeof. */
|
|
4
4
|
export declare function isJsonObject(value: JsonValue | undefined): value is JsonObject;
|
|
5
|
-
export type JsonHookTarget = 'claude-code' | 'codex';
|
|
5
|
+
export type JsonHookTarget = 'claude-code' | 'codex' | 'copilot';
|
|
6
6
|
export interface ToolDetection {
|
|
7
7
|
name: string;
|
|
8
8
|
configDir: string;
|
|
@@ -24,10 +24,14 @@ export declare const HIPPO_POST_COMPACT_MARKER = "hippo post-compact";
|
|
|
24
24
|
export declare function homeDir(): string;
|
|
25
25
|
/** Codex's config folder: $CODEX_HOME, else ~/.codex, as the Codex hooks docs describe. */
|
|
26
26
|
export declare function codexHomeDir(home?: string, env?: Readonly<Record<string, string | undefined>>): string;
|
|
27
|
+
/** Copilot's config folder: $COPILOT_HOME, else ~/.copilot under os.homedir(), as the Copilot apps resolve it (a HOME that differs from the profile must not move it). */
|
|
28
|
+
export declare function copilotHomeDir(home?: string, env?: Readonly<Record<string, string | undefined>>): string;
|
|
27
29
|
/** Claude Code's config folder, where it reads settings.json: $CLAUDE_CONFIG_DIR when set and non-empty, else ~/.claude under os.homedir(), as Claude Code does (a HOME that differs from the profile must not move it). */
|
|
28
30
|
export declare function claudeConfigDir(home?: string, env?: Readonly<Record<string, string | undefined>>): string;
|
|
29
31
|
/** Codex counts as installed only when its config folder exists: Codex itself refuses a CODEX_HOME that is not a folder. */
|
|
30
32
|
export declare function isCodexPresent(home?: string): boolean;
|
|
33
|
+
/** Copilot counts as installed only when its config folder exists, so setup never creates ~/.copilot on a machine without it. */
|
|
34
|
+
export declare function isCopilotPresent(home?: string): boolean;
|
|
31
35
|
/** Codex hashes each hook and skips new or changed ones until the user reviews them in `/hooks`, so the reminder says what they would trust. */
|
|
32
36
|
export declare const CODEX_TRUST_LINE = "The per-prompt hook sends your pinned memories plus up to 5 that match the prompt. Codex runs hippo's hooks only after you trust them once in `/hooks`.";
|
|
33
37
|
/**
|
package/dist/hooks/shared.js
CHANGED
|
@@ -24,6 +24,10 @@ export function homeDir() {
|
|
|
24
24
|
export function codexHomeDir(home = homeDir(), env = processEnv()) {
|
|
25
25
|
return env.CODEX_HOME || path.join(home, '.codex');
|
|
26
26
|
}
|
|
27
|
+
/** Copilot's config folder: $COPILOT_HOME, else ~/.copilot under os.homedir(), as the Copilot apps resolve it (a HOME that differs from the profile must not move it). */
|
|
28
|
+
export function copilotHomeDir(home = os.homedir(), env = processEnv()) {
|
|
29
|
+
return env.COPILOT_HOME || path.join(home, '.copilot');
|
|
30
|
+
}
|
|
27
31
|
/** Claude Code's config folder, where it reads settings.json: $CLAUDE_CONFIG_DIR when set and non-empty, else ~/.claude under os.homedir(), as Claude Code does (a HOME that differs from the profile must not move it). */
|
|
28
32
|
export function claudeConfigDir(home = os.homedir(), env = processEnv()) {
|
|
29
33
|
return env.CLAUDE_CONFIG_DIR || path.join(home, '.claude');
|
|
@@ -32,6 +36,10 @@ export function claudeConfigDir(home = os.homedir(), env = processEnv()) {
|
|
|
32
36
|
export function isCodexPresent(home = homeDir()) {
|
|
33
37
|
return fs.statSync(codexHomeDir(home), { throwIfNoEntry: false })?.isDirectory() === true;
|
|
34
38
|
}
|
|
39
|
+
/** Copilot counts as installed only when its config folder exists, so setup never creates ~/.copilot on a machine without it. */
|
|
40
|
+
export function isCopilotPresent(home = os.homedir()) {
|
|
41
|
+
return fs.statSync(copilotHomeDir(home), { throwIfNoEntry: false })?.isDirectory() === true;
|
|
42
|
+
}
|
|
35
43
|
/** Codex hashes each hook and skips new or changed ones until the user reviews them in `/hooks`, so the reminder says what they would trust. */
|
|
36
44
|
export const CODEX_TRUST_LINE = "The per-prompt hook sends your pinned memories plus up to 5 that match the prompt. Codex runs hippo's hooks only after you trust them once in `/hooks`.";
|
|
37
45
|
/**
|
|
@@ -68,6 +76,7 @@ export function detectInstalledTools() {
|
|
|
68
76
|
{ name: 'opencode', configDir: '~/.config/opencode', detected: exists('.config', 'opencode'), kind: 'plugin', notes: 'installs a TS plugin at ~/.config/opencode/plugins/hippo.ts' },
|
|
69
77
|
{ name: 'openclaw', configDir: '~/.openclaw', detected: exists('.openclaw'), kind: 'plugin', notes: 'install via `openclaw plugins install hippo-memory`' },
|
|
70
78
|
{ name: 'codex', configDir: '~/.codex', detected: isCodexPresent(home), kind: 'wrapper', notes: 'memory hooks in hooks.json, and wraps the detected codex launcher for session-end consolidation' },
|
|
79
|
+
{ name: 'copilot', configDir: copilotHomeDir(), detected: isCopilotPresent(), kind: 'json-hook', notes: 'hooks in hooks/hippo.json, the MCP server in mcp-config.json and a block in copilot-instructions.md' },
|
|
71
80
|
{ name: 'cursor', configDir: '~/.cursor', detected: exists('.cursor'), kind: 'markdown-instruction', notes: 'no hook API - patches AGENTS.md in the project' },
|
|
72
81
|
{ name: 'pi', configDir: '~/.pi', detected: exists('.pi'), kind: 'markdown-instruction', notes: 'no hook API - patches AGENTS.md in the project' },
|
|
73
82
|
];
|
package/dist/mcp/request.d.ts
CHANGED
|
@@ -4,8 +4,6 @@ import { type McpContext, type McpRequest, type McpResponse } from './protocol.j
|
|
|
4
4
|
* ledger failure never fails the tool call. Other tools are not recorded.
|
|
5
5
|
*/
|
|
6
6
|
export declare function recordMcpTokens(toolName: string, output: string, ctx?: McpContext): Promise<void>;
|
|
7
|
-
/** The tools listed and run under a store other than hippo.db. */
|
|
8
|
-
export declare const STORE_READY_TOOLS: ReadonlySet<string>;
|
|
9
7
|
/**
|
|
10
8
|
* Transport-agnostic MCP dispatcher. Both the stdio loop (below) and the
|
|
11
9
|
* HTTP/SSE transport in src/server.ts route every incoming JSON-RPC message
|
package/dist/mcp/request.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getGlobalRoot, initGlobal } from '../shared.js';
|
|
|
5
5
|
import { loadConfig } from '../config.js';
|
|
6
6
|
import { resolveTenantId } from '../tenant.js';
|
|
7
7
|
import { rethrowIfSqliteBlocked, runWithRequestStores } from '../db.js';
|
|
8
|
-
import { storeFor } from '../store-port.js';
|
|
8
|
+
import { hasGroup, storeFor } from '../store-port.js';
|
|
9
9
|
import { estimateTokens } from '../token-ledger.js';
|
|
10
10
|
import { PACKAGE_VERSION } from '../version.js';
|
|
11
11
|
import { validateToolArgs } from './tool-args.js';
|
|
@@ -60,7 +60,7 @@ export async function recordMcpTokens(toolName, output, ctx) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
const TOOL_HANDLERS = new Map([
|
|
63
|
-
['hippo_recall', { handler: runRecallTool, storeReady:
|
|
63
|
+
['hippo_recall', { handler: runRecallTool, storeReady: 'base' }],
|
|
64
64
|
['hippo_assemble', { handler: runAssembleTool }],
|
|
65
65
|
['hippo_drill', { handler: runDrillTool }],
|
|
66
66
|
['hippo_predict_baserate', { handler: runPredictBaserateTool }],
|
|
@@ -74,12 +74,14 @@ const TOOL_HANDLERS = new Map([
|
|
|
74
74
|
['hippo_share', { handler: runShareTool }],
|
|
75
75
|
['hippo_peers', { handler: runPeersTool }],
|
|
76
76
|
]);
|
|
77
|
-
/** The
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
/** The served store when it is not hippo.db, else null. */
|
|
78
|
+
function otherStore(ctx) {
|
|
79
|
+
const store = ctx?.store;
|
|
80
|
+
return store !== undefined && store.kind !== 'sqlite' ? store : null;
|
|
81
|
+
}
|
|
82
|
+
function runsOn(store, toolName) {
|
|
83
|
+
const group = TOOL_HANDLERS.get(toolName)?.storeReady;
|
|
84
|
+
return group !== undefined && hasGroup(store, group);
|
|
83
85
|
}
|
|
84
86
|
async function executeTool(name, args, ctx) {
|
|
85
87
|
// When a transport hands us a context (HTTP path), trust it: the HTTP
|
|
@@ -130,8 +132,10 @@ export async function handleMcpRequest(req, ctx) {
|
|
|
130
132
|
};
|
|
131
133
|
case 'notifications/initialized':
|
|
132
134
|
return null;
|
|
133
|
-
case 'tools/list':
|
|
134
|
-
|
|
135
|
+
case 'tools/list': {
|
|
136
|
+
const other = otherStore(ctx);
|
|
137
|
+
return { jsonrpc: '2.0', id, result: { tools: other ? TOOLS.filter((t) => runsOn(other, t.name)) : TOOLS } };
|
|
138
|
+
}
|
|
135
139
|
case 'tools/call': {
|
|
136
140
|
const nameValue = params?.name;
|
|
137
141
|
const toolName = isJsonString(nameValue) ? nameValue : '';
|
|
@@ -140,7 +144,8 @@ export async function handleMcpRequest(req, ctx) {
|
|
|
140
144
|
return { jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${toolName.slice(0, 128)}` } };
|
|
141
145
|
}
|
|
142
146
|
// The same refusal a ported tool gives when it reaches hippo.db, so a client handles one shape.
|
|
143
|
-
|
|
147
|
+
const other = otherStore(ctx);
|
|
148
|
+
if (other && !runsOn(other, toolName)) {
|
|
144
149
|
return { jsonrpc: '2.0', id, error: { code: -32603, message: STORE_NOT_PORTED_MESSAGE } };
|
|
145
150
|
}
|
|
146
151
|
const refusal = sharedStoreRefusal(toolName, ctx);
|
package/dist/prompt-hook.d.ts
CHANGED
|
@@ -21,7 +21,13 @@ export interface ContextView {
|
|
|
21
21
|
/** The static block's hash the caller says it printed last. Undefined keeps the ledger-only skip rule;
|
|
22
22
|
* a string or null also needs that hash to match, because the ledger counts blocks a caller that prints elsewhere may never have shown. */
|
|
23
23
|
readonly printedHash?: string | null;
|
|
24
|
+
/** The reply's JSON wrapper; absent means Claude Code's UserPromptSubmit. */
|
|
25
|
+
readonly envelope?: HookEnvelope;
|
|
24
26
|
}
|
|
27
|
+
/** `copilot-session-start` is the Copilot harness's top-level reply, `vscode-session-start` the nested one VS Code Local reads. */
|
|
28
|
+
export type HookEnvelope = 'user-prompt-submit' | 'copilot-session-start' | 'vscode-session-start';
|
|
29
|
+
/** A Copilot sessionStart reply's wrapper, picked by the sender's casing: stdin.ts keeps a camelCase `sessionId`, which only the Copilot harness sends. */
|
|
30
|
+
export declare function sessionStartEnvelope(stdinText: string | undefined): HookEnvelope;
|
|
25
31
|
export declare function hasContextData(result: ContextResult): boolean;
|
|
26
32
|
export type RenderItem = {
|
|
27
33
|
entry: MemoryEntry;
|
package/dist/prompt-hook.js
CHANGED
|
@@ -11,7 +11,7 @@ import { withLedgerDb } from './ledger-db.js';
|
|
|
11
11
|
import { sessionPilotArm } from './pilot-arm.js';
|
|
12
12
|
import { assertCallerProject, MAX_PROJECT_ALIASES } from './project-identity.js';
|
|
13
13
|
import { writeDeliveryEventAtRoot, writeDeliveryEventOnHandle } from './recall-trace.js';
|
|
14
|
-
import { blockHash, estimateTokens, isSubagentPayload, lastSentState, recordTokenUse, shouldSkipUnchanged } from './token-ledger.js';
|
|
14
|
+
import { blockHash, estimateTokens, hookPayloadString, isSubagentPayload, lastSentState, recordTokenUse, shouldSkipUnchanged, } from './token-ledger.js';
|
|
15
15
|
/** With `db`, writes on the token ledger's handle (same store); without it, opens its own. A second flush is a no-op. */
|
|
16
16
|
export function flushDeliveryRecorder(rec, db) {
|
|
17
17
|
if (rec === null)
|
|
@@ -24,6 +24,16 @@ export function flushDeliveryRecorder(rec, db) {
|
|
|
24
24
|
console.error(`[hippo] delivery ledger write failed:${error instanceof Error ? error.message : String(error)}`);
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
/** A Copilot sessionStart reply's wrapper, picked by the sender's casing: stdin.ts keeps a camelCase `sessionId`, which only the Copilot harness sends. */
|
|
28
|
+
export function sessionStartEnvelope(stdinText) {
|
|
29
|
+
return hookPayloadString(stdinText, 'sessionId') !== null ? 'copilot-session-start' : 'vscode-session-start';
|
|
30
|
+
}
|
|
31
|
+
function hookReply(envelope, additionalContext) {
|
|
32
|
+
if (envelope === 'copilot-session-start')
|
|
33
|
+
return JSON.stringify({ additionalContext });
|
|
34
|
+
const hookEventName = envelope === 'vscode-session-start' ? 'SessionStart' : 'UserPromptSubmit';
|
|
35
|
+
return JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } });
|
|
36
|
+
}
|
|
27
37
|
const NOTHING_RENDERED = Object.freeze({ stdout: '', staticHash: null });
|
|
28
38
|
export function hasContextData(result) {
|
|
29
39
|
return Boolean(result.entries.length > 0 ||
|
|
@@ -70,12 +80,7 @@ function renderAdditionalContext(view) {
|
|
|
70
80
|
rec?.delivered({ state: 'reused', staticHash: blockHash(staticBlock), staticReused });
|
|
71
81
|
return NOTHING_RENDERED;
|
|
72
82
|
}
|
|
73
|
-
const stdout =
|
|
74
|
-
hookSpecificOutput: {
|
|
75
|
-
hookEventName: 'UserPromptSubmit',
|
|
76
|
-
additionalContext,
|
|
77
|
-
},
|
|
78
|
-
});
|
|
83
|
+
const stdout = hookReply(view.envelope ?? 'user-prompt-submit', additionalContext);
|
|
79
84
|
rec?.delivered({
|
|
80
85
|
state: staticReused ? 'reused-recall-sent' : 'sent',
|
|
81
86
|
staticHash: staticBlock.trim() ? blockHash(staticBlock) : null,
|
|
@@ -94,6 +99,9 @@ function skipUnchangedStatic(view, surface, staticBlock, recallBlock, staticCoun
|
|
|
94
99
|
const ledgerOpts = { sharedStore: view.sharedStore };
|
|
95
100
|
if (!view.pinnedOnly || payloadSessionId === undefined)
|
|
96
101
|
return false;
|
|
102
|
+
// Session start is the only injection Copilot gets, so a resumed session needs the whole block again.
|
|
103
|
+
if (view.envelope === 'copilot-session-start' || view.envelope === 'vscode-session-start')
|
|
104
|
+
return false;
|
|
97
105
|
const injectCfg = loadConfig(hippoRoot).pinnedInject;
|
|
98
106
|
if (injectCfg.skipUnchanged === false)
|
|
99
107
|
return false;
|
package/dist/quality-repair.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { appendAuditEvent } from './audit.js';
|
|
4
4
|
import { withBackup } from './db/backup.js';
|
|
5
|
+
import { assertSqliteAllowed } from './db/open.js';
|
|
5
6
|
import { DatabaseSync } from './db/sqlite.js';
|
|
6
7
|
import { getMeta, setMeta } from './db/meta.js';
|
|
7
8
|
import { tableColumns } from './db/tables.js';
|
|
@@ -187,6 +188,8 @@ function repairOn(db, root, opts) {
|
|
|
187
188
|
return { ...result, warnings };
|
|
188
189
|
}
|
|
189
190
|
function openForRepair(root, readOnly, fn) {
|
|
191
|
+
// Opens without openHippoDb to skip migrations, so it takes the same refusal itself.
|
|
192
|
+
assertSqliteAllowed(root);
|
|
190
193
|
const file = path.join(root, 'hippo.db');
|
|
191
194
|
if (!fs.existsSync(file))
|
|
192
195
|
throw new Error(`No existing Hippo database at ${file}`);
|
package/dist/search/hybrid.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type BM25Corpus } from './bm25.js';
|
|
|
4
4
|
import { type HybridVectorCandidates } from './vector.js';
|
|
5
5
|
import { type GraphStreamOptions } from './fusion.js';
|
|
6
6
|
import { type ResultCost, type SearchResult } from './types.js';
|
|
7
|
+
import type { HippoStore } from '../store-port.js';
|
|
7
8
|
export interface HybridSearchOptions {
|
|
8
9
|
budget?: number;
|
|
9
10
|
now?: Date;
|
|
@@ -40,6 +41,8 @@ export interface HybridSearchOptions {
|
|
|
40
41
|
graphStream?: GraphStreamOptions;
|
|
41
42
|
/** Add the rows nearest the query vector, not only rescore `entries`; without it a row no query word matches cannot surface. */
|
|
42
43
|
vectorCandidates?: HybridVectorCandidates;
|
|
44
|
+
/** Where vectors are read; hippo.db under `hippoRoot` when unset. */
|
|
45
|
+
store?: HippoStore;
|
|
43
46
|
}
|
|
44
47
|
/** BM25 blended with cosine similarity when stored vectors and a provider are available, BM25 * strength * recency otherwise. */
|
|
45
48
|
export declare function hybridSearch(query: string, entries: MemoryEntry[], options?: HybridSearchOptions): Promise<SearchResult[]>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type MemoryEntry } from '../memory.js';
|
|
2
2
|
import { type PhysicsConfig } from '../physics-config.js';
|
|
3
|
+
import { type HippoStore } from '../store-port.js';
|
|
3
4
|
import { type HybridVectorCandidates } from './vector.js';
|
|
4
5
|
import { type ResultCost, type SearchResult } from './types.js';
|
|
5
6
|
export interface PhysicsSearchOptions {
|
|
@@ -23,6 +24,8 @@ export interface PhysicsSearchOptions {
|
|
|
23
24
|
summaryFreshness?: boolean;
|
|
24
25
|
/** Same as hybridSearch's: rows nearest the query join the pool before physics scoring. */
|
|
25
26
|
vectorCandidates?: HybridVectorCandidates;
|
|
27
|
+
/** Where vectors and particles are read; hippo.db under `hippoRoot` when unset. */
|
|
28
|
+
store?: HippoStore;
|
|
26
29
|
}
|
|
27
30
|
/** Scores memories by gravity, momentum and cluster amplification; rows without physics state rank through hybridSearch. */
|
|
28
31
|
export declare function physicsSearch(query: string, entries: MemoryEntry[], options?: PhysicsSearchOptions): Promise<SearchResult[]>;
|