llm-cli-gateway 1.17.9 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +140 -0
- package/README.md +142 -16
- package/dist/flight-recorder.d.ts +3 -0
- package/dist/flight-recorder.js +22 -20
- package/dist/index.d.ts +2 -0
- package/dist/index.js +22 -5
- package/dist/job-store.js +6 -12
- package/dist/sqlite-driver.d.ts +16 -0
- package/dist/sqlite-driver.js +149 -0
- package/dist/upstream-contracts.d.ts +10 -0
- package/dist/upstream-contracts.js +116 -6
- package/npm-shrinkwrap.json +3 -348
- package/package.json +4 -5
- package/socket.yml +20 -11
package/dist/index.js
CHANGED
|
@@ -143,8 +143,8 @@ function loadSkills() {
|
|
|
143
143
|
const loadedSkills = loadSkills();
|
|
144
144
|
const SERVER_INSTRUCTIONS = `llm-cli-gateway: Multi-LLM orchestration via MCP.
|
|
145
145
|
|
|
146
|
-
Tools: claude_request, codex_request, gemini_request, grok_request, mistral_request (sync) | *_request_async (async)
|
|
147
|
-
Validation: validate_with_models, second_opinion, compare_answers, red_team_review, consensus_check, ask_model, synthesize_validation
|
|
146
|
+
Tools: claude_request, codex_request, gemini_request, grok_request, mistral_request (sync) | *_request_async (async) | codex_fork_session (fork a Codex session into a new branch)
|
|
147
|
+
Validation: validate_with_models, second_opinion, compare_answers, red_team_review, consensus_check, ask_model, synthesize_validation, list_available_models | job_status/job_result (validation jobs)
|
|
148
148
|
Jobs: llm_job_status, llm_job_result, llm_job_cancel
|
|
149
149
|
Sessions: session_create, session_list, session_set_active, session_get, session_delete, session_clear_all
|
|
150
150
|
Other: list_models, cli_versions, upstream_contracts (use --probe-installed after CLI upgrades to detect drift), cli_upgrade, approval_list, llm_process_health, llm_request_result (read back any persisted request — sync or async — by correlationId)
|
|
@@ -159,7 +159,7 @@ Key behaviors:
|
|
|
159
159
|
Skills (full docs via MCP resources):
|
|
160
160
|
${loadedSkills.map(s => `- skills://${s.name} — ${s.description}`).join("\n")}`;
|
|
161
161
|
function newGatewayMcpServer() {
|
|
162
|
-
return new McpServer({ name: "llm-cli-gateway", version:
|
|
162
|
+
return new McpServer({ name: "llm-cli-gateway", version: packageVersion() }, { instructions: SERVER_INSTRUCTIONS });
|
|
163
163
|
}
|
|
164
164
|
let sessionManager;
|
|
165
165
|
let db = null;
|
|
@@ -1474,6 +1474,9 @@ export function prepareGrokRequest(params, runtime = resolveGatewayServerRuntime
|
|
|
1474
1474
|
if (params.restoreCode) {
|
|
1475
1475
|
args.push("--restore-code");
|
|
1476
1476
|
}
|
|
1477
|
+
if (params.leaderSocket) {
|
|
1478
|
+
args.push("--leader-socket", params.leaderSocket);
|
|
1479
|
+
}
|
|
1477
1480
|
if (params.nativeWorktree === true) {
|
|
1478
1481
|
args.push("--worktree");
|
|
1479
1482
|
}
|
|
@@ -1976,6 +1979,7 @@ export async function handleGrokRequest(deps, params) {
|
|
|
1976
1979
|
noSubagents: params.noSubagents,
|
|
1977
1980
|
oauth: params.oauth,
|
|
1978
1981
|
restoreCode: params.restoreCode,
|
|
1982
|
+
leaderSocket: params.leaderSocket,
|
|
1979
1983
|
nativeWorktree: params.nativeWorktree,
|
|
1980
1984
|
}, runtime);
|
|
1981
1985
|
if (!("args" in prep))
|
|
@@ -2133,6 +2137,7 @@ export async function handleGrokRequestAsync(deps, params) {
|
|
|
2133
2137
|
noSubagents: params.noSubagents,
|
|
2134
2138
|
oauth: params.oauth,
|
|
2135
2139
|
restoreCode: params.restoreCode,
|
|
2140
|
+
leaderSocket: params.leaderSocket,
|
|
2136
2141
|
nativeWorktree: params.nativeWorktree,
|
|
2137
2142
|
}, runtime);
|
|
2138
2143
|
if (!("args" in prep))
|
|
@@ -3488,12 +3493,17 @@ export function createGatewayServer(deps = {}) {
|
|
|
3488
3493
|
.boolean()
|
|
3489
3494
|
.optional()
|
|
3490
3495
|
.describe("Grok --restore-code: check out the original session commit when resuming."),
|
|
3496
|
+
leaderSocket: z
|
|
3497
|
+
.string()
|
|
3498
|
+
.min(1)
|
|
3499
|
+
.optional()
|
|
3500
|
+
.describe("Grok 0.2.32+ --leader-socket <PATH>: custom leader socket path (default ~/.grok/leader.sock). Targets an isolated leader process, e.g. a local/branch Grok build; name it ~/.grok/leader-*.sock to keep `grok leader list/kill` discovery working."),
|
|
3491
3501
|
nativeWorktree: z
|
|
3492
3502
|
.union([z.boolean(), z.string().min(1)])
|
|
3493
3503
|
.optional()
|
|
3494
3504
|
.describe("Grok -w/--worktree: native CLI worktree flag (`true` → bare `--worktree`, string → named). NOT gateway slice λ `worktree`."),
|
|
3495
3505
|
worktree: WORKTREE_SCHEMA.optional(),
|
|
3496
|
-
}, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, maxTurns, workingDir, sandbox, rules, systemPromptOverride, allow, deny, compactionMode, compactionDetail, agent, bestOfN, check, disableWebSearch, todoGate, verbatim, agents, promptFile, promptJson, single, experimentalMemory, noAltScreen, noMemory, noPlan, noSubagents, oauth, restoreCode, nativeWorktree, worktree, }) => {
|
|
3506
|
+
}, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, maxTurns, workingDir, sandbox, rules, systemPromptOverride, allow, deny, compactionMode, compactionDetail, agent, bestOfN, check, disableWebSearch, todoGate, verbatim, agents, promptFile, promptJson, single, experimentalMemory, noAltScreen, noMemory, noPlan, noSubagents, oauth, restoreCode, leaderSocket, nativeWorktree, worktree, }) => {
|
|
3497
3507
|
return handleGrokRequest({ sessionManager, logger, runtime }, {
|
|
3498
3508
|
prompt,
|
|
3499
3509
|
promptParts,
|
|
@@ -3542,6 +3552,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
3542
3552
|
noSubagents,
|
|
3543
3553
|
oauth,
|
|
3544
3554
|
restoreCode,
|
|
3555
|
+
leaderSocket,
|
|
3545
3556
|
nativeWorktree,
|
|
3546
3557
|
worktree,
|
|
3547
3558
|
});
|
|
@@ -4298,12 +4309,17 @@ export function createGatewayServer(deps = {}) {
|
|
|
4298
4309
|
.boolean()
|
|
4299
4310
|
.optional()
|
|
4300
4311
|
.describe("Grok --restore-code: check out the original session commit when resuming."),
|
|
4312
|
+
leaderSocket: z
|
|
4313
|
+
.string()
|
|
4314
|
+
.min(1)
|
|
4315
|
+
.optional()
|
|
4316
|
+
.describe("Grok 0.2.32+ --leader-socket <PATH>: custom leader socket path (default ~/.grok/leader.sock). Targets an isolated leader process, e.g. a local/branch Grok build; name it ~/.grok/leader-*.sock to keep `grok leader list/kill` discovery working."),
|
|
4301
4317
|
nativeWorktree: z
|
|
4302
4318
|
.union([z.boolean(), z.string().min(1)])
|
|
4303
4319
|
.optional()
|
|
4304
4320
|
.describe("Grok -w/--worktree: native CLI worktree flag (`true` → bare `--worktree`, string → named). NOT gateway slice λ `worktree`."),
|
|
4305
4321
|
worktree: WORKTREE_SCHEMA.optional(),
|
|
4306
|
-
}, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, maxTurns, workingDir, sandbox, rules, systemPromptOverride, allow, deny, compactionMode, compactionDetail, agent, bestOfN, check, disableWebSearch, todoGate, verbatim, agents, promptFile, promptJson, single, experimentalMemory, noAltScreen, noMemory, noPlan, noSubagents, oauth, restoreCode, nativeWorktree, worktree, }) => {
|
|
4322
|
+
}, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, maxTurns, workingDir, sandbox, rules, systemPromptOverride, allow, deny, compactionMode, compactionDetail, agent, bestOfN, check, disableWebSearch, todoGate, verbatim, agents, promptFile, promptJson, single, experimentalMemory, noAltScreen, noMemory, noPlan, noSubagents, oauth, restoreCode, leaderSocket, nativeWorktree, worktree, }) => {
|
|
4307
4323
|
return handleGrokRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
|
|
4308
4324
|
prompt,
|
|
4309
4325
|
promptParts,
|
|
@@ -4351,6 +4367,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
4351
4367
|
noSubagents,
|
|
4352
4368
|
oauth,
|
|
4353
4369
|
restoreCode,
|
|
4370
|
+
leaderSocket,
|
|
4354
4371
|
nativeWorktree,
|
|
4355
4372
|
worktree,
|
|
4356
4373
|
});
|
package/dist/job-store.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { chmodSync
|
|
1
|
+
import { chmodSync } from "fs";
|
|
2
2
|
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { createHash } from "crypto";
|
|
5
|
-
import {
|
|
5
|
+
import { openDatabase } from "./sqlite-driver.js";
|
|
6
6
|
import { noopLogger } from "./logger.js";
|
|
7
7
|
export function resolveJobStoreDbPath() {
|
|
8
8
|
const configured = process.env.LLM_GATEWAY_JOBS_DB ?? process.env.LLM_GATEWAY_LOGS_DB;
|
|
@@ -74,13 +74,7 @@ export class SqliteJobStore {
|
|
|
74
74
|
deleteExpiredStmt;
|
|
75
75
|
constructor(dbPath, logger = noopLogger, options = {}) {
|
|
76
76
|
this.logger = logger;
|
|
77
|
-
|
|
78
|
-
const BetterSqlite3 = require("better-sqlite3");
|
|
79
|
-
const directory = path.dirname(dbPath);
|
|
80
|
-
if (!existsSync(directory)) {
|
|
81
|
-
mkdirSync(directory, { recursive: true });
|
|
82
|
-
}
|
|
83
|
-
this.db = new BetterSqlite3(dbPath);
|
|
77
|
+
this.db = openDatabase(dbPath);
|
|
84
78
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
85
79
|
this.db.exec("PRAGMA synchronous = NORMAL");
|
|
86
80
|
this.db.exec(`
|
|
@@ -211,7 +205,7 @@ export class SqliteJobStore {
|
|
|
211
205
|
markOrphanedOnStartup() {
|
|
212
206
|
const now = new Date().toISOString();
|
|
213
207
|
const expiresAt = new Date(Date.now() + this.retentionMs).toISOString();
|
|
214
|
-
const rows =
|
|
208
|
+
const rows = this.selectRunningOrphansStmt.all();
|
|
215
209
|
const orphaned = rows.map(row => ({
|
|
216
210
|
id: row.id,
|
|
217
211
|
correlationId: row.correlation_id,
|
|
@@ -221,12 +215,12 @@ export class SqliteJobStore {
|
|
|
221
215
|
exitCode: row.exit_code,
|
|
222
216
|
}));
|
|
223
217
|
const result = this.markOrphanedStmt.run(now, expiresAt);
|
|
224
|
-
return { count: result
|
|
218
|
+
return { count: Number(result.changes), orphaned };
|
|
225
219
|
}
|
|
226
220
|
evictExpired() {
|
|
227
221
|
const now = new Date().toISOString();
|
|
228
222
|
const result = this.deleteExpiredStmt.run(now);
|
|
229
|
-
return result
|
|
223
|
+
return Number(result.changes);
|
|
230
224
|
}
|
|
231
225
|
close() {
|
|
232
226
|
try {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface GatewayStatement {
|
|
2
|
+
run(...args: unknown[]): {
|
|
3
|
+
changes: number;
|
|
4
|
+
lastInsertRowid: number | bigint;
|
|
5
|
+
};
|
|
6
|
+
get(...args: unknown[]): unknown;
|
|
7
|
+
all(...args: unknown[]): unknown[];
|
|
8
|
+
}
|
|
9
|
+
export interface GatewayDatabase {
|
|
10
|
+
exec(sql: string): void;
|
|
11
|
+
prepare(sql: string): GatewayStatement;
|
|
12
|
+
withTransaction<A extends unknown[]>(fn: (...args: A) => void): (...args: A) => void;
|
|
13
|
+
close(): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function openDatabase(dbPath: string): GatewayDatabase;
|
|
16
|
+
export declare function openReadOnly(dbPath: string): GatewayDatabase;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { createRequire } from "module";
|
|
4
|
+
function loadNodeSqlite() {
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
return require("node:sqlite");
|
|
7
|
+
}
|
|
8
|
+
function wrapStatement(stmt) {
|
|
9
|
+
return {
|
|
10
|
+
run(...args) {
|
|
11
|
+
return stmt.run(...args);
|
|
12
|
+
},
|
|
13
|
+
get(...args) {
|
|
14
|
+
return stmt.get(...args);
|
|
15
|
+
},
|
|
16
|
+
all(...args) {
|
|
17
|
+
return stmt.all(...args);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function statementLeadingKeywords(sql) {
|
|
22
|
+
const keywords = [];
|
|
23
|
+
let i = 0;
|
|
24
|
+
const skipTrivia = () => {
|
|
25
|
+
for (;;) {
|
|
26
|
+
while (i < sql.length && /\s|;/.test(sql[i] ?? ""))
|
|
27
|
+
i++;
|
|
28
|
+
if (sql.startsWith("--", i)) {
|
|
29
|
+
i += 2;
|
|
30
|
+
while (i < sql.length && sql[i] !== "\n")
|
|
31
|
+
i++;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (sql.startsWith("/*", i)) {
|
|
35
|
+
const end = sql.indexOf("*/", i + 2);
|
|
36
|
+
i = end === -1 ? sql.length : end + 2;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const skipQuoted = (quote) => {
|
|
43
|
+
i++;
|
|
44
|
+
while (i < sql.length) {
|
|
45
|
+
if (sql[i] === quote) {
|
|
46
|
+
if (sql[i + 1] === quote) {
|
|
47
|
+
i += 2;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
i++;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
i++;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
while (i < sql.length) {
|
|
57
|
+
skipTrivia();
|
|
58
|
+
const m = /^[a-zA-Z]+/.exec(sql.slice(i));
|
|
59
|
+
if (m) {
|
|
60
|
+
keywords.push(m[0].toUpperCase());
|
|
61
|
+
}
|
|
62
|
+
while (i < sql.length && sql[i] !== ";") {
|
|
63
|
+
if (sql.startsWith("--", i)) {
|
|
64
|
+
i += 2;
|
|
65
|
+
while (i < sql.length && sql[i] !== "\n")
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
else if (sql.startsWith("/*", i)) {
|
|
69
|
+
const end = sql.indexOf("*/", i + 2);
|
|
70
|
+
i = end === -1 ? sql.length : end + 2;
|
|
71
|
+
}
|
|
72
|
+
else if (sql[i] === "'" || sql[i] === '"' || sql[i] === "`") {
|
|
73
|
+
skipQuoted(sql[i]);
|
|
74
|
+
}
|
|
75
|
+
else if (sql[i] === "[") {
|
|
76
|
+
i++;
|
|
77
|
+
while (i < sql.length && sql[i] !== "]")
|
|
78
|
+
i++;
|
|
79
|
+
if (i < sql.length)
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
i++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return keywords;
|
|
88
|
+
}
|
|
89
|
+
class GatewayDatabaseImpl {
|
|
90
|
+
db;
|
|
91
|
+
readOnly;
|
|
92
|
+
inTransaction = false;
|
|
93
|
+
constructor(db, readOnly = false) {
|
|
94
|
+
this.db = db;
|
|
95
|
+
this.readOnly = readOnly;
|
|
96
|
+
}
|
|
97
|
+
guardReadOnly(sql) {
|
|
98
|
+
if (this.readOnly && statementLeadingKeywords(sql).includes("VACUUM")) {
|
|
99
|
+
throw new Error("read-only connection rejects VACUUM (writes to disk despite readOnly)");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exec(sql) {
|
|
103
|
+
this.guardReadOnly(sql);
|
|
104
|
+
this.db.exec(sql);
|
|
105
|
+
}
|
|
106
|
+
prepare(sql) {
|
|
107
|
+
this.guardReadOnly(sql);
|
|
108
|
+
return wrapStatement(this.db.prepare(sql));
|
|
109
|
+
}
|
|
110
|
+
withTransaction(fn) {
|
|
111
|
+
return (...args) => {
|
|
112
|
+
if (this.inTransaction) {
|
|
113
|
+
throw new Error("nested transaction");
|
|
114
|
+
}
|
|
115
|
+
this.db.exec("BEGIN");
|
|
116
|
+
this.inTransaction = true;
|
|
117
|
+
try {
|
|
118
|
+
fn(...args);
|
|
119
|
+
this.db.exec("COMMIT");
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
try {
|
|
123
|
+
this.db.exec("ROLLBACK");
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
this.inTransaction = false;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
close() {
|
|
135
|
+
this.db.close();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export function openDatabase(dbPath) {
|
|
139
|
+
const { DatabaseSync } = loadNodeSqlite();
|
|
140
|
+
const directory = path.dirname(dbPath);
|
|
141
|
+
if (!existsSync(directory)) {
|
|
142
|
+
mkdirSync(directory, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
return new GatewayDatabaseImpl(new DatabaseSync(dbPath));
|
|
145
|
+
}
|
|
146
|
+
export function openReadOnly(dbPath) {
|
|
147
|
+
const { DatabaseSync } = loadNodeSqlite();
|
|
148
|
+
return new GatewayDatabaseImpl(new DatabaseSync(dbPath, { readOnly: true }), true);
|
|
149
|
+
}
|
|
@@ -5,6 +5,7 @@ export interface CliFlagContract {
|
|
|
5
5
|
values?: readonly string[];
|
|
6
6
|
pattern?: RegExp;
|
|
7
7
|
description: string;
|
|
8
|
+
hiddenFromHelp?: boolean;
|
|
8
9
|
}
|
|
9
10
|
export interface CliUpstreamMetadata {
|
|
10
11
|
sourceUrls: readonly string[];
|
|
@@ -32,6 +33,7 @@ export interface CliContract {
|
|
|
32
33
|
resumeMaxPositionals?: number;
|
|
33
34
|
resumeOnlyFlags?: readonly string[];
|
|
34
35
|
resumeForbiddenFlags?: readonly string[];
|
|
36
|
+
acknowledgedUpstreamFlags?: readonly string[];
|
|
35
37
|
upstreamMetadata?: CliUpstreamMetadata;
|
|
36
38
|
}
|
|
37
39
|
export interface CliContractFixture {
|
|
@@ -57,6 +59,13 @@ export declare function assertUpstreamCliArgs(cli: CliType, args: readonly strin
|
|
|
57
59
|
export declare function validateUpstreamCliEnv(cli: CliType, env: Record<string, string> | undefined): ContractValidationResult;
|
|
58
60
|
export declare function assertUpstreamCliEnv(cli: CliType, env: Record<string, string> | undefined): void;
|
|
59
61
|
export declare function extractDiscoveredFlags(helpText: string): readonly string[];
|
|
62
|
+
export interface FlagDriftResult {
|
|
63
|
+
missingFlags: string[];
|
|
64
|
+
extraFlags: readonly string[];
|
|
65
|
+
acknowledgedExtraFlags: readonly string[];
|
|
66
|
+
warnings: string[];
|
|
67
|
+
}
|
|
68
|
+
export declare function computeFlagDrift(contract: CliContract, helpText: string, discoveredFlags: readonly string[]): FlagDriftResult;
|
|
60
69
|
export interface InstalledCliContractProbe {
|
|
61
70
|
cli: CliType;
|
|
62
71
|
executable: string;
|
|
@@ -66,6 +75,7 @@ export interface InstalledCliContractProbe {
|
|
|
66
75
|
checkedHelpCommands: string[][];
|
|
67
76
|
missingFlags: string[];
|
|
68
77
|
extraFlags: readonly string[];
|
|
78
|
+
acknowledgedExtraFlags: readonly string[];
|
|
69
79
|
discoveredFlags: readonly string[];
|
|
70
80
|
helpHash?: string;
|
|
71
81
|
versionHint?: string;
|
|
@@ -99,7 +99,12 @@ export const UPSTREAM_CLI_CONTRACTS = {
|
|
|
99
99
|
pattern: /^[0-9]+(?:\.[0-9]+)?$/,
|
|
100
100
|
description: "Budget cap in USD",
|
|
101
101
|
},
|
|
102
|
-
"--max-turns": {
|
|
102
|
+
"--max-turns": {
|
|
103
|
+
arity: "one",
|
|
104
|
+
pattern: /^[1-9][0-9]*$/,
|
|
105
|
+
description: "Turn cap",
|
|
106
|
+
hiddenFromHelp: true,
|
|
107
|
+
},
|
|
103
108
|
"--effort": { arity: "one", values: EFFORT_LEVELS, description: "Reasoning effort" },
|
|
104
109
|
"--exclude-dynamic-system-prompt-sections": {
|
|
105
110
|
arity: "none",
|
|
@@ -136,6 +141,37 @@ export const UPSTREAM_CLI_CONTRACTS = {
|
|
|
136
141
|
description: 'Restrict the available built-in tool set ("" disables all)',
|
|
137
142
|
},
|
|
138
143
|
},
|
|
144
|
+
acknowledgedUpstreamFlags: [
|
|
145
|
+
"--allow-dangerously-skip-permissions",
|
|
146
|
+
"--allowed",
|
|
147
|
+
"--bare",
|
|
148
|
+
"--betas",
|
|
149
|
+
"--brief",
|
|
150
|
+
"--chrome",
|
|
151
|
+
"--dangerously-skip-permissions",
|
|
152
|
+
"--debug",
|
|
153
|
+
"--debug-file",
|
|
154
|
+
"--disable-slash-commands",
|
|
155
|
+
"--disallowed",
|
|
156
|
+
"--file",
|
|
157
|
+
"--from-pr",
|
|
158
|
+
"--ide",
|
|
159
|
+
"--include-hook-events",
|
|
160
|
+
"--mcp-debug",
|
|
161
|
+
"--name",
|
|
162
|
+
"--no-chrome",
|
|
163
|
+
"--plugin-dir",
|
|
164
|
+
"--plugin-url",
|
|
165
|
+
"--print",
|
|
166
|
+
"--prompt-suggestions",
|
|
167
|
+
"--remote-control",
|
|
168
|
+
"--remote-control-session-name-prefix",
|
|
169
|
+
"--replay-user-messages",
|
|
170
|
+
"--resume",
|
|
171
|
+
"--tmux",
|
|
172
|
+
"--version",
|
|
173
|
+
"--worktree",
|
|
174
|
+
],
|
|
139
175
|
env: {},
|
|
140
176
|
conformanceFixtures: [
|
|
141
177
|
{
|
|
@@ -518,6 +554,26 @@ export const UPSTREAM_CLI_CONTRACTS = {
|
|
|
518
554
|
description: "Auto-approve all actions (gemini -y/--yolo). Functionally equivalent to --approval-mode yolo; the gateway emits at most one of the two.",
|
|
519
555
|
},
|
|
520
556
|
},
|
|
557
|
+
acknowledgedUpstreamFlags: [
|
|
558
|
+
"--accept-raw-output-risk",
|
|
559
|
+
"--acp",
|
|
560
|
+
"--debug",
|
|
561
|
+
"--delete-session",
|
|
562
|
+
"--experimental-acp",
|
|
563
|
+
"--extensions",
|
|
564
|
+
"--list-extensions",
|
|
565
|
+
"--list-sessions",
|
|
566
|
+
"--output-format",
|
|
567
|
+
"--prompt",
|
|
568
|
+
"--prompt-interactive",
|
|
569
|
+
"--raw-output",
|
|
570
|
+
"--sandbox",
|
|
571
|
+
"--screen-reader",
|
|
572
|
+
"--session-file",
|
|
573
|
+
"--session-id",
|
|
574
|
+
"--version",
|
|
575
|
+
"--worktree",
|
|
576
|
+
],
|
|
521
577
|
env: {},
|
|
522
578
|
conformanceFixtures: [
|
|
523
579
|
{
|
|
@@ -612,6 +668,7 @@ export const UPSTREAM_CLI_CONTRACTS = {
|
|
|
612
668
|
"noSubagents",
|
|
613
669
|
"oauth",
|
|
614
670
|
"restoreCode",
|
|
671
|
+
"leaderSocket",
|
|
615
672
|
"nativeWorktree",
|
|
616
673
|
],
|
|
617
674
|
flags: {
|
|
@@ -693,6 +750,10 @@ export const UPSTREAM_CLI_CONTRACTS = {
|
|
|
693
750
|
arity: "none",
|
|
694
751
|
description: "Check out the original session commit when resuming",
|
|
695
752
|
},
|
|
753
|
+
"--leader-socket": {
|
|
754
|
+
arity: "one",
|
|
755
|
+
description: "Custom leader socket path (isolated leader, Grok 0.2.32+)",
|
|
756
|
+
},
|
|
696
757
|
"--single": { arity: "one", description: "Single-turn prompt" },
|
|
697
758
|
"--todo-gate": { arity: "none", description: "Enable runtime turn-end TodoGate" },
|
|
698
759
|
"--verbatim": { arity: "none", description: "Send prompt exactly as given" },
|
|
@@ -843,6 +904,18 @@ export const UPSTREAM_CLI_CONTRACTS = {
|
|
|
843
904
|
],
|
|
844
905
|
expect: "pass",
|
|
845
906
|
},
|
|
907
|
+
{
|
|
908
|
+
id: "grok-leader-socket",
|
|
909
|
+
description: "Grok 0.2.32: --leader-socket <PATH> is accepted",
|
|
910
|
+
args: ["-p", "hello", "--leader-socket", "/home/user/.grok/leader-branch.sock"],
|
|
911
|
+
expect: "pass",
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
id: "grok-leader-socket-missing-value",
|
|
915
|
+
description: "Grok 0.2.32: --leader-socket without a path is rejected (arity one)",
|
|
916
|
+
args: ["-p", "hello", "--leader-socket"],
|
|
917
|
+
expect: "fail",
|
|
918
|
+
},
|
|
846
919
|
],
|
|
847
920
|
},
|
|
848
921
|
mistral: {
|
|
@@ -1220,6 +1293,42 @@ export function extractDiscoveredFlags(helpText) {
|
|
|
1220
1293
|
}
|
|
1221
1294
|
return Array.from(discovered).sort();
|
|
1222
1295
|
}
|
|
1296
|
+
export function computeFlagDrift(contract, helpText, discoveredFlags) {
|
|
1297
|
+
const warnings = [];
|
|
1298
|
+
const missingFlags = [];
|
|
1299
|
+
for (const [flag, spec] of Object.entries(contract.flags)) {
|
|
1300
|
+
const inHelp = helpText.includes(flag);
|
|
1301
|
+
if (spec.hiddenFromHelp) {
|
|
1302
|
+
if (inHelp) {
|
|
1303
|
+
warnings.push(`${flag} is marked hiddenFromHelp but now appears in ${contract.executable} help output; remove the hiddenFromHelp marker from the contract`);
|
|
1304
|
+
}
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
if (!inHelp)
|
|
1308
|
+
missingFlags.push(flag);
|
|
1309
|
+
}
|
|
1310
|
+
const contractFlagSet = new Set(Object.keys(contract.flags));
|
|
1311
|
+
const acknowledged = new Set(contract.acknowledgedUpstreamFlags ?? []);
|
|
1312
|
+
const extraFlags = [];
|
|
1313
|
+
const acknowledgedExtraFlags = [];
|
|
1314
|
+
for (const flag of discoveredFlags) {
|
|
1315
|
+
if (contractFlagSet.has(flag))
|
|
1316
|
+
continue;
|
|
1317
|
+
if (acknowledged.has(flag)) {
|
|
1318
|
+
acknowledgedExtraFlags.push(flag);
|
|
1319
|
+
}
|
|
1320
|
+
else {
|
|
1321
|
+
extraFlags.push(flag);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
const discoveredSet = new Set(discoveredFlags);
|
|
1325
|
+
for (const flag of acknowledged) {
|
|
1326
|
+
if (!discoveredSet.has(flag)) {
|
|
1327
|
+
warnings.push(`acknowledged upstream flag ${flag} no longer appears in ${contract.executable} help output; remove it from acknowledgedUpstreamFlags`);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return { missingFlags, extraFlags, acknowledgedExtraFlags, warnings };
|
|
1331
|
+
}
|
|
1223
1332
|
export function probeInstalledCliContract(cli, timeoutMs = 5_000) {
|
|
1224
1333
|
const contract = UPSTREAM_CLI_CONTRACTS[cli];
|
|
1225
1334
|
const outputs = [];
|
|
@@ -1252,6 +1361,7 @@ export function probeInstalledCliContract(cli, timeoutMs = 5_000) {
|
|
|
1252
1361
|
checkedHelpCommands: contract.helpArgs,
|
|
1253
1362
|
missingFlags: [],
|
|
1254
1363
|
extraFlags: [],
|
|
1364
|
+
acknowledgedExtraFlags: [],
|
|
1255
1365
|
discoveredFlags: [],
|
|
1256
1366
|
helpHash: undefined,
|
|
1257
1367
|
versionHint: undefined,
|
|
@@ -1265,10 +1375,9 @@ export function probeInstalledCliContract(cli, timeoutMs = 5_000) {
|
|
|
1265
1375
|
}
|
|
1266
1376
|
}
|
|
1267
1377
|
const helpText = outputs.join("\n");
|
|
1268
|
-
const missingFlags = Object.keys(contract.flags).filter(flag => !helpText.includes(flag));
|
|
1269
1378
|
const discoveredFlags = extractDiscoveredFlags(helpText);
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1379
|
+
const drift = computeFlagDrift(contract, helpText, discoveredFlags);
|
|
1380
|
+
warnings.push(...drift.warnings);
|
|
1272
1381
|
const versionMatch = helpText.match(/^\s*(?:[A-Za-z][\w .-]+)?v?\d+\.\d+\S*/m);
|
|
1273
1382
|
const versionHint = versionMatch ? versionMatch[0].trim().slice(0, 80) : undefined;
|
|
1274
1383
|
const helpHash = createHash("sha256").update(helpText).digest("hex");
|
|
@@ -1279,8 +1388,9 @@ export function probeInstalledCliContract(cli, timeoutMs = 5_000) {
|
|
|
1279
1388
|
resolvedArgs,
|
|
1280
1389
|
available: true,
|
|
1281
1390
|
checkedHelpCommands: contract.helpArgs,
|
|
1282
|
-
missingFlags,
|
|
1283
|
-
extraFlags,
|
|
1391
|
+
missingFlags: drift.missingFlags,
|
|
1392
|
+
extraFlags: drift.extraFlags,
|
|
1393
|
+
acknowledgedExtraFlags: drift.acknowledgedExtraFlags,
|
|
1284
1394
|
discoveredFlags,
|
|
1285
1395
|
helpHash,
|
|
1286
1396
|
versionHint,
|