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.
- package/apps/bridge/src/bin/hooks/statusline/claude-statusline-hook.ts +11 -0
- package/apps/bridge/src/install/hooks.ts +2 -1
- package/apps/bridge/src/install/inspect.ts +1 -0
- package/apps/bridge/src/mesh/catalog/models.ts +4 -3
- package/apps/bridge/src/mesh/snapshot/scoped-view.ts +1 -1
- package/apps/bridge/src/monitor/handlers/user-message.ts +1 -1
- package/apps/bridge/src/provider-account-status/README.md +18 -0
- package/apps/bridge/src/provider-account-status/claude-statusline.ts +63 -0
- package/apps/bridge/src/provider-account-status/index.ts +48 -0
- package/apps/bridge/src/provider-account-status/projection.ts +42 -0
- package/apps/bridge/src/provider-model-catalog/README.md +13 -0
- package/apps/bridge/src/provider-model-catalog/cli-models.ts +52 -0
- package/apps/bridge/src/provider-model-catalog/index.ts +25 -1
- package/apps/bridge/src/reply/README.md +4 -0
- package/apps/bridge/src/reply/index.ts +13 -105
- package/apps/bridge/src/reply/process/codex-headless.ts +104 -0
- package/apps/bridge/src/reply/process/provider-effort.ts +10 -0
- package/apps/bridge/src/reply/process/provider-headless.ts +5 -4
- package/apps/mcp/src/desktop/delivery/task-create.ts +5 -2
- package/apps/mcp/src/desktop/delivery/task-send.ts +6 -2
- package/apps/mcp/src/desktop/mesh-project.ts +1 -1
- package/apps/mcp/src/desktop/models/index.ts +4 -4
- package/bin/granttap-mcp.mjs +2 -2
- package/cursor-plugin/README.md +1 -1
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +1 -1
- package/package.json +1 -1
- package/packages/protocol/messages/interaction/index.ts +1 -1
- package/packages/protocol/messages/mesh/catalog/provider.ts +56 -0
- 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) =>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AdvertisedModel, EndpointModelCatalog, MeshProvider } from "../../../../../packages/protocol/schema";
|
|
2
|
-
import {
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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
|
-
...(
|
|
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 {
|
|
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
|
|
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
|
}
|
package/bin/granttap-mcp.mjs
CHANGED
|
@@ -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"
|
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.66
|
|
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.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.
|
|
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
|
@@ -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(),
|