@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.
- package/.mcp.json +1 -1
- package/CHANGELOG.md +13 -0
- package/README.md +3 -2
- package/dist/backend.d.ts +7 -0
- package/dist/helpers.d.ts +3 -2
- package/dist/helpers.js +7 -4
- package/dist/index.js +40 -6
- package/dist/install-skills.d.ts +13 -0
- package/dist/install-skills.js +107 -2
- package/dist/proxy-server.js +2 -1
- package/hooks/route-grep.mjs +63 -0
- package/package.json +1 -1
- package/project-memory/bundle.mjs +9 -2
- package/project-memory/graph-cues.mjs +268 -0
- package/project-memory/hook-config.mjs +41 -14
- package/project-memory/hook-events.mjs +1 -1
- package/project-memory/local.mjs +30 -3
- package/project-memory/pharaoh-memory-hook.mjs +39 -7
- package/project-memory/preferences.d.mts +12 -0
- package/project-memory/preferences.mjs +70 -0
- package/project-memory/setup.d.mts +35 -9
- package/project-memory/setup.mjs +134 -10
package/.mcp.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,19 @@ 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.61] - 2026-11-05
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Eligible project memory pilot setup recommends quiet reminders and remembers declines through upgrades. The normal authenticated MCP connection provides memory when hooks are disabled or unavailable.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- Optional pilot worktree cues suggest graph tools for structural Bash searches and flag local edits that differ from a previously consulted codebase map revision.
|
|
36
|
+
|
|
37
|
+
## [0.3.60] - 2026-11-05
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- When it connects, the proxy tells Pharaoh what your agent's instruction files say about it: whether `~/.claude/CLAUDE.md` holds the line setup suggests, mentions Pharaoh another way, doesn't, or is missing, and whether setup's block is still there. In Claude Code started at a git repo's root, the same for the project's `CLAUDE.md`, `CLAUDE.local.md`, `.claude/CLAUDE.md` or `AGENTS.md`. Only those values are sent, never file text. A file over 64 KB, or one that isn't a regular file or can't be read, counts as unknown. Pharaoh uses this to learn whether the line leads agents to use it (privacy policy § 9, connection records).
|
|
41
|
+
|
|
29
42
|
## [0.3.59] - 2026-11-04
|
|
30
43
|
|
|
31
44
|
### Changed
|
package/README.md
CHANGED
|
@@ -152,10 +152,11 @@ claude mcp add --scope user pharaoh -- npx @pharaoh-so/mcp@<version> --server ht
|
|
|
152
152
|
- Credentials are stored with restrictive file permissions (`0600` — owner-read/write only)
|
|
153
153
|
- Authentication uses the [RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628) device authorization flow — no secrets are embedded in the package
|
|
154
154
|
- All communication with the Pharaoh server uses HTTPS/TLS
|
|
155
|
-
- The proxy never
|
|
155
|
+
- The proxy never sends your code or the text of any file. Pharaoh reads your code from GitHub through its GitHub App, maps it into a knowledge graph of structural metadata (function names, file paths, dependency relationships), and deletes the clone when mapping ends.
|
|
156
|
+
- From 0.3.60, when it connects, the proxy also tells Pharaoh whether your `~/.claude/CLAUDE.md` holds the line setup suggests, mentions Pharaoh another way, doesn't, or is missing, and whether setup's block is still in it. In Claude Code started at a git repo's root, it does the same for that project's `CLAUDE.md`, `CLAUDE.local.md`, `.claude/CLAUDE.md` and `AGENTS.md`. Only these values are sent, never the files' text.
|
|
156
157
|
- Tokens expire after 7 days and are refreshed automatically
|
|
157
158
|
- Only use trusted server URLs — the proxy sends your auth token to the configured server
|
|
158
|
-
-
|
|
159
|
+
- The proxy's only network calls are the device flow, token refresh, the MCP connection, and one registry lookup during interactive setup (below). Over the MCP connection it tells Pharaoh which client it serves (the name and version that client reports) and whether Pharaoh's Claude Code plugin is enabled, which it reads from `~/.claude/settings.json`. Nothing else about your machine is sent.
|
|
159
160
|
|
|
160
161
|
### What the installer writes on your machine
|
|
161
162
|
|
package/dist/backend.d.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { CallToolResult, ClientCapabilities, Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
19
19
|
import type { DeviceFlowGateway } from "./gateway.js";
|
|
20
|
+
import type { InstructionState, ManagedBlockState } from "./install-skills.js";
|
|
20
21
|
/**
|
|
21
22
|
* Notification that tells Pharaoh which client the proxy serves. Pharaoh sees
|
|
22
23
|
* the proxy itself as `pharaoh-proxy`; this lands on the session's row.
|
|
@@ -32,6 +33,12 @@ export type DownstreamClient = {
|
|
|
32
33
|
};
|
|
33
34
|
/** Pharaoh's Claude Code plugin as ~/.claude/settings.json lists it. */
|
|
34
35
|
plugin: "enabled" | "disabled" | "absent";
|
|
36
|
+
/** What the client's instruction files say about Pharaoh (install-skills.ts instructionStates); never their text. */
|
|
37
|
+
instructions?: {
|
|
38
|
+
user: InstructionState;
|
|
39
|
+
managed: ManagedBlockState;
|
|
40
|
+
project: InstructionState;
|
|
41
|
+
};
|
|
35
42
|
};
|
|
36
43
|
/** The contract ProxyServer uses to delegate tool handling. */
|
|
37
44
|
export interface Backend {
|
package/dist/helpers.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare const NPX_COMMAND = "npx @pharaoh-so/mcp";
|
|
|
5
5
|
/** Write one or more lines to stderr. */
|
|
6
6
|
export declare function printLines(...lines: string[]): void;
|
|
7
7
|
/**
|
|
8
|
-
* Ask a yes/no question
|
|
8
|
+
* Ask a yes/no question. Enter uses the default; `y` or `yes` is yes; `n`
|
|
9
9
|
* or `no` is no; anything else asks again. Ctrl-C exits (130). On a terminal,
|
|
10
10
|
* keys pressed before the question showed (an Enter while sign-in waited) are
|
|
11
11
|
* dropped, so only a key pressed after reading it answers.
|
|
@@ -13,11 +13,12 @@ export declare function printLines(...lines: string[]): void;
|
|
|
13
13
|
* @param question - The prompt, ending in something like `[Y/n] `.
|
|
14
14
|
* @param input - Where the answer is read (defaults to stdin).
|
|
15
15
|
* @param output - Where the prompt is written (defaults to stderr, like every other line).
|
|
16
|
+
* @param defaultAnswer - The answer for Enter (defaults to yes).
|
|
16
17
|
* @returns The answer, or null when input ended before one.
|
|
17
18
|
*/
|
|
18
19
|
export declare function askYesNo(question: string, input?: NodeJS.ReadableStream & {
|
|
19
20
|
isTTY?: boolean;
|
|
20
|
-
}, output?: NodeJS.WritableStream): Promise<boolean | null>;
|
|
21
|
+
}, output?: NodeJS.WritableStream, defaultAnswer?: boolean): Promise<boolean | null>;
|
|
21
22
|
/** Parse CLI arguments. */
|
|
22
23
|
export declare function parseArgs(argv?: string[]): {
|
|
23
24
|
server: string;
|
package/dist/helpers.js
CHANGED
|
@@ -15,7 +15,7 @@ export function printLines(...lines) {
|
|
|
15
15
|
/** How long to read and drop input that was typed before a question showed. */
|
|
16
16
|
const TYPE_AHEAD_MS = 50;
|
|
17
17
|
/**
|
|
18
|
-
* Ask a yes/no question
|
|
18
|
+
* Ask a yes/no question. Enter uses the default; `y` or `yes` is yes; `n`
|
|
19
19
|
* or `no` is no; anything else asks again. Ctrl-C exits (130). On a terminal,
|
|
20
20
|
* keys pressed before the question showed (an Enter while sign-in waited) are
|
|
21
21
|
* dropped, so only a key pressed after reading it answers.
|
|
@@ -23,9 +23,10 @@ const TYPE_AHEAD_MS = 50;
|
|
|
23
23
|
* @param question - The prompt, ending in something like `[Y/n] `.
|
|
24
24
|
* @param input - Where the answer is read (defaults to stdin).
|
|
25
25
|
* @param output - Where the prompt is written (defaults to stderr, like every other line).
|
|
26
|
+
* @param defaultAnswer - The answer for Enter (defaults to yes).
|
|
26
27
|
* @returns The answer, or null when input ended before one.
|
|
27
28
|
*/
|
|
28
|
-
export async function askYesNo(question, input = process.stdin, output = process.stderr) {
|
|
29
|
+
export async function askYesNo(question, input = process.stdin, output = process.stderr, defaultAnswer = true) {
|
|
29
30
|
if (input.isTTY)
|
|
30
31
|
await dropTypedAhead(input);
|
|
31
32
|
const rl = createInterface({ input, output });
|
|
@@ -39,7 +40,9 @@ export async function askYesNo(question, input = process.stdin, output = process
|
|
|
39
40
|
// The iterator ends on EOF; rl.question never settles then.
|
|
40
41
|
for await (const reply of rl) {
|
|
41
42
|
const answer = reply.trim().toLowerCase();
|
|
42
|
-
if (answer === ""
|
|
43
|
+
if (answer === "")
|
|
44
|
+
return defaultAnswer;
|
|
45
|
+
if (answer === "y" || answer === "yes")
|
|
43
46
|
return true;
|
|
44
47
|
if (answer === "n" || answer === "no")
|
|
45
48
|
return false;
|
|
@@ -96,7 +99,7 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
96
99
|
return { server, logout };
|
|
97
100
|
}
|
|
98
101
|
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", " --memory-hooks
|
|
102
|
+
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 Accept project memory hooks during eligible interactive setup", " --disable-memory-hooks Remove Pharaoh's memory hooks and remember the choice", " --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
103
|
}
|
|
101
104
|
/**
|
|
102
105
|
* Validate that a server-supplied SSE URL shares the same origin as the configured server.
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* mode until device flow completes, then swap to SSE in
|
|
13
13
|
* place — the stdio session never disconnects.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
15
|
+
import { resetHookPreferences } from "../project-memory/preferences.mjs";
|
|
16
|
+
import { applyMemoryHookChoices, chooseMemoryHooks, syncMemoryHooks, } from "../project-memory/setup.mjs";
|
|
16
17
|
import { pollForToken, printActivationPrompt, printAuthSuccess, requestDeviceCode, } from "./auth.js";
|
|
17
18
|
import { AuthRequiredBackend, createSseBackend } from "./backend.js";
|
|
18
19
|
import { deleteCredentials, readCredentials, writeCredentials, } from "./credentials.js";
|
|
@@ -44,6 +45,34 @@ function reportMemorySetup(options) {
|
|
|
44
45
|
return false;
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
function clearMemoryHookChoices(disable) {
|
|
49
|
+
try {
|
|
50
|
+
resetHookPreferences(disable);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
printLines("Pharaoh: could not update local project memory choices.");
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function setupMemoryHooks(credentials, clients, requested) {
|
|
59
|
+
try {
|
|
60
|
+
const decision = await chooseMemoryHooks({
|
|
61
|
+
credentials,
|
|
62
|
+
clients,
|
|
63
|
+
requested,
|
|
64
|
+
ask: (question, defaultAnswer) => process.stderr.isTTY
|
|
65
|
+
? askYesNo(question, process.stdin, process.stderr, defaultAnswer)
|
|
66
|
+
: Promise.resolve(null),
|
|
67
|
+
});
|
|
68
|
+
const notices = applyMemoryHookChoices(decision, { credentials });
|
|
69
|
+
if (notices.length)
|
|
70
|
+
printLines(...notices);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
printLines("Pharaoh: could not finish local project memory setup. MCP remains available.");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
47
76
|
async function main() {
|
|
48
77
|
const args = process.argv.slice(2);
|
|
49
78
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -68,7 +97,8 @@ async function main() {
|
|
|
68
97
|
return;
|
|
69
98
|
}
|
|
70
99
|
if (args.includes("--disable-memory-hooks")) {
|
|
71
|
-
|
|
100
|
+
const saved = clearMemoryHookChoices(true);
|
|
101
|
+
if (!reportMemorySetup() || !saved)
|
|
72
102
|
process.exitCode = 1;
|
|
73
103
|
return;
|
|
74
104
|
}
|
|
@@ -77,10 +107,11 @@ async function main() {
|
|
|
77
107
|
if (args.includes("--clean")) {
|
|
78
108
|
const { cleanCodexSetup, cleanSlate } = await import("./setup.js");
|
|
79
109
|
const { uninstallClaudePlugin } = await import("./install-skills.js");
|
|
110
|
+
clearMemoryHookChoices(false);
|
|
111
|
+
reportMemorySetup();
|
|
80
112
|
cleanSlate();
|
|
81
113
|
uninstallClaudePlugin();
|
|
82
114
|
cleanCodexSetup();
|
|
83
|
-
reportMemorySetup();
|
|
84
115
|
printLines("Pharaoh: local setup cleanup finished.");
|
|
85
116
|
process.exit(0);
|
|
86
117
|
}
|
|
@@ -90,10 +121,11 @@ async function main() {
|
|
|
90
121
|
const { uninstallClaudePlugin } = await import("./install-skills.js");
|
|
91
122
|
// Credentials first: a cleanup step that fails must never leave the tokens behind.
|
|
92
123
|
deleteCredentials();
|
|
124
|
+
clearMemoryHookChoices(false);
|
|
125
|
+
reportMemorySetup();
|
|
93
126
|
cleanSlate();
|
|
94
127
|
uninstallClaudePlugin();
|
|
95
128
|
cleanCodexSetup();
|
|
96
|
-
reportMemorySetup();
|
|
97
129
|
printLines("Pharaoh: credentials cleared and local setup cleanup finished.");
|
|
98
130
|
process.exit(0);
|
|
99
131
|
}
|
|
@@ -130,8 +162,10 @@ async function main() {
|
|
|
130
162
|
if (runCodexSetup())
|
|
131
163
|
clients.push("codex");
|
|
132
164
|
}
|
|
133
|
-
if (!clients.length)
|
|
165
|
+
if (!clients.length) {
|
|
166
|
+
await setupMemoryHooks(newCreds, clients, args.includes("--memory-hooks"));
|
|
134
167
|
process.exit(1);
|
|
168
|
+
}
|
|
135
169
|
if (clients.includes("claude-code")) {
|
|
136
170
|
process.stderr.write(" Setting up...");
|
|
137
171
|
const { keptCommands } = runInstallSkills(undefined, { silent: true });
|
|
@@ -144,7 +178,7 @@ async function main() {
|
|
|
144
178
|
if (claudeMd)
|
|
145
179
|
printLines(claudeMd);
|
|
146
180
|
}
|
|
147
|
-
|
|
181
|
+
await setupMemoryHooks(newCreds, clients, args.includes("--memory-hooks"));
|
|
148
182
|
// The registration is pinned to this version; upgrading is a deliberate
|
|
149
183
|
// re-run. Interactive only, and quiet when the registry can't be reached.
|
|
150
184
|
const latest = await fetchLatestVersion();
|
package/dist/install-skills.d.ts
CHANGED
|
@@ -87,6 +87,19 @@ export declare function allowReadTools(home?: string): AllowReadToolsResult;
|
|
|
87
87
|
export declare function describeAllowReadTools(result: AllowReadToolsResult): string;
|
|
88
88
|
/** What the interactive setup asks before writing the line. */
|
|
89
89
|
export declare const CLAUDE_MD_QUESTION = "Pharaoh: add one line to ~/.claude/CLAUDE.md so Claude checks Pharaoh before searching your code?\n \"In repos Pharaoh has mapped, before searching or reading files, ask Pharaoh where the relevant code is, what calls it, and whether what you need already exists.\"\n[Y/n] ";
|
|
90
|
+
/** What an instruction file says about Pharaoh: the suggested line, another mention, none, no file, or unreadable. */
|
|
91
|
+
export type InstructionState = "exact" | "mention" | "none" | "missing" | "unknown";
|
|
92
|
+
/** Whether the block setup wrote into ~/.claude/CLAUDE.md is there and holds the current line. */
|
|
93
|
+
export type ManagedBlockState = "current" | "stale" | "absent" | "unknown";
|
|
94
|
+
/**
|
|
95
|
+
* The instruction state the proxy reports for its client. Project files only
|
|
96
|
+
* for clients known to start the proxy in the project folder. Never throws.
|
|
97
|
+
*/
|
|
98
|
+
export declare function instructionStates(clientName: string | undefined, cwd?: string, home?: string): {
|
|
99
|
+
user: InstructionState;
|
|
100
|
+
managed: ManagedBlockState;
|
|
101
|
+
project: InstructionState;
|
|
102
|
+
};
|
|
90
103
|
/** What `offerClaudeMdLine` did; `describeClaudeMdOutcome` words it. */
|
|
91
104
|
export type ClaudeMdOutcome = "added" | "updated" | "declined" | "kept" | "failed" | "current" | "answered" | "cancelled" | "no-terminal" | "no-claude";
|
|
92
105
|
/**
|
package/dist/install-skills.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* and OpenClaw skills in ~/.openclaw/skills/. Also merges the Pharaoh MCP
|
|
7
7
|
* server config into OpenClaw.
|
|
8
8
|
*/
|
|
9
|
-
import { closeSync, constants, cpSync, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
9
|
+
import { closeSync, constants, cpSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
-
import { dirname, join } from "node:path";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { ensurePharaohHome } from "./credentials.js";
|
|
14
14
|
import { CLAUDE_CLI_SETUP_URL, claudeCliFor } from "./setup.js";
|
|
@@ -765,6 +765,111 @@ function readClaudeMd(home) {
|
|
|
765
765
|
const current = text.slice(block.body, block.to) === `${PHARAOH_CLAUDE_MD_LINE}\n`;
|
|
766
766
|
return { state: current ? "current" : "stale", path, text, block };
|
|
767
767
|
}
|
|
768
|
+
/** A CLAUDE.md is a few KB; anything bigger reads as "unknown" instead of slowing initialize. */
|
|
769
|
+
const INSTRUCTION_READ_CAP = 64 * 1024;
|
|
770
|
+
/**
|
|
771
|
+
* A project's instruction files, read only for clients confirmed to start the
|
|
772
|
+
* proxy in the project folder (Claude Code 2.1.310 does: probed 2026-11-04).
|
|
773
|
+
*/
|
|
774
|
+
const PROJECT_INSTRUCTION_FILES = [
|
|
775
|
+
"CLAUDE.md",
|
|
776
|
+
"CLAUDE.local.md",
|
|
777
|
+
join(".claude", "CLAUDE.md"),
|
|
778
|
+
"AGENTS.md",
|
|
779
|
+
];
|
|
780
|
+
const PROJECT_CWD_CLIENTS = new Set(["claude-code"]);
|
|
781
|
+
/**
|
|
782
|
+
* Up to `cap` bytes of a regular file, read from the descriptor that was
|
|
783
|
+
* checked, so a FIFO or device swapped in can't block `initialize`. A symlink
|
|
784
|
+
* is followed: only a state is derived from what it points at.
|
|
785
|
+
*
|
|
786
|
+
* @returns The file's text, wrapped so no content can read as a state;
|
|
787
|
+
* "missing" when there's no file; "unknown" when it's over the cap, not a
|
|
788
|
+
* regular file, or unreadable
|
|
789
|
+
*/
|
|
790
|
+
function readBoundedText(path, cap = INSTRUCTION_READ_CAP) {
|
|
791
|
+
let fd;
|
|
792
|
+
try {
|
|
793
|
+
fd = openSync(path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
|
|
794
|
+
}
|
|
795
|
+
catch (err) {
|
|
796
|
+
const code = err.code;
|
|
797
|
+
return code === "ENOENT" || code === "ENOTDIR" ? "missing" : "unknown";
|
|
798
|
+
}
|
|
799
|
+
try {
|
|
800
|
+
if (!fstatSync(fd).isFile())
|
|
801
|
+
return "unknown";
|
|
802
|
+
const buf = Buffer.alloc(cap + 1);
|
|
803
|
+
let length = 0;
|
|
804
|
+
while (length <= cap) {
|
|
805
|
+
const n = readSync(fd, buf, length, cap + 1 - length, null);
|
|
806
|
+
if (n === 0)
|
|
807
|
+
break;
|
|
808
|
+
length += n;
|
|
809
|
+
}
|
|
810
|
+
return length > cap ? "unknown" : { text: buf.toString("utf-8", 0, length) };
|
|
811
|
+
}
|
|
812
|
+
catch {
|
|
813
|
+
return "unknown";
|
|
814
|
+
}
|
|
815
|
+
finally {
|
|
816
|
+
closeSync(fd);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
/** Whether `text` holds the suggested line, otherwise mentions Pharaoh, or neither. */
|
|
820
|
+
function classifyInstructionText(text) {
|
|
821
|
+
if (text.includes(PHARAOH_CLAUDE_MD_LINE))
|
|
822
|
+
return "exact";
|
|
823
|
+
return /\bpharaoh\b/i.test(text) ? "mention" : "none";
|
|
824
|
+
}
|
|
825
|
+
/** What ~/.claude/CLAUDE.md says about Pharaoh, and whether setup's block is there and current. */
|
|
826
|
+
function userInstructionState(home = homedir()) {
|
|
827
|
+
const file = readBoundedText(join(home, ".claude", "CLAUDE.md"));
|
|
828
|
+
if (file === "missing")
|
|
829
|
+
return { user: "missing", managed: "absent" };
|
|
830
|
+
if (file === "unknown")
|
|
831
|
+
return { user: "unknown", managed: "unknown" };
|
|
832
|
+
const { text } = file;
|
|
833
|
+
const block = findClaudeMdBlock(text);
|
|
834
|
+
const managed = block === null
|
|
835
|
+
? "absent"
|
|
836
|
+
: block === "broken"
|
|
837
|
+
? "unknown"
|
|
838
|
+
: text.slice(block.body, block.to) === `${PHARAOH_CLAUDE_MD_LINE}\n`
|
|
839
|
+
? "current"
|
|
840
|
+
: "stale";
|
|
841
|
+
return { user: classifyInstructionText(text), managed };
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* The strongest state across a project's instruction files: any exact line,
|
|
845
|
+
* then any mention, then any unreadable file, then a file without a mention.
|
|
846
|
+
* "unknown" outside a git repository's root or in the home folder.
|
|
847
|
+
*/
|
|
848
|
+
function projectInstructionState(cwd, home = homedir()) {
|
|
849
|
+
if (resolve(cwd) === resolve(home) || !existsSync(join(cwd, ".git")))
|
|
850
|
+
return "unknown";
|
|
851
|
+
const states = PROJECT_INSTRUCTION_FILES.map((file) => {
|
|
852
|
+
const read = readBoundedText(join(cwd, file));
|
|
853
|
+
return typeof read === "string" ? read : classifyInstructionText(read.text);
|
|
854
|
+
});
|
|
855
|
+
return (["exact", "mention", "unknown", "none"].find((s) => states.includes(s)) ?? "missing");
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* The instruction state the proxy reports for its client. Project files only
|
|
859
|
+
* for clients known to start the proxy in the project folder. Never throws.
|
|
860
|
+
*/
|
|
861
|
+
export function instructionStates(clientName, cwd, home = homedir()) {
|
|
862
|
+
try {
|
|
863
|
+
// process.cwd() throws when the folder was deleted: inside the try, so it reads as unknown.
|
|
864
|
+
const project = clientName && PROJECT_CWD_CLIENTS.has(clientName)
|
|
865
|
+
? projectInstructionState(cwd ?? process.cwd(), home)
|
|
866
|
+
: "unknown";
|
|
867
|
+
return { ...userInstructionState(home), project };
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
return { user: "unknown", managed: "unknown", project: "unknown" };
|
|
871
|
+
}
|
|
872
|
+
}
|
|
768
873
|
/**
|
|
769
874
|
* Offer the Pharaoh-first line in ~/.claude/CLAUDE.md. Interactive setup only,
|
|
770
875
|
* never a proxy start. Asks only when the block is absent and the user hasn't
|
package/dist/proxy-server.js
CHANGED
|
@@ -27,7 +27,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
|
|
|
27
27
|
import { AuthRequiredBackend, createSseBackend, } from "./backend.js";
|
|
28
28
|
import { loadBundledCatalog } from "./catalog.js";
|
|
29
29
|
import { createGateway } from "./gateway.js";
|
|
30
|
-
import { claudePluginState } from "./install-skills.js";
|
|
30
|
+
import { claudePluginState, instructionStates } from "./install-skills.js";
|
|
31
31
|
import { TenantSuspendedError, TokenExpiredError } from "./proxy.js";
|
|
32
32
|
import { registerTraceResource } from "./trace-resource.js";
|
|
33
33
|
import { getPackageVersion } from "./version.js";
|
|
@@ -70,6 +70,7 @@ export class ProxyServer {
|
|
|
70
70
|
const downstream = {
|
|
71
71
|
client: { name: info?.name, version: info?.version },
|
|
72
72
|
plugin: claudePluginState(),
|
|
73
|
+
instructions: instructionStates(info?.name),
|
|
73
74
|
};
|
|
74
75
|
this.downstream = downstream;
|
|
75
76
|
this.clientCapabilities = this.server.getClientCapabilities() ?? {};
|
package/hooks/route-grep.mjs
CHANGED
|
@@ -103,6 +103,69 @@ export function routeGrep(input, signedIn, redirected) {
|
|
|
103
103
|
].join(" ");
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Match one plain Bash rg invocation with an explicit declaration or call pattern.
|
|
108
|
+
* @param {unknown} command
|
|
109
|
+
* @returns {{ symbol: string, tool: "search_functions" | "get_blast_radius" } | undefined}
|
|
110
|
+
*/
|
|
111
|
+
export function classifyBashGrep(command) {
|
|
112
|
+
if (typeof command !== "string" || command.length > 1024 || /[\r\n]/.test(command)) return;
|
|
113
|
+
const source = command.trim();
|
|
114
|
+
// Only the quoting and flags needed by a simple rg search. Unrecognized
|
|
115
|
+
// shell syntax falls through, including expansions, redirects and pipelines.
|
|
116
|
+
const word = /(?:'([^']*)'|"([^"$`\\]*)"|([A-Za-z0-9_./:-]+))(?:[ \t]+|$)/gy;
|
|
117
|
+
const args = [];
|
|
118
|
+
while (word.lastIndex < source.length) {
|
|
119
|
+
const match = word.exec(source);
|
|
120
|
+
if (!match) return;
|
|
121
|
+
args.push(match[1] ?? match[2] ?? match[3]);
|
|
122
|
+
}
|
|
123
|
+
if (args.shift() !== "rg") return;
|
|
124
|
+
|
|
125
|
+
const scope = {};
|
|
126
|
+
while (args[0]?.startsWith("-")) {
|
|
127
|
+
const option = args.shift();
|
|
128
|
+
if (option === "--") break;
|
|
129
|
+
if (["-n", "--line-number", "--no-heading", "-S", "--smart-case"].includes(option ?? ""))
|
|
130
|
+
continue;
|
|
131
|
+
const value = args.shift();
|
|
132
|
+
if (!value) return;
|
|
133
|
+
if ((option === "-g" || option === "--glob") && scope.glob === undefined) scope.glob = value;
|
|
134
|
+
else if ((option === "-t" || option === "--type") && scope.type === undefined)
|
|
135
|
+
scope.type = value;
|
|
136
|
+
else return;
|
|
137
|
+
}
|
|
138
|
+
const [pattern, path] = args;
|
|
139
|
+
const globExtensions = scope.glob?.match(/^\*\.(?:\{([a-z]+(?:,[a-z]+)*)\}|([a-z]+))$/i);
|
|
140
|
+
if (
|
|
141
|
+
!pattern ||
|
|
142
|
+
args.length > 2 ||
|
|
143
|
+
(path &&
|
|
144
|
+
(!/^[A-Za-z0-9_./][A-Za-z0-9_./-]*$/.test(path) ||
|
|
145
|
+
path.startsWith("/") ||
|
|
146
|
+
path.split("/").includes(".."))) ||
|
|
147
|
+
[scope.glob, path].some((value) =>
|
|
148
|
+
/(?:^|[/.\-_])(?:config|settings|env|docs|documentation|fixtures|snapshots)(?:[/.\-_]|$)/i.test(
|
|
149
|
+
value ?? "",
|
|
150
|
+
),
|
|
151
|
+
) ||
|
|
152
|
+
(scope.glob &&
|
|
153
|
+
(!globExtensions ||
|
|
154
|
+
!(globExtensions[1] ?? globExtensions[2])
|
|
155
|
+
.toLowerCase()
|
|
156
|
+
.split(",")
|
|
157
|
+
.every((extension) => MAPPED_EXTENSIONS.has(extension)))) ||
|
|
158
|
+
!searchesMappedCode({ type: scope.type }) ||
|
|
159
|
+
!searchesMappedCode({ path })
|
|
160
|
+
)
|
|
161
|
+
return;
|
|
162
|
+
const symbol = symbolSearched(pattern);
|
|
163
|
+
if (!symbol || symbol.length > 80) return;
|
|
164
|
+
if (/\\?\($/.test(pattern)) return { symbol, tool: "get_blast_radius" };
|
|
165
|
+
if (/^(?:(?:export|default|async) +)*(?:function|class|def|interface|type|enum)\b/.test(pattern))
|
|
166
|
+
return { symbol, tool: "search_functions" };
|
|
167
|
+
}
|
|
168
|
+
|
|
106
169
|
/** Hook entry point: read the payload from stdin, reply with a PreToolUse deny or nothing. */
|
|
107
170
|
function main() {
|
|
108
171
|
try {
|
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.61",
|
|
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",
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { dirname, join, relative } from "node:path";
|
|
13
13
|
|
|
14
14
|
const LIMIT = 256 * 1024;
|
|
15
|
-
export const
|
|
15
|
+
export const LEGACY_MEMORY_RUNTIME_FILES = [
|
|
16
16
|
"execute-turn.mjs",
|
|
17
17
|
"hook-config.mjs",
|
|
18
18
|
"hook-events.mjs",
|
|
@@ -25,6 +25,13 @@ export const MEMORY_RUNTIME_FILES = [
|
|
|
25
25
|
"runtime/package.json",
|
|
26
26
|
"runtime/refresh.js",
|
|
27
27
|
];
|
|
28
|
+
export const MEMORY_RUNTIME_FILES = [
|
|
29
|
+
...LEGACY_MEMORY_RUNTIME_FILES,
|
|
30
|
+
"bundle.mjs",
|
|
31
|
+
"graph-cues.mjs",
|
|
32
|
+
"preferences.mjs",
|
|
33
|
+
"route-grep.mjs",
|
|
34
|
+
];
|
|
28
35
|
|
|
29
36
|
function digest(files) {
|
|
30
37
|
return createHash("sha256").update(JSON.stringify(files)).digest("hex");
|
|
@@ -51,7 +58,7 @@ export function packagedMemoryBundle(packageRoot = new URL("../", import.meta.ur
|
|
|
51
58
|
} else {
|
|
52
59
|
const location = name.startsWith("runtime/")
|
|
53
60
|
? new URL(`dist/${name.slice(8)}`, packageRoot)
|
|
54
|
-
: new URL(
|
|
61
|
+
: new URL(`${name === "route-grep.mjs" ? "hooks" : "project-memory"}/${name}`, packageRoot);
|
|
55
62
|
try {
|
|
56
63
|
bytes = readFileSync(location);
|
|
57
64
|
} catch {
|