Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,14 @@ bun run docker-git --help
docker-git auth github login --web
docker-git auth codex login --web
docker-git auth claude login --web
docker-git auth grok login --web
```

Grok support uses the official xAI CLI installer from `https://x.ai/cli/install.sh`
and the CLI device-code login flow. API-key auth can also be stored under the
selected Grok account label via `GROK_DEPLOYMENT_KEY`, `GROK_API_KEY`, or
`XAI_API_KEY`.

## CLI пример

Можно передавать ссылку на репозиторий, ветку (`/tree/...`), issue или PR.
Expand All @@ -44,8 +50,8 @@ docker-git clone https://github.com/ProverCoderAI/docker-git/issues/122 --force
docker-git clone https://github.com/ProverCoderAI/docker-git/issues/122 --force --auto
```

- `--auto` сам выбирает Claude или Codex по доступной авторизации. Если доступны оба, выбор случайный.
- `--auto=claude` или `--auto=codex` принудительно выбирает агента.
- `--auto` сам выбирает Claude, Codex, Gemini или Grok по доступной авторизации. Если доступно несколько, выбор случайный.
- `--auto=claude`, `--auto=codex`, `--auto=gemini` или `--auto=grok` принудительно выбирает агента.
- В auto-режиме агент сам выполняет задачу, создаёт PR и после завершения контейнер очищается.

Применение конфигурации:
Expand Down
18 changes: 15 additions & 3 deletions packages/api/src/api/contracts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type ProjectStatus = "running" | "stopped" | "unknown"

export type AgentProvider = "codex" | "opencode" | "claude" | "custom"
export type AgentProvider = "codex" | "opencode" | "claude" | "grok" | "custom"

export type AgentStatus = "starting" | "running" | "stopping" | "stopped" | "exited" | "failed"

Expand Down Expand Up @@ -183,19 +183,23 @@ export type AuthMenuFlow =
| "ClaudeLogout"
| "GeminiApiKey"
| "GeminiLogout"
| "GrokApiKey"
| "GrokLogout"

export type AuthTerminalFlow = "ClaudeOauth" | "GeminiOauth"
export type AuthTerminalFlow = "ClaudeOauth" | "GeminiOauth" | "GrokOauth"

export type AuthSnapshot = {
readonly globalEnvPath: string
readonly claudeAuthPath: string
readonly geminiAuthPath: string
readonly grokAuthPath: string
readonly totalEntries: number
readonly githubTokenEntries: number
readonly gitTokenEntries: number
readonly gitUserEntries: number
readonly claudeAuthEntries: number
readonly geminiAuthEntries: number
readonly grokAuthEntries: number
}

export type AuthMenuRequest = {
Expand Down Expand Up @@ -249,6 +253,8 @@ export type ProjectAuthFlow =
| "ProjectClaudeDisconnect"
| "ProjectGeminiConnect"
| "ProjectGeminiDisconnect"
| "ProjectGrokConnect"
| "ProjectGrokDisconnect"

export type ProjectAuthSnapshot = {
readonly projectDir: string
Expand All @@ -257,22 +263,25 @@ export type ProjectAuthSnapshot = {
readonly envProjectPath: string
readonly claudeAuthPath: string
readonly geminiAuthPath: string
readonly grokAuthPath: string
readonly githubTokenEntries: number
readonly gitTokenEntries: number
readonly claudeAuthEntries: number
readonly geminiAuthEntries: number
readonly grokAuthEntries: number
readonly activeGithubLabel: string | null
readonly activeGitLabel: string | null
readonly activeClaudeLabel: string | null
readonly activeGeminiLabel: string | null
readonly activeGrokLabel: string | null
}

export type ProjectAuthRequest = {
readonly flow: ProjectAuthFlow
readonly label?: string | null | undefined
}

export type ProjectPromptKind = "claude" | "codex" | "gemini"
export type ProjectPromptKind = "claude" | "codex" | "gemini" | "grok"

export type ProjectPromptFile = {
readonly kind: ProjectPromptKind
Expand Down Expand Up @@ -302,6 +311,7 @@ export type ProjectSkillScope =
| "claude/skills"
| "codex/skills"
| "gemini/skills"
| "grok/skills"

export type ProjectSkillFile = {
readonly id: string
Expand Down Expand Up @@ -400,6 +410,8 @@ export type CreateProjectRequest = {
readonly skipGithubAuth?: boolean | undefined
readonly codexTokenLabel?: string | undefined
readonly claudeTokenLabel?: string | undefined
readonly geminiTokenLabel?: string | undefined
readonly grokTokenLabel?: string | undefined
readonly agentAutoMode?: string | undefined
readonly up?: boolean | undefined
readonly openSsh?: boolean | undefined
Expand Down
19 changes: 13 additions & 6 deletions packages/api/src/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export const CreateProjectRequestSchema = Schema.Struct({
skipGithubAuth: OptionalBoolean,
codexTokenLabel: OptionalString,
claudeTokenLabel: OptionalString,
geminiTokenLabel: OptionalString,
grokTokenLabel: OptionalString,
agentAutoMode: OptionalString,
up: OptionalBoolean,
openSsh: OptionalBoolean,
Expand All @@ -60,10 +62,12 @@ export const AuthMenuFlowSchema = Schema.Literal(
"GitRemove",
"ClaudeLogout",
"GeminiApiKey",
"GeminiLogout"
"GeminiLogout",
"GrokApiKey",
"GrokLogout"
)

export const AuthTerminalFlowSchema = Schema.Literal("ClaudeOauth", "GeminiOauth")
export const AuthTerminalFlowSchema = Schema.Literal("ClaudeOauth", "GeminiOauth", "GrokOauth")

export const AuthMenuRequestSchema = Schema.Struct({
flow: AuthMenuFlowSchema,
Expand Down Expand Up @@ -107,15 +111,17 @@ export const ProjectAuthFlowSchema = Schema.Literal(
"ProjectClaudeConnect",
"ProjectClaudeDisconnect",
"ProjectGeminiConnect",
"ProjectGeminiDisconnect"
"ProjectGeminiDisconnect",
"ProjectGrokConnect",
"ProjectGrokDisconnect"
)

export const ProjectAuthRequestSchema = Schema.Struct({
flow: ProjectAuthFlowSchema,
label: OptionalNullableString
})

export const ProjectPromptKindSchema = Schema.Literal("claude", "codex", "gemini")
export const ProjectPromptKindSchema = Schema.Literal("claude", "codex", "gemini", "grok")

export const ProjectPromptUpdateRequestSchema = Schema.Struct({
content: Schema.String
Expand All @@ -127,7 +133,8 @@ export const ProjectSkillScopeSchema = Schema.Literal(
"agents/.skills",
"claude/skills",
"codex/skills",
"gemini/skills"
"gemini/skills",
"grok/skills"
)

export const ProjectSkillUpdateRequestSchema = Schema.Struct({
Expand Down Expand Up @@ -246,7 +253,7 @@ export const ProjectDatabaseForwardSchema = Schema.Struct({
targetPort: Schema.Number
})

export const AgentProviderSchema = Schema.Literal("codex", "opencode", "claude", "custom")
export const AgentProviderSchema = Schema.Literal("codex", "opencode", "claude", "grok", "custom")

export const AgentEnvVarSchema = Schema.Struct({
key: Schema.String,
Expand Down
20 changes: 16 additions & 4 deletions packages/api/src/auth-terminal-runner.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { authClaudeLogin, authGeminiLoginOauth } from "@effect-template/lib"
import { authClaudeLogin, authGeminiLoginOauth, authGrokLoginOauth } from "@effect-template/lib"
import { Effect, Match } from "effect"

type AuthTerminalRunnerFlow = "ClaudeOauth" | "GeminiOauth"
type AuthTerminalRunnerFlow = "ClaudeOauth" | "GeminiOauth" | "GrokOauth"

const parseFlow = (value: string | undefined): AuthTerminalRunnerFlow =>
value === "ClaudeOauth" || value === "GeminiOauth" ? value : "ClaudeOauth"
const parseFlow = (value: string | undefined): AuthTerminalRunnerFlow => {
if (value === "ClaudeOauth" || value === "GeminiOauth" || value === "GrokOauth") {
return value
}
process.stderr.write(`Unsupported auth terminal flow: ${value ?? "<empty>"}\n`)
process.exit(2)
}

const parseLabel = (value: string | undefined): string | null => {
const trimmed = value?.trim() ?? ""
Expand All @@ -29,6 +34,13 @@ const program = Match.value(flow).pipe(
geminiAuthPath: ".docker-git/.orch/auth/gemini",
isWeb: false
})),
Match.when("GrokOauth", () =>
authGrokLoginOauth({
_tag: "AuthGrokLogin",
label,
grokAuthPath: ".docker-git/.orch/auth/grok",
isWeb: false
})),
Match.exhaustive
)

Expand Down
86 changes: 37 additions & 49 deletions packages/api/src/http.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Chunk, Duration, Effect, Ref } from "effect"
import { Chunk, Duration, Effect, Match, Ref } from "effect"
import * as Stream from "effect/Stream"
import type { PlatformError } from "@effect/platform/Error"
import type * as HttpBody from "@effect/platform/HttpBody"
Expand Down Expand Up @@ -182,7 +182,7 @@ const ProjectDatabaseProfileParamsSchema = Schema.Struct({

const ProjectPromptParamsSchema = Schema.Struct({
projectId: Schema.String,
kind: Schema.Literal("claude", "codex", "gemini")
kind: Schema.Literal("claude", "codex", "gemini", "grok")
})

const ProjectSkillParamsSchema = Schema.Struct({
Expand Down Expand Up @@ -427,55 +427,43 @@ const readProjectSkillUpdateRequest = () => HttpServerRequest.schemaBodyJson(Pro
const readActiveProjectTerminalSessionRequest = () =>
HttpServerRequest.schemaBodyJson(ActiveProjectTerminalSessionRequestSchema)

const skillScopeFromId = (scopeId: string): ProjectSkillScope | null => {
switch (scopeId) {
case "skills":
return "skills"
case "agents-skills":
return "agents/skills"
case "agents-dot-skills":
return "agents/.skills"
case "claude-skills":
return "claude/skills"
case "codex-skills":
return "codex/skills"
case "gemini-skills":
return "gemini/skills"
default:
return null
}
}
const projectSkillScope = (scope: ProjectSkillScope): ProjectSkillScope => scope

const skillScopeFromId = (scopeId: string): ProjectSkillScope | null =>
Match.value(scopeId).pipe(
Match.when("skills", () => projectSkillScope("skills")),
Match.when("agents-skills", () => projectSkillScope("agents/skills")),
Match.when("agents-dot-skills", () => projectSkillScope("agents/.skills")),
Match.when("claude-skills", () => projectSkillScope("claude/skills")),
Match.when("codex-skills", () => projectSkillScope("codex/skills")),
Match.when("gemini-skills", () => projectSkillScope("gemini/skills")),
Match.when("grok-skills", () => projectSkillScope("grok/skills")),
Match.orElse(() => null)
)

export const skillScopeToId = (scope: ProjectSkillScope): string => {
switch (scope) {
case "skills":
return "skills"
case "agents/skills":
return "agents-skills"
case "agents/.skills":
return "agents-dot-skills"
case "claude/skills":
return "claude-skills"
case "codex/skills":
return "codex-skills"
case "gemini/skills":
return "gemini-skills"
}
}
export const skillScopeToId = (scope: ProjectSkillScope): string =>
Match.value(scope).pipe(
Match.when("skills", () => "skills"),
Match.when("agents/skills", () => "agents-skills"),
Match.when("agents/.skills", () => "agents-dot-skills"),
Match.when("claude/skills", () => "claude-skills"),
Match.when("codex/skills", () => "codex-skills"),
Match.when("gemini/skills", () => "gemini-skills"),
Match.when("grok/skills", () => "grok-skills"),
Match.exhaustive
)

const skillScopeFromBody = (scope: string): ProjectSkillScope | null => {
switch (scope) {
case "skills":
case "agents/skills":
case "agents/.skills":
case "claude/skills":
case "codex/skills":
case "gemini/skills":
return scope as ProjectSkillScope
default:
return null
}
}
const skillScopeFromBody = (scope: string): ProjectSkillScope | null =>
Match.value(scope).pipe(
Match.when("skills", () => projectSkillScope("skills")),
Match.when("agents/skills", () => projectSkillScope("agents/skills")),
Match.when("agents/.skills", () => projectSkillScope("agents/.skills")),
Match.when("claude/skills", () => projectSkillScope("claude/skills")),
Match.when("codex/skills", () => projectSkillScope("codex/skills")),
Match.when("gemini/skills", () => projectSkillScope("gemini/skills")),
Match.when("grok/skills", () => projectSkillScope("grok/skills")),
Match.orElse(() => null)
)
const readProjectPortForwardRequest = () => HttpServerRequest.schemaBodyJson(ProjectPortForwardRequestSchema)
const readProjectDatabaseProfileRequest = () => HttpServerRequest.schemaBodyJson(ProjectDatabaseProfileRequestSchema)
const readStateInitRequest = () => HttpServerRequest.schemaBodyJson(StateInitRequestSchema)
Expand Down
3 changes: 3 additions & 0 deletions packages/api/src/services/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ const pickDefaultCommand = (provider: CreateAgentRequest["provider"]): string =>
if (provider === "claude") {
return "MCP_PLAYWRIGHT_ISOLATED=1 claude"
}
if (provider === "grok") {
return "MCP_PLAYWRIGHT_ISOLATED=1 grok --no-sandbox"
}
return ""
}

Expand Down
Loading
Loading