mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
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:
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user