@vedmalex/ai-connect 0.2.1 → 0.5.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.
@@ -220,11 +220,7 @@ function normalizeTransport(providerId, input) {
220
220
  ...selector.whereEquals !== void 0 ? { whereEquals: selector.whereEquals } : {}
221
221
  };
222
222
  };
223
- const parser = (() => {
224
- if (!descriptor.cli?.parser) {
225
- return void 0;
226
- }
227
- const cliParser = descriptor.cli.parser;
223
+ const normalizeCliParser = (cliParser) => {
228
224
  if (cliParser.kind === "json") {
229
225
  assert(
230
226
  cliParser.textPath.trim().length > 0,
@@ -237,13 +233,22 @@ function normalizeTransport(providerId, input) {
237
233
  ...cliParser.usagePath?.trim() ? { usagePath: cliParser.usagePath.trim() } : {}
238
234
  };
239
235
  }
236
+ if (cliParser.kind === "text") {
237
+ return {
238
+ kind: "text",
239
+ // Default trim=true, stripAnsi=false; only persist explicit overrides.
240
+ ...cliParser.trim === false ? { trim: false } : {},
241
+ ...cliParser.stripAnsi === true ? { stripAnsi: true } : {}
242
+ };
243
+ }
240
244
  return {
241
245
  kind: "jsonl",
242
246
  text: normalizeSelector(cliParser.text, "text"),
243
247
  ...cliParser.error ? { error: normalizeSelector(cliParser.error, "error") } : {},
244
248
  ...cliParser.usage ? { usage: normalizeSelector(cliParser.usage, "usage") } : {}
245
249
  };
246
- })();
250
+ };
251
+ const parser = descriptor.cli?.parser ? normalizeCliParser(descriptor.cli.parser) : void 0;
247
252
  const normalized = {
248
253
  ...descriptor.cli.preset ? { preset: descriptor.cli.preset } : {},
249
254
  ...descriptor.cli.argsTemplate ? {
@@ -255,11 +260,13 @@ function normalizeTransport(providerId, input) {
255
260
  if (!descriptor.cli?.discovery) {
256
261
  return void 0;
257
262
  }
258
- const via = descriptor.cli.discovery.via ?? "none";
259
- assert(
260
- via === "none" || via === "acp",
261
- `Unsupported CLI discovery mode "${String(via)}" for provider "${providerId}".`
262
- );
263
+ const explicitVia = descriptor.cli.discovery.via;
264
+ if (explicitVia !== void 0) {
265
+ assert(
266
+ explicitVia === "none" || explicitVia === "acp" || explicitVia === "command" || explicitVia === "static",
267
+ `Unsupported CLI discovery mode "${String(explicitVia)}" for provider "${providerId}".`
268
+ );
269
+ }
263
270
  const launch = descriptor.cli.discovery.acp?.launch ? (() => {
264
271
  const contextMode = descriptor.cli.discovery?.acp?.launch?.contextMode ?? "workspace";
265
272
  const skillsMode = descriptor.cli.discovery?.acp?.launch?.skillsMode ?? "default";
@@ -280,7 +287,7 @@ function normalizeTransport(providerId, input) {
280
287
  methodId: descriptor.cli.discovery.acp.auth.methodId.trim(),
281
288
  params: descriptor.cli.discovery.acp.auth.params ?? {}
282
289
  } : void 0;
283
- const acp = via === "acp" ? {
290
+ const acp = descriptor.cli.discovery.acp ? {
284
291
  ...descriptor.cli.discovery.acp?.providerId?.trim() ? { providerId: descriptor.cli.discovery.acp.providerId.trim() } : {},
285
292
  ...descriptor.cli.discovery.acp?.transportId?.trim() ? {
286
293
  transportId: descriptor.cli.discovery.acp.transportId.trim()
@@ -288,9 +295,55 @@ function normalizeTransport(providerId, input) {
288
295
  ...auth ? { auth } : {},
289
296
  ...launch ? { launch } : {}
290
297
  } : void 0;
298
+ const commandInput = descriptor.cli.discovery.command;
299
+ const command = commandInput ? (() => {
300
+ assert(
301
+ Array.isArray(commandInput.argsTemplate) && commandInput.argsTemplate.length > 0,
302
+ `CLI discovery command.argsTemplate must be a non-empty array for provider "${providerId}".`
303
+ );
304
+ const cmdParserInput = commandInput.parser ?? { kind: "text" };
305
+ const cmdParser = cmdParserInput.kind === "text" ? {
306
+ kind: "text",
307
+ ...cmdParserInput.trim === false ? { trim: false } : {},
308
+ ...cmdParserInput.stripAnsi === true ? { stripAnsi: true } : {}
309
+ } : { kind: cmdParserInput.kind };
310
+ if (cmdParser.kind !== "text") {
311
+ assert(
312
+ Boolean(commandInput.models?.idPath?.trim()),
313
+ `CLI discovery command.models.idPath is required for a ${cmdParser.kind} parser for provider "${providerId}".`
314
+ );
315
+ }
316
+ const m = commandInput.models;
317
+ const models = m?.idPath?.trim() ? {
318
+ ...m.path?.trim() ? { path: m.path.trim() } : {},
319
+ idPath: m.idPath.trim(),
320
+ ...m.namePath?.trim() ? { namePath: m.namePath.trim() } : {},
321
+ ...m.descriptionPath?.trim() ? { descriptionPath: m.descriptionPath.trim() } : {},
322
+ ...m.contextLengthPath?.trim() ? { contextLengthPath: m.contextLengthPath.trim() } : {}
323
+ } : void 0;
324
+ return {
325
+ ...commandInput.command?.trim() ? { command: commandInput.command.trim() } : {},
326
+ argsTemplate: commandInput.argsTemplate.map((part) => String(part)),
327
+ parser: cmdParser,
328
+ ...models ? { models } : {}
329
+ };
330
+ })() : void 0;
331
+ assert(
332
+ explicitVia !== "command" || command !== void 0,
333
+ `CLI discovery via:"command" requires a discovery.command block for provider "${providerId}".`
334
+ );
335
+ const fallback = descriptor.cli.discovery.fallback;
336
+ if (fallback !== void 0) {
337
+ assert(
338
+ fallback === "static" || fallback === "none",
339
+ `Unsupported CLI discovery fallback "${String(fallback)}" for provider "${providerId}".`
340
+ );
341
+ }
291
342
  return {
292
- via,
293
- ...acp ? { acp } : {}
343
+ ...explicitVia !== void 0 ? { via: explicitVia } : {},
344
+ ...acp ? { acp } : {},
345
+ ...command ? { command } : {},
346
+ ...fallback !== void 0 ? { fallback } : {}
294
347
  };
295
348
  })();
296
349
  return {
@@ -331,7 +384,7 @@ function normalizeTransport(providerId, input) {
331
384
  };
332
385
  }
333
386
  if (descriptor.kind === "cli") {
334
- const inferredId2 = providerId === "anthropic" ? "claude-cli" : providerId === "openclaude" ? "openclaude-cli" : providerId === "openai" ? "codex-cli" : providerId === "gemini" ? "gemini-cli" : providerId === "qwen" ? "qwen-cli" : "cli";
387
+ const inferredId2 = providerId === "anthropic" ? "claude-cli" : providerId === "openclaude" ? "openclaude-cli" : providerId === "openai" ? "codex-cli" : providerId === "pi" ? "pi-cli" : "cli";
335
388
  return {
336
389
  kind: "cli",
337
390
  id: descriptor.id?.trim() || inferredId2,
@@ -348,7 +401,7 @@ function normalizeTransport(providerId, input) {
348
401
  ...descriptor.baseUrl?.trim() ? { baseUrl: descriptor.baseUrl.trim() } : {}
349
402
  };
350
403
  }
351
- const inferredId = providerId === "anthropic" ? "claude-code-acp" : providerId === "openai" ? "codex-acp" : providerId === "gemini" ? "gemini-acp" : "acp";
404
+ const inferredId = providerId === "anthropic" ? "claude-code-acp" : providerId === "openai" ? "codex-acp" : "acp";
352
405
  const normalizedLaunch = descriptor.launch ? (() => {
353
406
  const contextMode = descriptor.launch?.contextMode ?? "workspace";
354
407
  const skillsMode = descriptor.launch?.skillsMode ?? "default";
@@ -1576,18 +1629,14 @@ var MODEL_REFERENCE = {
1576
1629
  key: "gemini-3.1-flash-lite",
1577
1630
  contextLength: 1048576
1578
1631
  },
1632
+ "gemini-3.1-pro": { key: "gemini-3.1-pro", contextLength: 1048576 },
1579
1633
  "gemini-3-pro": { key: "gemini-3-pro", contextLength: 2097152 },
1580
1634
  "auto-gemini-3": { key: "auto-gemini-3", contextLength: 1048576 },
1581
1635
  "gemini-2.5-flash": { key: "gemini-2.5-flash", contextLength: 1048576 },
1582
1636
  "gemini-2.5-pro": { key: "gemini-2.5-pro", contextLength: 2097152 },
1583
1637
  "gemini-2.0-flash": { key: "gemini-2.0-flash", contextLength: 1048576 },
1584
1638
  "gemini-1.5-flash": { key: "gemini-1.5-flash", contextLength: 1048576 },
1585
- "gemini-1.5-pro": { key: "gemini-1.5-pro", contextLength: 2097152 },
1586
- // --- Qwen (catalog: qwen3-coder-plus) ---
1587
- "qwen3-coder-plus": { key: "qwen3-coder-plus", contextLength: 1048576 },
1588
- "qwen3-coder": { key: "qwen3-coder", contextLength: 262144 },
1589
- "qwen-max": { key: "qwen-max", contextLength: 32768 },
1590
- "qwen-plus": { key: "qwen-plus", contextLength: 131072 }
1639
+ "gemini-1.5-pro": { key: "gemini-1.5-pro", contextLength: 2097152 }
1591
1640
  };
1592
1641
  function normalizeModelKey(model) {
1593
1642
  let key = model.trim().toLowerCase();
@@ -1721,6 +1770,25 @@ function resolveModelContextWindow(input) {
1721
1770
  const fallback = isUsableContextLength(input.defaultContextWindow) ? input.defaultContextWindow : DEFAULT_CONTEXT_WINDOW;
1722
1771
  return { contextWindow: fallback, source: "default" };
1723
1772
  }
1773
+ function fillConfiguredContextLength(route, models) {
1774
+ if (!isUsableContextLength(route.contextWindow)) {
1775
+ return models;
1776
+ }
1777
+ const configured = route.contextWindow;
1778
+ return models.map((entry) => {
1779
+ if (isUsableContextLength(entry.contextLength)) {
1780
+ return entry;
1781
+ }
1782
+ if (route.model !== void 0 && entry.modelId !== route.model) {
1783
+ return entry;
1784
+ }
1785
+ return {
1786
+ ...entry,
1787
+ contextLength: configured,
1788
+ metadata: { ...entry.metadata ?? {}, contextWindowSource: "configured" }
1789
+ };
1790
+ });
1791
+ }
1724
1792
  function modelContextCacheKey(input) {
1725
1793
  if (typeof input === "string") {
1726
1794
  return { key: `::${input}`, model: input };
@@ -4696,18 +4764,19 @@ function canonicalGeminiImageModelId(modelId) {
4696
4764
  }
4697
4765
  function buildModelCatalog(route, availableModels, currentModelId) {
4698
4766
  const requestedModelId = route.model;
4699
- const requestedModelAdvertised = availableModels.some(
4767
+ const filledModels = fillConfiguredContextLength(route, availableModels);
4768
+ const requestedModelAdvertised = filledModels.some(
4700
4769
  (model) => model.modelId === requestedModelId
4701
4770
  );
4702
4771
  const canonicalModelId = route.provider === "gemini" ? canonicalGeminiImageModelId(requestedModelId) : void 0;
4703
- const resolvedModelId = requestedModelAdvertised ? requestedModelId : canonicalModelId && availableModels.some((model) => model.modelId === canonicalModelId) ? canonicalModelId : void 0;
4772
+ const resolvedModelId = requestedModelAdvertised ? requestedModelId : canonicalModelId && filledModels.some((model) => model.modelId === canonicalModelId) ? canonicalModelId : void 0;
4704
4773
  return {
4705
4774
  requestedModelId,
4706
4775
  requestedModelAdvertised,
4707
4776
  ...canonicalModelId ? { canonicalModelId } : {},
4708
4777
  ...resolvedModelId ? { resolvedModelId } : {},
4709
4778
  ...currentModelId ? { currentModelId } : {},
4710
- availableModels
4779
+ availableModels: filledModels
4711
4780
  };
4712
4781
  }
4713
4782
  function parseOpenAiModelCatalog(route, payload) {
@@ -6247,8 +6316,6 @@ import { pathToFileURL } from "node:url";
6247
6316
  var AI_CONNECT_DEFAULT_ACP_COMMANDS = {
6248
6317
  "anthropic:claude-code-acp": "npx -y @agentclientprotocol/claude-agent-acp@^0.25.0",
6249
6318
  "openai:codex-acp": "npx @zed-industries/codex-acp@^0.11.1",
6250
- "gemini:gemini-acp": "gemini --acp",
6251
- "qwen:qwen-acp": "qwen --acp",
6252
6319
  "opencode:opencode-acp": "opencode acp"
6253
6320
  };
6254
6321
 
@@ -6329,15 +6396,6 @@ function resolveHomeDir(env) {
6329
6396
  function resolveXdgConfigHome(env) {
6330
6397
  return env.XDG_CONFIG_HOME ?? path.join(resolveHomeDir(env), ".config");
6331
6398
  }
6332
- function resolveGeminiCliHome(env) {
6333
- return env.GEMINI_CLI_HOME ?? resolveHomeDir(env);
6334
- }
6335
- function resolveGeminiDir(env) {
6336
- return path.join(resolveGeminiCliHome(env), ".gemini");
6337
- }
6338
- function resolveQwenDir(env) {
6339
- return path.join(resolveHomeDir(env), ".qwen");
6340
- }
6341
6399
  function resolveCodexHome(env) {
6342
6400
  return env.CODEX_HOME ?? path.join(resolveHomeDir(env), ".codex");
6343
6401
  }
@@ -6383,118 +6441,6 @@ async function removeIfExists(targetPath) {
6383
6441
  }
6384
6442
  });
6385
6443
  }
6386
- async function readJsonFile(filePath) {
6387
- try {
6388
- const raw = await fs.readFile(filePath, "utf8");
6389
- const parsed = JSON.parse(raw);
6390
- return isRecord2(parsed) ? parsed : void 0;
6391
- } catch (error) {
6392
- if (error instanceof SyntaxError || error.code === "ENOENT") {
6393
- return void 0;
6394
- }
6395
- throw error;
6396
- }
6397
- }
6398
- function mergeRecordValues(current, next) {
6399
- const merged = { ...current };
6400
- for (const [key, value] of Object.entries(next)) {
6401
- const existing = merged[key];
6402
- if (isRecord2(existing) && isRecord2(value)) {
6403
- merged[key] = mergeRecordValues(existing, value);
6404
- continue;
6405
- }
6406
- merged[key] = value;
6407
- }
6408
- return merged;
6409
- }
6410
- async function writeJsonFile(filePath, value) {
6411
- await fs.mkdir(path.dirname(filePath), { recursive: true });
6412
- await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}
6413
- `, "utf8");
6414
- }
6415
- async function prepareGeminiLaunchRuntime(base, launch) {
6416
- if (launch.contextMode === "workspace" && launch.skillsMode === "default") {
6417
- return {
6418
- ...base,
6419
- launch
6420
- };
6421
- }
6422
- const sandbox = await createSandboxPaths("ai-connect-gemini-acp-");
6423
- const originalGeminiDir = resolveGeminiDir(base.env);
6424
- const sandboxGeminiDir = path.join(sandbox.root, ".gemini");
6425
- const settingsPath = path.join(sandboxGeminiDir, "settings.json");
6426
- const systemSettingsPath = path.join(sandbox.config, "gemini-settings.json");
6427
- const systemDefaultsPath = path.join(
6428
- sandbox.config,
6429
- "gemini-system-defaults.json"
6430
- );
6431
- const isolatedCwd = launch.contextMode === "clean" || launch.skillsMode === "disabled" ? sandbox.root : base.cwd;
6432
- await Promise.all(
6433
- [
6434
- "oauth_creds.json",
6435
- "google_accounts.json",
6436
- "mcp-oauth-tokens.json",
6437
- "a2a-oauth-tokens.json",
6438
- "installation_id"
6439
- ].map(
6440
- (fileName) => maybeCopyFile(
6441
- path.join(originalGeminiDir, fileName),
6442
- path.join(sandboxGeminiDir, fileName)
6443
- )
6444
- )
6445
- );
6446
- const settings = mergeRecordValues(
6447
- await readJsonFile(settingsPath) ?? {},
6448
- {
6449
- context: {
6450
- fileName: "AI_CONNECT_CONTEXT_DISABLED.md",
6451
- includeDirectoryTree: false,
6452
- memoryBoundaryMarkers: [],
6453
- includeDirectories: [],
6454
- loadMemoryFromIncludeDirectories: false,
6455
- discoveryMaxDirs: 0
6456
- },
6457
- admin: {
6458
- mcp: {
6459
- enabled: false,
6460
- config: {},
6461
- requiredConfig: {}
6462
- },
6463
- ...launch.skillsMode === "disabled" ? {
6464
- skills: {
6465
- enabled: false
6466
- }
6467
- } : {}
6468
- },
6469
- ...launch.skillsMode === "disabled" ? {
6470
- skills: {
6471
- enabled: false,
6472
- disabled: []
6473
- }
6474
- } : {}
6475
- }
6476
- );
6477
- await Promise.all([
6478
- writeJsonFile(settingsPath, settings),
6479
- writeJsonFile(systemSettingsPath, {}),
6480
- writeJsonFile(systemDefaultsPath, {})
6481
- ]);
6482
- return {
6483
- commandLine: base.commandLine,
6484
- cwd: isolatedCwd,
6485
- env: {
6486
- ...base.env,
6487
- HOME: sandbox.root,
6488
- GEMINI_CLI_HOME: sandbox.root,
6489
- GEMINI_CLI_SYSTEM_SETTINGS_PATH: systemSettingsPath,
6490
- GEMINI_CLI_SYSTEM_DEFAULTS_PATH: systemDefaultsPath
6491
- },
6492
- launch,
6493
- cleanup: async () => {
6494
- await removeIfExists(sandbox.root);
6495
- }
6496
- };
6497
- }
6498
6444
  async function prepareOpenCodeLaunchRuntime(base, launch) {
6499
6445
  if (launch.contextMode === "workspace" && launch.skillsMode === "default") {
6500
6446
  return {
@@ -6543,70 +6489,6 @@ async function prepareOpenCodeLaunchRuntime(base, launch) {
6543
6489
  }
6544
6490
  };
6545
6491
  }
6546
- async function prepareQwenLaunchRuntime(base, launch) {
6547
- if (launch.contextMode === "workspace" && launch.skillsMode === "default") {
6548
- return {
6549
- ...base,
6550
- launch
6551
- };
6552
- }
6553
- const sandbox = await createSandboxPaths("ai-connect-qwen-acp-");
6554
- const originalQwenDir = resolveQwenDir(base.env);
6555
- const sandboxQwenDir = path.join(sandbox.home, ".qwen");
6556
- const settingsPath = path.join(sandboxQwenDir, "settings.json");
6557
- const systemSettingsPath = path.join(sandbox.config, "qwen-settings.json");
6558
- const systemDefaultsPath = path.join(
6559
- sandbox.config,
6560
- "qwen-system-defaults.json"
6561
- );
6562
- await Promise.all(
6563
- [
6564
- "oauth_creds.json",
6565
- "mcp-oauth-tokens.json",
6566
- "installation_id",
6567
- ".env",
6568
- "settings.json"
6569
- ].map(
6570
- (fileName) => maybeCopyFile(
6571
- path.join(originalQwenDir, fileName),
6572
- path.join(sandboxQwenDir, fileName)
6573
- )
6574
- )
6575
- );
6576
- const settings = mergeRecordValues(
6577
- await readJsonFile(settingsPath) ?? {},
6578
- {
6579
- context: {
6580
- fileName: ["AI_CONNECT_CONTEXT_DISABLED.md"],
6581
- includeDirectories: [],
6582
- loadFromIncludeDirectories: false
6583
- },
6584
- model: {
6585
- skipStartupContext: true
6586
- }
6587
- }
6588
- );
6589
- await Promise.all([
6590
- writeJsonFile(settingsPath, settings),
6591
- writeJsonFile(systemSettingsPath, {}),
6592
- writeJsonFile(systemDefaultsPath, {})
6593
- ]);
6594
- const isolatedCwd = launch.contextMode === "clean" || launch.skillsMode === "disabled" ? sandbox.root : base.cwd;
6595
- return {
6596
- commandLine: base.commandLine,
6597
- cwd: isolatedCwd,
6598
- env: {
6599
- ...base.env,
6600
- HOME: sandbox.home,
6601
- QWEN_CODE_SYSTEM_SETTINGS_PATH: systemSettingsPath,
6602
- QWEN_CODE_SYSTEM_DEFAULTS_PATH: systemDefaultsPath
6603
- },
6604
- launch,
6605
- cleanup: async () => {
6606
- await removeIfExists(sandbox.root);
6607
- }
6608
- };
6609
- }
6610
6492
  async function prepareCodexLaunchRuntime(base, launch) {
6611
6493
  if (launch.contextMode === "workspace" && launch.skillsMode === "default") {
6612
6494
  return {
@@ -6724,15 +6606,9 @@ async function prepareAcpLaunchRuntime(route, options, commandLine, cwdOverride)
6724
6606
  ...options?.env ?? {}
6725
6607
  }
6726
6608
  };
6727
- if (route.transport.id === "gemini-acp") {
6728
- return prepareGeminiLaunchRuntime(base, launch);
6729
- }
6730
6609
  if (route.transport.id === "opencode-acp") {
6731
6610
  return prepareOpenCodeLaunchRuntime(base, launch);
6732
6611
  }
6733
- if (route.transport.id === "qwen-acp") {
6734
- return prepareQwenLaunchRuntime(base, launch);
6735
- }
6736
6612
  if (route.transport.id === "codex-acp") {
6737
6613
  return prepareCodexLaunchRuntime(base, launch);
6738
6614
  }
@@ -7313,10 +7189,6 @@ function buildAcpLifecycle(route, authRequest, mode) {
7313
7189
  steps.push("authenticate");
7314
7190
  }
7315
7191
  steps.push("session/new");
7316
- if (mode === "prompt" && route.transport.id === "gemini-acp") {
7317
- steps.push("session/set_model");
7318
- keys.push("session/set_model.modelId");
7319
- }
7320
7192
  if (mode === "prompt") {
7321
7193
  steps.push("session/prompt");
7322
7194
  }
@@ -7452,16 +7324,6 @@ var AcpConnection = class {
7452
7324
  );
7453
7325
  }
7454
7326
  const sessionId = session.sessionId;
7455
- if (context.route.transport.id === "gemini-acp") {
7456
- await measurePhase(
7457
- transport.phases ?? [],
7458
- "session/set_model",
7459
- async () => this.request("session/set_model", {
7460
- sessionId,
7461
- modelId: context.route.model
7462
- })
7463
- );
7464
- }
7465
7327
  this.activePrompt = {
7466
7328
  sessionId,
7467
7329
  text: "",
@@ -7553,6 +7415,10 @@ var AcpConnection = class {
7553
7415
  `ACP route "${context.route.id}" did not return an ACP model catalog in session/new.`
7554
7416
  );
7555
7417
  }
7418
+ catalog.availableModels = fillConfiguredContextLength(
7419
+ context.route,
7420
+ catalog.availableModels
7421
+ );
7556
7422
  return catalog;
7557
7423
  } finally {
7558
7424
  this.bumpIdleTimer();
@@ -7970,9 +7836,6 @@ var AcpConnection = class {
7970
7836
  });
7971
7837
  }
7972
7838
  };
7973
- function isGeminiAcpFallbackCandidate(route, commandLine) {
7974
- return route.transport.id === "gemini-acp" && commandLine.includes("--acp") && !commandLine.includes("--experimental-acp");
7975
- }
7976
7839
  function cacheKeyForConnection(route, runtime, options) {
7977
7840
  const envEntries = Object.entries(options.env).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("");
7978
7841
  return [
@@ -8219,18 +8082,7 @@ function createAcpTransportManager(options) {
8219
8082
  }
8220
8083
  async function drivePrompt(context, onDelta) {
8221
8084
  const commandLine = resolveAcpCommand(context.route, options);
8222
- try {
8223
- return await runWithCommand(context, commandLine, onDelta);
8224
- } catch (error) {
8225
- if (!isGeminiAcpFallbackCandidate(context.route, commandLine)) {
8226
- throw error;
8227
- }
8228
- return await runWithCommand(
8229
- context,
8230
- commandLine.replace("--acp", "--experimental-acp"),
8231
- onDelta
8232
- );
8233
- }
8085
+ return await runWithCommand(context, commandLine, onDelta);
8234
8086
  }
8235
8087
  return {
8236
8088
  async runPrompt(context) {
@@ -8299,17 +8151,7 @@ function createAcpTransportManager(options) {
8299
8151
  },
8300
8152
  async discoverModels(context) {
8301
8153
  const commandLine = resolveAcpCommand(context.route, options);
8302
- try {
8303
- return await discoverWithCommand(context, commandLine);
8304
- } catch (error) {
8305
- if (!isGeminiAcpFallbackCandidate(context.route, commandLine)) {
8306
- throw error;
8307
- }
8308
- return await discoverWithCommand(
8309
- context,
8310
- commandLine.replace("--acp", "--experimental-acp")
8311
- );
8312
- }
8154
+ return await discoverWithCommand(context, commandLine);
8313
8155
  },
8314
8156
  async dispose() {
8315
8157
  const values = [...connectionPools.values()].flat();
@@ -8336,47 +8178,21 @@ import path2 from "node:path";
8336
8178
 
8337
8179
  // src/cli-presets.ts
8338
8180
  var AI_CONNECT_DEFAULT_CLI_PRESETS = {
8339
- gemini: {
8340
- id: "gemini",
8341
- label: "Gemini CLI",
8342
- command: "gemini",
8343
- transportId: "gemini-cli",
8344
- options: {
8345
- preset: "gemini",
8346
- argsTemplate: ["-p", "{prompt}", "--output-format", "json", "--model", "{model}"],
8347
- discovery: {
8348
- via: "acp",
8349
- acp: {
8350
- transportId: "gemini-acp"
8351
- }
8352
- },
8353
- parser: {
8354
- kind: "json",
8355
- textPath: "response",
8356
- usagePath: "stats",
8357
- errorPath: "error"
8358
- }
8359
- }
8360
- },
8361
- qwen: {
8362
- id: "qwen",
8363
- label: "Qwen CLI",
8364
- command: "qwen",
8365
- transportId: "qwen-cli",
8181
+ pi: {
8182
+ id: "pi",
8183
+ label: "pi (coding agent)",
8184
+ command: "pi",
8185
+ transportId: "pi-cli",
8366
8186
  options: {
8367
- preset: "qwen",
8368
- argsTemplate: ["-p", "{prompt}", "--output-format", "json", "--model", "{model}"],
8187
+ preset: "pi",
8188
+ // pi print mode emits the answer as plain text on stdout (no JSON wrapper),
8189
+ // and has no ACP mode — so it uses the raw `text` parser and no discovery sidecar.
8190
+ argsTemplate: ["--print", "--model", "{model}", "{prompt}"],
8369
8191
  discovery: {
8370
- via: "acp",
8371
- acp: {
8372
- transportId: "qwen-acp"
8373
- }
8192
+ via: "none"
8374
8193
  },
8375
8194
  parser: {
8376
- kind: "json",
8377
- textPath: "__preset__:qwen.result",
8378
- usagePath: "__preset__:qwen.stats",
8379
- errorPath: "__preset__:qwen.error"
8195
+ kind: "text"
8380
8196
  }
8381
8197
  }
8382
8198
  },
@@ -8460,8 +8276,7 @@ var AI_CONNECT_DEFAULT_CLI_COMMANDS = {
8460
8276
  "anthropic:openclaude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.openclaude.command,
8461
8277
  "openclaude:openclaude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.openclaude.command,
8462
8278
  "openai:codex-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.codex.command,
8463
- "gemini:gemini-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.gemini.command,
8464
- "qwen:qwen-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.qwen.command
8279
+ "pi:pi-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.pi.command
8465
8280
  };
8466
8281
  function getCliTransportPreset(presetId) {
8467
8282
  return AI_CONNECT_DEFAULT_CLI_PRESETS[presetId];
@@ -8469,14 +8284,6 @@ function getCliTransportPreset(presetId) {
8469
8284
 
8470
8285
  // src/cli.ts
8471
8286
  var CLI_PRESET_ACP_DISCOVERY_DEFAULTS = {
8472
- gemini: {
8473
- transportId: "gemini-acp",
8474
- providerId: "gemini"
8475
- },
8476
- qwen: {
8477
- transportId: "qwen-acp",
8478
- providerId: "qwen"
8479
- },
8480
8287
  claude: {
8481
8288
  transportId: "claude-code-acp",
8482
8289
  providerId: "anthropic"
@@ -8583,10 +8390,8 @@ function buildCliCwd(context, options) {
8583
8390
  }
8584
8391
  function defaultCliPresetIdForRoute(route) {
8585
8392
  switch (route.transport.id) {
8586
- case "gemini-cli":
8587
- return "gemini";
8588
- case "qwen-cli":
8589
- return "qwen";
8393
+ case "pi-cli":
8394
+ return "pi";
8590
8395
  case "claude-cli":
8591
8396
  return "claude";
8592
8397
  case "openclaude-cli":
@@ -8629,14 +8434,26 @@ function normalizeCliDiscoveryAcpSource(route, discovery) {
8629
8434
  };
8630
8435
  }
8631
8436
  function resolveCliDiscoverySource(route) {
8632
- const via = route.transport.cli?.discovery?.via ?? (defaultCliDiscoveryTransportIdForRoute(route) ? "acp" : "none");
8633
- if (via === "none") {
8634
- return { via: "none" };
8437
+ const discovery = route.transport.cli?.discovery;
8438
+ const hasModels = (route.advertisedModels?.length ?? 0) > 0;
8439
+ const hasAcpDefault = Boolean(defaultCliDiscoveryTransportIdForRoute(route));
8440
+ const via = discovery?.via ?? (discovery?.command ? "command" : hasAcpDefault ? "acp" : hasModels ? "static" : "none");
8441
+ switch (via) {
8442
+ case "none":
8443
+ return { via: "none" };
8444
+ case "static":
8445
+ return { via: "static" };
8446
+ case "command": {
8447
+ const command = discovery?.command;
8448
+ if (!command) {
8449
+ return { via: "none" };
8450
+ }
8451
+ const fallback = discovery?.fallback ?? (hasModels ? "static" : "none");
8452
+ return { via: "command", command, fallback };
8453
+ }
8454
+ default:
8455
+ return normalizeCliDiscoveryAcpSource(route, discovery?.acp);
8635
8456
  }
8636
- return normalizeCliDiscoveryAcpSource(
8637
- route,
8638
- route.transport.cli?.discovery?.acp
8639
- );
8640
8457
  }
8641
8458
  function createCliDiscoveryAcpRoute(route) {
8642
8459
  const discovery = resolveCliDiscoverySource(route);
@@ -8745,6 +8562,42 @@ async function buildCliInvocation(context, options) {
8745
8562
  parameterKeys
8746
8563
  };
8747
8564
  }
8565
+ function buildCliDiscoveryInvocation(route, command, options) {
8566
+ const commandLine = command.command?.trim() ? command.command : resolveCliCommand(route, options);
8567
+ const resolved = splitCommandLine2(commandLine);
8568
+ const parameterKeys = [];
8569
+ const args = [
8570
+ ...resolved.args,
8571
+ ...command.argsTemplate.map((part) => {
8572
+ if (part === "{model}") {
8573
+ parameterKeys.push("model");
8574
+ return route.model;
8575
+ }
8576
+ if (part.startsWith("--")) {
8577
+ parameterKeys.push(part);
8578
+ }
8579
+ return part;
8580
+ })
8581
+ ];
8582
+ return {
8583
+ command: resolved.command,
8584
+ args,
8585
+ cwd: path2.resolve(options?.cwd ?? process.cwd()),
8586
+ env: buildCliEnvironment(options),
8587
+ parameterKeys
8588
+ };
8589
+ }
8590
+ function buildStaticCliCatalog(route) {
8591
+ const models = route.advertisedModels.map((id) => ({
8592
+ modelId: id,
8593
+ name: id
8594
+ }));
8595
+ return buildModelCatalog(
8596
+ route,
8597
+ models,
8598
+ currentModelIdForRoute(route, route.advertisedModels)
8599
+ );
8600
+ }
8748
8601
  function statsToUsage(stats) {
8749
8602
  if (!stats || typeof stats !== "object") {
8750
8603
  return void 0;
@@ -8782,6 +8635,9 @@ function getValueByPath(value, dotPath) {
8782
8635
  }
8783
8636
  return current;
8784
8637
  }
8638
+ function splitJsonlLines(stdout) {
8639
+ return stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
8640
+ }
8785
8641
  function normalizeErrorMessage(value) {
8786
8642
  if (typeof value === "string" && value.trim()) {
8787
8643
  return value.trim();
@@ -8829,7 +8685,7 @@ function parseGenericJsonCli(stdout, parser) {
8829
8685
  };
8830
8686
  }
8831
8687
  function parseGenericJsonlCli(stdout, parser) {
8832
- const entries = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
8688
+ const entries = splitJsonlLines(stdout);
8833
8689
  const errorValue = parser.error ? findJsonlSelection(entries, parser.error) : void 0;
8834
8690
  const errorMessage = normalizeErrorMessage(errorValue);
8835
8691
  if (errorMessage) {
@@ -8850,59 +8706,69 @@ function parseGenericJsonlCli(stdout, parser) {
8850
8706
  data: entries
8851
8707
  };
8852
8708
  }
8853
- function parseGeminiCli(stdout) {
8854
- const payload = JSON.parse(stdout);
8855
- if (typeof payload.error === "string") {
8856
- throw new AiConnectError("temporary_unavailable", payload.error);
8709
+ var ANSI_ESCAPE_PATTERN = /\[[0-?]*[ -/]*[@-~]/g;
8710
+ function parseTextCli(stdout, parser) {
8711
+ let text = stdout;
8712
+ if (parser.stripAnsi) {
8713
+ text = text.replace(ANSI_ESCAPE_PATTERN, "");
8714
+ }
8715
+ if (parser.trim !== false) {
8716
+ text = text.trim();
8857
8717
  }
8858
- if (typeof payload.response !== "string") {
8718
+ if (!text) {
8859
8719
  throw new AiConnectError(
8860
8720
  "temporary_unavailable",
8861
- "Gemini CLI JSON output did not include response text."
8721
+ "CLI text parser produced no output."
8862
8722
  );
8863
8723
  }
8864
- const usage = payload.stats ? statsToUsage(payload.stats) : void 0;
8724
+ return { text, data: stdout };
8725
+ }
8726
+ function modelInfoFromRecord(record, selector) {
8727
+ const rawId = getValueByPath(record, selector.idPath);
8728
+ const modelId = typeof rawId === "string" ? rawId.trim() : "";
8729
+ if (!modelId) {
8730
+ return void 0;
8731
+ }
8732
+ const rawName = selector.namePath ? getValueByPath(record, selector.namePath) : void 0;
8733
+ const name = typeof rawName === "string" && rawName.trim().length > 0 ? rawName : modelId;
8734
+ const rawDescription = selector.descriptionPath ? getValueByPath(record, selector.descriptionPath) : void 0;
8735
+ const description = typeof rawDescription === "string" && rawDescription.trim().length > 0 ? rawDescription : void 0;
8736
+ const rawContext = selector.contextLengthPath ? getValueByPath(record, selector.contextLengthPath) : void 0;
8737
+ const contextLength = typeof rawContext === "number" && Number.isFinite(rawContext) && rawContext > 0 ? Math.floor(rawContext) : void 0;
8865
8738
  return {
8866
- text: payload.response,
8867
- ...usage ? { usage } : {},
8868
- data: payload
8739
+ modelId,
8740
+ name,
8741
+ ...description ? { description } : {},
8742
+ ...contextLength !== void 0 ? { contextLength } : {}
8869
8743
  };
8870
8744
  }
8871
- function parseQwenCli(stdout) {
8872
- const payload = JSON.parse(stdout);
8873
- if (!Array.isArray(payload)) {
8874
- throw new AiConnectError(
8875
- "temporary_unavailable",
8876
- "Qwen CLI JSON output did not return a message array."
8877
- );
8878
- }
8879
- const resultMessage = payload.find(
8880
- (entry) => entry && typeof entry === "object" && entry.type === "result"
8881
- );
8882
- if (!resultMessage) {
8883
- throw new AiConnectError(
8884
- "temporary_unavailable",
8885
- "Qwen CLI JSON output did not contain a result message."
8886
- );
8745
+ function parseCliModelList(stdout, parser, selector) {
8746
+ if (parser.kind === "text") {
8747
+ let text = stdout;
8748
+ if (parser.stripAnsi) {
8749
+ text = text.replace(ANSI_ESCAPE_PATTERN, "");
8750
+ }
8751
+ return text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => ({ modelId: line, name: line }));
8887
8752
  }
8888
- if (resultMessage.is_error === true) {
8753
+ if (!selector) {
8889
8754
  throw new AiConnectError(
8890
- "temporary_unavailable",
8891
- typeof resultMessage.result === "string" ? resultMessage.result : "Qwen CLI returned an error result."
8755
+ "validation_error",
8756
+ `CLI ${parser.kind} discovery parser requires a models selector with idPath.`
8892
8757
  );
8893
8758
  }
8894
- if (typeof resultMessage.result !== "string") {
8895
- throw new AiConnectError(
8896
- "temporary_unavailable",
8897
- "Qwen CLI result message did not contain text output."
8898
- );
8759
+ const records = parser.kind === "jsonl" ? splitJsonlLines(stdout) : (() => {
8760
+ const payload = JSON.parse(stdout);
8761
+ const arr = selector.path ? getValueByPath(payload, selector.path) : payload;
8762
+ return Array.isArray(arr) ? arr : [];
8763
+ })();
8764
+ const models = [];
8765
+ for (const record of records) {
8766
+ const info = modelInfoFromRecord(record, selector);
8767
+ if (info) {
8768
+ models.push(info);
8769
+ }
8899
8770
  }
8900
- const usage = resultMessage.stats ? statsToUsage(resultMessage.stats) : void 0;
8901
- return {
8902
- text: resultMessage.result,
8903
- ...usage ? { usage } : {},
8904
- data: payload
8905
- };
8771
+ return models;
8906
8772
  }
8907
8773
  function parseClaudeCli(stdout) {
8908
8774
  const payload = JSON.parse(stdout);
@@ -8921,8 +8787,7 @@ function parseClaudeCli(stdout) {
8921
8787
  };
8922
8788
  }
8923
8789
  function parseCodexCli(stdout, outputFileContent) {
8924
- const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
8925
- const events = lines.map((line) => JSON.parse(line));
8790
+ const events = splitJsonlLines(stdout);
8926
8791
  let text = outputFileContent?.trim() || void 0;
8927
8792
  let usage;
8928
8793
  for (const event of events) {
@@ -8980,13 +8845,18 @@ function parseCliResult(route, result, outputFileContent) {
8980
8845
  `CLI route "${route.id}" declared a parser override but normalization did not preserve it.`
8981
8846
  );
8982
8847
  }
8983
- return parser.kind === "json" ? parseGenericJsonCli(stdout, parser) : parseGenericJsonlCli(stdout, parser);
8848
+ switch (parser.kind) {
8849
+ case "text":
8850
+ return parseTextCli(result.stdout, parser);
8851
+ case "json":
8852
+ return parseGenericJsonCli(stdout, parser);
8853
+ default:
8854
+ return parseGenericJsonlCli(stdout, parser);
8855
+ }
8984
8856
  }
8985
8857
  switch (cliOptions.preset) {
8986
- case "gemini":
8987
- return parseGeminiCli(stdout);
8988
- case "qwen":
8989
- return parseQwenCli(stdout);
8858
+ case "pi":
8859
+ return parseTextCli(stdout, { kind: "text" });
8990
8860
  case "claude":
8991
8861
  case "openclaude":
8992
8862
  return parseClaudeCli(stdout);
@@ -9083,6 +8953,88 @@ async function cleanupCliInvocation(invocation) {
9083
8953
  }
9084
8954
  function createCliTransportManager(options) {
9085
8955
  return {
8956
+ async discoverModels(context) {
8957
+ const source = resolveCliDiscoverySource(context.route);
8958
+ if (source.via === "none") {
8959
+ throw new AiConnectError(
8960
+ "not_supported",
8961
+ `CLI transport "${context.route.transport.id}" does not support model discovery (no list command, no ACP sidecar, and no configured models[]).`
8962
+ );
8963
+ }
8964
+ if (source.via === "acp") {
8965
+ throw new AiConnectError(
8966
+ "not_supported",
8967
+ `CLI route "${context.route.id}" resolves discovery via acp; dispatch to the acp discovery route instead.`
8968
+ );
8969
+ }
8970
+ if (source.via === "static") {
8971
+ return buildStaticCliCatalog(context.route);
8972
+ }
8973
+ const phases = [];
8974
+ const invocation = buildCliDiscoveryInvocation(
8975
+ context.route,
8976
+ source.command,
8977
+ options
8978
+ );
8979
+ context.telemetry?.captureTransport({
8980
+ protocol: "cli",
8981
+ endpoint: invocation.command,
8982
+ method: "process",
8983
+ bodyKeys: ["argv"],
8984
+ parameterKeys: invocation.parameterKeys,
8985
+ phases,
8986
+ stream: false
8987
+ });
8988
+ try {
8989
+ let models;
8990
+ try {
8991
+ const execution = await executeCliInvocation(
8992
+ invocation,
8993
+ options?.timeoutMs ?? 6e4,
8994
+ phases,
8995
+ context.abort.signal
8996
+ );
8997
+ if (execution.exitCode !== 0 || !execution.stdout.trim()) {
8998
+ throw new AiConnectError(
8999
+ "temporary_unavailable",
9000
+ execution.stderr.trim() || `CLI discovery command for "${context.route.transport.id}" exited with code ${execution.exitCode ?? "null"}.`
9001
+ );
9002
+ }
9003
+ models = parseCliModelList(
9004
+ execution.stdout,
9005
+ source.command.parser,
9006
+ source.command.models
9007
+ );
9008
+ } catch (error) {
9009
+ if (error instanceof AiConnectError && error.code === "aborted") {
9010
+ throw error;
9011
+ }
9012
+ if (source.fallback === "static") {
9013
+ return buildStaticCliCatalog(context.route);
9014
+ }
9015
+ throw error;
9016
+ }
9017
+ if (models.length === 0) {
9018
+ if (source.fallback === "static") {
9019
+ return buildStaticCliCatalog(context.route);
9020
+ }
9021
+ throw new AiConnectError(
9022
+ "temporary_unavailable",
9023
+ `CLI discovery command for "${context.route.transport.id}" returned no models.`
9024
+ );
9025
+ }
9026
+ return buildModelCatalog(
9027
+ context.route,
9028
+ models,
9029
+ currentModelIdForRoute(
9030
+ context.route,
9031
+ models.map((model) => model.modelId)
9032
+ )
9033
+ );
9034
+ } finally {
9035
+ await cleanupCliInvocation(invocation);
9036
+ }
9037
+ },
9086
9038
  async runPrompt(context) {
9087
9039
  const invocation = await buildCliInvocation(context, options);
9088
9040
  const phases = [];
@@ -9617,17 +9569,21 @@ function createLocalRouteHandlers(options = {}) {
9617
9569
  return cliTransport.runPrompt(context);
9618
9570
  },
9619
9571
  async discoverModels(context) {
9620
- const discoveryRoute = createCliDiscoveryAcpRoute(context.route);
9621
- if (!discoveryRoute) {
9622
- throw new AiConnectError(
9623
- "not_supported",
9624
- `CLI route "${context.route.id}" does not define a model discovery backend.`
9625
- );
9572
+ const source = resolveCliDiscoverySource(context.route);
9573
+ if (source.via === "acp") {
9574
+ const discoveryRoute = createCliDiscoveryAcpRoute(context.route);
9575
+ if (!discoveryRoute) {
9576
+ throw new AiConnectError(
9577
+ "not_supported",
9578
+ `CLI route "${context.route.id}" does not define a model discovery backend.`
9579
+ );
9580
+ }
9581
+ return acpTransport.discoverModels({
9582
+ ...context,
9583
+ route: discoveryRoute
9584
+ });
9626
9585
  }
9627
- return acpTransport.discoverModels({
9628
- ...context,
9629
- route: discoveryRoute
9630
- });
9586
+ return cliTransport.discoverModels(context);
9631
9587
  },
9632
9588
  async verify({ route, runtime }) {
9633
9589
  try {