llm-cli-gateway 1.11.0 → 1.12.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 CHANGED
@@ -2,6 +2,120 @@
2
2
 
3
3
  All notable changes to the llm-cli-gateway project.
4
4
 
5
+ ## [1.12.0] - 2026-05-27 — Phase 4 slice ζ (working-dir + add-dir cross-provider)
6
+
7
+ Ships the seventh Phase 4 slice: working-directory and additional-directory
8
+ flags are now reachable across four CLIs in a single bundled PR. Three
9
+ commits land together (feature wiring, contract registration, test-veracity
10
+ regressions) plus this release commit.
11
+
12
+ ### Added — working-dir + add-dir parity for four CLIs
13
+
14
+ - **Claude** — `claude_request` and `claude_request_async` accept a new
15
+ `addDir: string[]` field. Threaded through `prepareClaudeRequest` →
16
+ `prepareClaudeHighImpactFlags` (`src/request-helpers.ts:687`). Each
17
+ entry emits its own `--add-dir` instance per `claude --help` ("Additional
18
+ directories to allow tool access to"). Claude has no working-dir flag
19
+ (uses the process cwd).
20
+ - **Codex** — `codex_request` and `codex_request_async` accept new
21
+ `workingDir: string` (min 1) and `addDir: string[]` fields. Both flags
22
+ are already in `CODEX_RESUME_FILTERED_FLAGS` (the original session's cwd
23
+ and writable-dir policy are inherited on resume), so `prepareCodexRequest`
24
+ gates emission on `sessionPlan.mode === "new"` — resume argv stays clean
25
+ rather than emitting then stripping. Emits `-C <DIR>` (one) and
26
+ `--add-dir <DIR>` (one instance per entry).
27
+ - **Grok** — `grok_request` and `grok_request_async` accept a new
28
+ `workingDir: string` (min 1) field. `prepareGrokRequest` emits
29
+ `--cwd <DIR>`. Grok has no `--add-dir` analogue.
30
+ - **Vibe (Mistral)** — `mistral_request` and `mistral_request_async`
31
+ accept new `workingDir: string` (min 1) and `addDir: string[]` fields.
32
+ `prepareMistralRequest` (the `request-helpers.ts` helper) emits
33
+ `--workdir <DIR>` (one) and `--add-dir <DIR>` (one per entry; Vibe's
34
+ `--help` states the flag "Can be specified multiple times").
35
+ `buildMistralRetryPrep` threads both fields through to the stale-model
36
+ recovery argv per the slice-δ retry-path invariant.
37
+ - **Gemini** is not re-wired: `--include-directories` was wired in master
38
+ before this slice. A regression-guard test in REGRESSIONS Zε asserts
39
+ the existing wiring stays intact while adjacent contract entries
40
+ changed.
41
+
42
+ ### Out of scope — worktree flags
43
+
44
+ Worktree flags (`-w/--worktree` on Claude, Gemini, Grok) create new git
45
+ worktree directories on disk with lifecycle implications and are
46
+ explicitly deferred to a later slice with explicit cleanup semantics.
47
+
48
+ ### Contract surface
49
+
50
+ `UPSTREAM_CLI_CONTRACTS` updates:
51
+
52
+ - `claude.flags["--add-dir"]` (arity:"one"; repeated instances accepted)
53
+ - `codex.flags["-C"]` (the gateway only emits the short form; codex
54
+ 0.134.0 accepts `--cd` as an alias but the contract registers exactly
55
+ what we emit — a future code path that emitted `--cd` would correctly
56
+ fail the contract check).
57
+ - `codex.flags["--add-dir"]`
58
+ - `grok.flags["--cwd"]`
59
+ - `mistral.flags["--workdir"]`
60
+ - `mistral.flags["--add-dir"]`
61
+ - `mcpParameters` arrays updated for all four CLIs.
62
+ - Six new passing conformance fixtures (`claude-add-dir`,
63
+ `codex-working-dir`, `codex-add-dir`, `grok-working-dir`,
64
+ `mistral-working-dir`, `mistral-add-dir`); each is mechanically
65
+ validated against `validateUpstreamCliArgs` in the REGRESSIONS Zε
66
+ suite, closing the gap class identified in slice ε round 1.
67
+
68
+ ### Test-veracity audit
69
+
70
+ Per the standing protocol (`feedback_test_veracity_audit_protocol`),
71
+ this slice's tests were audited by all five LLM reviewers (Codex,
72
+ Gemini, Grok, Mistral, Claude) in async parallel with mandatory
73
+ mutation-probe execution against `docs/plans/test-veracity-audit-slice-zeta.spec.md`.
74
+
75
+ **Round 1 outcomes:**
76
+
77
+ - Codex: UNCONDITIONAL APPROVE — all 13 probes [as predicted], all 37
78
+ tests VERIFIED. Baseline (`npx vitest run` on the slice file: 37/37;
79
+ `npm test`: 54 files / 853 tests; build + format:check clean).
80
+ - Grok: UNCONDITIONAL APPROVE — all 13 probes [as predicted].
81
+ - Mistral: UNCONDITIONAL APPROVE — all 13 probes [as predicted].
82
+ - Claude: UNCONDITIONAL APPROVE — all 13 probes red as predicted; ran
83
+ in an isolated `/tmp/zeta-audit-claude` worktree because the four
84
+ parallel reviewers were concurrently mutating the live tree.
85
+ - Gemini: UNCONDITIONAL APPROVE — all 13 probes [as predicted].
86
+
87
+ First unanimous round-1 pass on a multi-CLI slice. The 37 new tests
88
+ (816 → 853 total) cover every new field/flag/fixture across REGRESSIONS
89
+ Zα/β/ε:
90
+
91
+ - **Zα** — Registered tool inputSchema for every new field on every
92
+ tool (sync + async), including `.min(1)` empty-string rejection on
93
+ `workingDir`.
94
+ - **Zβ** — `prepare*Request` end-to-end argv emission per CLI. The
95
+ Codex resume branch asserts NEITHER `-C` NOR `--add-dir` appears
96
+ in resume argv. `buildMistralRetryPrep` regression catches the
97
+ slice-δ retry-path bug class. Prepare → contract end-to-end
98
+ consistency covers all four CLIs.
99
+ - **Zε** — `UPSTREAM_CLI_CONTRACTS` introspection + mechanical
100
+ fixture validation in the same `it()` block (slice-ε round-1 gap
101
+ class). Includes a regression guard for the pre-existing Gemini
102
+ `--include-directories` wiring.
103
+
104
+ ### Mechanical anchors (verify with `rg` before relying)
105
+
106
+ - `src/request-helpers.ts` — `ClaudeHighImpactFlagsInput.addDir`
107
+ (`:610`), `prepareClaudeHighImpactFlags` emission (`:686-690`).
108
+ `PrepareMistralRequestInput.workingDir`/`.addDir` (`:248-264`),
109
+ `prepareMistralRequest` emission (`:300-307`).
110
+ - `src/index.ts` — `prepareClaudeRequest` (`:1338`),
111
+ `prepareCodexRequest` new-session gate (`:1687-1700`),
112
+ `prepareGrokRequest` `--cwd` emission (`:2065-2067`),
113
+ `prepareMistralRequest` wrapper (`:2153-2168`),
114
+ `buildMistralRetryPrep` (`:2249-2289`).
115
+ - `src/upstream-contracts.ts` — flag registrations and conformance
116
+ fixtures for the four CLIs (`:146-149`, `:281-292`, `:438-441`,
117
+ `:524-533`, plus `mcpParameters` entries).
118
+
5
119
  ## [1.11.0] - 2026-05-27 — Phase 4 slice η (Claude `--fallback-model` + `--json-schema`)
6
120
 
7
121
  Ships the sixth Phase 4 slice: Claude's reliability fallback and
package/dist/index.d.ts CHANGED
@@ -157,6 +157,7 @@ export declare function prepareClaudeRequest(params: {
157
157
  excludeDynamicSystemPromptSections?: boolean;
158
158
  fallbackModel?: string;
159
159
  jsonSchema?: string | Record<string, unknown>;
160
+ addDir?: string[];
160
161
  }, runtime?: GatewayServerRuntime): CliRequestPrep | ExtendedToolResponse;
161
162
  export interface CodexRequestPrep extends CliRequestPrep {
162
163
  /**
@@ -199,6 +200,8 @@ export declare function prepareCodexRequest(params: {
199
200
  images?: string[];
200
201
  ignoreUserConfig?: boolean;
201
202
  ignoreRules?: boolean;
203
+ workingDir?: string;
204
+ addDir?: string[];
202
205
  }, runtime?: GatewayServerRuntime): CodexRequestPrep | ExtendedToolResponse;
203
206
  export declare function prepareGeminiRequest(params: {
204
207
  prompt?: string;
@@ -254,6 +257,11 @@ export declare function prepareGrokRequest(params: {
254
257
  * iterations for cost / latency control. Mirrors Claude's wiring.
255
258
  */
256
259
  maxTurns?: number;
260
+ /**
261
+ * Phase 4 slice ζ: emit `--cwd <DIR>` so headless callers can set Grok's
262
+ * working directory without depending on the gateway process's cwd.
263
+ */
264
+ workingDir?: string;
257
265
  }, runtime?: GatewayServerRuntime): CliRequestPrep | ExtendedToolResponse;
258
266
  export declare function prepareMistralRequest(params: {
259
267
  prompt?: string;
@@ -280,6 +288,10 @@ export declare function prepareMistralRequest(params: {
280
288
  maxTurns?: number;
281
289
  /** Phase 4 slice δ: Vibe `--max-price DOLLARS` cumulative-cost cap. */
282
290
  maxPrice?: number;
291
+ /** Phase 4 slice ζ: Vibe `--workdir <DIR>` working-directory parity. */
292
+ workingDir?: string;
293
+ /** Phase 4 slice ζ: Vibe `--add-dir <DIR>` repeatable add-dir parity. */
294
+ addDir?: string[];
283
295
  }, runtime?: GatewayServerRuntime): (CliRequestPrep & {
284
296
  mistralEnv: Record<string, string>;
285
297
  }) | ExtendedToolResponse;
@@ -292,7 +304,7 @@ export declare function prepareMistralRequest(params: {
292
304
  * through here, or a fresh-workspace / budgeted run can degrade on
293
305
  * the second attempt.
294
306
  */
295
- export declare function buildMistralRetryPrep(params: Pick<MistralRequestParams, "outputFormat" | "permissionMode" | "effort" | "reasoningEffort" | "allowedTools" | "disallowedTools" | "approvalStrategy" | "trust" | "maxTurns" | "maxPrice"> & {
307
+ export declare function buildMistralRetryPrep(params: Pick<MistralRequestParams, "outputFormat" | "permissionMode" | "effort" | "reasoningEffort" | "allowedTools" | "disallowedTools" | "approvalStrategy" | "trust" | "maxTurns" | "maxPrice" | "workingDir" | "addDir"> & {
296
308
  effectivePrompt: string;
297
309
  }, recoveryModel: string): {
298
310
  args: string[];
@@ -368,6 +380,8 @@ export interface GrokRequestParams {
368
380
  forceRefresh?: boolean;
369
381
  /** Phase 4 slice δ: cap agent-loop iterations via `--max-turns N`. */
370
382
  maxTurns?: number;
383
+ /** Phase 4 slice ζ: emit `--cwd <DIR>` so the CLI uses the specified working directory. */
384
+ workingDir?: string;
371
385
  }
372
386
  export declare function handleGrokRequest(deps: HandlerDeps, params: GrokRequestParams): Promise<ExtendedToolResponse>;
373
387
  export declare function handleGrokRequestAsync(deps: AsyncHandlerDeps, params: Omit<GrokRequestParams, "optimizeResponse">): Promise<ExtendedToolResponse>;
@@ -398,6 +412,10 @@ export interface MistralRequestParams {
398
412
  maxTurns?: number;
399
413
  /** Phase 4 slice δ: Vibe `--max-price DOLLARS` cumulative-cost cap. */
400
414
  maxPrice?: number;
415
+ /** Phase 4 slice ζ: Vibe `--workdir <DIR>` working-directory parity. */
416
+ workingDir?: string;
417
+ /** Phase 4 slice ζ: Vibe `--add-dir <DIR>` repeatable add-dir parity. */
418
+ addDir?: string[];
401
419
  }
402
420
  export declare function handleMistralRequest(deps: HandlerDeps, params: MistralRequestParams): Promise<ExtendedToolResponse>;
403
421
  export declare function handleMistralRequestAsync(deps: AsyncHandlerDeps, params: Omit<MistralRequestParams, "optimizeResponse">): Promise<ExtendedToolResponse>;
@@ -430,6 +448,8 @@ export declare function handleCodexRequestAsync(deps: AsyncHandlerDeps, params:
430
448
  images?: string[];
431
449
  ignoreUserConfig?: boolean;
432
450
  ignoreRules?: boolean;
451
+ workingDir?: string;
452
+ addDir?: string[];
433
453
  }): Promise<ExtendedToolResponse>;
434
454
  export declare function createGatewayServer(deps?: GatewayServerDeps): McpServer;
435
455
  export {};
package/dist/index.js CHANGED
@@ -1007,6 +1007,7 @@ export function prepareClaudeRequest(params, runtime = resolveGatewayServerRunti
1007
1007
  excludeDynamicSystemPromptSections: params.excludeDynamicSystemPromptSections,
1008
1008
  fallbackModel: params.fallbackModel,
1009
1009
  jsonSchema: params.jsonSchema,
1010
+ addDir: params.addDir,
1010
1011
  }));
1011
1012
  return {
1012
1013
  corrId,
@@ -1126,6 +1127,19 @@ export function prepareCodexRequest(params, runtime = resolveGatewayServerRuntim
1126
1127
  // and are emitted in both branches.
1127
1128
  let highImpactCleanup;
1128
1129
  if (sessionPlan.mode === "new") {
1130
+ // Phase 4 slice ζ: emit working-dir and add-dir on new sessions only.
1131
+ // Both flags are listed in CODEX_RESUME_FILTERED_FLAGS — resume inherits
1132
+ // the original session's cwd and writable-dir policy, so emitting them
1133
+ // on resume would be silently stripped (wasteful + misleading on argv
1134
+ // logs). Gating here mirrors `--search` / `--sandbox` / `--full-auto`.
1135
+ if (params.workingDir) {
1136
+ args.push("-C", params.workingDir);
1137
+ }
1138
+ if (params.addDir && params.addDir.length > 0) {
1139
+ for (const dir of params.addDir) {
1140
+ args.push("--add-dir", dir);
1141
+ }
1142
+ }
1129
1143
  const high = prepareCodexHighImpactFlags({
1130
1144
  outputSchema: params.outputSchema,
1131
1145
  search: params.search,
@@ -1381,6 +1395,9 @@ export function prepareGrokRequest(params, runtime = resolveGatewayServerRuntime
1381
1395
  if (params.maxTurns !== undefined) {
1382
1396
  args.push("--max-turns", String(params.maxTurns));
1383
1397
  }
1398
+ if (params.workingDir) {
1399
+ args.push("--cwd", params.workingDir);
1400
+ }
1384
1401
  return {
1385
1402
  corrId,
1386
1403
  effectivePrompt,
@@ -1467,6 +1484,8 @@ export function prepareMistralRequest(params, runtime = resolveGatewayServerRunt
1467
1484
  trust: params.trust,
1468
1485
  maxTurns: params.maxTurns,
1469
1486
  maxPrice: params.maxPrice,
1487
+ workingDir: params.workingDir,
1488
+ addDir: params.addDir,
1470
1489
  });
1471
1490
  if (prep.ignoredDisallowedTools) {
1472
1491
  runtime.logger.info(`[${corrId}] Mistral does not support disallowedTools; ignoring (caller passed ${params.disallowedTools?.length ?? 0} entries)`);
@@ -1521,6 +1540,8 @@ export function buildMistralRetryPrep(params, recoveryModel) {
1521
1540
  trust: params.trust,
1522
1541
  maxTurns: params.maxTurns,
1523
1542
  maxPrice: params.maxPrice,
1543
+ workingDir: params.workingDir,
1544
+ addDir: params.addDir,
1524
1545
  });
1525
1546
  }
1526
1547
  function buildCliResponse(cli, stdout, optimizeResponse, corrId, sessionId, prep, durationMs, resumable, outputFormat, warnings) {
@@ -1862,6 +1883,7 @@ export async function handleGrokRequest(deps, params) {
1862
1883
  optimizePrompt: params.optimizePrompt,
1863
1884
  operation: "grok_request",
1864
1885
  maxTurns: params.maxTurns,
1886
+ workingDir: params.workingDir,
1865
1887
  }, runtime);
1866
1888
  if (!("args" in prep))
1867
1889
  return prep;
@@ -1983,6 +2005,7 @@ export async function handleGrokRequestAsync(deps, params) {
1983
2005
  optimizePrompt: params.optimizePrompt,
1984
2006
  operation: "grok_request_async",
1985
2007
  maxTurns: params.maxTurns,
2008
+ workingDir: params.workingDir,
1986
2009
  }, runtime);
1987
2010
  if (!("args" in prep))
1988
2011
  return prep;
@@ -2067,6 +2090,8 @@ export async function handleMistralRequest(deps, params) {
2067
2090
  trust: params.trust,
2068
2091
  maxTurns: params.maxTurns,
2069
2092
  maxPrice: params.maxPrice,
2093
+ workingDir: params.workingDir,
2094
+ addDir: params.addDir,
2070
2095
  }, runtime);
2071
2096
  if (!("args" in prep))
2072
2097
  return prep;
@@ -2202,6 +2227,8 @@ export async function handleMistralRequestAsync(deps, params) {
2202
2227
  trust: params.trust,
2203
2228
  maxTurns: params.maxTurns,
2204
2229
  maxPrice: params.maxPrice,
2230
+ workingDir: params.workingDir,
2231
+ addDir: params.addDir,
2205
2232
  }, runtime);
2206
2233
  if (!("args" in prep))
2207
2234
  return prep;
@@ -2290,6 +2317,8 @@ export async function handleCodexRequestAsync(deps, params) {
2290
2317
  images: params.images,
2291
2318
  ignoreUserConfig: params.ignoreUserConfig,
2292
2319
  ignoreRules: params.ignoreRules,
2320
+ workingDir: params.workingDir,
2321
+ addDir: params.addDir,
2293
2322
  }, runtime);
2294
2323
  if (!("args" in prep))
2295
2324
  return prep;
@@ -2493,6 +2522,11 @@ export function createGatewayServer(deps = {}) {
2493
2522
  .union([z.string(), z.record(z.unknown())])
2494
2523
  .optional()
2495
2524
  .describe("Claude --json-schema: JSON Schema literal (NOT a path) constraining structured output. Object values are JSON.stringify-d; string values are passed verbatim. Use with outputFormat='json'."),
2525
+ // Phase 4 slice ζ — Claude additional-workspace-dirs parity
2526
+ addDir: z
2527
+ .array(z.string())
2528
+ .optional()
2529
+ .describe("Claude --add-dir: additional directories the CLI is allowed to read/write beyond the process cwd. Each entry is emitted as its own --add-dir instance."),
2496
2530
  approvalStrategy: z
2497
2531
  .enum(["legacy", "mcp_managed"])
2498
2532
  .default("legacy")
@@ -2523,7 +2557,7 @@ export function createGatewayServer(deps = {}) {
2523
2557
  .boolean()
2524
2558
  .default(false)
2525
2559
  .describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
2526
- }, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, }) => {
2560
+ }, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, addDir, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, }) => {
2527
2561
  const startTime = Date.now();
2528
2562
  if (systemPrompt !== undefined && appendSystemPrompt !== undefined) {
2529
2563
  return createErrorResponse("claude", 1, "", correlationId, new Error("systemPrompt and appendSystemPrompt are mutually exclusive; use one or the other (not both)."));
@@ -2555,6 +2589,7 @@ export function createGatewayServer(deps = {}) {
2555
2589
  excludeDynamicSystemPromptSections,
2556
2590
  fallbackModel,
2557
2591
  jsonSchema,
2592
+ addDir,
2558
2593
  }, runtime);
2559
2594
  if (!("args" in prep))
2560
2595
  return prep;
@@ -2809,7 +2844,17 @@ export function createGatewayServer(deps = {}) {
2809
2844
  .boolean()
2810
2845
  .optional()
2811
2846
  .describe("Codex --ignore-rules: skip project rule files for this run."),
2812
- }, async ({ prompt, promptParts, model, fullAuto, sandboxMode, askForApproval, useLegacyFullAutoFlag, dangerouslyBypassApprovalsAndSandbox, approvalStrategy, approvalPolicy, mcpServers, sessionId, resumeLatest, createNewSession, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, outputFormat, outputSchema, search, profile, configOverrides, ephemeral, images, ignoreUserConfig, ignoreRules, }) => {
2847
+ // Phase 4 slice ζ Codex working-dir + add-dir parity (new sessions only).
2848
+ workingDir: z
2849
+ .string()
2850
+ .min(1)
2851
+ .optional()
2852
+ .describe("Codex -C/--cd <DIR>: working root for this session. Emitted on new sessions only; resume inherits the original session's cwd via CODEX_RESUME_FILTERED_FLAGS."),
2853
+ addDir: z
2854
+ .array(z.string())
2855
+ .optional()
2856
+ .describe("Codex --add-dir <DIR>: additional writable workspace directories. Emitted once per entry on new sessions only; resume inherits the original session's writable-dir policy."),
2857
+ }, async ({ prompt, promptParts, model, fullAuto, sandboxMode, askForApproval, useLegacyFullAutoFlag, dangerouslyBypassApprovalsAndSandbox, approvalStrategy, approvalPolicy, mcpServers, sessionId, resumeLatest, createNewSession, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, outputFormat, outputSchema, search, profile, configOverrides, ephemeral, images, ignoreUserConfig, ignoreRules, workingDir, addDir, }) => {
2813
2858
  const startTime = Date.now();
2814
2859
  const prep = prepareCodexRequest({
2815
2860
  prompt,
@@ -2838,6 +2883,8 @@ export function createGatewayServer(deps = {}) {
2838
2883
  images,
2839
2884
  ignoreUserConfig,
2840
2885
  ignoreRules,
2886
+ workingDir,
2887
+ addDir,
2841
2888
  }, runtime);
2842
2889
  if (!("args" in prep))
2843
2890
  return prep;
@@ -3209,7 +3256,13 @@ export function createGatewayServer(deps = {}) {
3209
3256
  .default(false)
3210
3257
  .describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
3211
3258
  maxTurns: MAX_TURNS_SCHEMA.optional().describe("Grok `--max-turns N`: cap on agent-loop iterations for cost / latency control (Phase 4 slice δ). Bounded to safe integers ≤ 10000."),
3212
- }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, maxTurns, }) => {
3259
+ // Phase 4 slice ζ Grok working-directory parity.
3260
+ workingDir: z
3261
+ .string()
3262
+ .min(1)
3263
+ .optional()
3264
+ .describe("Grok --cwd <DIR>: working directory for this invocation. Lets headless callers run Grok against a directory other than the gateway process's cwd."),
3265
+ }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, maxTurns, workingDir, }) => {
3213
3266
  return handleGrokRequest({ sessionManager, logger, runtime }, {
3214
3267
  prompt,
3215
3268
  promptParts,
@@ -3233,6 +3286,7 @@ export function createGatewayServer(deps = {}) {
3233
3286
  idleTimeoutMs,
3234
3287
  forceRefresh,
3235
3288
  maxTurns,
3289
+ workingDir,
3236
3290
  });
3237
3291
  });
3238
3292
  //──────────────────────────────────────────────────────────────────────────────
@@ -3312,7 +3366,17 @@ export function createGatewayServer(deps = {}) {
3312
3366
  .describe("Emit `--trust` so Vibe trusts the cwd for this invocation only (not persisted to trusted_folders.toml) and skips the interactive trust prompt (Phase 4 slice γ)."),
3313
3367
  maxTurns: MAX_TURNS_SCHEMA.optional().describe("Vibe `--max-turns N`: cap the agent-loop iteration count (programmatic mode only, Phase 4 slice δ). Bounded to safe integers ≤ 10000."),
3314
3368
  maxPrice: MAX_PRICE_SCHEMA.optional().describe("Vibe `--max-price DOLLARS`: interrupt the session when cumulative cost crosses this cap (programmatic mode only, Phase 4 slice δ). Bounded to finite values ≤ 10000 USD."),
3315
- }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, trust, maxTurns, maxPrice, }) => {
3369
+ // Phase 4 slice ζ Vibe working-directory + additional-dirs parity.
3370
+ workingDir: z
3371
+ .string()
3372
+ .min(1)
3373
+ .optional()
3374
+ .describe("Vibe --workdir <DIR>: change to this directory before running. Single value (Vibe accepts one --workdir per invocation)."),
3375
+ addDir: z
3376
+ .array(z.string())
3377
+ .optional()
3378
+ .describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance (Vibe states this flag may be specified multiple times)."),
3379
+ }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, trust, maxTurns, maxPrice, workingDir, addDir, }) => {
3316
3380
  return handleMistralRequest({ sessionManager, logger, runtime }, {
3317
3381
  prompt,
3318
3382
  promptParts,
@@ -3337,6 +3401,8 @@ export function createGatewayServer(deps = {}) {
3337
3401
  trust,
3338
3402
  maxTurns,
3339
3403
  maxPrice,
3404
+ workingDir,
3405
+ addDir,
3340
3406
  });
3341
3407
  });
3342
3408
  //──────────────────────────────────────────────────────────────────────────────
@@ -3432,6 +3498,11 @@ export function createGatewayServer(deps = {}) {
3432
3498
  .union([z.string(), z.record(z.unknown())])
3433
3499
  .optional()
3434
3500
  .describe("Claude --json-schema: JSON Schema literal (NOT a path) constraining structured output. Object values are JSON.stringify-d; string values are passed verbatim. Use with outputFormat='json'."),
3501
+ // Phase 4 slice ζ — Claude additional-workspace-dirs parity
3502
+ addDir: z
3503
+ .array(z.string())
3504
+ .optional()
3505
+ .describe("Claude --add-dir: additional directories the CLI is allowed to read/write beyond the process cwd. Each entry is emitted as its own --add-dir instance."),
3435
3506
  approvalStrategy: z
3436
3507
  .enum(["legacy", "mcp_managed"])
3437
3508
  .default("legacy")
@@ -3461,7 +3532,7 @@ export function createGatewayServer(deps = {}) {
3461
3532
  .boolean()
3462
3533
  .default(false)
3463
3534
  .describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
3464
- }, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, }) => {
3535
+ }, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, addDir, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, }) => {
3465
3536
  if (systemPrompt !== undefined && appendSystemPrompt !== undefined) {
3466
3537
  return createErrorResponse("claude", 1, "", correlationId, new Error("systemPrompt and appendSystemPrompt are mutually exclusive; use one or the other (not both)."));
3467
3538
  }
@@ -3492,6 +3563,7 @@ export function createGatewayServer(deps = {}) {
3492
3563
  excludeDynamicSystemPromptSections,
3493
3564
  fallbackModel,
3494
3565
  jsonSchema,
3566
+ addDir,
3495
3567
  }, runtime);
3496
3568
  if (!("args" in prep))
3497
3569
  return prep;
@@ -3646,7 +3718,17 @@ export function createGatewayServer(deps = {}) {
3646
3718
  images: z.array(z.string()).optional().describe("Codex -i <path>: image attachments."),
3647
3719
  ignoreUserConfig: z.boolean().optional().describe("Codex --ignore-user-config."),
3648
3720
  ignoreRules: z.boolean().optional().describe("Codex --ignore-rules."),
3649
- }, async ({ prompt, promptParts, model, fullAuto, sandboxMode, askForApproval, useLegacyFullAutoFlag, dangerouslyBypassApprovalsAndSandbox, approvalStrategy, approvalPolicy, mcpServers, sessionId, resumeLatest, createNewSession, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, outputFormat, outputSchema, search, profile, configOverrides, ephemeral, images, ignoreUserConfig, ignoreRules, }) => {
3721
+ // Phase 4 slice ζ Codex working-dir + add-dir parity (new sessions only).
3722
+ workingDir: z
3723
+ .string()
3724
+ .min(1)
3725
+ .optional()
3726
+ .describe("Codex -C/--cd <DIR>: working root for this session. New sessions only; resume inherits the original session's cwd."),
3727
+ addDir: z
3728
+ .array(z.string())
3729
+ .optional()
3730
+ .describe("Codex --add-dir <DIR>: additional writable workspace directories (repeat per entry). New sessions only."),
3731
+ }, async ({ prompt, promptParts, model, fullAuto, sandboxMode, askForApproval, useLegacyFullAutoFlag, dangerouslyBypassApprovalsAndSandbox, approvalStrategy, approvalPolicy, mcpServers, sessionId, resumeLatest, createNewSession, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, outputFormat, outputSchema, search, profile, configOverrides, ephemeral, images, ignoreUserConfig, ignoreRules, workingDir, addDir, }) => {
3650
3732
  return handleCodexRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
3651
3733
  prompt,
3652
3734
  promptParts,
@@ -3675,6 +3757,8 @@ export function createGatewayServer(deps = {}) {
3675
3757
  images,
3676
3758
  ignoreUserConfig,
3677
3759
  ignoreRules,
3760
+ workingDir,
3761
+ addDir,
3678
3762
  });
3679
3763
  });
3680
3764
  server.tool("gemini_request_async", {
@@ -3841,7 +3925,13 @@ export function createGatewayServer(deps = {}) {
3841
3925
  .default(false)
3842
3926
  .describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
3843
3927
  maxTurns: MAX_TURNS_SCHEMA.optional().describe("Grok `--max-turns N`: cap on agent-loop iterations for cost / latency control (Phase 4 slice δ). Bounded to safe integers ≤ 10000."),
3844
- }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, maxTurns, }) => {
3928
+ // Phase 4 slice ζ Grok working-directory parity.
3929
+ workingDir: z
3930
+ .string()
3931
+ .min(1)
3932
+ .optional()
3933
+ .describe("Grok --cwd <DIR>: working directory for this invocation. Lets headless callers run Grok against a directory other than the gateway process's cwd."),
3934
+ }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, maxTurns, workingDir, }) => {
3845
3935
  return handleGrokRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
3846
3936
  prompt,
3847
3937
  promptParts,
@@ -3864,6 +3954,7 @@ export function createGatewayServer(deps = {}) {
3864
3954
  idleTimeoutMs,
3865
3955
  forceRefresh,
3866
3956
  maxTurns,
3957
+ workingDir,
3867
3958
  });
3868
3959
  });
3869
3960
  server.tool("mistral_request_async", {
@@ -3939,7 +4030,17 @@ export function createGatewayServer(deps = {}) {
3939
4030
  .describe("Emit `--trust` so Vibe trusts the cwd for this invocation only (not persisted to trusted_folders.toml) and skips the interactive trust prompt (Phase 4 slice γ)."),
3940
4031
  maxTurns: MAX_TURNS_SCHEMA.optional().describe("Vibe `--max-turns N`: cap the agent-loop iteration count (programmatic mode only, Phase 4 slice δ). Bounded to safe integers ≤ 10000."),
3941
4032
  maxPrice: MAX_PRICE_SCHEMA.optional().describe("Vibe `--max-price DOLLARS`: interrupt the session when cumulative cost crosses this cap (programmatic mode only, Phase 4 slice δ). Bounded to finite values ≤ 10000 USD."),
3942
- }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, trust, maxTurns, maxPrice, }) => {
4033
+ // Phase 4 slice ζ Vibe working-directory + additional-dirs parity.
4034
+ workingDir: z
4035
+ .string()
4036
+ .min(1)
4037
+ .optional()
4038
+ .describe("Vibe --workdir <DIR>: change to this directory before running. Single value per invocation."),
4039
+ addDir: z
4040
+ .array(z.string())
4041
+ .optional()
4042
+ .describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance."),
4043
+ }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, trust, maxTurns, maxPrice, workingDir, addDir, }) => {
3943
4044
  return handleMistralRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
3944
4045
  prompt,
3945
4046
  promptParts,
@@ -3963,6 +4064,8 @@ export function createGatewayServer(deps = {}) {
3963
4064
  trust,
3964
4065
  maxTurns,
3965
4066
  maxPrice,
4067
+ workingDir,
4068
+ addDir,
3966
4069
  });
3967
4070
  });
3968
4071
  server.tool("llm_job_status", {
@@ -125,6 +125,17 @@ export interface PrepareMistralRequestInput {
125
125
  * only).
126
126
  */
127
127
  maxPrice?: number;
128
+ /**
129
+ * Phase 4 slice ζ: emit `--workdir <DIR>` so Vibe changes into the named
130
+ * directory before running. Single value (Vibe accepts one --workdir).
131
+ */
132
+ workingDir?: string;
133
+ /**
134
+ * Phase 4 slice ζ: emit `--add-dir <DIR>` per directory. Vibe's `--help`
135
+ * states the flag "Can be specified multiple times" — each entry is its
136
+ * own argv pair.
137
+ */
138
+ addDir?: string[];
128
139
  }
129
140
  export interface PrepareMistralRequestResult {
130
141
  args: string[];
@@ -364,6 +375,15 @@ export interface ClaudeHighImpactFlagsInput {
364
375
  * `--output-schema`, which takes a path).
365
376
  */
366
377
  jsonSchema?: string | Record<string, unknown>;
378
+ /**
379
+ * Phase 4 slice ζ — Claude `--add-dir <dirs...>`. Additional directories the
380
+ * Claude CLI is allowed to read/write beyond the process cwd. The CLI accepts
381
+ * a single variadic flag (space-separated values) per `claude --help`; we
382
+ * emit one `--add-dir` instance per directory so each path is its own argv
383
+ * token (survives any future tightening of the variadic parser without
384
+ * changing the call site).
385
+ */
386
+ addDir?: string[];
367
387
  }
368
388
  /**
369
389
  * Emit Claude high-impact feature flags (U25) as a flat argv segment.
@@ -185,6 +185,14 @@ export function prepareMistralRequest(input) {
185
185
  if (input.maxPrice !== undefined) {
186
186
  args.push("--max-price", String(input.maxPrice));
187
187
  }
188
+ if (input.workingDir) {
189
+ args.push("--workdir", input.workingDir);
190
+ }
191
+ if (input.addDir && input.addDir.length > 0) {
192
+ for (const dir of input.addDir) {
193
+ args.push("--add-dir", dir);
194
+ }
195
+ }
188
196
  const ignoredDisallowedTools = Boolean(input.disallowedTools && input.disallowedTools.length > 0);
189
197
  return { args, env, ignoredDisallowedTools };
190
198
  }
@@ -445,6 +453,11 @@ export function prepareClaudeHighImpactFlags(input) {
445
453
  const schemaArg = typeof input.jsonSchema === "string" ? input.jsonSchema : JSON.stringify(input.jsonSchema);
446
454
  args.push("--json-schema", schemaArg);
447
455
  }
456
+ if (input.addDir && input.addDir.length > 0) {
457
+ for (const dir of input.addDir) {
458
+ args.push("--add-dir", dir);
459
+ }
460
+ }
448
461
  return args;
449
462
  }
450
463
  //──────────────────────────────────────────────────────────────────────────────
@@ -39,6 +39,8 @@ export const UPSTREAM_CLI_CONTRACTS = {
39
39
  "excludeDynamicSystemPromptSections",
40
40
  "fallbackModel",
41
41
  "jsonSchema",
42
+ // Phase 4 slice ζ
43
+ "addDir",
42
44
  "approvalStrategy",
43
45
  "mcpServers",
44
46
  "strictMcpConfig",
@@ -88,6 +90,10 @@ export const UPSTREAM_CLI_CONTRACTS = {
88
90
  arity: "one",
89
91
  description: "JSON Schema literal constraining structured output",
90
92
  },
93
+ "--add-dir": {
94
+ arity: "one",
95
+ description: "Additional workspace directory (Phase 4 slice ζ; repeat once per directory)",
96
+ },
91
97
  "--continue": { arity: "none", description: "Continue active session" },
92
98
  "--session-id": { arity: "one", description: "Session id" },
93
99
  },
@@ -128,6 +134,14 @@ export const UPSTREAM_CLI_CONTRACTS = {
128
134
  ],
129
135
  expect: "pass",
130
136
  },
137
+ {
138
+ // Phase 4 slice ζ: --add-dir wired through prepareClaudeHighImpactFlags.
139
+ // Repeated once per directory; each instance has arity:"one".
140
+ id: "claude-add-dir",
141
+ description: "Phase 4 slice ζ: repeated --add-dir is accepted",
142
+ args: ["-p", "hello", "--add-dir", "/tmp/a", "--add-dir", "/tmp/b"],
143
+ expect: "pass",
144
+ },
131
145
  ],
132
146
  },
133
147
  codex: {
@@ -164,6 +178,9 @@ export const UPSTREAM_CLI_CONTRACTS = {
164
178
  "images",
165
179
  "ignoreUserConfig",
166
180
  "ignoreRules",
181
+ // Phase 4 slice ζ
182
+ "workingDir",
183
+ "addDir",
167
184
  ],
168
185
  resumeOnlyFlags: ["--last"],
169
186
  // Phase 4 slice α (v1.8.0) verified that `codex exec resume` accepts
@@ -203,6 +220,18 @@ export const UPSTREAM_CLI_CONTRACTS = {
203
220
  "-i": { arity: "one", description: "Image path" },
204
221
  "--ignore-user-config": { arity: "none", description: "Ignore user config" },
205
222
  "--ignore-rules": { arity: "none", description: "Ignore rule files" },
223
+ // The gateway only ever emits the short form `-C` (codex 0.134.0 accepts
224
+ // both `-C` and `--cd` as aliases). The contract registers exactly what
225
+ // we emit; if a future code path emits `--cd` instead, the contract
226
+ // check will fail loudly — which is the intended catch.
227
+ "-C": {
228
+ arity: "one",
229
+ description: "Working root for the session (Phase 4 slice ζ; new sessions only)",
230
+ },
231
+ "--add-dir": {
232
+ arity: "one",
233
+ description: "Additional writable workspace directory (Phase 4 slice ζ; repeat once per directory; new sessions only)",
234
+ },
206
235
  },
207
236
  env: {},
208
237
  conformanceFixtures: [
@@ -239,6 +268,26 @@ export const UPSTREAM_CLI_CONTRACTS = {
239
268
  args: ["exec", "resume", "--search", "session-id", "hello"],
240
269
  expect: "fail",
241
270
  },
271
+ {
272
+ id: "codex-working-dir",
273
+ description: "Phase 4 slice ζ: -C <DIR> accepted on a new session",
274
+ args: ["exec", "--skip-git-repo-check", "-C", "/tmp/work", "hello"],
275
+ expect: "pass",
276
+ },
277
+ {
278
+ id: "codex-add-dir",
279
+ description: "Phase 4 slice ζ: repeated --add-dir accepted on a new session",
280
+ args: [
281
+ "exec",
282
+ "--skip-git-repo-check",
283
+ "--add-dir",
284
+ "/tmp/a",
285
+ "--add-dir",
286
+ "/tmp/b",
287
+ "hello",
288
+ ],
289
+ expect: "pass",
290
+ },
242
291
  ],
243
292
  },
244
293
  gemini: {
@@ -350,6 +399,8 @@ export const UPSTREAM_CLI_CONTRACTS = {
350
399
  "disallowedTools",
351
400
  // Phase 4 slice δ
352
401
  "maxTurns",
402
+ // Phase 4 slice ζ
403
+ "workingDir",
353
404
  ],
354
405
  flags: {
355
406
  "-p": { arity: "one", description: "Prompt text" },
@@ -379,6 +430,10 @@ export const UPSTREAM_CLI_CONTRACTS = {
379
430
  pattern: /^[1-9][0-9]*$/,
380
431
  description: "Agent-loop iteration cap (Phase 4 slice δ)",
381
432
  },
433
+ "--cwd": {
434
+ arity: "one",
435
+ description: "Working directory for the invocation (Phase 4 slice ζ)",
436
+ },
382
437
  },
383
438
  env: {},
384
439
  conformanceFixtures: [
@@ -406,6 +461,12 @@ export const UPSTREAM_CLI_CONTRACTS = {
406
461
  args: ["-p", "hello", "--max-turns", "0"],
407
462
  expect: "fail",
408
463
  },
464
+ {
465
+ id: "grok-working-dir",
466
+ description: "Phase 4 slice ζ: --cwd <DIR> is accepted",
467
+ args: ["-p", "hello", "--cwd", "/tmp/work"],
468
+ expect: "pass",
469
+ },
409
470
  ],
410
471
  },
411
472
  mistral: {
@@ -434,6 +495,9 @@ export const UPSTREAM_CLI_CONTRACTS = {
434
495
  // Phase 4 slice δ
435
496
  "maxTurns",
436
497
  "maxPrice",
498
+ // Phase 4 slice ζ
499
+ "workingDir",
500
+ "addDir",
437
501
  ],
438
502
  flags: {
439
503
  "-p": { arity: "one", description: "Prompt text" },
@@ -468,6 +532,14 @@ export const UPSTREAM_CLI_CONTRACTS = {
468
532
  pattern: /^(0|[1-9][0-9]*)(\.[0-9]+)?$/,
469
533
  description: "Cumulative cost cap in USD (Phase 4 slice δ, programmatic mode only)",
470
534
  },
535
+ "--workdir": {
536
+ arity: "one",
537
+ description: "Working directory for the invocation (Phase 4 slice ζ)",
538
+ },
539
+ "--add-dir": {
540
+ arity: "one",
541
+ description: "Additional writable workspace directory (Phase 4 slice ζ; repeat once per directory)",
542
+ },
471
543
  },
472
544
  env: {
473
545
  VIBE_ACTIVE_MODEL: {
@@ -512,6 +584,29 @@ export const UPSTREAM_CLI_CONTRACTS = {
512
584
  env: { VIBE_ACTIVE_MODEL: "mistral-medium-3.5" },
513
585
  expect: "fail",
514
586
  },
587
+ {
588
+ id: "mistral-working-dir",
589
+ description: "Phase 4 slice ζ: --workdir <DIR> is accepted",
590
+ args: ["-p", "hello", "--agent", "auto-approve", "--workdir", "/tmp/work"],
591
+ env: { VIBE_ACTIVE_MODEL: "mistral-medium-3.5" },
592
+ expect: "pass",
593
+ },
594
+ {
595
+ id: "mistral-add-dir",
596
+ description: "Phase 4 slice ζ: repeated --add-dir is accepted",
597
+ args: [
598
+ "-p",
599
+ "hello",
600
+ "--agent",
601
+ "auto-approve",
602
+ "--add-dir",
603
+ "/tmp/a",
604
+ "--add-dir",
605
+ "/tmp/b",
606
+ ],
607
+ env: { VIBE_ACTIVE_MODEL: "mistral-medium-3.5" },
608
+ expect: "pass",
609
+ },
515
610
  ],
516
611
  },
517
612
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-cli-gateway",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "mcpName": "io.github.verivus-oss/llm-cli-gateway",
5
5
  "description": "MCP server providing unified access to Claude Code, Codex, Gemini, Grok, and Mistral Vibe CLIs with session management, retry logic, async job orchestration, durable job results, and cross-LLM validation.",
6
6
  "license": "MIT",