@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,235 @@
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, gitBefore } 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
+ export function projectMemorySession(credentials) {
31
+ const session = credentials?.project_memory_session;
32
+ if (typeof session === "string" && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(session))
33
+ return session;
34
+ }
35
+
36
+ function matchingHint(target, hints) {
37
+ return hints.find(
38
+ (entry) =>
39
+ typeof entry?.repo === "string" && entry.repo.toLowerCase() === target.repo.toLowerCase(),
40
+ );
41
+ }
42
+
43
+ function hasMappedCommit(target, revision, deadline = Infinity) {
44
+ if (typeof revision !== "string" || !REVISION.test(revision)) return false;
45
+ try {
46
+ // An origin URL is editable. Require an actual GitHub-mapped commit
47
+ // locally, without fetching, consulting replace refs, or running hooks.
48
+ return gitBefore(target.root, deadline, "cat-file", "-t", revision) === "commit";
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ export function eligibleCheckout(cwd, hints) {
55
+ if (!hints.length) return;
56
+ const target = checkout(cwd);
57
+ if (target && hasMappedCommit(target, matchingHint(target, hints)?.revision)) return target;
58
+ }
59
+
60
+ function hash(value) {
61
+ return createHash("sha256").update(value).digest("hex");
62
+ }
63
+
64
+ function readReadiness(path, binding) {
65
+ try {
66
+ const stat = lstatSync(path);
67
+ if (!stat.isFile() || stat.uid !== process.getuid?.() || stat.mode & 0o077 || stat.size > 512)
68
+ return;
69
+ const state = JSON.parse(readFileSync(path, "utf8"));
70
+ if (state.binding === binding) return state;
71
+ } catch {
72
+ // A missing or unreadable cache cannot authorize a checkout.
73
+ }
74
+ }
75
+
76
+ function tryReadinessLease(path, now) {
77
+ try {
78
+ try {
79
+ const stat = lstatSync(path);
80
+ if (
81
+ !stat.isFile() ||
82
+ stat.uid !== process.getuid?.() ||
83
+ now - stat.mtimeMs < READINESS_RETRY_MS
84
+ )
85
+ return;
86
+ rmSync(path, { force: true });
87
+ } catch (error) {
88
+ if (error.code !== "ENOENT") return;
89
+ }
90
+ const owner = randomUUID();
91
+ writeFileSync(path, owner, { flag: "wx", mode: 0o600 });
92
+ return () => {
93
+ try {
94
+ if (readFileSync(path, "utf8") === owner) rmSync(path, { force: true });
95
+ } catch {
96
+ // A stale lease expires without blocking ordinary work.
97
+ }
98
+ };
99
+ } catch {
100
+ // Another hook is already checking or private local state is unavailable.
101
+ }
102
+ }
103
+
104
+ async function resolveRevision(repo, credentials, probe, deadline) {
105
+ const session = projectMemorySession(credentials);
106
+ if (!session) return;
107
+ const binding = hash(session);
108
+ const path = join(memoryClientRoot(), `ready-${hash(repo.toLowerCase())}.json`);
109
+ const saved = readReadiness(path, binding);
110
+ if (typeof saved?.revision === "string" && REVISION.test(saved.revision)) return saved.revision;
111
+ const now = Date.now();
112
+ if (!probe || now - (saved?.checkedAt ?? 0) < READINESS_RETRY_MS) return;
113
+ const release = tryReadinessLease(`${path}.lock`, now);
114
+ if (!release) return;
115
+ try {
116
+ const latest = readReadiness(path, binding);
117
+ if (typeof latest?.revision === "string" && REVISION.test(latest.revision))
118
+ return latest.revision;
119
+ if (now - (latest?.checkedAt ?? 0) < READINESS_RETRY_MS) return;
120
+ atomicWrite(path, JSON.stringify({ binding, checkedAt: now }));
121
+ const revision = await requestMemoryReadiness(repo, credentials, deadline);
122
+ if (typeof revision === "string" && REVISION.test(revision)) {
123
+ atomicWrite(path, JSON.stringify({ binding, checkedAt: now, revision }));
124
+ return revision;
125
+ }
126
+ } catch {
127
+ // A cache failure must not activate memory or block the normal prompt.
128
+ } finally {
129
+ release();
130
+ }
131
+ }
132
+
133
+ async function resolveMappedCheckout(target, credentials, hint, probe, deadline = Infinity) {
134
+ const revision =
135
+ hint.revision === null
136
+ ? await resolveRevision(hint.repo, credentials, probe, deadline)
137
+ : hint.revision;
138
+ if (hasMappedCommit(target, revision, deadline)) return target;
139
+ }
140
+
141
+ export async function cachedMemoryCheckout(cwd, credentials) {
142
+ const hints = projectMemoryHint(credentials);
143
+ if (!hints.length) return;
144
+ const target = checkout(cwd);
145
+ const hint = target && matchingHint(target, hints);
146
+ if (hint) return resolveMappedCheckout(target, credentials, hint, false);
147
+ }
148
+
149
+ export function readHookSettings(path) {
150
+ try {
151
+ const info = lstatSync(path);
152
+ if (!info.isFile() || info.uid !== process.getuid?.() || info.size > 512 * 1024)
153
+ throw new Error("Expected a regular owner-controlled hook settings file.");
154
+ return JSON.parse(readFileSync(path, "utf8"));
155
+ } catch (error) {
156
+ if (error.code === "ENOENT") return {};
157
+ throw error;
158
+ }
159
+ }
160
+
161
+ export async function resolveMemoryHookTarget(
162
+ client,
163
+ cwd,
164
+ credentials,
165
+ probe = false,
166
+ deadline = Infinity,
167
+ ) {
168
+ if (!Object.hasOwn(LEGACY_SETTINGS, client)) return;
169
+ const hints = projectMemoryHint(credentials);
170
+ if (!hints.length) return;
171
+ const target = checkout(cwd, deadline);
172
+ const hint = target && matchingHint(target, hints);
173
+ if (!hint) return;
174
+ try {
175
+ // A checkout enrolled by the original pilot may still have its own hook.
176
+ // Leave that hook in charge until normal setup migrates the checkout.
177
+ if (
178
+ hasLegacyHooks(readHookSettings(legacySettingsPath(client, target.root)), client, target.root)
179
+ )
180
+ return;
181
+ } catch {
182
+ // A client settings file we cannot inspect is not safe to double-enroll.
183
+ return;
184
+ }
185
+ return resolveMappedCheckout(target, credentials, hint, probe, deadline);
186
+ }
187
+
188
+ export function memoryClientRoot(home = homedir()) {
189
+ return join(home, ".pharaoh", "project-memory-client");
190
+ }
191
+
192
+ export function readActiveMemoryClients(home = homedir()) {
193
+ try {
194
+ const root = memoryClientRoot(home);
195
+ const directory = lstatSync(root);
196
+ if (!directory.isDirectory() || directory.uid !== process.getuid?.() || directory.mode & 0o077)
197
+ return {};
198
+ const path = join(root, "active.json");
199
+ const file = lstatSync(path);
200
+ if (!file.isFile() || file.uid !== process.getuid?.() || file.mode & 0o077 || file.size > 512)
201
+ return {};
202
+ const active = JSON.parse(readFileSync(path, "utf8"));
203
+ return Object.fromEntries(
204
+ Object.keys(LEGACY_SETTINGS).flatMap((client) => {
205
+ const digest = active?.[client];
206
+ return typeof digest === "string" && /^[a-f0-9]{64}$/.test(digest)
207
+ ? [[client, digest]]
208
+ : [];
209
+ }),
210
+ );
211
+ } catch {
212
+ return {};
213
+ }
214
+ }
215
+
216
+ export function activeMemoryRuntime(client, directory, home = homedir()) {
217
+ const digest = readActiveMemoryClients(home)[client];
218
+ return Boolean(digest && directory === join(realpathSync(memoryClientRoot(home)), digest));
219
+ }
220
+
221
+ export async function readMemoryCredentials() {
222
+ try {
223
+ const { CREDENTIALS_PATH, readCredentials } = await import("./runtime/credentials.js");
224
+ const stat = lstatSync(CREDENTIALS_PATH);
225
+ if (
226
+ stat.isFile() &&
227
+ stat.uid === process.getuid?.() &&
228
+ !(stat.mode & 0o077) &&
229
+ stat.size <= 65_536
230
+ )
231
+ return readCredentials();
232
+ } catch {
233
+ // No current owner-private credential file.
234
+ }
235
+ }