@dinoxx/dinox-cli 2.10.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/commands/gateway/index.js +16 -10
- package/dist/commands/update.d.ts +16 -0
- package/dist/commands/update.js +61 -11
- package/dist/gateway/config.d.ts +1 -5
- package/dist/gateway/config.js +23 -2
- package/dist/gateway/detect.d.ts +26 -3
- package/dist/gateway/detect.js +126 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ winget install OpenJS.NodeJS.LTS
|
|
|
103
103
|
dino update
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
它会自动根据你当前的安装方式选择合适的包管理器执行更新。升级完成后会检查本机已安装的 Agent:有 `claude` 但没有 ACP 命令时安装 `@agentclientprotocol/claude-agent-acp`,有 `codex` 时安装 `@agentclientprotocol/codex-acp`,有 `zcode` 时安装 `zcode-acp`。已经装过的适配器会跳过;某个适配器安装失败只给出警告,不影响 CLI 升级本身。
|
|
107
107
|
|
|
108
108
|
---
|
|
109
109
|
|
|
@@ -947,7 +947,7 @@ dino gateway reload
|
|
|
947
947
|
dino gateway remove Hermes
|
|
948
948
|
```
|
|
949
949
|
|
|
950
|
-
`detect` 会在 `PATH` 上查找 ACP agent
|
|
950
|
+
`detect` 会在 `PATH` 上查找 ACP agent,并用真实握手确认能否启动。覆盖范围与 [Paseo 的 ACP catalog](https://github.com/getpaseo/paseo/blob/main/packages/app/src/data/acp-provider-catalog.ts) 对齐,只认本机已经装好的命令,不会为了探测去 `npx -y` 下载。每个 agent 一握手完就打印,不用等最慢的那个。某个启动方式一直不应答时,不再换参数重试。`--add` 把已确认的 agent 写入当前账号的 gateway 配置,并由 daemon 托管。`gateway list` 只显示环境变量名,不显示值。`dino gateway mcp` 由这些 agent 拉起,对外提供只读的 Dinox 知识工具。
|
|
951
951
|
|
|
952
952
|
### 6.16 `info`:查看版本与技能仓库地址
|
|
953
953
|
|
|
@@ -961,6 +961,8 @@ dino info
|
|
|
961
961
|
dino update
|
|
962
962
|
```
|
|
963
963
|
|
|
964
|
+
升级 CLI 之后,会为本机已有、但还没有 ACP 命令的 Agent 安装适配器(Claude Code、Codex、ZCode)。`--format json` 的 `acpAdapters` 返回每一项的 `installed` 或 `failed`。
|
|
965
|
+
|
|
964
966
|
---
|
|
965
967
|
|
|
966
968
|
## 7. AI / 脚本自动化
|
|
@@ -116,8 +116,23 @@ async function runAdd(context) {
|
|
|
116
116
|
printHumanReadableLine(`Added gateway "${gateway.name}" as device ${gateway.deviceId}.`);
|
|
117
117
|
printStatuses(statuses);
|
|
118
118
|
}
|
|
119
|
+
function formatDetectedAgent(agent) {
|
|
120
|
+
const launch = [agent.command, ...agent.args].join(' ');
|
|
121
|
+
const label = agent.verified
|
|
122
|
+
? `ready${agent.agentVersion ? ` (${agent.agentName ?? agent.name} ${agent.agentVersion})` : ''}`
|
|
123
|
+
: agent.installHint
|
|
124
|
+
? `installed, needs its ACP adapter: ${agent.installHint}`
|
|
125
|
+
: `not responding: ${agent.error ?? 'unknown'}`;
|
|
126
|
+
return `${agent.name}: ${label} · ${launch}`;
|
|
127
|
+
}
|
|
119
128
|
async function runDetect(context) {
|
|
120
|
-
const
|
|
129
|
+
const structured = Boolean(getRequestedStructuredOutputFormat());
|
|
130
|
+
if (!structured)
|
|
131
|
+
printHumanReadableLine('Checking installed ACP agents...');
|
|
132
|
+
const detected = await detectAcpAgents({
|
|
133
|
+
probe: true,
|
|
134
|
+
onDetected: structured ? undefined : (agent) => printHumanReadableLine(formatDetectedAgent(agent)),
|
|
135
|
+
});
|
|
121
136
|
const added = [];
|
|
122
137
|
const updated = [];
|
|
123
138
|
if (context.options.add) {
|
|
@@ -146,15 +161,6 @@ async function runDetect(context) {
|
|
|
146
161
|
printHumanReadableLine('No ACP agents found on PATH.');
|
|
147
162
|
return;
|
|
148
163
|
}
|
|
149
|
-
for (const agent of detected) {
|
|
150
|
-
const launch = [agent.command, ...agent.args].join(' ');
|
|
151
|
-
const label = agent.verified
|
|
152
|
-
? `ready${agent.agentVersion ? ` (${agent.agentName ?? agent.name} ${agent.agentVersion})` : ''}`
|
|
153
|
-
: agent.installHint
|
|
154
|
-
? `installed, needs its ACP adapter: ${agent.installHint}`
|
|
155
|
-
: `not responding: ${agent.error ?? 'unknown'}`;
|
|
156
|
-
printHumanReadableLine(`${agent.name}: ${label} · ${launch}`);
|
|
157
|
-
}
|
|
158
164
|
for (const gateway of added)
|
|
159
165
|
printHumanReadableLine(`Added gateway "${gateway.name}" (${gateway.deviceId}).`);
|
|
160
166
|
for (const gateway of updated) {
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import type { CommandDescriptor } from './descriptor/types.js';
|
|
3
|
+
import { type MissingAcpAdapter } from '../gateway/detect.js';
|
|
3
4
|
type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
|
|
4
5
|
type LogFn = (...parts: unknown[]) => void;
|
|
6
|
+
export type AcpAdapterInstallResult = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
package: string;
|
|
10
|
+
status: 'installed' | 'failed';
|
|
11
|
+
error: string | null;
|
|
12
|
+
};
|
|
5
13
|
type UpdateCommandDeps = {
|
|
6
14
|
detectPackageManager: () => PackageManager;
|
|
7
15
|
runUpdate: (pm: PackageManager, options: {
|
|
8
16
|
structuredOutput: boolean;
|
|
9
17
|
}) => Promise<void>;
|
|
18
|
+
installAcpAdapters: (pm: PackageManager, options: {
|
|
19
|
+
structuredOutput: boolean;
|
|
20
|
+
}) => Promise<AcpAdapterInstallResult[]>;
|
|
10
21
|
getVersion: () => string;
|
|
11
22
|
getLatestVersion: () => Promise<string | null>;
|
|
12
23
|
log: LogFn;
|
|
@@ -20,7 +31,12 @@ type UpdateSpawnSpec = {
|
|
|
20
31
|
args: string[];
|
|
21
32
|
shell: boolean;
|
|
22
33
|
};
|
|
34
|
+
export declare function resolvePackageManagerSpawn(pm: PackageManager, args: string[], platform?: NodeJS.Platform): UpdateSpawnSpec;
|
|
23
35
|
export declare function resolveUpdateSpawnSpec(pm: PackageManager, platform?: NodeJS.Platform): UpdateSpawnSpec;
|
|
36
|
+
/** Installs ACP adapters for local agent CLIs that do not already have one. */
|
|
37
|
+
export declare function installMissingAcpAdapters(pm: PackageManager, options: {
|
|
38
|
+
structuredOutput: boolean;
|
|
39
|
+
}, missing?: MissingAcpAdapter[]): Promise<AcpAdapterInstallResult[]>;
|
|
24
40
|
export declare function createUpdateCommandDescriptor(overrides?: Partial<UpdateCommandDeps>): CommandDescriptor;
|
|
25
41
|
export declare const updateCommandDescriptor: CommandDescriptor;
|
|
26
42
|
export declare function registerUpdateCommand(program: Command, overrides?: Partial<UpdateCommandDeps>): void;
|
package/dist/commands/update.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getSkillsReleaseMetadata } from '../constants/links.js';
|
|
|
6
6
|
import { RETIRED_SKILLS, RETIRED_SKILLS_CLEANUP, formatRetiredSkillsSummary, } from '../skills/retired.js';
|
|
7
7
|
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../utils/output.js';
|
|
8
8
|
import { DinoxError } from '../utils/errors.js';
|
|
9
|
+
import { findMissingAcpAdapters } from '../gateway/detect.js';
|
|
9
10
|
import { fetchLatestPublishedVersion } from '../utils/updateNotice.js';
|
|
10
11
|
import { getPackageVersion } from '../utils/version.js';
|
|
11
12
|
const PACKAGE_NAME = '@dinoxx/dinox-cli';
|
|
@@ -51,21 +52,31 @@ const PM_COMMANDS = {
|
|
|
51
52
|
yarn: { bin: 'yarn', args: ['global', 'add', `${PACKAGE_NAME}@latest`] },
|
|
52
53
|
bun: { bin: 'bun', args: ['add', '--global', `${PACKAGE_NAME}@latest`] },
|
|
53
54
|
};
|
|
54
|
-
export function
|
|
55
|
-
const
|
|
56
|
-
const usesWindowsCommandShim = platform === 'win32' && bin !== 'bun';
|
|
55
|
+
export function resolvePackageManagerSpawn(pm, args, platform = process.platform) {
|
|
56
|
+
const usesWindowsCommandShim = platform === 'win32' && pm !== 'bun';
|
|
57
57
|
if (usesWindowsCommandShim) {
|
|
58
58
|
return {
|
|
59
|
-
command: [`${
|
|
59
|
+
command: [`${pm}.cmd`, ...args].join(' '),
|
|
60
60
|
args: [],
|
|
61
61
|
shell: true,
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
-
return {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
return { command: pm, args: [...args], shell: false };
|
|
65
|
+
}
|
|
66
|
+
export function resolveUpdateSpawnSpec(pm, platform = process.platform) {
|
|
67
|
+
return resolvePackageManagerSpawn(pm, PM_COMMANDS[pm].args, platform);
|
|
68
|
+
}
|
|
69
|
+
function globalInstallArgs(pm, packageName) {
|
|
70
|
+
switch (pm) {
|
|
71
|
+
case 'npm':
|
|
72
|
+
return ['install', '--global', packageName];
|
|
73
|
+
case 'pnpm':
|
|
74
|
+
return ['add', '--global', packageName];
|
|
75
|
+
case 'yarn':
|
|
76
|
+
return ['global', 'add', packageName];
|
|
77
|
+
case 'bun':
|
|
78
|
+
return ['add', '--global', packageName];
|
|
79
|
+
}
|
|
69
80
|
}
|
|
70
81
|
function appendBoundedOutput(current, chunk) {
|
|
71
82
|
if (Buffer.byteLength(current, 'utf8') >= MAX_CAPTURED_UPDATE_OUTPUT_BYTES) {
|
|
@@ -74,8 +85,8 @@ function appendBoundedOutput(current, chunk) {
|
|
|
74
85
|
const remaining = MAX_CAPTURED_UPDATE_OUTPUT_BYTES - Buffer.byteLength(current, 'utf8');
|
|
75
86
|
return current + chunk.subarray(0, remaining).toString('utf8');
|
|
76
87
|
}
|
|
77
|
-
function
|
|
78
|
-
const { command, args, shell } =
|
|
88
|
+
function runPackageManager(pm, packageArgs, options) {
|
|
89
|
+
const { command, args, shell } = resolvePackageManagerSpawn(pm, packageArgs);
|
|
79
90
|
const invocation = [command, ...args].join(' ');
|
|
80
91
|
return new Promise((resolve, reject) => {
|
|
81
92
|
let stdout = '';
|
|
@@ -120,10 +131,38 @@ function runUpdate(pm, options) {
|
|
|
120
131
|
});
|
|
121
132
|
});
|
|
122
133
|
}
|
|
134
|
+
function runUpdate(pm, options) {
|
|
135
|
+
return runPackageManager(pm, PM_COMMANDS[pm].args, options);
|
|
136
|
+
}
|
|
137
|
+
function adapterErrorMessage(error) {
|
|
138
|
+
return error instanceof Error ? error.message : String(error);
|
|
139
|
+
}
|
|
140
|
+
/** Installs ACP adapters for local agent CLIs that do not already have one. */
|
|
141
|
+
export async function installMissingAcpAdapters(pm, options, missing) {
|
|
142
|
+
const adapters = missing ?? await findMissingAcpAdapters();
|
|
143
|
+
const results = [];
|
|
144
|
+
for (const adapter of adapters) {
|
|
145
|
+
try {
|
|
146
|
+
await runPackageManager(pm, globalInstallArgs(pm, adapter.packageName), options);
|
|
147
|
+
results.push({ id: adapter.id, name: adapter.name, package: adapter.packageName, status: 'installed', error: null });
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
results.push({
|
|
151
|
+
id: adapter.id,
|
|
152
|
+
name: adapter.name,
|
|
153
|
+
package: adapter.packageName,
|
|
154
|
+
status: 'failed',
|
|
155
|
+
error: adapterErrorMessage(error),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return results;
|
|
160
|
+
}
|
|
123
161
|
function resolveDeps(overrides) {
|
|
124
162
|
return {
|
|
125
163
|
detectPackageManager,
|
|
126
164
|
runUpdate,
|
|
165
|
+
installAcpAdapters: (pm, options) => installMissingAcpAdapters(pm, options),
|
|
127
166
|
getVersion: getPackageVersion,
|
|
128
167
|
getLatestVersion: fetchLatestPublishedVersion,
|
|
129
168
|
log: logHumanReadable,
|
|
@@ -185,6 +224,7 @@ export function createUpdateCommandDescriptor(overrides = {}) {
|
|
|
185
224
|
'skillsInstallCommand',
|
|
186
225
|
'retiredSkills',
|
|
187
226
|
'retiredSkillsCleanup',
|
|
227
|
+
'acpAdapters',
|
|
188
228
|
],
|
|
189
229
|
},
|
|
190
230
|
examples: ['dino update', 'dino update --package-manager pnpm --format json'],
|
|
@@ -199,6 +239,7 @@ export function createUpdateCommandDescriptor(overrides = {}) {
|
|
|
199
239
|
deps.log(`updating ${PACKAGE_NAME} via ${pm}...`);
|
|
200
240
|
}
|
|
201
241
|
await deps.runUpdate(pm, { structuredOutput });
|
|
242
|
+
const acpAdapters = await deps.installAcpAdapters(pm, { structuredOutput });
|
|
202
243
|
const observedVersion = deps.getVersion();
|
|
203
244
|
const version = observedVersion !== previousVersion
|
|
204
245
|
? observedVersion
|
|
@@ -219,10 +260,19 @@ export function createUpdateCommandDescriptor(overrides = {}) {
|
|
|
219
260
|
skillsInstallCommand: skills.installCommand,
|
|
220
261
|
retiredSkills: RETIRED_SKILLS,
|
|
221
262
|
retiredSkillsCleanup: RETIRED_SKILLS_CLEANUP,
|
|
263
|
+
acpAdapters,
|
|
222
264
|
});
|
|
223
265
|
return;
|
|
224
266
|
}
|
|
225
267
|
deps.log(`done: ${version}`);
|
|
268
|
+
for (const adapter of acpAdapters) {
|
|
269
|
+
if (adapter.status === 'installed') {
|
|
270
|
+
deps.log(`installed ACP adapter ${adapter.package} for ${adapter.name}`);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
deps.log(`warning: could not install ACP adapter ${adapter.package} for ${adapter.name} (${adapter.error})`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
226
276
|
printSkillsReminder(deps.log, version);
|
|
227
277
|
},
|
|
228
278
|
};
|
package/dist/gateway/config.d.ts
CHANGED
|
@@ -32,16 +32,12 @@ export declare function addGateway(userId: string, input: {
|
|
|
32
32
|
cwd?: string;
|
|
33
33
|
env?: Record<string, string>;
|
|
34
34
|
}): Promise<GatewayDefinition>;
|
|
35
|
-
/**
|
|
36
|
-
* Hosts a detected agent once. An agent already hosted, matched by type (or, for
|
|
37
|
-
* gateways from before types were recorded, by executable name and arguments),
|
|
38
|
-
* is updated in place when its executable moved, e.g. after a Node upgrade.
|
|
39
|
-
*/
|
|
40
35
|
export declare function adoptDetectedAgent(userId: string, agent: {
|
|
41
36
|
id: string;
|
|
42
37
|
name: string;
|
|
43
38
|
command: string;
|
|
44
39
|
args: string[];
|
|
40
|
+
env?: Record<string, string>;
|
|
45
41
|
}): Promise<{
|
|
46
42
|
gateway: GatewayDefinition;
|
|
47
43
|
change: 'added' | 'updated' | 'unchanged';
|
package/dist/gateway/config.js
CHANGED
|
@@ -214,6 +214,19 @@ function sameLaunch(a, b) {
|
|
|
214
214
|
* gateways from before types were recorded, by executable name and arguments),
|
|
215
215
|
* is updated in place when its executable moved, e.g. after a Node upgrade.
|
|
216
216
|
*/
|
|
217
|
+
function mergeRequiredEnv(existing, required) {
|
|
218
|
+
if (!required)
|
|
219
|
+
return { env: existing, changed: false };
|
|
220
|
+
const env = { ...(existing ?? {}) };
|
|
221
|
+
let changed = false;
|
|
222
|
+
for (const [key, value] of Object.entries(required)) {
|
|
223
|
+
if (env[key] === undefined) {
|
|
224
|
+
env[key] = value;
|
|
225
|
+
changed = true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { env, changed };
|
|
229
|
+
}
|
|
217
230
|
export async function adoptDetectedAgent(userId, agent) {
|
|
218
231
|
const gateways = await loadGateways(userId);
|
|
219
232
|
const existing = gateways.find((gateway) => gateway.agentId === agent.id) ??
|
|
@@ -223,10 +236,17 @@ export async function adoptDetectedAgent(userId, agent) {
|
|
|
223
236
|
path.basename(gateway.command) === path.basename(agent.command) &&
|
|
224
237
|
JSON.stringify(gateway.args ?? []) === JSON.stringify(agent.args));
|
|
225
238
|
if (existing) {
|
|
226
|
-
|
|
239
|
+
const requiredEnv = mergeRequiredEnv(existing.env, agent.env);
|
|
240
|
+
if (existing.agentId === agent.id && sameLaunch(existing, agent) && !requiredEnv.changed) {
|
|
227
241
|
return { gateway: existing, change: 'unchanged' };
|
|
228
242
|
}
|
|
229
|
-
const updated = {
|
|
243
|
+
const updated = {
|
|
244
|
+
...existing,
|
|
245
|
+
agentId: agent.id,
|
|
246
|
+
command: agent.command,
|
|
247
|
+
args: agent.args,
|
|
248
|
+
...(requiredEnv.env ? { env: requiredEnv.env } : {}),
|
|
249
|
+
};
|
|
230
250
|
await saveGateways(userId, gateways.map((gateway) => (gateway.id === existing.id ? updated : gateway)));
|
|
231
251
|
return { gateway: updated, change: 'updated' };
|
|
232
252
|
}
|
|
@@ -240,6 +260,7 @@ export async function adoptDetectedAgent(userId, agent) {
|
|
|
240
260
|
agentId: agent.id,
|
|
241
261
|
command: agent.command,
|
|
242
262
|
args: agent.args,
|
|
263
|
+
...(agent.env ? { env: agent.env } : {}),
|
|
243
264
|
});
|
|
244
265
|
return { gateway, change: 'added' };
|
|
245
266
|
}
|
package/dist/gateway/detect.d.ts
CHANGED
|
@@ -7,14 +7,22 @@ export declare const KNOWN_ACP_AGENTS: ReadonlyArray<{
|
|
|
7
7
|
id: string;
|
|
8
8
|
name: string;
|
|
9
9
|
candidates: string[][];
|
|
10
|
-
/**
|
|
10
|
+
/** Passed to the agent process. Saved on the gateway when this launch is adopted. */
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
/** The agent's own CLI, and the global package that provides ACP when that CLI has none. */
|
|
11
13
|
adapter?: {
|
|
12
14
|
baseBinary: string;
|
|
13
|
-
|
|
15
|
+
packageName: string;
|
|
14
16
|
};
|
|
15
17
|
}>;
|
|
16
18
|
/** The known agent a manual launch command starts, by executable name and arguments. */
|
|
17
19
|
export declare function inferAgentId(command: string | undefined, args?: string[]): string | undefined;
|
|
20
|
+
/** A launch stayed up but never completed the ACP handshake. Further flags of that agent are not tried. */
|
|
21
|
+
export declare class AcpProbeTimeoutError extends Error {
|
|
22
|
+
constructor();
|
|
23
|
+
}
|
|
24
|
+
/** A quick failure can be a wrong flag. A timeout means that agent is not going to answer. */
|
|
25
|
+
export declare function shouldTryNextAcpCandidate(error: unknown): boolean;
|
|
18
26
|
export type DetectedAgent = {
|
|
19
27
|
id: string;
|
|
20
28
|
name: string;
|
|
@@ -27,14 +35,29 @@ export type DetectedAgent = {
|
|
|
27
35
|
error?: string;
|
|
28
36
|
/** Set when the agent's CLI is installed but its ACP adapter is not. */
|
|
29
37
|
installHint?: string;
|
|
38
|
+
/** Environment the handshake needed. Stored on the gateway when adopted. */
|
|
39
|
+
env?: Record<string, string>;
|
|
30
40
|
};
|
|
31
41
|
export declare function findExecutable(name: string, envPath?: string): Promise<string | null>;
|
|
32
42
|
/** Starts the agent, runs `initialize`, and stops it. Never leaves the process running. */
|
|
33
|
-
export declare function probeAcpAgent(command: string, args: string[], timeoutMs?: number): Promise<{
|
|
43
|
+
export declare function probeAcpAgent(command: string, args: string[], timeoutMs?: number, env?: Record<string, string>): Promise<{
|
|
34
44
|
agentName: string | null;
|
|
35
45
|
agentVersion: string | null;
|
|
36
46
|
}>;
|
|
37
47
|
/** Finds installed ACP agents; with `probe`, keeps only launch variants that handshake. */
|
|
38
48
|
export declare function detectAcpAgents(options?: {
|
|
39
49
|
probe?: boolean;
|
|
50
|
+
timeoutMs?: number;
|
|
51
|
+
/** Called as soon as one agent finishes, so a slow handshake does not hide the ones already done. */
|
|
52
|
+
onDetected?: (agent: DetectedAgent) => void;
|
|
40
53
|
}): Promise<DetectedAgent[]>;
|
|
54
|
+
export type MissingAcpAdapter = {
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
packageName: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Agents whose own CLI is on PATH but none of the ACP launch commands are.
|
|
61
|
+
* Those need a global adapter package before a gateway can start them.
|
|
62
|
+
*/
|
|
63
|
+
export declare function findMissingAcpAdapters(envPath?: string): Promise<MissingAcpAdapter[]>;
|
package/dist/gateway/detect.js
CHANGED
|
@@ -17,13 +17,13 @@ export const KNOWN_ACP_AGENTS = [
|
|
|
17
17
|
id: 'claude',
|
|
18
18
|
name: 'Claude Code',
|
|
19
19
|
candidates: [['claude-agent-acp'], ['claude-code-acp']],
|
|
20
|
-
adapter: { baseBinary: 'claude',
|
|
20
|
+
adapter: { baseBinary: 'claude', packageName: '@agentclientprotocol/claude-agent-acp' },
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
id: 'codex',
|
|
24
24
|
name: 'Codex',
|
|
25
25
|
candidates: [['codex-acp']],
|
|
26
|
-
adapter: { baseBinary: 'codex',
|
|
26
|
+
adapter: { baseBinary: 'codex', packageName: '@agentclientprotocol/codex-acp' },
|
|
27
27
|
},
|
|
28
28
|
{ id: 'gemini', name: 'Gemini CLI', candidates: [['gemini', '--acp'], ['gemini', '--experimental-acp']] },
|
|
29
29
|
{ id: 'copilot', name: 'GitHub Copilot', candidates: [['copilot', '--acp']] },
|
|
@@ -31,8 +31,8 @@ export const KNOWN_ACP_AGENTS = [
|
|
|
31
31
|
{ id: 'opencode', name: 'OpenCode', candidates: [['opencode', 'acp']] },
|
|
32
32
|
{ id: 'pi', name: 'pi', candidates: [['pi-acp']] },
|
|
33
33
|
{ id: 'goose', name: 'Goose', candidates: [['goose', 'acp']] },
|
|
34
|
-
{ id: 'qwen', name: 'Qwen Code', candidates: [['qwen', '--acp'], ['qwen', '--experimental-acp']] },
|
|
35
|
-
{ id: 'kimi', name: 'Kimi CLI', candidates: [['kimi', 'acp'], ['kimi', '--acp']] },
|
|
34
|
+
{ id: 'qwen', name: 'Qwen Code', candidates: [['qwen', '--acp'], ['qwen', '--acp', '--experimental-skills'], ['qwen', '--experimental-acp']] },
|
|
35
|
+
{ id: 'kimi', name: 'Kimi Code CLI', candidates: [['kimi', 'acp'], ['kimi', '--acp']] },
|
|
36
36
|
// DeepSeek Harness: ACP is a dsh profile; the model key comes from DEEPSEEK_API_KEY.
|
|
37
37
|
{ id: 'deepseek', name: 'DeepSeek Harness', candidates: [['dsh', '--profile', 'acp']] },
|
|
38
38
|
{ id: 'qoder', name: 'Qoder CLI', candidates: [['qodercli', '--acp'], ['qoderclicn', '--acp']] },
|
|
@@ -40,7 +40,56 @@ export const KNOWN_ACP_AGENTS = [
|
|
|
40
40
|
id: 'zcode',
|
|
41
41
|
name: 'ZCode',
|
|
42
42
|
candidates: [['zcode-acp']],
|
|
43
|
-
adapter: { baseBinary: 'zcode',
|
|
43
|
+
adapter: { baseBinary: 'zcode', packageName: 'zcode-acp' },
|
|
44
|
+
},
|
|
45
|
+
// Launch lines follow Paseo's ACP catalog (packages/app/src/data/acp-provider-catalog.ts).
|
|
46
|
+
// Local binaries only: detect must not run `npx -y`, which would download a package just to probe it.
|
|
47
|
+
{ id: 'amp', name: 'Amp', candidates: [['amp-acp']] },
|
|
48
|
+
{
|
|
49
|
+
id: 'auggie',
|
|
50
|
+
name: 'Auggie CLI',
|
|
51
|
+
candidates: [['auggie', '--acp']],
|
|
52
|
+
env: { AUGMENT_DISABLE_AUTO_UPDATE: '1' },
|
|
53
|
+
},
|
|
54
|
+
{ id: 'autohand', name: 'Autohand Code', candidates: [['autohand-acp']] },
|
|
55
|
+
{ id: 'cline', name: 'Cline', candidates: [['cline', '--acp']] },
|
|
56
|
+
{ id: 'codebuddy', name: 'Codebuddy Code', candidates: [['codebuddy', '--acp']] },
|
|
57
|
+
{ id: 'codewhale', name: 'CodeWhale', candidates: [['codewhale', 'serve', '--acp']] },
|
|
58
|
+
{ id: 'cortex', name: 'Cortex Code', candidates: [['cortex', 'acp', 'serve']] },
|
|
59
|
+
{ id: 'corust', name: 'Corust Agent', candidates: [['corust-agent-acp']] },
|
|
60
|
+
{ id: 'crow', name: 'crow-cli', candidates: [['crow-cli', 'acp']] },
|
|
61
|
+
{ id: 'deepagents', name: 'DeepAgents', candidates: [['deepagents-acp']] },
|
|
62
|
+
{ id: 'devin', name: 'Devin CLI', candidates: [['devin', 'acp']] },
|
|
63
|
+
{ id: 'dimcode', name: 'DimCode', candidates: [['dimcode', 'acp'], ['dim', 'acp']] },
|
|
64
|
+
{ id: 'dirac', name: 'Dirac', candidates: [['dirac', '--acp']] },
|
|
65
|
+
{
|
|
66
|
+
id: 'droid',
|
|
67
|
+
name: 'Factory Droid',
|
|
68
|
+
candidates: [['droid', 'exec', '--output-format', 'acp-daemon']],
|
|
69
|
+
env: { DROID_DISABLE_AUTO_UPDATE: 'true', FACTORY_DROID_AUTO_UPDATE_ENABLED: 'false' },
|
|
70
|
+
},
|
|
71
|
+
{ id: 'fast-agent', name: 'fast-agent', candidates: [['fast-agent-acp', '-x']] },
|
|
72
|
+
{ id: 'gjc', name: 'Gajae Code', candidates: [['gjc', 'acp']], env: { GJC_ACP_PERMISSION_MODE: 'prompt' } },
|
|
73
|
+
{ id: 'glm', name: 'GLM Agent', candidates: [['glm-acp-agent']] },
|
|
74
|
+
{ id: 'grok', name: 'Grok', candidates: [['grok', 'agent', 'stdio']] },
|
|
75
|
+
{ id: 'hoplite', name: 'Hoplite', candidates: [['hoplite', 'acp']] },
|
|
76
|
+
{ id: 'junie', name: 'Junie', candidates: [['junie', '--acp', 'true']] },
|
|
77
|
+
{ id: 'kilo', name: 'Kilo Code', candidates: [['kilo', 'acp']] },
|
|
78
|
+
{ id: 'kiro', name: 'Kiro CLI', candidates: [['kiro-cli', 'acp']] },
|
|
79
|
+
{ id: 'minimax', name: 'MiniMax Code', candidates: [['mcode', 'acp']] },
|
|
80
|
+
{ id: 'minion', name: 'Minion Code', candidates: [['minion-code', 'acp']] },
|
|
81
|
+
{ id: 'vibe', name: 'Mistral Vibe', candidates: [['vibe-acp']] },
|
|
82
|
+
{ id: 'nova', name: 'Nova', candidates: [['nova', 'acp']] },
|
|
83
|
+
{ id: 'poolside', name: 'Poolside', candidates: [['pool', 'acp']] },
|
|
84
|
+
{ id: 'agoragentic', name: 'Agoragentic', candidates: [['agoragentic-mcp', '--acp']] },
|
|
85
|
+
{ id: 'sigit', name: 'siGit Code', candidates: [['sigit']] },
|
|
86
|
+
{ id: 'stakpak', name: 'Stakpak', candidates: [['stakpak', 'acp']] },
|
|
87
|
+
{ id: 'trae', name: 'TRAE CLI', candidates: [['traecli', 'acp', 'serve']] },
|
|
88
|
+
{
|
|
89
|
+
id: 'vtcode',
|
|
90
|
+
name: 'VT Code',
|
|
91
|
+
candidates: [['vtcode', 'acp']],
|
|
92
|
+
env: { VT_ACP_ENABLED: '1', VT_ACP_ZED_ENABLED: '1' },
|
|
44
93
|
},
|
|
45
94
|
];
|
|
46
95
|
/** The known agent a manual launch command starts, by executable name and arguments. */
|
|
@@ -52,6 +101,17 @@ export function inferAgentId(command, args = []) {
|
|
|
52
101
|
}
|
|
53
102
|
// Some agents cold-start slowly (OpenCode boots a local server: ~40s on first run).
|
|
54
103
|
const PROBE_TIMEOUT_MS = 45_000;
|
|
104
|
+
/** A launch stayed up but never completed the ACP handshake. Further flags of that agent are not tried. */
|
|
105
|
+
export class AcpProbeTimeoutError extends Error {
|
|
106
|
+
constructor() {
|
|
107
|
+
super('no ACP handshake within timeout');
|
|
108
|
+
this.name = 'AcpProbeTimeoutError';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** A quick failure can be a wrong flag. A timeout means that agent is not going to answer. */
|
|
112
|
+
export function shouldTryNextAcpCandidate(error) {
|
|
113
|
+
return !(error instanceof AcpProbeTimeoutError);
|
|
114
|
+
}
|
|
55
115
|
export async function findExecutable(name, envPath = process.env.PATH ?? '') {
|
|
56
116
|
const extensions = process.platform === 'win32' ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';') : [''];
|
|
57
117
|
for (const dir of envPath.split(path.delimiter).filter(Boolean)) {
|
|
@@ -70,8 +130,12 @@ export async function findExecutable(name, envPath = process.env.PATH ?? '') {
|
|
|
70
130
|
return null;
|
|
71
131
|
}
|
|
72
132
|
/** Starts the agent, runs `initialize`, and stops it. Never leaves the process running. */
|
|
73
|
-
export async function probeAcpAgent(command, args, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
74
|
-
const launch = agentLaunch(command, args, {
|
|
133
|
+
export async function probeAcpAgent(command, args, timeoutMs = PROBE_TIMEOUT_MS, env) {
|
|
134
|
+
const launch = agentLaunch(command, args, {
|
|
135
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
136
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
137
|
+
windowsHide: true,
|
|
138
|
+
});
|
|
75
139
|
const child = spawn(launch.command, launch.args, launch.options);
|
|
76
140
|
if (!child.stdin || !child.stdout)
|
|
77
141
|
throw new Error('ACP probe stdio was not piped');
|
|
@@ -82,7 +146,7 @@ export async function probeAcpAgent(command, args, timeoutMs = PROBE_TIMEOUT_MS)
|
|
|
82
146
|
child.once('exit', (code) => reject(new Error(`exited before handshake (code ${code})`)));
|
|
83
147
|
});
|
|
84
148
|
const timedOut = new Promise((_, reject) => {
|
|
85
|
-
timer = setTimeout(() => reject(new
|
|
149
|
+
timer = setTimeout(() => reject(new AcpProbeTimeoutError()), timeoutMs);
|
|
86
150
|
});
|
|
87
151
|
const connection = new ClientSideConnection(() => ({
|
|
88
152
|
sessionUpdate: async () => { },
|
|
@@ -118,17 +182,29 @@ export async function detectAcpAgents(options = {}) {
|
|
|
118
182
|
return { id: agent.id, name: agent.name, command, args, verified: false, agentName: null, agentVersion: null };
|
|
119
183
|
}
|
|
120
184
|
try {
|
|
121
|
-
const info = await probeAcpAgent(command, args);
|
|
122
|
-
|
|
185
|
+
const info = await probeAcpAgent(command, args, options.timeoutMs, agent.env);
|
|
186
|
+
const detected = {
|
|
187
|
+
id: agent.id,
|
|
188
|
+
name: agent.name,
|
|
189
|
+
command,
|
|
190
|
+
args,
|
|
191
|
+
verified: true,
|
|
192
|
+
...info,
|
|
193
|
+
...(agent.env ? { env: agent.env } : {}),
|
|
194
|
+
};
|
|
195
|
+
options.onDetected?.(detected);
|
|
196
|
+
return detected;
|
|
123
197
|
}
|
|
124
198
|
catch (error) {
|
|
125
199
|
lastError = error instanceof Error ? error.message : String(error);
|
|
200
|
+
if (!shouldTryNextAcpCandidate(error))
|
|
201
|
+
break;
|
|
126
202
|
}
|
|
127
203
|
}
|
|
128
204
|
if (!lastError) {
|
|
129
205
|
if (agent.adapter && (await findExecutable(agent.adapter.baseBinary))) {
|
|
130
206
|
const [binary, ...args] = agent.candidates[0];
|
|
131
|
-
|
|
207
|
+
const detected = {
|
|
132
208
|
id: agent.id,
|
|
133
209
|
name: agent.name,
|
|
134
210
|
command: binary,
|
|
@@ -136,14 +212,51 @@ export async function detectAcpAgents(options = {}) {
|
|
|
136
212
|
verified: false,
|
|
137
213
|
agentName: null,
|
|
138
214
|
agentVersion: null,
|
|
139
|
-
installHint: agent.adapter.
|
|
215
|
+
installHint: `npm install -g ${agent.adapter.packageName}`,
|
|
140
216
|
};
|
|
217
|
+
options.onDetected?.(detected);
|
|
218
|
+
return detected;
|
|
141
219
|
}
|
|
142
220
|
return null;
|
|
143
221
|
}
|
|
144
222
|
const [binary, ...args] = agent.candidates[0];
|
|
145
223
|
const command = (await findExecutable(binary)) ?? binary;
|
|
146
|
-
|
|
224
|
+
const detected = {
|
|
225
|
+
id: agent.id,
|
|
226
|
+
name: agent.name,
|
|
227
|
+
command,
|
|
228
|
+
args,
|
|
229
|
+
verified: false,
|
|
230
|
+
agentName: null,
|
|
231
|
+
agentVersion: null,
|
|
232
|
+
error: lastError,
|
|
233
|
+
};
|
|
234
|
+
options.onDetected?.(detected);
|
|
235
|
+
return detected;
|
|
147
236
|
}));
|
|
148
237
|
return results.filter((agent) => agent !== null);
|
|
149
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Agents whose own CLI is on PATH but none of the ACP launch commands are.
|
|
241
|
+
* Those need a global adapter package before a gateway can start them.
|
|
242
|
+
*/
|
|
243
|
+
export async function findMissingAcpAdapters(envPath = process.env.PATH ?? '') {
|
|
244
|
+
const missing = [];
|
|
245
|
+
for (const agent of KNOWN_ACP_AGENTS) {
|
|
246
|
+
if (!agent.adapter)
|
|
247
|
+
continue;
|
|
248
|
+
if (!(await findExecutable(agent.adapter.baseBinary, envPath)))
|
|
249
|
+
continue;
|
|
250
|
+
let adapterPresent = false;
|
|
251
|
+
for (const [binary] of agent.candidates) {
|
|
252
|
+
if (await findExecutable(binary, envPath)) {
|
|
253
|
+
adapterPresent = true;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!adapterPresent) {
|
|
258
|
+
missing.push({ id: agent.id, name: agent.name, packageName: agent.adapter.packageName });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return missing;
|
|
262
|
+
}
|