llm-cli-gateway 2.4.0 → 2.6.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/dist/index.js CHANGED
@@ -2,11 +2,12 @@
2
2
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { randomUUID } from "crypto";
5
- import { existsSync, readFileSync, readdirSync, renameSync, unlinkSync } from "fs";
5
+ import { createRequire } from "module";
6
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync, chmodSync, } from "fs";
6
7
  import { dirname, join } from "path";
7
8
  import { fileURLToPath } from "url";
8
9
  import { z } from "zod/v3";
9
- import { executeCli, killAllProcessGroups } from "./executor.js";
10
+ import { executeCli, killAllProcessGroups, providerCommandName } from "./executor.js";
10
11
  import { parseStreamJson } from "./stream-json-parser.js";
11
12
  import { parseCodexJsonStream } from "./codex-json-parser.js";
12
13
  import { parseGeminiJson, parseGeminiStreamJson } from "./gemini-json-parser.js";
@@ -17,7 +18,7 @@ import { createWorktree, createWorktreeSessionCleanupHook, } from "./worktree-ma
17
18
  import { ResourceProvider } from "./resources.js";
18
19
  import { PerformanceMetrics } from "./metrics.js";
19
20
  import { estimateTokens, optimizePrompt as optimizePromptText, optimizeResponse as optimizeResponseText, } from "./optimizer.js";
20
- import { loadConfig, loadPersistenceConfig, loadCacheAwarenessConfig, loadProvidersConfig, isXaiProviderEnabled, minStableTokensForModel, } from "./config.js";
21
+ import { loadConfig, loadPersistenceConfig, loadCacheAwarenessConfig, loadProvidersConfig, defaultGatewayConfigPath, isXaiProviderEnabled, minStableTokensForModel, } from "./config.js";
21
22
  import { createXaiResponse, XaiApiError, } from "./xai-api-provider.js";
22
23
  import { checkHealth } from "./health.js";
23
24
  import { clearModelRegistryCache, getAvailableCliInfo, getCliInfo, resolveModelAlias, } from "./model-registry.js";
@@ -26,15 +27,19 @@ import { createJobStore } from "./job-store.js";
26
27
  import { ApprovalManager } from "./approval-manager.js";
27
28
  import { checkReviewIntegrity } from "./review-integrity.js";
28
29
  import { buildClaudeMcpConfig, CLAUDE_MCP_SERVER_NAMES, } from "./claude-mcp-config.js";
29
- import { resolveGrokSessionArgs, resolveMistralSessionArgs, resolveCodexSessionArgs, sanitizeCliArgValues, prepareMistralRequest as buildMistralCliInvocation, MISTRAL_AGENT_MODES, GATEWAY_SESSION_PREFIX, resolveClaudePermissionFlags, resolveCodexSandboxFlags, CLAUDE_PERMISSION_MODES, GEMINI_APPROVAL_MODES, CODEX_SANDBOX_MODES, CODEX_ASK_FOR_APPROVAL_MODES, CLAUDE_EFFORT_LEVELS, prepareClaudeHighImpactFlags, validateClaudeAgentsMap, prepareCodexHighImpactFlags, prepareCodexForkRequest, CODEX_CONFIG_OVERRIDES_SCHEMA, prepareGeminiHighImpactFlags, prependGeminiAttachments, resolveGeminiSessionPlan, GEMINI_HIGH_IMPACT_PARAMS_SCHEMA, } from "./request-helpers.js";
30
+ import { resolveGrokSessionArgs, resolveMistralSessionArgs, resolveCodexSessionArgs, sanitizeCliArgValues, prepareMistralRequest as buildMistralCliInvocation, MISTRAL_AGENT_MODES, GATEWAY_SESSION_PREFIX, resolveClaudePermissionFlags, resolveCodexSandboxFlags, CLAUDE_PERMISSION_MODES, GEMINI_APPROVAL_MODES, CODEX_SANDBOX_MODES, CODEX_ASK_FOR_APPROVAL_MODES, CLAUDE_EFFORT_LEVELS, prepareClaudeHighImpactFlags, validateClaudeAgentsMap, prepareCodexHighImpactFlags, prepareCodexForkRequest, CODEX_CONFIG_OVERRIDES_SCHEMA, resolveGeminiSessionPlan, GEMINI_HIGH_IMPACT_PARAMS_SCHEMA, } from "./request-helpers.js";
30
31
  import { createFlightRecorder } from "./flight-recorder.js";
31
32
  import { resolvePromptInput, PromptPartsSchema, assembleClaudeCacheBlocks, } from "./prompt-parts.js";
32
33
  import { computeSessionCacheStats, computeTtlRemaining, readPersistedRequest, PERSISTED_REQUEST_DEFAULT_MAX_CHARS, } from "./cache-stats.js";
33
34
  import { getCliVersions, runCliUpgrade } from "./cli-updater.js";
34
35
  import { startHttpGateway } from "./http-transport.js";
36
+ import { getRequestContext } from "./request-context.js";
35
37
  import { printDoctorJson } from "./doctor.js";
38
+ import { createWorkspace, describeWorkspace, getWorkspace, loadWorkspaceRegistry, registerExistingWorkspace, resolveWorkspaceForProvider, validatePathInsideWorkspace, } from "./workspace-registry.js";
39
+ import { generateSecret, hashSecret } from "./oauth.js";
36
40
  import { registerValidationTools } from "./validation-tools.js";
37
- import { assertUpstreamCliArgs, assertUpstreamCliEnv, buildProviderSubcommandsCompactCatalog, buildUpstreamContractReport, getCliSubcommandContract, probeInstalledCliContract, serializeCliSubcommandContract, } from "./upstream-contracts.js";
41
+ import { assertUpstreamCliArgs, assertUpstreamCliEnv, buildProviderSubcommandsCompactCatalog, buildUpstreamContractReport, getCliSubcommandContract, probeInstalledCliContract, serializeCliSubcommandContract, UPSTREAM_CLI_CONTRACTS, } from "./upstream-contracts.js";
42
+ import { buildArgvFromGeneration, deriveZodShapeFromGeneration, GROK_FLAG_GENERATION, GROK_GEN_OUTPUT_FORMAT, GROK_GEN_MAIN, GROK_GEN_PROMPT_FILE, GROK_GEN_SINGLE, GROK_GEN_TAIL, } from "./provider-codegen.js";
38
43
  import { entrypointFileURL } from "./entrypoint-url.js";
39
44
  const logger = {
40
45
  info: (message, ...args) => {
@@ -158,7 +163,7 @@ Other: list_models, cli_versions, upstream_contracts, provider_subcommands_* (re
158
163
 
159
164
  Key behaviors:
160
165
  ${deferralLine}
161
- - Sessions: Claude --continue, Gemini --resume, Grok --resume/--continue, Mistral --resume/--continue (current Vibe defaults session logging on; doctor flags explicit session_logging.enabled=false), Codex \`exec resume <ID>\` / \`exec resume --last\` (all real CLI continuity). For Codex, sessionId must be a real Codex UUID (from ~/.codex/sessions/); gateway-generated gw-* IDs are rejected.
166
+ - Sessions: Claude --continue, Gemini (Antigravity) --conversation <id>/--continue, Grok --resume/--continue, Mistral --resume/--continue (current Vibe defaults session logging on; doctor flags explicit session_logging.enabled=false), Codex \`exec resume <ID>\` / \`exec resume --last\` (all real CLI continuity). For Codex, sessionId must be a real Codex UUID (from ~/.codex/sessions/); gateway-generated gw-* IDs are rejected.
162
167
  - Approval gates: opt-in via approvalStrategy:"mcp_managed".
163
168
  - Upstream drift detection: After upgrading any provider CLI (especially grok), use upstream_contracts with probeInstalled:true and provider_subcommand_drift for declared subcommand help surfaces. Probes are safe, read-only --help checks.
164
169
  - Idle timeout kills stuck processes (default 10min, configurable via idleTimeoutMs).
@@ -226,6 +231,7 @@ function getApprovalManager(runtimeLogger = logger) {
226
231
  const MCP_SERVER_ENUM = z.enum(CLAUDE_MCP_SERVER_NAMES);
227
232
  const CLI_TYPE_ENUM = z.enum(CLI_TYPES);
228
233
  export const MAX_TURNS_SCHEMA = z.number().int().positive().safe().max(10_000);
234
+ const GROK_GENERATED_SHAPE = deriveZodShapeFromGeneration(UPSTREAM_CLI_CONTRACTS.grok, GROK_FLAG_GENERATION);
229
235
  export const MAX_TOKENS_SCHEMA = z.number().int().positive().safe().max(100_000_000);
230
236
  export const MAX_PRICE_SCHEMA = z.number().positive().finite().min(1e-6).max(10_000);
231
237
  export const WORKTREE_SCHEMA = z
@@ -252,6 +258,12 @@ export const WORKTREE_SCHEMA = z
252
258
  "path. NOTE: callers should `.gitignore` the `.worktrees/` " +
253
259
  "directory in their repo (the gateway does NOT auto-gitignore — " +
254
260
  "see slice λ spec Q4).");
261
+ export const WORKSPACE_ALIAS_SCHEMA = z
262
+ .string()
263
+ .min(1)
264
+ .max(64)
265
+ .regex(/^[A-Za-z][A-Za-z0-9._-]{0,63}$/)
266
+ .describe("Registered workspace alias. Remote clients use aliases, not absolute paths.");
255
267
  export const SESSION_PROVIDER_VALUES = PROVIDER_TYPES;
256
268
  export const SESSION_PROVIDER_ENUM = z.enum(SESSION_PROVIDER_VALUES);
257
269
  let activeServer = null;
@@ -286,6 +298,7 @@ export function resolveGatewayServerRuntime(deps = {}, options = {}) {
286
298
  persistence: deps.persistence ?? getPersistenceConfig(runtimeLogger),
287
299
  cacheAwareness: deps.cacheAwareness ?? getCacheAwarenessConfig(runtimeLogger),
288
300
  providers: deps.providers ?? getProvidersConfig(runtimeLogger),
301
+ workspaces: deps.workspaces ?? loadWorkspaceRegistry(runtimeLogger),
289
302
  };
290
303
  }
291
304
  export function shouldRegisterGrokApiTools(providers) {
@@ -329,7 +342,7 @@ async function awaitJobOrDefer(cli, args, corrId, idleTimeoutMs, outputFormat, f
329
342
  runtime.persistence.asyncJobsEnabled &&
330
343
  runtime.asyncJobManager.hasStore();
331
344
  if (SYNC_DEADLINE_MS === 0 || !deferralAvailable) {
332
- const command = cli === "mistral" ? "vibe" : cli;
345
+ const command = providerCommandName(cli);
333
346
  try {
334
347
  return await executeCli(command, args, {
335
348
  idleTimeout: idleTimeoutMs,
@@ -415,7 +428,7 @@ function buildDeferredToolResponse(deferred, sessionId) {
415
428
  ],
416
429
  };
417
430
  }
418
- export async function resolveWorktreeForRequest(worktreeOpt, sessionId, runtime) {
431
+ export async function resolveWorktreeForRequest(worktreeOpt, sessionId, runtime, options = {}) {
419
432
  if (!worktreeOpt)
420
433
  return {};
421
434
  const sessionManager = runtime.sessionManager;
@@ -423,12 +436,21 @@ export async function resolveWorktreeForRequest(worktreeOpt, sessionId, runtime)
423
436
  const session = await Promise.resolve(sessionManager.getSession(sessionId));
424
437
  const existingPath = session?.metadata?.worktreePath;
425
438
  if (typeof existingPath === "string" && existingPath.length > 0) {
426
- return { cwd: existingPath, worktreePath: existingPath };
439
+ return {
440
+ cwd: existingPath,
441
+ worktreePath: existingPath,
442
+ workspaceAlias: typeof session?.metadata?.workspaceAlias === "string"
443
+ ? session.metadata.workspaceAlias
444
+ : options.workspaceAlias,
445
+ workspaceRoot: typeof session?.metadata?.workspaceRoot === "string"
446
+ ? session.metadata.workspaceRoot
447
+ : options.workspaceRoot,
448
+ };
427
449
  }
428
450
  }
429
451
  const name = worktreeOpt === true ? undefined : worktreeOpt.name;
430
452
  const ref = worktreeOpt === true ? undefined : worktreeOpt.ref;
431
- const repoRoot = process.cwd();
453
+ const repoRoot = options.repoRoot ?? process.cwd();
432
454
  const handle = await createWorktree({
433
455
  repoRoot,
434
456
  name,
@@ -439,13 +461,204 @@ export async function resolveWorktreeForRequest(worktreeOpt, sessionId, runtime)
439
461
  await Promise.resolve(sessionManager.updateSessionMetadata(sessionId, {
440
462
  worktreePath: handle.path,
441
463
  worktreeName: handle.name,
464
+ ...(options.workspaceAlias ? { workspaceAlias: options.workspaceAlias } : {}),
465
+ ...(options.workspaceRoot ? { workspaceRoot: options.workspaceRoot } : {}),
466
+ }));
467
+ }
468
+ return {
469
+ cwd: handle.path,
470
+ worktreePath: handle.path,
471
+ workspaceAlias: options.workspaceAlias,
472
+ workspaceRoot: options.workspaceRoot,
473
+ };
474
+ }
475
+ function isGatewayAppDirCwd() {
476
+ return process.cwd() === join(homedir(), ".llm-cli-gateway");
477
+ }
478
+ async function resolveWorkspaceAndWorktreeForRequest(args) {
479
+ const session = args.sessionId
480
+ ? await Promise.resolve(args.runtime.sessionManager.getSession(args.sessionId))
481
+ : null;
482
+ let workspace;
483
+ if (args.workspace ||
484
+ args.runtime.workspaces.defaultAlias ||
485
+ typeof session?.metadata?.workspaceAlias === "string") {
486
+ workspace = resolveWorkspaceForProvider(args.runtime.workspaces, args.provider, args.workspace, session?.metadata);
487
+ }
488
+ else if (isGatewayAppDirCwd()) {
489
+ throw new Error("No workspace selected. Configure [workspaces].default or pass a registered workspace alias.");
490
+ }
491
+ if (!workspace && getRequestContext()?.authKind === "oauth") {
492
+ throw new Error("Remote OAuth provider requests require a registered workspace alias or [workspaces].default.");
493
+ }
494
+ if (!workspace &&
495
+ (args.workingDir || (args.addDir?.length ?? 0) > 0) &&
496
+ !args.runtime.workspaces.allowUnregisteredWorkingDir) {
497
+ throw new Error("workingDir/addDir require a registered workspace alias unless [workspaces].allow_unregistered_working_dir is explicitly enabled.");
498
+ }
499
+ if (workspace) {
500
+ if (args.workingDir) {
501
+ validatePathInsideWorkspace(workspace, args.workingDir, "workingDir");
502
+ }
503
+ for (const dir of args.addDir ?? []) {
504
+ validatePathInsideWorkspace(workspace, dir, "addDir");
505
+ }
506
+ }
507
+ if (args.worktree) {
508
+ if (workspace && !workspace.repo.allowWorktree) {
509
+ throw new Error(`Workspace "${workspace.alias}" does not allow worktree requests`);
510
+ }
511
+ const resolved = await resolveWorktreeForRequest(args.worktree, args.sessionId, args.runtime, {
512
+ repoRoot: workspace?.root,
513
+ workspaceAlias: workspace?.alias,
514
+ workspaceRoot: workspace?.root,
515
+ });
516
+ return { cwd: resolved.cwd, worktreePath: resolved.worktreePath, workspace };
517
+ }
518
+ if (workspace && args.sessionId) {
519
+ await Promise.resolve(args.runtime.sessionManager.updateSessionMetadata(args.sessionId, {
520
+ workspaceAlias: workspace.alias,
521
+ workspaceRoot: workspace.root,
442
522
  }));
443
523
  }
444
- return { cwd: handle.path, worktreePath: handle.path };
524
+ return { cwd: workspace?.cwd, workspace };
445
525
  }
446
526
  export function formatWorktreePrefix(worktreePath) {
447
527
  return worktreePath ? `[gateway] worktree=${worktreePath}\n` : "";
448
528
  }
529
+ function workspaceAdminEnabled() {
530
+ const scopes = getRequestContext()?.authScopes ?? [];
531
+ return process.env.LLM_GATEWAY_WORKSPACE_ADMIN === "1" && scopes.includes("workspace:admin");
532
+ }
533
+ function registerWorkspaceTools(server, runtime) {
534
+ server.tool("workspace_list", "List registered workspace aliases and summary metadata. Does not browse files.", {}, {
535
+ title: "List workspaces",
536
+ readOnlyHint: true,
537
+ destructiveHint: false,
538
+ idempotentHint: true,
539
+ openWorldHint: false,
540
+ }, async () => {
541
+ const registry = loadWorkspaceRegistry(runtime.logger);
542
+ return {
543
+ content: [
544
+ {
545
+ type: "text",
546
+ text: JSON.stringify({
547
+ success: true,
548
+ enabled: registry.enabled,
549
+ default: registry.defaultAlias,
550
+ workspaces: registry.repos.map(describeWorkspace),
551
+ allowed_roots: registry.allowedRoots.map(root => ({
552
+ alias: root.alias,
553
+ path: root.path,
554
+ allow_register_existing_git_repos: root.allowRegisterExistingGitRepos,
555
+ allow_create_directories: root.allowCreateDirectories,
556
+ allow_init_git_repos: root.allowInitGitRepos,
557
+ max_create_depth: root.maxCreateDepth,
558
+ })),
559
+ }, null, 2),
560
+ },
561
+ ],
562
+ };
563
+ });
564
+ server.tool("workspace_get", "Inspect a registered workspace alias. Does not list files.", { alias: WORKSPACE_ALIAS_SCHEMA }, {
565
+ title: "Get workspace",
566
+ readOnlyHint: true,
567
+ destructiveHint: false,
568
+ idempotentHint: true,
569
+ openWorldHint: false,
570
+ }, async ({ alias }) => {
571
+ try {
572
+ const registry = loadWorkspaceRegistry(runtime.logger);
573
+ return {
574
+ content: [
575
+ {
576
+ type: "text",
577
+ text: JSON.stringify({ success: true, workspace: describeWorkspace(getWorkspace(registry, alias)) }, null, 2),
578
+ },
579
+ ],
580
+ };
581
+ }
582
+ catch (error) {
583
+ return createErrorResponse("workspace_get", 1, "", undefined, error);
584
+ }
585
+ });
586
+ server.tool("workspace_create", "Create a new local folder or git repo under a configured allowed root. Requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin.", {
587
+ alias: WORKSPACE_ALIAS_SCHEMA,
588
+ root: WORKSPACE_ALIAS_SCHEMA.describe("Allowed-root alias from workspace_list."),
589
+ slug: z.string().min(1).max(255).describe("Safe relative path under the allowed root."),
590
+ kind: z.enum(["folder", "git"]).default("git"),
591
+ setDefault: z.boolean().default(false),
592
+ }, {
593
+ title: "Create workspace",
594
+ readOnlyHint: false,
595
+ destructiveHint: true,
596
+ idempotentHint: false,
597
+ openWorldHint: false,
598
+ }, async ({ alias, root, slug, kind, setDefault }) => {
599
+ try {
600
+ if (!workspaceAdminEnabled()) {
601
+ throw new Error("workspace_create requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin");
602
+ }
603
+ const repo = createWorkspace({
604
+ alias,
605
+ rootAlias: root,
606
+ slug,
607
+ kind,
608
+ setDefault,
609
+ logger: runtime.logger,
610
+ });
611
+ return {
612
+ content: [
613
+ {
614
+ type: "text",
615
+ text: JSON.stringify({ success: true, workspace: describeWorkspace(repo) }, null, 2),
616
+ },
617
+ ],
618
+ };
619
+ }
620
+ catch (error) {
621
+ return createErrorResponse("workspace_create", 1, "", undefined, error);
622
+ }
623
+ });
624
+ server.tool("workspace_register_existing_repo", "Register an existing local Git repo under an allowed root. Requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin.", {
625
+ alias: WORKSPACE_ALIAS_SCHEMA,
626
+ path: z
627
+ .string()
628
+ .min(1)
629
+ .describe("Absolute path to an existing Git repo under an allowed root."),
630
+ setDefault: z.boolean().default(false),
631
+ }, {
632
+ title: "Register workspace",
633
+ readOnlyHint: false,
634
+ destructiveHint: false,
635
+ idempotentHint: false,
636
+ openWorldHint: false,
637
+ }, async ({ alias, path, setDefault }) => {
638
+ try {
639
+ if (!workspaceAdminEnabled()) {
640
+ throw new Error("workspace_register_existing_repo requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin");
641
+ }
642
+ const repo = registerExistingWorkspace({
643
+ alias,
644
+ repoPath: path,
645
+ setDefault,
646
+ logger: runtime.logger,
647
+ });
648
+ return {
649
+ content: [
650
+ {
651
+ type: "text",
652
+ text: JSON.stringify({ success: true, workspace: describeWorkspace(repo) }, null, 2),
653
+ },
654
+ ],
655
+ };
656
+ }
657
+ catch (error) {
658
+ return createErrorResponse("workspace_register_existing_repo", 1, "", undefined, error);
659
+ }
660
+ });
661
+ }
449
662
  function createErrorResponse(cli, code, stderr, correlationId, error) {
450
663
  let errorMessage = `Error executing ${cli} CLI`;
451
664
  const isLaunchExit = code === 127 || code === -4058;
@@ -476,6 +689,7 @@ function createErrorResponse(cli, code, stderr, correlationId, error) {
476
689
  content: [{ type: "text", text: errorMessage }],
477
690
  isError: true,
478
691
  structuredContent: {
692
+ response: errorMessage,
479
693
  correlationId: correlationId || null,
480
694
  cli,
481
695
  exitCode: code,
@@ -900,7 +1114,7 @@ export function prepareClaudeRequest(params, runtime = resolveGatewayServerRunti
900
1114
  logOptimizationTokens("prompt", corrId, effectivePrompt, optimized);
901
1115
  effectivePrompt = optimized;
902
1116
  }
903
- const requestedMcpServers = normalizeMcpServers(params.mcpServers);
1117
+ const requestedMcpServers = params.mcpServers ? [...new Set(params.mcpServers)] : [];
904
1118
  const mcpConfigResolution = resolveClaudeMcpConfig(params.operation, corrId, requestedMcpServers, params.strictMcpConfig);
905
1119
  if ("errorResponse" in mcpConfigResolution) {
906
1120
  return mcpConfigResolution.errorResponse;
@@ -1109,7 +1323,7 @@ export function prepareCodexRequest(params, runtime = resolveGatewayServerRuntim
1109
1323
  logOptimizationTokens("prompt", corrId, effectivePrompt, optimized);
1110
1324
  effectivePrompt = optimized;
1111
1325
  }
1112
- const requestedMcpServers = normalizeMcpServers(params.mcpServers);
1326
+ const requestedMcpServers = params.mcpServers ? [...new Set(params.mcpServers)] : [];
1113
1327
  let approvalDecision = null;
1114
1328
  if (params.approvalStrategy === "mcp_managed") {
1115
1329
  approvalDecision = runtime.approvalManager.decide({
@@ -1267,7 +1481,7 @@ export function prepareGeminiRequest(params, runtime = resolveGatewayServerRunti
1267
1481
  logOptimizationTokens("prompt", corrId, effectivePrompt, optimized);
1268
1482
  effectivePrompt = optimized;
1269
1483
  }
1270
- const requestedMcpServers = normalizeMcpServers(params.mcpServers);
1484
+ const requestedMcpServers = params.mcpServers ? [...new Set(params.mcpServers)] : [];
1271
1485
  let approvalDecision = null;
1272
1486
  if (params.approvalStrategy === "mcp_managed") {
1273
1487
  approvalDecision = runtime.approvalManager.decide({
@@ -1287,51 +1501,45 @@ export function prepareGeminiRequest(params, runtime = resolveGatewayServerRunti
1287
1501
  }
1288
1502
  }
1289
1503
  const effectiveApprovalMode = params.approvalStrategy === "mcp_managed" ? "yolo" : params.approvalMode;
1290
- const highImpact = prepareGeminiHighImpactFlags({
1291
- sandbox: params.sandbox,
1292
- policyFiles: params.policyFiles,
1293
- adminPolicyFiles: params.adminPolicyFiles,
1294
- });
1295
- if (highImpact.missingPolicyPath) {
1296
- return createErrorResponse(params.operation, 1, "", corrId, new Error(`${highImpact.missingPolicyField}: path does not exist: ${highImpact.missingPolicyPath}`));
1297
- }
1298
- if (params.attachments && params.attachments.length > 0) {
1299
- try {
1300
- effectivePrompt = prependGeminiAttachments(effectivePrompt, params.attachments);
1301
- }
1302
- catch (err) {
1303
- return createErrorResponse(params.operation, 1, "", corrId, err instanceof Error ? err : new Error(String(err)));
1304
- }
1305
- }
1306
- const args = ["-p", effectivePrompt];
1307
- if (resolvedModel)
1308
- args.push("--model", resolvedModel);
1309
- if (effectiveApprovalMode)
1310
- args.push("--approval-mode", effectiveApprovalMode);
1311
- if (params.yolo && effectiveApprovalMode !== "yolo") {
1312
- args.push("--yolo");
1504
+ const unsupported = (field, detail) => createErrorResponse(params.operation, 1, "", corrId, new Error(`${field} is not supported by Antigravity CLI (agy): ${detail}`));
1505
+ if (effectiveApprovalMode &&
1506
+ effectiveApprovalMode !== "default" &&
1507
+ effectiveApprovalMode !== "yolo") {
1508
+ return unsupported("approvalMode", "use 'default' for prompted execution or 'yolo'/yolo=true for --dangerously-skip-permissions");
1313
1509
  }
1314
1510
  if (params.allowedTools && params.allowedTools.length > 0) {
1315
- sanitizeCliArgValues(params.allowedTools, "allowedTools");
1316
- params.allowedTools.forEach(tool => args.push("--allowed-tools", tool));
1511
+ return unsupported("allowedTools", "agy has no non-interactive allowed-tools flag");
1317
1512
  }
1318
1513
  if (requestedMcpServers.length > 0) {
1319
- sanitizeCliArgValues(requestedMcpServers, "mcpServers");
1320
- requestedMcpServers.forEach(serverName => args.push("--allowed-mcp-server-names", serverName));
1514
+ return unsupported("mcpServers", "agy has no non-interactive allowed MCP server allowlist flag");
1321
1515
  }
1322
- if (params.includeDirs && params.includeDirs.length > 0) {
1323
- sanitizeCliArgValues(params.includeDirs, "includeDirs");
1324
- params.includeDirs.forEach(dir => args.push("--include-directories", dir));
1516
+ if (params.outputFormat && params.outputFormat !== "text") {
1517
+ return unsupported("outputFormat", "agy print mode currently emits text only");
1325
1518
  }
1326
- args.push(...highImpact.args);
1327
- if (params.outputFormat === "json") {
1328
- args.push("-o", "json");
1519
+ if (params.policyFiles && params.policyFiles.length > 0) {
1520
+ return unsupported("policyFiles", "agy has no --policy flag");
1329
1521
  }
1330
- else if (params.outputFormat === "stream-json") {
1331
- args.push("-o", "stream-json");
1522
+ if (params.adminPolicyFiles && params.adminPolicyFiles.length > 0) {
1523
+ return unsupported("adminPolicyFiles", "agy has no --admin-policy flag");
1524
+ }
1525
+ if (params.attachments && params.attachments.length > 0) {
1526
+ return unsupported("attachments", "agy has no documented @path attachment-token contract");
1332
1527
  }
1333
1528
  if (params.skipTrust) {
1334
- args.push("--skip-trust");
1529
+ return unsupported("skipTrust", "agy has no --skip-trust flag");
1530
+ }
1531
+ const args = ["--print", effectivePrompt];
1532
+ if (resolvedModel)
1533
+ args.push("--model", resolvedModel);
1534
+ if (params.includeDirs && params.includeDirs.length > 0) {
1535
+ sanitizeCliArgValues(params.includeDirs, "includeDirs");
1536
+ params.includeDirs.forEach(dir => args.push("--add-dir", dir));
1537
+ }
1538
+ if (params.sandbox) {
1539
+ args.push("--sandbox");
1540
+ }
1541
+ if (params.yolo || effectiveApprovalMode === "yolo") {
1542
+ args.push("--dangerously-skip-permissions");
1335
1543
  }
1336
1544
  return {
1337
1545
  corrId,
@@ -1400,76 +1608,19 @@ export function prepareGrokRequest(params, runtime = resolveGatewayServerRuntime
1400
1608
  }
1401
1609
  }
1402
1610
  const effectiveAlwaysApprove = params.approvalStrategy === "mcp_managed" ? true : Boolean(params.alwaysApprove);
1611
+ const grokContract = UPSTREAM_CLI_CONTRACTS.grok;
1612
+ const genParams = params;
1403
1613
  const args = ["-p", effectivePrompt];
1404
1614
  if (resolvedModel)
1405
1615
  args.push("--model", resolvedModel);
1406
- if (params.outputFormat)
1407
- args.push("--output-format", params.outputFormat);
1616
+ args.push(...buildArgvFromGeneration(grokContract, GROK_GEN_OUTPUT_FORMAT, genParams));
1408
1617
  if (effectiveAlwaysApprove) {
1409
1618
  args.push("--always-approve");
1410
1619
  }
1411
1620
  else if (params.permissionMode) {
1412
1621
  args.push("--permission-mode", params.permissionMode);
1413
1622
  }
1414
- if (params.effort)
1415
- args.push("--effort", params.effort);
1416
- if (params.reasoningEffort)
1417
- args.push("--reasoning-effort", params.reasoningEffort);
1418
- if (params.allowedTools && params.allowedTools.length > 0) {
1419
- args.push("--tools", params.allowedTools.join(","));
1420
- }
1421
- if (params.disallowedTools && params.disallowedTools.length > 0) {
1422
- args.push("--disallowed-tools", params.disallowedTools.join(","));
1423
- }
1424
- if (params.maxTurns !== undefined) {
1425
- args.push("--max-turns", String(params.maxTurns));
1426
- }
1427
- if (params.workingDir) {
1428
- args.push("--cwd", params.workingDir);
1429
- }
1430
- if (params.sandbox) {
1431
- args.push("--sandbox", params.sandbox);
1432
- }
1433
- if (params.rules) {
1434
- args.push("--rules", params.rules);
1435
- }
1436
- if (params.systemPromptOverride) {
1437
- args.push("--system-prompt-override", params.systemPromptOverride);
1438
- }
1439
- if (params.allow && params.allow.length > 0) {
1440
- for (const rule of params.allow) {
1441
- args.push("--allow", rule);
1442
- }
1443
- }
1444
- if (params.deny && params.deny.length > 0) {
1445
- for (const rule of params.deny) {
1446
- args.push("--deny", rule);
1447
- }
1448
- }
1449
- if (params.compactionMode) {
1450
- args.push("--compaction-mode", params.compactionMode);
1451
- }
1452
- if (params.compactionDetail) {
1453
- args.push("--compaction-detail", params.compactionDetail);
1454
- }
1455
- if (params.agent) {
1456
- args.push("--agent", params.agent);
1457
- }
1458
- if (params.bestOfN !== undefined) {
1459
- args.push("--best-of-n", String(params.bestOfN));
1460
- }
1461
- if (params.check) {
1462
- args.push("--check");
1463
- }
1464
- if (params.disableWebSearch) {
1465
- args.push("--disable-web-search");
1466
- }
1467
- if (params.todoGate) {
1468
- args.push("--todo-gate");
1469
- }
1470
- if (params.verbatim) {
1471
- args.push("--verbatim");
1472
- }
1623
+ args.push(...buildArgvFromGeneration(grokContract, GROK_GEN_MAIN, genParams));
1473
1624
  if (params.agents !== undefined) {
1474
1625
  if (typeof params.agents === "string") {
1475
1626
  if (!params.agents.trim()) {
@@ -1485,9 +1636,7 @@ export function prepareGrokRequest(params, runtime = resolveGatewayServerRuntime
1485
1636
  args.push("--agents", JSON.stringify(agentsResult.value));
1486
1637
  }
1487
1638
  }
1488
- if (params.promptFile) {
1489
- args.push("--prompt-file", params.promptFile);
1490
- }
1639
+ args.push(...buildArgvFromGeneration(grokContract, GROK_GEN_PROMPT_FILE, genParams));
1491
1640
  if (params.promptJson !== undefined) {
1492
1641
  const promptJsonValue = typeof params.promptJson === "string" ? params.promptJson : JSON.stringify(params.promptJson);
1493
1642
  if (!promptJsonValue.trim()) {
@@ -1495,33 +1644,8 @@ export function prepareGrokRequest(params, runtime = resolveGatewayServerRuntime
1495
1644
  }
1496
1645
  args.push("--prompt-json", promptJsonValue);
1497
1646
  }
1498
- if (params.single) {
1499
- args.push("--single", params.single);
1500
- }
1501
- if (params.experimentalMemory) {
1502
- args.push("--experimental-memory");
1503
- }
1504
- if (params.noAltScreen) {
1505
- args.push("--no-alt-screen");
1506
- }
1507
- if (params.noMemory) {
1508
- args.push("--no-memory");
1509
- }
1510
- if (params.noPlan) {
1511
- args.push("--no-plan");
1512
- }
1513
- if (params.noSubagents) {
1514
- args.push("--no-subagents");
1515
- }
1516
- if (params.oauth) {
1517
- args.push("--oauth");
1518
- }
1519
- if (params.restoreCode) {
1520
- args.push("--restore-code");
1521
- }
1522
- if (params.leaderSocket) {
1523
- args.push("--leader-socket", params.leaderSocket);
1524
- }
1647
+ args.push(...buildArgvFromGeneration(grokContract, GROK_GEN_SINGLE, genParams));
1648
+ args.push(...buildArgvFromGeneration(grokContract, GROK_GEN_TAIL, genParams));
1525
1649
  if (params.nativeWorktree === true) {
1526
1650
  args.push("--worktree");
1527
1651
  }
@@ -1678,6 +1802,7 @@ function buildCliResponse(cli, stdout, optimizeResponse, corrId, sessionId, prep
1678
1802
  const response = {
1679
1803
  content: [{ type: "text", text: finalStdout }],
1680
1804
  structuredContent: {
1805
+ response: finalStdout,
1681
1806
  model: prep.resolvedModel || "default",
1682
1807
  cli,
1683
1808
  correlationId: corrId,
@@ -1813,6 +1938,7 @@ function buildGrokApiToolResponse(args) {
1813
1938
  const response = {
1814
1939
  content: [{ type: "text", text }],
1815
1940
  structuredContent: {
1941
+ response: text,
1816
1942
  provider: "grok-api",
1817
1943
  cli: "grok-api",
1818
1944
  model: args.result.model || args.prep.resolvedModel,
@@ -2016,6 +2142,7 @@ function resolveHandlerRuntime(deps) {
2016
2142
  sessionManager: deps.sessionManager,
2017
2143
  logger: normalizedLogger,
2018
2144
  asyncJobManager: asyncDeps.asyncJobManager,
2145
+ workspaces: deps.workspaces,
2019
2146
  });
2020
2147
  }
2021
2148
  export async function handleGeminiRequest(deps, params) {
@@ -2071,13 +2198,20 @@ export async function handleGeminiRequest(deps, params) {
2071
2198
  args.push(...sessionPlan.args);
2072
2199
  let worktreeResolution = {};
2073
2200
  try {
2074
- worktreeResolution = await resolveWorktreeForRequest(params.worktree, effectiveSessionIdHint, runtime);
2201
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
2202
+ provider: "gemini",
2203
+ workspace: params.workspace,
2204
+ worktree: params.worktree,
2205
+ sessionId: effectiveSessionIdHint,
2206
+ runtime,
2207
+ addDir: params.includeDirs,
2208
+ });
2075
2209
  }
2076
2210
  catch (err) {
2077
2211
  return createErrorResponse("gemini_request", 1, "", corrId, err);
2078
2212
  }
2079
2213
  const geminiFrHandoff = buildAsyncFlightRecorderHandoff("gemini", prep, params.sessionId, params.outputFormat);
2080
- const result = await awaitJobOrDefer("gemini", args, corrId, resolveIdleTimeout("gemini", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, runtime, undefined, undefined, geminiFrHandoff.flightRecorderEntry, geminiFrHandoff.extractUsage, worktreeResolution.cwd);
2214
+ const result = await awaitJobOrDefer("gemini", args, corrId, resolveIdleTimeout("gemini", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, runtime, undefined, undefined, geminiFrHandoff.flightRecorderEntry, geminiFrHandoff.extractUsage, undefined, worktreeResolution.cwd);
2081
2215
  if (isDeferredResponse(result)) {
2082
2216
  return buildDeferredToolResponse(result, effectiveSessionIdHint);
2083
2217
  }
@@ -2209,7 +2343,14 @@ export async function handleGeminiRequestAsync(deps, params) {
2209
2343
  }
2210
2344
  let worktreeResolution = {};
2211
2345
  try {
2212
- worktreeResolution = await resolveWorktreeForRequest(params.worktree, effectiveSessionId, runtime);
2346
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
2347
+ provider: "gemini",
2348
+ workspace: params.workspace,
2349
+ worktree: params.worktree,
2350
+ sessionId: effectiveSessionId,
2351
+ runtime,
2352
+ addDir: params.includeDirs,
2353
+ });
2213
2354
  }
2214
2355
  catch (err) {
2215
2356
  return createErrorResponse("gemini_request_async", 1, "", corrId, err);
@@ -2322,7 +2463,14 @@ export async function handleGrokRequest(deps, params) {
2322
2463
  args.push(...sessionResult.resumeArgs);
2323
2464
  let worktreeResolution = {};
2324
2465
  try {
2325
- worktreeResolution = await resolveWorktreeForRequest(params.worktree, sessionResult.effectiveSessionId, runtime);
2466
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
2467
+ provider: "grok",
2468
+ workspace: params.workspace,
2469
+ worktree: params.worktree,
2470
+ sessionId: sessionResult.effectiveSessionId,
2471
+ runtime,
2472
+ workingDir: params.workingDir,
2473
+ });
2326
2474
  }
2327
2475
  catch (err) {
2328
2476
  return createErrorResponse("grok_request", 1, "", corrId, err);
@@ -2490,7 +2638,14 @@ export async function handleGrokRequestAsync(deps, params) {
2490
2638
  }
2491
2639
  let worktreeResolution = {};
2492
2640
  try {
2493
- worktreeResolution = await resolveWorktreeForRequest(params.worktree, effectiveSessionId, runtime);
2641
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
2642
+ provider: "grok",
2643
+ workspace: params.workspace,
2644
+ worktree: params.worktree,
2645
+ sessionId: effectiveSessionId,
2646
+ runtime,
2647
+ workingDir: params.workingDir,
2648
+ });
2494
2649
  }
2495
2650
  catch (err) {
2496
2651
  return createErrorResponse("grok_request_async", 1, "", corrId, err);
@@ -2578,7 +2733,15 @@ export async function handleMistralRequest(deps, params) {
2578
2733
  args.push(...sessionResult.resumeArgs);
2579
2734
  let worktreeResolution = {};
2580
2735
  try {
2581
- worktreeResolution = await resolveWorktreeForRequest(params.worktree, sessionResult.effectiveSessionId, runtime);
2736
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
2737
+ provider: "mistral",
2738
+ workspace: params.workspace,
2739
+ worktree: params.worktree,
2740
+ sessionId: sessionResult.effectiveSessionId,
2741
+ runtime,
2742
+ workingDir: params.workingDir,
2743
+ addDir: params.addDir,
2744
+ });
2582
2745
  }
2583
2746
  catch (err) {
2584
2747
  return createErrorResponse("mistral_request", 1, "", corrId, err);
@@ -2732,7 +2895,15 @@ export async function handleMistralRequestAsync(deps, params) {
2732
2895
  }
2733
2896
  let worktreeResolution = {};
2734
2897
  try {
2735
- worktreeResolution = await resolveWorktreeForRequest(params.worktree, effectiveSessionId, runtime);
2898
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
2899
+ provider: "mistral",
2900
+ workspace: params.workspace,
2901
+ worktree: params.worktree,
2902
+ sessionId: effectiveSessionId,
2903
+ runtime,
2904
+ workingDir: params.workingDir,
2905
+ addDir: params.addDir,
2906
+ });
2736
2907
  }
2737
2908
  catch (err) {
2738
2909
  return createErrorResponse("mistral_request_async", 1, "", corrId, err);
@@ -2844,7 +3015,15 @@ export async function handleCodexRequestAsync(deps, params) {
2844
3015
  }
2845
3016
  let worktreeResolution = {};
2846
3017
  try {
2847
- worktreeResolution = await resolveWorktreeForRequest(params.worktree, effectiveSessionId, runtime);
3018
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
3019
+ provider: "codex",
3020
+ workspace: params.workspace,
3021
+ worktree: params.worktree,
3022
+ sessionId: effectiveSessionId,
3023
+ runtime,
3024
+ workingDir: params.workingDir,
3025
+ addDir: params.addDir,
3026
+ });
2848
3027
  }
2849
3028
  catch (err) {
2850
3029
  runPrepCleanupLocally();
@@ -2899,6 +3078,7 @@ export function createGatewayServer(deps = {}) {
2899
3078
  const server = newGatewayMcpServer(asyncJobsEnabled, grokApiToolsEnabled);
2900
3079
  registerBaseResources(server, runtime);
2901
3080
  registerValidationTools(server, { asyncJobManager });
3081
+ registerWorkspaceTools(server, runtime);
2902
3082
  if (grokApiToolsEnabled) {
2903
3083
  server.tool("grok_api_request", "Run an xAI Grok API request synchronously through the Responses API. Requires exactly one of prompt or promptParts. Registered only when [providers.xai] is configured and its API-key env var is present.", {
2904
3084
  prompt: z
@@ -3082,6 +3262,7 @@ export function createGatewayServer(deps = {}) {
3082
3262
  .array(z.string())
3083
3263
  .optional()
3084
3264
  .describe('Claude --tools: restrict the available built-in tool set (distinct from allowedTools permission gating). Pass [""] to disable all tools.'),
3265
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
3085
3266
  worktree: WORKTREE_SCHEMA.optional(),
3086
3267
  approvalStrategy: z
3087
3268
  .enum(["legacy", "mcp_managed"])
@@ -3119,7 +3300,7 @@ export function createGatewayServer(deps = {}) {
3119
3300
  destructiveHint: true,
3120
3301
  idempotentHint: false,
3121
3302
  openWorldHint: true,
3122
- }, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, addDir, noSessionPersistence, settingSources, settings, tools, worktree, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, }) => {
3303
+ }, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, addDir, noSessionPersistence, settingSources, settings, tools, workspace, worktree, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, }) => {
3123
3304
  const startTime = Date.now();
3124
3305
  if (systemPrompt !== undefined && appendSystemPrompt !== undefined) {
3125
3306
  return createErrorResponse("claude", 1, "", correlationId, new Error("systemPrompt and appendSystemPrompt are mutually exclusive; use one or the other (not both)."));
@@ -3214,7 +3395,14 @@ export function createGatewayServer(deps = {}) {
3214
3395
  }
3215
3396
  let worktreeResolution = {};
3216
3397
  try {
3217
- worktreeResolution = await resolveWorktreeForRequest(worktree, effectiveSessionId, runtime);
3398
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
3399
+ provider: "claude",
3400
+ workspace,
3401
+ worktree,
3402
+ sessionId: effectiveSessionId,
3403
+ runtime,
3404
+ addDir,
3405
+ });
3218
3406
  }
3219
3407
  catch (err) {
3220
3408
  return createErrorResponse("claude_request", 1, "", corrId, err);
@@ -3425,6 +3613,7 @@ export function createGatewayServer(deps = {}) {
3425
3613
  .array(z.string())
3426
3614
  .optional()
3427
3615
  .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."),
3616
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
3428
3617
  worktree: WORKTREE_SCHEMA.optional(),
3429
3618
  }, {
3430
3619
  title: "Codex request",
@@ -3432,7 +3621,7 @@ export function createGatewayServer(deps = {}) {
3432
3621
  destructiveHint: true,
3433
3622
  idempotentHint: false,
3434
3623
  openWorldHint: true,
3435
- }, 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, worktree, }) => {
3624
+ }, 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, workspace, worktree, }) => {
3436
3625
  const startTime = Date.now();
3437
3626
  const prep = prepareCodexRequest({
3438
3627
  prompt,
@@ -3488,7 +3677,15 @@ export function createGatewayServer(deps = {}) {
3488
3677
  const prepCleanup = "cleanup" in prep && typeof prep.cleanup === "function" ? prep.cleanup : undefined;
3489
3678
  let worktreeResolution = {};
3490
3679
  try {
3491
- worktreeResolution = await resolveWorktreeForRequest(worktree, sessionId, runtime);
3680
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
3681
+ provider: "codex",
3682
+ workspace,
3683
+ worktree,
3684
+ sessionId,
3685
+ runtime,
3686
+ workingDir,
3687
+ addDir,
3688
+ });
3492
3689
  }
3493
3690
  catch (err) {
3494
3691
  return createErrorResponse("codex_request", 1, "", corrId, err);
@@ -3610,13 +3807,14 @@ export function createGatewayServer(deps = {}) {
3610
3807
  .max(3_600_000)
3611
3808
  .optional()
3612
3809
  .describe("Idle timeout in ms (min 30s, max 1h, omit=CLI default)"),
3810
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
3613
3811
  }, {
3614
3812
  title: "Fork Codex session",
3615
3813
  readOnlyHint: false,
3616
3814
  destructiveHint: true,
3617
3815
  idempotentHint: false,
3618
3816
  openWorldHint: true,
3619
- }, async ({ prompt, sessionId, forkLast, model, sandboxMode, askForApproval, correlationId, idleTimeoutMs, }) => {
3817
+ }, async ({ prompt, sessionId, forkLast, model, sandboxMode, askForApproval, correlationId, idleTimeoutMs, workspace, }) => {
3620
3818
  const corrId = correlationId || randomUUID();
3621
3819
  const startTime = Date.now();
3622
3820
  let durationMs = 0;
@@ -3656,7 +3854,13 @@ export function createGatewayServer(deps = {}) {
3656
3854
  const finalArgs = [forkArgs[0], ...flagSegment, ...forkArgs.slice(1)];
3657
3855
  logger.info(`[${corrId}] codex_fork_session invoked (forkLast=${Boolean(forkLast)}, sessionId=${sessionId ? "set" : "unset"})`);
3658
3856
  try {
3659
- const result = await awaitJobOrDefer("codex", finalArgs, corrId, resolveIdleTimeout("codex", idleTimeoutMs), undefined, false, runtime);
3857
+ const worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
3858
+ provider: "codex",
3859
+ workspace,
3860
+ sessionId,
3861
+ runtime,
3862
+ });
3863
+ const result = await awaitJobOrDefer("codex", finalArgs, corrId, resolveIdleTimeout("codex", idleTimeoutMs), undefined, false, runtime, undefined, undefined, undefined, undefined, undefined, worktreeResolution.cwd);
3660
3864
  if (isDeferredResponse(result)) {
3661
3865
  return buildDeferredToolResponse(result, sessionId);
3662
3866
  }
@@ -3678,23 +3882,23 @@ export function createGatewayServer(deps = {}) {
3678
3882
  performanceMetrics.recordRequest("codex", finalizedDurationMs, wasSuccessful);
3679
3883
  }
3680
3884
  });
3681
- server.tool("gemini_request", "Run a Google Gemini CLI request synchronously (when async jobs are enabled, auto-defers to a pollable job past the sync deadline; otherwise runs to completion). Requires exactly one of prompt or promptParts.", {
3885
+ server.tool("gemini_request", "Run a Google Antigravity CLI (`agy`) request through the Gemini-compatible gateway tool synchronously (when async jobs are enabled, auto-defers to a pollable job past the sync deadline; otherwise runs to completion). Requires exactly one of prompt or promptParts.", {
3682
3886
  prompt: z
3683
3887
  .string()
3684
3888
  .min(1, "Prompt cannot be empty")
3685
3889
  .max(100000, "Prompt too long (max 100k chars)")
3686
3890
  .optional()
3687
- .describe("Prompt text for Gemini (mutually exclusive with promptParts)"),
3891
+ .describe("Prompt text for Antigravity CLI (mutually exclusive with promptParts)"),
3688
3892
  promptParts: PromptPartsSchema.optional().describe("Cache-aware structured prompt: { system?, tools?, context?, task }. Mutually exclusive with prompt. Stable parts hash into cache_state for prefix-discipline tracking."),
3689
3893
  model: z
3690
3894
  .string()
3691
3895
  .optional()
3692
- .describe("Model name or alias (e.g. gemini-3-pro-preview, gemini-2.5-flash, pro, flash, latest)"),
3896
+ .describe("Model name or alias passed to agy --model (e.g. gemini-3-pro-preview, gemini-2.5-flash, pro, flash, latest)"),
3693
3897
  sessionId: z
3694
3898
  .string()
3695
3899
  .optional()
3696
- .describe("Gemini session ID to resume (emits --resume <id>), or 'latest' for the most recent session in this cwd"),
3697
- resumeLatest: z.boolean().default(false).describe("Resume latest session"),
3900
+ .describe("Antigravity conversation ID to resume (emits --conversation <id>)"),
3901
+ resumeLatest: z.boolean().default(false).describe("Continue the most recent conversation"),
3698
3902
  createNewSession: z.boolean().default(false).describe("Force new session"),
3699
3903
  approvalMode: z
3700
3904
  .enum(GEMINI_APPROVAL_MODES)
@@ -3710,13 +3914,16 @@ export function createGatewayServer(deps = {}) {
3710
3914
  .describe("Approval policy override"),
3711
3915
  mcpServers: z
3712
3916
  .array(MCP_SERVER_ENUM)
3713
- .default(["sqry"])
3714
- .describe("MCP server names passed to Gemini as --allowed-mcp-server-names"),
3917
+ .default([])
3918
+ .describe("Unsupported for Antigravity CLI; non-empty values are rejected"),
3715
3919
  allowedTools: z
3716
3920
  .array(z.string())
3717
3921
  .optional()
3718
- .describe("Allowed tools (['Write','Edit','Bash'])"),
3719
- includeDirs: z.array(z.string()).optional().describe("Additional workspace directories"),
3922
+ .describe("Unsupported for Antigravity CLI; non-empty values are rejected"),
3923
+ includeDirs: z
3924
+ .array(z.string())
3925
+ .optional()
3926
+ .describe("Additional workspace directories passed as --add-dir"),
3720
3927
  correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
3721
3928
  optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
3722
3929
  optimizeResponse: z.boolean().default(false).describe("Optimize response output"),
@@ -3734,19 +3941,20 @@ export function createGatewayServer(deps = {}) {
3734
3941
  outputFormat: z
3735
3942
  .enum(["text", "json", "stream-json"])
3736
3943
  .default("text")
3737
- .describe("Gemini output format. `json` emits `-o json` (single JSON with usageMetadata). `stream-json` emits `-o stream-json` (NDJSON event stream — `init`/`message`/`result` lines, usage extracted from the terminal `result.stats` event). Both report usage to the flight recorder."),
3738
- sandbox: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.sandbox.describe("Run Gemini in sandbox mode (-s)"),
3739
- policyFiles: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.policyFiles.describe("Policy file paths (--policy <path>, one per file). Paths must exist."),
3740
- adminPolicyFiles: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.adminPolicyFiles.describe("Admin policy file paths (--admin-policy <path>, one per file). Paths must exist."),
3741
- attachments: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.attachments.describe("Absolute file paths prepended as @<path> tokens to the prompt"),
3944
+ .describe("Antigravity CLI currently supports text output only through the gateway; json and stream-json are rejected."),
3945
+ sandbox: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.sandbox.describe("Run Antigravity in sandbox mode (--sandbox)"),
3946
+ policyFiles: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.policyFiles.describe("Unsupported for Antigravity CLI; non-empty values are rejected."),
3947
+ adminPolicyFiles: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.adminPolicyFiles.describe("Unsupported for Antigravity CLI; non-empty values are rejected."),
3948
+ attachments: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.attachments.describe("Unsupported for Antigravity CLI; non-empty values are rejected."),
3742
3949
  skipTrust: z
3743
3950
  .boolean()
3744
3951
  .default(false)
3745
- .describe("Emit `--skip-trust` so Gemini trusts the workspace for this session and skips the interactive trust prompt (Phase 4 slice γ). Required for headless runs in fresh workspaces."),
3952
+ .describe("Unsupported for Antigravity CLI; true is rejected."),
3746
3953
  yolo: z
3747
3954
  .boolean()
3748
3955
  .optional()
3749
- .describe("Emit `--yolo` to auto-approve all actions. Equivalent to approvalMode 'yolo'; routed through the same approval gate. Under mcp_managed the gate still decides."),
3956
+ .describe("Emit `--dangerously-skip-permissions` to auto-approve all actions. Routed through the same approval gate. Under mcp_managed the gate still decides."),
3957
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
3750
3958
  worktree: WORKTREE_SCHEMA.optional(),
3751
3959
  }, {
3752
3960
  title: "Gemini request",
@@ -3754,7 +3962,7 @@ export function createGatewayServer(deps = {}) {
3754
3962
  destructiveHint: true,
3755
3963
  idempotentHint: false,
3756
3964
  openWorldHint: true,
3757
- }, async ({ prompt, promptParts, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, outputFormat, sandbox, policyFiles, adminPolicyFiles, attachments, skipTrust, yolo, worktree, }) => {
3965
+ }, async ({ prompt, promptParts, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, outputFormat, sandbox, policyFiles, adminPolicyFiles, attachments, skipTrust, yolo, workspace, worktree, }) => {
3758
3966
  return handleGeminiRequest({ sessionManager, logger, runtime }, {
3759
3967
  prompt,
3760
3968
  promptParts,
@@ -3780,6 +3988,7 @@ export function createGatewayServer(deps = {}) {
3780
3988
  attachments,
3781
3989
  skipTrust,
3782
3990
  yolo,
3991
+ workspace,
3783
3992
  worktree,
3784
3993
  });
3785
3994
  });
@@ -3792,10 +4001,7 @@ export function createGatewayServer(deps = {}) {
3792
4001
  .describe("Prompt text for Grok (mutually exclusive with promptParts)"),
3793
4002
  promptParts: PromptPartsSchema.optional().describe("Cache-aware structured prompt: { system?, tools?, context?, task }. Mutually exclusive with prompt. Stable parts hash into cache_state for prefix-discipline tracking."),
3794
4003
  model: z.string().optional().describe("Model name or alias (e.g. grok-build, latest)"),
3795
- outputFormat: z
3796
- .enum(["plain", "json", "streaming-json"])
3797
- .optional()
3798
- .describe("Output format (plain|json|streaming-json). Grok default is plain."),
4004
+ ...GROK_GENERATED_SHAPE,
3799
4005
  sessionId: z
3800
4006
  .string()
3801
4007
  .optional()
@@ -3813,11 +4019,6 @@ export function createGatewayServer(deps = {}) {
3813
4019
  .enum(["default", "acceptEdits", "auto", "dontAsk", "bypassPermissions", "plan"])
3814
4020
  .optional()
3815
4021
  .describe("Grok permission mode"),
3816
- effort: z
3817
- .enum(["low", "medium", "high", "xhigh", "max"])
3818
- .optional()
3819
- .describe("Grok effort level"),
3820
- reasoningEffort: z.string().optional().describe("Reasoning effort for reasoning models"),
3821
4022
  approvalStrategy: z
3822
4023
  .enum(["legacy", "mcp_managed"])
3823
4024
  .default("legacy")
@@ -3830,14 +4031,6 @@ export function createGatewayServer(deps = {}) {
3830
4031
  .array(MCP_SERVER_ENUM)
3831
4032
  .default(["sqry"])
3832
4033
  .describe("MCP server names for approval tracking (Grok manages its own MCP config via `grok mcp`)"),
3833
- allowedTools: z
3834
- .array(z.string())
3835
- .optional()
3836
- .describe("Allowed built-in tools (passed as --tools comma list)"),
3837
- disallowedTools: z
3838
- .array(z.string())
3839
- .optional()
3840
- .describe("Disallowed built-in tools (passed as --disallowed-tools comma list)"),
3841
4034
  correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
3842
4035
  optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
3843
4036
  optimizeResponse: z.boolean().default(false).describe("Optimize response output"),
@@ -3852,111 +4045,19 @@ export function createGatewayServer(deps = {}) {
3852
4045
  .boolean()
3853
4046
  .default(false)
3854
4047
  .describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
3855
- 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."),
3856
- workingDir: z
3857
- .string()
3858
- .min(1)
3859
- .optional()
3860
- .describe("Grok --cwd <DIR>: working directory for this invocation. Lets headless callers run Grok against a directory other than the gateway process's cwd."),
3861
- sandbox: z
3862
- .string()
3863
- .min(1)
3864
- .optional()
3865
- .describe("Grok --sandbox <PROFILE>: sandbox profile for filesystem and network access. Freeform per `grok --help` (no enum constraint on Grok 0.1.210); also settable via GROK_SANDBOX env var. Caller responsibility to pass a valid profile name."),
3866
- rules: z
3867
- .string()
3868
- .min(1)
3869
- .optional()
3870
- .describe("Grok --rules <RULES>: extra rules to append to the system prompt. Supports `@file` prefix per `grok --help` to load from a file; gateway passes the value verbatim and lets Grok parse the prefix."),
3871
- systemPromptOverride: z
3872
- .string()
3873
- .min(1)
3874
- .optional()
3875
- .describe("Grok --system-prompt-override <PROMPT>: replace the agent's system prompt entirely. Distinct from Claude's --system-prompt / --append-system-prompt (Grok has only one override flag, not a pair)."),
3876
- allow: z
3877
- .array(z.string())
3878
- .optional()
3879
- .describe('Grok --allow <RULE>: permission allow rules. Each entry is emitted as its own --allow instance (per `grok --help`: "Repeat to add multiple rules").'),
3880
- deny: z
3881
- .array(z.string())
3882
- .optional()
3883
- .describe('Grok --deny <RULE>: permission deny rules. Each entry is emitted as its own --deny instance (per `grok --help`: "Repeat to add multiple rules").'),
3884
- compactionMode: z
3885
- .enum(["summary", "transcript", "segments"])
3886
- .optional()
3887
- .describe("Grok --compaction-mode: summary (default; no pointer) | transcript (points at the raw transcript) | segments (persists per-segment markdown to grep). Sets GROK_COMPACTION_MODE."),
3888
- compactionDetail: z
3889
- .enum(["none", "minimal", "balanced", "verbose"])
3890
- .optional()
3891
- .describe("Grok --compaction-detail: verbatim segment detail (none|minimal|balanced|verbose, default verbose). Only affects `--compaction-mode segments`. Sets GROK_COMPACTION_DETAIL."),
3892
- agent: z
3893
- .string()
3894
- .min(1)
3895
- .optional()
3896
- .describe("Grok --agent <NAME>: agent name or definition file path."),
3897
- bestOfN: MAX_TURNS_SCHEMA.optional().describe("Grok --best-of-n <N>: run the task N ways in parallel and pick the best (headless only)."),
3898
- check: z
3899
- .boolean()
3900
- .optional()
3901
- .describe("Grok --check: append a self-verification loop to the prompt (headless only)."),
3902
- disableWebSearch: z
3903
- .boolean()
3904
- .optional()
3905
- .describe("Grok --disable-web-search: disable web search and remote retrieval tools."),
3906
- todoGate: z
3907
- .boolean()
3908
- .optional()
3909
- .describe("Grok --todo-gate: enable runtime turn-end TodoGate for this session (session-scoped, not persisted)."),
3910
- verbatim: z
3911
- .boolean()
3912
- .optional()
3913
- .describe("Grok --verbatim: send the prompt exactly as given. Also skips gateway optimizePrompt when true."),
3914
4048
  agents: z
3915
4049
  .union([z.string().min(1), z.record(z.string(), z.record(z.string(), z.unknown()))])
3916
4050
  .optional()
3917
4051
  .describe("Grok --agents <JSON>: inline subagent definitions (JSON string or name → { description, prompt, … } map)."),
3918
- promptFile: z
3919
- .string()
3920
- .min(1)
3921
- .optional()
3922
- .describe("Grok --prompt-file <PATH>: single-turn prompt loaded from a file."),
3923
4052
  promptJson: z
3924
4053
  .union([z.string(), z.array(z.unknown()), z.record(z.string(), z.unknown())])
3925
4054
  .optional()
3926
4055
  .describe("Grok --prompt-json <JSON>: single-turn prompt JSON blocks (string or serializable value)."),
3927
- single: z
3928
- .string()
3929
- .min(1)
3930
- .optional()
3931
- .describe("Grok --single <PROMPT>: single-turn prompt (in addition to gateway -p)."),
3932
- experimentalMemory: z
3933
- .boolean()
3934
- .optional()
3935
- .describe("Grok --experimental-memory: enable cross-session memory."),
3936
- noAltScreen: z
3937
- .boolean()
3938
- .optional()
3939
- .describe("Grok --no-alt-screen: run inline without alt screen."),
3940
- noMemory: z.boolean().optional().describe("Grok --no-memory: disable cross-session memory."),
3941
- noPlan: z.boolean().optional().describe("Grok --no-plan: disable plan mode."),
3942
- noSubagents: z
3943
- .boolean()
3944
- .optional()
3945
- .describe("Grok --no-subagents: disable subagent spawning."),
3946
- oauth: z.boolean().optional().describe("Grok --oauth: use OAuth during authentication."),
3947
- restoreCode: z
3948
- .boolean()
3949
- .optional()
3950
- .describe("Grok --restore-code: check out the original session commit when resuming."),
3951
- leaderSocket: z
3952
- .string()
3953
- .min(1)
3954
- .optional()
3955
- .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."),
3956
4056
  nativeWorktree: z
3957
4057
  .union([z.boolean(), z.string().min(1)])
3958
4058
  .optional()
3959
4059
  .describe("Grok -w/--worktree: native CLI worktree flag (`true` → bare `--worktree`, string → named). NOT gateway slice λ `worktree`."),
4060
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
3960
4061
  worktree: WORKTREE_SCHEMA.optional(),
3961
4062
  }, {
3962
4063
  title: "Grok request",
@@ -3964,7 +4065,7 @@ export function createGatewayServer(deps = {}) {
3964
4065
  destructiveHint: true,
3965
4066
  idempotentHint: false,
3966
4067
  openWorldHint: true,
3967
- }, 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, }) => {
4068
+ }, 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, workspace, worktree, }) => {
3968
4069
  return handleGrokRequest({ sessionManager, logger, runtime }, {
3969
4070
  prompt,
3970
4071
  promptParts,
@@ -4015,6 +4116,7 @@ export function createGatewayServer(deps = {}) {
4015
4116
  restoreCode,
4016
4117
  leaderSocket,
4017
4118
  nativeWorktree,
4119
+ workspace,
4018
4120
  worktree,
4019
4121
  });
4020
4122
  });
@@ -4097,6 +4199,7 @@ export function createGatewayServer(deps = {}) {
4097
4199
  .array(z.string())
4098
4200
  .optional()
4099
4201
  .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)."),
4202
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4100
4203
  worktree: WORKTREE_SCHEMA.optional(),
4101
4204
  }, {
4102
4205
  title: "Mistral Vibe request",
@@ -4104,7 +4207,7 @@ export function createGatewayServer(deps = {}) {
4104
4207
  destructiveHint: true,
4105
4208
  idempotentHint: false,
4106
4209
  openWorldHint: true,
4107
- }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, trust, maxTurns, maxPrice, maxTokens, workingDir, addDir, worktree, }) => {
4210
+ }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, trust, maxTurns, maxPrice, maxTokens, workingDir, addDir, workspace, worktree, }) => {
4108
4211
  return handleMistralRequest({ sessionManager, logger, runtime }, {
4109
4212
  prompt,
4110
4213
  promptParts,
@@ -4130,6 +4233,7 @@ export function createGatewayServer(deps = {}) {
4130
4233
  maxTokens,
4131
4234
  workingDir,
4132
4235
  addDir,
4236
+ workspace,
4133
4237
  worktree,
4134
4238
  });
4135
4239
  });
@@ -4242,6 +4346,7 @@ export function createGatewayServer(deps = {}) {
4242
4346
  .array(z.string())
4243
4347
  .optional()
4244
4348
  .describe('Claude --tools: restrict the available built-in tool set (distinct from allowedTools permission gating). Pass [""] to disable all tools.'),
4349
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4245
4350
  worktree: WORKTREE_SCHEMA.optional(),
4246
4351
  approvalStrategy: z
4247
4352
  .enum(["legacy", "mcp_managed"])
@@ -4278,7 +4383,7 @@ export function createGatewayServer(deps = {}) {
4278
4383
  destructiveHint: true,
4279
4384
  idempotentHint: false,
4280
4385
  openWorldHint: true,
4281
- }, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, addDir, noSessionPersistence, settingSources, settings, tools, worktree, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, }) => {
4386
+ }, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, addDir, noSessionPersistence, settingSources, settings, tools, workspace, worktree, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, }) => {
4282
4387
  if (systemPrompt !== undefined && appendSystemPrompt !== undefined) {
4283
4388
  return createErrorResponse("claude", 1, "", correlationId, new Error("systemPrompt and appendSystemPrompt are mutually exclusive; use one or the other (not both)."));
4284
4389
  }
@@ -4349,7 +4454,14 @@ export function createGatewayServer(deps = {}) {
4349
4454
  });
4350
4455
  let worktreeResolution = {};
4351
4456
  try {
4352
- worktreeResolution = await resolveWorktreeForRequest(worktree, effectiveSessionId, runtime);
4457
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
4458
+ provider: "claude",
4459
+ workspace,
4460
+ worktree,
4461
+ sessionId: effectiveSessionId,
4462
+ runtime,
4463
+ addDir,
4464
+ });
4353
4465
  }
4354
4466
  catch (err) {
4355
4467
  return createErrorResponse("claude_request_async", 1, "", corrId, err);
@@ -4489,6 +4601,7 @@ export function createGatewayServer(deps = {}) {
4489
4601
  .array(z.string())
4490
4602
  .optional()
4491
4603
  .describe("Codex --add-dir <DIR>: additional writable workspace directories (repeat per entry). New sessions only."),
4604
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4492
4605
  worktree: WORKTREE_SCHEMA.optional(),
4493
4606
  }, {
4494
4607
  title: "Codex request (async job)",
@@ -4496,7 +4609,7 @@ export function createGatewayServer(deps = {}) {
4496
4609
  destructiveHint: true,
4497
4610
  idempotentHint: false,
4498
4611
  openWorldHint: true,
4499
- }, 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, worktree, }) => {
4612
+ }, 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, workspace, worktree, }) => {
4500
4613
  return handleCodexRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
4501
4614
  prompt,
4502
4615
  promptParts,
@@ -4527,26 +4640,27 @@ export function createGatewayServer(deps = {}) {
4527
4640
  ignoreRules,
4528
4641
  workingDir,
4529
4642
  addDir,
4643
+ workspace,
4530
4644
  worktree,
4531
4645
  });
4532
4646
  });
4533
- server.tool("gemini_request_async", "Start a Google Gemini CLI request as a durable background job. Poll with llm_job_status, collect with llm_job_result.", {
4647
+ server.tool("gemini_request_async", "Start a Google Antigravity CLI (`agy`) request as a durable background job through the Gemini-compatible gateway tool. Poll with llm_job_status, collect with llm_job_result.", {
4534
4648
  prompt: z
4535
4649
  .string()
4536
4650
  .min(1, "Prompt cannot be empty")
4537
4651
  .max(100000, "Prompt too long (max 100k chars)")
4538
4652
  .optional()
4539
- .describe("Prompt text for Gemini (mutually exclusive with promptParts)"),
4653
+ .describe("Prompt text for Antigravity CLI (mutually exclusive with promptParts)"),
4540
4654
  promptParts: PromptPartsSchema.optional().describe("Cache-aware structured prompt: { system?, tools?, context?, task }. Mutually exclusive with prompt. Stable parts hash into cache_state for prefix-discipline tracking."),
4541
4655
  model: z
4542
4656
  .string()
4543
4657
  .optional()
4544
- .describe("Model name or alias (e.g. gemini-3-pro-preview, gemini-2.5-flash, pro, flash, latest)"),
4658
+ .describe("Model name or alias passed to agy --model (e.g. gemini-3-pro-preview, gemini-2.5-flash, pro, flash, latest)"),
4545
4659
  sessionId: z
4546
4660
  .string()
4547
4661
  .optional()
4548
- .describe("Gemini session ID to resume (emits --resume <id>), or 'latest' for the most recent session in this cwd"),
4549
- resumeLatest: z.boolean().default(false).describe("Resume latest session"),
4662
+ .describe("Antigravity conversation ID to resume (emits --conversation <id>)"),
4663
+ resumeLatest: z.boolean().default(false).describe("Continue the most recent conversation"),
4550
4664
  createNewSession: z.boolean().default(false).describe("Force new session"),
4551
4665
  approvalMode: z
4552
4666
  .enum(GEMINI_APPROVAL_MODES)
@@ -4562,13 +4676,16 @@ export function createGatewayServer(deps = {}) {
4562
4676
  .describe("Approval policy override"),
4563
4677
  mcpServers: z
4564
4678
  .array(MCP_SERVER_ENUM)
4565
- .default(["sqry"])
4566
- .describe("MCP server names passed to Gemini as --allowed-mcp-server-names"),
4679
+ .default([])
4680
+ .describe("Unsupported for Antigravity CLI; non-empty values are rejected"),
4567
4681
  allowedTools: z
4568
4682
  .array(z.string())
4569
4683
  .optional()
4570
- .describe("Allowed tools (['Write','Edit','Bash'])"),
4571
- includeDirs: z.array(z.string()).optional().describe("Additional workspace directories"),
4684
+ .describe("Unsupported for Antigravity CLI; non-empty values are rejected"),
4685
+ includeDirs: z
4686
+ .array(z.string())
4687
+ .optional()
4688
+ .describe("Additional workspace directories passed as --add-dir"),
4572
4689
  correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
4573
4690
  optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
4574
4691
  idleTimeoutMs: z
@@ -4585,19 +4702,20 @@ export function createGatewayServer(deps = {}) {
4585
4702
  outputFormat: z
4586
4703
  .enum(["text", "json", "stream-json"])
4587
4704
  .default("text")
4588
- .describe("Gemini output format. `json` emits `-o json` (single JSON with usageMetadata). `stream-json` emits `-o stream-json` (NDJSON event stream — `init`/`message`/`result` lines, usage extracted from the terminal `result.stats` event). Both report usage to the flight recorder."),
4589
- sandbox: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.sandbox.describe("Run Gemini in sandbox mode (-s)"),
4590
- policyFiles: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.policyFiles.describe("Policy file paths (--policy <path>, one per file). Paths must exist."),
4591
- adminPolicyFiles: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.adminPolicyFiles.describe("Admin policy file paths (--admin-policy <path>, one per file). Paths must exist."),
4592
- attachments: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.attachments.describe("Absolute file paths prepended as @<path> tokens to the prompt"),
4705
+ .describe("Antigravity CLI currently supports text output only through the gateway; json and stream-json are rejected."),
4706
+ sandbox: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.sandbox.describe("Run Antigravity in sandbox mode (--sandbox)"),
4707
+ policyFiles: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.policyFiles.describe("Unsupported for Antigravity CLI; non-empty values are rejected."),
4708
+ adminPolicyFiles: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.adminPolicyFiles.describe("Unsupported for Antigravity CLI; non-empty values are rejected."),
4709
+ attachments: GEMINI_HIGH_IMPACT_PARAMS_SCHEMA.shape.attachments.describe("Unsupported for Antigravity CLI; non-empty values are rejected."),
4593
4710
  skipTrust: z
4594
4711
  .boolean()
4595
4712
  .default(false)
4596
- .describe("Emit `--skip-trust` so Gemini trusts the workspace for this session and skips the interactive trust prompt (Phase 4 slice γ). Required for headless runs in fresh workspaces."),
4713
+ .describe("Unsupported for Antigravity CLI; true is rejected."),
4597
4714
  yolo: z
4598
4715
  .boolean()
4599
4716
  .optional()
4600
- .describe("Emit `--yolo` to auto-approve all actions. Equivalent to approvalMode 'yolo'; routed through the same approval gate. Under mcp_managed the gate still decides."),
4717
+ .describe("Emit `--dangerously-skip-permissions` to auto-approve all actions. Routed through the same approval gate. Under mcp_managed the gate still decides."),
4718
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4601
4719
  worktree: WORKTREE_SCHEMA.optional(),
4602
4720
  }, {
4603
4721
  title: "Gemini request (async job)",
@@ -4605,7 +4723,7 @@ export function createGatewayServer(deps = {}) {
4605
4723
  destructiveHint: true,
4606
4724
  idempotentHint: false,
4607
4725
  openWorldHint: true,
4608
- }, async ({ prompt, promptParts, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, outputFormat, sandbox, policyFiles, adminPolicyFiles, attachments, skipTrust, yolo, worktree, }) => {
4726
+ }, async ({ prompt, promptParts, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, outputFormat, sandbox, policyFiles, adminPolicyFiles, attachments, skipTrust, yolo, workspace, worktree, }) => {
4609
4727
  return handleGeminiRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
4610
4728
  prompt,
4611
4729
  promptParts,
@@ -4630,6 +4748,7 @@ export function createGatewayServer(deps = {}) {
4630
4748
  attachments,
4631
4749
  skipTrust,
4632
4750
  yolo,
4751
+ workspace,
4633
4752
  worktree,
4634
4753
  });
4635
4754
  });
@@ -4809,6 +4928,7 @@ export function createGatewayServer(deps = {}) {
4809
4928
  .union([z.boolean(), z.string().min(1)])
4810
4929
  .optional()
4811
4930
  .describe("Grok -w/--worktree: native CLI worktree flag (`true` → bare `--worktree`, string → named). NOT gateway slice λ `worktree`."),
4931
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4812
4932
  worktree: WORKTREE_SCHEMA.optional(),
4813
4933
  }, {
4814
4934
  title: "Grok request (async job)",
@@ -4816,7 +4936,7 @@ export function createGatewayServer(deps = {}) {
4816
4936
  destructiveHint: true,
4817
4937
  idempotentHint: false,
4818
4938
  openWorldHint: true,
4819
- }, 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, }) => {
4939
+ }, 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, workspace, worktree, }) => {
4820
4940
  return handleGrokRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
4821
4941
  prompt,
4822
4942
  promptParts,
@@ -4866,6 +4986,7 @@ export function createGatewayServer(deps = {}) {
4866
4986
  restoreCode,
4867
4987
  leaderSocket,
4868
4988
  nativeWorktree,
4989
+ workspace,
4869
4990
  worktree,
4870
4991
  });
4871
4992
  });
@@ -4947,6 +5068,7 @@ export function createGatewayServer(deps = {}) {
4947
5068
  .array(z.string())
4948
5069
  .optional()
4949
5070
  .describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance."),
5071
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4950
5072
  worktree: WORKTREE_SCHEMA.optional(),
4951
5073
  }, {
4952
5074
  title: "Mistral Vibe request (async job)",
@@ -4954,7 +5076,7 @@ export function createGatewayServer(deps = {}) {
4954
5076
  destructiveHint: true,
4955
5077
  idempotentHint: false,
4956
5078
  openWorldHint: true,
4957
- }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, trust, maxTurns, maxPrice, maxTokens, workingDir, addDir, worktree, }) => {
5079
+ }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, trust, maxTurns, maxPrice, maxTokens, workingDir, addDir, workspace, worktree, }) => {
4958
5080
  return handleMistralRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
4959
5081
  prompt,
4960
5082
  promptParts,
@@ -4979,6 +5101,7 @@ export function createGatewayServer(deps = {}) {
4979
5101
  maxTokens,
4980
5102
  workingDir,
4981
5103
  addDir,
5104
+ workspace,
4982
5105
  worktree,
4983
5106
  });
4984
5107
  });
@@ -5845,6 +5968,210 @@ async function shutdown(signal) {
5845
5968
  }
5846
5969
  process.on("SIGTERM", () => shutdown("SIGTERM"));
5847
5970
  process.on("SIGINT", () => shutdown("SIGINT"));
5971
+ function readMutableGatewayConfig(configPath = defaultGatewayConfigPath()) {
5972
+ if (!existsSync(configPath))
5973
+ return {};
5974
+ const require = createRequire(import.meta.url);
5975
+ const TOML = require("smol-toml");
5976
+ return TOML.parse(readFileSync(configPath, "utf8"));
5977
+ }
5978
+ function writeMutableGatewayConfig(data, configPath = defaultGatewayConfigPath()) {
5979
+ const require = createRequire(import.meta.url);
5980
+ const TOML = require("smol-toml");
5981
+ mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
5982
+ writeFileSync(configPath, TOML.stringify(data), { mode: 0o600 });
5983
+ chmodSync(configPath, 0o600);
5984
+ }
5985
+ function ensureOAuthTable(config) {
5986
+ config.http ??= {};
5987
+ config.http.oauth ??= {};
5988
+ const oauth = config.http.oauth;
5989
+ oauth.enabled ??= true;
5990
+ oauth.issuer ??= "auto";
5991
+ oauth.require_pkce ??= true;
5992
+ oauth.registration_policy ??= "static_clients";
5993
+ oauth.allow_public_clients ??= false;
5994
+ oauth.token_ttl_seconds ??= 3600;
5995
+ oauth.clients ??= [];
5996
+ return oauth;
5997
+ }
5998
+ function argValue(args, name) {
5999
+ const idx = args.indexOf(name);
6000
+ return idx >= 0 ? args[idx + 1] : undefined;
6001
+ }
6002
+ function requireArg(args, name) {
6003
+ const value = argValue(args, name);
6004
+ if (!value)
6005
+ throw new Error(`Missing ${name}`);
6006
+ return value;
6007
+ }
6008
+ function localBaseUrlForPrint() {
6009
+ const publicUrl = process.env.LLM_GATEWAY_PUBLIC_URL;
6010
+ if (publicUrl) {
6011
+ try {
6012
+ return new URL(publicUrl).origin;
6013
+ }
6014
+ catch {
6015
+ }
6016
+ }
6017
+ return `http://${process.env.LLM_GATEWAY_HTTP_HOST ?? "127.0.0.1"}:${process.env.LLM_GATEWAY_HTTP_PORT ?? "3333"}`;
6018
+ }
6019
+ function printJsonLine(value) {
6020
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
6021
+ }
6022
+ function runOAuthCommand(args) {
6023
+ const [scope, action] = args;
6024
+ const config = readMutableGatewayConfig();
6025
+ const oauth = ensureOAuthTable(config);
6026
+ if (scope === "client") {
6027
+ const clients = (Array.isArray(oauth.clients) ? oauth.clients : []);
6028
+ oauth.clients = clients;
6029
+ if (action === "add") {
6030
+ const clientId = args[2];
6031
+ if (!clientId)
6032
+ throw new Error("Usage: llm-cli-gateway oauth client add <client-id> --redirect-uri <uri> [--print-once]");
6033
+ const redirectUri = requireArg(args, "--redirect-uri");
6034
+ const secret = generateSecret();
6035
+ clients.push({
6036
+ client_id: clientId,
6037
+ client_secret_hash: hashSecret(secret),
6038
+ allowed_redirect_uris: [redirectUri],
6039
+ scopes: ["mcp"],
6040
+ });
6041
+ writeMutableGatewayConfig(config);
6042
+ printJsonLine({
6043
+ ok: true,
6044
+ client_id: clientId,
6045
+ ...(args.includes("--print-once") ? { client_secret: secret } : {}),
6046
+ oauth: {
6047
+ issuer: localBaseUrlForPrint(),
6048
+ authorization_url: `${localBaseUrlForPrint()}/oauth/authorize`,
6049
+ token_url: `${localBaseUrlForPrint()}/oauth/token`,
6050
+ },
6051
+ note: args.includes("--print-once")
6052
+ ? "client_secret is shown once; it is stored only as a hash."
6053
+ : "client secret generated and stored only as a hash; rerun rotate --print-once if needed.",
6054
+ });
6055
+ return;
6056
+ }
6057
+ if (action === "list") {
6058
+ printJsonLine({
6059
+ ok: true,
6060
+ clients: clients.map(client => ({
6061
+ client_id: client.client_id,
6062
+ redirect_uris: client.allowed_redirect_uris ?? [],
6063
+ secret_configured: Boolean(client.client_secret_hash),
6064
+ })),
6065
+ });
6066
+ return;
6067
+ }
6068
+ if (action === "rotate") {
6069
+ const clientId = args[2];
6070
+ const client = clients.find(candidate => candidate.client_id === clientId);
6071
+ if (!client)
6072
+ throw new Error(`Unknown OAuth client ${clientId}`);
6073
+ const secret = generateSecret();
6074
+ client.client_secret_hash = hashSecret(secret);
6075
+ writeMutableGatewayConfig(config);
6076
+ printJsonLine({
6077
+ ok: true,
6078
+ client_id: clientId,
6079
+ ...(args.includes("--print-once") ? { client_secret: secret } : {}),
6080
+ note: "Future OAuth exchanges use the rotated secret; already-issued opaque access tokens expire by token TTL or server restart.",
6081
+ });
6082
+ return;
6083
+ }
6084
+ if (action === "revoke") {
6085
+ const clientId = args[2];
6086
+ oauth.clients = clients.filter(client => client.client_id !== clientId);
6087
+ writeMutableGatewayConfig(config);
6088
+ printJsonLine({
6089
+ ok: true,
6090
+ client_id: clientId,
6091
+ note: "Future OAuth exchanges are revoked; already-issued opaque access tokens expire by token TTL or server restart.",
6092
+ });
6093
+ return;
6094
+ }
6095
+ }
6096
+ if (scope === "shared-secret") {
6097
+ if (action === "set" || action === "rotate") {
6098
+ const secret = generateSecret();
6099
+ oauth.registration_policy = "shared_secret";
6100
+ oauth.shared_secret = {
6101
+ enabled: true,
6102
+ secret_hash: hashSecret(secret),
6103
+ prompt_label: "Gateway access code",
6104
+ };
6105
+ writeMutableGatewayConfig(config);
6106
+ printJsonLine({
6107
+ ok: true,
6108
+ shared_secret_enabled: true,
6109
+ ...(args.includes("--print-once") ? { shared_secret: secret } : {}),
6110
+ note: args.includes("--print-once")
6111
+ ? "shared_secret is shown once; it is stored only as a hash."
6112
+ : "shared secret generated and stored only as a hash.",
6113
+ });
6114
+ return;
6115
+ }
6116
+ if (action === "disable") {
6117
+ oauth.shared_secret = { enabled: false, prompt_label: "Gateway access code" };
6118
+ if (oauth.registration_policy === "shared_secret")
6119
+ oauth.registration_policy = "static_clients";
6120
+ writeMutableGatewayConfig(config);
6121
+ printJsonLine({ ok: true, shared_secret_enabled: false });
6122
+ return;
6123
+ }
6124
+ }
6125
+ throw new Error("Usage: llm-cli-gateway oauth client|shared-secret ...");
6126
+ }
6127
+ function runWorkspaceCommand(args) {
6128
+ const [action] = args;
6129
+ if (action === "list") {
6130
+ const registry = loadWorkspaceRegistry(logger);
6131
+ printJsonLine({
6132
+ ok: true,
6133
+ default: registry.defaultAlias,
6134
+ workspaces: registry.repos.map(describeWorkspace),
6135
+ allowed_roots: registry.allowedRoots.map(root => ({
6136
+ alias: root.alias,
6137
+ path: root.path,
6138
+ allow_create_directories: root.allowCreateDirectories,
6139
+ allow_init_git_repos: root.allowInitGitRepos,
6140
+ })),
6141
+ });
6142
+ return;
6143
+ }
6144
+ if (action === "create") {
6145
+ const alias = args[1];
6146
+ if (!alias)
6147
+ throw new Error("Usage: llm-cli-gateway workspace create <alias> --root <root> --slug <slug> --kind folder|git [--default]");
6148
+ const repo = createWorkspace({
6149
+ alias,
6150
+ rootAlias: requireArg(args, "--root"),
6151
+ slug: requireArg(args, "--slug"),
6152
+ kind: (argValue(args, "--kind") ?? "git"),
6153
+ setDefault: args.includes("--default"),
6154
+ logger,
6155
+ });
6156
+ printJsonLine({ ok: true, workspace: describeWorkspace(repo) });
6157
+ return;
6158
+ }
6159
+ if (action === "add") {
6160
+ const alias = args[1];
6161
+ const repoPath = args[2];
6162
+ if (!alias || !repoPath)
6163
+ throw new Error("Usage: llm-cli-gateway workspace add <alias> <path> [--default]");
6164
+ const repo = registerExistingWorkspace({
6165
+ alias,
6166
+ repoPath,
6167
+ setDefault: args.includes("--default"),
6168
+ logger,
6169
+ });
6170
+ printJsonLine({ ok: true, workspace: describeWorkspace(repo) });
6171
+ return;
6172
+ }
6173
+ throw new Error("Usage: llm-cli-gateway workspace list|add|create ...");
6174
+ }
5848
6175
  async function main() {
5849
6176
  startWindowsBootstrapperSelfHeal();
5850
6177
  const args = process.argv.slice(2);
@@ -5858,6 +6185,8 @@ async function main() {
5858
6185
  "",
5859
6186
  "Usage:",
5860
6187
  " llm-cli-gateway [doctor --json|contracts --json|--transport=http|--version]",
6188
+ " llm-cli-gateway oauth client add <id> --redirect-uri <uri> [--print-once]",
6189
+ " llm-cli-gateway workspace list|add|create",
5861
6190
  "",
5862
6191
  "Doctor:",
5863
6192
  " doctor --json # environment, providers, declared contracts",
@@ -5879,6 +6208,14 @@ async function main() {
5879
6208
  process.stderr.write("Only doctor --json is supported in this layer.\n");
5880
6209
  process.exit(2);
5881
6210
  }
6211
+ if (args[0] === "oauth") {
6212
+ runOAuthCommand(args.slice(1));
6213
+ return;
6214
+ }
6215
+ if (args[0] === "workspace") {
6216
+ runWorkspaceCommand(args.slice(1));
6217
+ return;
6218
+ }
5882
6219
  if (args[0] === "contracts") {
5883
6220
  if (args.includes("--json")) {
5884
6221
  const cliArg = args.find(arg => arg.startsWith("--cli="))?.split("=")[1];