hippo-memory 1.66.0 → 1.68.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 +16 -11
- package/dist/api/auth.js +117 -61
- package/dist/api/types.d.ts +9 -0
- package/dist/auth.d.ts +22 -1
- package/dist/auth.js +20 -8
- 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/sqlite-blocked.js +1 -1
- package/dist/delivery-recorder.d.ts +3 -1
- package/dist/delivery-recorder.js +1 -1
- package/dist/embeddings.d.ts +10 -10
- package/dist/embeddings.js +103 -18
- 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/search/vector.js +2 -5
- package/dist/server/routes/admin.js +3 -3
- package/dist/server/types.d.ts +5 -4
- package/dist/server.d.ts +11 -8
- package/dist/server.js +20 -19
- 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/key-writes.d.ts +9 -0
- package/dist/store/key-writes.js +30 -0
- package/dist/store/vector-writes.d.ts +6 -0
- package/dist/store/vector-writes.js +59 -0
- package/dist/store-port.d.ts +101 -9
- package/dist/store-port.js +53 -2
- 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.d.ts
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Falls back silently if the library is not installed.
|
|
5
5
|
*/
|
|
6
6
|
import { MemoryEntry } from './memory.js';
|
|
7
|
+
import { type DatabaseSyncLike } from './db.js';
|
|
7
8
|
import { EMBEDDING_MODEL_META_KEY } from './vector-store.js';
|
|
8
9
|
import { type EmbeddingProvider } from './embedding-provider.js';
|
|
10
|
+
import type { HippoStore } from './store-port.js';
|
|
9
11
|
export { EMBEDDING_MODEL_META_KEY };
|
|
10
12
|
/**
|
|
11
13
|
* Bump whenever `embeddingInputText`'s composition changes in a way that
|
|
@@ -73,10 +75,13 @@ export interface EmbeddingIndexState {
|
|
|
73
75
|
export declare function indexedModel(state: EmbeddingIndexState): string | null;
|
|
74
76
|
/** The one rebuild rule every store applies to its indexed model. */
|
|
75
77
|
export declare function indexNeedsRebuild(indexed: string | null, providerId: string): boolean;
|
|
78
|
+
/** Whether a vector write under the index identity `model` first drops the stored index, which another model built. */
|
|
79
|
+
export declare function replacesIndex(state: EmbeddingIndexState, model: string): boolean;
|
|
76
80
|
export declare function resolveIndexedEmbeddingModel(hippoRoot: string, index?: Record<string, number[]>): string | null;
|
|
77
81
|
export declare function embeddingModelRequiresReindex(hippoRoot: string, model: string, index?: Record<string, number[]>): boolean;
|
|
78
82
|
/** hippo.db's index state, the meta row and the EXISTS on one handle. */
|
|
79
83
|
export declare function embeddingIndexStateAt(hippoRoot: string): EmbeddingIndexState;
|
|
84
|
+
export declare function embeddingIndexStateOn(db: DatabaseSyncLike): EmbeddingIndexState;
|
|
80
85
|
/**
|
|
81
86
|
* Cosine similarity between two vectors. Handles unnormalized vectors.
|
|
82
87
|
* Returns 0 for empty or mismatched vectors.
|
|
@@ -88,14 +93,9 @@ export declare function loadEmbeddingIndex(hippoRoot: string): Record<string, nu
|
|
|
88
93
|
export declare function loadStoredVectors(hippoRoot: string, ids: readonly string[]): Map<string, number[]>;
|
|
89
94
|
/** Replace every stored vector with `index`; `model` defaults to the stored index identity. */
|
|
90
95
|
export declare function saveEmbeddingIndex(hippoRoot: string, index: Record<string, number[]>, model?: string): void;
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
* Embed all entries in hippoRoot that don't already have cached vectors.
|
|
97
|
-
* Prunes orphaned embeddings for memories that no longer exist.
|
|
98
|
-
* Returns the count of newly embedded entries. `provider` defaults to the store's configured one.
|
|
99
|
-
*/
|
|
100
|
-
export declare function embedAll(hippoRoot: string, model?: string, provider?: EmbeddingProvider): Promise<number>;
|
|
96
|
+
/** Embed a single memory entry and cache the result in the embedding index; with `store`, through its vectorWrites, never opening hippo.db. */
|
|
97
|
+
export declare function embedMemory(hippoRoot: string, entry: MemoryEntry, model?: string, store?: HippoStore): Promise<void>;
|
|
98
|
+
/** Embeds every entry with no cached vector, prunes vectors of deleted memories, and returns how many it embedded; `provider` defaults to the
|
|
99
|
+
* configured one. With `store`, every tenant's memories with no vector for the provider's model go through its `vectorWrites`, never hippo.db. */
|
|
100
|
+
export declare function embedAll(hippoRoot: string, model?: string, provider?: EmbeddingProvider, store?: HippoStore): Promise<number>;
|
|
101
101
|
//# sourceMappingURL=embeddings.d.ts.map
|
package/dist/embeddings.js
CHANGED
|
@@ -16,6 +16,7 @@ import { resolveEmbeddingProvider } from './embedding-provider.js';
|
|
|
16
16
|
import { DEFAULT_EMBEDDING_MODEL } from './local-embedding.js';
|
|
17
17
|
import { redactSecretsStrict } from './secret-detect.js';
|
|
18
18
|
import { log } from './log.js';
|
|
19
|
+
import { StoreNotPortedError } from './db/sqlite-blocked.js';
|
|
19
20
|
export { EMBEDDING_MODEL_META_KEY };
|
|
20
21
|
/**
|
|
21
22
|
* Bump whenever `embeddingInputText`'s composition changes in a way that
|
|
@@ -109,6 +110,11 @@ export function indexedModel(state) {
|
|
|
109
110
|
export function indexNeedsRebuild(indexed, providerId) {
|
|
110
111
|
return indexed !== null && indexed !== embeddingIndexIdentity(providerId);
|
|
111
112
|
}
|
|
113
|
+
/** Whether a vector write under the index identity `model` first drops the stored index, which another model built. */
|
|
114
|
+
export function replacesIndex(state, model) {
|
|
115
|
+
const indexed = indexedModel(state);
|
|
116
|
+
return indexed !== null && indexed !== model;
|
|
117
|
+
}
|
|
112
118
|
export function resolveIndexedEmbeddingModel(hippoRoot, index) {
|
|
113
119
|
const storedModel = loadStoredEmbeddingModel(hippoRoot);
|
|
114
120
|
if (storedModel)
|
|
@@ -120,10 +126,13 @@ export function embeddingModelRequiresReindex(hippoRoot, model, index) {
|
|
|
120
126
|
}
|
|
121
127
|
/** hippo.db's index state, the meta row and the EXISTS on one handle. */
|
|
122
128
|
export function embeddingIndexStateAt(hippoRoot) {
|
|
123
|
-
return withVectorDb(hippoRoot,
|
|
129
|
+
return withVectorDb(hippoRoot, embeddingIndexStateOn);
|
|
130
|
+
}
|
|
131
|
+
export function embeddingIndexStateOn(db) {
|
|
132
|
+
return {
|
|
124
133
|
storedModel: getMeta(db, EMBEDDING_MODEL_META_KEY, '').trim() || null,
|
|
125
134
|
hasVectors: hasStoredVectors(db),
|
|
126
|
-
}
|
|
135
|
+
};
|
|
127
136
|
}
|
|
128
137
|
async function rebuildEmbeddingIndex(entries, provider) {
|
|
129
138
|
const rebuilt = {};
|
|
@@ -273,12 +282,7 @@ async function acquireEmbedFileLock(hippoRoot) {
|
|
|
273
282
|
}
|
|
274
283
|
}
|
|
275
284
|
async function withEmbedLock(hippoRoot, fn) {
|
|
276
|
-
|
|
277
|
-
const next = new Promise(r => { resolve = r; });
|
|
278
|
-
const prev = _embedWriteLock;
|
|
279
|
-
_embedWriteLock = next;
|
|
280
|
-
await prev;
|
|
281
|
-
try {
|
|
285
|
+
return withProcessEmbedLock(async () => {
|
|
282
286
|
const release = await acquireEmbedFileLock(hippoRoot);
|
|
283
287
|
try {
|
|
284
288
|
return await fn();
|
|
@@ -286,6 +290,17 @@ async function withEmbedLock(hippoRoot, fn) {
|
|
|
286
290
|
finally {
|
|
287
291
|
release();
|
|
288
292
|
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
// A store keeps each write whole in its own transaction, and a lock file in hippoRoot would not reach a worker on another host.
|
|
296
|
+
async function withProcessEmbedLock(fn) {
|
|
297
|
+
let resolve;
|
|
298
|
+
const next = new Promise(r => { resolve = r; });
|
|
299
|
+
const prev = _embedWriteLock;
|
|
300
|
+
_embedWriteLock = next;
|
|
301
|
+
await prev;
|
|
302
|
+
try {
|
|
303
|
+
return await fn();
|
|
289
304
|
}
|
|
290
305
|
finally {
|
|
291
306
|
resolve();
|
|
@@ -301,10 +316,79 @@ function warnEmbedFailureOnce(source, rawMessage) {
|
|
|
301
316
|
const message = redactSecretsStrict(rawMessage).replace(/\s+/g, ' ').replace(/\.+$/, '');
|
|
302
317
|
log.warn(`embedding failed (${source}): ${message}. Memories are stored without embeddings until this is fixed.`);
|
|
303
318
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
319
|
+
// store-port.ts imports this module, so its requireGroup would close an import cycle.
|
|
320
|
+
function vectorGroups(store) {
|
|
321
|
+
if (store.vectors === undefined)
|
|
322
|
+
throw new StoreNotPortedError(store.kind, 'vectors');
|
|
323
|
+
if (store.vectorWrites === undefined)
|
|
324
|
+
throw new StoreNotPortedError(store.kind, 'vectorWrites');
|
|
325
|
+
return [store.vectors, store.vectorWrites];
|
|
326
|
+
}
|
|
327
|
+
/** Writes one page's vectors, one writeVectors per tenant, and stops at the first refused write; `withParticles` seeds each memory's particle. */
|
|
328
|
+
async function writeVectorPage(writes, write, page, vectors, withParticles) {
|
|
329
|
+
const byTenant = new Map();
|
|
330
|
+
page.forEach((entry, i) => {
|
|
331
|
+
const vector = vectors[i];
|
|
332
|
+
if (!vector || vector.length === 0)
|
|
333
|
+
return noteSkippedEmbedding(entry.id);
|
|
334
|
+
const row = withParticles ? { memoryId: entry.id, vector, particle: initializeParticle(entry, vector) } : { memoryId: entry.id, vector };
|
|
335
|
+
byTenant.set(entry.tenantId, [...(byTenant.get(entry.tenantId) ?? []), row]);
|
|
336
|
+
});
|
|
337
|
+
let written = 0;
|
|
338
|
+
for (const [tenantId, rows] of byTenant) {
|
|
339
|
+
const result = await writes.writeVectors({ ...write, tenantId, rows });
|
|
340
|
+
if (result.modelMismatch)
|
|
341
|
+
return { written, modelMismatch: true };
|
|
342
|
+
written += result.written;
|
|
343
|
+
}
|
|
344
|
+
return { written, modelMismatch: false };
|
|
345
|
+
}
|
|
346
|
+
const STORE_PAGE = 64;
|
|
347
|
+
const OTHER_MODEL_INDEX = "the vector index was built by another embedding model; run 'hippo embed' to rebuild it";
|
|
348
|
+
/** Embeds every memory with no vector under `model`, page by page, so a provider failure keeps the pages written; a rebuild reseeds the particles it drops. */
|
|
349
|
+
async function backfillInStore(writes, provider, model, rebuild) {
|
|
350
|
+
let count = 0;
|
|
351
|
+
let afterId;
|
|
352
|
+
for (;;) {
|
|
353
|
+
const page = await writes.entriesWithoutVector({ model, afterId, limit: STORE_PAGE });
|
|
354
|
+
if (page.length === 0)
|
|
355
|
+
return count;
|
|
356
|
+
afterId = page[page.length - 1].id;
|
|
357
|
+
const vectors = await provider.embed(page.map((e) => embeddingInputText(e)), 'passage');
|
|
358
|
+
const result = await writeVectorPage(writes, { model, replaceIndex: rebuild }, page, vectors, rebuild);
|
|
359
|
+
// Another process rebuilt the index mid-run; every later page would be refused too, after paying the provider for it.
|
|
360
|
+
if (result.modelMismatch)
|
|
361
|
+
throw new Error(OTHER_MODEL_INDEX);
|
|
362
|
+
count += result.written;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/** embedAll on a store: a model change re-embeds every memory, and the first page written drops the old index. */
|
|
366
|
+
async function embedAllInStore(store, provider) {
|
|
367
|
+
const [reads, writes] = vectorGroups(store);
|
|
368
|
+
const rebuild = indexNeedsRebuild(indexedModel(await reads.embeddingIndexState()), provider.id);
|
|
369
|
+
return backfillInStore(writes, provider, embeddingIndexIdentity(provider.id), rebuild);
|
|
370
|
+
}
|
|
371
|
+
/** embedMemory on a store; like the hippo.db path it resolves whatever fails. Unlike it, it leaves a model change to embedAll, so a stale
|
|
372
|
+
* read here never drops another process's rebuild; the read only saves a paid embed call, as the write refuses another model itself. */
|
|
373
|
+
function embedMemoryInStore(store, provider, entry) {
|
|
374
|
+
return withProcessEmbedLock(async () => {
|
|
375
|
+
const [reads, writes] = vectorGroups(store);
|
|
376
|
+
const model = embeddingIndexIdentity(provider.id);
|
|
377
|
+
try {
|
|
378
|
+
const refused = replacesIndex(await reads.embeddingIndexState(), model)
|
|
379
|
+
|| (await writeVectorPage(writes, { model, replaceIndex: false }, [entry], await provider.embed([embeddingInputText(entry)], 'passage'), true)).modelMismatch;
|
|
380
|
+
if (refused)
|
|
381
|
+
warnEmbedFailureOnce('index', OTHER_MODEL_INDEX);
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
warnEmbedFailureOnce(provider.kind, err instanceof Error ? err.message : String(err));
|
|
385
|
+
}
|
|
386
|
+
}).catch((err) => {
|
|
387
|
+
log.warn(`skipped embedding ${entry.id} (${err instanceof Error ? err.message : String(err)})`);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
/** Embed a single memory entry and cache the result in the embedding index; with `store`, through its vectorWrites, never opening hippo.db. */
|
|
391
|
+
export async function embedMemory(hippoRoot, entry, model, store) {
|
|
308
392
|
let provider;
|
|
309
393
|
try {
|
|
310
394
|
provider = resolveEmbeddingProvider(hippoRoot, { model });
|
|
@@ -316,6 +400,8 @@ export async function embedMemory(hippoRoot, entry, model) {
|
|
|
316
400
|
}
|
|
317
401
|
if (!provider.isAvailable())
|
|
318
402
|
return;
|
|
403
|
+
if (store)
|
|
404
|
+
return embedMemoryInStore(store, provider, entry);
|
|
319
405
|
return withEmbedLock(hippoRoot, async () => {
|
|
320
406
|
// embedMemory is best-effort: an embedding failure (API down / bad key / 5xx)
|
|
321
407
|
// must not reject the caller's write — `getEmbedding` historically swallowed
|
|
@@ -418,16 +504,15 @@ async function backfillPending(hippoRoot, provider, pending, model) {
|
|
|
418
504
|
}
|
|
419
505
|
return { count, backfillError };
|
|
420
506
|
}
|
|
421
|
-
/**
|
|
422
|
-
*
|
|
423
|
-
|
|
424
|
-
* Returns the count of newly embedded entries. `provider` defaults to the store's configured one.
|
|
425
|
-
*/
|
|
426
|
-
export async function embedAll(hippoRoot, model, provider = resolveEmbeddingProvider(hippoRoot, { model })) {
|
|
507
|
+
/** Embeds every entry with no cached vector, prunes vectors of deleted memories, and returns how many it embedded; `provider` defaults to the
|
|
508
|
+
* configured one. With `store`, every tenant's memories with no vector for the provider's model go through its `vectorWrites`, never hippo.db. */
|
|
509
|
+
export async function embedAll(hippoRoot, model, provider = resolveEmbeddingProvider(hippoRoot, { model }), store) {
|
|
427
510
|
if (!provider.isAvailable()) {
|
|
428
511
|
throwIfProviderKeyMissing(hippoRoot, provider);
|
|
429
512
|
return 0;
|
|
430
513
|
}
|
|
514
|
+
if (store)
|
|
515
|
+
return withProcessEmbedLock(() => embedAllInStore(store, provider));
|
|
431
516
|
return withEmbedLock(hippoRoot, async () => {
|
|
432
517
|
const identity = provider.id;
|
|
433
518
|
// Host-wide by design. embedAll backfills vectors for all tenants'
|
|
@@ -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
|