granttap-mcp 0.8.110 → 0.8.112

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.
@@ -5,7 +5,7 @@ import { loadConfig, machineConfigPath } from '../config';
5
5
  import { observedComputerId } from '../mesh/identity/computer';
6
6
  import { clearComputerCache } from './cache';
7
7
  import { providerLogins } from './login';
8
- import { managementSnapshot, maintenanceIdle, restartProvider, restartRuntime, runtimeRoot, updateRuntime } from './maintenance';
8
+ import { managementSnapshot, maintenanceIdle, restartProvider, restartRuntime, runtimeRoot, updateRuntime, updateProvider } from './maintenance';
9
9
 
10
10
  type Send = (result: DeviceManagementResult) => Promise<void>;
11
11
  type Entry = { fingerprint: string; result?: DeviceManagementResult; pending?: Promise<void>; at: number; ttl: number };
@@ -35,6 +35,10 @@ export async function handleDeviceManagement(client: RelayClient, request: Devic
35
35
  return true;
36
36
  }
37
37
  if (requests.size >= 128) return false;
38
+ if (request.action === 'status' && request.operationId) {
39
+ const old = requests.get(client.room + ':' + peer + ':' + request.operationId)?.result;
40
+ if (old && old.action !== 'status' && !old.action.startsWith('provider-login')) await send(old);
41
+ }
38
42
  const entry: Entry = { fingerprint, at: Date.now(), ttl: request.action === 'status' ? 60_000 : 15 * 60_000 };
39
43
  requests.set(key, entry);
40
44
  const report: Send = async result => { entry.result = result; await send(result); };
@@ -76,6 +80,8 @@ async function execute(request: DeviceManagementRequest, scope: string, computer
76
80
  message = await updateRuntime();
77
81
  } else if (request.action === 'provider-restart') {
78
82
  message = await restartProvider(request.agent!, request.interruptActiveTasks);
83
+ } else if (request.action === 'provider-update') {
84
+ message = await updateProvider(request.agent!);
79
85
  } else if (request.action === 'runtime-restart') {
80
86
  // The phone waits for a different runtimeEpoch, rather than treating this acknowledgement as a restart.
81
87
  if (!maintenanceIdle()) throw Error('Finish active requests and sign-in before restarting MCP.');
@@ -17,6 +17,7 @@ import { computerCache } from './cache';
17
17
  import { providerLogins } from './login';
18
18
  import { ownedProviderProcesses, signalOwnedProcess } from '../reply/process';
19
19
  import { approvalsStatus } from '../approvals/state';
20
+ import { inspectTool, updateTool } from '../tools/updater';
20
21
 
21
22
  export const runtimeRoot = fileURLToPath(new URL('../../../../', import.meta.url));
22
23
  const runtimeEpoch = randomUUID();
@@ -39,15 +40,17 @@ export function maintenanceIdle(): boolean {
39
40
  }
40
41
 
41
42
  export async function managementSnapshot(): Promise<DeviceManagementSnapshot> {
43
+ const observedAt = Date.now();
42
44
  const directory = join(configDir(), 'runtime-updates');
43
45
  const selected = readState(directory);
44
46
  const providers = await Promise.all(agents.map(async agent => {
45
47
  const command = providerCommand(agent), installed = installedProvider(command);
46
48
  return { agent, installed, version: binaryVersion(command), authentication: await providerAuthentication(agent, command),
47
49
  loginSupported: installed, restartSupported: installed,
50
+ updateSupported: installed && inspectTool(agent, providerEnvironment()).update != null,
48
51
  activeRequests: ownedProviderProcesses(command).length };
49
52
  }));
50
- return { computerId: observedComputerId(), computerName: hostname(), observedAt: Date.now(),
53
+ return { computerId: observedComputerId(), computerName: hostname(), observedAt,
51
54
  runtimeVersion: runtimeVersion(runtimeRoot), runtimeEpoch, selectedRuntimeVersion: selected.version,
52
55
  automaticUpdatesEnabled: automaticUpdatesAllowed(directory),
53
56
  runtimeUpdateSupported: runtimeRoot.includes('/node_modules/'), runtimeRestartSupported: runtimeRestartSupported(),
@@ -89,3 +92,11 @@ export async function restartProvider(agent: CodingAgent, interrupt = false): Pr
89
92
  await promisify(execFile)(command, ['--version'], { env: providerEnvironment(), timeout: 15_000, maxBuffer: 16_384 });
90
93
  return 'A fresh provider CLI started successfully. The next message starts a new GrantTap connection to it.';
91
94
  }
95
+
96
+ export async function updateProvider(agent: CodingAgent): Promise<string> {
97
+ if (providerLogins.active()) throw Error('Finish provider sign-in before updating coding tools.');
98
+ if (ownedProviderProcesses(providerCommand(agent)).length) throw Error('Finish this provider’s active GrantTap requests before updating it.');
99
+ const outcome = await updateTool(agent, providerEnvironment());
100
+ if (!outcome.ok) throw Error(outcome.message);
101
+ return outcome.message;
102
+ }
@@ -23,6 +23,8 @@ export function resolveCodexBinary(
23
23
  if (override) return override;
24
24
  const onPath = executableOnPath("codex", env);
25
25
  if (onPath) return onPath;
26
+ const native = join(home, '.local', 'bin', 'codex');
27
+ if (executable(native)) return native;
26
28
  for (const app of ["ChatGPT.app", "Codex.app"]) {
27
29
  for (const root of [applicationsRoot, join(home, "Applications")]) {
28
30
  for (const relative of [
@@ -1,5 +1,10 @@
1
1
  # Task delivery
2
2
 
3
+ An explicit Grok Build usage-balance refusal is returned as a usage error rather
4
+ than a startup timeout or a successful answer. Both stderr exits and streaming
5
+ error events use the same explanation. Earlier confirmed response activity stays
6
+ recorded if a turn later fails; an error alone never creates admission evidence.
7
+
3
8
  This module prepares phone attachments, routes selected MCP/skill intent, and runs local agent commands. The public `reply.ts` entry point owns session queueing and agent-specific command selection.
4
9
 
5
10
  `provider-headless.ts` owns the provider-neutral headless start used when a Mesh
@@ -1,5 +1,6 @@
1
1
  import type { ReplyResult } from '../payload/types';
2
2
  import type { ProviderAdmission } from '../process';
3
+ import { providerUsageFailure } from '../process/auth-failure';
3
4
 
4
5
  function events(stdout: string): Record<string, unknown>[] {
5
6
  return stdout.split('\n').flatMap(line => {
@@ -40,7 +41,7 @@ export function parseGrok(stdout: string, fallback: string): ReplyResult {
40
41
  if (typeof event.session_id === 'string') sessionId = event.session_id;
41
42
  const error = typeof event.message === 'string' ? event.message : event.error;
42
43
  if (event.type === 'error' && typeof error === 'string') {
43
- return { ok: false, error: error.slice(0, 500) };
44
+ return { ok: false, error: providerUsageFailure(error) ?? error.slice(0, 500) };
44
45
  }
45
46
  }
46
47
  return text.trim() ? { ok: true, text: text.trim().slice(0, 4_000), sessionId }
@@ -9,3 +9,14 @@ export function providerSignInFailure(command: string, stderr: string): string |
9
9
  : executable.includes('codex') ? 'Codex' : executable.includes('cursor') ? 'Cursor' : 'the coding agent';
10
10
  return `Sign-in required for ${provider}. Open Devices, choose this computer, then Sign in for ${provider}. This message has not been submitted to the agent.`;
11
11
  }
12
+
13
+ /** Match the provider's explicit diagnostic, not an arbitrary HTTP 402 from a tool. */
14
+ export function providerUsageFailure(stderr: string): string | undefined {
15
+ if (!/\bGrok Build usage balance exhausted\b/i.test(stderr)) return;
16
+ return 'Grok usage limit reached: Grok Build usage balance exhausted. Check your Grok usage before retrying.';
17
+ }
18
+
19
+ export function providerExitFailure(command: string, stderr: string, stdout: string, code: number | null): string {
20
+ return providerUsageFailure(stderr)
21
+ ?? `${command} exited with code ${code}: ${(stderr.trim() || stdout.trim()).slice(0, 300)}`;
22
+ }
@@ -1,7 +1,7 @@
1
1
  import { spawn, type ChildProcess } from "node:child_process";
2
2
  import { delimiter, dirname } from 'node:path';
3
3
  import { realpathSync } from 'node:fs';
4
- import { providerSignInFailure } from './auth-failure';
4
+ import { providerSignInFailure, providerExitFailure } from './auth-failure';
5
5
  import { computerId } from "../../mesh/identity/computer";
6
6
  import { inspectRepository } from "../../mesh/catalog";
7
7
  import { localMeshStore } from "../../mesh/local-remote/local";
@@ -172,7 +172,7 @@ export function runProcess(
172
172
  if (code !== 0) {
173
173
  finish({
174
174
  ok: false,
175
- error: `${command} exited with code ${code}: ${(stderr.trim() || stdout.trim()).slice(0, 300)}`,
175
+ error: providerExitFailure(command, stderr, stdout, code),
176
176
  });
177
177
  return;
178
178
  }
@@ -12,12 +12,13 @@
12
12
  import { spawn } from "node:child_process";
13
13
  import { accessSync, constants, realpathSync } from "node:fs";
14
14
  import { homedir } from "node:os";
15
- import { basename, dirname, join } from "node:path";
15
+ import { basename, delimiter, dirname, join } from "node:path";
16
16
  import type { CodingAgent } from "../../../../packages/protocol/schema";
17
17
  import { compareVersions, executableOnPath, resolveClaudeBinary } from "../providers/claude-bin";
18
18
  import { resolveCodexBinary } from "../providers/codex-bin";
19
19
  import { resolveCursorAgentBin } from "../reply/process/cursor-agent-bin";
20
20
  import { askVersion, binaryVersion, forgetVersion, stripAnsi } from "./version";
21
+ import { resolveGrokBinary } from '../providers/grok-bin';
21
22
 
22
23
  export const TOOL_NAMES: Record<CodingAgent, string> = {
23
24
  claude: "Claude Code", codex: "Codex CLI", cursor: "Cursor CLI", grok: "Grok CLI",
@@ -51,6 +52,9 @@ export type ToolUpdateOutcome = {
51
52
 
52
53
  const UPDATE_TIMEOUT_MS = 10 * 60_000;
53
54
  const MAX_OUTPUT = 4_000;
55
+ // The desktop bundle has no self-update channel. Use the provider's standalone
56
+ // installer without modifying the signed app; its CLI is resolved after install.
57
+ const CODEX_INSTALL = 'installer="$(/usr/bin/curl -fsSL https://chatgpt.com/codex/install.sh)" && CODEX_NON_INTERACTIVE=1 /bin/sh -c "$installer"';
54
58
 
55
59
  function executable(path: string): boolean {
56
60
  try {
@@ -110,12 +114,13 @@ export function updateArgv(
110
114
  case "claude":
111
115
  if (install.method === "npm") return npmArgv(install, "@anthropic-ai/claude-code", env);
112
116
  if (install.method === "brew") return brewArgv("claude-code", env);
113
- if (install.method === "desktop") return undefined;
117
+ if (install.method === "desktop") return path ? [path, "install", "latest"] : undefined;
114
118
  return path ? [path, "update"] : undefined;
115
119
  case "codex":
116
120
  if (install.method === "npm") return npmArgv(install, "@openai/codex", env);
117
121
  if (install.method === "brew") return brewArgv("codex", env);
118
- return undefined;
122
+ if (path?.includes('.app/Contents/')) return ['/bin/sh', '-c', CODEX_INSTALL];
123
+ return path ? [path, "update"] : undefined;
119
124
  case "cursor":
120
125
  case "grok":
121
126
  return path ? [path, "update"] : undefined;
@@ -124,7 +129,11 @@ export function updateArgv(
124
129
 
125
130
  function locate(agent: CodingAgent, env: NodeJS.ProcessEnv, home: string): string | undefined {
126
131
  switch (agent) {
127
- case "claude": return env.GRANTTAP_CLAUDE_BIN ?? env.NODVOX_CLAUDE_BIN ?? executableOnPath("claude", env);
132
+ case "claude": {
133
+ const path = env.GRANTTAP_CLAUDE_BIN ?? env.NODVOX_CLAUDE_BIN ?? executableOnPath("claude", env)
134
+ ?? (executable(join(home, '.local/bin/claude')) ? join(home, '.local/bin/claude') : resolveClaudeBinary(home, env).path);
135
+ return executable(path) ? path : undefined;
136
+ }
128
137
  case "codex": {
129
138
  const path = resolveCodexBinary(home, env);
130
139
  return path.includes("/") ? (executable(path) ? path : undefined) : executableOnPath(path, env);
@@ -133,7 +142,10 @@ function locate(agent: CodingAgent, env: NodeJS.ProcessEnv, home: string): strin
133
142
  const bin = resolveCursorAgentBin(env);
134
143
  return bin.includes("/") ? (executable(bin) ? bin : undefined) : executableOnPath(bin, env);
135
144
  }
136
- case "grok": return env.GRANTTAP_GROK_BIN ?? executableOnPath("grok", env);
145
+ case "grok": {
146
+ const path = resolveGrokBinary(env, home);
147
+ return path.includes('/') ? (executable(path) ? path : undefined) : executableOnPath(path, env);
148
+ }
137
149
  }
138
150
  }
139
151
 
@@ -166,6 +178,7 @@ export function inspectTools(env: NodeJS.ProcessEnv = process.env, home = homedi
166
178
 
167
179
  /** The command as the phone shows it: the binary by name, the arguments as run. */
168
180
  export function describeCommand(argv: string[]): string {
181
+ if (argv[2] === CODEX_INSTALL) return 'Codex CLI installer';
169
182
  return [basename(argv[0] ?? ""), ...argv.slice(1)].join(" ");
170
183
  }
171
184
 
@@ -246,12 +259,14 @@ export function updateTool(
246
259
  const argv = status.update;
247
260
  const command = describeCommand(argv);
248
261
  const task = (async (): Promise<ToolUpdateOutcome> => {
249
- const before = status.version;
250
- const result = await runner(argv, env, home, timeoutMs);
262
+ const before = status.version ?? (status.path ? binaryVersion(status.path, askVersion) : undefined);
263
+ const nodeDirectory = dirname(process.execPath);
264
+ const updaterEnv = { ...env, PATH: [nodeDirectory, env.PATH ?? ''].join(delimiter) };
265
+ const result = await runner(argv, updaterEnv, home, timeoutMs);
251
266
  forgetVersion();
252
267
  // The updater just ran the tool; asking it its version now adds nothing new.
253
268
  const updated = inspectTool(agent, env, home);
254
- const after = updated.version ?? (updated.path ? askVersion(updated.path) : undefined);
269
+ const after = updated.version ?? (updated.path ? binaryVersion(updated.path, askVersion) : undefined);
255
270
  const output = stripAnsi(result.output).trim().slice(-MAX_OUTPUT) || undefined;
256
271
  if (result.error || result.code !== 0) {
257
272
  const why = result.error ?? `exited with code ${result.code}`;
@@ -261,7 +276,8 @@ export function updateTool(
261
276
  ? `${name} updated from ${before} to ${after}.`
262
277
  : !before && after
263
278
  ? `${name} is now ${after}.`
264
- : `${name} is up to date${after ? ` (${after})` : ""}.`;
279
+ : after ? `${name} is up to date (${after}).`
280
+ : `${name} updater completed. The installed version could not be verified.`;
265
281
  return { ok: true, before, after, command, output, message };
266
282
  })().finally(() => running.delete(agent));
267
283
  running.set(agent, task);
@@ -52,7 +52,7 @@ export function passiveVersion(resolvedPath: string): string | undefined {
52
52
  }
53
53
 
54
54
  export function parseVersionOutput(output: string): string | undefined {
55
- return SEMVER.exec(output)?.[0];
55
+ return /\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/.exec(output)?.[0];
56
56
  }
57
57
 
58
58
  export function stripAnsi(text: string): string {
@@ -91,9 +91,10 @@ export function binaryVersion(path: string, probe?: VersionProbe): string | unde
91
91
  return undefined;
92
92
  }
93
93
  const stated = passiveVersion(resolved);
94
- if (stated || !probe) return stated;
94
+ if (stated) return stated;
95
95
  const hit = cache.get(resolved);
96
96
  if (hit && hit.key === key) return hit.version;
97
+ if (!probe) return undefined;
97
98
  const version = probe(resolved);
98
99
  cache.set(resolved, { key, version });
99
100
  return version;
@@ -10,7 +10,7 @@ keeps each phone response tied to the exact originating chat and prompt.
10
10
  Install the supported CLI on this computer, then run:
11
11
 
12
12
  ```bash
13
- npm install -g granttap-mcp@0.8.110
13
+ npm install -g granttap-mcp@0.8.112
14
14
  granttap setup
15
15
  granttap status
16
16
  ```
@@ -4,7 +4,7 @@
4
4
  "command": "node",
5
5
  "args": [
6
6
  "-e",
7
- "\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.110\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 22.13.0+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
7
+ "\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.112\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 22.13.0+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
8
8
  ]
9
9
  }
10
10
  }
@@ -7,7 +7,7 @@ const { spawn } = require("node:child_process");
7
7
  const { existsSync } = require("node:fs");
8
8
  const { delimiter, dirname, join } = require("node:path");
9
9
 
10
- const PACKAGE = "granttap-mcp@0.8.110";
10
+ const PACKAGE = "granttap-mcp@0.8.112";
11
11
  const dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === "1";
12
12
  const platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM
13
13
  ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;
@@ -1,6 +1,6 @@
1
1
  # Computer controls from GrantTap
2
2
 
3
- MCP 0.8.110 adds the paired-controller `device.manage` protocol. The iOS client
3
+ MCP 0.8.109 adds the paired-controller `device.manage` protocol. The iOS client
4
4
  must also support these controls. A computer without an installed, connected
5
5
  MCP cannot execute remote commands; install and pair MCP on that computer first.
6
6
 
@@ -47,3 +47,13 @@ streams, launchd Node PATH recovery and preservation during cache cleanup.
47
47
  The latest sign-in state is replayed privately when its controller refreshes status.
48
48
  This restores completion after a phone returns from the browser without starting
49
49
  another login, resending a prompt, or retaining terminal authorization codes.
50
+
51
+ MCP 0.8.111 adds provider-update controls for Claude Code, Codex CLI, Grok CLI and Cursor CLI. Each runs its own fixed updater (or its owning npm/Homebrew installer) on the selected computer; desktop Claude can install the latest native CLI. An explicit update probes versions before and after, caches the observed version, and refuses active GrantTap requests for that provider or an active sign-in. Background recovery asks for the original operation ID; it returns the saved private result without running the update again. Older clients can ignore the optional updateSupported capability.
52
+
53
+ A Codex copy bundled with the desktop app is preserved. Its update action uses
54
+ the [official standalone installer](https://learn.chatgpt.com/docs/cli) with
55
+ noninteractive input and then resolves the native `~/.local/bin/codex` copy,
56
+ including under launchd's stripped PATH. An explicit binary override keeps
57
+ priority. Version output retains prerelease suffixes so an alpha is not shown
58
+ as a stable release. Regression tests simulate installer output and verify the
59
+ original app binary remains unchanged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granttap-mcp",
3
- "version": "0.8.110",
3
+ "version": "0.8.112",
4
4
  "description": "Commercial computer runtime connecting local coding agents to GrantTap on Mac, iPhone, iPad and Apple Watch.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,7 +4,7 @@ import { CodingAgent } from '../primitives';
4
4
  const identifier = z.string().trim().min(1).max(160);
5
5
  export const DeviceManagementAction = z.enum([
6
6
  'status', 'clear-cache', 'runtime-update', 'runtime-install', 'runtime-restart',
7
- 'provider-login', 'provider-login-code', 'provider-login-cancel', 'provider-restart',
7
+ 'provider-login', 'provider-login-code', 'provider-login-cancel', 'provider-restart', 'provider-update',
8
8
  ]);
9
9
  export const DeviceManagementRequest = z.object({
10
10
  type: z.literal('device.manage'),
@@ -41,6 +41,7 @@ export const DeviceProviderState = z.object({
41
41
  agent: CodingAgent, installed: z.boolean(), version: z.string().max(80).optional(),
42
42
  authentication: z.enum(['signed-in', 'signed-out', 'unknown', 'not-installed']),
43
43
  loginSupported: z.boolean(), restartSupported: z.boolean(),
44
+ updateSupported: z.boolean().optional(),
44
45
  activeRequests: z.number().int().nonnegative(),
45
46
  });
46
47
  export const DeviceManagementSnapshot = z.object({