@pharaoh-so/mcp 0.3.45 → 0.3.49

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 CHANGED
@@ -2,7 +2,9 @@
2
2
  "mcpServers": {
3
3
  "pharaoh": {
4
4
  "command": "npx",
5
- "args": ["@pharaoh-so/mcp"]
5
+ "args": [
6
+ "@pharaoh-so/mcp@0.3.49"
7
+ ]
6
8
  }
7
9
  }
8
10
  }
package/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ 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
+ ## [0.3.49] - 2026-10-20
9
+
10
+ ### Changed
11
+ - Publish from the standalone public source repository with npm provenance, after the reviewed source passes its required checks.
12
+ - Include the test runtime needed to validate the package independently.
13
+
14
+ ## [0.3.48] - 2026-10-20
15
+
16
+ ### Added
17
+ - Compact Trace views for supporting MCP App clients, with expandable evidence, yellow bars and green success markers. Existing text results remain available.
18
+ - The same static Trace resource before and after sign-in. Resource reads do not start device authentication, and view data comes only from authorized tool results.
19
+
20
+ ## [Unreleased]
21
+
22
+ Security review of 2026-10-17 (findings SC-2, SC-6, SC-7, SC-8, SC-9). See README § Security for what the installer writes.
23
+
24
+ ### Changed
25
+ - The interactive setup allow-lists Pharaoh's six read-only tools and `pharaoh_account` by full name in `~/.claude/settings.json`, only when that file already exists, and says so. It replaces the blanket `mcp__pharaoh` rule earlier versions wrote on every start, which also auto-approved account deletion and `pharaoh_admin`. Proxy starts no longer touch permissions.
26
+ - Slash command files in `~/.claude/commands/` are written only when absent or Pharaoh's own (front-matter marker `# managed-by: pharaoh-mcp`, or the header and `$ARGUMENTS` footer of an earlier install). A user's file with the same name is kept and named in the output; symlinks are never followed.
27
+ - `~/.pharaoh` is created `0700` and set to `0700` on every start, whichever path creates it first.
28
+ - Every registration (`claude mcp add`, OpenClaw, the shipped `.mcp.json`) runs `npx @pharaoh-so/mcp@<the installed version>`. Upgrading is `npx @pharaoh-so/mcp@latest`; the interactive setup prints a hint when the registry has a newer version.
29
+ - `package.json` `repository` names the building repository. The release workflow requests npm provenance when the source repository is public; private-source releases publish without it.
30
+
8
31
  ## [0.1.5] - 2026-03-24
9
32
 
10
33
  ### Added
package/README.md CHANGED
@@ -129,10 +129,10 @@ npx @pharaoh-so/mcp --logout
129
129
 
130
130
  ### Custom Server
131
131
 
132
- For self-hosted Pharaoh instances or development, register manually:
132
+ For self-hosted Pharaoh instances or development, register manually, pinned to a version (`npx @pharaoh-so/mcp --version` prints the one you have):
133
133
 
134
134
  ```bash
135
- claude mcp add --scope user pharaoh -- npx @pharaoh-so/mcp --server https://your-pharaoh-instance.com
135
+ claude mcp add --scope user pharaoh -- npx @pharaoh-so/mcp@<version> --server https://your-pharaoh-instance.com
136
136
  ```
137
137
 
138
138
  ### Environment Variables
@@ -155,6 +155,33 @@ claude mcp add --scope user pharaoh -- npx @pharaoh-so/mcp --server https://your
155
155
  - The proxy never reads or sends your local files. 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
156
  - Tokens expire after 7 days and are refreshed automatically
157
157
  - Only use trusted server URLs — the proxy sends your auth token to the configured server
158
+ - No telemetry. The proxy's only network calls are the device flow, token refresh, the MCP connection, and one registry lookup during interactive setup (below).
159
+
160
+ ### What the installer writes on your machine
161
+
162
+ `npx @pharaoh-so/mcp` in a terminal is the interactive setup. Every later proxy start (Claude Code launching the server) repeats the plugin, skill and command steps so a new version takes effect, and nothing else.
163
+
164
+ | Path | Mode | When | What |
165
+ |------|------|------|------|
166
+ | `~/.pharaoh/` | `0700`, set again on every start | Always | Credentials and the Claude Code plugin marketplace |
167
+ | `~/.pharaoh/credentials.json` | `0600` | After sign-in | Access token, refresh token, expiry |
168
+ | `~/.pharaoh/claude-marketplace/` | — | `~/.claude/` exists | The one-plugin marketplace Claude Code installs as `pharaoh@pharaoh`: the skills and the Grep routing hook. Rewritten when the package version changes |
169
+ | `~/.claude/commands/plan.md`, `review.md`, `sessions.md` | — | `~/.claude/` exists | Slash commands. A file is written only when it is absent or is Pharaoh's: it carries the front-matter comment `# managed-by: pharaoh-mcp`, or has the exact header and `$ARGUMENTS` footer of a Pharaoh command from 0.3.46 or earlier. Any other file with that name is yours: it is kept, a symlink is never followed, and the setup output names it. Rename your file to keep both |
170
+ | `~/.claude.json` | — | Interactive setup | Removes earlier `pharaoh` entries, then `claude mcp add --scope user pharaoh -- npx @pharaoh-so/mcp@<version>` |
171
+ | `~/.claude/settings.json` | — | Interactive setup, and only if the file already exists | Adds to `permissions.allow`, each by full name: `mcp__pharaoh__get_codebase_map`, `mcp__pharaoh__get_module_context`, `mcp__pharaoh__search_functions`, `mcp__pharaoh__get_blast_radius`, `mcp__pharaoh__get_unused_code`, `mcp__pharaoh__pharaoh_recon`, `mcp__pharaoh__pharaoh_account`. The setup prints a line saying what it did. The proxy never creates this file and never touches it on a start |
172
+ | `~/.openclaw/skills/`, `~/.openclaw/openclaw.json` | — | `~/.openclaw/` exists | Skills prefixed `pharaoh-`, and an `mcpServers.pharaoh` entry running `npx @pharaoh-so/mcp@<version>`. An entry that is not an `npx @pharaoh-so/mcp` command is left alone |
173
+
174
+ Versions 0.3.46 and earlier wrote one rule, `mcp__pharaoh`, on every proxy start. That prefix auto-approved every Pharaoh tool, including account deletion and `pharaoh_admin`, so a prompt injection could run them without a human prompt. The next interactive setup replaces that rule with the list above; account deletion, `pharaoh_admin` and any tool not listed prompt before they run. To take the blanket rule out by hand, delete `mcp__pharaoh` from `permissions.allow`.
175
+
176
+ ### Pinned version and upgrades
177
+
178
+ Every registration the setup writes runs the exact version that wrote it (`npx @pharaoh-so/mcp@<version>`). A new release on npm changes nothing on your machine until you upgrade:
179
+
180
+ ```bash
181
+ npx @pharaoh-so/mcp@latest
182
+ ```
183
+
184
+ This signs you in again, rewrites the registration with the new version and refreshes the plugin. The interactive setup asks the npm registry for the newest version and prints a one-line hint when there is one; proxy starts never check.
158
185
 
159
186
  ### Reporting Vulnerabilities
160
187
 
@@ -33,12 +33,13 @@ Before merging any branch. Before opening any PR. When reviewing changes that to
33
33
  [ -n "$TARGET" ] || for b in origin/main origin/master main master; do
34
34
  git rev-parse -q --verify "$b" >/dev/null && TARGET=$b && break
35
35
  done
36
- git merge-base HEAD "$TARGET" # prints the base commit: $BASE below
36
+ BASE=$(git merge-base HEAD "$TARGET") && echo "base: $BASE"
37
37
  ```
38
- If no base comes out, stop and ask which branch this merges into. An empty base makes every diff below come out empty, and the review would report no changes.
38
+ If no base comes out, stop and ask which branch this merges into. An empty base makes every diff below come out empty, and the review would report no changes. If your shell doesn't keep variables between commands, set `BASE` to the printed commit at the start of each one.
39
39
 
40
40
  3. Collect the full changeset from the base:
41
41
  ```bash
42
+ : "${BASE:?no base: ask which branch this merges into}" # stops here rather than diff HEAD against itself
42
43
  git diff --name-only "$BASE"...HEAD # all changed files
43
44
  git diff --stat "$BASE"...HEAD # summary stats
44
45
  git log --oneline "$BASE"..HEAD # all commits on this branch
@@ -256,7 +257,7 @@ If none of these triggers are met, **skip Phase 3** and proceed to Phase 4.
256
257
  ### Auto-block triggers (any of these = BLOCK)
257
258
 
258
259
  - Any CRITICAL security finding (injection, broken access control, tenant isolation violation, secret exposure)
259
- - Unreachable exports in an application (new public code that nothing calls). In a library, exports are the public API: check each new one is documented or tested instead. Without reachability data (no Pharaoh, no PR Guard), raise it under SHIP WITH CHANGES, not BLOCK
260
+ - Unreachable exports in an application (new public code that nothing calls). In a library, exports are the public API: check each new one is documented or tested instead. Without graph reachability data, a search can't prove nothing calls it: raise it under SHIP WITH CHANGES, not BLOCK
260
261
  - New circular dependencies between modules
261
262
  - HIGH regression risk without corresponding test coverage
262
263
  - **A MUST-bullet in `docs/VISION.md` is violated by this change** (without VISION.md being updated in the same PR with user approval)
@@ -275,7 +276,7 @@ If none of these triggers are met, **skip Phase 3** and proceed to Phase 4.
275
276
  ### SHIP triggers
276
277
 
277
278
  - No CRITICAL or confirmed IMPORTANT findings
278
- - All new exports are reachable (applications) or documented and tested (libraries)
279
+ - All new exports are reachable (applications) or documented or tested (libraries)
279
280
  - Regression risk is LOW or MEDIUM with adequate test coverage
280
281
  - Spec alignment is clean or intentionally divergent with documentation
281
282
 
package/dist/backend.d.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  * resolves each tool call's handler at request-arrival time, so in-flight
16
16
  * calls complete against whichever backend started them.
17
17
  */
18
- import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
18
+ import type { CallToolResult, ClientCapabilities, Tool } from "@modelcontextprotocol/sdk/types.js";
19
19
  import type { DeviceFlowGateway } from "./gateway.js";
20
20
  /** The contract ProxyServer uses to delegate tool handling. */
21
21
  export interface Backend {
@@ -32,6 +32,8 @@ export interface Backend {
32
32
  * act on. Backends without a supervisor loop (AuthRequiredBackend) omit this.
33
33
  */
34
34
  waitForExit?(): Promise<void>;
35
+ /** Return an unstarted replacement only if the negotiated UI support changed. */
36
+ withClientCapabilities?(capabilities: ClientCapabilities): Backend;
35
37
  }
36
38
  /**
37
39
  * Pre-auth backend. Every tool call triggers a device-flow state step via the
@@ -75,7 +77,9 @@ export declare class SseProxyBackend implements Backend {
75
77
  private transport;
76
78
  private stopped;
77
79
  private reconnectPromise;
78
- constructor(sseUrl: string, token: string);
80
+ private readonly clientCapabilities;
81
+ constructor(sseUrl: string, token: string, capabilities?: ClientCapabilities);
82
+ withClientCapabilities(capabilities: ClientCapabilities): Backend;
79
83
  start(): Promise<void>;
80
84
  /**
81
85
  * Expose the long-running reconnect loop so a supervisor can await it.
package/dist/backend.js CHANGED
@@ -19,6 +19,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
19
19
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
20
20
  import { loadBundledCatalog } from "./catalog.js";
21
21
  import { classifySSEError, TenantSuspendedError, TokenExpiredError } from "./proxy.js";
22
+ import { TRACE_MIME_TYPE } from "./trace-resource.js";
22
23
  import { getPackageVersion } from "./version.js";
23
24
  /**
24
25
  * Pre-auth backend. Every tool call triggers a device-flow state step via the
@@ -143,9 +144,23 @@ export class SseProxyBackend {
143
144
  transport = null;
144
145
  stopped = false;
145
146
  reconnectPromise = null;
146
- constructor(sseUrl, token) {
147
+ clientCapabilities;
148
+ constructor(sseUrl, token, capabilities = {}) {
147
149
  this.sseUrl = sseUrl;
148
150
  this.token = token;
151
+ // The proxy can deliver only this static UI extension. Advertising other
152
+ // capabilities (sampling, elicitation, etc.) would promise unsupported RPCs.
153
+ const ui = capabilities.extensions?.["io.modelcontextprotocol/ui"];
154
+ this.clientCapabilities =
155
+ Array.isArray(ui?.mimeTypes) && ui.mimeTypes.includes(TRACE_MIME_TYPE)
156
+ ? { extensions: { "io.modelcontextprotocol/ui": { mimeTypes: [TRACE_MIME_TYPE] } } }
157
+ : {};
158
+ }
159
+ withClientCapabilities(capabilities) {
160
+ const next = new SseProxyBackend(this.sseUrl, this.token, capabilities);
161
+ return JSON.stringify(next.clientCapabilities) === JSON.stringify(this.clientCapabilities)
162
+ ? this
163
+ : next;
149
164
  }
150
165
  async start() {
151
166
  if (this.client)
@@ -169,9 +184,29 @@ export class SseProxyBackend {
169
184
  return this.reconnectPromise ?? Promise.resolve();
170
185
  }
171
186
  async connectOnce() {
172
- this.transport = createSSETransport(this.sseUrl, this.token);
173
- this.client = new Client({ name: "pharaoh-proxy", version: getPackageVersion() }, { capabilities: {} });
174
- await this.client.connect(this.transport);
187
+ const transport = createSSETransport(this.sseUrl, this.token);
188
+ const client = new Client({ name: "pharaoh-proxy", version: getPackageVersion() }, { capabilities: this.clientCapabilities });
189
+ this.transport = transport;
190
+ this.client = client;
191
+ try {
192
+ await client.connect(transport);
193
+ }
194
+ catch (error) {
195
+ // A failed warm capability handshake must not leave EventSource's
196
+ // automatic retries alive beside the previous backend.
197
+ await client.close().catch(() => {
198
+ process.stderr.write("Pharaoh: failed SSE connection cleanup\n");
199
+ });
200
+ if (this.transport === transport)
201
+ this.transport = null;
202
+ if (this.client === client)
203
+ this.client = null;
204
+ // Initial and negotiated connects need the same typed auth recovery
205
+ // as reconnects. Generic failures still surface to the caller.
206
+ if (error instanceof Error)
207
+ classifySSEError(error);
208
+ throw error;
209
+ }
175
210
  }
176
211
  /** Wait for the current transport to close, then decide what to do. */
177
212
  waitForClose() {
@@ -204,6 +239,8 @@ export class SseProxyBackend {
204
239
  const delay = BACKOFF_MS[attempt - 1];
205
240
  process.stderr.write(`Pharaoh: reconnecting in ${delay / 1000}s (attempt ${attempt}/${BACKOFF_MS.length})...\n`);
206
241
  await sleep(delay);
242
+ if (this.stopped)
243
+ return;
207
244
  await this.connectOnce();
208
245
  process.stderr.write("Pharaoh: reconnected\n");
209
246
  }
@@ -213,6 +250,8 @@ export class SseProxyBackend {
213
250
  while (!this.stopped) {
214
251
  try {
215
252
  await this.waitForClose();
253
+ if (this.stopped)
254
+ return;
216
255
  attempt++;
217
256
  if (attempt > BACKOFF_MS.length) {
218
257
  process.stderr.write("Pharaoh: max reconnect attempts reached — exiting\n");
@@ -46,6 +46,15 @@ export declare function writeCredentials(creds: Credentials, path?: string): voi
46
46
  * @throws If the lock stays held for LOCK_WAIT_MS, or a stale lock can't be removed.
47
47
  */
48
48
  export declare function acquireCredentialsLock(path?: string): Promise<() => void>;
49
+ /**
50
+ * Create `~/.pharaoh` with mode 0700 and set that mode again if it already
51
+ * exists. The one place the directory is created, whichever path gets there
52
+ * first (credentials or the Claude Code plugin marketplace), so it is never
53
+ * left world-readable (security review SC-8).
54
+ *
55
+ * @param dir - The directory (defaults to `~/.pharaoh`).
56
+ */
57
+ export declare function ensurePharaohHome(dir?: string): void;
49
58
  /** Delete the credential file. No-op if it doesn't exist. */
50
59
  export declare function deleteCredentials(path?: string): void;
51
60
  /**
@@ -6,7 +6,7 @@
6
6
  * refreshes take a lock (acquireCredentialsLock) and writes are atomic.
7
7
  */
8
8
  import { randomUUID } from "node:crypto";
9
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
9
+ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
10
10
  import { homedir } from "node:os";
11
11
  import { dirname, join } from "node:path";
12
12
  import { setTimeout as sleep } from "node:timers/promises";
@@ -147,12 +147,21 @@ function lockAgeMs(lockPath) {
147
147
  return 0; // released between our create attempt and this stat — retry immediately
148
148
  }
149
149
  }
150
- /** Create the credentials directory (0700) if it doesn't exist. */
150
+ /**
151
+ * Create `~/.pharaoh` with mode 0700 and set that mode again if it already
152
+ * exists. The one place the directory is created, whichever path gets there
153
+ * first (credentials or the Claude Code plugin marketplace), so it is never
154
+ * left world-readable (security review SC-8).
155
+ *
156
+ * @param dir - The directory (defaults to `~/.pharaoh`).
157
+ */
158
+ export function ensurePharaohHome(dir = join(homedir(), ".pharaoh")) {
159
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
160
+ chmodSync(dir, 0o700);
161
+ }
162
+ /** Create the directory holding a credentials file (0700). */
151
163
  function ensureDir(path) {
152
- const dir = dirname(path);
153
- if (!existsSync(dir)) {
154
- mkdirSync(dir, { recursive: true, mode: 0o700 });
155
- }
164
+ ensurePharaohHome(dirname(path));
156
165
  }
157
166
  /** Delete the credential file. No-op if it doesn't exist. */
158
167
  export function deleteCredentials(path = CREDENTIALS_PATH) {
package/dist/index.js CHANGED
@@ -17,11 +17,12 @@ import { AuthRequiredBackend, createSseBackend } from "./backend.js";
17
17
  import { deleteCredentials, readCredentials, writeCredentials, } from "./credentials.js";
18
18
  import { createGateway } from "./gateway.js";
19
19
  import { formatTtl, parseArgs, printLines, printUsage, resolveSseUrl, tokenToCredentials, } from "./helpers.js";
20
- import { runInstallSkills } from "./install-skills.js";
20
+ import { allowReadTools, describeAllowReadTools, formatKeptCommands, runInstallSkills, } from "./install-skills.js";
21
21
  import { handleRefreshFailure, tryMidSessionRefresh } from "./lifecycle.js";
22
22
  import { TenantSuspendedError, TokenExpiredError } from "./proxy.js";
23
23
  import { startInAuthRequiredMode, startInSseMode } from "./proxy-server.js";
24
24
  import { loadAndRefresh } from "./refresh.js";
25
+ import { fetchLatestVersion, getPackageVersion, isNewerVersion, UPGRADE_COMMAND, } from "./version.js";
25
26
  const BANNER = [
26
27
  "",
27
28
  " ___ _ _ _ ___ _ ___ _ _ ",
@@ -90,8 +91,22 @@ async function main() {
90
91
  process.stderr.write(" Setting up...");
91
92
  const { runSetup } = await import("./setup.js");
92
93
  runSetup({ silent: true });
93
- runInstallSkills(undefined, { silent: true });
94
+ const { keptCommands } = runInstallSkills(undefined, { silent: true });
94
95
  process.stderr.write(" done.\n");
96
+ // Two things the user must hear about: their settings.json (the only
97
+ // place this setup edits permissions, never a silent start) and any
98
+ // command file of theirs that was left alone.
99
+ printLines(describeAllowReadTools(allowReadTools()));
100
+ const kept = formatKeptCommands(keptCommands);
101
+ if (kept)
102
+ printLines(kept);
103
+ // The registration is pinned to this version; upgrading is a deliberate
104
+ // re-run. Interactive only, and quiet when the registry can't be reached.
105
+ const latest = await fetchLatestVersion();
106
+ const running = getPackageVersion();
107
+ if (latest && isNewerVersion(latest, running)) {
108
+ printLines(`Pharaoh: ${latest} is available (this is ${running}). Upgrade: ${UPGRADE_COMMAND}`);
109
+ }
95
110
  printLines("", "Pharaoh is ready. Try these in a new Claude Code conversation:", "", ' "What modules does this codebase have?"', ' "Search for functions related to authentication"', ' "What breaks if I change this file?"', "");
96
111
  process.exit(0);
97
112
  }
package/dist/inspect.js CHANGED
@@ -11,6 +11,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
12
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
13
13
  import { loadBundledCatalog } from "./catalog.js";
14
+ import { registerTraceResource } from "./trace-resource.js";
14
15
  import { getPackageVersion } from "./version.js";
15
16
  const INSPECT_MSG = "This is Pharaoh's inspection server for directory listings. " +
16
17
  "Connect to https://mcp.pharaoh.so for actual usage.";
@@ -29,5 +30,6 @@ export async function runInspect() {
29
30
  server.setRequestHandler(CallToolRequestSchema, async () => ({
30
31
  content: [{ type: "text", text: INSPECT_MSG }],
31
32
  }));
33
+ registerTraceResource(server);
32
34
  await server.connect(new StdioServerTransport());
33
35
  }
@@ -1,3 +1,4 @@
1
+ import { type ClaudeCli } from "./setup.js";
1
2
  /**
2
3
  * Detect whether OpenClaw is installed by checking for ~/.openclaw/.
3
4
  *
@@ -5,8 +6,6 @@
5
6
  * @returns True if ~/.openclaw/ exists.
6
7
  */
7
8
  export declare function detectOpenClaw(home?: string): boolean;
8
- /** Runs one `claude` CLI command; true on success. */
9
- type ClaudeCli = (args: string[]) => boolean;
10
9
  /**
11
10
  * Uninstall Pharaoh's Claude Code plugin: through the CLI, then the local
12
11
  * marketplace directory and any pre-marketplace install.
@@ -15,6 +14,22 @@ type ClaudeCli = (args: string[]) => boolean;
15
14
  * @param claude - `claude` CLI runner (defaults to the real CLI for `home`).
16
15
  */
17
16
  export declare function uninstallClaudePlugin(home?: string, claude?: ClaudeCli): void;
17
+ /**
18
+ * Marker the installer writes into the front-matter of every command file it
19
+ * installs, as a YAML comment Claude Code ignores. A file in
20
+ * `~/.claude/commands/` is rewritten only when it is Pharaoh's: it carries
21
+ * this marker, or the exact header and `$ARGUMENTS` footer every installer
22
+ * before the marker shipped. Any other file with the same name is the user's
23
+ * and is left alone (security review SC-7).
24
+ */
25
+ export declare const PHARAOH_COMMAND_MARKER = "# managed-by: pharaoh-mcp";
26
+ /**
27
+ * The one-line notice for command files the installer left alone, or null
28
+ * when there were none.
29
+ *
30
+ * @param kept - File names from `runInstallSkills().keptCommands`.
31
+ */
32
+ export declare function formatKeptCommands(kept: readonly string[]): string | null;
18
33
  /**
19
34
  * Install bundled skills to `~/.openclaw/skills/` with the same `pharaoh-`
20
35
  * prefix that Claude Code installs use. Keeps naming consistent across both
@@ -28,26 +43,64 @@ export declare function uninstallClaudePlugin(home?: string, claude?: ClaudeCli)
28
43
  */
29
44
  export declare function installSkills(home?: string): number;
30
45
  /**
31
- * Read ~/.openclaw/openclaw.json, add the Pharaoh MCP server under `mcpServers`,
32
- * and write it back. Creates the file if it does not exist. Does NOT overwrite
33
- * an existing `pharaoh` entry — skips if already present.
46
+ * Read ~/.openclaw/openclaw.json, set the Pharaoh MCP server under `mcpServers`
47
+ * to `npx @pharaoh-so/mcp@<this version>`, and write it back. Creates the file
48
+ * if it does not exist. An existing `pharaoh` entry is re-pinned when it is an
49
+ * `npx @pharaoh-so/mcp` command (extra arguments kept) and left alone when it
50
+ * is anything else.
34
51
  *
35
52
  * @param home - Home directory override (defaults to os.homedir()).
36
- * @returns True if the config was written/updated, false if pharaoh was already present.
53
+ * @returns True if the config was written, false if it was already current or is not ours.
37
54
  */
38
55
  export declare function mergeOpenClawConfig(home?: string): boolean;
39
56
  /**
40
- * Main entry point for --install-skills.
57
+ * Rules the interactive setup adds to `permissions.allow` in
58
+ * ~/.claude/settings.json: the six read-only tools and `pharaoh_account`,
59
+ * each by full name. Never a prefix rule — `mcp__pharaoh` also auto-approves
60
+ * account deletion and `pharaoh_admin`, so a prompt injection could run them
61
+ * with no human in the loop (security review SC-6). Those tools keep prompting.
62
+ */
63
+ export declare const PHARAOH_ALLOW_RULES: readonly string[];
64
+ /** What `allowReadTools` did to ~/.claude/settings.json. */
65
+ export type AllowReadToolsResult = "added" | "migrated" | "current" | "no-settings" | "error";
66
+ /**
67
+ * Add `PHARAOH_ALLOW_RULES` to `permissions.allow` in ~/.claude/settings.json,
68
+ * replacing the blanket `mcp__pharaoh` rule an earlier version wrote. Runs
69
+ * only from the interactive setup, which prints what happened; never on a
70
+ * proxy start. Never creates the file: with no settings.json the tools
71
+ * simply prompt on first use.
72
+ *
73
+ * @param home - Home directory override (defaults to os.homedir()).
74
+ * @returns "added" or "migrated" when the file was written, "current" when
75
+ * nothing was missing, "no-settings" when there is no file, "error" when
76
+ * the file is not valid JSON (left untouched).
77
+ */
78
+ export declare function allowReadTools(home?: string): AllowReadToolsResult;
79
+ /**
80
+ * The line the interactive setup prints for an `allowReadTools` result, so
81
+ * the user always learns what was done to their settings.
82
+ *
83
+ * @param result - What `allowReadTools` returned.
84
+ */
85
+ export declare function describeAllowReadTools(result: AllowReadToolsResult): string;
86
+ /** What `runInstallSkills` wants the caller to tell the user. */
87
+ export interface InstallResult {
88
+ /** Files in ~/.claude/commands/ left as they were because they are not Pharaoh's. */
89
+ keptCommands: string[];
90
+ }
91
+ /**
92
+ * Main entry point for --install-skills, also run on every proxy start.
41
93
  *
42
94
  * Detection priority: Claude Code > OpenClaw > manual instructions.
43
- * Installs to all detected platforms (a user may use both).
95
+ * Installs to all detected platforms (a user may use both). Never touches
96
+ * Claude Code permissions: that is `allowReadTools`, interactive setup only.
44
97
  *
45
98
  * @param home - Home directory override (defaults to os.homedir()).
46
99
  * @param options.silent - Print nothing (the interactive setup prints its own summary).
47
100
  * @param options.claude - `claude` CLI runner (defaults to the real CLI for `home`).
101
+ * @returns What was kept, for the caller's own summary.
48
102
  */
49
103
  export declare function runInstallSkills(home?: string, options?: {
50
104
  silent?: boolean;
51
105
  claude?: ClaudeCli;
52
- }): void;
53
- export {};
106
+ }): InstallResult;