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.
Files changed (53) hide show
  1. package/README.md +2 -0
  2. package/dist/api/auth.d.ts +11 -2
  3. package/dist/api/auth.js +25 -2
  4. package/dist/auth.d.ts +3 -1
  5. package/dist/auth.js +6 -2
  6. package/dist/capture/compact.d.ts +2 -0
  7. package/dist/capture/compact.js +12 -6
  8. package/dist/capture/copilot-transcript.d.ts +15 -0
  9. package/dist/capture/copilot-transcript.js +40 -0
  10. package/dist/capture/failure-reading.js +5 -2
  11. package/dist/capture/transcript.js +7 -1
  12. package/dist/capture/working-state.js +14 -3
  13. package/dist/capture-contract.d.ts +4 -2
  14. package/dist/capture-contract.js +3 -3
  15. package/dist/cli/context.js +11 -8
  16. package/dist/cli/hook-blocks.d.ts +2 -0
  17. package/dist/cli/hook-blocks.js +6 -0
  18. package/dist/cli/session-hooks.d.ts +1 -1
  19. package/dist/cli/session-hooks.js +28 -16
  20. package/dist/cli/setup.js +66 -7
  21. package/dist/cli/shared.d.ts +5 -0
  22. package/dist/cli/shared.js +22 -3
  23. package/dist/cli/usage.js +20 -9
  24. package/dist/db/sqlite-blocked.js +1 -1
  25. package/dist/delivery-recorder.d.ts +3 -1
  26. package/dist/delivery-recorder.js +1 -1
  27. package/dist/hooks/copilot.d.ts +45 -0
  28. package/dist/hooks/copilot.js +187 -0
  29. package/dist/hooks/json-hooks.d.ts +1 -1
  30. package/dist/hooks/json-hooks.js +41 -2
  31. package/dist/hooks/shared.d.ts +5 -1
  32. package/dist/hooks/shared.js +9 -0
  33. package/dist/mcp/request.d.ts +0 -2
  34. package/dist/mcp/request.js +16 -11
  35. package/dist/prompt-hook.d.ts +6 -0
  36. package/dist/prompt-hook.js +15 -7
  37. package/dist/search/vector.js +2 -5
  38. package/dist/server/routes/admin.js +1 -1
  39. package/dist/server/types.d.ts +5 -4
  40. package/dist/server.d.ts +5 -5
  41. package/dist/server.js +13 -14
  42. package/dist/stdin.d.ts +5 -0
  43. package/dist/stdin.js +62 -0
  44. package/dist/store/key-audit.d.ts +6 -0
  45. package/dist/store/key-audit.js +25 -0
  46. package/dist/store-port.d.ts +29 -8
  47. package/dist/store-port.js +28 -1
  48. package/dist/version.d.ts +1 -1
  49. package/dist/version.js +1 -1
  50. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  51. package/extensions/openclaw-plugin/package.json +1 -1
  52. package/openclaw.plugin.json +1 -1
  53. 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).
@@ -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;
@@ -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, or additional-context (Claude Code hook 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; all but claude-code
557
- also patch an existing AGENTS.md
558
- hook uninstall <target> Remove hook`],
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; other tools get a
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}' reads; this code path is not ported to the store yet`);
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
@@ -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);