feat(daemon): add AtomCode CLI as an agent runtime (#5654)

* feat(daemon): add AtomCode CLI as an agent runtime

Add a declarative RuntimeAgentDef for AtomCode CLI (open-source, Rust,
MIT, v4.26.0), making it invocable from both the web UI and the `od` CLI
alongside the existing 26 agent adapters.

AtomCode's headless mode (`atomcode -p "..."`) streams the assistant
reply on stdout — Claude Code `-p` style. The adapter reuses the same
`promptViaFile: true` + `streamFormat: 'plain'` infrastructure that
grok-build already exercises: the daemon stages the composed prompt in
a temp file and passes it via `--prompt-file`, staying clear of argv
length limits. `-y` auto-approves tool calls in the non-interactive
headless context. Authentication is CLI-owned (`atomcode login` /
`~/.atomcode/config.toml`), matching the grok-build / qoder pattern.

Closes nexu-io/open-design#5653

* fix(daemon): address AtomCode adapter review feedback
This commit is contained in:
huqi
2026-07-15 09:41:40 +00:00
committed by GitHub
parent 27f8421851
commit 03617d1296
4 changed files with 145 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
import { DEFAULT_MODEL_OPTION } from './shared.js';
import type { RuntimeAgentDef } from '../types.js';
// AtomCode — open-source terminal AI coding agent written in Rust.
// Repo: https://atomgit.com/atomgit_atomcode/atomcode
// Docs: https://atomcode.atomgit.com/docs/en/index.html
// Install: `cargo install --path crates/atomcode-cli --locked` (from source)
// or `npm install -g @atomgit.com/atomcode` / `brew install --cask atomcode`
//
// Headless mode with `--prompt-file <PATH>` runs a single prompt
// non-interactively and streams the assistant reply on stdout. OD composes the
// prompt in a temp file to stay clear of argv length limits, exactly like
// grok-build.
//
// AtomCode owns its own authentication: users run `atomcode login` (AtomGit
// OAuth, auto-persisted under `~/.atomcode/`) or set up an API key in
// `~/.atomcode/config.toml` before OD detects the binary. The daemon does not
// inject credentials. Any OpenAI-compatible provider works (Anthropic, OpenAI,
// DeepSeek, Zhipu/GLM, Qwen, SiliconFlow, Ollama, …).
//
// Output: AtomCode headless has no structured stream-json mode yet — it emits
// plain-text assistant replies on stdout and verbose diagnostics (tool calls,
// token usage) on stderr with `-v`. We therefore use `streamFormat: 'plain'`
// (single-turn text reply, no tool_use streaming). Upgrading to a structured
// parser is follow-up work once AtomCode ships a stable JSON event stream.
//
// Safety: OD passes AtomCode's documented `-y` flag so the non-interactive run
// can proceed without waiting for an approval prompt it cannot surface.
export const atomcodeAgentDef = {
id: 'atomcode',
name: 'AtomCode CLI',
bin: 'atomcode',
versionArgs: ['--version'],
fallbackModels: [
DEFAULT_MODEL_OPTION,
{ id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' },
{ id: 'claude-sonnet-4-5', label: 'Claude Sonnet 4.5' },
{ id: 'gpt-5.2', label: 'GPT-5.2' },
{ id: 'glm-5.2', label: 'GLM-5.2' },
{ id: 'deepseek-v4', label: 'DeepSeek V4' },
],
// `--prompt-file <PATH>` avoids argv length limits for long OD prompts.
// `-y` auto-approves tool calls in headless mode (no interactive TTY).
// `--model <id>` selects the upstream provider/model when non-default.
buildArgs: (
_prompt,
_imagePaths,
_extraAllowedDirs = [],
options = {},
runtimeContext = {},
) => {
if (!runtimeContext.promptFilePath) {
throw new Error('atomcode requires runtimeContext.promptFilePath');
}
const args = [
'--prompt-file',
runtimeContext.promptFilePath,
'-y',
];
if (options.model && options.model !== DEFAULT_MODEL_OPTION.id) {
args.push('--model', options.model);
}
return args;
},
promptViaFile: true,
promptViaStdin: false,
streamFormat: 'plain',
installUrl: 'https://atomcode.atomgit.com/docs/en/quickstart.html',
docsUrl: 'https://atomcode.atomgit.com/docs/en/index.html',
} satisfies RuntimeAgentDef;
+4
View File
@@ -83,6 +83,10 @@ const AGENT_INSTALL_LINKS: Record<
installUrl: 'https://www.codebuddy.cn',
docsUrl: 'https://www.codebuddy.cn/docs/workbuddy/Overview',
},
atomcode: {
installUrl: 'https://atomcode.atomgit.com/docs/en/quickstart.html',
docsUrl: 'https://atomcode.atomgit.com/docs/en/index.html',
},
};
function sanitizeHttpsUrl(value: string | undefined): string | undefined {
+2
View File
@@ -23,6 +23,7 @@ import { antigravityAgentDef } from './defs/antigravity.js';
import { codebuddyAgentDef } from './defs/codebuddy.js';
import { reasonixAgentDef } from './defs/reasonix.js';
import { mimoAgentDef } from './defs/mimo.js';
import { atomcodeAgentDef } from './defs/atomcode.js';
import { readLocalAgentProfileDefs as readLocalAgentProfileDefsFromFile } from './local-profiles.js';
import type { RuntimeAgentDef } from './types.js';
@@ -52,6 +53,7 @@ const BASE_AGENT_DEFS: RuntimeAgentDef[] = [
reasonixAgentDef,
codebuddyAgentDef,
mimoAgentDef,
atomcodeAgentDef,
];
export function readLocalAgentProfileDefs(
@@ -0,0 +1,69 @@
import { describe, expect, it } from 'vitest';
import { atomcodeAgentDef } from '../../src/runtimes/defs/atomcode.js';
import { DEFAULT_MODEL_OPTION } from '../../src/runtimes/defs/shared.js';
const promptFileContext = { promptFilePath: '/tmp/open-design/atomcode-prompt.md' };
describe('atomcode buildArgs', () => {
it('requires a staged prompt file', () => {
expect(() => atomcodeAgentDef.buildArgs('prompt', [], [], {}, {})).toThrow(
'atomcode requires runtimeContext.promptFilePath',
);
});
it('emits the exact base argv without embedding the prompt', () => {
const prompt = 'do not put this prompt in argv';
const args = atomcodeAgentDef.buildArgs(prompt, [], [], {}, promptFileContext);
expect(args).toEqual([
'--prompt-file',
promptFileContext.promptFilePath,
'-y',
]);
expect(args).not.toContain(prompt);
});
it('appends --model for a non-default model', () => {
const args = atomcodeAgentDef.buildArgs(
'prompt',
[],
[],
{ model: 'claude-sonnet-4-6' },
promptFileContext,
);
expect(args).toEqual([
'--prompt-file',
promptFileContext.promptFilePath,
'-y',
'--model',
'claude-sonnet-4-6',
]);
});
it('omits --model for the default sentinel', () => {
const args = atomcodeAgentDef.buildArgs(
'prompt',
[],
[],
{ model: DEFAULT_MODEL_OPTION.id },
promptFileContext,
);
expect(args).not.toContain('--model');
});
});
describe('atomcode definition metadata', () => {
it('declares the runtime identity', () => {
expect(atomcodeAgentDef.id).toBe('atomcode');
expect(atomcodeAgentDef.name).toBe('AtomCode CLI');
expect(atomcodeAgentDef.bin).toBe('atomcode');
});
it('uses prompt-file transport and plain output', () => {
expect(atomcodeAgentDef.promptViaFile).toBe(true);
expect(atomcodeAgentDef.promptViaStdin).toBe(false);
expect(atomcodeAgentDef.streamFormat).toBe('plain');
});
});