hippo-memory 1.66.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/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/sqlite-blocked.js +1 -1
- package/dist/delivery-recorder.d.ts +3 -1
- package/dist/delivery-recorder.js +1 -1
- 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 +1 -1
- package/dist/server/types.d.ts +5 -4
- package/dist/server.d.ts +5 -5
- package/dist/server.js +13 -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-port.d.ts +29 -8
- package/dist/store-port.js +28 -1
- 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/cli/setup.js
CHANGED
|
@@ -6,6 +6,7 @@ import { installJsonHooks, uninstallJsonHooks, checkUninstallable, resolveJsonHo
|
|
|
6
6
|
import { detectInstalledTools } from '../hooks/shared.js';
|
|
7
7
|
import { ensureCodexWrapperInstalled, installCodexWrapper, detectRealCodexPath, uninstallCodexWrapper, } from '../hooks/codex-wrapper.js';
|
|
8
8
|
import { installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath } from '../hooks/opencode.js';
|
|
9
|
+
import { copilotMcpSnippet, copilotPaths, installCopilot, isMcpFailure, uninstallCopilot } from '../hooks/copilot.js';
|
|
9
10
|
import { isInitialized } from '../store/open.js';
|
|
10
11
|
import { currentMachine, importUserMemories } from '../agent-memories/sync.js';
|
|
11
12
|
import { getGlobalRoot } from '../shared.js';
|
|
@@ -15,7 +16,7 @@ import { log } from '../log.js';
|
|
|
15
16
|
import { printError } from './output.js';
|
|
16
17
|
import { printAgentImport, installCodexMemoryHooks, setupDailySchedule, warnClaudeSettingsUnusable } from './shared.js';
|
|
17
18
|
import { repairQualityOnceAt } from './quality-repair-once.js';
|
|
18
|
-
import { HOOK_MARKERS, HOOKS, hippoBlock } from './hook-blocks.js';
|
|
19
|
+
import { HOOK_MARKERS, HOOKS, hippoBlock, withoutHookBlock } from './hook-blocks.js';
|
|
19
20
|
import { escapeRegex } from '../escape.js';
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
// Hook install/uninstall
|
|
@@ -41,13 +42,17 @@ function hookList() {
|
|
|
41
42
|
for (const [name, hook] of Object.entries(HOOKS)) {
|
|
42
43
|
console.log(` ${name.padEnd(15)} -> ${hook.file} (${hook.description})`);
|
|
43
44
|
}
|
|
45
|
+
console.log(` ${'copilot'.padEnd(15)} -> $COPILOT_HOME or ~/.copilot (GitHub Copilot: hooks, MCP server and instructions)`);
|
|
44
46
|
console.log('\nUsage: hippo hook install <name>');
|
|
45
47
|
console.log(' hippo hook uninstall <name>');
|
|
46
48
|
}
|
|
47
49
|
function hookInstall(target) {
|
|
50
|
+
// Copilot's instructions live in its home folder, not in a project file, so it has no HOOKS entry.
|
|
51
|
+
if (target === 'copilot')
|
|
52
|
+
return printCopilotInstall(installCopilot(), '');
|
|
48
53
|
if (!target || !HOOKS[target]) {
|
|
49
54
|
printError(`Unknown hook target: ${target ?? '(none)'}`);
|
|
50
|
-
printError(` Available: ${Object.keys(HOOKS).join(', ')}`);
|
|
55
|
+
printError(` Available: ${[...Object.keys(HOOKS), 'copilot'].join(', ')}`);
|
|
51
56
|
process.exit(1);
|
|
52
57
|
}
|
|
53
58
|
const hook = HOOKS[target];
|
|
@@ -150,7 +155,56 @@ function installOpencodeHook() {
|
|
|
150
155
|
console.log(`WARNING: opencode.json is unparseable; legacy hooks block could not be auto-removed. Fix the file manually.`);
|
|
151
156
|
}
|
|
152
157
|
}
|
|
158
|
+
const MCP_MERGE_LINES = {
|
|
159
|
+
added: (file) => `Added the "hippo" MCP server -> ${file}`,
|
|
160
|
+
present: (file) => `The "hippo" MCP server is already in ${file}`,
|
|
161
|
+
'user-owned': (file) => `Left the "hippo" MCP server in ${file} as it is: hippo did not write it`,
|
|
162
|
+
unreadable: (file) => `WARNING: ${file} has comments or is not a JSON object hippo can merge into, so it was left unchanged; add this under "mcpServers" by hand: ${copilotMcpSnippet()}`,
|
|
163
|
+
};
|
|
164
|
+
const mcpFailureLine = (file, failure, fix) => `WARNING: hippo could not use ${file} (${failure.failed}), so it was left unchanged; ${fix}`;
|
|
165
|
+
const unclosedLine = (file) => `WARNING: ${file} has ${HOOK_MARKERS.start} with no ${HOOK_MARKERS.end}, so hippo left it unchanged; fix the markers by hand`;
|
|
166
|
+
const INSTRUCTIONS_INSTALL_LINES = {
|
|
167
|
+
written: (file) => `Wrote the hippo block -> ${file}`,
|
|
168
|
+
present: (file) => `The hippo block is already in ${file}`,
|
|
169
|
+
kept: (file) => `Kept the hippo block in ${file} as it is: it is not the Copilot text hippo wrote, so it was edited or is another agent's`,
|
|
170
|
+
unclosed: unclosedLine,
|
|
171
|
+
};
|
|
172
|
+
const INSTRUCTIONS_REMOVE_LINES = {
|
|
173
|
+
removed: (file) => `Removed the hippo block from ${file}`,
|
|
174
|
+
absent: () => '',
|
|
175
|
+
kept: (file) => `Left the hippo block in ${file}: it is not the Copilot text hippo wrote. Delete it by hand if no agent needs it.`,
|
|
176
|
+
unclosed: unclosedLine,
|
|
177
|
+
};
|
|
178
|
+
function printCopilotInstall(result, indent) {
|
|
179
|
+
const { paths, mcp } = result;
|
|
180
|
+
const hooks = result.hooks ? "Installed hippo's Copilot hooks (sessionStart, postToolUseFailure, preCompact, sessionEnd) ->" : "hippo's Copilot hooks are already in";
|
|
181
|
+
console.log(`${indent}${hooks} ${paths.hooks}`);
|
|
182
|
+
const mcpLine = isMcpFailure(mcp) ? mcpFailureLine(paths.mcpConfig, mcp, `add this under "mcpServers" by hand: ${copilotMcpSnippet()}`) : MCP_MERGE_LINES[mcp](paths.mcpConfig);
|
|
183
|
+
console.log(`${indent}${mcpLine}`);
|
|
184
|
+
console.log(`${indent}${INSTRUCTIONS_INSTALL_LINES[result.instructions](paths.instructions)}`);
|
|
185
|
+
console.log(`${indent}VS Code Copilot runs new hooks in a new chat session.`);
|
|
186
|
+
}
|
|
187
|
+
function printCopilotUninstall(result) {
|
|
188
|
+
const { paths, mcp } = result;
|
|
189
|
+
if (result.hooks)
|
|
190
|
+
console.log(`Removed hippo's Copilot hooks file ${paths.hooks}`);
|
|
191
|
+
if (isMcpFailure(mcp))
|
|
192
|
+
console.log(mcpFailureLine(paths.mcpConfig, mcp, 'remove the "hippo" server by hand if hippo added it'));
|
|
193
|
+
if (mcp === 'removed')
|
|
194
|
+
console.log(`Removed the "hippo" MCP server from ${paths.mcpConfig}`);
|
|
195
|
+
if (mcp === 'user-owned')
|
|
196
|
+
console.log(`Left the "hippo" MCP server in ${paths.mcpConfig}: hippo did not write it`);
|
|
197
|
+
if (mcp === 'unreadable')
|
|
198
|
+
console.log(`WARNING: ${paths.mcpConfig} has comments or is not a JSON object hippo can edit, so it was left unchanged; remove the "hippo" server by hand if hippo added it`);
|
|
199
|
+
const instructions = INSTRUCTIONS_REMOVE_LINES[result.instructions](paths.instructions);
|
|
200
|
+
if (instructions)
|
|
201
|
+
console.log(instructions);
|
|
202
|
+
if (!result.hooks && mcp === 'absent' && result.instructions === 'absent')
|
|
203
|
+
console.log('No hippo Copilot hooks, MCP server or instructions block found.');
|
|
204
|
+
}
|
|
153
205
|
function hookUninstall(target) {
|
|
206
|
+
if (target === 'copilot')
|
|
207
|
+
return printCopilotUninstall(uninstallCopilot());
|
|
154
208
|
if (!target || !HOOKS[target]) {
|
|
155
209
|
printError(`Unknown hook target: ${target ?? '(none)'}`);
|
|
156
210
|
process.exit(1);
|
|
@@ -218,10 +272,6 @@ function removeLegacyCursorRules() {
|
|
|
218
272
|
console.log(left ? 'Removed the old Hippo hook from .cursorrules' : 'Deleted .cursorrules, which held only the old Hippo hook');
|
|
219
273
|
}
|
|
220
274
|
}
|
|
221
|
-
function withoutHookBlock(text) {
|
|
222
|
-
const re = new RegExp(`\\n?${escapeRegex(HOOK_MARKERS.start)}[\\s\\S]*?${escapeRegex(HOOK_MARKERS.end)}\\n?`, 'g');
|
|
223
|
-
return text.replace(re, '\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
224
|
-
}
|
|
225
275
|
// `hippo setup` -- one-shot configuration for every AI coding tool on the box.
|
|
226
276
|
// Detection and install logic live in ./hooks.ts.
|
|
227
277
|
export function cmdSetup(flags) {
|
|
@@ -237,7 +287,7 @@ export function cmdSetup(flags) {
|
|
|
237
287
|
const markdownTools = tools.filter((t) => t.kind === 'markdown-instruction' && t.detected);
|
|
238
288
|
const pluginTools = tools.filter((t) => t.kind === 'plugin' && t.detected);
|
|
239
289
|
if (jsonTools.length === 0 && !forceAll) {
|
|
240
|
-
console.log('No JSON-hook-capable tools detected (checked: claude-code).');
|
|
290
|
+
console.log('No JSON-hook-capable tools detected (checked: claude-code, copilot).');
|
|
241
291
|
console.log('Run with --all to install hooks anyway.');
|
|
242
292
|
}
|
|
243
293
|
for (const tool of jsonTools)
|
|
@@ -276,7 +326,16 @@ export function cmdSetup(flags) {
|
|
|
276
326
|
console.log('');
|
|
277
327
|
console.log('Done. Restart your AI tool to activate the hooks.');
|
|
278
328
|
}
|
|
329
|
+
function setupCopilot(dryRun) {
|
|
330
|
+
const indent = ` ${'copilot'.padEnd(14)} `;
|
|
331
|
+
if (!dryRun)
|
|
332
|
+
return printCopilotInstall(installCopilot(), indent);
|
|
333
|
+
const { hooks, mcpConfig, instructions } = copilotPaths();
|
|
334
|
+
console.log(`[dry-run] would install hooks in ${hooks}, the MCP server in ${mcpConfig} and the hippo block in ${instructions}`);
|
|
335
|
+
}
|
|
279
336
|
function setupJsonTool(tool, dryRun) {
|
|
337
|
+
if (tool.name === 'copilot')
|
|
338
|
+
return setupCopilot(dryRun);
|
|
280
339
|
if (dryRun) {
|
|
281
340
|
// Resolve the real settings path so the filename is right for each tool
|
|
282
341
|
// (claude-code -> settings.json, opencode -> opencode.json).
|
package/dist/cli/shared.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type InstallResult } from '../hooks/json-hooks.js';
|
|
2
2
|
import { TaskSnapshot, SessionEvent } from '../store/rows.js';
|
|
3
|
+
import type { HookRuntime } from '../capture-contract.js';
|
|
3
4
|
import type { SessionHandoff } from '../handoff.js';
|
|
4
5
|
import type { SearchResult } from '../search/types.js';
|
|
5
6
|
import { type HippoConfig } from '../config.js';
|
|
@@ -113,6 +114,10 @@ export declare function captureConsole(fn: () => void): string;
|
|
|
113
114
|
* is looked for in the other.
|
|
114
115
|
*/
|
|
115
116
|
export declare function hookStoreRoot(hippoRoot: string): string;
|
|
117
|
+
/** `--runtime copilot`, or `--format copilot` on `hippo context`, marks a Copilot hook: the flag decides, never the payload. */
|
|
118
|
+
export declare function hookRuntime(flags: CliFlags): HookRuntime;
|
|
119
|
+
/** A Copilot hook's project root, from the payload's `cwd`, since VS Code runs user-level hooks in the home folder; other runtimes keep `hippoRoot`. */
|
|
120
|
+
export declare function payloadCwdRoot(hippoRoot: string, stdinText: string | undefined, runtime: HookRuntime): string;
|
|
116
121
|
export declare function learnFromRepo(hippoRoot: string, repoPath: string, days: number, label?: string): {
|
|
117
122
|
added: number;
|
|
118
123
|
skipped: number;
|
package/dist/cli/shared.js
CHANGED
|
@@ -7,13 +7,13 @@ import { execFileSync, execSync } from 'child_process';
|
|
|
7
7
|
import { installJsonHooks } from '../hooks/json-hooks.js';
|
|
8
8
|
import { CODEX_TRUST_LINE } from '../hooks/shared.js';
|
|
9
9
|
import { confidenceLabel } from '../memory.js';
|
|
10
|
-
import { isInitialized } from '../store/open.js';
|
|
10
|
+
import { getHippoRoot, isInitialized } from '../store/open.js';
|
|
11
11
|
import { explainMatch } from '../search/explain.js';
|
|
12
12
|
import { isSharedStore } from '../config.js';
|
|
13
13
|
import { openHippoDb, closeHippoDb, isSqliteBusy, noteStoreBusy, runWithRequestStores, HOOK_DB_WAIT_MS } from '../db.js';
|
|
14
14
|
import { withLedgerDb } from '../ledger-db.js';
|
|
15
15
|
import { sessionPilotArm } from '../pilot-arm.js';
|
|
16
|
-
import { hookPayloadSessionId, isSubagentPayload, recordTokenUse } from '../token-ledger.js';
|
|
16
|
+
import { hookPayloadSessionId, hookPayloadString, isSubagentPayload, recordTokenUse } from '../token-ledger.js';
|
|
17
17
|
import { importAtSessionEnd, currentMachine } from '../agent-memories/sync.js';
|
|
18
18
|
import { summaryLine } from '../agent-memories/report.js';
|
|
19
19
|
import { detectChurnStale } from '../invalidation.js';
|
|
@@ -27,7 +27,7 @@ import { detectServer, removePidfileIfOwned } from '../server-detect.js';
|
|
|
27
27
|
import { resolveTenantId } from '../tenant.js';
|
|
28
28
|
import { adminActor, learn, CLI_LEARN } from '../api.js';
|
|
29
29
|
import { snapshotText, sessionTrailText, handoffText } from '../context-render.js';
|
|
30
|
-
import { log } from '../log.js';
|
|
30
|
+
import { errorMessage, log } from '../log.js';
|
|
31
31
|
import { printError } from './output.js';
|
|
32
32
|
export function parseLimitFlag(value) {
|
|
33
33
|
if (!value)
|
|
@@ -441,6 +441,25 @@ export function hookStoreRoot(hippoRoot) {
|
|
|
441
441
|
const globalRoot = getGlobalRoot();
|
|
442
442
|
return isInitialized(globalRoot) ? globalRoot : hippoRoot;
|
|
443
443
|
}
|
|
444
|
+
/** `--runtime copilot`, or `--format copilot` on `hippo context`, marks a Copilot hook: the flag decides, never the payload. */
|
|
445
|
+
export function hookRuntime(flags) {
|
|
446
|
+
return flags['runtime'] === 'copilot' || flags['format'] === 'copilot' ? 'copilot' : 'claude-code';
|
|
447
|
+
}
|
|
448
|
+
/** A Copilot hook's project root, from the payload's `cwd`, since VS Code runs user-level hooks in the home folder; other runtimes keep `hippoRoot`. */
|
|
449
|
+
export function payloadCwdRoot(hippoRoot, stdinText, runtime) {
|
|
450
|
+
const cwd = runtime === 'copilot' ? hookPayloadString(stdinText, 'cwd') : null;
|
|
451
|
+
if (cwd === null || cwd.trim() === '')
|
|
452
|
+
return hippoRoot;
|
|
453
|
+
try {
|
|
454
|
+
// Moving, not just re-rooting, keeps project identity, scope, handoff evidence and the session-end worker on that folder too.
|
|
455
|
+
process.chdir(cwd);
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
log.warn(`hippo: payload cwd ${cwd} is not usable, so the hook stays in ${process.cwd()}: ${errorMessage(err)}`);
|
|
459
|
+
return hippoRoot;
|
|
460
|
+
}
|
|
461
|
+
return getHippoRoot(process.cwd());
|
|
462
|
+
}
|
|
444
463
|
export function learnFromRepo(hippoRoot, repoPath, days, label) {
|
|
445
464
|
const prefix = label ? `[${label}] ` : '';
|
|
446
465
|
const ctx = { hippoRoot, tenantId: resolveTenantId({}), actor: adminActor('cli') };
|
package/dist/cli/usage.js
CHANGED
|
@@ -255,18 +255,24 @@ export const VERB_USAGE = {
|
|
|
255
255
|
session-end SessionEnd hook: count this session's re-read tokens, run sleep, then
|
|
256
256
|
capture from the session's last 20 user and 10 assistant messages,
|
|
257
257
|
in a detached worker
|
|
258
|
-
--log-file <path> Tee the worker's output to a log file (paired with 'hippo last-sleep')
|
|
258
|
+
--log-file <path> Tee the worker's output to a log file (paired with 'hippo last-sleep')
|
|
259
|
+
--runtime copilot The payload came from a Copilot hook: use the store of its cwd and find
|
|
260
|
+
the Copilot CLI session log by session id; with no --log-file, log to
|
|
261
|
+
~/.hippo/logs/copilot-sleep.log`],
|
|
259
262
|
'pre-compact': [`
|
|
260
263
|
pre-compact PreCompact hook: record the compaction, save a working-state snapshot, and
|
|
261
264
|
ask the summariser to end with a "Memories for hippo" list
|
|
262
|
-
--log-file <p> Diagnostic log path (default: ~/.hippo/logs/pre-compact.log)
|
|
265
|
+
--log-file <p> Diagnostic log path (default: ~/.hippo/logs/pre-compact.log)
|
|
266
|
+
--runtime copilot The payload came from a Copilot hook: use the store of its cwd and save
|
|
267
|
+
the snapshot only (Copilot has no PostCompact hook to close a record)`],
|
|
263
268
|
'post-compact': [`
|
|
264
269
|
post-compact PostCompact hook: keep that list as memories (a busy store leaves the save to
|
|
265
270
|
the next hippo sleep) and print one line saying how many
|
|
266
271
|
--log-file <p> Same log path as pre-compact (default: ~/.hippo/logs/pre-compact.log)`],
|
|
267
272
|
'capture-error': [`
|
|
268
273
|
capture-error Store a failed tool call as an error memory (reads the Claude Code
|
|
269
|
-
PostToolUseFailure hook payload on stdin; skips routine failures)
|
|
274
|
+
PostToolUseFailure hook payload on stdin; skips routine failures)
|
|
275
|
+
--runtime copilot The payload came from a Copilot hook: use the store of its cwd`],
|
|
270
276
|
'compact-resume': [`
|
|
271
277
|
compact-resume SessionStart(compact) hook: re-print the snapshot, if under 15 minutes old`],
|
|
272
278
|
'codex-run': [`
|
|
@@ -544,23 +550,28 @@ export const VERB_USAGE = {
|
|
|
544
550
|
--pinned-only Only inject pinned memories (used by UserPromptSubmit hook)
|
|
545
551
|
--include-recent <n> With --pinned-only, also inject the last N writes regardless of pinning
|
|
546
552
|
(the hook payload's "prompt" drives prompt recall instead of --include-recent when pinnedInject.promptRecall is on, the default)
|
|
547
|
-
--format <fmt> Output format: markdown (default), json,
|
|
553
|
+
--format <fmt> Output format: markdown (default), json, additional-context (Claude Code hook JSON),
|
|
554
|
+
or copilot (Copilot sessionStart hook JSON, from the store of the payload's cwd)
|
|
548
555
|
--framing <mode> Framing: observe (default), suggest, assert`],
|
|
549
556
|
hook: [`
|
|
550
557
|
hook <sub> [target] Manage framework integrations
|
|
551
558
|
hook list Show available hooks
|
|
552
|
-
hook install <target> Install hook (claude-code|codex|cursor|openclaw|opencode|pi)
|
|
559
|
+
hook install <target> Install hook (claude-code|codex|copilot|cursor|openclaw|opencode|pi)
|
|
553
560
|
claude-code adds 7 hooks to $CLAUDE_CONFIG_DIR/settings.json
|
|
554
561
|
(~/.claude by default); opencode installs a plugin; codex
|
|
555
562
|
adds 2 hooks to $CODEX_HOME/hooks.json (trust them once in /hooks) and
|
|
556
|
-
wraps the detected launcher in place;
|
|
557
|
-
|
|
558
|
-
|
|
563
|
+
wraps the detected launcher in place; copilot writes
|
|
564
|
+
hooks/hippo.json, the "hippo" MCP server and an instructions
|
|
565
|
+
block under $COPILOT_HOME (~/.copilot by default); all but
|
|
566
|
+
claude-code and copilot also patch an existing AGENTS.md
|
|
567
|
+
hook uninstall <target> Remove hook; for copilot, only what hippo wrote`],
|
|
559
568
|
setup: [`
|
|
560
569
|
setup One-shot: detect installed AI tools and install their hooks:
|
|
561
570
|
claude-code gets 7 hooks in $CLAUDE_CONFIG_DIR/settings.json
|
|
562
571
|
(~/.claude by default), opencode a plugin, codex 2 hooks in
|
|
563
|
-
its hooks.json plus a launcher wrapper
|
|
572
|
+
its hooks.json plus a launcher wrapper, copilot 4 hooks, the
|
|
573
|
+
MCP server and an instructions block under $COPILOT_HOME
|
|
574
|
+
(~/.copilot by default); other tools get a
|
|
564
575
|
hint. Then imports each agent's
|
|
565
576
|
user-level memories into the global store
|
|
566
577
|
--all Install for every JSON-hook tool, even if not detected
|
|
@@ -12,7 +12,7 @@ export class SqliteBlockedError extends Error {
|
|
|
12
12
|
export class StoreNotPortedError extends SqliteBlockedError {
|
|
13
13
|
group;
|
|
14
14
|
constructor(storeKind, group) {
|
|
15
|
-
super(storeKind, `the '${storeKind}' store has no '${group}'
|
|
15
|
+
super(storeKind, `the '${storeKind}' store has no '${group}' group; this code path is not ported to the store yet`);
|
|
16
16
|
this.group = group;
|
|
17
17
|
this.name = 'StoreNotPortedError';
|
|
18
18
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MemoryEntry } from './memory.js';
|
|
2
2
|
import { type PromptRecallGate } from './prompt-recall.js';
|
|
3
|
-
export type DeliveryRuntime = 'claude-code' | 'codex' | 'unknown';
|
|
3
|
+
export type DeliveryRuntime = 'claude-code' | 'codex' | 'copilot' | 'unknown';
|
|
4
4
|
export type DeliveryEventType = 'prompt-submit' | 'pinned-manual';
|
|
5
5
|
export type DeliverySurface = 'hook' | 'context';
|
|
6
6
|
export type DeliveryWriteStore = 'local' | 'global';
|
|
@@ -121,6 +121,8 @@ export interface DeliveryRecorderInit {
|
|
|
121
121
|
tenantId: string;
|
|
122
122
|
stdinText?: string;
|
|
123
123
|
envSessionId?: string;
|
|
124
|
+
/** Set by the caller's runtime flag; Copilot payloads carry hook_event_name too, so inference would say claude-code. */
|
|
125
|
+
runtime?: DeliveryRuntime;
|
|
124
126
|
}
|
|
125
127
|
/** A recorder for one call; every observer method is guarded, and a throw marks it broken instead of escaping. */
|
|
126
128
|
export declare function createDeliveryRecorder(init: DeliveryRecorderInit): DeliveryRecorder;
|
|
@@ -92,7 +92,7 @@ function buildEvent(init, payload, state, ts, startedMs) {
|
|
|
92
92
|
return {
|
|
93
93
|
ts,
|
|
94
94
|
tenantId: init.tenantId,
|
|
95
|
-
runtime: payload.hostTurnId !== null ? 'codex' : payload.hookEvent !== null ? 'claude-code' : 'unknown',
|
|
95
|
+
runtime: init.runtime ?? (payload.hostTurnId !== null ? 'codex' : payload.hookEvent !== null ? 'claude-code' : 'unknown'),
|
|
96
96
|
eventType: payload.hookEvent === 'UserPromptSubmit' ? 'prompt-submit' : 'pinned-manual',
|
|
97
97
|
surface: 'hook',
|
|
98
98
|
storeHash: init.storeHash,
|
|
@@ -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);
|