@pharaoh-so/mcp 0.3.57 → 0.3.59
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.
- package/.mcp.json +1 -1
- package/CHANGELOG.md +11 -0
- package/dist/helpers.js +1 -1
- package/dist/index.js +10 -3
- package/npx-skills/brainstorm/SKILL.md +1 -1
- package/npx-skills/debug/SKILL.md +1 -1
- package/npx-skills/pharaoh/SKILL.md +1 -1
- package/npx-skills/plan/SKILL.md +1 -1
- package/npx-skills/refactor/SKILL.md +1 -1
- package/npx-skills/review/SKILL.md +1 -1
- package/npx-skills/tdd/SKILL.md +1 -1
- package/npx-skills/verify/SKILL.md +1 -1
- package/package.json +1 -1
- package/project-memory/hook-config.mjs +29 -19
- package/project-memory/hook-events.mjs +12 -2
- package/project-memory/pharaoh-memory-hook.mjs +16 -17
- package/project-memory/setup.d.mts +1 -1
- package/project-memory/setup.mjs +130 -86
- package/project-memory/user-target.mjs +3 -9
- package/trace-app.html +0 -2
package/.mcp.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
26
26
|
- The `pharaoh` skill's connection URL is `https://mcp.pharaoh.so/mcp` instead of `/sse`. The skill also carries the guidance that was only in the server instructions: one Pharaoh call answers what grep and file reads take many turns to piece together, load Pharaoh's tools before the first code search when your client defers them, summarize a block marked "Trace view available" instead of reproducing it, and check related repos when a change touches shared interfaces.
|
|
27
27
|
- The server instructions state facts: what Pharaoh maps, which tool answers which question, what the graph leaves to grep, how repos are mapped and what each account tool does. Asking Pharaoh first, not re-checking its answers with grep, surfacing and crediting its ★ blocks and the rest of that guidance now live in the `pharaoh` skill. Claude Code and OpenClaw list the skill in every session and load its body only when they use it. The Grep hook and the optional `~/.claude/CLAUDE.md` line are unchanged and apply from the start of a session.
|
|
28
28
|
|
|
29
|
+
## [0.3.59] - 2026-11-04
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- The project memory pilot uses Pharaoh's MCP tools by default. Interactive setup removes managed pilot hooks from user settings and from eligible ignored settings in the current checkout. Tracked or linked checkout hooks require manual removal.
|
|
33
|
+
- Use `--memory-hooks` during interactive setup to opt in to a prompt reminder for the MCP tools. Use `--disable-memory-hooks` to remove managed pilot hooks without signing in again.
|
|
34
|
+
|
|
35
|
+
## [0.3.58] - 2026-11-04
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- The ankh (☥) is gone: the Trace card's header has no mark, and the skills no longer set it as their emoji.
|
|
39
|
+
|
|
29
40
|
## [0.3.57] - 2026-11-04
|
|
30
41
|
|
|
31
42
|
### Changed
|
package/dist/helpers.js
CHANGED
|
@@ -96,7 +96,7 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
96
96
|
return { server, logout };
|
|
97
97
|
}
|
|
98
98
|
export function printUsage() {
|
|
99
|
-
printLines("Usage: pharaoh-mcp [options]", "", "Options:", " --server <url> Pharaoh server URL (default: https://mcp.pharaoh.so)", " --logout Clear credentials, MCP config, the Claude Code plugin and the CLAUDE.md line, then exit", " --clean Remove Pharaoh setup, including managed local project memory hooks", " --install-skills Reinstall the Claude Code plugin and OpenClaw skills", " --version, -v Print the version", " --help, -h Show this help", "", "Get started:", ` ${NPX_COMMAND}`, "");
|
|
99
|
+
printLines("Usage: pharaoh-mcp [options]", "", "Options:", " --server <url> Pharaoh server URL (default: https://mcp.pharaoh.so)", " --logout Clear credentials, MCP config, the Claude Code plugin and the CLAUDE.md line, then exit", " --clean Remove Pharaoh setup, including managed local project memory hooks", " --memory-hooks Install optional project memory reminders during interactive setup", " --disable-memory-hooks Remove Pharaoh's local project memory hooks", " --install-skills Reinstall the Claude Code plugin and OpenClaw skills", " --version, -v Print the version", " --help, -h Show this help", "", "Get started:", ` ${NPX_COMMAND}`, "");
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
102
|
* Validate that a server-supplied SSE URL shares the same origin as the configured server.
|
package/dist/index.js
CHANGED
|
@@ -37,9 +37,11 @@ function reportMemorySetup(options) {
|
|
|
37
37
|
const notices = syncMemoryHooks(options);
|
|
38
38
|
if (notices.length)
|
|
39
39
|
printLines(...notices);
|
|
40
|
+
return notices.length === 0;
|
|
40
41
|
}
|
|
41
42
|
catch {
|
|
42
43
|
printLines("Pharaoh: project memory setup could not update local activation.");
|
|
44
|
+
return false;
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
async function main() {
|
|
@@ -65,6 +67,11 @@ async function main() {
|
|
|
65
67
|
runInstallSkills();
|
|
66
68
|
return;
|
|
67
69
|
}
|
|
70
|
+
if (args.includes("--disable-memory-hooks")) {
|
|
71
|
+
if (!reportMemorySetup())
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
68
75
|
// Clean mode — remove all pharaoh traces from Claude Code config (MCP entries,
|
|
69
76
|
// disabled states, project-scoped configs). Useful for resetting broken installs.
|
|
70
77
|
if (args.includes("--clean")) {
|
|
@@ -73,7 +80,7 @@ async function main() {
|
|
|
73
80
|
cleanSlate();
|
|
74
81
|
uninstallClaudePlugin();
|
|
75
82
|
cleanCodexSetup();
|
|
76
|
-
reportMemorySetup(
|
|
83
|
+
reportMemorySetup();
|
|
77
84
|
printLines("Pharaoh: local setup cleanup finished.");
|
|
78
85
|
process.exit(0);
|
|
79
86
|
}
|
|
@@ -86,7 +93,7 @@ async function main() {
|
|
|
86
93
|
cleanSlate();
|
|
87
94
|
uninstallClaudePlugin();
|
|
88
95
|
cleanCodexSetup();
|
|
89
|
-
reportMemorySetup(
|
|
96
|
+
reportMemorySetup();
|
|
90
97
|
printLines("Pharaoh: credentials cleared and local setup cleanup finished.");
|
|
91
98
|
process.exit(0);
|
|
92
99
|
}
|
|
@@ -137,7 +144,7 @@ async function main() {
|
|
|
137
144
|
if (claudeMd)
|
|
138
145
|
printLines(claudeMd);
|
|
139
146
|
}
|
|
140
|
-
reportMemorySetup({ credentials: newCreds, clients });
|
|
147
|
+
reportMemorySetup({ credentials: newCreds, clients, enable: args.includes("--memory-hooks") });
|
|
141
148
|
// The registration is pinned to this version; upgrading is a deliberate
|
|
142
149
|
// re-run. Interactive only, and quiet when the registry can't be reached.
|
|
143
150
|
const latest = await fetchLatestVersion();
|
|
@@ -6,7 +6,7 @@ license: MIT. Adapted from obra/superpowers, Copyright (c) 2025 Jesse Vincent; s
|
|
|
6
6
|
version: 0.2.1
|
|
7
7
|
homepage: https://pharaoh.so
|
|
8
8
|
user-invocable: true
|
|
9
|
-
metadata: {"
|
|
9
|
+
metadata: {"tags": ["planning", "design", "brainstorming", "requirements", "architecture"]}
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# Brainstorm
|
|
@@ -6,7 +6,7 @@ license: MIT. Adapted from obra/superpowers, Copyright (c) 2025 Jesse Vincent; s
|
|
|
6
6
|
version: 0.2.1
|
|
7
7
|
homepage: https://pharaoh.so
|
|
8
8
|
user-invocable: true
|
|
9
|
-
metadata: {"
|
|
9
|
+
metadata: {"tags": ["debugging", "root-cause", "investigation", "troubleshooting", "methodology"]}
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# Systematic Debugging
|
|
@@ -4,7 +4,7 @@ prompt-name: pharaoh-core
|
|
|
4
4
|
description: "Codebase knowledge graph — gives AI agents full architectural awareness. Query architecture, dependencies, blast radius, module context, and function search instead of reading files one at a time. Code intelligence via MCP: understand any codebase in seconds."
|
|
5
5
|
version: 0.3.1
|
|
6
6
|
homepage: https://pharaoh.so
|
|
7
|
-
metadata: {"
|
|
7
|
+
metadata: {"openclaw": {"always": true}, "tags": ["code-intelligence", "architecture", "mcp", "knowledge-graph", "dependencies", "codebase-understanding"]}
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Pharaoh Tool Guide
|
package/npx-skills/plan/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ prompt-name: plan-with-pharaoh
|
|
|
4
4
|
description: Review a plan or spec against the codebase before implementing it. Pharaoh reconnaissance, a BIG or SMALL change choice, then architecture, code quality, wiring, tests, performance and security sections with numbered decisions.
|
|
5
5
|
version: 0.7.1
|
|
6
6
|
homepage: https://pharaoh.so
|
|
7
|
-
metadata: {"
|
|
7
|
+
metadata: {"tags": ["planning", "architecture", "blast-radius", "pharaoh", "review", "interactive"]}
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Plan Review
|
|
@@ -4,7 +4,7 @@ prompt-name: safe-refactor
|
|
|
4
4
|
description: "Safe refactoring workflow using Pharaoh codebase knowledge graph. Six-step process: module context, blast radius of detected downstream callers, reachability verification, dependency mapping, naming conflict detection, and a search for the affected tests. Produces a refactoring plan with detected callers listed, test files identified, unreachable code flagged, and high-risk paths warned. Use when refactoring, renaming, moving or splitting code in a repo Pharaoh maps, with Pharaoh's tools connected."
|
|
5
5
|
version: 0.3.1
|
|
6
6
|
homepage: https://pharaoh.so
|
|
7
|
-
metadata: {"
|
|
7
|
+
metadata: {"tags": ["refactoring", "blast-radius", "architecture", "pharaoh", "safe-refactor", "test-coverage"]}
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Safe Refactor
|
|
@@ -4,7 +4,7 @@ prompt-name: review-with-pharaoh
|
|
|
4
4
|
description: "Five-phase workflow: git context detection (worktree, branch, base, changed files), Pharaoh recon (architecture map, module context, blast radius in one call, with a fallback when the repo isn't mapped), parallel specialized agent dispatch (code quality, security against the project's own rules, error handling, test coverage, type design), adversarial review by the other model (Codex from Claude Code, Claude from Codex) for security-sensitive changes, and a final synthesized verdict of SHIP / SHIP WITH CHANGES / BLOCK. Architecture-aware wherever Pharaoh maps the repo. Use when reviewing a branch or pull request before merging, or when the user asks for a code review."
|
|
5
5
|
version: 0.6.1
|
|
6
6
|
homepage: https://pharaoh.so
|
|
7
|
-
metadata: {"
|
|
7
|
+
metadata: {"tags": ["code-review", "pull-request", "architecture", "pharaoh", "multi-agent", "adversarial-review", "cross-model", "regression-risk", "spec-alignment"]}
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Review with Pharaoh
|
package/npx-skills/tdd/SKILL.md
CHANGED
|
@@ -6,7 +6,7 @@ license: MIT. Adapted from obra/superpowers, Copyright (c) 2025 Jesse Vincent; s
|
|
|
6
6
|
version: 0.2.1
|
|
7
7
|
homepage: https://pharaoh.so
|
|
8
8
|
user-invocable: true
|
|
9
|
-
metadata: {"
|
|
9
|
+
metadata: {"tags": ["tdd", "testing", "red-green-refactor", "quality", "discipline"]}
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# Test-Driven Development
|
|
@@ -6,7 +6,7 @@ license: MIT. Adapted from obra/superpowers, Copyright (c) 2025 Jesse Vincent; s
|
|
|
6
6
|
version: 0.2.1
|
|
7
7
|
homepage: https://pharaoh.so
|
|
8
8
|
user-invocable: true
|
|
9
|
-
metadata: {"
|
|
9
|
+
metadata: {"tags": ["verification", "quality", "completion", "evidence", "discipline"]}
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# Verification Before Completion
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pharaoh-so/mcp",
|
|
3
3
|
"mcpName": "so.pharaoh/pharaoh",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.59",
|
|
5
5
|
"description": "MCP proxy for Pharaoh — maps codebases into queryable knowledge graphs for coding agents. Enables Claude Code in headless environments (VPS, SSH, CI) via device flow auth.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -20,15 +20,29 @@ function hookCommand(entry) {
|
|
|
20
20
|
return entry?.type === "command" && typeof entry.command === "string" ? entry.command : "";
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function matchesLegacyHook(client, root) {
|
|
24
|
+
const suffix = ` ${legacyMarker(client, root)}`;
|
|
25
|
+
return (value) => value.includes("pharaoh-memory-hook.mjs") && value.endsWith(suffix);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function matchesUserHook(value) {
|
|
29
|
+
return (
|
|
30
|
+
value.includes("pharaoh-memory-hook.mjs") &&
|
|
31
|
+
(value.endsWith(` ${GLOBAL_MARKER}`) || LEGACY_MARKER.test(value))
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
/** Keep unrelated handlers even when they share an event group with ours. */
|
|
24
|
-
function replaceHooks(config, client, matches, command) {
|
|
36
|
+
function replaceHooks(config, client, matches, command, installEvents = EVENTS[client]) {
|
|
25
37
|
if (!isObject(config) || (config.hooks !== undefined && !isObject(config.hooks))) {
|
|
26
38
|
throw new Error("The existing hooks configuration is not a JSON object.");
|
|
27
39
|
}
|
|
28
40
|
const hooks = { ...config.hooks };
|
|
29
41
|
for (const event of EVENTS[client]) {
|
|
30
42
|
const current = hooks[event];
|
|
31
|
-
|
|
43
|
+
const install = command && installEvents.includes(event);
|
|
44
|
+
if (!install && (current === undefined || (Array.isArray(current) && !current.length)))
|
|
45
|
+
continue;
|
|
32
46
|
if (
|
|
33
47
|
current !== undefined &&
|
|
34
48
|
(!Array.isArray(current) || current.some((group) => !Array.isArray(group?.hooks)))
|
|
@@ -40,7 +54,7 @@ function replaceHooks(config, client, matches, command) {
|
|
|
40
54
|
if (rest.length === group.hooks.length) return [group];
|
|
41
55
|
return rest.length ? [{ ...group, hooks: rest }] : [];
|
|
42
56
|
});
|
|
43
|
-
if (
|
|
57
|
+
if (install) kept.push({ hooks: [{ type: "command", command, timeout: 3 }] });
|
|
44
58
|
if (kept.length) hooks[event] = kept;
|
|
45
59
|
else delete hooks[event];
|
|
46
60
|
}
|
|
@@ -55,37 +69,33 @@ export function updateHooks(config, client, target, remove, hookPath, node = pro
|
|
|
55
69
|
const command = remove
|
|
56
70
|
? null
|
|
57
71
|
: `${[node, hookPath, client, target.root, target.repo].map(shellWord).join(" ")} ${marker}`;
|
|
58
|
-
return replaceHooks(config, client, (
|
|
72
|
+
return replaceHooks(config, client, matchesLegacyHook(client, target.root), command);
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
export function updateUserHooks(config, client, hookPath, node = process.execPath) {
|
|
62
76
|
const command = hookPath
|
|
63
77
|
? `${[node, hookPath, client, "--user"].map(shellWord).join(" ")} ${GLOBAL_MARKER}`
|
|
64
78
|
: null;
|
|
65
|
-
return replaceHooks(
|
|
66
|
-
config,
|
|
67
|
-
client,
|
|
68
|
-
(value) =>
|
|
69
|
-
value.endsWith(` ${GLOBAL_MARKER}`) ||
|
|
70
|
-
(value.includes("pharaoh-memory-hook.mjs") && LEGACY_MARKER.test(value)),
|
|
71
|
-
command,
|
|
72
|
-
);
|
|
79
|
+
return replaceHooks(config, client, matchesUserHook, command, ["UserPromptSubmit"]);
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
const suffix = ` ${legacyMarker(client, root)}`;
|
|
82
|
+
function hasHooks(config, client, matches) {
|
|
77
83
|
return EVENTS[client].some((event) => {
|
|
78
84
|
const groups = config?.hooks?.[event];
|
|
79
85
|
return (
|
|
80
86
|
Array.isArray(groups) &&
|
|
81
87
|
groups.some(
|
|
82
88
|
(group) =>
|
|
83
|
-
Array.isArray(group?.hooks) &&
|
|
84
|
-
group.hooks.some((entry) => {
|
|
85
|
-
const command = hookCommand(entry);
|
|
86
|
-
return command.includes("pharaoh-memory-hook.mjs") && command.endsWith(suffix);
|
|
87
|
-
}),
|
|
89
|
+
Array.isArray(group?.hooks) && group.hooks.some((entry) => matches(hookCommand(entry))),
|
|
88
90
|
)
|
|
89
91
|
);
|
|
90
92
|
});
|
|
91
93
|
}
|
|
94
|
+
|
|
95
|
+
export function hasLegacyHooks(config, client, root) {
|
|
96
|
+
return hasHooks(config, client, matchesLegacyHook(client, root));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function hasUserHooks(config, client) {
|
|
100
|
+
return hasHooks(config, client, matchesUserHook);
|
|
101
|
+
}
|
|
@@ -35,7 +35,7 @@ function promptContext(token, compact, mayCapture) {
|
|
|
35
35
|
].join("\n");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function
|
|
38
|
+
export function skipMemoryPrompt(value) {
|
|
39
39
|
const prompt = typeof value === "string" ? value.trim() : "";
|
|
40
40
|
return (
|
|
41
41
|
!prompt ||
|
|
@@ -47,6 +47,16 @@ function skipPrompt(value) {
|
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export function memoryReminder(repo) {
|
|
51
|
+
if (!REPO.test(repo)) return;
|
|
52
|
+
return {
|
|
53
|
+
hookSpecificOutput: {
|
|
54
|
+
hookEventName: "UserPromptSubmit",
|
|
55
|
+
additionalContext: `For work in ${repo}, use pharaoh_memory_recall if prior decisions may matter. Use pharaoh_memory_expand if you need the full record, and pharaoh_memory_capture for one supported candidate when the task permits. Mention memory only when it changes a decision.`,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
/** Hook input can contain raw prompts and transcripts. Neither is persisted or transmitted. */
|
|
51
61
|
export function handleEvent(
|
|
52
62
|
input,
|
|
@@ -90,7 +100,7 @@ export function handleEvent(
|
|
|
90
100
|
input.prompt,
|
|
91
101
|
);
|
|
92
102
|
const opened = store.begin(key, target, client, nativeTurn, {
|
|
93
|
-
skip:
|
|
103
|
+
skip: skipMemoryPrompt(input.prompt),
|
|
94
104
|
readOnly: noCapture,
|
|
95
105
|
memorySession,
|
|
96
106
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { handleEvent } from "./hook-events.mjs";
|
|
4
|
-
import {
|
|
3
|
+
import { handleEvent, memoryReminder, skipMemoryPrompt } from "./hook-events.mjs";
|
|
4
|
+
import { readStdin } from "./local.mjs";
|
|
5
5
|
import {
|
|
6
6
|
activeMemoryRuntime,
|
|
7
7
|
readMemoryCredentials,
|
|
@@ -15,30 +15,29 @@ const exitQuietly = setTimeout(() => process.exit(0), Math.max(0, deadline - per
|
|
|
15
15
|
try {
|
|
16
16
|
const [client, root, repo] = process.argv.slice(2);
|
|
17
17
|
const input = await readStdin(1024 * 1024);
|
|
18
|
-
|
|
19
|
-
let target = { root, repo };
|
|
18
|
+
let result;
|
|
20
19
|
if (root === "--user") {
|
|
21
20
|
const runtime = dirname(fileURLToPath(import.meta.url));
|
|
22
21
|
if (!activeMemoryRuntime(client, runtime)) process.exit(0);
|
|
23
|
-
|
|
22
|
+
if (input?.hook_event_name !== "UserPromptSubmit" || skipMemoryPrompt(input.prompt))
|
|
23
|
+
process.exit(0);
|
|
24
24
|
const eligible = await resolveMemoryHookTarget(
|
|
25
25
|
client,
|
|
26
26
|
input?.cwd,
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
await readMemoryCredentials(),
|
|
28
|
+
true,
|
|
29
29
|
deadline,
|
|
30
30
|
);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
if (eligible && performance.now() < deadline) result = memoryReminder(eligible.repo);
|
|
32
|
+
} else {
|
|
33
|
+
result = handleEvent(input, {
|
|
34
|
+
client,
|
|
35
|
+
root,
|
|
36
|
+
repo,
|
|
37
|
+
credentials: await readMemoryCredentials(),
|
|
38
|
+
deadline,
|
|
39
|
+
});
|
|
35
40
|
}
|
|
36
|
-
const result = handleEvent(input, {
|
|
37
|
-
client,
|
|
38
|
-
...target,
|
|
39
|
-
credentials,
|
|
40
|
-
deadline,
|
|
41
|
-
});
|
|
42
41
|
if (result) process.stdout.write(JSON.stringify(result));
|
|
43
42
|
} catch {
|
|
44
43
|
// Project memory never blocks ordinary work or opens a continuation.
|
|
@@ -5,10 +5,10 @@ export function syncMemoryHooks(options?: {
|
|
|
5
5
|
project_memory_status?: "unavailable";
|
|
6
6
|
};
|
|
7
7
|
clients?: string[];
|
|
8
|
+
enable?: boolean;
|
|
8
9
|
home?: string;
|
|
9
10
|
env?: NodeJS.ProcessEnv;
|
|
10
11
|
cwd?: string;
|
|
11
12
|
node?: string;
|
|
12
|
-
removeLegacy?: boolean;
|
|
13
13
|
packageRoot?: URL;
|
|
14
14
|
}): string[];
|
package/project-memory/setup.mjs
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { lstatSync, mkdirSync, realpathSync, rmSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, isAbsolute, join } from "node:path";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
4
4
|
import { bundledHook, installMemoryBundle, packagedMemoryBundle } from "./bundle.mjs";
|
|
5
|
-
import { hasLegacyHooks, updateHooks, updateUserHooks } from "./hook-config.mjs";
|
|
5
|
+
import { hasLegacyHooks, hasUserHooks, updateHooks, updateUserHooks } from "./hook-config.mjs";
|
|
6
6
|
import { atomicWrite, checkout, git, hasMemoryGit } from "./local.mjs";
|
|
7
7
|
import {
|
|
8
|
-
eligibleCheckout,
|
|
9
8
|
legacySettingsPath,
|
|
9
|
+
matchingMemoryHint,
|
|
10
10
|
memoryClientRoot,
|
|
11
11
|
projectMemoryHint,
|
|
12
12
|
readActiveMemoryClients,
|
|
13
13
|
readHookSettings,
|
|
14
14
|
} from "./user-target.mjs";
|
|
15
15
|
|
|
16
|
+
const CLIENTS = ["claude-code", "codex"];
|
|
17
|
+
|
|
16
18
|
function userSettings(client, home, env) {
|
|
17
19
|
const codex = client === "codex";
|
|
18
20
|
const directory =
|
|
@@ -21,131 +23,173 @@ function userSettings(client, home, env) {
|
|
|
21
23
|
return join(directory, codex ? "hooks.json" : "settings.json");
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
+
function canonicalSettingsPath(path) {
|
|
27
|
+
try {
|
|
28
|
+
return realpathSync(path);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const parent = dirname(path);
|
|
31
|
+
if (error.code !== "ENOENT" || parent === path) throw error;
|
|
32
|
+
// Even a missing settings file can be under a linked ancestor.
|
|
33
|
+
return join(canonicalSettingsPath(parent), basename(path));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function inspectSettings(path) {
|
|
38
|
+
const resolved = canonicalSettingsPath(path);
|
|
39
|
+
return { path, resolved, config: readHookSettings(resolved) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeSettings({ path, resolved, config }, next, target, requireIgnored = false) {
|
|
43
|
+
if (JSON.stringify(config) === JSON.stringify(next)) return;
|
|
44
|
+
if (requireIgnored && path !== resolved)
|
|
45
|
+
throw new Error("The checkout hook settings are linked.");
|
|
46
|
+
if (target) {
|
|
47
|
+
const location = relative(target.root, resolved);
|
|
48
|
+
if (
|
|
49
|
+
location &&
|
|
50
|
+
location !== ".." &&
|
|
51
|
+
!location.startsWith(`..${sep}`) &&
|
|
52
|
+
!isAbsolute(location)
|
|
53
|
+
) {
|
|
54
|
+
if (git(target.root, "ls-files", "--", `:(literal)${location}`))
|
|
55
|
+
throw new Error("The checkout hook settings are tracked.");
|
|
56
|
+
if (requireIgnored) git(target.root, "check-ignore", "--quiet", "--no-index", "--", location);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
26
59
|
const directory = dirname(path);
|
|
60
|
+
const file = lstatSync(path, { throwIfNoEntry: false });
|
|
61
|
+
if (file && !file.isFile()) throw new Error("The hook settings are linked.");
|
|
27
62
|
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
28
63
|
const info = lstatSync(directory);
|
|
29
64
|
if (!info.isDirectory() || info.uid !== process.getuid?.())
|
|
30
65
|
throw new Error("Client settings directory is not owner-controlled.");
|
|
31
|
-
atomicWrite(
|
|
66
|
+
atomicWrite(resolved, `${JSON.stringify(next, null, "\t")}\n`);
|
|
32
67
|
}
|
|
33
68
|
|
|
34
|
-
function migrateCheckout(client, target) {
|
|
69
|
+
function migrateCheckout(client, target, notices) {
|
|
35
70
|
if (!target) return;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
let known = false;
|
|
72
|
+
try {
|
|
73
|
+
const settings = inspectSettings(legacySettingsPath(client, target.root));
|
|
74
|
+
known = hasLegacyHooks(settings.config, client, target.root);
|
|
75
|
+
if (known)
|
|
76
|
+
writeSettings(settings, updateHooks(settings.config, client, target, true), target, true);
|
|
77
|
+
} catch {
|
|
78
|
+
if (known)
|
|
79
|
+
notices.push(
|
|
80
|
+
`Pharaoh: the earlier ${client} checkout hook remains in place for this checkout.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
46
83
|
}
|
|
47
84
|
|
|
48
|
-
|
|
85
|
+
function syncUserHooks(client, home, env, target, hookPath, node, alreadyActive) {
|
|
86
|
+
let known = Boolean(alreadyActive);
|
|
87
|
+
try {
|
|
88
|
+
const settings = inspectSettings(userSettings(client, home, env));
|
|
89
|
+
known ||= hasUserHooks(settings.config, client);
|
|
90
|
+
if (hookPath || known) {
|
|
91
|
+
const legacy = target && legacySettingsPath(client, target.root);
|
|
92
|
+
const checkoutHook =
|
|
93
|
+
legacy &&
|
|
94
|
+
[settings.path, settings.resolved].includes(legacy) &&
|
|
95
|
+
hasLegacyHooks(settings.config, client, target.root);
|
|
96
|
+
writeSettings(
|
|
97
|
+
settings,
|
|
98
|
+
updateUserHooks(settings.config, client, hookPath, node),
|
|
99
|
+
target,
|
|
100
|
+
checkoutHook,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (hookPath || known) throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function writeActivation(path, active) {
|
|
109
|
+
if (Object.keys(active).length) atomicWrite(path, `${JSON.stringify(active)}\n`);
|
|
110
|
+
else rmSync(path, { force: true });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Run only during setup or explicit cleanup of the local memory hooks. */
|
|
49
114
|
export function syncMemoryHooks({
|
|
50
115
|
credentials,
|
|
51
116
|
clients = [],
|
|
117
|
+
enable = false,
|
|
52
118
|
home = homedir(),
|
|
53
119
|
env = process.env,
|
|
54
120
|
cwd = process.cwd(),
|
|
55
121
|
node = process.execPath,
|
|
56
|
-
removeLegacy = false,
|
|
57
122
|
packageRoot,
|
|
58
123
|
} = {}) {
|
|
59
|
-
|
|
124
|
+
if (typeof process.getuid !== "function")
|
|
125
|
+
return enable ? ["Pharaoh: project memory reminders need a POSIX client."] : [];
|
|
60
126
|
const root = memoryClientRoot(home);
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
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 [];
|
|
127
|
+
const activePath = join(root, "active.json");
|
|
128
|
+
let active = readActiveMemoryClients(home);
|
|
129
|
+
const target = checkout(cwd);
|
|
68
130
|
const notices = [];
|
|
69
|
-
const active = {};
|
|
70
131
|
const configured = [];
|
|
132
|
+
|
|
71
133
|
let installed;
|
|
72
134
|
let digest;
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
entry?.revision === null &&
|
|
81
|
-
entry.repo?.toLowerCase() === pendingCheckout.repo.toLowerCase(),
|
|
82
|
-
)
|
|
83
|
-
? pendingCheckout.root
|
|
84
|
-
: undefined;
|
|
85
|
-
|
|
86
|
-
if (hint.length) {
|
|
135
|
+
const hint = projectMemoryHint(credentials);
|
|
136
|
+
if (enable) {
|
|
137
|
+
if (credentials?.project_memory_status === "unavailable")
|
|
138
|
+
return [
|
|
139
|
+
"Pharaoh: project memory eligibility could not be checked. Retry setup to enable reminders.",
|
|
140
|
+
];
|
|
141
|
+
if (!hint.length) return ["Pharaoh: this session has no project memory hook grant."];
|
|
87
142
|
try {
|
|
88
|
-
if (!hasMemoryGit
|
|
89
|
-
|
|
143
|
+
if (!hasMemoryGit) throw new Error("Project memory needs a POSIX client with system Git.");
|
|
144
|
+
const enrolledRoot = target && matchingMemoryHint(target, hint) ? target.root : undefined;
|
|
90
145
|
const bundle = packagedMemoryBundle(packageRoot);
|
|
91
146
|
digest = bundle.sha256;
|
|
92
|
-
installed = bundledHook(installMemoryBundle(root,
|
|
147
|
+
installed = bundledHook(installMemoryBundle(root, enrolledRoot, bundle));
|
|
93
148
|
} catch (error) {
|
|
94
|
-
return [`Pharaoh: project memory
|
|
149
|
+
return [`Pharaoh: project memory reminders unavailable. ${error.message}`];
|
|
95
150
|
}
|
|
96
151
|
}
|
|
97
152
|
|
|
98
|
-
for (const client of
|
|
99
|
-
|
|
100
|
-
|
|
153
|
+
for (const client of CLIENTS) {
|
|
154
|
+
if (enable && !clients.includes(client)) continue;
|
|
155
|
+
const next = { ...active, [client]: digest };
|
|
101
156
|
try {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
157
|
+
// Enable the replacement before swapping any settings, so failed
|
|
158
|
+
// activation cannot strand an existing hook or remove a legacy one.
|
|
159
|
+
const replacing = enable && active[client] !== digest;
|
|
160
|
+
if (replacing) writeActivation(activePath, next);
|
|
161
|
+
try {
|
|
162
|
+
syncUserHooks(client, home, env, target, installed, node, active[client]);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (replacing) writeActivation(activePath, active);
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
if (enable) {
|
|
168
|
+
active = next;
|
|
107
169
|
configured.push(client);
|
|
108
170
|
}
|
|
109
171
|
} catch {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
`Pharaoh: project memory could not update ${client} hooks. Check the client settings and rerun normal setup.`,
|
|
114
|
-
);
|
|
172
|
+
notices.push(
|
|
173
|
+
`Pharaoh: could not update ${client} project memory hooks. Check the client settings and retry.`,
|
|
174
|
+
);
|
|
115
175
|
}
|
|
116
176
|
}
|
|
117
177
|
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
178
|
+
// 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);
|
|
181
|
+
if (configured.length) {
|
|
182
|
+
notices.push(
|
|
183
|
+
"Pharaoh project memory reminders are set up for your eligible repositories. Start a new local conversation.",
|
|
184
|
+
);
|
|
185
|
+
if (hint.some((entry) => entry?.revision === null))
|
|
125
186
|
notices.push(
|
|
126
|
-
"
|
|
187
|
+
"A connected repository is still mapping. Its reminder will run after the mapped revision is available.",
|
|
127
188
|
);
|
|
128
|
-
|
|
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 {
|
|
189
|
+
if (configured.includes("codex"))
|
|
145
190
|
notices.push(
|
|
146
|
-
|
|
191
|
+
"In Codex, review and trust the Pharaoh reminder in /hooks or Settings > Hooks.",
|
|
147
192
|
);
|
|
148
|
-
}
|
|
149
193
|
}
|
|
150
194
|
return notices;
|
|
151
195
|
}
|
|
@@ -33,7 +33,7 @@ export function projectMemorySession(credentials) {
|
|
|
33
33
|
return session;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function
|
|
36
|
+
export function matchingMemoryHint(target, hints) {
|
|
37
37
|
return hints.find(
|
|
38
38
|
(entry) =>
|
|
39
39
|
typeof entry?.repo === "string" && entry.repo.toLowerCase() === target.repo.toLowerCase(),
|
|
@@ -51,12 +51,6 @@ function hasMappedCommit(target, revision, deadline = Infinity) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
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
54
|
function hash(value) {
|
|
61
55
|
return createHash("sha256").update(value).digest("hex");
|
|
62
56
|
}
|
|
@@ -142,7 +136,7 @@ export async function cachedMemoryCheckout(cwd, credentials) {
|
|
|
142
136
|
const hints = projectMemoryHint(credentials);
|
|
143
137
|
if (!hints.length) return;
|
|
144
138
|
const target = checkout(cwd);
|
|
145
|
-
const hint = target &&
|
|
139
|
+
const hint = target && matchingMemoryHint(target, hints);
|
|
146
140
|
if (hint) return resolveMappedCheckout(target, credentials, hint, false);
|
|
147
141
|
}
|
|
148
142
|
|
|
@@ -169,7 +163,7 @@ export async function resolveMemoryHookTarget(
|
|
|
169
163
|
const hints = projectMemoryHint(credentials);
|
|
170
164
|
if (!hints.length) return;
|
|
171
165
|
const target = checkout(cwd, deadline);
|
|
172
|
-
const hint = target &&
|
|
166
|
+
const hint = target && matchingMemoryHint(target, hints);
|
|
173
167
|
if (!hint) return;
|
|
174
168
|
try {
|
|
175
169
|
// A checkout enrolled by the original pilot may still have its own hook.
|
package/trace-app.html
CHANGED
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
.mark-s { font:var(--strong) var(--type-caption) var(--mono); width:1ch; }
|
|
30
30
|
#trace { color:var(--text); border:var(--line); border-radius:var(--radius); padding:10px 12px; font:var(--regular) var(--type-body)/var(--leading) var(--sans); min-width:0; }
|
|
31
31
|
.head { display:flex; flex-wrap:wrap; align-items:baseline; gap:2px 10px; }
|
|
32
|
-
.mark { color:var(--gold); font-size:var(--type-heading); line-height:1; }
|
|
33
32
|
.headline { flex:1 1 16em; margin:0; min-width:0; overflow-wrap:anywhere; }
|
|
34
33
|
#kind { color:var(--muted); }
|
|
35
34
|
#target { color:var(--ink); font:var(--strong) var(--type-heading)/var(--leading) var(--mono); }
|
|
@@ -126,7 +125,6 @@
|
|
|
126
125
|
<body>
|
|
127
126
|
<main id="trace" data-state="waiting">
|
|
128
127
|
<div class="head">
|
|
129
|
-
<span class="mark" aria-hidden="true">☥</span>
|
|
130
128
|
<p class="headline"><span id="kind"></span><span id="target"></span><span id="title">Pharaoh</span><span id="summary" role="status" aria-live="polite">Waiting for the tool</span></p>
|
|
131
129
|
<span class="meta"><span id="verdict" hidden></span><span id="sha" hidden></span></span>
|
|
132
130
|
</div>
|