@pharaoh-so/mcp 0.3.59 → 0.3.61

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.
@@ -4,6 +4,7 @@ import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
4
4
  import { bundledHook, installMemoryBundle, packagedMemoryBundle } from "./bundle.mjs";
5
5
  import { hasLegacyHooks, hasUserHooks, updateHooks, updateUserHooks } from "./hook-config.mjs";
6
6
  import { atomicWrite, checkout, git, hasMemoryGit } from "./local.mjs";
7
+ import { MEMORY_HOOK_CLIENTS, readHookPreferences, writeHookPreferences } from "./preferences.mjs";
7
8
  import {
8
9
  legacySettingsPath,
9
10
  matchingMemoryHint,
@@ -13,8 +14,6 @@ import {
13
14
  readHookSettings,
14
15
  } from "./user-target.mjs";
15
16
 
16
- const CLIENTS = ["claude-code", "codex"];
17
-
18
17
  function userSettings(client, home, env) {
19
18
  const codex = client === "codex";
20
19
  const directory =
@@ -82,7 +81,7 @@ function migrateCheckout(client, target, notices) {
82
81
  }
83
82
  }
84
83
 
85
- function syncUserHooks(client, home, env, target, hookPath, node, alreadyActive) {
84
+ function syncUserHooks(client, home, env, target, hookPath, node, alreadyActive, graphCues) {
86
85
  let known = Boolean(alreadyActive);
87
86
  try {
88
87
  const settings = inspectSettings(userSettings(client, home, env));
@@ -95,7 +94,7 @@ function syncUserHooks(client, home, env, target, hookPath, node, alreadyActive)
95
94
  hasLegacyHooks(settings.config, client, target.root);
96
95
  writeSettings(
97
96
  settings,
98
- updateUserHooks(settings.config, client, hookPath, node),
97
+ updateUserHooks(settings.config, client, hookPath, node, graphCues),
99
98
  target,
100
99
  checkoutHook,
101
100
  );
@@ -110,10 +109,118 @@ function writeActivation(path, active) {
110
109
  else rmSync(path, { force: true });
111
110
  }
112
111
 
112
+ /** Resolve explicit choices only after a supported client's MCP registration. */
113
+ export async function chooseMemoryHooks({
114
+ credentials,
115
+ clients,
116
+ requested = false,
117
+ ask,
118
+ home = homedir(),
119
+ env = process.env,
120
+ }) {
121
+ const choices = { accepted: [], declined: [], graphClients: [], notices: [] };
122
+ if (typeof process.getuid !== "function") {
123
+ if (requested)
124
+ choices.notices.push("Pharaoh: project memory hooks need a POSIX client with system Git.");
125
+ return choices;
126
+ }
127
+ if (credentials?.project_memory_status === "unavailable") {
128
+ if (requested)
129
+ choices.notices.push(
130
+ "Pharaoh: project memory eligibility could not be checked. Retry setup to enable hooks.",
131
+ );
132
+ return choices;
133
+ }
134
+ if (!projectMemoryHint(credentials).length) {
135
+ if (requested) choices.notices.push("Pharaoh: this session has no project memory hook grant.");
136
+ return credentials ? { ...choices, removeUnentitled: true } : choices;
137
+ }
138
+ if (!clients.length || !hasMemoryGit) {
139
+ if (requested && !hasMemoryGit)
140
+ choices.notices.push("Pharaoh: project memory hooks need a POSIX client with system Git.");
141
+ return choices;
142
+ }
143
+ const previous = readHookPreferences(home);
144
+ const active = readActiveMemoryClients(home);
145
+ const next = { ...previous };
146
+ for (const client of MEMORY_HOOK_CLIENTS) {
147
+ if (!clients.includes(client)) continue;
148
+ const label = client === "codex" ? "Codex" : "Claude Code";
149
+ let config;
150
+ try {
151
+ config = inspectSettings(userSettings(client, home, env)).config;
152
+ } catch {
153
+ // An unreadable settings file cannot establish a previous installation.
154
+ }
155
+ if (client === "claude-code" && config?.disableAllHooks === true) continue;
156
+ const choice = { ...previous[client] };
157
+ const memoryHook =
158
+ active[client] &&
159
+ hasUserHooks({ hooks: { UserPromptSubmit: config?.hooks?.UserPromptSubmit } }, client);
160
+ if (requested) choice.memory = true;
161
+ else if (choice.memory !== false && (choice.memory !== true || !memoryHook)) {
162
+ const answer = await ask(
163
+ `Enable Pharaoh project memory hooks in ${label} (recommended)? [Y/n] `,
164
+ true,
165
+ );
166
+ if (answer === null) continue;
167
+ choice.memory = answer;
168
+ if (!answer && choice.graph === true) delete choice.graph;
169
+ }
170
+ if (choice.memory === true) {
171
+ let graphEnabled =
172
+ choice.graph === true &&
173
+ ["PreToolUse", "PostToolUse"].every((event) =>
174
+ hasUserHooks({ hooks: { [event]: config?.hooks?.[event] } }, client),
175
+ );
176
+ if (requested || choice.graph === undefined || (choice.graph === true && !graphEnabled)) {
177
+ const answer = await ask(
178
+ `Enable advisory Pharaoh worktree cues in ${label}? [y/N] `,
179
+ false,
180
+ );
181
+ if (answer !== null) {
182
+ choice.graph = answer;
183
+ graphEnabled = answer;
184
+ }
185
+ }
186
+ choices.accepted.push(client);
187
+ if (graphEnabled) choices.graphClients.push(client);
188
+ } else {
189
+ choices.declined.push(client);
190
+ }
191
+ next[client] = choice;
192
+ }
193
+ writeHookPreferences(next, home, previous);
194
+ return choices;
195
+ }
196
+
197
+ /** Apply the outcome against the saved choices, which can change after the prompt. */
198
+ export function applyMemoryHookChoices(decision, options = {}) {
199
+ const { accepted, declined, graphClients, removeUnentitled } = decision;
200
+ const notices = [...decision.notices];
201
+ if (removeUnentitled)
202
+ return [...notices, ...syncMemoryHooks({ ...options, clients: MEMORY_HOOK_CLIENTS })];
203
+
204
+ if (declined.length) {
205
+ const saved = readHookPreferences(options.home);
206
+ const clients = declined.filter((client) => saved[client]?.memory === false);
207
+ if (clients.length) notices.push(...syncMemoryHooks({ ...options, clients }));
208
+ }
209
+ if (accepted.length) {
210
+ const saved = readHookPreferences(options.home);
211
+ const clients = accepted.filter((client) => saved[client]?.memory === true);
212
+ const cues = graphClients.filter((client) => saved[client]?.graph === true);
213
+ if (clients.length)
214
+ notices.push(...syncMemoryHooks({ ...options, clients, graphClients: cues, enable: true }));
215
+ }
216
+ return notices;
217
+ }
218
+
113
219
  /** Run only during setup or explicit cleanup of the local memory hooks. */
114
220
  export function syncMemoryHooks({
115
221
  credentials,
116
- clients = [],
222
+ clients = MEMORY_HOOK_CLIENTS,
223
+ graphClients = [],
117
224
  enable = false,
118
225
  home = homedir(),
119
226
  env = process.env,
@@ -150,8 +257,8 @@ export function syncMemoryHooks({
150
257
  }
151
258
  }
152
259
 
153
- for (const client of CLIENTS) {
154
- if (enable && !clients.includes(client)) continue;
260
+ for (const client of MEMORY_HOOK_CLIENTS) {
261
+ if (!clients.includes(client)) continue;
155
262
  const next = { ...active, [client]: digest };
156
263
  try {
157
264
  // Enable the replacement before swapping any settings, so failed
@@ -159,7 +266,16 @@ export function syncMemoryHooks({
159
266
  const replacing = enable && active[client] !== digest;
160
267
  if (replacing) writeActivation(activePath, next);
161
268
  try {
162
- syncUserHooks(client, home, env, target, installed, node, active[client]);
269
+ syncUserHooks(
270
+ client,
271
+ home,
272
+ env,
273
+ target,
274
+ installed,
275
+ node,
276
+ active[client],
277
+ graphClients.includes(client),
278
+ );
163
279
  } catch (error) {
164
280
  if (replacing) writeActivation(activePath, active);
165
281
  throw error;
@@ -176,8 +292,16 @@ export function syncMemoryHooks({
176
292
  }
177
293
 
178
294
  // Deactivate loaded user hooks even if known settings could not be edited.
179
- if (!enable && Object.keys(active).length) writeActivation(activePath, {});
180
- for (const client of enable ? configured : CLIENTS) migrateCheckout(client, target, notices);
295
+ if (!enable) {
296
+ const remaining = Object.fromEntries(
297
+ Object.entries(active).filter(([client]) => !clients.includes(client)),
298
+ );
299
+ if (Object.keys(remaining).length !== Object.keys(active).length)
300
+ writeActivation(activePath, remaining);
301
+ }
302
+ for (const client of enable ? configured : clients) {
303
+ if (MEMORY_HOOK_CLIENTS.includes(client)) migrateCheckout(client, target, notices);
304
+ }
181
305
  if (configured.length) {
182
306
  notices.push(
183
307
  "Pharaoh project memory reminders are set up for your eligible repositories. Start a new local conversation.",