mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
Add Trae CLI as an ACP coding-agent adapter (#2729)
Some checks failed
visual-baseline / Capture visual baselines (push) Waiting to run
ci / Detect CI change scopes (push) Successful in 0s
nix-check / build (push) Failing after 1s
ci / Validate Nix flake (push) Has been skipped
ci / Preflight (push) Failing after 1s
ci / Workspace unit tests (push) Failing after 1s
ci / Daemon workspace tests (push) Failing after 1s
ci / Web workspace tests (push) Failing after 1s
ci / Browser tests (push) Failing after 1s
ci / Build workspaces (push) Failing after 1s
ci / Validate workspace (push) Failing after 1s
ci / Runtime trace (push) Has been skipped
Some checks failed
visual-baseline / Capture visual baselines (push) Waiting to run
ci / Detect CI change scopes (push) Successful in 0s
nix-check / build (push) Failing after 1s
ci / Validate Nix flake (push) Has been skipped
ci / Preflight (push) Failing after 1s
ci / Workspace unit tests (push) Failing after 1s
ci / Daemon workspace tests (push) Failing after 1s
ci / Web workspace tests (push) Failing after 1s
ci / Browser tests (push) Failing after 1s
ci / Build workspaces (push) Failing after 1s
ci / Validate workspace (push) Failing after 1s
ci / Runtime trace (push) Has been skipped
* Add Trae CLI ACP adapter * Add Trae CLI binary override support * Update mature ACP MCP discovery test * Stabilize Orbit summary tracking test --------- Co-authored-by: AI Bot <bot@example.com>
This commit is contained in:
parent
f8d71fdcb3
commit
840019c8e2
14 changed files with 266 additions and 49 deletions
|
|
@ -157,6 +157,7 @@ const AGENT_CLI_ENV_KEYS: ReadonlyMap<string, ReadonlySet<string>> = new Map([
|
|||
['pi', new Set(['PI_BIN'])],
|
||||
['qoder', new Set(['QODER_BIN'])],
|
||||
['qwen', new Set(['QWEN_BIN'])],
|
||||
['trae-cli', new Set(['TRAE_CLI_BIN'])],
|
||||
['vibe', new Set(['VIBE_BIN'])],
|
||||
]);
|
||||
|
||||
|
|
|
|||
23
apps/daemon/src/runtimes/defs/trae-cli.ts
Normal file
23
apps/daemon/src/runtimes/defs/trae-cli.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { detectAcpModels, DEFAULT_MODEL_OPTION } from './shared.js';
|
||||
import type { RuntimeAgentDef } from '../types.js';
|
||||
|
||||
export const traeCliAgentDef = {
|
||||
id: 'trae-cli',
|
||||
name: 'Trae CLI',
|
||||
bin: 'traecli',
|
||||
versionArgs: ['--version'],
|
||||
versionProbeTimeoutMs: 10_000,
|
||||
fetchModels: async (resolvedBin, env) =>
|
||||
detectAcpModels({
|
||||
bin: resolvedBin,
|
||||
args: ['acp', 'serve'],
|
||||
env,
|
||||
timeoutMs: 15_000,
|
||||
defaultModelOption: DEFAULT_MODEL_OPTION,
|
||||
}),
|
||||
fallbackModels: [DEFAULT_MODEL_OPTION],
|
||||
buildArgs: () => ['acp', 'serve'],
|
||||
streamFormat: 'acp-json-rpc',
|
||||
mcpDiscovery: 'mature-acp',
|
||||
externalMcpInjection: 'acp-merge',
|
||||
} satisfies RuntimeAgentDef;
|
||||
|
|
@ -100,7 +100,7 @@ async function probeVersionAtPath(
|
|||
try {
|
||||
const { stdout } = await execAgentFile(resolved, def.versionArgs, {
|
||||
env,
|
||||
timeout: 3000,
|
||||
timeout: def.versionProbeTimeoutMs ?? 3000,
|
||||
});
|
||||
const version = String(stdout).trim().split('\n')[0] ?? null;
|
||||
return { kind: 'spawned', version };
|
||||
|
|
@ -213,6 +213,7 @@ function stripFns(
|
|||
helpArgs,
|
||||
capabilityFlags,
|
||||
fallbackBins,
|
||||
versionProbeTimeoutMs,
|
||||
maxPromptArgBytes,
|
||||
env,
|
||||
...rest
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const AGENT_BIN_ENV_KEYS = new Map<string, string>([
|
|||
['pi', 'PI_BIN'],
|
||||
['qoder', 'QODER_BIN'],
|
||||
['qwen', 'QWEN_BIN'],
|
||||
['trae-cli', 'TRAE_CLI_BIN'],
|
||||
['vibe', 'VIBE_BIN'],
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ const AGENT_INSTALL_LINKS: Record<
|
|||
installUrl: 'https://github.com/nexu-io/open-design/blob/main/docs/agent-adapters.md',
|
||||
docsUrl: 'https://hermes-agent.nousresearch.com/docs/',
|
||||
},
|
||||
'trae-cli': {
|
||||
installUrl: 'https://www.volcengine.com/docs/86677/2227861?lang=zh',
|
||||
docsUrl: 'https://www.volcengine.com/docs/86677/2227861?lang=zh',
|
||||
},
|
||||
kimi: {
|
||||
installUrl: 'https://github.com/MoonshotAI/kimi-cli',
|
||||
docsUrl: 'https://www.kimi.com/code/docs/en/kimi-cli/guides/getting-started.html',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { devinAgentDef } from './defs/devin.js';
|
|||
import { geminiAgentDef } from './defs/gemini.js';
|
||||
import { opencodeAgentDef } from './defs/opencode.js';
|
||||
import { hermesAgentDef } from './defs/hermes.js';
|
||||
import { traeCliAgentDef } from './defs/trae-cli.js';
|
||||
import { grokBuildAgentDef } from './defs/grok-build.js';
|
||||
import { kimiAgentDef } from './defs/kimi.js';
|
||||
import { cursorAgentDef } from './defs/cursor-agent.js';
|
||||
|
|
@ -25,6 +26,7 @@ const BASE_AGENT_DEFS: RuntimeAgentDef[] = [
|
|||
geminiAgentDef,
|
||||
opencodeAgentDef,
|
||||
hermesAgentDef,
|
||||
traeCliAgentDef,
|
||||
grokBuildAgentDef,
|
||||
kimiAgentDef,
|
||||
cursorAgentDef,
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export type RuntimeAgentDef = {
|
|||
) => string[];
|
||||
streamFormat: string;
|
||||
fallbackBins?: string[];
|
||||
versionProbeTimeoutMs?: number;
|
||||
helpArgs?: string[];
|
||||
capabilityFlags?: Record<string, string>;
|
||||
promptViaStdin?: boolean;
|
||||
|
|
@ -111,6 +112,7 @@ export type DetectedAgent = Omit<
|
|||
| 'helpArgs'
|
||||
| 'capabilityFlags'
|
||||
| 'fallbackBins'
|
||||
| 'versionProbeTimeoutMs'
|
||||
| 'maxPromptArgBytes'
|
||||
| 'env'
|
||||
> & {
|
||||
|
|
|
|||
|
|
@ -317,6 +317,9 @@ describe('app-config', () => {
|
|||
CODEX_BIN: '~/bin/codex-next',
|
||||
OPENAI_API_KEY: ' sk-proxy-openai ',
|
||||
},
|
||||
'trae-cli': {
|
||||
TRAE_CLI_BIN: ' ~/bin/traecli-public ',
|
||||
},
|
||||
gemini: {
|
||||
GEMINI_API_KEY: 'should-not-persist',
|
||||
},
|
||||
|
|
@ -331,6 +334,7 @@ describe('app-config', () => {
|
|||
expect(cfg.agentCliEnv).toEqual({
|
||||
claude: { CLAUDE_CONFIG_DIR: '~/.claude-2', ANTHROPIC_API_KEY: 'sk-proxy-anthropic' },
|
||||
codex: { CODEX_HOME: '~/.codex-alt', CODEX_BIN: '~/bin/codex-next', OPENAI_API_KEY: 'sk-proxy-openai' },
|
||||
'trae-cli': { TRAE_CLI_BIN: '~/bin/traecli-public' },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -407,10 +407,23 @@ describe('OrbitService', () => {
|
|||
});
|
||||
|
||||
it('tracks the most recent run per template alongside the global last run', async () => {
|
||||
const realSetImmediate = setImmediate;
|
||||
vi.useFakeTimers();
|
||||
const dataDir = await mkdtemp(path.join(os.tmpdir(), 'orbit-test-'));
|
||||
try {
|
||||
const service = new OrbitService(dataDir);
|
||||
const waitForStatus = async (
|
||||
predicate: (status: Awaited<ReturnType<OrbitService['status']>>) => boolean,
|
||||
) => {
|
||||
let status = await service.status();
|
||||
for (let attempt = 0; attempt < 50 && !predicate(status); attempt += 1) {
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
// Fake timers do not drive the real filesystem callbacks that persist Orbit summaries.
|
||||
await new Promise<void>((resolve) => realSetImmediate(resolve));
|
||||
status = await service.status();
|
||||
}
|
||||
return status;
|
||||
};
|
||||
let runCount = 0;
|
||||
service.setTemplateResolver(async (skillId) => ({
|
||||
id: skillId,
|
||||
|
|
@ -435,41 +448,23 @@ describe('OrbitService', () => {
|
|||
service.configure({ enabled: false, time: '08:00', templateSkillId: 'orbit-general' });
|
||||
vi.setSystemTime(new Date('2026-05-06T08:00:00.000Z'));
|
||||
await service.start('manual');
|
||||
let status = await service.status();
|
||||
for (
|
||||
let attempt = 0;
|
||||
attempt < 10 && (status.running || status.lastRunsByTemplate['orbit-general']?.agentRunId !== 'agent-1');
|
||||
attempt += 1
|
||||
) {
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
status = await service.status();
|
||||
}
|
||||
await waitForStatus((status) =>
|
||||
!status.running && status.lastRunsByTemplate['orbit-general']?.agentRunId === 'agent-1',
|
||||
);
|
||||
|
||||
service.configure({ enabled: false, time: '08:00', templateSkillId: 'orbit-editorial' });
|
||||
vi.setSystemTime(new Date('2026-05-06T09:00:00.000Z'));
|
||||
await service.start('manual');
|
||||
for (
|
||||
let attempt = 0;
|
||||
attempt < 10 && (status.running || status.lastRunsByTemplate['orbit-editorial']?.agentRunId !== 'agent-2');
|
||||
attempt += 1
|
||||
) {
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
status = await service.status();
|
||||
}
|
||||
await waitForStatus((status) =>
|
||||
!status.running && status.lastRunsByTemplate['orbit-editorial']?.agentRunId === 'agent-2',
|
||||
);
|
||||
|
||||
service.configure({ enabled: false, time: '08:00', templateSkillId: 'orbit-general' });
|
||||
vi.setSystemTime(new Date('2026-05-06T10:00:00.000Z'));
|
||||
await service.start('manual');
|
||||
for (
|
||||
let attempt = 0;
|
||||
attempt < 10 && (status.running || status.lastRunsByTemplate['orbit-general']?.agentRunId !== 'agent-3');
|
||||
attempt += 1
|
||||
) {
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
status = await service.status();
|
||||
}
|
||||
|
||||
status = await service.status();
|
||||
const status = await waitForStatus((status) =>
|
||||
!status.running && status.lastRunsByTemplate['orbit-general']?.agentRunId === 'agent-3',
|
||||
);
|
||||
|
||||
expect(status.lastRun).toMatchObject({
|
||||
agentRunId: 'agent-3',
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ test('resolveAgentExecutable supports configured binary overrides for non-Codex
|
|||
['qoder', 'qodercli', 'QODER_BIN'],
|
||||
['copilot', 'copilot', 'COPILOT_BIN'],
|
||||
['deepseek', 'deepseek', 'DEEPSEEK_BIN'],
|
||||
['trae-cli', 'traecli', 'TRAE_CLI_BIN'],
|
||||
];
|
||||
const dir = mkdtempSync(join(tmpdir(), 'od-agent-bin-overrides-'));
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,28 +1,20 @@
|
|||
import { test } from 'vitest';
|
||||
import { createLiveArtifactsMcpTools, handleLiveArtifactsMcpRequest } from '../../src/mcp-live-artifacts-server.js';
|
||||
import { AGENT_DEFS, assert, buildLiveArtifactsMcpServersForAgent, hermes, kimi } from './helpers/test-helpers.js';
|
||||
import { AGENT_DEFS, assert, buildLiveArtifactsMcpServersForAgent, hermes } from './helpers/test-helpers.js';
|
||||
|
||||
const liveArtifactsMcpServer = {
|
||||
name: 'open-design-live-artifacts',
|
||||
command: 'od',
|
||||
args: ['mcp', 'live-artifacts'],
|
||||
env: [{ name: 'ELECTRON_RUN_AS_NODE', value: '1' }],
|
||||
};
|
||||
|
||||
test('live artifact MCP discovery is limited to mature ACP agents', () => {
|
||||
assert.deepEqual(buildLiveArtifactsMcpServersForAgent(hermes), [
|
||||
{
|
||||
name: 'open-design-live-artifacts',
|
||||
command: 'od',
|
||||
args: ['mcp', 'live-artifacts'],
|
||||
env: [{ name: 'ELECTRON_RUN_AS_NODE', value: '1' }],
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(buildLiveArtifactsMcpServersForAgent(kimi), [
|
||||
{
|
||||
name: 'open-design-live-artifacts',
|
||||
command: 'od',
|
||||
args: ['mcp', 'live-artifacts'],
|
||||
env: [{ name: 'ELECTRON_RUN_AS_NODE', value: '1' }],
|
||||
},
|
||||
]);
|
||||
|
||||
for (const agent of AGENT_DEFS) {
|
||||
if (agent.id === 'hermes' || agent.id === 'kimi') continue;
|
||||
assert.deepEqual(buildLiveArtifactsMcpServersForAgent(agent), []);
|
||||
assert.deepEqual(
|
||||
buildLiveArtifactsMcpServersForAgent(agent),
|
||||
agent.mcpDiscovery === 'mature-acp' ? [liveArtifactsMcpServer] : [],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -163,6 +163,94 @@ describe('probe (issue #658) — ghost CLI after the binary is uninstalled', ()
|
|||
expect(codex?.version).toBe('codex 1.2.3');
|
||||
});
|
||||
|
||||
it('honors Trae CLI adapter-specific version probe timeout', async () => {
|
||||
execAgentFileMock.mockResolvedValue({ stdout: 'agent 1.2.3\n', stderr: '' });
|
||||
resolveAgentLaunchMock.mockImplementation((def: { id: string }) => ({
|
||||
configuredOverridePath: null,
|
||||
pathResolvedPath: `/fake/bin/${def.id}`,
|
||||
selectedPath: `/fake/bin/${def.id}`,
|
||||
launchPath: `/fake/bin/${def.id}`,
|
||||
launchKind: 'selected' as const,
|
||||
childPathPrepend: ['/fake/bin'],
|
||||
diagnostic: null,
|
||||
}));
|
||||
const { detectAgents } = await import('../../src/runtimes/detection.js');
|
||||
|
||||
await detectAgents();
|
||||
|
||||
const traeVersionCall = execAgentFileMock.mock.calls.find(
|
||||
([command, args]) =>
|
||||
command === '/fake/bin/trae-cli' &&
|
||||
Array.isArray(args) &&
|
||||
args.join('\0') === '--version',
|
||||
);
|
||||
|
||||
expect(traeVersionCall).toBeDefined();
|
||||
expect(traeVersionCall?.[2]).toMatchObject({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
it('keeps the default version probe timeout for other runtimes', async () => {
|
||||
execAgentFileMock.mockResolvedValue({ stdout: 'agent 1.2.3\n', stderr: '' });
|
||||
resolveAgentLaunchMock.mockImplementation((def: { id: string }) => ({
|
||||
configuredOverridePath: null,
|
||||
pathResolvedPath: `/fake/bin/${def.id}`,
|
||||
selectedPath: `/fake/bin/${def.id}`,
|
||||
launchPath: `/fake/bin/${def.id}`,
|
||||
launchKind: 'selected' as const,
|
||||
childPathPrepend: ['/fake/bin'],
|
||||
diagnostic: null,
|
||||
}));
|
||||
const { detectAgents } = await import('../../src/runtimes/detection.js');
|
||||
|
||||
await detectAgents();
|
||||
|
||||
const codexVersionCall = execAgentFileMock.mock.calls.find(
|
||||
([command, args]) =>
|
||||
command === '/fake/bin/codex' &&
|
||||
Array.isArray(args) &&
|
||||
args.join('\0') === '--version',
|
||||
);
|
||||
|
||||
expect(codexVersionCall).toBeDefined();
|
||||
expect(codexVersionCall?.[2]).toMatchObject({ timeout: 3000 });
|
||||
});
|
||||
|
||||
it('reports missing Trae CLI as unavailable without breaking full detection', async () => {
|
||||
resolveAgentLaunchMock.mockImplementation((def: { id: string }) => {
|
||||
if (def.id === 'trae-cli') {
|
||||
return {
|
||||
configuredOverridePath: null,
|
||||
pathResolvedPath: null,
|
||||
selectedPath: null,
|
||||
launchPath: null,
|
||||
launchKind: 'unresolved' as const,
|
||||
childPathPrepend: [],
|
||||
diagnostic: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
configuredOverridePath: null,
|
||||
pathResolvedPath: `/fake/bin/${def.id}`,
|
||||
selectedPath: `/fake/bin/${def.id}`,
|
||||
launchPath: `/fake/bin/${def.id}`,
|
||||
launchKind: 'selected' as const,
|
||||
childPathPrepend: ['/fake/bin'],
|
||||
diagnostic: null,
|
||||
};
|
||||
});
|
||||
execAgentFileMock.mockResolvedValue({ stdout: 'agent 1.2.3\n', stderr: '' });
|
||||
const { detectAgents } = await import('../../src/runtimes/detection.js');
|
||||
|
||||
const agents = await detectAgents();
|
||||
const traeCli = agents.find((agent) => agent.id === 'trae-cli');
|
||||
const codex = agents.find((agent) => agent.id === 'codex');
|
||||
|
||||
expect(traeCli).toBeDefined();
|
||||
expect(traeCli?.available).toBe(false);
|
||||
expect(codex).toBeDefined();
|
||||
expect(codex?.available).toBe(true);
|
||||
});
|
||||
|
||||
it('reports unavailable for a stale configured override even when a different PATH binary exists', async () => {
|
||||
// Regression for Siri-Ray's #1301 review: an earlier revision tried
|
||||
// to fall back to a PATH candidate when the configured override
|
||||
|
|
|
|||
94
apps/daemon/tests/runtimes/trae-cli.test.ts
Normal file
94
apps/daemon/tests/runtimes/trae-cli.test.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { readFile } from 'node:fs/promises';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../src/runtimes/defs/shared.js', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../src/runtimes/defs/shared.js')>();
|
||||
return {
|
||||
...actual,
|
||||
detectAcpModels: vi.fn(async () => [
|
||||
{ id: 'live-trae-model', label: 'Live Trae model' },
|
||||
]),
|
||||
};
|
||||
});
|
||||
|
||||
import {
|
||||
DEFAULT_MODEL_OPTION,
|
||||
detectAcpModels,
|
||||
} from '../../src/runtimes/defs/shared.js';
|
||||
import { traeCliAgentDef } from '../../src/runtimes/defs/trae-cli.js';
|
||||
import { installMetaForAgent } from '../../src/runtimes/metadata.js';
|
||||
import { getAgentDef } from '../../src/runtimes/registry.js';
|
||||
import type { RuntimeAgentDef, RuntimeEnv } from '../../src/runtimes/types.js';
|
||||
|
||||
const rootUrl = new URL('../../../../', import.meta.url);
|
||||
|
||||
async function readRepoFile(path: string) {
|
||||
return readFile(new URL(path, rootUrl), 'utf8');
|
||||
}
|
||||
|
||||
describe('Trae CLI runtime adapter', () => {
|
||||
it('defines Trae CLI as a public ACP adapter', () => {
|
||||
const runtimeDef = traeCliAgentDef as RuntimeAgentDef;
|
||||
|
||||
expect(traeCliAgentDef.id).toBe('trae-cli');
|
||||
expect(traeCliAgentDef.name).toBe('Trae CLI');
|
||||
expect(traeCliAgentDef.bin).toBe('traecli');
|
||||
expect(runtimeDef.fallbackBins).toBeUndefined();
|
||||
expect(traeCliAgentDef.versionArgs).toEqual(['--version']);
|
||||
expect(traeCliAgentDef.versionProbeTimeoutMs).toBeGreaterThan(3000);
|
||||
expect(runtimeDef.buildArgs('', [], [], {})).toEqual(['acp', 'serve']);
|
||||
expect(traeCliAgentDef.streamFormat).toBe('acp-json-rpc');
|
||||
});
|
||||
|
||||
it('fetches live models from traecli acp serve', async () => {
|
||||
const env: RuntimeEnv = { PATH: '/opt/bin' };
|
||||
|
||||
await expect(
|
||||
traeCliAgentDef.fetchModels?.('/opt/bin/traecli', env),
|
||||
).resolves.toEqual([{ id: 'live-trae-model', label: 'Live Trae model' }]);
|
||||
|
||||
expect(detectAcpModels).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
bin: '/opt/bin/traecli',
|
||||
args: ['acp', 'serve'],
|
||||
env,
|
||||
timeoutMs: 15_000,
|
||||
defaultModelOption: DEFAULT_MODEL_OPTION,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('registers Trae CLI in the agent registry', () => {
|
||||
expect(getAgentDef('trae-cli')).toBe(traeCliAgentDef);
|
||||
});
|
||||
|
||||
it('exposes public install metadata and docs without internal COCO leakage', async () => {
|
||||
const meta = installMetaForAgent('trae-cli');
|
||||
expect(meta.installUrl).toBe(
|
||||
'https://www.volcengine.com/docs/86677/2227861?lang=zh',
|
||||
);
|
||||
expect(meta.docsUrl).toBe(
|
||||
'https://www.volcengine.com/docs/86677/2227861?lang=zh',
|
||||
);
|
||||
|
||||
const publicSurface = [
|
||||
await readRepoFile('apps/daemon/src/runtimes/defs/trae-cli.ts'),
|
||||
await readRepoFile('apps/daemon/src/runtimes/metadata.ts'),
|
||||
await readRepoFile('docs/agent-adapters.md'),
|
||||
].join('\n');
|
||||
|
||||
for (const forbidden of [
|
||||
'coco',
|
||||
'code.byted.org',
|
||||
'codebase-api.byted.org',
|
||||
'bytedance',
|
||||
'/Users/bytedance',
|
||||
'ByteDance SSO',
|
||||
'install_coco',
|
||||
'GPT-5.4',
|
||||
]) {
|
||||
expect(publicSurface.toLowerCase()).not.toContain(forbidden.toLowerCase());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -97,6 +97,7 @@ If both signals agree, detection is confident. If only one signal fires, we mark
|
|||
| **kiro** | `kiro-cli` | `~/.kiro/` | ❌ | ✅ | ✅ (`acp-json-rpc`) | P2 |
|
||||
| **kilo** | `kilo` | — | ❌ | ✅ | ✅ (`acp-json-rpc`) | P2 |
|
||||
| **vibe** | `vibe-acp` | `~/.vibe/` | ❌ | ✅ | ✅ (`acp-json-rpc`) | P2 |
|
||||
| **trae-cli** | `traecli` | Trae CLI config | Trae CLI managed | ❌ (prompt-injected) | ✅ | ✅ (`acp-json-rpc`) | P2 |
|
||||
| **deepseek** | `deepseek` | `~/.deepseek/` | `~/.deepseek/skills/` | ❌ (prompt-injected) | ✅ | ✅ (plain text) | P2 |
|
||||
| **qoder** | `qodercli` | Qoder CLI config | Qoder CLI managed | ❌ (prompt-injected) | ✅ | ✅ (`stream-json`) | P2 |
|
||||
| **pi** | `pi` | `~/.pi/agent/` | `~/.pi/agent/skills/` | ❌ (prompt-injected) | ✅ | ✅ (`pi-rpc` JSON-RPC) | P2 |
|
||||
|
|
@ -211,6 +212,14 @@ The adapter declares which strategy to use via `capabilities().nativeSkillLoadin
|
|||
- Permission: `--permission-mode bypass_permissions` avoids headless approval prompts in the web UI. Users should treat this as the same trust posture as running Qoder directly with that flag in the selected project directory.
|
||||
- **Gotcha:** Detection only proves `qodercli --version` can run. Qoder authentication and account scope remain owned by Qoder CLI, with credentials read from Qoder's `~/.qoder/config.json`; the daemon surfaces stderr/stdout failures from the spawned run instead of running login or editing Qoder config.
|
||||
|
||||
### 5.10 Trae CLI
|
||||
|
||||
- Invocation: `traecli acp serve`, using the daemon's shared ACP JSON-RPC transport. The adapter follows Trae CLI's public ACP entrypoint documented at https://www.volcengine.com/docs/86677/2227861?lang=zh.
|
||||
- Streaming: `acp-json-rpc`; the daemon uses the same ACP event path as the other ACP-backed adapters.
|
||||
- Models: dynamic via the ACP handshake. If model discovery fails, the picker falls back to the CLI's default configuration rather than requiring CI or startup detection to log in to Trae CLI.
|
||||
- Skills: prompt injection only in v1. External MCP servers can be forwarded through the ACP launch descriptor with the existing `acp-merge` path.
|
||||
- **Gotcha:** Detection only proves `traecli --version` and model discovery can run in the current environment. Trae CLI owns login, account scope, and model entitlement; the daemon does not run login flows or edit Trae CLI configuration.
|
||||
|
||||
### 5.11 Pi
|
||||
|
||||
- Invocation: `pi --mode rpc [--model <id>] [--thinking <level>] [--append-system-prompt <dir> …]`, with the composed prompt delivered over stdin via JSON-RPC. The daemon sends a `prompt` command (optionally with `images` for multimodal input) and pi streams back typed events until `agent_end`. Pi's RPC process stays alive after `agent_end` (designed for multi-prompt sessions); the daemon closes stdin and SIGTERMs after a grace period since `/api/chat` is single-shot.
|
||||
|
|
@ -222,7 +231,7 @@ The adapter declares which strategy to use via `capabilities().nativeSkillLoadin
|
|||
- Extension UI: auto-resolved. pi's RPC protocol can request user dialogs (`select`, `confirm`, `input`, `editor`) and fire-and-forget notifications (`setStatus`, `setWidget`, `notify`, `setTitle`, `set_editor_text`). Dialog methods are auto-approved (confirm → true, select → first option) and fire-and-forget methods are silently consumed because the web UI has no surface for them.
|
||||
- **Gotcha:** pi's RPC `prompt` response is asynchronous — `success: true` only means the prompt was accepted, not that the agent finished. Agent failures after acceptance surface through the normal event stream (`extension_error`, `auto_retry_end` with `success: false`) and the empty-output guard.
|
||||
|
||||
### 5.10 DeepSeek TUI
|
||||
### 5.12 DeepSeek TUI
|
||||
|
||||
- Invocation: `deepseek exec --auto [--model <id>] "<prompt>"`. The `deepseek` dispatcher owns the `exec` / `--auto` subcommands and delegates to a sibling `deepseek-tui` runtime binary at exec time; upstream documents both binaries as required (the npm and cargo paths install them together). We only probe the dispatcher — `deepseek-tui` on its own doesn't accept this argv shape, so advertising it as a fallback would surface the agent as available but fail on the first chat run. A future revision could teach resolution + buildArgs which binary was selected and emit a verified `deepseek-tui` invocation, with a regression test exercising that path.
|
||||
- Streaming: plain text deltas to stdout in non-`--json` mode (tool-call notifications go to stderr). Skipping `--json` is intentional — `deepseek exec --json` batches the entire run into one trailing summary object instead of streaming, which would freeze the chat UI until end-of-turn.
|
||||
|
|
|
|||
Loading…
Reference in a new issue