granttap-mcp 0.8.64 → 0.8.66

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.
Files changed (30) hide show
  1. package/apps/bridge/src/bin/hooks/statusline/claude-statusline-hook.ts +11 -0
  2. package/apps/bridge/src/install/hooks.ts +2 -1
  3. package/apps/bridge/src/install/inspect.ts +1 -0
  4. package/apps/bridge/src/mesh/catalog/models.ts +4 -3
  5. package/apps/bridge/src/mesh/snapshot/scoped-view.ts +1 -1
  6. package/apps/bridge/src/monitor/handlers/user-message.ts +1 -1
  7. package/apps/bridge/src/provider-account-status/README.md +18 -0
  8. package/apps/bridge/src/provider-account-status/claude-statusline.ts +63 -0
  9. package/apps/bridge/src/provider-account-status/index.ts +48 -0
  10. package/apps/bridge/src/provider-account-status/projection.ts +42 -0
  11. package/apps/bridge/src/provider-model-catalog/README.md +13 -0
  12. package/apps/bridge/src/provider-model-catalog/cli-models.ts +52 -0
  13. package/apps/bridge/src/provider-model-catalog/index.ts +25 -1
  14. package/apps/bridge/src/reply/README.md +4 -0
  15. package/apps/bridge/src/reply/index.ts +13 -105
  16. package/apps/bridge/src/reply/process/codex-headless.ts +104 -0
  17. package/apps/bridge/src/reply/process/provider-effort.ts +10 -0
  18. package/apps/bridge/src/reply/process/provider-headless.ts +5 -4
  19. package/apps/mcp/src/desktop/delivery/task-create.ts +5 -2
  20. package/apps/mcp/src/desktop/delivery/task-send.ts +6 -2
  21. package/apps/mcp/src/desktop/mesh-project.ts +1 -1
  22. package/apps/mcp/src/desktop/models/index.ts +4 -4
  23. package/bin/granttap-mcp.mjs +2 -2
  24. package/cursor-plugin/README.md +1 -1
  25. package/cursor-plugin/mcp.json +1 -1
  26. package/cursor-plugin/stdio-bootstrap.cjs +1 -1
  27. package/package.json +1 -1
  28. package/packages/protocol/messages/interaction/index.ts +1 -1
  29. package/packages/protocol/messages/mesh/catalog/provider.ts +56 -0
  30. package/packages/protocol/messages/mesh/snapshot.ts +2 -38
@@ -0,0 +1,11 @@
1
+ import { runClaudeStatusLine } from "../../../provider-account-status/claude-statusline";
2
+
3
+ async function main() {
4
+ let raw = "";
5
+ for await (const chunk of process.stdin) {
6
+ raw += chunk.toString();
7
+ if (Buffer.byteLength(raw) > 64_000) return;
8
+ }
9
+ await runClaudeStatusLine(raw);
10
+ }
11
+ main().catch(() => undefined);
@@ -1,6 +1,7 @@
1
1
  /** Install the GrantTap-owned hooks without changing unrelated handlers.
2
2
  * Configuration alone does not establish Codex hook trust.
3
3
  */
4
+ import { ensureClaudeUsageStatusLine } from "../provider-account-status/claude-statusline";
4
5
  import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
6
  import { homedir } from "node:os";
6
7
  import { dirname, join } from "node:path";
@@ -61,7 +62,7 @@ export function installClaudeHook(): InstallResult {
61
62
  // The prompt hook rides along with the approval hook: what the chat could
62
63
  // not know by itself — background runs in it, the Mesh around it — is added
63
64
  // to each prompt. It never blocks; anything wrong, and it adds nothing.
64
- const promptHookChanged = ensureClaudePromptHook(settings);
65
+ const promptHookChanged = [ensureClaudePromptHook(settings), ensureClaudeUsageStatusLine(settings, hookCommand("claude-statusline"))].some(Boolean);
65
66
  const entries: any[] = (((settings.hooks ??= {}).PreToolUse ??= []) as any[]);
66
67
  const currentCommand = hookCommand("claude");
67
68
  const presentEntry = entries.find((entry) =>
@@ -49,6 +49,7 @@ export { isCursorHelperNode, resolveMonitorNodeBin };
49
49
  const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
50
50
 
51
51
  export type HookRoute =
52
+ | "claude-statusline"
52
53
  | "claude"
53
54
  | "claude-prompt"
54
55
  | "codex"
@@ -1,5 +1,5 @@
1
1
  import type { AdvertisedModel, EndpointModelCatalog, MeshProvider } from "../../../../../packages/protocol/schema";
2
- import { readCodexModelCatalog } from "../../provider-model-catalog";
2
+ import { readProviderModelCatalog } from "../../provider-model-catalog";
3
3
 
4
4
  const PROVIDERS = new Set(["claude", "codex", "cursor", "grok"]);
5
5
 
@@ -54,9 +54,10 @@ export function catalogFromEndpoint(
54
54
  endpointId: string, sessions: Parameters<typeof catalogFromSessions>[1], now = Date.now(),
55
55
  ): EndpointModelCatalog {
56
56
  const observed = catalogFromSessions(endpointId, sessions, now);
57
- const native = readCodexModelCatalog(endpointId, { now });
57
+ const native = readProviderModelCatalog(endpointId);
58
58
  const models = [...native.models, ...observed.models.filter(item => native.stale || item.provider !== "codex")];
59
- return { endpointId, observedAt: now, models: models.slice(0, 64),
59
+ return { endpointId, observedAt: now, models: models.slice(0, 256),
60
+ accountUsage: native.accountUsage,
60
61
  ...(native.stale ? { stale: true, reason: native.reason } : {}),
61
62
  };
62
63
  }
@@ -116,7 +116,7 @@ export function scopedMeshView(
116
116
  skills: snapshot.skills ?? [],
117
117
  mcpServers: snapshot.mcpServers ?? [],
118
118
  capabilityRequests: snapshot.capabilityRequests ?? [],
119
- modelCatalog: snapshot.modelCatalog ?? [],
119
+ modelCatalog: snapshot.modelCatalog?.map(({ accountUsage: _usage, ...catalog }) => catalog) ?? [],
120
120
  executionPolicy: snapshot.execution,
121
121
  restrictions: snapshot.restrictions,
122
122
  environment: snapshot.environment,
@@ -67,7 +67,7 @@ async function startNewAgentTask(input: {
67
67
  grok: createGrokSession,
68
68
  }[agent];
69
69
  const result = await create(
70
- message.text, requestedCwd, DELIVERY_TIMEOUT_MS, resolved.attachments, message.model,
70
+ message.text, requestedCwd, DELIVERY_TIMEOUT_MS, resolved.attachments, message.model, undefined, message.effort,
71
71
  );
72
72
  if (result.ok) {
73
73
  await say(result.text, result.sessionId, true);
@@ -0,0 +1,18 @@
1
+ # Provider account status
2
+
3
+ Public entry: `index.ts`. The catalog loop reads the installed, already signed-in
4
+ Codex app-server's `model/list` and `account/rateLimits/read`. No conversation or
5
+ turn is created, no authentication flow is started, and chat delivery never waits
6
+ for the account request. Reads coalesce and are throttled to two minutes; reports
7
+ expire after five minutes and changing the native auth file clears the cache.
8
+
9
+ Only visible picker metadata and reported usage percentages, window durations,
10
+ reset times and observation times cross the existing encrypted Mesh channel.
11
+ Identity, credentials, credits, instructions and raw provider responses stay out.
12
+ Missing windows remain unknown. Claude CLI aliases remain available. The GrantTap status-line wrapper observes
13
+ Claude's documented `rate_limits` after a native response, preserves the prior
14
+ status command/options, and stores only independently present 5h/7d windows.
15
+ Cursor and Grok subscription limits remain unavailable when their installed
16
+ interfaces do not report them; their session tokens and tools are still counted.
17
+
18
+ Tests: `tests/status.test.ts`. Apache License 2.0, see root LICENSE.
@@ -0,0 +1,63 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ import { configDir } from "../config/runtime/paths";
5
+ import { writePrivateFile } from "../config/access/write-private";
6
+ import { ProviderAccountUsage, type ProviderAccountUsage as Usage } from "../../../../packages/protocol/schema";
7
+
8
+ const previousPath = () => join(configDir(), "claude-statusline-previous.json");
9
+ const reportPath = () => join(configDir(), "claude-account-usage.json");
10
+
11
+ export function projectClaudeUsage(input: any, now = Date.now()): Usage | undefined {
12
+ const windows = ([["five_hour", "primary", 300], ["seven_day", "secondary", 10_080]] as const)
13
+ .flatMap(([key, id, duration]) => {
14
+ const value = input?.rate_limits?.[key];
15
+ if (!Number.isFinite(value?.used_percentage) || value.used_percentage < 0 || value.used_percentage > 100
16
+ || !Number.isFinite(value?.resets_at) || value.resets_at * 1_000 <= now) return [];
17
+ return [{ id: id as "primary" | "secondary", usedPercent: value.used_percentage,
18
+ windowMinutes: Number(duration), resetsAt: value.resets_at * 1_000 }];
19
+ });
20
+ return windows.length ? { provider: "claude", limitId: "claude", observedAt: now, windows } : undefined;
21
+ }
22
+
23
+ export function readClaudeUsage(now = Date.now()): Usage[] {
24
+ try {
25
+ const text = readFileSync(reportPath(), "utf8");
26
+ if (text.length > 4_096) return [];
27
+ const value = ProviderAccountUsage.parse(JSON.parse(text));
28
+ return now - value.observedAt <= 5 * 60_000 && value.observedAt <= now + 60_000 ? [value] : [];
29
+ } catch { return []; }
30
+ }
31
+
32
+ /** Keep the person's existing status renderer and its options; never touch approval policy. */
33
+ export function ensureClaudeUsageStatusLine(settings: any, command: string): boolean {
34
+ if (settings.statusLine?.command === command) return false;
35
+ if (typeof settings.statusLine?.command === "string" && settings.statusLine.command.includes("internal hook claude-statusline")) {
36
+ settings.statusLine.command = command;
37
+ return true;
38
+ }
39
+ if (settings.statusLine && settings.statusLine.type !== "command") return false;
40
+ writePrivateFile(previousPath(), JSON.stringify(settings.statusLine ?? null));
41
+ settings.statusLine = { ...settings.statusLine, type: "command", command };
42
+ return true;
43
+ }
44
+
45
+ export async function runClaudeStatusLine(raw: string): Promise<void> {
46
+ try {
47
+ const input = JSON.parse(raw);
48
+ const usage = projectClaudeUsage(input);
49
+ if (usage) writePrivateFile(reportPath(), JSON.stringify(usage));
50
+ const previous = existsSync(previousPath()) ? JSON.parse(readFileSync(previousPath(), "utf8")) : null;
51
+ if (typeof previous?.command !== "string" || !previous.command.trim()) {
52
+ if (usage) process.stdout.write(usage.windows.map(w => `${w.windowMinutes === 300 ? "5h" : "7d"} ${w.usedPercent}%`).join(" · "));
53
+ return;
54
+ }
55
+ await new Promise<void>(resolve => {
56
+ const child = spawn(previous.command, { shell: true, stdio: ["pipe", "inherit", "ignore"], timeout: 3_000 });
57
+ child.stdin.on("error", () => undefined);
58
+ child.on("error", () => resolve());
59
+ child.on("close", () => resolve());
60
+ child.stdin.end(raw);
61
+ });
62
+ } catch { /* A status renderer must never interfere with a provider conversation. */ }
63
+ }
@@ -0,0 +1,48 @@
1
+ import { statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { EndpointModelCatalog } from "../../../../packages/protocol/schema";
5
+ import { codexHookRpc, type HookRpc } from "../codex-hook-trust";
6
+ import { projectAccountModels, projectAccountUsage } from "./projection";
7
+
8
+ export { projectAccountModels, projectAccountUsage } from "./projection";
9
+
10
+ /** A bounded read of an already signed-in provider; no threads, turns or login. */
11
+ export function createProviderAccountReader(rpc: HookRpc, clock = Date.now) {
12
+ let value: EndpointModelCatalog | undefined;
13
+ let pending: Promise<void> | undefined;
14
+ let attemptedAt = -Infinity;
15
+ return {
16
+ current(endpointId: string): EndpointModelCatalog | undefined {
17
+ return value && value.endpointId === endpointId && clock() - value.observedAt <= 5 * 60_000
18
+ ? value : undefined;
19
+ },
20
+ refresh(endpointId: string): Promise<void> {
21
+ if (pending) return pending;
22
+ if (clock() - attemptedAt < 120_000) return Promise.resolve();
23
+ attemptedAt = clock();
24
+ pending = Promise.allSettled([
25
+ rpc("model/list", { limit: 64, includeHidden: false }), rpc("account/rateLimits/read", {}),
26
+ ]).then(([models, usage]) => {
27
+ const now = clock();
28
+ const rows = models.status === "fulfilled" ? projectAccountModels(models.value, endpointId, now) : [];
29
+ value = { endpointId, observedAt: now, stale: rows.length === 0, models: rows,
30
+ accountUsage: usage.status === "fulfilled" ? projectAccountUsage(usage.value, now) : [] };
31
+ }).finally(() => { pending = undefined; });
32
+ return pending;
33
+ },
34
+ };
35
+ }
36
+
37
+ let scope = "";
38
+ let reader = createProviderAccountReader(codexHookRpc());
39
+
40
+ /** Catalog polling schedules account reads without delaying chat/catalog delivery. */
41
+ export function cachedProviderAccount(endpointId: string, env = process.env): EndpointModelCatalog | undefined {
42
+ const root = env.GRANTTAP_CODEX_DIR ?? env.NODVOX_CODEX_DIR ?? env.CODEX_HOME ?? join(homedir(), ".codex");
43
+ let identity: string;
44
+ try { identity = `${root}:${statSync(join(root, "auth.json")).mtimeMs}`; } catch { return undefined; }
45
+ if (identity !== scope) { scope = identity; reader = createProviderAccountReader(codexHookRpc({ env })); }
46
+ void reader.refresh(endpointId);
47
+ return reader.current(endpointId);
48
+ }
@@ -0,0 +1,42 @@
1
+ import type { AdvertisedModel, ProviderAccountUsage } from "../../../../packages/protocol/schema";
2
+
3
+ const EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"] as const;
4
+ type Effort = typeof EFFORTS[number];
5
+
6
+ const ID = /^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,159}$/;
7
+ const text = (value: unknown) => typeof value === "string"
8
+ ? value.replace(/[\x00-\x1f\x7f]/g, " ").trim().slice(0, 240) || undefined : undefined;
9
+
10
+ /** The account API's allowlist, never its identity, instructions or credits. */
11
+ export function projectAccountModels(value: any, endpointId: string, now: number): AdvertisedModel[] {
12
+ if (!Array.isArray(value?.data)) return [];
13
+ const rows = new Map<string, AdvertisedModel>();
14
+ for (const item of value.data) {
15
+ if (item?.hidden === true || typeof item?.model !== "string" || !ID.test(item.model)) continue;
16
+ rows.set(item.model, { modelId: item.model, endpointId, provider: "codex", source: "advertised",
17
+ label: text(item.displayName), description: text(item.description), priority: rows.size, observedAt: now,
18
+ ...(Array.isArray(item.supportedReasoningEfforts) ? { supportedEfforts:
19
+ [...new Set<Effort>(item.supportedReasoningEfforts.map((x: any) => x?.reasoningEffort)
20
+ .filter((x: unknown): x is Effort => EFFORTS.includes(x as Effort)))] } : {}) });
21
+ if (rows.size === 64) break;
22
+ }
23
+ return [...rows.values()];
24
+ }
25
+
26
+ export function projectAccountUsage(value: any, now: number): ProviderAccountUsage[] {
27
+ const limits = value?.rateLimitsByLimitId && typeof value.rateLimitsByLimitId === "object"
28
+ ? Object.values(value.rateLimitsByLimitId) : value?.rateLimits ? [value.rateLimits] : [];
29
+ return limits.slice(0, 16).flatMap((item: any) => {
30
+ const limitId = text(item?.limitId) ?? "codex";
31
+ if (!ID.test(limitId)) return [];
32
+ const windows = (["primary", "secondary"] as const).flatMap(id => {
33
+ const row = item?.[id];
34
+ if (!Number.isFinite(row?.usedPercent) || row.usedPercent < 0 || row.usedPercent > 100
35
+ || !Number.isInteger(row?.windowDurationMins) || row.windowDurationMins <= 0
36
+ || !Number.isFinite(row?.resetsAt) || row.resetsAt <= 0) return [];
37
+ return [{ id, usedPercent: row.usedPercent, windowMinutes: row.windowDurationMins,
38
+ resetsAt: row.resetsAt * 1_000 }];
39
+ });
40
+ return windows.length ? [{ provider: "codex" as const, limitId, observedAt: now, windows }] : [];
41
+ });
42
+ }
@@ -20,3 +20,16 @@ CLI aliases as aliases, whose resolved versions remain the provider's choice.
20
20
  Behavior tests: [`tests/catalog.test.ts`](tests/catalog.test.ts).
21
21
 
22
22
  Apache License 2.0. See [LICENSE](../../../../LICENSE).
23
+
24
+ ## All four providers
25
+
26
+ `readProviderModelCatalog` is the shared endpoint entry point. Fresh Codex
27
+ app-server picker metadata takes precedence over its disk cache. Cursor's native
28
+ `models` and Grok's native `models` commands supply exact account model IDs;
29
+ listing runs without a prompt, and failed/signed-out catalogs advertise nothing.
30
+ The wire allows up to 256 models so Cursor's parameter variants are not silently
31
+ cut to 64. Claude's supported CLI aliases remain aliases rather than a frozen
32
+ list of version numbers. Client model choices use the existing resume flags.
33
+
34
+ Account percentages travel separately as optional `accountUsage` observations.
35
+ See [provider account status](../provider-account-status/README.md).
@@ -0,0 +1,52 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import type { AdvertisedModel } from "../../../../packages/protocol/schema";
6
+ import { resolveCursorAgentBin } from "../reply/process/cursor-agent-bin";
7
+
8
+ export function parseCLIModels(output: string, provider: "cursor" | "grok", endpointId: string,
9
+ now: number): AdvertisedModel[] {
10
+ if (/not authenticated|please log in|login required/i.test(output)) return [];
11
+ const clean = output.replace(/\x1b\[[0-9;]*m/g, "");
12
+ return clean.split("\n").flatMap(line => {
13
+ const match = provider === "cursor"
14
+ ? /^([a-zA-Z0-9][a-zA-Z0-9._:/-]{0,159})\s+-\s+(.+)$/.exec(line.trim())
15
+ : /^\s*[*-]\s+([a-zA-Z0-9][a-zA-Z0-9._:/-]{0,159})(?:\s+\(([^)]+)\))?\s*$/.exec(line);
16
+ return match ? [{ modelId: match[1]!, label: (provider === "cursor" ? match[2]! : match[1]!).slice(0, 160),
17
+ provider, endpointId, observedAt: now, source: "advertised" as const }] : [];
18
+ }).slice(0, 240);
19
+ }
20
+
21
+ let report: AdvertisedModel[] = [];
22
+ let checkedAt = 0;
23
+ let pending = false;
24
+ let scope = "";
25
+
26
+ function read(binary: string, args: string[]): Promise<string> {
27
+ return new Promise(resolve => execFile(binary, args,
28
+ { cwd: homedir(), timeout: 10_000, maxBuffer: 128_000, windowsHide: true },
29
+ (error, stdout) => resolve(error ? "" : stdout)));
30
+ }
31
+
32
+ /** Only native listing commands; no prompt, session, login or auto-approval flags. */
33
+ export function cachedCLIModels(endpointId: string, env = process.env): AdvertisedModel[] {
34
+ const currentScope = [homedir(), endpointId, env.GRANTTAP_CURSOR_AGENT_BIN,
35
+ env.GRANTTAP_GROK_BIN, env.GRANTTAP_GROK_DIR].join("\u001f");
36
+ if (scope !== currentScope) { scope = currentScope; report = []; checkedAt = 0; }
37
+ if (!pending && Date.now() - checkedAt > 120_000) {
38
+ checkedAt = Date.now();
39
+ pending = true;
40
+ const cursor = resolveCursorAgentBin(env);
41
+ const grokRoot = env.GRANTTAP_GROK_DIR ?? join(homedir(), ".grok");
42
+ const grok = env.GRANTTAP_GROK_BIN ?? join(grokRoot, "bin/grok");
43
+ void Promise.all([
44
+ existsSync(cursor) ? read(cursor, ["models"]) : "",
45
+ existsSync(join(grokRoot, "auth.json")) && existsSync(grok) ? read(grok, ["models"]) : "",
46
+ ]).then(([a, b]) => {
47
+ if (scope === currentScope) report = [...parseCLIModels(a, "cursor", endpointId, Date.now()),
48
+ ...parseCLIModels(b, "grok", endpointId, Date.now())];
49
+ }).finally(() => { pending = false; });
50
+ }
51
+ return report.filter(row => Date.now() - row.observedAt <= 5 * 60_000);
52
+ }
@@ -3,6 +3,10 @@ import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import type { AdvertisedModel, EndpointModelCatalog } from "../../../../packages/protocol/schema";
5
5
 
6
+ import { readClaudeUsage } from "../provider-account-status/claude-statusline";
7
+ import { cachedCLIModels } from "./cli-models";
8
+ import { cachedProviderAccount } from "../provider-account-status";
9
+
6
10
  const MAX_BYTES = 2_000_000;
7
11
  const MAX_AGE = 24 * 60 * 60_000;
8
12
  const MODEL_ID = /^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,159}$/;
@@ -36,7 +40,7 @@ function readCache(path: string): Record<string, unknown> {
36
40
  }
37
41
 
38
42
  /** Project only picker metadata from Codex's account-scoped cache, never prompts or identity. */
39
- export function readCodexModelCatalog(
43
+ function readCachedCodexModels(
40
44
  endpointId: string, options: { path?: string; now?: number; env?: NodeJS.ProcessEnv } = {},
41
45
  ): EndpointModelCatalog {
42
46
  const now = options.now ?? Date.now();
@@ -61,3 +65,23 @@ export function readCodexModelCatalog(
61
65
  };
62
66
  } catch { return missing; }
63
67
  }
68
+
69
+ /** Fresh native account metadata takes precedence over an expired on-disk cache. */
70
+ export function readCodexModelCatalog(
71
+ endpointId: string, options: Parameters<typeof readCachedCodexModels>[1] = {},
72
+ ): EndpointModelCatalog {
73
+ const cached = readCachedCodexModels(endpointId, options);
74
+ if (options.path) return cached;
75
+ const live = cachedProviderAccount(endpointId, options.env);
76
+ if (!live) return cached;
77
+ return live.models.length ? live : { ...cached, accountUsage: live.accountUsage };
78
+ }
79
+
80
+ /** A shared endpoint catalog for all four existing coding providers. */
81
+ export function readProviderModelCatalog(endpointId: string): EndpointModelCatalog {
82
+ const codex = readCodexModelCatalog(endpointId);
83
+ const cli = cachedCLIModels(endpointId);
84
+ return { ...codex, observedAt: Math.max(codex.observedAt, ...cli.map(row => row.observedAt)),
85
+ accountUsage: [...(codex.accountUsage ?? []), ...readClaudeUsage()],
86
+ stale: codex.stale === true && cli.length === 0, models: [...codex.models, ...cli].slice(0, 256) };
87
+ }
@@ -14,3 +14,7 @@ resources that outlive the agent process need their own cancellation receipts.
14
14
 
15
15
  License: this module is distributed under the Apache License 2.0
16
16
  in the repository-root `LICENSE` file.
17
+
18
+ Model reasoning effort reaches resumed and new provider turns as native arguments.
19
+ Codex uses `model_reasoning_effort`; Claude uses `--effort`; Grok uses
20
+ `--reasoning-effort`. Cursor effort variants are native model IDs.
@@ -4,7 +4,8 @@ import { join } from "node:path";
4
4
  import type { SessionInfo, UserAttachment } from "../../../../packages/protocol/schema";
5
5
  import type { CodingAgent } from "../../../../packages/protocol/schema";
6
6
  import { explainClaudeFailure, resolveClaudeBinary } from "../providers/claude-bin";
7
- import { resolveCodexBinary } from "../providers/codex-bin";
7
+ import { runCodex, runCodexNew } from "./process/codex-headless";
8
+ import { providerEffortArgs } from "./process/provider-effort";
8
9
  import { configDir, isSessionPaused, loadRuntimeConfig, PAUSED_SESSION_REASON } from "../config";
9
10
  import { withAttachments } from "./payload/attachments";
10
11
  import { abortProcesses, runProcess } from "./process";
@@ -65,10 +66,11 @@ export function createCodexSession(
65
66
  attachments: UserAttachment[] = [],
66
67
  model?: string,
67
68
  operationKey?: string,
69
+ effort?: string,
68
70
  ): Promise<ReplyResult> {
69
71
  const workspace = resolveAgentWorkspace(cwd, "codex");
70
72
  return queued("__new_codex_task__", () => withAttachments(attachments, text, (prepared) =>
71
- runCodexNew(prepared.prompt, workspace, timeoutMs, prepared.images, model, operationKey),
73
+ runCodexNew(prepared.prompt, workspace, timeoutMs, prepared.images, model, operationKey, effort),
72
74
  ));
73
75
  }
74
76
 
@@ -80,10 +82,11 @@ export function createClaudeSession(
80
82
  attachments: UserAttachment[] = [],
81
83
  model?: string,
82
84
  operationKey?: string,
85
+ effort?: string,
83
86
  ): Promise<ReplyResult> {
84
87
  const workspace = resolveAgentWorkspace(cwd, "claude");
85
88
  return queued("__new_claude_task__", () => withAttachments(attachments, text, (prepared) =>
86
- runClaudeNew(prepared.claudePrompt, workspace, timeoutMs, model, operationKey),
89
+ runClaudeNew(prepared.claudePrompt, workspace, timeoutMs, model, operationKey, effort),
87
90
  ));
88
91
  }
89
92
 
@@ -94,10 +97,11 @@ export function createCursorSession(
94
97
  attachments: UserAttachment[] = [],
95
98
  model?: string,
96
99
  operationKey?: string,
100
+ effort?: string,
97
101
  ): Promise<ReplyResult> {
98
102
  const workspace = resolveAgentWorkspace(cwd, "cursor");
99
103
  return queued("__new_cursor_task__", () => withAttachments(attachments, text, (prepared) =>
100
- runCursorNew(prepared.claudePrompt, workspace, timeoutMs, model, operationKey),
104
+ runCursorNew(prepared.claudePrompt, workspace, timeoutMs, model, operationKey, effort),
101
105
  ));
102
106
  }
103
107
 
@@ -108,10 +112,11 @@ export function createGrokSession(
108
112
  attachments: UserAttachment[] = [],
109
113
  model?: string,
110
114
  operationKey?: string,
115
+ effort?: string,
111
116
  ): Promise<ReplyResult> {
112
117
  const workspace = resolveAgentWorkspace(cwd, "grok");
113
118
  return queued("__new_grok_task__", () => withAttachments(attachments, text, (prepared) =>
114
- runGrokNew(prepared.claudePrompt, workspace, timeoutMs, model, operationKey),
119
+ runGrokNew(prepared.claudePrompt, workspace, timeoutMs, model, operationKey, effort),
115
120
  ));
116
121
  }
117
122
 
@@ -154,7 +159,7 @@ function runClaude(
154
159
  const modeArgs = options.permissionMode
155
160
  ? ["--permission-mode", options.permissionMode]
156
161
  : [];
157
- const effortArgs = options.effort ? ["--effort", options.effort] : [];
162
+ const effortArgs = providerEffortArgs("claude", options.effort);
158
163
  const args = [
159
164
  "-p", "--resume", session.sessionId, ...mcpArgs, ...modelArgs,
160
165
  ...modeArgs, ...effortArgs,
@@ -180,9 +185,9 @@ function runClaude(
180
185
  }
181
186
 
182
187
  function runClaudeNew(
183
- text: string, cwd: string, timeoutMs: number, model?: string, operationKey?: string,
188
+ text: string, cwd: string, timeoutMs: number, model?: string, operationKey?: string, effort?: string,
184
189
  ): Promise<ReplyResult> {
185
- const args = ["-p", ...(model ? ["--model", model] : []), "--output-format", "json", text];
190
+ const args = ["-p", ...(model ? ["--model", model] : []), ...providerEffortArgs("claude", effort), "--output-format", "json", text];
186
191
  const claude = resolveClaudeBinary();
187
192
  return runProcess(claude.path, args, cwd, timeoutMs, (stdout) => {
188
193
  try {
@@ -202,100 +207,3 @@ function runClaudeNew(
202
207
  : { ok: false, error: "Claude returned an empty response." };
203
208
  }, undefined, operationKey);
204
209
  }
205
-
206
- function parseCodexJsonl(stdout: string, fallbackSessionId?: string): ReplyResult {
207
- let lastMessage = "";
208
- let sessionId = fallbackSessionId;
209
- let structured = false;
210
- let failure = "";
211
- for (const line of stdout.split("\n")) {
212
- if (!line.trim()) continue;
213
- try {
214
- const event = JSON.parse(line) as {
215
- type?: string;
216
- thread_id?: string;
217
- session_id?: string;
218
- item?: { type?: string; text?: string; message?: string };
219
- error?: { message?: string } | string;
220
- };
221
- structured = true;
222
- if (event.type === "thread.started") {
223
- sessionId = event.thread_id ?? event.session_id ?? sessionId;
224
- }
225
- if (event.item?.type === "agent_message" && typeof event.item.text === "string") {
226
- lastMessage = event.item.text;
227
- }
228
- if (event.type === "error" || event.type === "turn.failed" || event.item?.type === "error") {
229
- failure = (typeof event.error === "string" ? event.error : event.error?.message)
230
- ?? event.item?.message ?? "Codex reported an error.";
231
- }
232
- } catch {
233
- // Ignore non-JSON diagnostic lines.
234
- }
235
- }
236
- if (failure) return { ok: false, error: failure.slice(0, 500) };
237
- if (lastMessage) return { ok: true, text: lastMessage.slice(0, 4000), sessionId };
238
- if (structured) return { ok: false, error: "Codex returned no agent message." };
239
- const trimmed = stdout.trim();
240
- return trimmed
241
- ? { ok: true, text: trimmed.slice(0, 4000), sessionId }
242
- : { ok: false, error: "Codex returned an empty response." };
243
- }
244
-
245
- function runCodex(
246
- session: SessionInfo,
247
- text: string,
248
- timeoutMs: number,
249
- images: string[] = [],
250
- options: DeliveryOptions = {},
251
- ): Promise<ReplyResult> {
252
- const configured = loadRuntimeConfig().sessionAccess[session.sessionId];
253
- const disabledMcp = loadRuntimeConfig().sessionMcpDisabled[session.sessionId] ?? [];
254
- const sandbox = configured === "read-only"
255
- ? "read-only"
256
- : configured === "workspace"
257
- ? "workspace-write"
258
- : configured === "full"
259
- ? "danger-full-access"
260
- : null;
261
- const accessArgs = sandbox ? ["-c", `sandbox_mode="${sandbox}"`] : [];
262
- const mcpArgs = disabledMcp.flatMap((name) => [
263
- "-c",
264
- `mcp_servers.${tomlKey(name)}.enabled=false`,
265
- ]);
266
- const modelArgs = options.model ? ["-m", options.model] : [];
267
- const imageArgs = images.flatMap((path) => ["-i", path]);
268
- const args = [
269
- "exec", "resume", ...accessArgs, ...mcpArgs, ...modelArgs, ...imageArgs,
270
- session.sessionId, "--json", "-",
271
- ];
272
- return runProcess(
273
- resolveCodexBinary(),
274
- args,
275
- session.cwd,
276
- timeoutMs,
277
- (stdout) => parseCodexJsonl(stdout, session.sessionId),
278
- text,
279
- session.sessionId,
280
- );
281
- }
282
-
283
- function tomlKey(value: string): string {
284
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
285
- }
286
-
287
- function runCodexNew(
288
- text: string,
289
- cwd: string,
290
- timeoutMs: number,
291
- images: string[] = [],
292
- model?: string,
293
- operationKey?: string,
294
- ): Promise<ReplyResult> {
295
- // This is the supported non-interactive Codex path. It preserves the user's
296
- // normal sandbox, approval, hooks, model, and auth configuration.
297
- const imageArgs = images.flatMap((path) => ["-i", path]);
298
- const args = ["exec", ...(model ? ["-m", model] : []), ...imageArgs,
299
- "--json", "--skip-git-repo-check", "-"];
300
- return runProcess(resolveCodexBinary(), args, cwd, timeoutMs, parseCodexJsonl, text, operationKey);
301
- }
@@ -0,0 +1,104 @@
1
+ import type { SessionInfo } from "../../../../../packages/protocol/schema";
2
+ import { loadRuntimeConfig } from "../../config";
3
+ import { resolveCodexBinary } from "../../providers/codex-bin";
4
+ import { runProcess } from ".";
5
+ import { providerEffortArgs } from "./provider-effort";
6
+ import type { DeliveryOptions, ReplyResult } from "../payload/types";
7
+
8
+ function parseCodexJsonl(stdout: string, fallbackSessionId?: string): ReplyResult {
9
+ let lastMessage = "";
10
+ let sessionId = fallbackSessionId;
11
+ let structured = false;
12
+ let failure = "";
13
+ for (const line of stdout.split("\n")) {
14
+ if (!line.trim()) continue;
15
+ try {
16
+ const event = JSON.parse(line) as {
17
+ type?: string;
18
+ thread_id?: string;
19
+ session_id?: string;
20
+ item?: { type?: string; text?: string; message?: string };
21
+ error?: { message?: string } | string;
22
+ };
23
+ structured = true;
24
+ if (event.type === "thread.started") {
25
+ sessionId = event.thread_id ?? event.session_id ?? sessionId;
26
+ }
27
+ if (event.item?.type === "agent_message" && typeof event.item.text === "string") {
28
+ lastMessage = event.item.text;
29
+ }
30
+ if (event.type === "error" || event.type === "turn.failed" || event.item?.type === "error") {
31
+ failure = (typeof event.error === "string" ? event.error : event.error?.message)
32
+ ?? event.item?.message ?? "Codex reported an error.";
33
+ }
34
+ } catch {
35
+ // Ignore non-JSON diagnostic lines.
36
+ }
37
+ }
38
+ if (failure) return { ok: false, error: failure.slice(0, 500) };
39
+ if (lastMessage) return { ok: true, text: lastMessage.slice(0, 4000), sessionId };
40
+ if (structured) return { ok: false, error: "Codex returned no agent message." };
41
+ const trimmed = stdout.trim();
42
+ return trimmed
43
+ ? { ok: true, text: trimmed.slice(0, 4000), sessionId }
44
+ : { ok: false, error: "Codex returned an empty response." };
45
+ }
46
+
47
+ export function runCodex(
48
+ session: SessionInfo,
49
+ text: string,
50
+ timeoutMs: number,
51
+ images: string[] = [],
52
+ options: DeliveryOptions = {},
53
+ ): Promise<ReplyResult> {
54
+ const configured = loadRuntimeConfig().sessionAccess[session.sessionId];
55
+ const disabledMcp = loadRuntimeConfig().sessionMcpDisabled[session.sessionId] ?? [];
56
+ const sandbox = configured === "read-only"
57
+ ? "read-only"
58
+ : configured === "workspace"
59
+ ? "workspace-write"
60
+ : configured === "full"
61
+ ? "danger-full-access"
62
+ : null;
63
+ const accessArgs = sandbox ? ["-c", `sandbox_mode="${sandbox}"`] : [];
64
+ const mcpArgs = disabledMcp.flatMap((name) => [
65
+ "-c",
66
+ `mcp_servers.${tomlKey(name)}.enabled=false`,
67
+ ]);
68
+ const modelArgs = options.model ? ["-m", options.model] : [];
69
+ const imageArgs = images.flatMap((path) => ["-i", path]);
70
+ const args = [
71
+ "exec", "resume", ...accessArgs, ...mcpArgs, ...modelArgs, ...providerEffortArgs("codex", options.effort), ...imageArgs,
72
+ session.sessionId, "--json", "-",
73
+ ];
74
+ return runProcess(
75
+ resolveCodexBinary(),
76
+ args,
77
+ session.cwd,
78
+ timeoutMs,
79
+ (stdout) => parseCodexJsonl(stdout, session.sessionId),
80
+ text,
81
+ session.sessionId,
82
+ );
83
+ }
84
+
85
+ function tomlKey(value: string): string {
86
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
87
+ }
88
+
89
+ export function runCodexNew(
90
+ text: string,
91
+ cwd: string,
92
+ timeoutMs: number,
93
+ images: string[] = [],
94
+ model?: string,
95
+ operationKey?: string,
96
+ effort?: string,
97
+ ): Promise<ReplyResult> {
98
+ // This is the supported non-interactive Codex path. It preserves the user's
99
+ // normal sandbox, approval, hooks, model, and auth configuration.
100
+ const imageArgs = images.flatMap((path) => ["-i", path]);
101
+ const args = ["exec", ...(model ? ["-m", model] : []), ...providerEffortArgs("codex", effort), ...imageArgs,
102
+ "--json", "--skip-git-repo-check", "-"];
103
+ return runProcess(resolveCodexBinary(), args, cwd, timeoutMs, parseCodexJsonl, text, operationKey);
104
+ }
@@ -0,0 +1,10 @@
1
+ /** Provider-native controls, never interpolated into a prompt or shell. */
2
+ export function providerEffortArgs(provider: string, effort?: string): string[] {
3
+ if (!effort) return [];
4
+ const supported = provider === "codex" ? ["low", "medium", "high", "xhigh", "max", "ultra"]
5
+ : provider === "claude" ? ["low", "medium", "high", "xhigh", "max"]
6
+ : provider === "grok" ? ["low", "medium", "high"] : [];
7
+ if (!supported.includes(effort)) return [];
8
+ return provider === "codex" ? ["-c", `model_reasoning_effort="${effort}"`]
9
+ : [provider === "grok" ? "--reasoning-effort" : "--effort", effort];
10
+ }
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { SessionInfo } from "../../../../../packages/protocol/schema";
3
3
  import { runProcess } from ".";
4
+ import { providerEffortArgs } from "./provider-effort";
4
5
  import type { ReplyResult } from "../payload/types";
5
6
  import type { DeliveryOptions } from "../payload/types";
6
7
  import { resolveCursorAgentBin } from "./cursor-agent-bin";
@@ -61,7 +62,7 @@ function parseGrok(stdout: string, fallback: string): ReplyResult {
61
62
  }
62
63
 
63
64
  export function runCursorNew(
64
- text: string, cwd: string, timeoutMs: number, model?: string, operationKey?: string,
65
+ text: string, cwd: string, timeoutMs: number, model?: string, operationKey?: string, effort?: string,
65
66
  ): Promise<ReplyResult> {
66
67
  return runProcess(
67
68
  cursorBin(), ["-p", ...(model ? ["--model", model] : []),
@@ -81,12 +82,12 @@ export function runCursorResume(
81
82
  }
82
83
 
83
84
  export function runGrokNew(
84
- text: string, cwd: string, timeoutMs: number, model?: string, operationKey?: string,
85
+ text: string, cwd: string, timeoutMs: number, model?: string, operationKey?: string, effort?: string,
85
86
  ): Promise<ReplyResult> {
86
87
  const sessionId = randomUUID();
87
88
  const args = [
88
89
  "--no-auto-update", "--cwd", cwd, "--session-id", sessionId,
89
- ...(model ? ["--model", model] : []),
90
+ ...(model ? ["--model", model] : []), ...providerEffortArgs("grok", effort),
90
91
  "-p", text, "--output-format", "streaming-json",
91
92
  ];
92
93
  return runProcess(grokBin(), args, cwd, timeoutMs, (stdout) => parseGrok(stdout, sessionId),
@@ -101,7 +102,7 @@ export function runGrokResume(
101
102
  "--no-auto-update", ...(session.cwd ? ["--cwd", session.cwd] : []),
102
103
  ...(options.model ? ["--model", options.model] : []),
103
104
  ...(permission ? ["--permission-mode", permission] : []),
104
- ...(options.effort ? ["--reasoning-effort", options.effort] : []),
105
+ ...providerEffortArgs("grok", options.effort),
105
106
  "--resume", session.sessionId, "-p", text, "--output-format", "streaming-json",
106
107
  ];
107
108
  return runProcess(
@@ -31,7 +31,7 @@ export function desktopTaskCreate(
31
31
  const query = input as Record<string, unknown>;
32
32
  const projectId = query.project_id, bindingId = query.binding_id;
33
33
  const operationId = query.operation_id, text = query.text;
34
- const provider = query.provider, model = query.model;
34
+ const provider = query.provider, model = query.model, effort = query.effort;
35
35
  if (typeof projectId !== "string" || !projectId || projectId.length > 128
36
36
  || typeof bindingId !== "string" || !bindingId || bindingId.length > 128
37
37
  || query.endpoint_id !== endpoint
@@ -42,6 +42,9 @@ export function desktopTaskCreate(
42
42
  || model !== undefined && (typeof model !== "string" || model.length > 128)) {
43
43
  return Promise.resolve(undefined);
44
44
  }
45
+ if (effort !== undefined && !["low", "medium", "high", "xhigh", "max", "ultra"].includes(effort as string)) {
46
+ return Promise.resolve(undefined);
47
+ }
45
48
  const loaded = readStoreState(storePath);
46
49
  const binding = loaded.status === "ok"
47
50
  ? loaded.state.bindings.find((row) => row.bindingId === bindingId
@@ -86,7 +89,7 @@ export function desktopTaskCreate(
86
89
  if (!attachments || !text.trim() && attachments.length === 0) return Promise.resolve(undefined);
87
90
  const start = create ?? providers[agent];
88
91
  const result = start(text.trim(), workspace, 240_000, attachments, model as string | undefined,
89
- operationId).then((reply) => ({
92
+ operationId, effort as string | undefined).then((reply) => ({
90
93
  operation: "desktop.task_create", created: reply.ok,
91
94
  session_id: reply.ok ? reply.sessionId ?? null : null,
92
95
  error: reply.ok ? null : reply.error.slice(0, 500),
@@ -15,7 +15,7 @@ export function desktopTaskSend(input: unknown,
15
15
  if (!input || typeof input !== "object" || Array.isArray(input)) return Promise.resolve(undefined);
16
16
  const query = input as Record<string, unknown>;
17
17
  const { project_id: projectId, task_id: taskId, session_id: sessionId,
18
- delivery_id: deliveryId, text, model } = query;
18
+ delivery_id: deliveryId, text, model, effort } = query;
19
19
  if (typeof projectId !== "string" || !projectId || projectId.length > 128
20
20
  || typeof taskId !== "string" || !taskId || taskId.length > 128
21
21
  || typeof sessionId !== "string" || !sessionId || sessionId.length > 128
@@ -25,6 +25,9 @@ export function desktopTaskSend(input: unknown,
25
25
  || model !== undefined && (typeof model !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,159}$/.test(model))) {
26
26
  return Promise.resolve(undefined);
27
27
  }
28
+ if (effort !== undefined && !["low", "medium", "high", "xhigh", "max", "ultra"].includes(effort as string)) {
29
+ return Promise.resolve(undefined);
30
+ }
28
31
  const loaded = readStoreState(storePath);
29
32
  if (loaded.status !== "ok") return Promise.resolve(undefined);
30
33
  const task = loaded.state.tasks.find((item) => item.taskId === taskId);
@@ -51,7 +54,8 @@ export function desktopTaskSend(input: unknown,
51
54
  lastActivityAt: execution.activeAt ?? execution.updatedAt ?? execution.startedAt,
52
55
  tokensSession: 0, tokensLastTurn: 0,
53
56
  };
54
- const options = model === undefined ? {} : { model: model as string };
57
+ const options = { ...(model === undefined ? {} : { model: model as string }),
58
+ ...(effort === undefined ? {} : { effort: effort as string }) };
55
59
  const operation = deliver(session, text.trim(), 240_000, attachments, options).then((result) => ({
56
60
  operation: "desktop.task_send", accepted: result.ok,
57
61
  error: result.ok ? null : result.error.slice(0, 500),
@@ -84,7 +84,7 @@ export function desktopMeshSnapshots(storePath = join(configDir(), "project-mesh
84
84
  operation: "desktop.mesh_snapshots" as const,
85
85
  snapshots: (desktopWorkspace(storePath)?.projects ?? []).flatMap((project) =>
86
86
  desktopFixtureSnapshot({ project_id: project.project_id }, storePath) ?? [])
87
- .map((snapshot) => withDesktopModels(snapshot, models)),
87
+ .map((snapshot, index) => withDesktopModels(snapshot, models, index === 0)),
88
88
  };
89
89
  }
90
90
 
@@ -1,13 +1,13 @@
1
1
  import type { EndpointModelCatalog, MeshSnapshot } from "../../../../../packages/protocol/schema";
2
2
  import { computerId } from "../../../../bridge/src/mesh/identity/computer";
3
- import { readCodexModelCatalog } from "../../../../bridge/src/provider-model-catalog";
3
+ import { readProviderModelCatalog } from "../../../../bridge/src/provider-model-catalog";
4
4
 
5
5
  /** Native picker metadata is local to this Mac; discovering it never starts an agent. */
6
6
  export function desktopModelCatalog(): EndpointModelCatalog {
7
- return readCodexModelCatalog(computerId());
7
+ return readProviderModelCatalog(computerId());
8
8
  }
9
9
 
10
10
  export function withDesktopModels(snapshot: MeshSnapshot,
11
- catalog = desktopModelCatalog()): MeshSnapshot {
12
- return { ...snapshot, publisherEndpointId: catalog.endpointId, modelCatalog: [catalog] };
11
+ catalog = desktopModelCatalog(), includeModels = true): MeshSnapshot {
12
+ return { ...snapshot, publisherEndpointId: catalog.endpointId, modelCatalog: includeModels ? [catalog] : undefined };
13
13
  }
@@ -59,11 +59,11 @@ if (command === "mcp") {
59
59
  } else if (command === "internal" && argument === "monitor") {
60
60
  entry = join(root, "apps", "bridge", "src", "bin", "runtime", "monitor.ts");
61
61
  } else if (command === "internal" && argument === "hook" && [
62
- "claude", "claude-prompt", "codex", "codex-policy", "cursor", "cursor-after", "cursor-mcp",
62
+ "claude", "claude-prompt", "claude-statusline", "codex", "codex-policy", "cursor", "cursor-after", "cursor-mcp",
63
63
  ].includes(commandArgs[1])) {
64
64
  const route = commandArgs[1];
65
65
  const hookDir = route === "cursor-mcp" ? "runtime" : "hooks";
66
- const hookEntry = route === "codex-policy"
66
+ const hookEntry = route === "claude-statusline" ? "statusline/claude-statusline-hook.ts" : route === "codex-policy"
67
67
  ? "codex-policy-hook.ts"
68
68
  : route === "cursor-after"
69
69
  ? "cursor-after-shell.ts"
@@ -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.64
13
+ npm install -g granttap-mcp@0.8.66
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.64\";\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 20+ 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.66\";\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 20+ 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.64";
10
+ const PACKAGE = "granttap-mcp@0.8.66";
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granttap-mcp",
3
- "version": "0.8.64",
3
+ "version": "0.8.66",
4
4
  "description": "Personal live control center runtime for local coding agents on iPhone and Apple Watch.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -61,7 +61,7 @@ export const UserMessage = z.object({
61
61
  permissionMode: z.enum([
62
62
  "acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan",
63
63
  ]).optional(),
64
- effort: z.enum(["low", "medium", "high", "xhigh", "max"]).optional(),
64
+ effort: z.enum(["low", "medium", "high", "xhigh", "max", "ultra"]).optional(),
65
65
  createdAt: z.number(),
66
66
  });
67
67
  export type UserMessage = z.infer<typeof UserMessage>;
@@ -0,0 +1,56 @@
1
+ import { z } from "zod";
2
+ import { Identifier, Label, MeshProvider } from "../endpoint";
3
+
4
+ /**
5
+ * One SKILL.md the Project catalog published. Presence is not permission;
6
+ * Governance is the only authority for what may run.
7
+ */
8
+ export const SharedSkillState = z.enum(["discovered", "installed", "available", "used", "conflict", "unknown"]);
9
+ export type SharedSkillState = z.infer<typeof SharedSkillState>;
10
+ export const SharedSkill = z.object({
11
+ name: z.string().trim().min(1).max(160),
12
+ endpointId: Identifier.optional(),
13
+ description: z.string().trim().min(1).max(500).optional(),
14
+ version: z.string().trim().min(1).max(64).optional(),
15
+ digest: z.string().trim().min(1).max(128).optional(),
16
+ source: z.string().trim().min(1).max(240).optional(),
17
+ state: SharedSkillState.optional(),
18
+ }).strict();
19
+ export type SharedSkill = z.infer<typeof SharedSkill>;
20
+
21
+ export const ProviderAccountUsage = z.object({
22
+ provider: MeshProvider,
23
+ limitId: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,159}$/),
24
+ observedAt: z.number().nonnegative(),
25
+ windows: z.array(z.object({
26
+ id: z.enum(["primary", "secondary"]),
27
+ usedPercent: z.number().min(0).max(100),
28
+ windowMinutes: z.number().int().positive(),
29
+ resetsAt: z.number().positive(),
30
+ }).strict()).max(2),
31
+ }).strict();
32
+ export type ProviderAccountUsage = z.infer<typeof ProviderAccountUsage>;
33
+
34
+ export const AdvertisedModel = z.object({
35
+ modelId: z.string().trim().min(1).max(160),
36
+ provider: MeshProvider,
37
+ endpointId: Identifier,
38
+ source: z.enum(["observed", "advertised"]),
39
+ label: Label.optional(),
40
+ description: z.string().trim().min(1).max(240).optional(),
41
+ priority: z.number().int().nonnegative().max(10_000).optional(),
42
+ observedAt: z.number().nonnegative(),
43
+ supportedEfforts: z.array(z.enum(["low", "medium", "high", "xhigh", "max", "ultra"])).max(6).optional(),
44
+ }).strict();
45
+ export type AdvertisedModel = z.infer<typeof AdvertisedModel>;
46
+
47
+ export const EndpointModelCatalog = z.object({
48
+ endpointId: Identifier,
49
+ observedAt: z.number().nonnegative(),
50
+ stale: z.boolean().optional(),
51
+ models: z.array(AdvertisedModel).max(256),
52
+ reason: z.string().trim().min(1).max(240).optional(),
53
+ accountUsage: z.array(ProviderAccountUsage).max(16).optional(),
54
+ }).strict();
55
+ export type EndpointModelCatalog = z.infer<typeof EndpointModelCatalog>;
56
+
@@ -1,4 +1,6 @@
1
1
  import { z } from "zod";
2
+ import { SharedSkill, EndpointModelCatalog } from "./catalog/provider";
3
+ export * from "./catalog/provider";
2
4
  import { ProjectCodeMap } from "./graph/code-map";
3
5
  import { Identifier, Label, MeshProvider } from "./endpoint";
4
6
  import {
@@ -38,23 +40,6 @@ export const IntegrationPeer = z.object({
38
40
  }).strict();
39
41
  export type IntegrationPeer = z.infer<typeof IntegrationPeer>;
40
42
 
41
- /**
42
- * One SKILL.md the Project catalog published. Presence is not permission;
43
- * Governance is the only authority for what may run.
44
- */
45
- export const SharedSkillState = z.enum(["discovered", "installed", "available", "used", "conflict", "unknown"]);
46
- export type SharedSkillState = z.infer<typeof SharedSkillState>;
47
- export const SharedSkill = z.object({
48
- name: z.string().trim().min(1).max(160),
49
- endpointId: Identifier.optional(),
50
- description: z.string().trim().min(1).max(500).optional(),
51
- version: z.string().trim().min(1).max(64).optional(),
52
- digest: z.string().trim().min(1).max(128).optional(),
53
- source: z.string().trim().min(1).max(240).optional(),
54
- state: SharedSkillState.optional(),
55
- }).strict();
56
- export type SharedSkill = z.infer<typeof SharedSkill>;
57
-
58
43
  /** One MCP server actually advertised by a Project execution. */
59
44
  export const ProjectMcpServer = z.object({
60
45
  name: Label,
@@ -110,27 +95,6 @@ export const ProjectCapabilityObservation = z.object({
110
95
  }).strict();
111
96
  export type ProjectCapabilityObservation = z.infer<typeof ProjectCapabilityObservation>;
112
97
 
113
- export const AdvertisedModel = z.object({
114
- modelId: z.string().trim().min(1).max(160),
115
- provider: MeshProvider,
116
- endpointId: Identifier,
117
- source: z.enum(["observed", "advertised"]),
118
- label: Label.optional(),
119
- description: z.string().trim().min(1).max(240).optional(),
120
- priority: z.number().int().nonnegative().max(10_000).optional(),
121
- observedAt: z.number().nonnegative(),
122
- }).strict();
123
- export type AdvertisedModel = z.infer<typeof AdvertisedModel>;
124
-
125
- export const EndpointModelCatalog = z.object({
126
- endpointId: Identifier,
127
- observedAt: z.number().nonnegative(),
128
- stale: z.boolean().optional(),
129
- models: z.array(AdvertisedModel).max(64),
130
- reason: z.string().trim().min(1).max(240).optional(),
131
- }).strict();
132
- export type EndpointModelCatalog = z.infer<typeof EndpointModelCatalog>;
133
-
134
98
  export const ProjectBackbone = z.object({
135
99
  projectId: Identifier,
136
100
  head: z.string().trim().min(1).max(128).optional(),