hippo-memory 1.67.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/auth.d.ts +14 -18
- package/dist/api/auth.js +93 -60
- 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/api/types.d.ts +9 -0
- package/dist/auth.d.ts +19 -0
- package/dist/auth.js +14 -6
- 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/embeddings.d.ts +10 -10
- package/dist/embeddings.js +103 -18
- 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/admin.js +2 -2
- package/dist/server/routes/memories.js +9 -6
- package/dist/server-detect.js +3 -2
- package/dist/server.d.ts +20 -4
- package/dist/server.js +23 -11
- 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/key-writes.d.ts +9 -0
- package/dist/store/key-writes.js +30 -0
- 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/vector-writes.d.ts +6 -0
- package/dist/store/vector-writes.js +59 -0
- package/dist/store-port.d.ts +143 -1
- package/dist/store-port.js +48 -12
- 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'
|
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
|
package/dist/hooks/copilot.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
// GitHub Copilot: hippo's hooks file, one MCP server key and an instructions block
|
|
1
|
+
// GitHub Copilot: hippo's hooks file, one MCP server key and an instructions block under the Copilot home folder,
|
|
2
|
+
// plus the MCP server and an instructions file in each VS Code User folder.
|
|
2
3
|
import * as fs from 'fs';
|
|
3
4
|
import * as path from 'path';
|
|
4
5
|
import { isDeepStrictEqual } from 'node:util';
|
|
5
6
|
import { readJsonFile } from '../json.js';
|
|
6
7
|
import { HOOK_MARKERS, hippoBlock } from '../cli/hook-blocks.js';
|
|
7
|
-
import { copilotHomeDir, isJsonObject } from './shared.js';
|
|
8
|
+
import { copilotHomeDir, isJsonObject, vscodeUserDirs } from './shared.js';
|
|
8
9
|
import { installJsonHooks, resolveJsonHookPaths, uninstallJsonHooks, writeSettingsFile } from './json-hooks.js';
|
|
9
10
|
const MCP_KEY = 'hippo';
|
|
10
11
|
/** Copilot drops what a per-prompt hook prints, so per-task recall goes through the MCP tool the block names. */
|
|
@@ -19,32 +20,75 @@ reason, a user preference, why something failed), call the \`hippo_remember\`
|
|
|
19
20
|
tool right then, while you work, never as a closing step. Set \`error\` to true
|
|
20
21
|
for a failure. Leave out secrets and personal details.
|
|
21
22
|
|
|
23
|
+
The installed hooks load pinned memories when a session starts and capture
|
|
24
|
+
the session as you work, so there is nothing to run before you finish.
|
|
25
|
+
`.trim();
|
|
26
|
+
/** Every Copilot text a release has written, so install upgrades an old block and uninstall still removes it; add the old text when the text changes. */
|
|
27
|
+
const SHIPPED_COPILOT_INSTRUCTIONS = [
|
|
28
|
+
COPILOT_INSTRUCTIONS,
|
|
29
|
+
`
|
|
30
|
+
## Project Memory (Hippo)
|
|
31
|
+
|
|
32
|
+
At the start of each task, call the \`hippo_recall\` tool with a short query that
|
|
33
|
+
names the task. Read the memories it returns before you write any code.
|
|
34
|
+
|
|
35
|
+
When you learn something that should outlive this session (a decision and its
|
|
36
|
+
reason, a user preference, why something failed), call the \`hippo_remember\`
|
|
37
|
+
tool right then, while you work, never as a closing step. Set \`error\` to true
|
|
38
|
+
for a failure. Leave out secrets and personal details.
|
|
39
|
+
|
|
22
40
|
The installed hooks load pinned memories when a session starts, store failed
|
|
23
41
|
tool calls and capture the session when it ends, so there is nothing to run
|
|
24
42
|
before you finish.
|
|
25
|
-
`.trim()
|
|
43
|
+
`.trim(),
|
|
44
|
+
];
|
|
45
|
+
/** VS Code attaches a User `*.instructions.md` file to every chat request its `applyTo` glob matches. */
|
|
46
|
+
const withApplyToAll = (text) => `---\napplyTo: "**"\n---\n${text}\n`;
|
|
47
|
+
export const VSCODE_INSTRUCTIONS = withApplyToAll(COPILOT_INSTRUCTIONS);
|
|
48
|
+
function isHipposVscodeInstructions(text) {
|
|
49
|
+
return SHIPPED_COPILOT_INSTRUCTIONS.some((shipped) => text === withApplyToAll(shipped));
|
|
50
|
+
}
|
|
51
|
+
function profileDirs(userDir) {
|
|
52
|
+
const dir = path.join(userDir, 'profiles');
|
|
53
|
+
if (!fs.existsSync(dir))
|
|
54
|
+
return [];
|
|
55
|
+
return fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => path.join(dir, e.name)).sort();
|
|
56
|
+
}
|
|
26
57
|
export function copilotPaths() {
|
|
27
58
|
const home = copilotHomeDir();
|
|
28
59
|
return {
|
|
29
60
|
hooks: resolveJsonHookPaths('copilot').settings,
|
|
30
61
|
mcpConfig: path.join(home, 'mcp-config.json'),
|
|
31
62
|
instructions: path.join(home, 'copilot-instructions.md'),
|
|
63
|
+
vscode: vscodeUserDirs().map((userDir) => ({
|
|
64
|
+
userDir,
|
|
65
|
+
mcpConfig: path.join(userDir, 'mcp.json'),
|
|
66
|
+
instructions: path.join(userDir, 'prompts', 'hippo.instructions.md'),
|
|
67
|
+
profiles: profileDirs(userDir),
|
|
68
|
+
})),
|
|
32
69
|
};
|
|
33
70
|
}
|
|
34
71
|
/** Windows gets npm's .cmd shim, as the Codex hooks do, since a PowerShell execution policy can block hippo.ps1. */
|
|
35
|
-
function
|
|
36
|
-
return
|
|
72
|
+
function hippoCommand() {
|
|
73
|
+
return process.platform === 'win32' ? 'hippo.cmd' : 'hippo';
|
|
37
74
|
}
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
75
|
+
/** The Copilot CLI's mcp-config.json keeps servers under "mcpServers", each with an env and a tool list. */
|
|
76
|
+
export const COPILOT_CLI_MCP = { key: 'mcpServers', server: () => ({ type: 'local', command: hippoCommand(), args: ['mcp'], env: {}, tools: ['*'] }) };
|
|
77
|
+
/** VS Code's mcp.json keeps stdio servers under "servers"; it has no "mcpServers" key. */
|
|
78
|
+
export const VSCODE_MCP = { key: 'servers', server: () => ({ type: 'stdio', command: hippoCommand(), args: ['mcp'] }) };
|
|
79
|
+
/** Only a server hippo wrote counts as hippo's: another command, other args, or any key or value setup does not write (an env you added) makes it the user's. */
|
|
80
|
+
function isHipposServer(entry, host) {
|
|
81
|
+
if (!isJsonObject(entry) || !('command' in entry) || !('args' in entry))
|
|
82
|
+
return false;
|
|
83
|
+
const ours = host.server();
|
|
84
|
+
return Object.entries(entry).every(([key, value]) => (key === 'command' ? value === 'hippo' || value === 'hippo.cmd' : isDeepStrictEqual(value, ours[key])));
|
|
41
85
|
}
|
|
42
|
-
/** What to paste under
|
|
43
|
-
export function copilotMcpSnippet() {
|
|
44
|
-
return `"${MCP_KEY}": ${JSON.stringify(
|
|
86
|
+
/** What to paste under the host's key when hippo cannot edit the file itself. */
|
|
87
|
+
export function copilotMcpSnippet(host = COPILOT_CLI_MCP) {
|
|
88
|
+
return `"${MCP_KEY}": ${JSON.stringify(host.server())}`;
|
|
45
89
|
}
|
|
46
90
|
/** 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) {
|
|
91
|
+
function readMcpConfig(file, host) {
|
|
48
92
|
let config = {};
|
|
49
93
|
if (fs.existsSync(file)) {
|
|
50
94
|
try {
|
|
@@ -52,45 +96,47 @@ function readMcpConfig(file) {
|
|
|
52
96
|
}
|
|
53
97
|
catch (err) {
|
|
54
98
|
// 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)
|
|
99
|
+
if (!(err instanceof SyntaxError))
|
|
100
|
+
throw err;
|
|
101
|
+
// An empty file is a new one with no servers yet, not one with comments.
|
|
102
|
+
if (fs.readFileSync(file, 'utf8').trim() !== '')
|
|
56
103
|
return null;
|
|
57
|
-
throw err;
|
|
58
104
|
}
|
|
59
105
|
}
|
|
60
106
|
if (!isJsonObject(config))
|
|
61
107
|
return null;
|
|
62
|
-
if (config.
|
|
63
|
-
config.
|
|
64
|
-
const servers = config.
|
|
108
|
+
if (config[host.key] === undefined)
|
|
109
|
+
config[host.key] = {};
|
|
110
|
+
const servers = config[host.key];
|
|
65
111
|
return isJsonObject(servers) ? { config, servers } : null;
|
|
66
112
|
}
|
|
67
113
|
/** 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);
|
|
114
|
+
export function mergeMcpServer(file, host = COPILOT_CLI_MCP) {
|
|
115
|
+
const read = readMcpConfig(file, host);
|
|
70
116
|
if (read === null)
|
|
71
117
|
return 'unreadable';
|
|
72
118
|
const existing = read.servers[MCP_KEY];
|
|
73
119
|
if (existing !== undefined)
|
|
74
|
-
return isHipposServer(existing) ? 'present' : 'user-owned';
|
|
75
|
-
read.servers[MCP_KEY] =
|
|
120
|
+
return isHipposServer(existing, host) ? 'present' : 'user-owned';
|
|
121
|
+
read.servers[MCP_KEY] = host.server();
|
|
76
122
|
writeSettingsFile(file, read.config);
|
|
77
123
|
return 'added';
|
|
78
124
|
}
|
|
79
125
|
/** Removes the "hippo" key only when hippo wrote it. */
|
|
80
|
-
export function removeMcpServer(file) {
|
|
126
|
+
export function removeMcpServer(file, host = COPILOT_CLI_MCP) {
|
|
81
127
|
if (!fs.existsSync(file))
|
|
82
128
|
return 'absent';
|
|
83
|
-
const read = readMcpConfig(file);
|
|
129
|
+
const read = readMcpConfig(file, host);
|
|
84
130
|
if (read === null)
|
|
85
131
|
return 'unreadable';
|
|
86
132
|
const existing = read.servers[MCP_KEY];
|
|
87
133
|
if (existing === undefined)
|
|
88
134
|
return 'absent';
|
|
89
|
-
if (!isHipposServer(existing))
|
|
135
|
+
if (!isHipposServer(existing, host))
|
|
90
136
|
return 'user-owned';
|
|
91
137
|
delete read.servers[MCP_KEY];
|
|
92
138
|
if (Object.keys(read.servers).length === 0)
|
|
93
|
-
delete read.config.
|
|
139
|
+
delete read.config[host.key];
|
|
94
140
|
writeSettingsFile(file, read.config);
|
|
95
141
|
return 'removed';
|
|
96
142
|
}
|
|
@@ -100,7 +146,7 @@ export function isMcpFailure(status) {
|
|
|
100
146
|
function hasErrnoCode(err) {
|
|
101
147
|
return 'code' in err && typeof err.code === 'string';
|
|
102
148
|
}
|
|
103
|
-
/** One
|
|
149
|
+
/** One MCP config step; a filesystem error comes back as a failure, so the hooks file and the instructions still install. */
|
|
104
150
|
function mcpStep(step) {
|
|
105
151
|
try {
|
|
106
152
|
return step();
|
|
@@ -111,9 +157,8 @@ function mcpStep(step) {
|
|
|
111
157
|
throw err;
|
|
112
158
|
}
|
|
113
159
|
}
|
|
114
|
-
// A later release adds the hash of each earlier Copilot text here, as SHIPPED_HOOK_HASHES does for the other agents.
|
|
115
160
|
function isCopilotBlock(inner) {
|
|
116
|
-
return inner
|
|
161
|
+
return SHIPPED_COPILOT_INSTRUCTIONS.includes(inner);
|
|
117
162
|
}
|
|
118
163
|
/** Replaces only the text between the markers, in the file's own line ending, or appends a block after the user's text. */
|
|
119
164
|
function withCopilotBlock(text, found) {
|
|
@@ -166,13 +211,49 @@ export function removeInstructionsBlock(file) {
|
|
|
166
211
|
fs.writeFileSync(file, left, 'utf8');
|
|
167
212
|
return 'removed';
|
|
168
213
|
}
|
|
214
|
+
/** The file is hippo's alone, so it is written whole; an earlier hippo text is upgraded, and any other text already there stays. */
|
|
215
|
+
export function ensureVscodeInstructions(file) {
|
|
216
|
+
if (fs.existsSync(file)) {
|
|
217
|
+
const old = fs.readFileSync(file, 'utf8');
|
|
218
|
+
if (old === VSCODE_INSTRUCTIONS)
|
|
219
|
+
return 'present';
|
|
220
|
+
if (!isHipposVscodeInstructions(old))
|
|
221
|
+
return 'kept';
|
|
222
|
+
}
|
|
223
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
224
|
+
fs.writeFileSync(file, VSCODE_INSTRUCTIONS, 'utf8');
|
|
225
|
+
return 'written';
|
|
226
|
+
}
|
|
227
|
+
/** Deletes the file only when it still holds exactly what a hippo release wrote. */
|
|
228
|
+
export function removeVscodeInstructions(file) {
|
|
229
|
+
if (!fs.existsSync(file))
|
|
230
|
+
return 'absent';
|
|
231
|
+
if (!isHipposVscodeInstructions(fs.readFileSync(file, 'utf8')))
|
|
232
|
+
return 'kept';
|
|
233
|
+
fs.rmSync(file);
|
|
234
|
+
return 'removed';
|
|
235
|
+
}
|
|
236
|
+
/** 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. */
|
|
237
|
+
export function isCopilotCliPresent(home = copilotHomeDir()) {
|
|
238
|
+
if (fs.statSync(home, { throwIfNoEntry: false })?.isDirectory() !== true)
|
|
239
|
+
return false;
|
|
240
|
+
const entries = fs.readdirSync(home);
|
|
241
|
+
return entries.length !== 1 || entries[0] !== 'hooks';
|
|
242
|
+
}
|
|
169
243
|
export function installCopilot() {
|
|
170
244
|
const paths = copilotPaths();
|
|
245
|
+
// Read before the hooks install, which makes the folder on a machine with only VS Code.
|
|
246
|
+
const cli = isCopilotCliPresent();
|
|
171
247
|
return {
|
|
172
248
|
paths,
|
|
173
249
|
hooks: installJsonHooks('copilot').installedSessionStart,
|
|
174
|
-
mcp: mcpStep(() => mergeMcpServer(paths.mcpConfig)),
|
|
175
|
-
instructions: ensureInstructionsBlock(paths.instructions),
|
|
250
|
+
mcp: cli ? mcpStep(() => mergeMcpServer(paths.mcpConfig)) : null,
|
|
251
|
+
instructions: cli ? ensureInstructionsBlock(paths.instructions) : null,
|
|
252
|
+
vscode: paths.vscode.map((dir) => ({
|
|
253
|
+
paths: dir,
|
|
254
|
+
mcp: mcpStep(() => mergeMcpServer(dir.mcpConfig, VSCODE_MCP)),
|
|
255
|
+
instructions: ensureVscodeInstructions(dir.instructions),
|
|
256
|
+
})),
|
|
176
257
|
};
|
|
177
258
|
}
|
|
178
259
|
export function uninstallCopilot() {
|
|
@@ -182,6 +263,11 @@ export function uninstallCopilot() {
|
|
|
182
263
|
hooks: uninstallJsonHooks('copilot'),
|
|
183
264
|
mcp: mcpStep(() => removeMcpServer(paths.mcpConfig)),
|
|
184
265
|
instructions: removeInstructionsBlock(paths.instructions),
|
|
266
|
+
vscode: paths.vscode.map((dir) => ({
|
|
267
|
+
paths: dir,
|
|
268
|
+
mcp: mcpStep(() => removeMcpServer(dir.mcpConfig, VSCODE_MCP)),
|
|
269
|
+
instructions: removeVscodeInstructions(dir.instructions),
|
|
270
|
+
})),
|
|
185
271
|
};
|
|
186
272
|
}
|
|
187
273
|
//# sourceMappingURL=copilot.js.map
|
|
@@ -63,6 +63,8 @@ export interface InstallResult {
|
|
|
63
63
|
}
|
|
64
64
|
/** The paths for `target` under the current home directory; reads and writes nothing. */
|
|
65
65
|
export declare function resolveJsonHookPaths(target: JsonHookTarget): JsonHookPaths;
|
|
66
|
+
/** The events in hippo's Copilot hooks file, in file order, for setup to name. */
|
|
67
|
+
export declare function copilotHookEvents(): string[];
|
|
66
68
|
/** Adds hippo's hooks to `target`'s settings file and migrates older hippo entries; a file that is not JSON stays untouched. */
|
|
67
69
|
export declare function installJsonHooks(target: JsonHookTarget): InstallResult;
|
|
68
70
|
/** Swaps the file in with one rename, so a crash mid-write cannot leave a truncated settings.json that Claude Code cannot parse. */
|
package/dist/hooks/json-hooks.js
CHANGED
|
@@ -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,
|
|
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, copilotHooksFile, 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';
|
|
@@ -58,7 +58,7 @@ export function resolveJsonHookPaths(target) {
|
|
|
58
58
|
};
|
|
59
59
|
case 'copilot':
|
|
60
60
|
return {
|
|
61
|
-
settings:
|
|
61
|
+
settings: copilotHooksFile(),
|
|
62
62
|
logFile: path.join(logsDir, 'copilot-sleep.log'),
|
|
63
63
|
display: 'Copilot',
|
|
64
64
|
};
|
|
@@ -214,10 +214,19 @@ function copilotHooksTable() {
|
|
|
214
214
|
sessionStart: copilotCommandHook('context --pinned-only --include-recent 5 --format copilot', 10),
|
|
215
215
|
postToolUseFailure: copilotCommandHook('capture-error --runtime copilot', 10),
|
|
216
216
|
preCompact: copilotCommandHook('pre-compact --runtime copilot', 30),
|
|
217
|
+
// VS Code maps no camelCase preCompact. The Copilot CLI may run both names, so pre-compact skips a snapshot saved seconds before.
|
|
218
|
+
PreCompact: copilotCommandHook('pre-compact --runtime copilot', 30),
|
|
219
|
+
// VS Code's per-reply Stop. --turn acts on a VS Code payload only, so the Copilot CLI's agentStop does nothing.
|
|
220
|
+
agentStop: copilotCommandHook('session-end --runtime copilot --turn', 30),
|
|
217
221
|
sessionEnd: copilotCommandHook('session-end --runtime copilot', 30),
|
|
218
222
|
},
|
|
219
223
|
};
|
|
220
224
|
}
|
|
225
|
+
/** The events in hippo's Copilot hooks file, in file order, for setup to name. */
|
|
226
|
+
export function copilotHookEvents() {
|
|
227
|
+
const hooks = copilotHooksTable().hooks;
|
|
228
|
+
return isJsonObject(hooks) ? Object.keys(hooks) : [];
|
|
229
|
+
}
|
|
221
230
|
/** Hippo owns this whole file, so install writes the full table and a second install changes no byte. */
|
|
222
231
|
function installCopilotHooks(settingsPath) {
|
|
223
232
|
const result = nothingInstalled('copilot', settingsPath);
|
package/dist/hooks/shared.d.ts
CHANGED
|
@@ -30,7 +30,13 @@ export declare function copilotHomeDir(home?: string, env?: Readonly<Record<stri
|
|
|
30
30
|
export declare function claudeConfigDir(home?: string, env?: Readonly<Record<string, string | undefined>>): string;
|
|
31
31
|
/** Codex counts as installed only when its config folder exists: Codex itself refuses a CODEX_HOME that is not a folder. */
|
|
32
32
|
export declare function isCodexPresent(home?: string): boolean;
|
|
33
|
-
/**
|
|
33
|
+
/** hippo's own hooks file, in the hooks folder the Copilot CLI reads. */
|
|
34
|
+
export declare function copilotHooksFile(): string;
|
|
35
|
+
/** Where VS Code's agent finds hippo.json: its docs name ~/.copilot/hooks as the user hooks folder, whatever COPILOT_HOME says. */
|
|
36
|
+
export declare function vscodeUserHooksFile(home?: string): string;
|
|
37
|
+
/** The VS Code User folders that exist; Stable and Insiders keep their own, and only an edition the user has run has one. */
|
|
38
|
+
export declare function vscodeUserDirs(env?: Readonly<Record<string, string | undefined>>, home?: string): string[];
|
|
39
|
+
/** Copilot counts as installed only when its config folder or a VS Code User folder exists, so setup never creates ~/.copilot on a machine with neither. */
|
|
34
40
|
export declare function isCopilotPresent(home?: string): boolean;
|
|
35
41
|
/** 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
42
|
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`.";
|