Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 0
Add bundled Codex CLI support with interactive app-server integration#21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
24cbadddac4ff341dfd91e4de84a5e11904a2863a0a90a4d8c2851a73227d6c545b311File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,11 @@ | ||
| #!/bin/sh | ||
| staged_files="$(mktemp)" | ||
| git diff --cached --name-only --diff-filter=ACMR -z > "$staged_files" | ||
| bun run format | ||
| if [ -s "$staged_files" ]; then | ||
| xargs -0 git add -- < "$staged_files" | ||
| fi | ||
| rm -f "$staged_files" | ||
| bun run check |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -3,6 +3,7 @@ import { randomUUID } from "crypto"; | ||
| import { agentStmts, remoteStmts, ticketStmts } from "../db/index.ts"; | ||
| import { errorMeta, logger } from "../lib/logger.ts"; | ||
| import { agentProcessManager } from "../services/AgentProcessManager.ts"; | ||
| import { codexAppServerManager } from "../services/CodexAppServerManager.ts"; | ||
| import { GitWorktreeManager } from "../services/GitWorktreeManager.ts"; | ||
| import type { OrchestratorService } from "../services/OrchestratorService.ts"; | ||
| import type { AgentType } from "../../common/types.ts"; | ||
| @@ -43,6 +44,19 @@ export function agentsRouter(orchestrator: OrchestratorService) { | ||
| } | ||
| }); | ||
| app.get("/:id/codex-state", async (c) => { | ||
| const agent = agentStmts.get.get(c.req.param("id")); | ||
| if (!agent) return c.json({ error: "agent not found" }, 404); | ||
| if (agent.type !== "codex") return c.json({ error: "agent is not a codex agent" }, 400); | ||
| try { | ||
| return c.json(await codexAppServerManager.getState(agent)); | ||
| } catch (err) { | ||
| log.error("failed to load codex state", { agentId: agent.id, ...errorMeta(err) }); | ||
| return c.json({ error: (err as Error).message }, 500); | ||
| } | ||
| }); | ||
| app.post("/:id/merge", async (c) => { | ||
| const agent = agentStmts.get.get(c.req.param("id")); | ||
| if (!agent) return c.json({ error: "agent not found" }, 404); | ||
| @@ -97,20 +111,34 @@ export function agentsRouter(orchestrator: OrchestratorService) { | ||
| let targetAgentId = agent.id; | ||
| if (!agentProcessManager.isRunning(agent.id)) { | ||
| const isRunning = | ||
| agent.type === "codex" | ||
| ? codexAppServerManager.isRunning(agent.id) | ||
| : agentProcessManager.isRunning(agent.id); | ||
| if (!isRunning) { | ||
| log.info("spawning agent for commit", { agentId: agent.id, ticketId: agent.ticketId }); | ||
| await orchestrator.spawnAgent(agent.ticketId, agent.type as AgentType); | ||
| const ticket = ticketStmts.get.get(agent.ticketId); | ||
| if (ticket?.agentId) targetAgentId = ticket.agentId; | ||
| } | ||
| log.info("sending commit prompt", { agentId: targetAgentId }); | ||
| agentProcessManager.write( | ||
| targetAgentId, | ||
| "Please commit all current changes with a descriptive commit message.", | ||
| ); | ||
| await Bun.sleep(100); | ||
| agentProcessManager.write(targetAgentId, Buffer.from([0x0d])); | ||
| if (agent.type === "codex") { | ||
| const targetAgent = agentStmts.get.get(targetAgentId); | ||
| if (!targetAgent) return c.json({ error: "agent not found" }, 404); | ||
| await codexAppServerManager.writeToAgent( | ||
| targetAgent, | ||
| "Please commit all current changes with a descriptive commit message.", | ||
| ); | ||
| } else { | ||
| agentProcessManager.write( | ||
| targetAgentId, | ||
| "Please commit all current changes with a descriptive commit message.", | ||
| ); | ||
| await Bun.sleep(100); | ||
| agentProcessManager.write(targetAgentId, Buffer.from([0x0d])); | ||
| } | ||
| return c.json({ ok: true }); | ||
| }); | ||
| @@ -121,7 +149,10 @@ export function agentsRouter(orchestrator: OrchestratorService) { | ||
| const remoteConfig = remoteStmts.get.get(); | ||
| if (!remoteConfig) return c.json({ error: "no remote configured" }, 400); | ||
| const isRunning = agentProcessManager.isRunning(agent.id); | ||
| const isRunning = | ||
| agent.type === "codex" | ||
| ? codexAppServerManager.isRunning(agent.id) | ||
| : agentProcessManager.isRunning(agent.id); | ||
| // Only abort on conflict when the agent isn't running — if it is running, | ||
| // leave the worktree in the conflicted state so the agent can resolve it. | ||
| log.info("rebase requested", { | ||
| @@ -137,12 +168,15 @@ export function agentsRouter(orchestrator: OrchestratorService) { | ||
| } else if (result.conflicted) { | ||
| log.warn("rebase conflict detected", { agentId: agent.id, isRunning }); | ||
| if (isRunning) { | ||
| agentProcessManager.write( | ||
| agent.id, | ||
| "There are conflicts when rebasing onto the base branch. Please resolve the conflicts, complete the rebase, and commit.", | ||
| ); | ||
| await Bun.sleep(100); | ||
| agentProcessManager.write(agent.id, Buffer.from([0x0d])); | ||
| const prompt = | ||
| "There are conflicts when rebasing onto the base branch. Please resolve the conflicts, complete the rebase, and commit."; | ||
| if (agent.type === "codex") { | ||
| await codexAppServerManager.writeToAgent(agent, prompt); | ||
| } else { | ||
| agentProcessManager.write(agent.id, prompt); | ||
| await Bun.sleep(100); | ||
| agentProcessManager.write(agent.id, Buffer.from([0x0d])); | ||
| } | ||
| } | ||
| } | ||
| return c.json({ ...result, resolving: result.conflicted && isRunning }); | ||
| @@ -155,13 +189,28 @@ export function agentsRouter(orchestrator: OrchestratorService) { | ||
| } | ||
| }); | ||
| app.post("/:id/interrupt", (c) => { | ||
| const id = c.req.param("id"); | ||
| const agent = agentStmts.get.get(id); | ||
| if (!agent) return c.json({ error: "agent not found" }, 404); | ||
| if (agent.type !== "codex") { | ||
| return c.json({ error: "interrupt unsupported for agent type" }, 400); | ||
| } | ||
| codexAppServerManager.interrupt(id); | ||
| return c.body(null, 204); | ||
| }); | ||
| app.post("/:id/kill", (c) => { | ||
| const id = c.req.param("id"); | ||
| const agent = agentStmts.get.get(id); | ||
| if (!agent) return c.json({ error: "agent not found" }, 404); | ||
| log.info("killing agent", { agentId: id }); | ||
| agentProcessManager.kill(id); | ||
| if (agent.type === "codex") { | ||
| codexAppServerManager.kill(id); | ||
| } else { | ||
| agentProcessManager.kill(id); | ||
| } | ||
| agentStmts.updateStatus.run({ $id: id, $status: "error", $endedAt: Date.now() }); | ||
| return c.body(null, 204); | ||
| }); | ||
| @@ -172,23 +221,33 @@ export function agentsRouter(orchestrator: OrchestratorService) { | ||
| if (!agent) return c.json({ error: "agent not found" }, 404); | ||
| log.info("restarting agent", { agentId: id }); | ||
| await agentProcessManager.killAndWait(id); | ||
| if (agent.type === "codex") { | ||
| await codexAppServerManager.killAndWait(id); | ||
| } else { | ||
| await agentProcessManager.killAndWait(id); | ||
| } | ||
| await orchestrator.resumeAgent(agent); | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| return c.body(null, 204); | ||
| }); | ||
| app.post("/:id/input", async (c) => { | ||
| const id = c.req.param("id"); | ||
| let body: { input?: string }; | ||
| let body: { input?: string; clientId?: string }; | ||
| try { | ||
| body = await c.req.json<{ input?: string }>(); | ||
| body = await c.req.json<{ input?: string; clientId?: string }>(); | ||
| } catch { | ||
| return c.json({ error: "invalid JSON" }, 400); | ||
| } | ||
| if (!body.input) return c.json({ error: "input is required" }, 400); | ||
| try { | ||
| agentProcessManager.write(id, body.input); | ||
| const agent = agentStmts.get.get(id); | ||
| if (!agent) return c.json({ error: "agent not found" }, 404); | ||
| if (agent.type === "codex") { | ||
| await codexAppServerManager.writeToAgent(agent, body.input, body.clientId); | ||
| } else { | ||
| agentProcessManager.write(id, body.input); | ||
| } | ||
| return c.json({ ok: true }); | ||
| } catch (err) { | ||
| log.error("failed to write input to agent", { agentId: id, ...errorMeta(err) }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -2,6 +2,7 @@ import { Hono } from "hono"; | ||
| import { integrationStmts, remoteStmts } from "../db/index.ts"; | ||
| import { logger } from "../lib/logger.ts"; | ||
| import { GitHubService } from "../services/GitHubService.ts"; | ||
| import { codexService } from "../services/CodexService.ts"; | ||
| import { globalConfig } from "../services/GlobalConfigService.ts"; | ||
| import { LinearService } from "../services/LinearService.ts"; | ||
| @@ -27,6 +28,15 @@ function parseGitHubOwnerRepo(repoUrl: string): { owner: string; repo: string } | ||
| export const integrationsRouter = new Hono(); | ||
| integrationsRouter.get("/codex/status", async (c) => { | ||
| try { | ||
| return c.json(await codexService.getStatus()); | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } catch (err) { | ||
| log.error("codex status probe failed", { error: (err as Error).message }); | ||
| return c.json({ error: "Failed to check Codex status." }, 502); | ||
| } | ||
| }); | ||
| // ─── Config endpoints ────────────────────────────────────────────────────── | ||
| integrationsRouter.get("/:provider/config", (c) => { | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import type { EventEmitter } from "events"; | ||
| export interface IAgentManager { | ||
| spawn( | ||
| agentId: string, | ||
| input: string, | ||
| worktreePath: string, | ||
| onExit: (agentId: string, code: number) => void, | ||
| ): void; | ||
| write(agentId: string, input: string): void; | ||
| kill(agentId: string): void; | ||
| killAndWait(agentId: string): Promise<void>; | ||
| subscribe(agentId: string): EventEmitter | null; | ||
| isRunning(agentId: string): boolean; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.