@pharaoh-so/mcp 0.3.54 → 0.3.57

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 +36 -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 +1 -1
  12. package/dist/helpers.js +15 -2
  13. package/dist/index.js +47 -28
  14. package/dist/install-skills.d.ts +4 -3
  15. package/dist/install-skills.js +12 -8
  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 +269 -0
  32. package/project-memory/hook-config.mjs +91 -0
  33. package/project-memory/hook-events.mjs +117 -0
  34. package/project-memory/local.mjs +480 -0
  35. package/project-memory/memory-api.mjs +200 -0
  36. package/project-memory/pharaoh-memory-hook.mjs +47 -0
  37. package/project-memory/pharaoh-memory.mjs +60 -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 +235 -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,200 @@
1
+ export const PROJECT_MEMORY_ORIGIN = "https://mcp.pharaoh.so";
2
+ const READY_TIMEOUT_MS = 1_200;
3
+ const REQUEST_TIMEOUT_MS = 12_000;
4
+ const BUILD = new URL("runtime/refresh.js", import.meta.url);
5
+ const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
6
+ const STATUS = new Set(["candidate", "approved", "superseded", "retracted"]);
7
+
8
+ export class PilotError extends Error {
9
+ constructor(message, rejected = false) {
10
+ super(message);
11
+ this.rejected = rejected;
12
+ }
13
+ }
14
+
15
+ async function responseJson(response) {
16
+ if (!response.body) throw new PilotError("Pharaoh returned an incomplete response.");
17
+ const chunks = [];
18
+ let bytes = 0;
19
+ for await (const chunk of response.body) {
20
+ bytes += chunk.length;
21
+ if (bytes > 32_768) throw new PilotError("Pharaoh returned an oversized response.");
22
+ chunks.push(chunk);
23
+ }
24
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
25
+ }
26
+
27
+ export async function refreshHelper() {
28
+ try {
29
+ const helper = await import(BUILD.href);
30
+ if (typeof helper.originBoundRefresh === "function") return helper;
31
+ } catch {
32
+ // Missing or incomplete build.
33
+ }
34
+ throw new PilotError(
35
+ "The installed project memory bundle has no compatible refresh helper. Rerun normal Pharaoh setup.",
36
+ );
37
+ }
38
+
39
+ /** Check only mapping metadata using the current device session. Normal proxy
40
+ * startup handles token refresh; the prompt hook never waits on a refresh lock. */
41
+ export async function requestMemoryReadiness(repo, credentials, deadline = Infinity) {
42
+ const expiresAt = Date.parse(credentials?.expires_at);
43
+ if (typeof credentials?.access_token !== "string" || !(expiresAt > Date.now())) return;
44
+ const timeout = Math.min(READY_TIMEOUT_MS, Math.floor(deadline - performance.now()));
45
+ if (timeout < 1) return;
46
+ try {
47
+ if (new URL(credentials.sse_url).origin !== PROJECT_MEMORY_ORIGIN) return;
48
+ const response = await fetch(`${PROJECT_MEMORY_ORIGIN}/api/project-memory/readiness`, {
49
+ method: "POST",
50
+ redirect: "error",
51
+ headers: {
52
+ "Content-Type": "application/json",
53
+ Authorization: `Bearer ${credentials.access_token}`,
54
+ },
55
+ body: JSON.stringify({ repo }),
56
+ signal: AbortSignal.timeout(timeout),
57
+ });
58
+ if (!response.ok) {
59
+ await response.body?.cancel();
60
+ return;
61
+ }
62
+ const result = await responseJson(response);
63
+ return result?.revision;
64
+ } catch {
65
+ // Pending, denied, unavailable and timed-out lookups leave hooks dormant.
66
+ }
67
+ }
68
+
69
+ export async function requestMemory(
70
+ operation,
71
+ payload,
72
+ { fetcher = fetch, helper, credentials } = {},
73
+ ) {
74
+ if (!["recall", "capture", "expand"].includes(operation))
75
+ throw new PilotError("Unknown memory request.");
76
+ // Capture supplies its session snapshot so dispatch cannot adopt a different sign-in.
77
+ if (!credentials) {
78
+ const { originBoundRefresh } = helper ?? (await refreshHelper());
79
+ const outcome = await originBoundRefresh(PROJECT_MEMORY_ORIGIN).load();
80
+ if (outcome?.ok) credentials = outcome.creds;
81
+ }
82
+ if (!credentials)
83
+ throw new PilotError(
84
+ "Connect to Pharaoh with its device sign-in, or retry if sign-in is temporarily unavailable.",
85
+ );
86
+ let response;
87
+ try {
88
+ response = await fetcher(`${PROJECT_MEMORY_ORIGIN}/api/project-memory/${operation}`, {
89
+ method: "POST",
90
+ redirect: "error",
91
+ headers: {
92
+ "Content-Type": "application/json",
93
+ Authorization: `Bearer ${credentials.access_token}`,
94
+ },
95
+ body: JSON.stringify(payload),
96
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
97
+ });
98
+ } catch {
99
+ throw new PilotError(
100
+ operation === "capture"
101
+ ? "Pharaoh did not confirm whether this capture was saved. Check the project memory inspector before retrying."
102
+ : "Pharaoh project memory could not respond before the request ended.",
103
+ );
104
+ }
105
+ if (!response.ok) {
106
+ await response.body?.cancel();
107
+ // The shared credential file can now name a different OAuth session. Do
108
+ // not reuse an already prepared operation under newly loaded credentials.
109
+ if (response.status === 401)
110
+ throw new PilotError("Pharaoh rejected this sign-in. Reconnect before trying again.", true);
111
+ if (response.status === 404)
112
+ throw new PilotError("Project memory is unavailable for this repository or account.", true);
113
+ if (response.status === 400 || response.status === 413)
114
+ throw new PilotError(
115
+ "Pharaoh rejected the memory payload. Check its shape, size, and sensitive content.",
116
+ true,
117
+ );
118
+ if (response.status === 409)
119
+ throw new PilotError(
120
+ "Pharaoh could not save this candidate because the operation conflicts or the project log is full.",
121
+ true,
122
+ );
123
+ if (response.status === 429)
124
+ throw new PilotError("Pharaoh project memory reached its request limit.", true);
125
+ if (operation === "capture" && response.status >= 500) {
126
+ throw new PilotError(
127
+ "Pharaoh did not confirm whether this capture was saved. Check the project memory inspector before retrying.",
128
+ );
129
+ }
130
+ throw new PilotError(
131
+ "Pharaoh project memory is temporarily unavailable.",
132
+ response.status >= 400 && response.status < 500,
133
+ );
134
+ }
135
+ let result;
136
+ try {
137
+ result = await responseJson(response);
138
+ } catch (error) {
139
+ if (operation === "capture")
140
+ throw new PilotError(
141
+ "Pharaoh did not confirm whether this capture was saved. Check the project memory inspector before retrying.",
142
+ );
143
+ throw error;
144
+ }
145
+ if (
146
+ operation === "recall" &&
147
+ Array.isArray(result?.records) &&
148
+ result.records.length <= 5 &&
149
+ result.records.every((record) => typeof record?.id === "string" && UUID.test(record.id))
150
+ ) {
151
+ if (payload.format === "compact-v1") {
152
+ if (
153
+ result.format === "compact-v1" &&
154
+ typeof result.repo === "string" &&
155
+ result.repo.toLowerCase() === payload.repo.toLowerCase() &&
156
+ typeof result.moreMatches === "boolean" &&
157
+ Number.isInteger(result.omittedRecords) &&
158
+ result.omittedRecords >= 0 &&
159
+ result.records.every(
160
+ (record) =>
161
+ Number.isInteger(record.version) &&
162
+ record.version >= 1 &&
163
+ record.version <= 1_000_000 &&
164
+ STATUS.has(record.status),
165
+ )
166
+ )
167
+ return result;
168
+ throw new PilotError("Pharaoh returned an incomplete compact recall packet.");
169
+ }
170
+ return { records: result.records, graph: result.graph };
171
+ }
172
+ if (
173
+ operation === "expand" &&
174
+ result?.record?.id === payload.id &&
175
+ result.record.version === payload.version &&
176
+ STATUS.has(result.record.status) &&
177
+ Number.isInteger(result.current?.version) &&
178
+ result.current.version >= payload.version &&
179
+ STATUS.has(result.current.status)
180
+ ) {
181
+ const omitted = result.omittedHistoricalEvidence;
182
+ return {
183
+ record: result.record,
184
+ current: result.current,
185
+ ...(Number.isInteger(omitted) && omitted >= 0 && { omittedHistoricalEvidence: omitted }),
186
+ };
187
+ }
188
+ if (
189
+ operation === "capture" &&
190
+ typeof result?.record?.id === "string" &&
191
+ UUID.test(result.record.id)
192
+ ) {
193
+ return { record: { id: result.record.id, status: result.record.status } };
194
+ }
195
+ if (operation === "capture")
196
+ throw new PilotError(
197
+ "Pharaoh did not confirm whether this capture was saved. Check the project memory inspector before retrying.",
198
+ );
199
+ throw new PilotError("Pharaoh returned an incomplete response.");
200
+ }
@@ -0,0 +1,47 @@
1
+ import { dirname } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { handleEvent } from "./hook-events.mjs";
4
+ import { checkout, readStdin } from "./local.mjs";
5
+ import {
6
+ activeMemoryRuntime,
7
+ readMemoryCredentials,
8
+ resolveMemoryHookTarget,
9
+ } from "./user-target.mjs";
10
+
11
+ // Leave margin inside the client's three-second hook timeout. The process
12
+ // clock includes startup and imports, and no late network request can start.
13
+ const deadline = 2_400;
14
+ const exitQuietly = setTimeout(() => process.exit(0), Math.max(0, deadline - performance.now()));
15
+ try {
16
+ const [client, root, repo] = process.argv.slice(2);
17
+ const input = await readStdin(1024 * 1024);
18
+ const credentials = await readMemoryCredentials();
19
+ let target = { root, repo };
20
+ if (root === "--user") {
21
+ const runtime = dirname(fileURLToPath(import.meta.url));
22
+ if (!activeMemoryRuntime(client, runtime)) process.exit(0);
23
+ const isPrompt = input?.hook_event_name === "UserPromptSubmit";
24
+ const eligible = await resolveMemoryHookTarget(
25
+ client,
26
+ input?.cwd,
27
+ credentials,
28
+ isPrompt && typeof input?.prompt === "string" && Boolean(input.prompt.trim()),
29
+ deadline,
30
+ );
31
+ // Cleanup must still close the turn after sign-out or loss of eligibility.
32
+ const resolved = eligible ?? (!isPrompt && checkout(input?.cwd, deadline));
33
+ if (!resolved) process.exit(0);
34
+ target = { ...resolved, runtime };
35
+ }
36
+ const result = handleEvent(input, {
37
+ client,
38
+ ...target,
39
+ credentials,
40
+ deadline,
41
+ });
42
+ if (result) process.stdout.write(JSON.stringify(result));
43
+ } catch {
44
+ // Project memory never blocks ordinary work or opens a continuation.
45
+ } finally {
46
+ clearTimeout(exitQuietly);
47
+ }
@@ -0,0 +1,60 @@
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", "receipt"].includes(operation)
25
+ ) {
26
+ throw new PilotError(
27
+ "Use doctor, or recall, expand, capture, or receipt 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 isActive = () =>
35
+ activeMemoryRuntime("claude-code", runtime) || activeMemoryRuntime("codex", runtime);
36
+ const credentials = await readMemoryCredentials();
37
+ const eligible = isActive() && (await cachedMemoryCheckout(cwd, credentials));
38
+ const result = await executeTurn(operation, token, {
39
+ target: eligible ? { ...target, runtime } : target,
40
+ store: turnStore(),
41
+ read: () => readStdin(8_192),
42
+ compact: compactFlag === "--compact" || process.env.PHARAOH_PROJECT_MEMORY_COMPACT !== "false",
43
+ loadCaptureCredentials: async () => {
44
+ const current = await readMemoryCredentials();
45
+ if (eligible && (await cachedMemoryCheckout(cwd, current)) && isActive()) return current;
46
+ },
47
+ });
48
+ if (result !== undefined) process.stdout.write(`${JSON.stringify(result)}\n`);
49
+ }
50
+
51
+ try {
52
+ await main(process.argv.slice(2));
53
+ } catch (error) {
54
+ const message =
55
+ error instanceof PilotError
56
+ ? error.message
57
+ : "Pharaoh project memory is temporarily unavailable.";
58
+ process.stderr.write(`${message}\n`);
59
+ process.exitCode = 1;
60
+ }
@@ -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
+ }