@zhihand/mcp 0.18.1 → 0.19.0

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/dist/core/sse.js CHANGED
@@ -27,7 +27,7 @@ export function connectSSE(config) {
27
27
  return; // Already connected
28
28
  sseAbortController = new AbortController();
29
29
  const { signal } = sseAbortController;
30
- const url = `${config.controlPlaneEndpoint}/v1/credentials/${encodeURIComponent(config.credentialId)}/events?topic=commands`;
30
+ const url = `${config.controlPlaneEndpoint}/v1/credentials/${encodeURIComponent(config.credentialId)}/events/stream?topic=commands`;
31
31
  (async () => {
32
32
  while (!signal.aborted) {
33
33
  try {
@@ -1,4 +1,5 @@
1
- import { spawn } from "node:child_process";
1
+ import { spawn, execSync } from "node:child_process";
2
+ import fs from "node:fs";
2
3
  import fsp from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import os from "node:os";
@@ -12,6 +13,93 @@ const SESSION_STABILITY_DELAY = 2_000; // wait 2s after outcome before returning
12
13
  // Resolve pty-wrap.py relative to this file (works from both src/ and dist/)
13
14
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
15
  const PTY_WRAP_SCRIPT = path.resolve(__dirname, "../../scripts/pty-wrap.py");
16
+ // ── Executable Path Resolution ───────────────────────────────
17
+ /** Cache of resolved executable paths to avoid repeated lookups */
18
+ const executableCache = new Map();
19
+ /**
20
+ * Resolve the full path of a CLI executable.
21
+ * Searches PATH first via `which`, then falls back to platform-specific known locations.
22
+ */
23
+ function resolveExecutable(name, fallbackPaths) {
24
+ const cached = executableCache.get(name);
25
+ if (cached)
26
+ return cached;
27
+ // Try `which` first (works when the binary is in PATH)
28
+ try {
29
+ const resolved = execSync(`which ${name}`, { encoding: "utf8", timeout: 5000 }).trim();
30
+ if (resolved) {
31
+ executableCache.set(name, resolved);
32
+ return resolved;
33
+ }
34
+ }
35
+ catch {
36
+ // Not in PATH, try fallback locations
37
+ }
38
+ // Try known platform-specific paths
39
+ for (const candidate of fallbackPaths) {
40
+ // Support glob-like patterns with * (e.g. version directories)
41
+ if (candidate.includes("*")) {
42
+ try {
43
+ const dir = path.dirname(candidate);
44
+ const pattern = path.basename(candidate);
45
+ // Walk one level of glob for version directories
46
+ const parentDir = path.dirname(dir);
47
+ const globSegment = path.basename(dir);
48
+ if (globSegment === "*") {
49
+ const entries = fs.readdirSync(parentDir, { withFileTypes: true });
50
+ // Sort descending to prefer latest version
51
+ const dirs = entries
52
+ .filter(e => e.isDirectory())
53
+ .map(e => e.name)
54
+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
55
+ for (const d of dirs) {
56
+ const full = path.join(parentDir, d, pattern);
57
+ if (fs.existsSync(full)) {
58
+ executableCache.set(name, full);
59
+ return full;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ catch {
65
+ // Glob resolution failed, skip
66
+ }
67
+ }
68
+ else {
69
+ if (fs.existsSync(candidate)) {
70
+ executableCache.set(name, candidate);
71
+ return candidate;
72
+ }
73
+ }
74
+ }
75
+ // Last resort: return bare name and let spawn fail with a clear error
76
+ return name;
77
+ }
78
+ /** Resolve gemini executable path */
79
+ function resolveGemini() {
80
+ return resolveExecutable("gemini", [
81
+ "/opt/homebrew/bin/gemini", // macOS ARM (Homebrew)
82
+ "/usr/local/bin/gemini", // macOS Intel / Linux
83
+ path.join(os.homedir(), ".local/bin/gemini"), // pip --user install
84
+ path.join(os.homedir(), "bin/gemini"),
85
+ ]);
86
+ }
87
+ /** Resolve claude executable path */
88
+ function resolveClaude() {
89
+ const platform = process.platform;
90
+ const fallbacks = [];
91
+ if (platform === "darwin") {
92
+ // macOS: Claude Code installed via Claude desktop app
93
+ fallbacks.push(path.join(os.homedir(), "Library/Application Support/Claude/claude-code/*/claude.app/Contents/MacOS/claude"), "/usr/local/bin/claude", "/opt/homebrew/bin/claude");
94
+ }
95
+ else if (platform === "linux") {
96
+ fallbacks.push("/usr/local/bin/claude", path.join(os.homedir(), ".local/bin/claude"), "/snap/bin/claude");
97
+ }
98
+ else if (platform === "win32") {
99
+ fallbacks.push(path.join(process.env.LOCALAPPDATA ?? "", "Programs/Claude/claude.exe"), path.join(process.env.APPDATA ?? "", "npm/claude.cmd"));
100
+ }
101
+ return resolveExecutable("claude", fallbacks);
102
+ }
15
103
  // Gemini session directories
16
104
  const GEMINI_TMP_DIR = path.join(os.homedir(), ".gemini", "tmp");
17
105
  let activeChild = null;
@@ -396,7 +484,8 @@ function dispatchGemini(prompt, startTime, log, model) {
396
484
  COLORTERM: "truecolor",
397
485
  };
398
486
  // Wrap with PTY so gemini sees isatty()==true
399
- const child = spawn("python3", [PTY_WRAP_SCRIPT, "gemini", ...cliArgs], {
487
+ const geminiPath = resolveGemini();
488
+ const child = spawn("python3", [PTY_WRAP_SCRIPT, geminiPath, ...cliArgs], {
400
489
  env,
401
490
  stdio: ["ignore", "pipe", "pipe"],
402
491
  detached: false,
@@ -427,7 +516,8 @@ function dispatchCodex(prompt, startTime, model) {
427
516
  }
428
517
  // ── Claude Dispatch ────────────────────────────────────────
429
518
  function dispatchClaude(prompt, startTime, model) {
430
- const child = spawn("claude", ["-p", prompt, "--output-format", "json"], {
519
+ const claudePath = resolveClaude();
520
+ const child = spawn(claudePath, ["-p", prompt, "--output-format", "json"], {
431
521
  env: process.env,
432
522
  stdio: ["ignore", "pipe", "pipe"],
433
523
  detached: false,
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { controlSchema, screenshotSchema, pairSchema } from "./tools/schemas.js"
5
5
  import { executeControl } from "./tools/control.js";
6
6
  import { handleScreenshot } from "./tools/screenshot.js";
7
7
  import { handlePair } from "./tools/pair.js";
8
- const PACKAGE_VERSION = "0.18.1";
8
+ const PACKAGE_VERSION = "0.19.0";
9
9
  export function createServer(deviceName) {
10
10
  const server = new McpServer({
11
11
  name: "zhihand",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhihand/mcp",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "ZhiHand MCP Server — phone control tools for Claude Code, Codex, Gemini CLI, and OpenClaw",