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.
- package/apps/bridge/src/device-management/index.ts +7 -1
- package/apps/bridge/src/device-management/maintenance.ts +12 -1
- package/apps/bridge/src/providers/codex-bin.ts +2 -0
- package/apps/bridge/src/reply/README.md +5 -0
- package/apps/bridge/src/reply/grok-response/index.ts +2 -1
- package/apps/bridge/src/reply/process/auth-failure.ts +11 -0
- package/apps/bridge/src/reply/process/index.ts +2 -2
- package/apps/bridge/src/tools/updater.ts +25 -9
- package/apps/bridge/src/tools/version.ts +3 -2
- package/cursor-plugin/README.md +1 -1
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +1 -1
- package/docs/device-management.md +11 -1
- package/package.json +1 -1
- package/packages/protocol/messages/device/management.ts +2 -1
|
@@ -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
|
|
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:
|
|
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
|
|
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":
|
|
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":
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
|
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
|
|
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;
|
package/cursor-plugin/README.md
CHANGED
|
@@ -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.
|
|
13
|
+
npm install -g granttap-mcp@0.8.112
|
|
14
14
|
granttap setup
|
|
15
15
|
granttap status
|
|
16
16
|
```
|
package/cursor-plugin/mcp.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
@@ -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({
|