@pharaoh-so/mcp 0.3.53 → 0.3.56

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 (43) hide show
  1. package/.mcp.json +1 -1
  2. package/CHANGELOG.md +41 -0
  3. package/LICENSE +1 -1
  4. package/NOTICE +28 -0
  5. package/README.md +5 -5
  6. package/commands/plan.md +11 -11
  7. package/commands/review.md +2 -2
  8. package/dist/auth.d.ts +5 -0
  9. package/dist/credentials.d.ts +8 -0
  10. package/dist/gateway.js +1 -1
  11. package/dist/helpers.d.ts +15 -5
  12. package/dist/helpers.js +71 -2
  13. package/dist/index.js +51 -26
  14. package/dist/install-skills.d.ts +30 -5
  15. package/dist/install-skills.js +210 -11
  16. package/dist/refresh.d.ts +8 -2
  17. package/dist/refresh.js +33 -6
  18. package/dist/setup.d.ts +5 -0
  19. package/dist/setup.js +66 -0
  20. package/inspect-tools.json +89 -36
  21. package/{skills → npx-skills}/brainstorm/SKILL.md +2 -1
  22. package/{skills → npx-skills}/debug/SKILL.md +2 -1
  23. package/{skills → npx-skills}/pharaoh/SKILL.md +13 -6
  24. package/{skills → npx-skills}/plan/SKILL.md +13 -14
  25. package/npx-skills/refactor/SKILL.md +38 -0
  26. package/{skills → npx-skills}/review/SKILL.md +4 -5
  27. package/{skills → npx-skills}/tdd/SKILL.md +2 -1
  28. package/{skills → npx-skills}/verify/SKILL.md +2 -1
  29. package/package.json +6 -4
  30. package/project-memory/bundle.mjs +137 -0
  31. package/project-memory/execute-turn.mjs +240 -0
  32. package/project-memory/hook-config.mjs +91 -0
  33. package/project-memory/hook-events.mjs +141 -0
  34. package/project-memory/local.mjs +463 -0
  35. package/project-memory/memory-api.mjs +182 -0
  36. package/project-memory/pharaoh-memory-hook.mjs +33 -0
  37. package/project-memory/pharaoh-memory.mjs +55 -0
  38. package/project-memory/setup.d.mts +14 -0
  39. package/project-memory/setup.mjs +151 -0
  40. package/project-memory/user-target.mjs +221 -0
  41. package/trace-app.html +222 -101
  42. package/skills/.gitkeep +0 -0
  43. package/skills/refactor/SKILL.md +0 -34
@@ -0,0 +1,55 @@
1
+ import { dirname } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { executeTurn } from "./execute-turn.mjs";
4
+ import { checkout, readStdin, turnStore } from "./local.mjs";
5
+ import { PilotError, refreshHelper } from "./memory-api.mjs";
6
+ import {
7
+ activeMemoryRuntime,
8
+ cachedMemoryCheckout,
9
+ readMemoryCredentials,
10
+ } from "./user-target.mjs";
11
+
12
+ async function main(args) {
13
+ const [operation, option, token, compactFlag, ...extra] = args;
14
+ if (operation === "doctor" && args.length === 1) {
15
+ await refreshHelper();
16
+ process.stdout.write("The local Pharaoh refresh helper is ready.\n");
17
+ return;
18
+ }
19
+ if (
20
+ option !== "--turn" ||
21
+ !/^[a-f0-9]{64}$/.test(token) ||
22
+ extra.length > 0 ||
23
+ (compactFlag !== undefined && (compactFlag !== "--compact" || operation !== "recall")) ||
24
+ !["recall", "expand", "capture", "nothing-material", "receipt"].includes(operation)
25
+ ) {
26
+ throw new PilotError(
27
+ "Use doctor, or recall, expand, capture, receipt, or nothing-material with the active hook's --turn token.",
28
+ );
29
+ }
30
+ const cwd = process.cwd();
31
+ const target = checkout(cwd);
32
+ if (!target) throw new PilotError("Run this inside the enrolled GitHub checkout.");
33
+ const runtime = dirname(fileURLToPath(import.meta.url));
34
+ const active =
35
+ activeMemoryRuntime("claude-code", runtime) || activeMemoryRuntime("codex", runtime);
36
+ const eligible = active && (await cachedMemoryCheckout(cwd, await readMemoryCredentials()));
37
+ const result = await executeTurn(operation, token, {
38
+ target: eligible ? { ...target, runtime } : target,
39
+ store: turnStore(),
40
+ read: () => readStdin(8_192),
41
+ compact: compactFlag === "--compact" || process.env.PHARAOH_PROJECT_MEMORY_COMPACT === "true",
42
+ });
43
+ process.stdout.write(`${JSON.stringify(result)}\n`);
44
+ }
45
+
46
+ try {
47
+ await main(process.argv.slice(2));
48
+ } catch (error) {
49
+ const message =
50
+ error instanceof PilotError
51
+ ? error.message
52
+ : "Pharaoh project memory is temporarily unavailable.";
53
+ process.stderr.write(`${message}\n`);
54
+ process.exitCode = 1;
55
+ }
@@ -0,0 +1,14 @@
1
+ export function syncMemoryHooks(options?: {
2
+ credentials?: {
3
+ sse_url: string;
4
+ project_memory?: Array<{ repo: string; revision: string | null }>;
5
+ project_memory_status?: "unavailable";
6
+ };
7
+ clients?: string[];
8
+ home?: string;
9
+ env?: NodeJS.ProcessEnv;
10
+ cwd?: string;
11
+ node?: string;
12
+ removeLegacy?: boolean;
13
+ packageRoot?: URL;
14
+ }): string[];
@@ -0,0 +1,151 @@
1
+ import { lstatSync, mkdirSync, realpathSync, rmSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, isAbsolute, join } from "node:path";
4
+ import { bundledHook, installMemoryBundle, packagedMemoryBundle } from "./bundle.mjs";
5
+ import { hasLegacyHooks, updateHooks, updateUserHooks } from "./hook-config.mjs";
6
+ import { atomicWrite, checkout, git, hasMemoryGit } from "./local.mjs";
7
+ import {
8
+ eligibleCheckout,
9
+ legacySettingsPath,
10
+ memoryClientRoot,
11
+ projectMemoryHint,
12
+ readActiveMemoryClients,
13
+ readHookSettings,
14
+ } from "./user-target.mjs";
15
+
16
+ function userSettings(client, home, env) {
17
+ const codex = client === "codex";
18
+ const directory =
19
+ (codex ? env.CODEX_HOME : env.CLAUDE_CONFIG_DIR) ?? join(home, codex ? ".codex" : ".claude");
20
+ if (!isAbsolute(directory)) throw new Error("Client configuration directory must be absolute.");
21
+ return join(directory, codex ? "hooks.json" : "settings.json");
22
+ }
23
+
24
+ function writeSettings(path, previous, next) {
25
+ if (JSON.stringify(previous) === JSON.stringify(next)) return;
26
+ const directory = dirname(path);
27
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
28
+ const info = lstatSync(directory);
29
+ if (!info.isDirectory() || info.uid !== process.getuid?.())
30
+ throw new Error("Client settings directory is not owner-controlled.");
31
+ atomicWrite(path, `${JSON.stringify(next, null, "\t")}\n`);
32
+ }
33
+
34
+ function migrateCheckout(client, target) {
35
+ if (!target) return;
36
+ const path = legacySettingsPath(client, target.root);
37
+ const before = readHookSettings(path);
38
+ if (!hasLegacyHooks(before, client, target.root)) return;
39
+ const relative = path.slice(target.root.length + 1);
40
+ if (realpathSync(dirname(path)) !== dirname(path) || git(target.root, "ls-files", "--", relative))
41
+ throw new Error("The earlier checkout hook is in tracked or linked settings.");
42
+ // The legacy installer always wrote ignored files. Never create or alter
43
+ // Git excludes, and never migrate project settings that can be committed.
44
+ git(target.root, "check-ignore", "--quiet", "--no-index", "--", relative);
45
+ writeSettings(path, before, updateHooks(before, client, target, true));
46
+ }
47
+
48
+ /** Run only as part of a deliberate normal setup, upgrade, clean or logout. */
49
+ export function syncMemoryHooks({
50
+ credentials,
51
+ clients = [],
52
+ home = homedir(),
53
+ env = process.env,
54
+ cwd = process.cwd(),
55
+ node = process.execPath,
56
+ removeLegacy = false,
57
+ packageRoot,
58
+ } = {}) {
59
+ const hint = projectMemoryHint(credentials);
60
+ const root = memoryClientRoot(home);
61
+ const previous = readActiveMemoryClients(home);
62
+ if (!removeLegacy && credentials?.project_memory_status === "unavailable") {
63
+ return [
64
+ "Pharaoh: project memory eligibility could not be checked. Existing hook settings were kept. Rerun normal setup to retry.",
65
+ ];
66
+ }
67
+ if (!hint.length && !Object.keys(previous).length && !removeLegacy) return [];
68
+ const notices = [];
69
+ const active = {};
70
+ const configured = [];
71
+ let installed;
72
+ let digest;
73
+ const target = removeLegacy ? checkout(cwd) : eligibleCheckout(cwd, hint);
74
+ const pendingCheckout =
75
+ !target && hint.some((entry) => entry?.revision === null) ? checkout(cwd) : undefined;
76
+ const pendingRoot =
77
+ pendingCheckout &&
78
+ hint.some(
79
+ (entry) =>
80
+ entry?.revision === null &&
81
+ entry.repo?.toLowerCase() === pendingCheckout.repo.toLowerCase(),
82
+ )
83
+ ? pendingCheckout.root
84
+ : undefined;
85
+
86
+ if (hint.length) {
87
+ try {
88
+ if (!hasMemoryGit || typeof process.getuid !== "function")
89
+ throw new Error("Project memory needs a POSIX client with system Git.");
90
+ const bundle = packagedMemoryBundle(packageRoot);
91
+ digest = bundle.sha256;
92
+ installed = bundledHook(installMemoryBundle(root, target?.root ?? pendingRoot, bundle));
93
+ } catch (error) {
94
+ return [`Pharaoh: project memory setup unavailable. ${error.message}`];
95
+ }
96
+ }
97
+
98
+ for (const client of ["claude-code", "codex"]) {
99
+ const desired = clients.includes(client) ? installed : undefined;
100
+ if (!desired && !previous[client] && !removeLegacy) continue;
101
+ try {
102
+ const path = userSettings(client, home, env);
103
+ const before = readHookSettings(path);
104
+ writeSettings(path, before, updateUserHooks(before, client, desired, node));
105
+ if (desired) {
106
+ active[client] = digest;
107
+ configured.push(client);
108
+ }
109
+ } catch {
110
+ if (desired && previous[client]) active[client] = previous[client];
111
+ if (desired || previous[client])
112
+ notices.push(
113
+ `Pharaoh: project memory could not update ${client} hooks. Check the client settings and rerun normal setup.`,
114
+ );
115
+ }
116
+ }
117
+
118
+ // This private activation record makes older hook commands inert even if a
119
+ // client still has them loaded or its settings could not be updated.
120
+ const activePath = join(root, "active.json");
121
+ if (Object.keys(active).length) {
122
+ if (JSON.stringify(previous) !== JSON.stringify(active))
123
+ atomicWrite(activePath, `${JSON.stringify(active)}\n`);
124
+ if (configured.length) {
125
+ notices.push(
126
+ "Pharaoh project memory is set up for your eligible repositories. Start a new local conversation.",
127
+ );
128
+ if (hint.some((entry) => entry?.revision === null))
129
+ notices.push(
130
+ "A connected repository is still mapping. Its hooks will start memory automatically when the mapped revision is available.",
131
+ );
132
+ if (configured.includes("codex"))
133
+ notices.push("In Codex, review and trust the Pharaoh hooks in /hooks or Settings > Hooks.");
134
+ }
135
+ } else if (Object.keys(previous).length) {
136
+ rmSync(activePath, { force: true });
137
+ }
138
+ // The old checkout hook is a fallback until its replacement settings and
139
+ // the private activation record are both in place.
140
+ for (const client of ["claude-code", "codex"]) {
141
+ if (!removeLegacy && !configured.includes(client)) continue;
142
+ try {
143
+ migrateCheckout(client, target);
144
+ } catch {
145
+ notices.push(
146
+ `Pharaoh: the earlier ${client} checkout hook remains in place for this checkout.`,
147
+ );
148
+ }
149
+ }
150
+ return notices;
151
+ }
@@ -0,0 +1,221 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { lstatSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { hasLegacyHooks } from "./hook-config.mjs";
6
+ import { atomicWrite, checkout, git } from "./local.mjs";
7
+ import { PROJECT_MEMORY_ORIGIN, requestMemoryReadiness } from "./memory-api.mjs";
8
+
9
+ const REVISION = /^(?!0{40}$)[a-f0-9]{40}$/;
10
+ const READINESS_RETRY_MS = 15_000;
11
+ const LEGACY_SETTINGS = {
12
+ "claude-code": ".claude/settings.local.json",
13
+ codex: ".codex/hooks.json",
14
+ };
15
+
16
+ export function legacySettingsPath(client, root) {
17
+ return join(root, LEGACY_SETTINGS[client]);
18
+ }
19
+
20
+ export function projectMemoryHint(credentials) {
21
+ if (!credentials || !Array.isArray(credentials.project_memory)) return [];
22
+ try {
23
+ if (new URL(credentials.sse_url).origin !== PROJECT_MEMORY_ORIGIN) return [];
24
+ } catch {
25
+ return [];
26
+ }
27
+ return credentials.project_memory;
28
+ }
29
+
30
+ function matchingHint(target, hints) {
31
+ return hints.find(
32
+ (entry) =>
33
+ typeof entry?.repo === "string" && entry.repo.toLowerCase() === target.repo.toLowerCase(),
34
+ );
35
+ }
36
+
37
+ function hasMappedCommit(target, revision) {
38
+ if (typeof revision !== "string" || !REVISION.test(revision)) return false;
39
+ try {
40
+ // An origin URL is editable. Require an actual GitHub-mapped commit
41
+ // locally, without fetching, consulting replace refs, or running hooks.
42
+ return git(target.root, "cat-file", "-t", revision) === "commit";
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ export function eligibleCheckout(cwd, hints) {
49
+ if (!hints.length) return;
50
+ const target = checkout(cwd);
51
+ if (target && hasMappedCommit(target, matchingHint(target, hints)?.revision)) return target;
52
+ }
53
+
54
+ function hash(value) {
55
+ return createHash("sha256").update(value).digest("hex");
56
+ }
57
+
58
+ function readReadiness(path, binding) {
59
+ try {
60
+ const stat = lstatSync(path);
61
+ if (!stat.isFile() || stat.uid !== process.getuid?.() || stat.mode & 0o077 || stat.size > 512)
62
+ return;
63
+ const state = JSON.parse(readFileSync(path, "utf8"));
64
+ if (state.binding === binding) return state;
65
+ } catch {
66
+ // A missing or unreadable cache cannot authorize a checkout.
67
+ }
68
+ }
69
+
70
+ function tryReadinessLease(path, now) {
71
+ try {
72
+ try {
73
+ const stat = lstatSync(path);
74
+ if (
75
+ !stat.isFile() ||
76
+ stat.uid !== process.getuid?.() ||
77
+ now - stat.mtimeMs < READINESS_RETRY_MS
78
+ )
79
+ return;
80
+ rmSync(path, { force: true });
81
+ } catch (error) {
82
+ if (error.code !== "ENOENT") return;
83
+ }
84
+ const owner = randomUUID();
85
+ writeFileSync(path, owner, { flag: "wx", mode: 0o600 });
86
+ return () => {
87
+ try {
88
+ if (readFileSync(path, "utf8") === owner) rmSync(path, { force: true });
89
+ } catch {
90
+ // A stale lease expires without blocking ordinary work.
91
+ }
92
+ };
93
+ } catch {
94
+ // Another hook is already checking or private local state is unavailable.
95
+ }
96
+ }
97
+
98
+ async function resolveRevision(repo, credentials, probe) {
99
+ const session = credentials.project_memory_session;
100
+ if (typeof session !== "string" || !/^[0-9a-f-]{36}$/.test(session)) return;
101
+ const binding = hash(session);
102
+ const path = join(memoryClientRoot(), `ready-${hash(repo.toLowerCase())}.json`);
103
+ const saved = readReadiness(path, binding);
104
+ if (typeof saved?.revision === "string" && REVISION.test(saved.revision)) return saved.revision;
105
+ const now = Date.now();
106
+ if (!probe || now - (saved?.checkedAt ?? 0) < READINESS_RETRY_MS) return;
107
+ const release = tryReadinessLease(`${path}.lock`, now);
108
+ if (!release) return;
109
+ try {
110
+ const latest = readReadiness(path, binding);
111
+ if (typeof latest?.revision === "string" && REVISION.test(latest.revision))
112
+ return latest.revision;
113
+ if (now - (latest?.checkedAt ?? 0) < READINESS_RETRY_MS) return;
114
+ atomicWrite(path, JSON.stringify({ binding, checkedAt: now }));
115
+ const revision = await requestMemoryReadiness(repo, credentials);
116
+ if (typeof revision === "string" && REVISION.test(revision)) {
117
+ atomicWrite(path, JSON.stringify({ binding, checkedAt: now, revision }));
118
+ return revision;
119
+ }
120
+ } catch {
121
+ // A cache failure must not activate memory or block the normal prompt.
122
+ } finally {
123
+ release();
124
+ }
125
+ }
126
+
127
+ async function resolveMappedCheckout(target, credentials, hint, probe) {
128
+ const revision =
129
+ hint.revision === null ? await resolveRevision(hint.repo, credentials, probe) : hint.revision;
130
+ if (hasMappedCommit(target, revision)) return target;
131
+ }
132
+
133
+ export async function cachedMemoryCheckout(cwd, credentials) {
134
+ const hints = projectMemoryHint(credentials);
135
+ if (!hints.length) return;
136
+ const target = checkout(cwd);
137
+ const hint = target && matchingHint(target, hints);
138
+ if (hint) return resolveMappedCheckout(target, credentials, hint, false);
139
+ }
140
+
141
+ export function readHookSettings(path) {
142
+ try {
143
+ const info = lstatSync(path);
144
+ if (!info.isFile() || info.uid !== process.getuid?.() || info.size > 512 * 1024)
145
+ throw new Error("Expected a regular owner-controlled hook settings file.");
146
+ return JSON.parse(readFileSync(path, "utf8"));
147
+ } catch (error) {
148
+ if (error.code === "ENOENT") return {};
149
+ throw error;
150
+ }
151
+ }
152
+
153
+ export async function resolveMemoryHookTarget(client, cwd, credentials, probe = false) {
154
+ if (!Object.hasOwn(LEGACY_SETTINGS, client)) return;
155
+ const hints = projectMemoryHint(credentials);
156
+ if (!hints.length) return;
157
+ const target = checkout(cwd);
158
+ const hint = target && matchingHint(target, hints);
159
+ if (!hint) return;
160
+ try {
161
+ // A checkout enrolled by the original pilot may still have its own hook.
162
+ // Leave that hook in charge until normal setup migrates the checkout.
163
+ if (
164
+ hasLegacyHooks(readHookSettings(legacySettingsPath(client, target.root)), client, target.root)
165
+ )
166
+ return;
167
+ } catch {
168
+ // A client settings file we cannot inspect is not safe to double-enroll.
169
+ return;
170
+ }
171
+ return resolveMappedCheckout(target, credentials, hint, probe);
172
+ }
173
+
174
+ export function memoryClientRoot(home = homedir()) {
175
+ return join(home, ".pharaoh", "project-memory-client");
176
+ }
177
+
178
+ export function readActiveMemoryClients(home = homedir()) {
179
+ try {
180
+ const root = memoryClientRoot(home);
181
+ const directory = lstatSync(root);
182
+ if (!directory.isDirectory() || directory.uid !== process.getuid?.() || directory.mode & 0o077)
183
+ return {};
184
+ const path = join(root, "active.json");
185
+ const file = lstatSync(path);
186
+ if (!file.isFile() || file.uid !== process.getuid?.() || file.mode & 0o077 || file.size > 512)
187
+ return {};
188
+ const active = JSON.parse(readFileSync(path, "utf8"));
189
+ return Object.fromEntries(
190
+ Object.keys(LEGACY_SETTINGS).flatMap((client) => {
191
+ const digest = active?.[client];
192
+ return typeof digest === "string" && /^[a-f0-9]{64}$/.test(digest)
193
+ ? [[client, digest]]
194
+ : [];
195
+ }),
196
+ );
197
+ } catch {
198
+ return {};
199
+ }
200
+ }
201
+
202
+ export function activeMemoryRuntime(client, directory, home = homedir()) {
203
+ const digest = readActiveMemoryClients(home)[client];
204
+ return Boolean(digest && directory === join(realpathSync(memoryClientRoot(home)), digest));
205
+ }
206
+
207
+ export async function readMemoryCredentials() {
208
+ try {
209
+ const { CREDENTIALS_PATH, readCredentials } = await import("./runtime/credentials.js");
210
+ const stat = lstatSync(CREDENTIALS_PATH);
211
+ if (
212
+ stat.isFile() &&
213
+ stat.uid === process.getuid?.() &&
214
+ !(stat.mode & 0o077) &&
215
+ stat.size <= 65_536
216
+ )
217
+ return readCredentials();
218
+ } catch {
219
+ // No current owner-private credential file.
220
+ }
221
+ }