@axhub/acp 0.1.2-beta.0 → 0.1.2-beta.2

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.
Files changed (60) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/fallback-build-manifest.json +3 -3
  4. package/.next/server/app/_global-error.html +1 -1
  5. package/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/server/app/_not-found.html +1 -1
  12. package/.next/server/app/_not-found.rsc +1 -1
  13. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  15. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  16. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  19. package/.next/server/app/api/acp/capabilities/route.js +1 -1
  20. package/.next/server/app/api/acp/capabilities/route.js.nft.json +1 -1
  21. package/.next/server/app/api/chat/cancel/route.js +1 -1
  22. package/.next/server/app/api/chat/cancel/route.js.nft.json +1 -1
  23. package/.next/server/app/api/chat/resume/[streamId]/route.js +1 -1
  24. package/.next/server/app/api/chat/resume/[streamId]/route.js.nft.json +1 -1
  25. package/.next/server/app/api/chat/route.js +1 -1
  26. package/.next/server/app/api/chat/route.js.nft.json +1 -1
  27. package/.next/server/app/api/conversations/[threadId]/messages/route.js +2 -2
  28. package/.next/server/app/api/conversations/[threadId]/runtime/route.js +1 -1
  29. package/.next/server/app/api/conversations/[threadId]/runtime/route.js.nft.json +1 -1
  30. package/.next/server/app/page_client-reference-manifest.js +1 -1
  31. package/.next/server/app/thread/[threadId]/page_client-reference-manifest.js +1 -1
  32. package/.next/server/chunks/[root-of-the-server]__0aovkxs._.js +1 -1
  33. package/.next/server/chunks/[root-of-the-server]__0c.r6ru._.js +5 -5
  34. package/.next/server/chunks/{[root-of-the-server]__04pn6ap._.js → [root-of-the-server]__0cki6-w._.js} +1 -1
  35. package/.next/server/chunks/[root-of-the-server]__0gmxr~m._.js +1 -1
  36. package/.next/server/chunks/{[root-of-the-server]__0zn3~pq._.js → [root-of-the-server]__0lfmu8f._.js} +1 -1
  37. package/.next/server/chunks/{[root-of-the-server]__0lbwo2g._.js → [root-of-the-server]__0m33tyh._.js} +1 -1
  38. package/.next/server/chunks/[root-of-the-server]__0o2epta._.js +1 -1
  39. package/.next/server/chunks/{[root-of-the-server]__0ml.1wa._.js → [root-of-the-server]__0on1ayi._.js} +1 -1
  40. package/.next/server/chunks/[root-of-the-server]__0os92l7._.js +1 -1
  41. package/.next/server/chunks/{[root-of-the-server]__0zmyki-._.js → [root-of-the-server]__0va2ld4._.js} +1 -1
  42. package/.next/server/chunks/[root-of-the-server]__13xepwb._.js +1 -1
  43. package/.next/server/chunks/ssr/_03.pm1z._.js +1 -1
  44. package/.next/server/middleware-build-manifest.js +3 -3
  45. package/.next/server/pages/404.html +1 -1
  46. package/.next/server/pages/500.html +1 -1
  47. package/.next/static/chunks/{0r125n-._y5ny.js → 0g95d4qshh~b1.js} +1 -1
  48. package/README.md +9 -3
  49. package/bin/acp.mjs +3 -3
  50. package/bin/defaults.mjs +3 -0
  51. package/dist/lib/acp2aisdk/provider-command.d.ts +22 -0
  52. package/dist/lib/acp2aisdk/provider-command.mjs +268 -0
  53. package/dist/lib/acp2aisdk/provider-registry.mjs +35 -2
  54. package/dist/lib/acp2aisdk/session-runtime.mjs +54 -16
  55. package/dist/lib/acp2aisdk/types.d.ts +7 -4
  56. package/dist/public-api/server.d.ts +1 -1
  57. package/package.json +2 -2
  58. /package/.next/static/{0241KNhzDEg6VFWZ_e_qK → afQGa38bHoajAY5kxCATW}/_buildManifest.js +0 -0
  59. /package/.next/static/{0241KNhzDEg6VFWZ_e_qK → afQGa38bHoajAY5kxCATW}/_clientMiddlewareManifest.js +0 -0
  60. /package/.next/static/{0241KNhzDEg6VFWZ_e_qK → afQGa38bHoajAY5kxCATW}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -19,7 +19,7 @@ npx -y @axhub/acp --port 32123
19
19
  npx -y @axhub/acp --cors-origin https://client.example.com
20
20
  ```
21
21
 
22
- `--cors-origin` configures the allowed browser Origin for `/api/*` routes. It also supports comma-separated origins and the aliases `--cors` and `--cor`.
22
+ By default the CLI allows `/api/*` CORS requests from `http://localhost:53817` and `http://127.0.0.1:53817`. `--cors-origin` replaces that default with another allowed browser Origin list. It also supports comma-separated origins and the aliases `--cors` and `--cor`.
23
23
 
24
24
  ### External Integration
25
25
 
@@ -54,10 +54,10 @@ pnpm dev
54
54
 
55
55
  Open [http://localhost:32123](http://localhost:32123) with your browser to see the result.
56
56
 
57
- `npm run dev` uses the local source checkout and runs `next dev`. For a local host app on another origin, pass the same CORS option through npm:
57
+ `npm run dev` uses the local source checkout and runs `next dev`. It allows the same default host-app origins on port `53817`. For a local host app on another origin, pass the CORS option through npm:
58
58
 
59
59
  ```bash
60
- npm run dev -- --cors-origin http://localhost:53817
60
+ npm run dev -- --cors-origin http://localhost:3000
61
61
  ```
62
62
 
63
63
  For the common local host-app setup, use the host-aware development command. It binds the ACP UI dev server on `0.0.0.0`, allows the usual host app origins on port `53817`, and also lets Next.js dev assets/HMR accept those origins:
@@ -111,3 +111,9 @@ npm run refresh:default-capabilities -- --provider=codex
111
111
 
112
112
  Use `ACP_PROVIDER_COMMAND_OVERRIDES` or per-provider `ACP_<PROVIDER>_COMMAND`
113
113
  environment variables when refreshing defaults for a new or local provider.
114
+ Provider startup recipes live in `lib/acp2aisdk/provider-registry.ts`; each
115
+ entry can define `command`, `args`, and optional `fallbackCommands` without
116
+ touching the shared session runtime. Cross-platform PATH/PATHEXT enrichment and
117
+ Windows npm shim wrapping are centralized in `lib/acp2aisdk/provider-command.ts`,
118
+ so new providers should only need a startup recipe unless they have protocol
119
+ differences.
package/bin/acp.mjs CHANGED
@@ -5,11 +5,11 @@ import { existsSync } from "node:fs";
5
5
  import { createRequire } from "node:module";
6
6
  import { dirname, join } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { DEFAULT_CORS_ORIGINS, DEFAULT_PORT } from "./defaults.mjs";
8
9
 
9
10
  const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
10
11
  const launchCwd = process.cwd();
11
12
  const require = createRequire(import.meta.url);
12
- const DEFAULT_PORT = "32123";
13
13
 
14
14
  function printHelp() {
15
15
  console.log(`Usage: acp [options]
@@ -19,7 +19,7 @@ Start the ACP Web UI.
19
19
  Options:
20
20
  -p, --port <port> Port to listen on (default: ${DEFAULT_PORT})
21
21
  -H, --hostname <hostname> Hostname to bind (default: 0.0.0.0)
22
- --cors-origin <origin[,..]> Allowed CORS Origin for /api/* routes
22
+ --cors-origin <origin[,..]> Allowed CORS Origin for /api/* routes (default: ${DEFAULT_CORS_ORIGINS})
23
23
  --cors <origin[,..]> Alias for --cors-origin
24
24
  --cor <origin[,..]> Alias for --cors-origin
25
25
  -h, --help Show help
@@ -49,7 +49,7 @@ function parseArgs(args) {
49
49
  const options = {
50
50
  port: process.env.PORT || DEFAULT_PORT,
51
51
  hostname: process.env.HOSTNAME || "0.0.0.0",
52
- corsOrigins: process.env.ACP_UI_CORS_ORIGINS || "",
52
+ corsOrigins: process.env.ACP_UI_CORS_ORIGINS ?? DEFAULT_CORS_ORIGINS,
53
53
  };
54
54
 
55
55
  for (let index = 0; index < args.length; index += 1) {
@@ -0,0 +1,3 @@
1
+ export const DEFAULT_PORT = "32123";
2
+ export const DEFAULT_CORS_ORIGINS =
3
+ "http://localhost:53817,http://127.0.0.1:53817";
@@ -0,0 +1,22 @@
1
+ import type { AcpProviderCommand } from "./types";
2
+ type ProviderCommandEnvInput = Record<string, string | undefined>;
3
+ type ProviderCommandOptions = {
4
+ platform?: NodeJS.Platform;
5
+ execPath?: string;
6
+ env?: ProviderCommandEnvInput;
7
+ };
8
+ export declare function buildProviderCommandEnv(options?: ProviderCommandOptions): Record<string, string>;
9
+ export declare function isProviderCommandAvailable(commandConfig: AcpProviderCommand, options?: NodeJS.Platform | ProviderCommandOptions): boolean;
10
+ export declare function resolveProviderCommand(commandConfig: AcpProviderCommand, options?: NodeJS.Platform | ProviderCommandOptions): AcpProviderCommand;
11
+ export declare function resolveProviderCommandCandidates({ command, args, fallbackCommands, platform, env, execPath, }: AcpProviderCommand & {
12
+ fallbackCommands?: AcpProviderCommand[];
13
+ platform?: NodeJS.Platform;
14
+ env?: ProviderCommandEnvInput;
15
+ execPath?: string;
16
+ }): AcpProviderCommand[];
17
+ export declare function formatProviderCommand(commandConfig: AcpProviderCommand): string;
18
+ export declare function createProviderCommandNotFoundError(commandConfig: AcpProviderCommand): Error & {
19
+ code: string;
20
+ };
21
+ export declare function isProviderCommandStartupError(error: unknown): boolean;
22
+ export {};
@@ -0,0 +1,268 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { accessSync, constants, existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ const WINDOWS_COMMAND_PROCESSOR = "cmd.exe";
5
+ const POSIX_GUI_PATHS = [
6
+ "/opt/homebrew/bin",
7
+ "/usr/local/bin",
8
+ "/usr/bin",
9
+ "/bin",
10
+ "/usr/sbin",
11
+ "/sbin",
12
+ ];
13
+ const WINDOWS_NODE_PATHS = ["C:\\Program Files\\nodejs"];
14
+ function normalizeProviderCommandOptions(options) {
15
+ var _a, _b, _c;
16
+ if (typeof options === "string") {
17
+ return {
18
+ platform: options,
19
+ execPath: process.execPath,
20
+ env: process.env,
21
+ };
22
+ }
23
+ return {
24
+ platform: (_a = options === null || options === void 0 ? void 0 : options.platform) !== null && _a !== void 0 ? _a : process.platform,
25
+ execPath: (_b = options === null || options === void 0 ? void 0 : options.execPath) !== null && _b !== void 0 ? _b : process.execPath,
26
+ env: (_c = options === null || options === void 0 ? void 0 : options.env) !== null && _c !== void 0 ? _c : process.env,
27
+ };
28
+ }
29
+ function getEnvValue(env, key) {
30
+ const direct = env[key];
31
+ if (typeof direct === "string" && direct.length > 0)
32
+ return direct;
33
+ const matchedKey = Object.keys(env).find((candidate) => candidate.toLowerCase() === key.toLowerCase());
34
+ if (!matchedKey)
35
+ return undefined;
36
+ const value = env[matchedKey];
37
+ return typeof value === "string" && value.length > 0 ? value : undefined;
38
+ }
39
+ function cloneDefinedEnv(env) {
40
+ const cloned = {};
41
+ for (const [key, value] of Object.entries(env)) {
42
+ if (typeof value === "string")
43
+ cloned[key] = value;
44
+ }
45
+ return cloned;
46
+ }
47
+ function appendUniquePathSegment(segments, nextSegment, platform) {
48
+ if (!nextSegment)
49
+ return;
50
+ const exists = segments.some((segment) => platform === "win32"
51
+ ? segment.toLowerCase() === nextSegment.toLowerCase()
52
+ : segment === nextSegment);
53
+ if (!exists)
54
+ segments.push(nextSegment);
55
+ }
56
+ function getWindowsPathExtList(env) {
57
+ const pathExt = getEnvValue(env, "PATHEXT") || ".COM;.EXE;.BAT;.CMD";
58
+ return pathExt
59
+ .split(";")
60
+ .map((extension) => extension.trim())
61
+ .filter(Boolean)
62
+ .map((extension) => extension.startsWith(".")
63
+ ? extension.toLowerCase()
64
+ : `.${extension.toLowerCase()}`);
65
+ }
66
+ export function buildProviderCommandEnv(options) {
67
+ const { platform, execPath, env: inputEnv, } = normalizeProviderCommandOptions(options);
68
+ const env = cloneDefinedEnv(inputEnv);
69
+ const pathApi = platform === "win32" ? path.win32 : path.posix;
70
+ const delimiter = platform === "win32" ? ";" : ":";
71
+ const pathKey = platform === "win32" ? "Path" : "PATH";
72
+ const existingPath = getEnvValue(env, "PATH") || "";
73
+ const segments = existingPath.split(delimiter).filter(Boolean);
74
+ appendUniquePathSegment(segments, pathApi.dirname(execPath), platform);
75
+ if (platform === "win32") {
76
+ const appData = getEnvValue(env, "APPDATA");
77
+ if (appData) {
78
+ appendUniquePathSegment(segments, path.win32.join(appData, "npm"), platform);
79
+ }
80
+ for (const executablePath of WINDOWS_NODE_PATHS) {
81
+ appendUniquePathSegment(segments, executablePath, platform);
82
+ }
83
+ const pathExtSegments = getWindowsPathExtList(env);
84
+ for (const requiredExtension of [".com", ".exe", ".bat", ".cmd"]) {
85
+ if (!pathExtSegments.includes(requiredExtension)) {
86
+ pathExtSegments.push(requiredExtension);
87
+ }
88
+ }
89
+ env.PATHEXT = pathExtSegments
90
+ .map((extension) => extension.toUpperCase())
91
+ .join(";");
92
+ for (const key of Object.keys(env)) {
93
+ if (key.toLowerCase() === "path")
94
+ delete env[key];
95
+ }
96
+ const pathValue = segments.join(delimiter);
97
+ env[pathKey] = pathValue;
98
+ // acp-ai-provider merges config.env over process.env before spawning; set
99
+ // PATH too so a pre-existing uppercase key cannot shadow the enhanced Path.
100
+ env.PATH = pathValue;
101
+ }
102
+ else {
103
+ for (const executablePath of POSIX_GUI_PATHS) {
104
+ appendUniquePathSegment(segments, executablePath, platform);
105
+ }
106
+ delete env.Path;
107
+ env[pathKey] = segments.join(delimiter);
108
+ }
109
+ return env;
110
+ }
111
+ function hasPathSeparator(command) {
112
+ return /[\\/]/.test(command);
113
+ }
114
+ function canExecuteFile(filePath, platform) {
115
+ if (!existsSync(filePath))
116
+ return false;
117
+ if (platform === "win32")
118
+ return true;
119
+ try {
120
+ accessSync(filePath, constants.X_OK);
121
+ return true;
122
+ }
123
+ catch (_a) {
124
+ return false;
125
+ }
126
+ }
127
+ function resolveWindowsCommandPath(command, env) {
128
+ const trimmed = command.trim();
129
+ if (!trimmed)
130
+ return trimmed;
131
+ const hasCommandPathSeparator = hasPathSeparator(trimmed);
132
+ const extension = path.win32.extname(trimmed);
133
+ const pathExtensions = extension ? [""] : getWindowsPathExtList(env);
134
+ const candidateDirectories = hasCommandPathSeparator
135
+ ? [""]
136
+ : (getEnvValue(env, "PATH") || "")
137
+ .split(";")
138
+ .map((entry) => entry.trim())
139
+ .filter(Boolean);
140
+ const baseCandidates = hasCommandPathSeparator
141
+ ? [trimmed]
142
+ : candidateDirectories.map((directory) => path.win32.join(directory, trimmed));
143
+ for (const baseCandidate of baseCandidates) {
144
+ for (const suffix of pathExtensions) {
145
+ const fullPath = suffix ? `${baseCandidate}${suffix}` : baseCandidate;
146
+ if (existsSync(fullPath))
147
+ return fullPath;
148
+ }
149
+ }
150
+ return trimmed;
151
+ }
152
+ export function isProviderCommandAvailable(commandConfig, options) {
153
+ const { platform, execPath, env: inputEnv, } = normalizeProviderCommandOptions(options);
154
+ const env = buildProviderCommandEnv({ platform, execPath, env: inputEnv });
155
+ const { command } = commandConfig;
156
+ if (platform === "win32") {
157
+ if (command.toLowerCase() === WINDOWS_COMMAND_PROCESSOR)
158
+ return true;
159
+ const resolvedCommand = resolveWindowsCommandPath(command, env);
160
+ if (canExecuteFile(resolvedCommand, platform))
161
+ return true;
162
+ if (hasPathSeparator(command))
163
+ return false;
164
+ try {
165
+ execFileSync("where.exe", [command], {
166
+ encoding: "utf8",
167
+ env: env,
168
+ stdio: ["ignore", "pipe", "ignore"],
169
+ });
170
+ return true;
171
+ }
172
+ catch (_a) {
173
+ return false;
174
+ }
175
+ }
176
+ if (hasPathSeparator(command))
177
+ return canExecuteFile(command, platform);
178
+ try {
179
+ execFileSync("which", [command], {
180
+ encoding: "utf8",
181
+ env: env,
182
+ stdio: ["ignore", "pipe", "ignore"],
183
+ });
184
+ return true;
185
+ }
186
+ catch (_b) {
187
+ return false;
188
+ }
189
+ }
190
+ function shouldUseWindowsCommandProcessor(command, platform) {
191
+ if (platform !== "win32")
192
+ return false;
193
+ if (command.toLowerCase() === WINDOWS_COMMAND_PROCESSOR)
194
+ return false;
195
+ return /\.(cmd|bat)$/i.test(command) || !/\.(exe|com)$/i.test(command);
196
+ }
197
+ function quoteWindowsCommandLinePart(value) {
198
+ if (!value)
199
+ return '""';
200
+ if (!/[\s"&^|<>]/.test(value))
201
+ return value;
202
+ const escaped = value
203
+ .replace(/(\\*)"/g, '$1$1\\"')
204
+ .replace(/(\\+)$/g, "$1$1");
205
+ return `"${escaped}"`;
206
+ }
207
+ function buildWindowsCommandLine(command, args) {
208
+ return [command, ...args]
209
+ .map((part) => quoteWindowsCommandLinePart(String(part)))
210
+ .join(" ");
211
+ }
212
+ function resolveWindowsSpawnCommand(commandConfig, platform, env) {
213
+ var _a;
214
+ const command = platform === "win32"
215
+ ? resolveWindowsCommandPath(commandConfig.command, env)
216
+ : commandConfig.command;
217
+ if (!shouldUseWindowsCommandProcessor(command, platform)) {
218
+ if (command === commandConfig.command)
219
+ return commandConfig;
220
+ return {
221
+ command,
222
+ args: commandConfig.args,
223
+ };
224
+ }
225
+ if (command.toLowerCase() === WINDOWS_COMMAND_PROCESSOR) {
226
+ return commandConfig;
227
+ }
228
+ return {
229
+ command: WINDOWS_COMMAND_PROCESSOR,
230
+ args: [
231
+ "/d",
232
+ "/s",
233
+ "/c",
234
+ buildWindowsCommandLine(command, (_a = commandConfig.args) !== null && _a !== void 0 ? _a : []),
235
+ ],
236
+ };
237
+ }
238
+ export function resolveProviderCommand(commandConfig, options) {
239
+ const { platform, execPath, env: inputEnv, } = normalizeProviderCommandOptions(options);
240
+ const env = buildProviderCommandEnv({ platform, execPath, env: inputEnv });
241
+ return resolveWindowsSpawnCommand(commandConfig, platform, env);
242
+ }
243
+ export function resolveProviderCommandCandidates({ command, args, fallbackCommands, platform = process.platform, env, execPath, }) {
244
+ const candidates = [{ command, args }, ...(fallbackCommands !== null && fallbackCommands !== void 0 ? fallbackCommands : [])];
245
+ return candidates.map((candidate) => resolveProviderCommand(candidate, { platform, env, execPath }));
246
+ }
247
+ export function formatProviderCommand(commandConfig) {
248
+ var _a;
249
+ return [commandConfig.command, ...((_a = commandConfig.args) !== null && _a !== void 0 ? _a : [])]
250
+ .map((part) => (/\s/.test(part) ? JSON.stringify(part) : part))
251
+ .join(" ");
252
+ }
253
+ export function createProviderCommandNotFoundError(commandConfig) {
254
+ const error = new Error(`ACP provider command not found: ${formatProviderCommand(commandConfig)}`);
255
+ error.code = "ENOENT";
256
+ return error;
257
+ }
258
+ export function isProviderCommandStartupError(error) {
259
+ if (!error || typeof error !== "object")
260
+ return false;
261
+ const code = error.code;
262
+ if (code === "ENOENT" || code === "EINVAL")
263
+ return true;
264
+ const message = error instanceof Error ? error.message : String(error.message);
265
+ return (/\bspawn\b.+\b(ENOENT|EINVAL)\b/i.test(message) ||
266
+ /\b(?:ACP\s+)?connection closed\b/i.test(message) ||
267
+ /(not recognized as an internal or external command|command not found)/i.test(message));
268
+ }
@@ -34,8 +34,14 @@ export const ACP_PROVIDER_REGISTRY = {
34
34
  gemini: {
35
35
  provider: "gemini",
36
36
  label: "Gemini",
37
- command: "npx",
38
- args: ["-y", "@google/gemini-cli@^0.44.1", "--acp", "--skip-trust"],
37
+ command: "gemini",
38
+ args: ["--acp", "--skip-trust"],
39
+ fallbackCommands: [
40
+ {
41
+ command: "npx",
42
+ args: ["-y", "@google/gemini-cli@^0.44.1", "--acp", "--skip-trust"],
43
+ },
44
+ ],
39
45
  defaultModel: "gemini-3-pro-preview",
40
46
  defaultModeId: ACP_PROVIDER_DEFAULT_MODE_IDS.gemini,
41
47
  supportsImages: false,
@@ -91,6 +97,26 @@ function parseCommandLine(value) {
91
97
  args,
92
98
  };
93
99
  }
100
+ function parseCommandObject(value) {
101
+ if (typeof value === "string") {
102
+ const commandLine = value.trim();
103
+ return commandLine ? parseCommandLine(commandLine) : null;
104
+ }
105
+ if (!value || typeof value !== "object" || Array.isArray(value))
106
+ return null;
107
+ const record = value;
108
+ const command = typeof record.command === "string" && record.command.trim()
109
+ ? record.command.trim()
110
+ : null;
111
+ if (!command)
112
+ return null;
113
+ const args = Array.isArray(record.args)
114
+ ? record.args
115
+ .map((item) => String(item).trim())
116
+ .filter((item) => Boolean(item))
117
+ : undefined;
118
+ return { command, args };
119
+ }
94
120
  function parseProviderOverrideObject(value) {
95
121
  if (typeof value === "string") {
96
122
  const commandLine = value.trim();
@@ -107,6 +133,11 @@ function parseProviderOverrideObject(value) {
107
133
  .map((item) => String(item).trim())
108
134
  .filter((item) => Boolean(item))
109
135
  : undefined;
136
+ const fallbackCommands = Array.isArray(record.fallbackCommands)
137
+ ? record.fallbackCommands
138
+ .map(parseCommandObject)
139
+ .filter((item) => Boolean(item))
140
+ : undefined;
110
141
  const sessionDelayMs = typeof record.sessionDelayMs === "number" &&
111
142
  Number.isFinite(record.sessionDelayMs) &&
112
143
  record.sessionDelayMs >= 0
@@ -117,6 +148,8 @@ function parseProviderOverrideObject(value) {
117
148
  override.command = command;
118
149
  if (args)
119
150
  override.args = args;
151
+ if (fallbackCommands)
152
+ override.fallbackCommands = fallbackCommands;
120
153
  if (typeof record.authMethodId === "string") {
121
154
  override.authMethodId = record.authMethodId.trim() || null;
122
155
  }
@@ -1,18 +1,19 @@
1
- import process from "node:process";
2
1
  import { getConversation } from "../conversations/store.mjs";
3
2
  import { getBuiltinToolRuntimeKey, mergeToolMcpServers, } from "../../tools/registry.mjs";
4
3
  import { buildAcpCapabilitySnapshot } from "./capabilities.mjs";
5
4
  import { applyAcpConfigOption, applyAcpThoughtLevel } from "./config-options.mjs";
6
5
  import { normalizeAdditionalDirectories, normalizeMcpServers, } from "./mcp-servers.mjs";
6
+ import { buildProviderCommandEnv, createProviderCommandNotFoundError, formatProviderCommand, isProviderCommandAvailable, isProviderCommandStartupError, resolveProviderCommand, } from "./provider-command.mjs";
7
7
  import { installAcpProviderCompatibility } from "./provider-compat.mjs";
8
8
  import { getAcpProviderDefaultModeId, normalizeAcpProvider, resolveAcpProviderConfig, } from "./provider-registry.mjs";
9
9
  import { normalizePermissionMode } from "./runtime-options.mjs";
10
10
  import { createAcpSessionKey, createInitialMetadata, getGlobalAcpSessionStore, normalizeThreadId, normalizeWorkspacePath, } from "./session-store.mjs";
11
11
  import { createACPProvider, } from "./vendor/acp-ai-provider/index.mjs";
12
- function buildProviderSettings({ command, args, authMethodId, sessionDelayMs, persistSession, workspacePath, mcpServers, additionalDirectories, existingSessionId, }) {
12
+ function buildProviderSettings({ command, args, env, authMethodId, sessionDelayMs, persistSession, workspacePath, mcpServers, additionalDirectories, existingSessionId, }) {
13
13
  const session = Object.assign({ cwd: workspacePath, mcpServers }, (additionalDirectories.length > 0 ? { additionalDirectories } : {}));
14
14
  return Object.assign(Object.assign(Object.assign(Object.assign({ command,
15
- args, env: Object.assign({}, process.env), initialize: {
15
+ args,
16
+ env, initialize: {
16
17
  clientCapabilities: {
17
18
  fs: {
18
19
  readTextFile: false,
@@ -45,7 +46,7 @@ async function prepareSessionCapabilities(entry, tools) {
45
46
  }
46
47
  }
47
48
  export async function getOrCreateSession(request, tools) {
48
- var _a, _b;
49
+ var _a, _b, _c;
49
50
  const store = getGlobalAcpSessionStore();
50
51
  store.cleanupIdle();
51
52
  const providerKey = normalizeAcpProvider(request.provider);
@@ -79,7 +80,11 @@ export async function getOrCreateSession(request, tools) {
79
80
  const resumableSessionId = (conversation === null || conversation === void 0 ? void 0 : conversation.provider) === providerKey
80
81
  ? ((_b = conversation.providerSessionId) !== null && _b !== void 0 ? _b : null)
81
82
  : null;
82
- const createEntry = (existingSessionId, warnings = []) => {
83
+ const providerCommandEnv = buildProviderCommandEnv();
84
+ const createEntry = (commandConfig, existingSessionId, warnings = []) => {
85
+ const resolvedCommand = resolveProviderCommand(commandConfig, {
86
+ env: providerCommandEnv,
87
+ });
83
88
  const normalizedMcp = normalizeMcpServers(request.mcpServers);
84
89
  const mergedMcpServers = mergeToolMcpServers(normalizedMcp.servers, {
85
90
  workspacePath,
@@ -93,8 +98,9 @@ export async function getOrCreateSession(request, tools) {
93
98
  context: request.context,
94
99
  });
95
100
  const provider = createACPProvider(buildProviderSettings({
96
- command: providerConfig.command,
97
- args: providerConfig.args,
101
+ command: resolvedCommand.command,
102
+ args: resolvedCommand.args,
103
+ env: providerCommandEnv,
98
104
  authMethodId: providerConfig.authMethodId,
99
105
  sessionDelayMs: providerConfig.sessionDelayMs,
100
106
  persistSession: providerConfig.persistSession,
@@ -122,23 +128,55 @@ export async function getOrCreateSession(request, tools) {
122
128
  promptHistoryStrategy: existingSessionId ? "session" : "full",
123
129
  };
124
130
  };
125
- const entry = createEntry(resumableSessionId);
126
- store.set(sessionKey, entry);
131
+ const commandCandidates = [
132
+ { command: providerConfig.command, args: providerConfig.args },
133
+ ...((_c = providerConfig.fallbackCommands) !== null && _c !== void 0 ? _c : []),
134
+ ];
135
+ const prepareEntryWithCommandCandidates = async (existingSessionId, warnings = []) => {
136
+ const commandWarnings = [];
137
+ let lastStartupError = null;
138
+ for (const [index, commandConfig] of commandCandidates.entries()) {
139
+ const hasFallback = index < commandCandidates.length - 1;
140
+ const commandLabel = formatProviderCommand(commandConfig);
141
+ if (!isProviderCommandAvailable(commandConfig, {
142
+ env: providerCommandEnv,
143
+ })) {
144
+ const error = createProviderCommandNotFoundError(commandConfig);
145
+ lastStartupError = error;
146
+ if (!hasFallback)
147
+ throw error;
148
+ commandWarnings.push(`ACP provider command unavailable (${commandLabel}); trying fallback.`);
149
+ continue;
150
+ }
151
+ const entry = createEntry(commandConfig, existingSessionId, [
152
+ ...warnings,
153
+ ...commandWarnings,
154
+ ]);
155
+ store.set(sessionKey, entry);
156
+ try {
157
+ await prepareSessionCapabilities(entry, tools);
158
+ return entry;
159
+ }
160
+ catch (error) {
161
+ store.cleanup(sessionKey);
162
+ lastStartupError = error;
163
+ if (!hasFallback || !isProviderCommandStartupError(error))
164
+ throw error;
165
+ commandWarnings.push(`ACP provider command failed (${commandLabel}); trying fallback.`);
166
+ }
167
+ }
168
+ throw (lastStartupError !== null && lastStartupError !== void 0 ? lastStartupError : createProviderCommandNotFoundError(commandCandidates[0]));
169
+ };
127
170
  try {
128
- await prepareSessionCapabilities(entry, tools);
129
- return entry;
171
+ return await prepareEntryWithCommandCandidates(resumableSessionId);
130
172
  }
131
173
  catch (error) {
132
- store.cleanup(sessionKey);
133
174
  if (!resumableSessionId)
134
175
  throw error;
135
176
  }
136
- const fallbackEntry = createEntry(null, [
177
+ return prepareEntryWithCommandCandidates(null, [
137
178
  `Failed to resume ACP session "${resumableSessionId}"; started a new session with UI message history instead.`,
138
179
  ]);
139
- store.set(sessionKey, fallbackEntry);
140
- await prepareSessionCapabilities(fallbackEntry, tools);
141
- return fallbackEntry;
142
180
  }
143
181
  export async function applyRequestedModelAndMode(entry) {
144
182
  const { model, modeId } = entry.metadata;
@@ -8,11 +8,14 @@ export type AcpBuiltinToolId = string;
8
8
  export type AcpBuiltinToolSettings = Record<string, unknown>;
9
9
  export type AcpProviderInstance = ReturnType<typeof createACPProvider>;
10
10
  export type AcpToolsInput = Parameters<AcpProviderInstance["initSession"]>[0];
11
- export type AcpProviderRegistryEntry = {
12
- provider: AcpProviderKey;
13
- label: string;
11
+ export type AcpProviderCommand = {
14
12
  command: string;
15
13
  args?: string[];
14
+ };
15
+ export type AcpProviderRegistryEntry = AcpProviderCommand & {
16
+ provider: AcpProviderKey;
17
+ label: string;
18
+ fallbackCommands?: AcpProviderCommand[];
16
19
  authMethodId?: string | null;
17
20
  sessionDelayMs?: number | null;
18
21
  persistSession?: boolean;
@@ -20,7 +23,7 @@ export type AcpProviderRegistryEntry = {
20
23
  defaultModeId?: string | null;
21
24
  supportsImages: boolean;
22
25
  };
23
- export type AcpProviderOverride = Partial<Pick<AcpProviderRegistryEntry, "command" | "args" | "authMethodId" | "sessionDelayMs" | "persistSession" | "defaultModel" | "defaultModeId">>;
26
+ export type AcpProviderOverride = Partial<Pick<AcpProviderRegistryEntry, "command" | "args" | "fallbackCommands" | "authMethodId" | "sessionDelayMs" | "persistSession" | "defaultModel" | "defaultModeId">>;
24
27
  export type AcpChatRequest = {
25
28
  id?: string;
26
29
  threadId?: string;
@@ -2,4 +2,4 @@ export { cancelAcpSession, cleanupAcpSession, getAcpSessionMetadata, listAcpSess
2
2
  export { buildAcpCapabilitySnapshot } from "../lib/acp2aisdk/capabilities";
3
3
  export { ACP_PROVIDER_ALIASES, ACP_PROVIDER_DEFAULT_MODE_IDS, ACP_PROVIDER_KEYS, ACP_PROVIDER_REGISTRY, getAcpProviderDefaultModeId, isAcpProviderKey, normalizeAcpProvider, readProviderOverridesFromEnv, resolveAcpProviderConfig, } from "../lib/acp2aisdk/provider-registry";
4
4
  export { createAcpSessionKey, createInitialMetadata, DEFAULT_PERMISSION_MODE, DEFAULT_THREAD_ID, getDefaultWorkspacePath, getGlobalAcpSessionStore, normalizeThreadId, normalizeWorkspacePath, SESSION_IDLE_TTL_MS, } from "../lib/acp2aisdk/session-store";
5
- export type { AcpCapability, AcpCapabilityOption, AcpCapabilitySnapshot, AcpChatRequest, AcpCleanupResult, AcpMcpServer, AcpPermissionMode, AcpProviderKey, AcpProviderRegistryEntry, AcpRunState, AcpRuntimeMetadata, AcpSessionLookupRequest, AcpStreamChatResult, } from "../lib/acp2aisdk/types";
5
+ export type { AcpCapability, AcpCapabilityOption, AcpCapabilitySnapshot, AcpChatRequest, AcpCleanupResult, AcpMcpServer, AcpPermissionMode, AcpProviderCommand, AcpProviderKey, AcpProviderRegistryEntry, AcpRunState, AcpRuntimeMetadata, AcpSessionLookupRequest, AcpStreamChatResult, } from "../lib/acp2aisdk/types";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axhub/acp",
3
- "version": "0.1.2-beta.0",
3
+ "version": "0.1.2-beta.2",
4
4
  "private": false,
5
5
  "engines": {
6
6
  "node": ">=20.9.0"
@@ -44,7 +44,7 @@
44
44
  ],
45
45
  "scripts": {
46
46
  "dev": "node scripts/dev-server.mjs",
47
- "dev:host": "node scripts/dev-server.mjs --hostname 0.0.0.0 --cors-origin http://localhost:53817,http://127.0.0.1:53817",
47
+ "dev:host": "node scripts/dev-server.mjs --hostname 0.0.0.0",
48
48
  "build": "next build",
49
49
  "build:public-api": "node scripts/build-public-api.mjs",
50
50
  "build:sanitize": "node scripts/sanitize-next-build.mjs",