@pharaoh-so/mcp 0.2.4 → 0.2.6

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pharaoh",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
4
4
  "description": "Codebase knowledge graph with 23 development workflow skills. Query architecture, dependencies, blast radius, and more instead of reading files one at a time.",
5
5
  "author": {
6
6
  "name": "Pharaoh",
package/dist/helpers.js CHANGED
@@ -40,7 +40,7 @@ export function parseArgs(argv = process.argv.slice(2)) {
40
40
  return { server, logout };
41
41
  }
42
42
  export function printUsage() {
43
- printLines("Usage: pharaoh-mcp [options]", "", "Options:", " --server <url> Pharaoh server URL (default: https://mcp.pharaoh.so)", " --logout Clear stored credentials and exit", " --install-skills Install Pharaoh skills to OpenClaw (~/.openclaw/skills/)", " --help, -h Show this help", "", "Add to Claude Code:", " claude mcp add pharaoh -- npx @pharaoh-so/mcp", "");
43
+ printLines("Usage: pharaoh-mcp [options]", "", "Options:", " --server <url> Pharaoh server URL (default: https://mcp.pharaoh.so)", " --logout Clear stored credentials and exit", " --install-skills Force reinstall Pharaoh skills (Claude Code + OpenClaw)", " --help, -h Show this help", "", "Add to Claude Code:", " claude mcp add pharaoh -- npx @pharaoh-so/mcp", "");
44
44
  }
45
45
  /**
46
46
  * Validate that a server-supplied SSE URL shares the same origin as the configured server.
package/dist/index.js CHANGED
@@ -10,10 +10,11 @@
10
10
  * - Interactive (TTY): authenticate, print setup instructions, exit.
11
11
  * - Proxy (pipe): require pre-existing credentials, bridge stdio ↔ SSE.
12
12
  */
13
- import { printActivationPrompt, printAuthSuccess, pollForToken, requestDeviceCode, } from "./auth.js";
13
+ import { pollForToken, printActivationPrompt, printAuthSuccess, requestDeviceCode, } from "./auth.js";
14
14
  import { deleteCredentials, isExpired, readCredentials, writeCredentials } from "./credentials.js";
15
15
  import { formatIdentity, formatTtl, parseArgs, printLines, printSetupInstructions, printUsage, resolveSseUrl, tokenToCredentials, } from "./helpers.js";
16
- import { TokenExpiredError, TenantSuspendedError, startProxy } from "./proxy.js";
16
+ import { runInstallSkills } from "./install-skills.js";
17
+ import { startProxy, TenantSuspendedError, TokenExpiredError } from "./proxy.js";
17
18
  async function main() {
18
19
  const args = process.argv.slice(2);
19
20
  if (args.includes("--help") || args.includes("-h")) {
@@ -46,6 +47,8 @@ async function main() {
46
47
  if (isInteractive) {
47
48
  if (creds && !isExpired(creds)) {
48
49
  printLines(`Pharaoh: authenticated as ${formatIdentity(creds)} — token valid for ${formatTtl(creds.expires_at)}, ${creds.repos.length} repo${creds.repos.length === 1 ? "" : "s"} connected`);
50
+ // Ensure skills are installed/up-to-date on every interactive run
51
+ runInstallSkills();
49
52
  printSetupInstructions();
50
53
  process.exit(0);
51
54
  }
@@ -61,6 +64,8 @@ async function main() {
61
64
  const newCreds = tokenToCredentials(token, sseUrl);
62
65
  writeCredentials(newCreds);
63
66
  printAuthSuccess(token.github_login ?? null, token.tenant_name ?? null, token.repos?.length ?? 0);
67
+ // Auto-install skills to detected platforms (Claude Code, OpenClaw)
68
+ runInstallSkills();
64
69
  printSetupInstructions();
65
70
  process.exit(0);
66
71
  }
@@ -70,7 +75,14 @@ async function main() {
70
75
  printLines("Pharaoh: no valid credentials — cannot start proxy.", "Run this command first to authenticate:", " npx @pharaoh-so/mcp", "");
71
76
  process.exit(1);
72
77
  }
73
- // Valid credentials — start the proxy
78
+ // Valid credentials — ensure skills are installed before starting proxy
79
+ // Silently installs/updates on first run or package update; fast no-op if current.
80
+ try {
81
+ runInstallSkills();
82
+ }
83
+ catch {
84
+ // ALLOWED-CATCH: skill install failure must not block MCP proxy startup
85
+ }
74
86
  printLines(`Pharaoh: token valid for ${formatTtl(creds.expires_at)} — connecting`);
75
87
  try {
76
88
  await startProxy(creds.sse_url, creds.access_token);
package/dist/inspect.js CHANGED
@@ -14,6 +14,17 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
15
  const INSPECT_MSG = "This is Pharaoh's inspection server for directory listings. " +
16
16
  "Connect to https://mcp.pharaoh.so for actual usage.";
17
+ /** Read version from package.json (single source of truth). */
18
+ function getPackageVersion() {
19
+ try {
20
+ const pkgPath = join(import.meta.dirname, "..", "package.json");
21
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
22
+ return pkg.version ?? "0.0.0";
23
+ }
24
+ catch {
25
+ return "0.0.0";
26
+ }
27
+ }
17
28
  export async function runInspect() {
18
29
  // Load pre-generated schemas (shipped in the npm package)
19
30
  const schemasPath = join(import.meta.dirname, "..", "inspect-tools.json");
@@ -25,7 +36,7 @@ export async function runInspect() {
25
36
  process.stderr.write("Pharaoh: inspect-tools.json not found — rebuild the package.\n");
26
37
  process.exit(1);
27
38
  }
28
- const server = new McpServer({ name: "pharaoh", version: "0.1.0" }, {
39
+ const server = new McpServer({ name: "pharaoh", version: getPackageVersion() }, {
29
40
  instructions: "Pharaoh turns codebases into knowledge graphs that AI agents can think with. " +
30
41
  "Connect at https://mcp.pharaoh.so for full functionality.",
31
42
  });
@@ -14,6 +14,16 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
14
14
  const PKG_ROOT = join(__dirname, "..");
15
15
  /** Path to the bundled skills directory. */
16
16
  const BUNDLED_SKILLS_DIR = join(PKG_ROOT, "skills");
17
+ /** Read the package version from package.json (single source of truth). */
18
+ function getPackageVersion() {
19
+ try {
20
+ const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf-8"));
21
+ return pkg.version ?? "0.0.0";
22
+ }
23
+ catch {
24
+ return "0.0.0";
25
+ }
26
+ }
17
27
  // ── Detection ───────────────────────────────────────────────────────
18
28
  /**
19
29
  * Detect whether Claude Code is installed by checking for ~/.claude/.
@@ -59,50 +69,75 @@ function installClaudeCodePlugin(home = homedir()) {
59
69
  return -1;
60
70
  }
61
71
  // Copy skills/
72
+ let skillCount = 0;
62
73
  if (existsSync(BUNDLED_SKILLS_DIR)) {
63
74
  cpSync(BUNDLED_SKILLS_DIR, join(pluginDir, "skills"), { recursive: true, force: true });
75
+ const entries = readdirSync(BUNDLED_SKILLS_DIR, { withFileTypes: true });
76
+ skillCount = entries.filter((e) => e.isDirectory()).length;
64
77
  }
65
78
  // Copy .mcp.json
66
79
  const mcpSrc = join(PKG_ROOT, ".mcp.json");
67
80
  if (existsSync(mcpSrc)) {
68
81
  cpSync(mcpSrc, join(pluginDir, ".mcp.json"), { force: true });
69
82
  }
70
- // Count installed skills
71
- const entries = readdirSync(BUNDLED_SKILLS_DIR, { withFileTypes: true });
72
- return entries.filter((e) => e.isDirectory()).length;
83
+ return skillCount;
73
84
  }
74
85
  /**
75
- * Register Pharaoh in Claude Code's installed_plugins.json.
76
- * Does NOT overwrite an existing entry.
86
+ * Register Pharaoh in Claude Code's installed_plugins.json (v2 format).
87
+ * Adds a new entry or updates the version/lastUpdated of an existing one.
88
+ * Preserves all other plugin entries.
77
89
  *
78
90
  * @param home - Home directory override (defaults to os.homedir()).
79
- * @returns True if the entry was added, false if already present.
91
+ * @returns "added" if new entry created, "updated" if version bumped, "current" if already up-to-date, "error" on failure.
80
92
  */
81
93
  function registerClaudeCodePlugin(home = homedir()) {
82
94
  const registryPath = join(home, ".claude", "plugins", "installed_plugins.json");
83
95
  const pluginKey = "pharaoh@pharaoh-so";
84
- let registry = {};
96
+ const pluginDir = join(home, ".claude", "plugins", "data", "pharaoh");
97
+ const version = getPackageVersion();
98
+ const now = new Date().toISOString();
99
+ let registry = { version: 2, plugins: {} };
85
100
  if (existsSync(registryPath)) {
86
101
  try {
87
- registry = JSON.parse(readFileSync(registryPath, "utf-8"));
102
+ const raw = JSON.parse(readFileSync(registryPath, "utf-8"));
103
+ // Handle both v2 format (with plugins key) and any other shape
104
+ if (raw.version === 2 && raw.plugins && typeof raw.plugins === "object") {
105
+ registry = raw;
106
+ }
107
+ else {
108
+ // Non-v2 file — preserve as-is, add v2 structure around it
109
+ registry = { version: 2, plugins: {}, ...raw };
110
+ if (!registry.plugins)
111
+ registry.plugins = {};
112
+ }
88
113
  }
89
114
  catch {
90
115
  process.stderr.write("Pharaoh: installed_plugins.json exists but is not valid JSON — skipping registration.\n");
91
- return false;
116
+ return "error";
92
117
  }
93
118
  }
94
- if (registry[pluginKey]) {
95
- return false;
96
- }
97
- registry[pluginKey] = {
98
- name: "pharaoh",
99
- version: "0.2.0",
100
- source: "npm:@pharaoh-so/mcp",
101
- pluginRoot: join(home, ".claude", "plugins", "data", "pharaoh"),
102
- installedAt: new Date().toISOString(),
119
+ const existing = registry.plugins[pluginKey];
120
+ const entry = {
121
+ scope: "user",
122
+ installPath: pluginDir,
123
+ version,
124
+ installedAt: now,
125
+ lastUpdated: now,
103
126
  };
127
+ if (existing && existing.length > 0) {
128
+ const current = existing[0];
129
+ if (current.version === version) {
130
+ return "current";
131
+ }
132
+ // Version changed — update in place, preserve original installedAt
133
+ entry.installedAt = current.installedAt || now;
134
+ registry.plugins[pluginKey] = [entry];
135
+ writeFileSync(registryPath, JSON.stringify(registry, null, "\t"));
136
+ return "updated";
137
+ }
138
+ registry.plugins[pluginKey] = [entry];
104
139
  writeFileSync(registryPath, JSON.stringify(registry, null, "\t"));
105
- return true;
140
+ return "added";
106
141
  }
107
142
  // ── OpenClaw installer ──────────────────────────────────────────────
108
143
  /**
@@ -181,17 +216,19 @@ export function runInstallSkills(home = homedir()) {
181
216
  if (hasClaudeCode) {
182
217
  const count = installClaudeCodePlugin(home);
183
218
  if (count >= 0) {
184
- const registered = registerClaudeCodePlugin(home);
185
- const regMsg = registered
186
- ? "Plugin registered in installed_plugins.json"
187
- : "Plugin already registered skipped";
219
+ const regResult = registerClaudeCodePlugin(home);
220
+ const regMessages = {
221
+ added: "Plugin registered in installed_plugins.json",
222
+ updated: "Plugin version updated in installed_plugins.json",
223
+ current: "Plugin already registered and up-to-date",
224
+ error: "Could not update installed_plugins.json",
225
+ };
188
226
  process.stderr.write([
189
227
  `Pharaoh: installed ${count} skills as Claude Code plugin`,
190
228
  ` → ~/.claude/plugins/data/pharaoh/`,
191
- ` → ${regMsg}`,
192
- "",
193
- "Restart Claude Code to pick up the new skills.",
229
+ ` → ${regMessages[regResult]}`,
194
230
  "",
231
+ ...(regResult === "added" ? ["Restart Claude Code to pick up the new skills.", ""] : []),
195
232
  ].join("\n"));
196
233
  installed = true;
197
234
  }
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.2.4",
4
+ "version": "0.2.6",
5
5
  "description": "MCP proxy for Pharaoh — maps codebases into queryable knowledge graphs for AI agents. Enables Claude Code in headless environments (VPS, SSH, CI) via device flow auth.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",