Skip to content

Commit 61978c7

Browse files
committed
fix(auth): include grok in auto agent selection
1 parent b6cb6bf commit 61978c7

4 files changed

Lines changed: 168 additions & 35 deletions

File tree

packages/app/src/docker-git/cli/usage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ Options:
7676
--up | --no-up Run docker compose up after init (default: --up)
7777
--ssh | --no-ssh Auto-open SSH after create/clone (default: clone=--ssh, create=--no-ssh)
7878
--mcp-playwright | --no-mcp-playwright Enable Playwright MCP + Chromium sidecar (default: --no-mcp-playwright)
79-
--auto[=claude|codex] Auto-execute an agent; without value picks by auth, random if both are available
79+
--auto[=claude|codex|gemini|grok] Auto-execute an agent; without value picks by auth, random if multiple are available
8080
--active apply-all: apply only to currently running containers (skip stopped ones)
8181
--force Overwrite existing files, replace conflicting docker-git projects/containers, and wipe compose volumes
8282
--force-env Reset project env defaults only (keep workspace volume/data)

packages/app/src/lib/usecases/agent-auto-select.ts

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type * as Path from "@effect/platform/Path"
55
import { Effect } from "effect"
66

77
import type { AgentMode, ParseError, TemplateConfig } from "../core/domain.js"
8+
import { hasGrokCredentials } from "./auth-grok-helpers.js"
89
import { normalizeAccountLabel } from "./auth-helpers.js"
910
import { hasNonEmptyFile } from "./auth-sync-helpers.js"
1011

@@ -14,6 +15,16 @@ const autoOptionError = (reason: string): ParseError => ({
1415
reason
1516
})
1617

18+
type AvailableAgentAuth = {
19+
readonly claudeAvailable: boolean
20+
readonly codexAvailable: boolean
21+
readonly grokAvailable: boolean
22+
}
23+
24+
const claudeMode: ReadonlyArray<AgentMode> = ["claude"]
25+
const codexMode: ReadonlyArray<AgentMode> = ["codex"]
26+
const grokMode: ReadonlyArray<AgentMode> = ["grok"]
27+
1728
const isRegularFile = (
1829
fs: FileSystem.FileSystem,
1930
filePath: string
@@ -39,6 +50,15 @@ const hasCodexAuth = (
3950
return hasNonEmptyFile(fs, authPath)
4051
}
4152

53+
const hasGrokAuth = (
54+
fs: FileSystem.FileSystem,
55+
rootPath: string,
56+
label: string | undefined
57+
): Effect.Effect<boolean, PlatformError> => {
58+
const normalized = normalizeAccountLabel(label ?? null, "default")
59+
return hasGrokCredentials(fs, `${rootPath}/${normalized}`)
60+
}
61+
4262
const resolveClaudeAccountPath = (rootPath: string, label: string | undefined): ReadonlyArray<string> => {
4363
const normalized = normalizeAccountLabel(label ?? null, "default")
4464
if (normalized !== "default") {
@@ -78,18 +98,22 @@ const resolveClaudeRoot = (codexSharedAuthPath: string): string =>
7898

7999
const resolveAvailableAgentAuth = (
80100
fs: FileSystem.FileSystem,
81-
config: Pick<TemplateConfig, "claudeAuthLabel" | "codexAuthLabel" | "codexSharedAuthPath">
82-
): Effect.Effect<{ readonly claudeAvailable: boolean; readonly codexAvailable: boolean }, PlatformError> =>
101+
config: Pick<
102+
TemplateConfig,
103+
"claudeAuthLabel" | "codexAuthLabel" | "codexSharedAuthPath" | "grokAuthLabel" | "grokAuthPath"
104+
>
105+
): Effect.Effect<AvailableAgentAuth, PlatformError> =>
83106
Effect.gen(function*(_) {
84107
const claudeAvailable = yield* _(
85108
hasClaudeAuth(fs, resolveClaudeRoot(config.codexSharedAuthPath), config.claudeAuthLabel)
86109
)
87110
const codexAvailable = yield* _(hasCodexAuth(fs, config.codexSharedAuthPath, config.codexAuthLabel))
88-
return { claudeAvailable, codexAvailable }
111+
const grokAvailable = yield* _(hasGrokAuth(fs, config.grokAuthPath, config.grokAuthLabel))
112+
return { claudeAvailable, codexAvailable, grokAvailable }
89113
})
90114

91115
const resolveExplicitAutoAgentMode = (
92-
available: { readonly claudeAvailable: boolean; readonly codexAvailable: boolean },
116+
available: AvailableAgentAuth,
93117
mode: AgentMode | undefined
94118
): Effect.Effect<AgentMode | undefined, ParseError> => {
95119
if (mode === "claude") {
@@ -102,26 +126,43 @@ const resolveExplicitAutoAgentMode = (
102126
? Effect.succeed("codex")
103127
: Effect.fail(autoOptionError("Codex auth not found"))
104128
}
129+
if (mode === "grok") {
130+
return available.grokAvailable
131+
? Effect.succeed("grok")
132+
: Effect.fail(autoOptionError("Grok auth not found"))
133+
}
105134
return Effect.sync(() => mode)
106135
}
107136

108-
const pickRandomAutoAgentMode = (
109-
available: { readonly claudeAvailable: boolean; readonly codexAvailable: boolean }
110-
): Effect.Effect<AgentMode, ParseError> => {
111-
if (!available.claudeAvailable && !available.codexAvailable) {
112-
return Effect.fail(autoOptionError("no Claude or Codex auth found"))
113-
}
114-
if (available.claudeAvailable && !available.codexAvailable) {
115-
return Effect.succeed("claude")
137+
const availableAgentModes = (available: AvailableAgentAuth): ReadonlyArray<AgentMode> => [
138+
...(available.claudeAvailable ? claudeMode : []),
139+
...(available.codexAvailable ? codexMode : []),
140+
...(available.grokAvailable ? grokMode : [])
141+
]
142+
143+
const pickRandomAutoAgentMode = (available: AvailableAgentAuth): Effect.Effect<AgentMode, ParseError> => {
144+
const modes = availableAgentModes(available)
145+
const firstMode = modes[0]
146+
if (firstMode === undefined) {
147+
return Effect.fail(autoOptionError("no Claude, Codex or Grok auth found"))
116148
}
117-
if (!available.claudeAvailable && available.codexAvailable) {
118-
return Effect.succeed("codex")
149+
if (modes.length === 1) {
150+
return Effect.succeed(firstMode)
119151
}
120-
return Effect.sync(() => (process.hrtime.bigint() % 2n === 0n ? "claude" : "codex"))
152+
return Effect.sync(() => modes[Number(process.hrtime.bigint() % BigInt(modes.length))] ?? firstMode)
121153
}
122154

123155
export const resolveAutoAgentMode = (
124-
config: Pick<TemplateConfig, "agentAuto" | "agentMode" | "claudeAuthLabel" | "codexAuthLabel" | "codexSharedAuthPath">
156+
config: Pick<
157+
TemplateConfig,
158+
| "agentAuto"
159+
| "agentMode"
160+
| "claudeAuthLabel"
161+
| "codexAuthLabel"
162+
| "codexSharedAuthPath"
163+
| "grokAuthLabel"
164+
| "grokAuthPath"
165+
>
125166
): Effect.Effect<AgentMode | undefined, ParseError | PlatformError, FileSystem.FileSystem | Path.Path> =>
126167
Effect.gen(function*(_) {
127168
const fs = yield* _(FileSystem.FileSystem)

packages/lib/src/usecases/agent-auto-select.ts

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type * as Path from "@effect/platform/Path"
44
import { Effect } from "effect"
55

66
import type { AgentMode, ParseError, TemplateConfig } from "../core/domain.js"
7+
import { hasGrokCredentials } from "./auth-grok-helpers.js"
78
import { normalizeAccountLabel } from "./auth-helpers.js"
89
import { hasNonEmptyFile } from "./auth-sync-helpers.js"
910

@@ -13,6 +14,16 @@ const autoOptionError = (reason: string): ParseError => ({
1314
reason
1415
})
1516

17+
type AvailableAgentAuth = {
18+
readonly claudeAvailable: boolean
19+
readonly codexAvailable: boolean
20+
readonly grokAvailable: boolean
21+
}
22+
23+
const claudeMode: ReadonlyArray<AgentMode> = ["claude"]
24+
const codexMode: ReadonlyArray<AgentMode> = ["codex"]
25+
const grokMode: ReadonlyArray<AgentMode> = ["grok"]
26+
1627
const isRegularFile = (
1728
fs: FileSystem.FileSystem,
1829
filePath: string
@@ -38,6 +49,15 @@ const hasCodexAuth = (
3849
return hasNonEmptyFile(fs, authPath)
3950
}
4051

52+
const hasGrokAuth = (
53+
fs: FileSystem.FileSystem,
54+
rootPath: string,
55+
label: string | undefined
56+
): Effect.Effect<boolean, PlatformError> => {
57+
const normalized = normalizeAccountLabel(label ?? null, "default")
58+
return hasGrokCredentials(fs, `${rootPath}/${normalized}`)
59+
}
60+
4161
const resolveClaudeAccountPath = (rootPath: string, label: string | undefined): ReadonlyArray<string> => {
4262
const normalized = normalizeAccountLabel(label ?? null, "default")
4363
if (normalized !== "default") {
@@ -77,18 +97,22 @@ const resolveClaudeRoot = (codexSharedAuthPath: string): string =>
7797

7898
const resolveAvailableAgentAuth = (
7999
fs: FileSystem.FileSystem,
80-
config: Pick<TemplateConfig, "claudeAuthLabel" | "codexAuthLabel" | "codexSharedAuthPath">
81-
): Effect.Effect<{ readonly claudeAvailable: boolean; readonly codexAvailable: boolean }, PlatformError> =>
100+
config: Pick<
101+
TemplateConfig,
102+
"claudeAuthLabel" | "codexAuthLabel" | "codexSharedAuthPath" | "grokAuthLabel" | "grokAuthPath"
103+
>
104+
): Effect.Effect<AvailableAgentAuth, PlatformError> =>
82105
Effect.gen(function*(_) {
83106
const claudeAvailable = yield* _(
84107
hasClaudeAuth(fs, resolveClaudeRoot(config.codexSharedAuthPath), config.claudeAuthLabel)
85108
)
86109
const codexAvailable = yield* _(hasCodexAuth(fs, config.codexSharedAuthPath, config.codexAuthLabel))
87-
return { claudeAvailable, codexAvailable }
110+
const grokAvailable = yield* _(hasGrokAuth(fs, config.grokAuthPath, config.grokAuthLabel))
111+
return { claudeAvailable, codexAvailable, grokAvailable }
88112
})
89113

90114
const resolveExplicitAutoAgentMode = (
91-
available: { readonly claudeAvailable: boolean; readonly codexAvailable: boolean },
115+
available: AvailableAgentAuth,
92116
mode: AgentMode | undefined
93117
): Effect.Effect<AgentMode | undefined, ParseError> => {
94118
if (mode === "claude") {
@@ -101,26 +125,43 @@ const resolveExplicitAutoAgentMode = (
101125
? Effect.succeed("codex")
102126
: Effect.fail(autoOptionError("Codex auth not found"))
103127
}
128+
if (mode === "grok") {
129+
return available.grokAvailable
130+
? Effect.succeed("grok")
131+
: Effect.fail(autoOptionError("Grok auth not found"))
132+
}
104133
return Effect.sync(() => mode)
105134
}
106135

107-
const pickRandomAutoAgentMode = (
108-
available: { readonly claudeAvailable: boolean; readonly codexAvailable: boolean }
109-
): Effect.Effect<AgentMode, ParseError> => {
110-
if (!available.claudeAvailable && !available.codexAvailable) {
111-
return Effect.fail(autoOptionError("no Claude or Codex auth found"))
112-
}
113-
if (available.claudeAvailable && !available.codexAvailable) {
114-
return Effect.succeed("claude")
136+
const availableAgentModes = (available: AvailableAgentAuth): ReadonlyArray<AgentMode> => [
137+
...(available.claudeAvailable ? claudeMode : []),
138+
...(available.codexAvailable ? codexMode : []),
139+
...(available.grokAvailable ? grokMode : [])
140+
]
141+
142+
const pickRandomAutoAgentMode = (available: AvailableAgentAuth): Effect.Effect<AgentMode, ParseError> => {
143+
const modes = availableAgentModes(available)
144+
const firstMode = modes[0]
145+
if (firstMode === undefined) {
146+
return Effect.fail(autoOptionError("no Claude, Codex or Grok auth found"))
115147
}
116-
if (!available.claudeAvailable && available.codexAvailable) {
117-
return Effect.succeed("codex")
148+
if (modes.length === 1) {
149+
return Effect.succeed(firstMode)
118150
}
119-
return Effect.sync(() => (process.hrtime.bigint() % 2n === 0n ? "claude" : "codex"))
151+
return Effect.sync(() => modes[Number(process.hrtime.bigint() % BigInt(modes.length))] ?? firstMode)
120152
}
121153

122154
export const resolveAutoAgentMode = (
123-
config: Pick<TemplateConfig, "agentAuto" | "agentMode" | "claudeAuthLabel" | "codexAuthLabel" | "codexSharedAuthPath">
155+
config: Pick<
156+
TemplateConfig,
157+
| "agentAuto"
158+
| "agentMode"
159+
| "claudeAuthLabel"
160+
| "codexAuthLabel"
161+
| "codexSharedAuthPath"
162+
| "grokAuthLabel"
163+
| "grokAuthPath"
164+
>
124165
): Effect.Effect<AgentMode | undefined, ParseError | PlatformError, FileSystem.FileSystem | Path.Path> =>
125166
Effect.gen(function*(_) {
126167
const fs = yield* _(FileSystem.FileSystem)

packages/lib/tests/usecases/agent-auto-select.test.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,22 +116,57 @@ describe("resolveAutoAgentMode", () => {
116116
})
117117
).pipe(Effect.provide(NodeContext.layer)))
118118

119-
it.effect("returns one of the available agents when both Claude and Codex auth exist", () =>
119+
it.effect("chooses Grok when only Grok auth exists", () =>
120+
withTempDir((root) =>
121+
Effect.gen(function*(_) {
122+
const fs = yield* _(FileSystem.FileSystem)
123+
const path = yield* _(Path.Path)
124+
const config = makeConfig(root, path)
125+
const grokRoot = path.join(root, ".orch/auth/grok/default")
126+
127+
yield* _(fs.makeDirectory(grokRoot, { recursive: true }))
128+
yield* _(fs.writeFileString(path.join(grokRoot, ".api-key"), "grok-token\n"))
129+
130+
const mode = yield* _(resolveAutoAgentMode(config))
131+
expect(mode).toBe("grok")
132+
})
133+
).pipe(Effect.provide(NodeContext.layer)))
134+
135+
it.effect("keeps explicit Grok mode when Grok auth exists", () =>
136+
withTempDir((root) =>
137+
Effect.gen(function*(_) {
138+
const fs = yield* _(FileSystem.FileSystem)
139+
const path = yield* _(Path.Path)
140+
const config: TemplateConfig = { ...makeConfig(root, path), agentMode: "grok" }
141+
const grokRoot = path.join(root, ".orch/auth/grok/default")
142+
143+
yield* _(fs.makeDirectory(grokRoot, { recursive: true }))
144+
yield* _(fs.writeFileString(path.join(grokRoot, ".api-key"), "grok-token\n"))
145+
146+
const mode = yield* _(resolveAutoAgentMode(config))
147+
expect(mode).toBe("grok")
148+
})
149+
).pipe(Effect.provide(NodeContext.layer)))
150+
151+
it.effect("returns one of the available agents when multiple agent auth entries exist", () =>
120152
withTempDir((root) =>
121153
Effect.gen(function*(_) {
122154
const fs = yield* _(FileSystem.FileSystem)
123155
const path = yield* _(Path.Path)
124156
const config = makeConfig(root, path)
125157
const claudeRoot = path.join(root, ".orch/auth/claude/default")
126158
const codexRoot = path.join(root, ".orch/auth/codex")
159+
const grokRoot = path.join(root, ".orch/auth/grok/default")
127160

128161
yield* _(fs.makeDirectory(claudeRoot, { recursive: true }))
129162
yield* _(fs.makeDirectory(codexRoot, { recursive: true }))
163+
yield* _(fs.makeDirectory(grokRoot, { recursive: true }))
130164
yield* _(fs.writeFileString(path.join(claudeRoot, ".oauth-token"), "token\n"))
131165
yield* _(fs.writeFileString(path.join(codexRoot, "auth.json"), "{\"ok\":true}\n"))
166+
yield* _(fs.writeFileString(path.join(grokRoot, ".api-key"), "grok-token\n"))
132167

133168
const mode = yield* _(resolveAutoAgentMode(config))
134-
expect(["claude", "codex"]).toContain(mode)
169+
expect(["claude", "codex", "grok"]).toContain(mode)
135170
})
136171
).pipe(Effect.provide(NodeContext.layer)))
137172

@@ -167,6 +202,22 @@ describe("resolveAutoAgentMode", () => {
167202
})
168203
).pipe(Effect.provide(NodeContext.layer)))
169204

205+
it.effect("fails explicit Grok mode when Grok auth is missing", () =>
206+
withTempDir((root) =>
207+
Effect.gen(function*(_) {
208+
const path = yield* _(Path.Path)
209+
const config: TemplateConfig = { ...makeConfig(root, path), agentMode: "grok" }
210+
211+
const exit = yield* _(
212+
resolveAutoAgentMode(config).pipe(
213+
Effect.flip,
214+
Effect.map((error) => error._tag)
215+
)
216+
)
217+
expect(exit).toBe("InvalidOption")
218+
})
219+
).pipe(Effect.provide(NodeContext.layer)))
220+
170221
it.effect("fails when no auth exists", () =>
171222
withTempDir((root) =>
172223
Effect.gen(function*(_) {

0 commit comments

Comments
 (0)