@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 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,并用真实握手确认能否启动。`--add` 把已确认的 agent 写入当前账号的 gateway 配置,并由 daemon 托管。`gateway list` 只显示环境变量名,不显示值。`dino gateway mcp` 由这些 agent 拉起,对外提供只读的 Dinox 知识工具。
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 detected = await detectAcpAgents({ probe: true });
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;
@@ -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 resolveUpdateSpawnSpec(pm, platform = process.platform) {
55
- const { bin, args } = PM_COMMANDS[pm];
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: [`${bin}.cmd`, ...args].join(' '),
59
+ command: [`${pm}.cmd`, ...args].join(' '),
60
60
  args: [],
61
61
  shell: true,
62
62
  };
63
63
  }
64
- return {
65
- command: bin,
66
- args: [...args],
67
- shell: false,
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 runUpdate(pm, options) {
78
- const { command, args, shell } = resolveUpdateSpawnSpec(pm);
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
  };
@@ -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';
@@ -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
- if (existing.agentId === agent.id && sameLaunch(existing, agent)) {
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 = { ...existing, agentId: agent.id, command: agent.command, args: agent.args };
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
  }
@@ -7,14 +7,22 @@ export declare const KNOWN_ACP_AGENTS: ReadonlyArray<{
7
7
  id: string;
8
8
  name: string;
9
9
  candidates: string[][];
10
- /** The agent's own CLI and the ACP adapter to install when only the CLI is present. */
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
- install: string;
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[]>;
@@ -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', install: 'npm install -g @agentclientprotocol/claude-agent-acp' },
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', install: 'npm install -g @agentclientprotocol/codex-acp' },
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', install: 'npm install -g zcode-acp' },
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, { stdio: ['pipe', 'pipe', 'ignore'], env: process.env, windowsHide: true });
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 Error('no ACP handshake within timeout')), timeoutMs);
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
- return { id: agent.id, name: agent.name, command, args, verified: true, ...info };
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
- return {
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.install,
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
- return { id: agent.id, name: agent.name, command, args, verified: false, agentName: null, agentVersion: null, error: lastError };
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dinoxx/dinox-cli",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "Dinox CLI",
5
5
  "main": "dist/dinox.js",
6
6
  "scripts": {