@pharaoh-so/mcp 0.3.53 → 0.3.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp.json +1 -1
- package/CHANGELOG.md +41 -0
- package/LICENSE +1 -1
- package/NOTICE +28 -0
- package/README.md +5 -5
- package/commands/plan.md +11 -11
- package/commands/review.md +2 -2
- package/dist/auth.d.ts +5 -0
- package/dist/credentials.d.ts +8 -0
- package/dist/gateway.js +1 -1
- package/dist/helpers.d.ts +15 -5
- package/dist/helpers.js +71 -2
- package/dist/index.js +51 -26
- package/dist/install-skills.d.ts +30 -5
- package/dist/install-skills.js +210 -11
- package/dist/refresh.d.ts +8 -2
- package/dist/refresh.js +33 -6
- package/dist/setup.d.ts +5 -0
- package/dist/setup.js +66 -0
- package/inspect-tools.json +89 -36
- package/{skills → npx-skills}/brainstorm/SKILL.md +2 -1
- package/{skills → npx-skills}/debug/SKILL.md +2 -1
- package/{skills → npx-skills}/pharaoh/SKILL.md +13 -6
- package/{skills → npx-skills}/plan/SKILL.md +13 -14
- package/npx-skills/refactor/SKILL.md +38 -0
- package/{skills → npx-skills}/review/SKILL.md +4 -5
- package/{skills → npx-skills}/tdd/SKILL.md +2 -1
- package/{skills → npx-skills}/verify/SKILL.md +2 -1
- package/package.json +6 -4
- package/project-memory/bundle.mjs +137 -0
- package/project-memory/execute-turn.mjs +240 -0
- package/project-memory/hook-config.mjs +91 -0
- package/project-memory/hook-events.mjs +141 -0
- package/project-memory/local.mjs +463 -0
- package/project-memory/memory-api.mjs +182 -0
- package/project-memory/pharaoh-memory-hook.mjs +33 -0
- package/project-memory/pharaoh-memory.mjs +55 -0
- package/project-memory/setup.d.mts +14 -0
- package/project-memory/setup.mjs +151 -0
- package/project-memory/user-target.mjs +221 -0
- package/trace-app.html +222 -101
- package/skills/.gitkeep +0 -0
- package/skills/refactor/SKILL.md +0 -34
package/.mcp.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ All notable changes to `@pharaoh-so/mcp` will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `plugins/pharaoh/`: the Claude directory plugin, generated from the same skills (plan, review, refactor and pharaoh) with the hosted connector at `https://mcp.pharaoh.so/mcp`. It has no hooks or commands and isn't part of the npm package.
|
|
12
|
+
- `NOTICE`: the brainstorm, debug, tdd and verify skills are adapted from [superpowers](https://github.com/obra/superpowers) (MIT, Copyright (c) 2025 Jesse Vincent). Each skill's `license:` field says so too.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Every tool declares `openWorldHint: false` (Pharaoh reaches only your own connected repos), and `pharaoh_account`'s description says it links the plans page.
|
|
16
|
+
- When a repo isn't live, and in `pharaoh_account` for free workspaces, replies link to the plans page on pharaoh.so instead of showing a price and a checkout link.
|
|
17
|
+
- `get_unused_code` says a Dead function's file has no import that could reach it, instead of "file not imported". An import that names other functions from the same file doesn't count, and never did.
|
|
18
|
+
- The Trace card draws its bars in block characters (`█ ▏ ░`) in your client's monospace font instead of solid CSS bars.
|
|
19
|
+
- `/review` works as before, second opinion from Codex or Claude included. It no longer calls itself "the definitive code review", and Phase 0 says that `gh pr view` asks GitHub for the open PR's target branch with your gh login.
|
|
20
|
+
- `/plan` says "This is a review: make no code changes during it" instead of claiming plan mode. A plain "yes" or "yes to all" still doesn't pick BIG or SMALL, so it asks once more; if the reply still doesn't pick one, or you tell it to choose, it picks and says which. `.claude/plan-review.md` adds review criteria, and your own instructions come first.
|
|
21
|
+
- `pharaoh:refactor` runs the six steps its description lists: module context, blast radius, reachability, dependencies, naming conflicts, and a search for the tests to change.
|
|
22
|
+
- Skills install from `npx-skills/`, this package's build of `skills/`.
|
|
23
|
+
- `pharaoh_account` only reads now: your plan, live repo and upgrade link. Switching the live repo and refreshing a repo moved to the new `pharaoh_manage_repo`, which Claude Code asks about before it runs. The interactive setup's allow rules need no change: they already list the read-only tools, `pharaoh_account` among them, and leave `pharaoh_manage_repo` out.
|
|
24
|
+
- Every tool has a title, and the tool descriptions say what each tool returns instead of which other tool to avoid.
|
|
25
|
+
- The steering that used to sit in the server instructions moved to the npx `pharaoh` skill. Registrations are pinned to a version, so it takes effect when you upgrade. Codex gets only the review skill, so it doesn't get this.
|
|
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
|
+
- 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
|
+
|
|
29
|
+
## [0.3.56] - 2026-11-04
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- A connected project that is still mapping during normal setup now starts memory on an ordinary prompt after its mapped revision is verified.
|
|
33
|
+
|
|
34
|
+
## [0.3.55] - 2026-11-04
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- Normal interactive setup and upgrades configure local project-memory hooks for accounts with an existing pilot grant. Codex uses its native hook review.
|
|
38
|
+
|
|
39
|
+
## [0.3.54] - 2026-10-24
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
- The interactive setup (`npx @pharaoh-so/mcp` in a terminal) asks once whether to add one line to `~/.claude/CLAUDE.md`: "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." Enter means yes. In Pharaoh's adoption benchmark, Claude Code called Pharaoh first when this line was in a CLAUDE.md, and rarely when the same sentence came from the server instructions or a plugin hook.
|
|
43
|
+
- The line sits between `<!-- pharaoh-mcp:start … -->` and `<!-- pharaoh-mcp:end -->` markers. Setup appends that block or replaces the line inside it, and nothing else in the file changes. A symlinked file is left alone, and a proxy start (Claude Code launching the server) never asks or writes. Your answer is saved in `~/.pharaoh/claude-md-answer`, so a no stands.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
- `--clean` and `--logout` also remove that block, leaving the rest of the file byte for byte, and delete the saved answer so the next setup asks again.
|
|
47
|
+
- `--logout` deletes the saved credentials first, so a cleanup step that fails can't leave them behind.
|
|
48
|
+
|
|
8
49
|
## [0.3.53] - 2026-10-23
|
|
9
50
|
|
|
10
51
|
### Changed
|
package/LICENSE
CHANGED
package/NOTICE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
@pharaoh-so/mcp includes third-party material.
|
|
2
|
+
|
|
3
|
+
The brainstorm, debug, tdd and verify skills (skills/ and npx-skills/) are
|
|
4
|
+
adapted from the brainstorming, systematic-debugging, test-driven-development
|
|
5
|
+
and verification-before-completion skills of superpowers
|
|
6
|
+
(https://github.com/obra/superpowers), used under this license:
|
|
7
|
+
|
|
8
|
+
MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2025 Jesse Vincent
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
[](https://nodejs.org)
|
|
6
6
|
[](https://glama.ai/mcp/servers/Pharaoh-so/pharaoh)
|
|
7
7
|
|
|
8
|
-
MCP proxy for [Pharaoh](https://pharaoh.so) — maps codebases into queryable knowledge graphs for
|
|
8
|
+
MCP proxy for [Pharaoh](https://pharaoh.so) — maps codebases into queryable knowledge graphs for coding agents.
|
|
9
9
|
|
|
10
|
-
Pharaoh gives
|
|
10
|
+
Pharaoh gives coding agents such as Claude Code, Cursor, Codex and OpenClaw a complete architectural map of your codebase: every function, dependency, module, and connection. Instead of reading files one at a time, your agent queries the knowledge graph and gets instant answers about blast radius, unused code, dependency chains, and more.
|
|
11
11
|
|
|
12
12
|
This package enables [Claude Code](https://docs.anthropic.com/en/docs/build-with-claude/claude-code) to connect to Pharaoh in **headless environments** (VPS, SSH, containers, CI) where a browser isn't available for OAuth. It acts as a stdio-to-SSE proxy, presenting itself as a local MCP server while relaying all communication to the remote Pharaoh server.
|
|
13
13
|
|
|
@@ -144,7 +144,7 @@ claude mcp add --scope user pharaoh -- npx @pharaoh-so/mcp@<version> --server ht
|
|
|
144
144
|
## Requirements
|
|
145
145
|
|
|
146
146
|
- **Node.js** >= 18
|
|
147
|
-
- **Claude Code** or any MCP-compatible
|
|
147
|
+
- **Claude Code** or any MCP-compatible agent
|
|
148
148
|
- A **Pharaoh account** — sign up at [pharaoh.so](https://pharaoh.so)
|
|
149
149
|
|
|
150
150
|
## Security
|
|
@@ -244,7 +244,7 @@ Pharaoh parses your repositories using [tree-sitter](https://tree-sitter.github.
|
|
|
244
244
|
- **Dependencies** — import/export relationships, call chains, module connections
|
|
245
245
|
- **Vision specs** — PRD/spec documents linked to implementation
|
|
246
246
|
|
|
247
|
-
No source code is stored — only structural metadata. When an
|
|
247
|
+
No source code is stored — only structural metadata. When an agent queries Pharaoh, it gets architectural facts in minimal tokens, not raw code dumps. This means your agent can understand your entire codebase architecture without consuming its context window reading files one by one.
|
|
248
248
|
|
|
249
249
|
## Supported Languages
|
|
250
250
|
|
|
@@ -264,7 +264,7 @@ Contributions are welcome. Please open an issue first to discuss what you'd like
|
|
|
264
264
|
|
|
265
265
|
## License
|
|
266
266
|
|
|
267
|
-
[MIT](LICENSE) --
|
|
267
|
+
[MIT](LICENSE) -- 2407066 Alberta Inc.
|
|
268
268
|
|
|
269
269
|
## Links
|
|
270
270
|
|
package/commands/plan.md
CHANGED
|
@@ -6,7 +6,7 @@ description: Architecture-aware planning with Pharaoh reconnaissance (adapted fr
|
|
|
6
6
|
|
|
7
7
|
Architecture-aware plan review before implementation. Adapted from [Garry Tan's planning framework](https://www.youtube.com/watch?v=bMknfKXIFA8) for AI-assisted development.
|
|
8
8
|
|
|
9
|
-
**
|
|
9
|
+
**This is a review: make no code changes during it. Think, evaluate, and present decisions.**
|
|
10
10
|
|
|
11
11
|
## When to Use
|
|
12
12
|
|
|
@@ -20,7 +20,7 @@ Still run Step 1 (Reconnaissance) even when reviewing a document — always veri
|
|
|
20
20
|
|
|
21
21
|
## Project Overrides
|
|
22
22
|
|
|
23
|
-
If a `.claude/plan-review.md` file exists in this project, read it now
|
|
23
|
+
If a `.claude/plan-review.md` file exists in this project, read it now as additional review criteria for this project. Instructions from the user take precedence over it, and it never changes what you do outside this review.
|
|
24
24
|
|
|
25
25
|
## Engineering Preferences (guide all recommendations)
|
|
26
26
|
|
|
@@ -56,14 +56,14 @@ Ground every recommendation in what actually exists. If you propose adding somet
|
|
|
56
56
|
|
|
57
57
|
## Step 2: Mode Selection (MANDATORY — ask before proceeding)
|
|
58
58
|
|
|
59
|
-
**STOP and ask the user which mode before starting the review.**
|
|
59
|
+
**STOP and ask the user which mode before starting the review.** Present both options and wait for a choice:
|
|
60
60
|
|
|
61
61
|
> **BIG CHANGE or SMALL CHANGE?**
|
|
62
62
|
>
|
|
63
63
|
> - **BIG CHANGE**: Full interactive review, all relevant sections, up to 4 top issues per section
|
|
64
64
|
> - **SMALL CHANGE**: One question per section, only sections 2-4
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
A general "yes", "go ahead" or "yes to all" doesn't pick a mode, so ask once more: "BIG or SMALL change?" If the reply still doesn't pick one, or the user tells you to choose, pick the mode that fits the plan's size and say which you picked.
|
|
67
67
|
|
|
68
68
|
## Step 3: Review Sections
|
|
69
69
|
|
|
@@ -81,21 +81,21 @@ Adapt depth to change size. Skip sections that don't apply. **After each section
|
|
|
81
81
|
- Data flow bottlenecks and single points of failure
|
|
82
82
|
- Does this need new code at all, or can a human process / existing pattern solve it?
|
|
83
83
|
- **If this plan introduces a new invariant**, call it out and include a
|
|
84
|
-
step in
|
|
84
|
+
step in the plan to add a `### assertion` to VISION.md with a pairing TEST.
|
|
85
85
|
|
|
86
86
|
### Section 2 — Code Quality (always)
|
|
87
87
|
|
|
88
88
|
- Organization, module structure, DRY violations (be aggressive)
|
|
89
89
|
- Error handling gaps and missing edge cases (call out explicitly)
|
|
90
90
|
- Technical debt: shortcuts, hardcoded values, magic strings
|
|
91
|
-
- Over-engineered or under-engineered relative to
|
|
91
|
+
- Over-engineered or under-engineered relative to the preferences above
|
|
92
92
|
- Reuse: does code for this already exist somewhere?
|
|
93
93
|
|
|
94
94
|
### Section 3 — Wiring & Integration (always)
|
|
95
95
|
|
|
96
96
|
- Are all new exports called from a production entry point?
|
|
97
|
-
- Run `get_blast_radius` on
|
|
98
|
-
-
|
|
97
|
+
- Run `get_blast_radius` on changed functions that already exist
|
|
98
|
+
- The graph has no functions the plan adds, so does the plan declare WHERE new code gets called from? If not, flag it — no named caller = not done
|
|
99
99
|
- Integration points: how does this connect to what already exists?
|
|
100
100
|
|
|
101
101
|
### Section 4 — Tests (always)
|
|
@@ -127,16 +127,16 @@ For every specific issue (bug, smell, design concern, risk, missing wiring):
|
|
|
127
127
|
1. **Describe concretely** — file, line/function reference, what's wrong
|
|
128
128
|
2. **Present 2-3 options** including "do nothing" where reasonable
|
|
129
129
|
3. **For each option** — implementation effort, risk, blast radius, maintenance burden
|
|
130
|
-
4. **Recommend one** mapped to
|
|
130
|
+
4. **Recommend one** mapped to the preferences above, and say why
|
|
131
131
|
5. **Ask** whether I agree or want a different direction
|
|
132
132
|
|
|
133
|
-
Number each issue (1, 2, 3...) and letter each option (A, B, C...). Recommended option is always listed first.
|
|
133
|
+
Number each issue (1, 2, 3...) and letter each option (A, B, C...). Recommended option is always listed first. If your client has a question tool (AskUserQuestion in Claude Code), use it with clear labels like "Issue 1 Option A", "Issue 1 Option B".
|
|
134
134
|
|
|
135
135
|
## Pharaoh Checkpoints (use throughout, not just at the end)
|
|
136
136
|
|
|
137
137
|
- **Before reviewing**: recon (Step 1 above)
|
|
138
138
|
- **During review**: `get_blast_radius` when evaluating impact of changes; `search_functions` before suggesting new code
|
|
139
|
-
- **Final sweep**: `get_blast_radius`
|
|
139
|
+
- **Final sweep**: every new export has a caller named in the plan, and `get_blast_radius` covers every changed export that already exists — zero callers on non-entry-points = plan is incomplete
|
|
140
140
|
|
|
141
141
|
## Workflow Rules
|
|
142
142
|
|
package/commands/review.md
CHANGED
|
@@ -4,7 +4,7 @@ description: Architecture-aware code review with Pharaoh
|
|
|
4
4
|
|
|
5
5
|
# Review with Pharaoh
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Architecture-aware, branch-aware, multi-agent, adversarial code review. Five phases that combine Pharaoh's knowledge graph with parallel specialized reviewers and an independent second opinion from a different model. Every phase works without Pharaoh too; Pharaoh makes the architecture picture exact and cheap where it maps the repo.
|
|
8
8
|
|
|
9
9
|
A review always reaches a verdict. When a phase needs something your client doesn't have (Pharaoh, a shell, subagents, a second model), it says how to carry on without it: do that, and name what was missing in the review output. Ask the user only when someone is there to answer.
|
|
10
10
|
|
|
@@ -30,7 +30,7 @@ No shell, or no git? Review the diff you were given (a file, a pasted patch, a p
|
|
|
30
30
|
git log --oneline -1 # latest commit
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
2. Determine the base (where this branch diverged from the branch it merges into). The target is the first of these that resolves:
|
|
33
|
+
2. Determine the base (where this branch diverged from the branch it merges into). The target is the first of these that resolves (`gh pr view` asks GitHub, with your own gh login, for the open PR's target branch):
|
|
34
34
|
```bash
|
|
35
35
|
TARGET=$(gh pr view --json baseRefName --jq '"origin/" + .baseRefName' 2>/dev/null) # the open PR's target branch
|
|
36
36
|
[ -n "$TARGET" ] || TARGET=$(git symbolic-ref -q --short refs/remotes/origin/HEAD) # the default branch, set by git clone
|
package/dist/auth.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export interface TokenResponse {
|
|
|
25
25
|
github_login?: string;
|
|
26
26
|
tenant_name?: string;
|
|
27
27
|
repos?: string[];
|
|
28
|
+
project_memory?: Array<{
|
|
29
|
+
repo: string;
|
|
30
|
+
revision: string | null;
|
|
31
|
+
}>;
|
|
32
|
+
project_memory_status?: "unavailable";
|
|
28
33
|
provisional?: boolean;
|
|
29
34
|
install_url?: string;
|
|
30
35
|
}
|
package/dist/credentials.d.ts
CHANGED
|
@@ -21,6 +21,14 @@ export interface Credentials {
|
|
|
21
21
|
github_login: string | null;
|
|
22
22
|
tenant_name: string | null;
|
|
23
23
|
repos: string[];
|
|
24
|
+
/** Device sign-in hint only; the server authorizes every memory operation. */
|
|
25
|
+
project_memory?: Array<{
|
|
26
|
+
repo: string;
|
|
27
|
+
revision: string | null;
|
|
28
|
+
}>;
|
|
29
|
+
project_memory_status?: "unavailable";
|
|
30
|
+
/** Local readiness binding, stable only across refreshes of this sign-in. */
|
|
31
|
+
project_memory_session?: string;
|
|
24
32
|
}
|
|
25
33
|
/** Default credential file path (route-grep-hook.ts checks the same file to know Pharaoh is signed in). */
|
|
26
34
|
export declare const CREDENTIALS_PATH: string;
|
package/dist/gateway.js
CHANGED
|
@@ -105,7 +105,7 @@ export function createGateway(opts) {
|
|
|
105
105
|
*/
|
|
106
106
|
function persistAuthorizedToken(token) {
|
|
107
107
|
const sseUrl = resolveSseUrl(token.sse_url, opts.server);
|
|
108
|
-
const creds = tokenToCredentials(token, sseUrl);
|
|
108
|
+
const creds = tokenToCredentials(token, sseUrl, opts.server);
|
|
109
109
|
writeCredentials(creds, opts.credentialsPath);
|
|
110
110
|
lastCreds = creds;
|
|
111
111
|
pending = null;
|
package/dist/helpers.d.ts
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure helper functions for the mcp-proxy CLI — no side effects on import.
|
|
3
|
-
* Separated from index.ts so tests can import without triggering main().
|
|
4
|
-
*/
|
|
5
1
|
import type { TokenResponse } from "./auth.js";
|
|
6
2
|
import type { Credentials } from "./credentials.js";
|
|
7
3
|
/** The npx command users run to set up Pharaoh. Single source of truth for all output. */
|
|
8
4
|
export declare const NPX_COMMAND = "npx @pharaoh-so/mcp";
|
|
9
5
|
/** Write one or more lines to stderr. */
|
|
10
6
|
export declare function printLines(...lines: string[]): void;
|
|
7
|
+
/**
|
|
8
|
+
* Ask a yes/no question that defaults to yes: Enter, `y` or `yes` is yes; `n`
|
|
9
|
+
* or `no` is no; anything else asks again. Ctrl-C exits (130). On a terminal,
|
|
10
|
+
* keys pressed before the question showed (an Enter while sign-in waited) are
|
|
11
|
+
* dropped, so only a key pressed after reading it answers.
|
|
12
|
+
*
|
|
13
|
+
* @param question - The prompt, ending in something like `[Y/n] `.
|
|
14
|
+
* @param input - Where the answer is read (defaults to stdin).
|
|
15
|
+
* @param output - Where the prompt is written (defaults to stderr, like every other line).
|
|
16
|
+
* @returns The answer, or null when input ended before one.
|
|
17
|
+
*/
|
|
18
|
+
export declare function askYesNo(question: string, input?: NodeJS.ReadableStream & {
|
|
19
|
+
isTTY?: boolean;
|
|
20
|
+
}, output?: NodeJS.WritableStream): Promise<boolean | null>;
|
|
11
21
|
/** Parse CLI arguments. */
|
|
12
22
|
export declare function parseArgs(argv?: string[]): {
|
|
13
23
|
server: string;
|
|
@@ -28,7 +38,7 @@ export declare function resolveSseUrl(tokenSseUrl: string | undefined, server: s
|
|
|
28
38
|
* will eventually hit the same silent-expiry cliff we're trying to eliminate.
|
|
29
39
|
* Fail loud so the server regression surfaces instead of being absorbed.
|
|
30
40
|
*/
|
|
31
|
-
export declare function tokenToCredentials(token: TokenResponse, sseUrl: string): Credentials;
|
|
41
|
+
export declare function tokenToCredentials(token: TokenResponse, sseUrl: string, server?: string): Credentials;
|
|
32
42
|
/** Format remaining TTL as human-readable string (e.g. "5d 12h"). */
|
|
33
43
|
export declare function formatTtl(expiresAt: string): string;
|
|
34
44
|
/** Format a credential identity string (e.g. "alice (my-org)"). */
|
package/dist/helpers.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper functions for the mcp-proxy CLI — no side effects on import.
|
|
3
|
+
* Separated from index.ts so tests can import without triggering main().
|
|
4
|
+
*/
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
1
7
|
import { isExpired } from "./credentials.js";
|
|
2
8
|
const DEFAULT_SERVER = "https://mcp.pharaoh.so";
|
|
3
9
|
/** The npx command users run to set up Pharaoh. Single source of truth for all output. */
|
|
@@ -6,6 +12,54 @@ export const NPX_COMMAND = "npx @pharaoh-so/mcp";
|
|
|
6
12
|
export function printLines(...lines) {
|
|
7
13
|
process.stderr.write(`${lines.join("\n")}\n`);
|
|
8
14
|
}
|
|
15
|
+
/** How long to read and drop input that was typed before a question showed. */
|
|
16
|
+
const TYPE_AHEAD_MS = 50;
|
|
17
|
+
/**
|
|
18
|
+
* Ask a yes/no question that defaults to yes: Enter, `y` or `yes` is yes; `n`
|
|
19
|
+
* or `no` is no; anything else asks again. Ctrl-C exits (130). On a terminal,
|
|
20
|
+
* keys pressed before the question showed (an Enter while sign-in waited) are
|
|
21
|
+
* dropped, so only a key pressed after reading it answers.
|
|
22
|
+
*
|
|
23
|
+
* @param question - The prompt, ending in something like `[Y/n] `.
|
|
24
|
+
* @param input - Where the answer is read (defaults to stdin).
|
|
25
|
+
* @param output - Where the prompt is written (defaults to stderr, like every other line).
|
|
26
|
+
* @returns The answer, or null when input ended before one.
|
|
27
|
+
*/
|
|
28
|
+
export async function askYesNo(question, input = process.stdin, output = process.stderr) {
|
|
29
|
+
if (input.isTTY)
|
|
30
|
+
await dropTypedAhead(input);
|
|
31
|
+
const rl = createInterface({ input, output });
|
|
32
|
+
rl.on("SIGINT", () => {
|
|
33
|
+
output.write("\n");
|
|
34
|
+
process.exit(130);
|
|
35
|
+
});
|
|
36
|
+
rl.setPrompt(question);
|
|
37
|
+
rl.prompt();
|
|
38
|
+
try {
|
|
39
|
+
// The iterator ends on EOF; rl.question never settles then.
|
|
40
|
+
for await (const reply of rl) {
|
|
41
|
+
const answer = reply.trim().toLowerCase();
|
|
42
|
+
if (answer === "" || answer === "y" || answer === "yes")
|
|
43
|
+
return true;
|
|
44
|
+
if (answer === "n" || answer === "no")
|
|
45
|
+
return false;
|
|
46
|
+
rl.prompt();
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
rl.close();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Read and drop the input already waiting, in raw mode so a half-typed line goes too. */
|
|
55
|
+
async function dropTypedAhead(input) {
|
|
56
|
+
input.setRawMode?.(true);
|
|
57
|
+
const drop = () => { };
|
|
58
|
+
input.on("data", drop);
|
|
59
|
+
await new Promise((resolve) => setTimeout(resolve, TYPE_AHEAD_MS));
|
|
60
|
+
input.off("data", drop);
|
|
61
|
+
input.pause();
|
|
62
|
+
}
|
|
9
63
|
/** Parse CLI arguments. */
|
|
10
64
|
export function parseArgs(argv = process.argv.slice(2)) {
|
|
11
65
|
let server = DEFAULT_SERVER;
|
|
@@ -42,7 +96,7 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
42
96
|
return { server, logout };
|
|
43
97
|
}
|
|
44
98
|
export function printUsage() {
|
|
45
|
-
printLines("Usage: pharaoh-mcp [options]", "", "Options:", " --server <url> Pharaoh server URL (default: https://mcp.pharaoh.so)", " --logout Clear credentials, MCP config
|
|
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}`, "");
|
|
46
100
|
}
|
|
47
101
|
/**
|
|
48
102
|
* Validate that a server-supplied SSE URL shares the same origin as the configured server.
|
|
@@ -74,12 +128,20 @@ export function resolveSseUrl(tokenSseUrl, server) {
|
|
|
74
128
|
* will eventually hit the same silent-expiry cliff we're trying to eliminate.
|
|
75
129
|
* Fail loud so the server regression surfaces instead of being absorbed.
|
|
76
130
|
*/
|
|
77
|
-
export function tokenToCredentials(token, sseUrl) {
|
|
131
|
+
export function tokenToCredentials(token, sseUrl, server) {
|
|
78
132
|
if (!token.refresh_token || !token.refresh_token_expires_in) {
|
|
79
133
|
throw new Error("Pharaoh server response missing refresh_token — server is older than this client. " +
|
|
80
134
|
"Update the server to a version that issues refresh tokens, or downgrade the client.");
|
|
81
135
|
}
|
|
82
136
|
const now = Date.now();
|
|
137
|
+
const production = server === DEFAULT_SERVER && new URL(sseUrl).origin === DEFAULT_SERVER;
|
|
138
|
+
const memory = production && Array.isArray(token.project_memory)
|
|
139
|
+
? token.project_memory.filter((entry) => typeof entry?.repo === "string" &&
|
|
140
|
+
/^[a-z0-9][a-z0-9-]{0,38}\/[a-z0-9_.-]{1,100}$/i.test(entry.repo) &&
|
|
141
|
+
(entry.revision === null ||
|
|
142
|
+
(typeof entry.revision === "string" &&
|
|
143
|
+
/^(?!0{40}$)[a-f0-9]{40}$/.test(entry.revision))))
|
|
144
|
+
: [];
|
|
83
145
|
return {
|
|
84
146
|
version: 2,
|
|
85
147
|
access_token: token.access_token,
|
|
@@ -91,6 +153,13 @@ export function tokenToCredentials(token, sseUrl) {
|
|
|
91
153
|
github_login: token.github_login ?? null,
|
|
92
154
|
tenant_name: token.tenant_name ?? null,
|
|
93
155
|
repos: token.repos ?? [],
|
|
156
|
+
...(memory.length ? { project_memory: memory } : {}),
|
|
157
|
+
...(memory.some((entry) => entry.revision === null)
|
|
158
|
+
? { project_memory_session: randomUUID() }
|
|
159
|
+
: {}),
|
|
160
|
+
...(production && token.project_memory_status === "unavailable"
|
|
161
|
+
? { project_memory_status: "unavailable" }
|
|
162
|
+
: {}),
|
|
94
163
|
};
|
|
95
164
|
}
|
|
96
165
|
/** Format remaining TTL as human-readable string (e.g. "5d 12h"). */
|
package/dist/index.js
CHANGED
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
* mode until device flow completes, then swap to SSE in
|
|
13
13
|
* place — the stdio session never disconnects.
|
|
14
14
|
*/
|
|
15
|
+
import { syncMemoryHooks } from "../project-memory/setup.mjs";
|
|
15
16
|
import { pollForToken, printActivationPrompt, printAuthSuccess, requestDeviceCode, } from "./auth.js";
|
|
16
17
|
import { AuthRequiredBackend, createSseBackend } from "./backend.js";
|
|
17
18
|
import { deleteCredentials, readCredentials, writeCredentials, } from "./credentials.js";
|
|
18
19
|
import { createGateway } from "./gateway.js";
|
|
19
|
-
import { formatTtl, parseArgs, printLines, printUsage, resolveSseUrl, tokenToCredentials, } from "./helpers.js";
|
|
20
|
-
import { allowReadTools, describeAllowReadTools, formatKeptCommands, runInstallSkills, } from "./install-skills.js";
|
|
20
|
+
import { askYesNo, formatTtl, parseArgs, printLines, printUsage, resolveSseUrl, tokenToCredentials, } from "./helpers.js";
|
|
21
|
+
import { allowReadTools, CLAUDE_MD_QUESTION, describeAllowReadTools, describeClaudeMdOutcome, formatKeptCommands, offerClaudeMdLine, runInstallSkills, } from "./install-skills.js";
|
|
21
22
|
import { handleRefreshFailure, tryMidSessionRefresh } from "./lifecycle.js";
|
|
22
23
|
import { TenantSuspendedError, TokenExpiredError } from "./proxy.js";
|
|
23
24
|
import { startInAuthRequiredMode, startInSseMode } from "./proxy-server.js";
|
|
@@ -31,6 +32,16 @@ const BANNER = [
|
|
|
31
32
|
" |_| |_||_/_/ \\_\\_|_\\/_/ \\_\\\\___/|_||_|",
|
|
32
33
|
"",
|
|
33
34
|
].join("\n");
|
|
35
|
+
function reportMemorySetup(options) {
|
|
36
|
+
try {
|
|
37
|
+
const notices = syncMemoryHooks(options);
|
|
38
|
+
if (notices.length)
|
|
39
|
+
printLines(...notices);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
printLines("Pharaoh: project memory setup could not update local activation.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
34
45
|
async function main() {
|
|
35
46
|
const args = process.argv.slice(2);
|
|
36
47
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -57,21 +68,26 @@ async function main() {
|
|
|
57
68
|
// Clean mode — remove all pharaoh traces from Claude Code config (MCP entries,
|
|
58
69
|
// disabled states, project-scoped configs). Useful for resetting broken installs.
|
|
59
70
|
if (args.includes("--clean")) {
|
|
60
|
-
const { cleanSlate } = await import("./setup.js");
|
|
71
|
+
const { cleanCodexSetup, cleanSlate } = await import("./setup.js");
|
|
61
72
|
const { uninstallClaudePlugin } = await import("./install-skills.js");
|
|
62
73
|
cleanSlate();
|
|
63
74
|
uninstallClaudePlugin();
|
|
64
|
-
|
|
75
|
+
cleanCodexSetup();
|
|
76
|
+
reportMemorySetup({ removeLegacy: true });
|
|
77
|
+
printLines("Pharaoh: local setup cleanup finished.");
|
|
65
78
|
process.exit(0);
|
|
66
79
|
}
|
|
67
80
|
const { server, logout } = parseArgs(args);
|
|
68
81
|
if (logout) {
|
|
69
|
-
const { cleanSlate } = await import("./setup.js");
|
|
82
|
+
const { cleanCodexSetup, cleanSlate } = await import("./setup.js");
|
|
70
83
|
const { uninstallClaudePlugin } = await import("./install-skills.js");
|
|
84
|
+
// Credentials first: a cleanup step that fails must never leave the tokens behind.
|
|
85
|
+
deleteCredentials();
|
|
71
86
|
cleanSlate();
|
|
72
87
|
uninstallClaudePlugin();
|
|
73
|
-
|
|
74
|
-
|
|
88
|
+
cleanCodexSetup();
|
|
89
|
+
reportMemorySetup({ removeLegacy: true });
|
|
90
|
+
printLines("Pharaoh: credentials cleared and local setup cleanup finished.");
|
|
75
91
|
process.exit(0);
|
|
76
92
|
}
|
|
77
93
|
const isInteractive = Boolean(process.stdin.isTTY);
|
|
@@ -80,10 +96,10 @@ async function main() {
|
|
|
80
96
|
// Running `npx @pharaoh-so/mcp` is the only command a user needs.
|
|
81
97
|
if (isInteractive) {
|
|
82
98
|
process.stderr.write(`${BANNER}\n`);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
if (!
|
|
99
|
+
const { claudeCliMissing, hasClaude, hasCodex, runCodexSetup, runSetup } = await import("./setup.js");
|
|
100
|
+
const claude = hasClaude();
|
|
101
|
+
const codex = hasCodex();
|
|
102
|
+
if (!claude && !codex) {
|
|
87
103
|
printLines(...claudeCliMissing());
|
|
88
104
|
process.exit(1);
|
|
89
105
|
}
|
|
@@ -96,23 +112,32 @@ async function main() {
|
|
|
96
112
|
printLines(`Pharaoh: provisional access — install the GitHub App to map your repos: ${token.install_url ?? ""}`);
|
|
97
113
|
}
|
|
98
114
|
const sseUrl = resolveSseUrl(token.sse_url, server);
|
|
99
|
-
const newCreds = tokenToCredentials(token, sseUrl);
|
|
115
|
+
const newCreds = tokenToCredentials(token, sseUrl, server);
|
|
100
116
|
writeCredentials(newCreds);
|
|
101
117
|
printAuthSuccess(token.github_login ?? null, token.tenant_name ?? null, token.repos?.length ?? 0);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
const clients = [];
|
|
119
|
+
if (claude && runSetup({ silent: true }))
|
|
120
|
+
clients.push("claude-code");
|
|
121
|
+
// Existing Claude-only installs stay unchanged for nonpilot accounts.
|
|
122
|
+
if (codex && (!claude || newCreds.project_memory?.length)) {
|
|
123
|
+
if (runCodexSetup())
|
|
124
|
+
clients.push("codex");
|
|
125
|
+
}
|
|
126
|
+
if (!clients.length)
|
|
105
127
|
process.exit(1);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
if (clients.includes("claude-code")) {
|
|
129
|
+
process.stderr.write(" Setting up...");
|
|
130
|
+
const { keptCommands } = runInstallSkills(undefined, { silent: true });
|
|
131
|
+
process.stderr.write(" done.\n");
|
|
132
|
+
printLines(describeAllowReadTools(allowReadTools()));
|
|
133
|
+
const kept = formatKeptCommands(keptCommands);
|
|
134
|
+
if (kept)
|
|
135
|
+
printLines(kept);
|
|
136
|
+
const claudeMd = describeClaudeMdOutcome(await offerClaudeMdLine(() => askYesNo(CLAUDE_MD_QUESTION), Boolean(process.stdin.isTTY && process.stderr.isTTY)));
|
|
137
|
+
if (claudeMd)
|
|
138
|
+
printLines(claudeMd);
|
|
139
|
+
}
|
|
140
|
+
reportMemorySetup({ credentials: newCreds, clients });
|
|
116
141
|
// The registration is pinned to this version; upgrading is a deliberate
|
|
117
142
|
// re-run. Interactive only, and quiet when the registry can't be reached.
|
|
118
143
|
const latest = await fetchLatestVersion();
|
|
@@ -120,7 +145,7 @@ async function main() {
|
|
|
120
145
|
if (latest && isNewerVersion(latest, running)) {
|
|
121
146
|
printLines(`Pharaoh: ${latest} is available (this is ${running}). Upgrade: ${UPGRADE_COMMAND}`);
|
|
122
147
|
}
|
|
123
|
-
printLines("", "Pharaoh is ready. Try these in a new
|
|
148
|
+
printLines("", "Pharaoh is ready. Try these in a new conversation:", "", ' "What modules does this codebase have?"', ' "Search for functions related to authentication"', ' "What breaks if I change this file?"', "");
|
|
124
149
|
process.exit(0);
|
|
125
150
|
}
|
|
126
151
|
// ── Proxy mode (Claude Code spawned us as a stdio MCP server) ──
|
package/dist/install-skills.d.ts
CHANGED
|
@@ -7,8 +7,9 @@ import { type ClaudeCli } from "./setup.js";
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function claudePluginState(home?: string): "enabled" | "disabled" | "absent";
|
|
9
9
|
/**
|
|
10
|
-
* Uninstall Pharaoh's Claude Code plugin
|
|
11
|
-
* marketplace directory and any pre-marketplace install
|
|
10
|
+
* Uninstall Pharaoh's Claude Code plugin (through the CLI, then the local
|
|
11
|
+
* marketplace directory and any pre-marketplace install) and its block in
|
|
12
|
+
* ~/.claude/CLAUDE.md. Run by --clean and --logout.
|
|
12
13
|
*
|
|
13
14
|
* @param home - Home directory override (defaults to os.homedir()).
|
|
14
15
|
* @param claude - `claude` CLI runner (defaults to the real CLI for `home`).
|
|
@@ -55,10 +56,11 @@ export declare function installSkills(home?: string): number;
|
|
|
55
56
|
export declare function mergeOpenClawConfig(home?: string): boolean;
|
|
56
57
|
/**
|
|
57
58
|
* Rules the interactive setup adds to `permissions.allow` in
|
|
58
|
-
* ~/.claude/settings.json: the
|
|
59
|
+
* ~/.claude/settings.json: the seven read-only tools (`readOnlyHint: true`),
|
|
59
60
|
* each by full name. Never a prefix rule — `mcp__pharaoh` also auto-approves
|
|
60
|
-
*
|
|
61
|
-
*
|
|
61
|
+
* the tools that change things (`pharaoh_manage_repo`, which queues a re-map,
|
|
62
|
+
* and the superadmin tools), so a prompt injection could run them with
|
|
63
|
+
* no human in the loop (security review SC-6). Those tools keep prompting.
|
|
62
64
|
*/
|
|
63
65
|
export declare const PHARAOH_ALLOW_RULES: readonly string[];
|
|
64
66
|
/** What `allowReadTools` did to ~/.claude/settings.json. */
|
|
@@ -83,6 +85,29 @@ export declare function allowReadTools(home?: string): AllowReadToolsResult;
|
|
|
83
85
|
* @param result - What `allowReadTools` returned.
|
|
84
86
|
*/
|
|
85
87
|
export declare function describeAllowReadTools(result: AllowReadToolsResult): string;
|
|
88
|
+
/** What the interactive setup asks before writing the line. */
|
|
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 `offerClaudeMdLine` did; `describeClaudeMdOutcome` words it. */
|
|
91
|
+
export type ClaudeMdOutcome = "added" | "updated" | "declined" | "kept" | "failed" | "current" | "answered" | "cancelled" | "no-terminal" | "no-claude";
|
|
92
|
+
/**
|
|
93
|
+
* Offer the Pharaoh-first line in ~/.claude/CLAUDE.md. Interactive setup only,
|
|
94
|
+
* never a proxy start. Asks only when the block is absent and the user hasn't
|
|
95
|
+
* answered before; a stale block (an earlier yes) is updated without asking.
|
|
96
|
+
* The line is appended inside markers, and nothing else in the file changes.
|
|
97
|
+
*
|
|
98
|
+
* @param ask - Asks the user: true for yes, false for no, null when input ended.
|
|
99
|
+
* @param isTerminal - Whether stdin and stderr are a terminal; false asks and writes nothing.
|
|
100
|
+
* @param home - Home directory override (defaults to os.homedir()).
|
|
101
|
+
* @returns What happened, for `describeClaudeMdOutcome`.
|
|
102
|
+
*/
|
|
103
|
+
export declare function offerClaudeMdLine(ask: () => Promise<boolean | null>, isTerminal: boolean, home?: string): Promise<ClaudeMdOutcome>;
|
|
104
|
+
/**
|
|
105
|
+
* The line the interactive setup prints for an `offerClaudeMdLine` outcome, or
|
|
106
|
+
* null when there is nothing to say (already current, already answered, no terminal).
|
|
107
|
+
*
|
|
108
|
+
* @param outcome - What `offerClaudeMdLine` returned.
|
|
109
|
+
*/
|
|
110
|
+
export declare function describeClaudeMdOutcome(outcome: ClaudeMdOutcome): string | null;
|
|
86
111
|
/** What `runInstallSkills` wants the caller to tell the user. */
|
|
87
112
|
export interface InstallResult {
|
|
88
113
|
/** Files in ~/.claude/commands/ left as they were because they are not Pharaoh's. */
|