@vedmalex/ai-connect 0.4.0 → 0.6.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.
@@ -2,6 +2,128 @@
2
2
  import { accessSync, constants } from "node:fs";
3
3
  import { delimiter, join } from "node:path";
4
4
 
5
+ // src/cli-presets.ts
6
+ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
7
+ pi: {
8
+ id: "pi",
9
+ label: "pi (coding agent)",
10
+ command: "pi",
11
+ transportId: "pi-cli",
12
+ options: {
13
+ preset: "pi",
14
+ // pi print mode emits the answer as plain text on stdout (no JSON wrapper),
15
+ // and has no ACP mode — so it uses the raw `text` parser and no discovery sidecar.
16
+ // pi takes file/image attachments as separate `@<path>` argv tokens (REQ-024);
17
+ // {files} expands one `@<staged-path>` token per attachment. pi stages all
18
+ // categories (PDFs as-is, no extraction guarantee).
19
+ argsTemplate: ["--print", "--model", "{model}", "{files}", "{prompt}"],
20
+ discovery: {
21
+ via: "none"
22
+ },
23
+ parser: {
24
+ kind: "text"
25
+ },
26
+ fileInput: {
27
+ placement: "args",
28
+ perFileArgs: ["@{path}"]
29
+ }
30
+ }
31
+ },
32
+ claude: {
33
+ id: "claude",
34
+ label: "Claude CLI",
35
+ command: "claude",
36
+ transportId: "claude-cli",
37
+ options: {
38
+ preset: "claude",
39
+ argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "{prompt}"],
40
+ discovery: {
41
+ via: "acp",
42
+ acp: {
43
+ transportId: "claude-code-acp"
44
+ }
45
+ },
46
+ parser: {
47
+ kind: "json",
48
+ textPath: "__preset__:claude.result",
49
+ usagePath: "usage",
50
+ errorPath: "__preset__:claude.error"
51
+ }
52
+ }
53
+ },
54
+ openclaude: {
55
+ id: "openclaude",
56
+ label: "OpenClaude CLI",
57
+ command: "openclaude",
58
+ transportId: "openclaude-cli",
59
+ options: {
60
+ preset: "openclaude",
61
+ argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "{prompt}"],
62
+ parser: {
63
+ kind: "json",
64
+ textPath: "__preset__:claude.result",
65
+ usagePath: "usage",
66
+ errorPath: "__preset__:claude.error"
67
+ }
68
+ }
69
+ },
70
+ codex: {
71
+ id: "codex",
72
+ label: "Codex CLI",
73
+ command: "codex",
74
+ transportId: "codex-cli",
75
+ options: {
76
+ preset: "codex",
77
+ // codex exec takes image attachments via `-i/--image <FILE>` flags
78
+ // (REQ-024); {files} expands a `--image <staged-path>` pair per image.
79
+ // Images only — a non-image attachment cleanly rejects pre-spawn.
80
+ argsTemplate: [
81
+ "exec",
82
+ "--json",
83
+ "--output-last-message",
84
+ "{output_file}",
85
+ "--model",
86
+ "{model}",
87
+ "{files}",
88
+ "{prompt}"
89
+ ],
90
+ discovery: {
91
+ via: "acp",
92
+ acp: {
93
+ transportId: "codex-acp"
94
+ }
95
+ },
96
+ fileInput: {
97
+ placement: "args",
98
+ perFileArgs: ["--image", "{path}"],
99
+ categories: ["image"]
100
+ },
101
+ parser: {
102
+ kind: "jsonl",
103
+ text: {
104
+ path: "__preset__:codex.text"
105
+ },
106
+ usage: {
107
+ path: "__preset__:codex.usage"
108
+ },
109
+ error: {
110
+ path: "__preset__:codex.error"
111
+ }
112
+ }
113
+ }
114
+ }
115
+ };
116
+ var AI_CONNECT_DEFAULT_CLI_COMMANDS = {
117
+ "anthropic:claude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.claude.command,
118
+ "anthropic:openclaude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.openclaude.command,
119
+ "openclaude:openclaude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.openclaude.command,
120
+ "openai:codex-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.codex.command,
121
+ "pi:pi-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.pi.command
122
+ };
123
+ function getCliTransportPreset(presetId) {
124
+ return AI_CONNECT_DEFAULT_CLI_PRESETS[presetId];
125
+ }
126
+
5
127
  // src/errors.ts
6
128
  var AiConnectError = class extends Error {
7
129
  code;
@@ -346,9 +468,38 @@ function normalizeTransport(providerId, input) {
346
468
  ...fallback !== void 0 ? { fallback } : {}
347
469
  };
348
470
  })();
471
+ const fileInput = (() => {
472
+ const fi = descriptor.cli?.fileInput;
473
+ if (!fi) {
474
+ return void 0;
475
+ }
476
+ const validCategories = ["image", "document", "text", "other"];
477
+ if (fi.categories) {
478
+ for (const c of fi.categories) {
479
+ assert(
480
+ validCategories.includes(c),
481
+ `Unsupported CLI fileInput category "${String(c)}" for provider "${providerId}".`
482
+ );
483
+ }
484
+ }
485
+ const placement = fi.placement ?? "args";
486
+ assert(
487
+ placement === "args" || placement === "prompt",
488
+ `Unsupported CLI fileInput placement "${String(placement)}" for provider "${providerId}".`
489
+ );
490
+ return {
491
+ placement,
492
+ ...fi.perFileArgs ? { perFileArgs: fi.perFileArgs.map((part) => String(part)) } : {},
493
+ ...fi.mentionTemplate?.trim() ? { mentionTemplate: fi.mentionTemplate.trim() } : {},
494
+ ...fi.separator !== void 0 ? { separator: fi.separator } : {},
495
+ ...fi.categories ? { categories: [...fi.categories] } : {},
496
+ ...fi.stagingDir?.trim() ? { stagingDir: fi.stagingDir.trim() } : {}
497
+ };
498
+ })();
349
499
  return {
350
500
  ...normalized,
351
- ...discovery ? { discovery } : {}
501
+ ...discovery ? { discovery } : {},
502
+ ...fileInput ? { fileInput } : {}
352
503
  };
353
504
  })();
354
505
  const normalizedAuth = descriptor.auth?.methodId?.trim() ? {
@@ -384,7 +535,7 @@ function normalizeTransport(providerId, input) {
384
535
  };
385
536
  }
386
537
  if (descriptor.kind === "cli") {
387
- const inferredId2 = providerId === "anthropic" ? "claude-cli" : providerId === "openclaude" ? "openclaude-cli" : providerId === "openai" ? "codex-cli" : providerId === "gemini" ? "gemini-cli" : providerId === "qwen" ? "qwen-cli" : "cli";
538
+ const inferredId2 = providerId === "anthropic" ? "claude-cli" : providerId === "openclaude" ? "openclaude-cli" : providerId === "openai" ? "codex-cli" : providerId === "pi" ? "pi-cli" : "cli";
388
539
  return {
389
540
  kind: "cli",
390
541
  id: descriptor.id?.trim() || inferredId2,
@@ -401,7 +552,7 @@ function normalizeTransport(providerId, input) {
401
552
  ...descriptor.baseUrl?.trim() ? { baseUrl: descriptor.baseUrl.trim() } : {}
402
553
  };
403
554
  }
404
- const inferredId = providerId === "anthropic" ? "claude-code-acp" : providerId === "openai" ? "codex-acp" : providerId === "gemini" ? "gemini-acp" : "acp";
555
+ const inferredId = providerId === "anthropic" ? "claude-code-acp" : providerId === "openai" ? "codex-acp" : "acp";
405
556
  const normalizedLaunch = descriptor.launch ? (() => {
406
557
  const contextMode = descriptor.launch?.contextMode ?? "workspace";
407
558
  const skillsMode = descriptor.launch?.skillsMode ?? "default";
@@ -436,7 +587,40 @@ function normalizeRuntime(transport, runtime) {
436
587
  }
437
588
  return "universal";
438
589
  }
590
+ function cliPresetIdForTransportId(transportId) {
591
+ switch (transportId) {
592
+ case "pi-cli":
593
+ return "pi";
594
+ case "claude-cli":
595
+ return "claude";
596
+ case "openclaude-cli":
597
+ return "openclaude";
598
+ case "codex-cli":
599
+ return "codex";
600
+ default:
601
+ return void 0;
602
+ }
603
+ }
604
+ function cliFileInputCategories(transport) {
605
+ if (transport.kind !== "cli") {
606
+ return /* @__PURE__ */ new Set();
607
+ }
608
+ const presetId = transport.cli?.preset ?? cliPresetIdForTransportId(transport.id);
609
+ const presetFileInput = presetId ? AI_CONNECT_DEFAULT_CLI_PRESETS[presetId]?.options.fileInput : void 0;
610
+ const routeFileInput = transport.cli?.fileInput;
611
+ if (!presetFileInput && !routeFileInput) {
612
+ return /* @__PURE__ */ new Set();
613
+ }
614
+ const merged = {
615
+ ...presetFileInput ?? {},
616
+ ...routeFileInput ?? {}
617
+ };
618
+ return new Set(
619
+ merged.categories ?? ["image", "document", "text", "other"]
620
+ );
621
+ }
439
622
  function defaultCapabilities(transport, runtime) {
623
+ const cliFileCategories = cliFileInputCategories(transport);
440
624
  const localOnly = runtime === "local" || transport.kind === "acp" || transport.kind === "cli" || transport.kind === "server";
441
625
  const browserSafe = !localOnly;
442
626
  return {
@@ -451,10 +635,12 @@ function defaultCapabilities(transport, runtime) {
451
635
  supportsClientToolExecution: transport.kind === "api",
452
636
  // C6 / F-P-4: api routes now advertise document + image input parity with acp.
453
637
  // The per-account `capabilities` override remains the escape hatch for
454
- // text-only api proxies that must opt out.
455
- supportsFileUpload: transport.kind === "acp" || transport.kind === "api",
638
+ // text-only api proxies that must opt out. REQ-024: a cli route advertises
639
+ // document/image input ONLY for the categories its fileInput actually stages
640
+ // (category-aware so e.g. an image-only cli cleanly rejects a PDF pre-spawn).
641
+ supportsFileUpload: transport.kind === "acp" || transport.kind === "api" || cliFileCategories.has("document"),
456
642
  supportsFileOutput: transport.kind === "acp",
457
- supportsImageInput: transport.kind === "acp" || transport.kind === "api",
643
+ supportsImageInput: transport.kind === "acp" || transport.kind === "api" || cliFileCategories.has("image"),
458
644
  supportsImageOutput: transport.kind === "acp",
459
645
  supportsProfileVerification: true,
460
646
  supportsCredentialRotation: false,
@@ -1005,6 +1191,10 @@ function extension(value) {
1005
1191
  }
1006
1192
  return name.slice(index + 1).toLowerCase();
1007
1193
  }
1194
+ function extensionForMime(mimeType) {
1195
+ const subtype = mimeType.split("/")[1]?.toLowerCase().replace(/[^a-z0-9.+-]/g, "");
1196
+ return subtype && subtype.length > 0 ? subtype : "bin";
1197
+ }
1008
1198
  function isRemoteUrl(value) {
1009
1199
  try {
1010
1200
  const url = new URL(value);
@@ -1629,18 +1819,14 @@ var MODEL_REFERENCE = {
1629
1819
  key: "gemini-3.1-flash-lite",
1630
1820
  contextLength: 1048576
1631
1821
  },
1822
+ "gemini-3.1-pro": { key: "gemini-3.1-pro", contextLength: 1048576 },
1632
1823
  "gemini-3-pro": { key: "gemini-3-pro", contextLength: 2097152 },
1633
1824
  "auto-gemini-3": { key: "auto-gemini-3", contextLength: 1048576 },
1634
1825
  "gemini-2.5-flash": { key: "gemini-2.5-flash", contextLength: 1048576 },
1635
1826
  "gemini-2.5-pro": { key: "gemini-2.5-pro", contextLength: 2097152 },
1636
1827
  "gemini-2.0-flash": { key: "gemini-2.0-flash", contextLength: 1048576 },
1637
1828
  "gemini-1.5-flash": { key: "gemini-1.5-flash", contextLength: 1048576 },
1638
- "gemini-1.5-pro": { key: "gemini-1.5-pro", contextLength: 2097152 },
1639
- // --- Qwen (catalog: qwen3-coder-plus) ---
1640
- "qwen3-coder-plus": { key: "qwen3-coder-plus", contextLength: 1048576 },
1641
- "qwen3-coder": { key: "qwen3-coder", contextLength: 262144 },
1642
- "qwen-max": { key: "qwen-max", contextLength: 32768 },
1643
- "qwen-plus": { key: "qwen-plus", contextLength: 131072 }
1829
+ "gemini-1.5-pro": { key: "gemini-1.5-pro", contextLength: 2097152 }
1644
1830
  };
1645
1831
  function normalizeModelKey(model) {
1646
1832
  let key = model.trim().toLowerCase();
@@ -6320,8 +6506,6 @@ import { pathToFileURL } from "node:url";
6320
6506
  var AI_CONNECT_DEFAULT_ACP_COMMANDS = {
6321
6507
  "anthropic:claude-code-acp": "npx -y @agentclientprotocol/claude-agent-acp@^0.25.0",
6322
6508
  "openai:codex-acp": "npx @zed-industries/codex-acp@^0.11.1",
6323
- "gemini:gemini-acp": "gemini --acp",
6324
- "qwen:qwen-acp": "qwen --acp",
6325
6509
  "opencode:opencode-acp": "opencode acp"
6326
6510
  };
6327
6511
 
@@ -6402,15 +6586,6 @@ function resolveHomeDir(env) {
6402
6586
  function resolveXdgConfigHome(env) {
6403
6587
  return env.XDG_CONFIG_HOME ?? path.join(resolveHomeDir(env), ".config");
6404
6588
  }
6405
- function resolveGeminiCliHome(env) {
6406
- return env.GEMINI_CLI_HOME ?? resolveHomeDir(env);
6407
- }
6408
- function resolveGeminiDir(env) {
6409
- return path.join(resolveGeminiCliHome(env), ".gemini");
6410
- }
6411
- function resolveQwenDir(env) {
6412
- return path.join(resolveHomeDir(env), ".qwen");
6413
- }
6414
6589
  function resolveCodexHome(env) {
6415
6590
  return env.CODEX_HOME ?? path.join(resolveHomeDir(env), ".codex");
6416
6591
  }
@@ -6456,118 +6631,6 @@ async function removeIfExists(targetPath) {
6456
6631
  }
6457
6632
  });
6458
6633
  }
6459
- async function readJsonFile(filePath) {
6460
- try {
6461
- const raw = await fs.readFile(filePath, "utf8");
6462
- const parsed = JSON.parse(raw);
6463
- return isRecord2(parsed) ? parsed : void 0;
6464
- } catch (error) {
6465
- if (error instanceof SyntaxError || error.code === "ENOENT") {
6466
- return void 0;
6467
- }
6468
- throw error;
6469
- }
6470
- }
6471
- function mergeRecordValues(current, next) {
6472
- const merged = { ...current };
6473
- for (const [key, value] of Object.entries(next)) {
6474
- const existing = merged[key];
6475
- if (isRecord2(existing) && isRecord2(value)) {
6476
- merged[key] = mergeRecordValues(existing, value);
6477
- continue;
6478
- }
6479
- merged[key] = value;
6480
- }
6481
- return merged;
6482
- }
6483
- async function writeJsonFile(filePath, value) {
6484
- await fs.mkdir(path.dirname(filePath), { recursive: true });
6485
- await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}
6486
- `, "utf8");
6487
- }
6488
- async function prepareGeminiLaunchRuntime(base, launch) {
6489
- if (launch.contextMode === "workspace" && launch.skillsMode === "default") {
6490
- return {
6491
- ...base,
6492
- launch
6493
- };
6494
- }
6495
- const sandbox = await createSandboxPaths("ai-connect-gemini-acp-");
6496
- const originalGeminiDir = resolveGeminiDir(base.env);
6497
- const sandboxGeminiDir = path.join(sandbox.root, ".gemini");
6498
- const settingsPath = path.join(sandboxGeminiDir, "settings.json");
6499
- const systemSettingsPath = path.join(sandbox.config, "gemini-settings.json");
6500
- const systemDefaultsPath = path.join(
6501
- sandbox.config,
6502
- "gemini-system-defaults.json"
6503
- );
6504
- const isolatedCwd = launch.contextMode === "clean" || launch.skillsMode === "disabled" ? sandbox.root : base.cwd;
6505
- await Promise.all(
6506
- [
6507
- "oauth_creds.json",
6508
- "google_accounts.json",
6509
- "mcp-oauth-tokens.json",
6510
- "a2a-oauth-tokens.json",
6511
- "installation_id"
6512
- ].map(
6513
- (fileName) => maybeCopyFile(
6514
- path.join(originalGeminiDir, fileName),
6515
- path.join(sandboxGeminiDir, fileName)
6516
- )
6517
- )
6518
- );
6519
- const settings = mergeRecordValues(
6520
- await readJsonFile(settingsPath) ?? {},
6521
- {
6522
- context: {
6523
- fileName: "AI_CONNECT_CONTEXT_DISABLED.md",
6524
- includeDirectoryTree: false,
6525
- memoryBoundaryMarkers: [],
6526
- includeDirectories: [],
6527
- loadMemoryFromIncludeDirectories: false,
6528
- discoveryMaxDirs: 0
6529
- },
6530
- admin: {
6531
- mcp: {
6532
- enabled: false,
6533
- config: {},
6534
- requiredConfig: {}
6535
- },
6536
- ...launch.skillsMode === "disabled" ? {
6537
- skills: {
6538
- enabled: false
6539
- }
6540
- } : {}
6541
- },
6542
- ...launch.skillsMode === "disabled" ? {
6543
- skills: {
6544
- enabled: false,
6545
- disabled: []
6546
- }
6547
- } : {}
6548
- }
6549
- );
6550
- await Promise.all([
6551
- writeJsonFile(settingsPath, settings),
6552
- writeJsonFile(systemSettingsPath, {}),
6553
- writeJsonFile(systemDefaultsPath, {})
6554
- ]);
6555
- return {
6556
- commandLine: base.commandLine,
6557
- cwd: isolatedCwd,
6558
- env: {
6559
- ...base.env,
6560
- HOME: sandbox.root,
6561
- GEMINI_CLI_HOME: sandbox.root,
6562
- GEMINI_CLI_SYSTEM_SETTINGS_PATH: systemSettingsPath,
6563
- GEMINI_CLI_SYSTEM_DEFAULTS_PATH: systemDefaultsPath
6564
- },
6565
- launch,
6566
- cleanup: async () => {
6567
- await removeIfExists(sandbox.root);
6568
- }
6569
- };
6570
- }
6571
6634
  async function prepareOpenCodeLaunchRuntime(base, launch) {
6572
6635
  if (launch.contextMode === "workspace" && launch.skillsMode === "default") {
6573
6636
  return {
@@ -6616,70 +6679,6 @@ async function prepareOpenCodeLaunchRuntime(base, launch) {
6616
6679
  }
6617
6680
  };
6618
6681
  }
6619
- async function prepareQwenLaunchRuntime(base, launch) {
6620
- if (launch.contextMode === "workspace" && launch.skillsMode === "default") {
6621
- return {
6622
- ...base,
6623
- launch
6624
- };
6625
- }
6626
- const sandbox = await createSandboxPaths("ai-connect-qwen-acp-");
6627
- const originalQwenDir = resolveQwenDir(base.env);
6628
- const sandboxQwenDir = path.join(sandbox.home, ".qwen");
6629
- const settingsPath = path.join(sandboxQwenDir, "settings.json");
6630
- const systemSettingsPath = path.join(sandbox.config, "qwen-settings.json");
6631
- const systemDefaultsPath = path.join(
6632
- sandbox.config,
6633
- "qwen-system-defaults.json"
6634
- );
6635
- await Promise.all(
6636
- [
6637
- "oauth_creds.json",
6638
- "mcp-oauth-tokens.json",
6639
- "installation_id",
6640
- ".env",
6641
- "settings.json"
6642
- ].map(
6643
- (fileName) => maybeCopyFile(
6644
- path.join(originalQwenDir, fileName),
6645
- path.join(sandboxQwenDir, fileName)
6646
- )
6647
- )
6648
- );
6649
- const settings = mergeRecordValues(
6650
- await readJsonFile(settingsPath) ?? {},
6651
- {
6652
- context: {
6653
- fileName: ["AI_CONNECT_CONTEXT_DISABLED.md"],
6654
- includeDirectories: [],
6655
- loadFromIncludeDirectories: false
6656
- },
6657
- model: {
6658
- skipStartupContext: true
6659
- }
6660
- }
6661
- );
6662
- await Promise.all([
6663
- writeJsonFile(settingsPath, settings),
6664
- writeJsonFile(systemSettingsPath, {}),
6665
- writeJsonFile(systemDefaultsPath, {})
6666
- ]);
6667
- const isolatedCwd = launch.contextMode === "clean" || launch.skillsMode === "disabled" ? sandbox.root : base.cwd;
6668
- return {
6669
- commandLine: base.commandLine,
6670
- cwd: isolatedCwd,
6671
- env: {
6672
- ...base.env,
6673
- HOME: sandbox.home,
6674
- QWEN_CODE_SYSTEM_SETTINGS_PATH: systemSettingsPath,
6675
- QWEN_CODE_SYSTEM_DEFAULTS_PATH: systemDefaultsPath
6676
- },
6677
- launch,
6678
- cleanup: async () => {
6679
- await removeIfExists(sandbox.root);
6680
- }
6681
- };
6682
- }
6683
6682
  async function prepareCodexLaunchRuntime(base, launch) {
6684
6683
  if (launch.contextMode === "workspace" && launch.skillsMode === "default") {
6685
6684
  return {
@@ -6797,15 +6796,9 @@ async function prepareAcpLaunchRuntime(route, options, commandLine, cwdOverride)
6797
6796
  ...options?.env ?? {}
6798
6797
  }
6799
6798
  };
6800
- if (route.transport.id === "gemini-acp") {
6801
- return prepareGeminiLaunchRuntime(base, launch);
6802
- }
6803
6799
  if (route.transport.id === "opencode-acp") {
6804
6800
  return prepareOpenCodeLaunchRuntime(base, launch);
6805
6801
  }
6806
- if (route.transport.id === "qwen-acp") {
6807
- return prepareQwenLaunchRuntime(base, launch);
6808
- }
6809
6802
  if (route.transport.id === "codex-acp") {
6810
6803
  return prepareCodexLaunchRuntime(base, launch);
6811
6804
  }
@@ -7386,10 +7379,6 @@ function buildAcpLifecycle(route, authRequest, mode) {
7386
7379
  steps.push("authenticate");
7387
7380
  }
7388
7381
  steps.push("session/new");
7389
- if (mode === "prompt" && route.transport.id === "gemini-acp") {
7390
- steps.push("session/set_model");
7391
- keys.push("session/set_model.modelId");
7392
- }
7393
7382
  if (mode === "prompt") {
7394
7383
  steps.push("session/prompt");
7395
7384
  }
@@ -7525,16 +7514,6 @@ var AcpConnection = class {
7525
7514
  );
7526
7515
  }
7527
7516
  const sessionId = session.sessionId;
7528
- if (context.route.transport.id === "gemini-acp") {
7529
- await measurePhase(
7530
- transport.phases ?? [],
7531
- "session/set_model",
7532
- async () => this.request("session/set_model", {
7533
- sessionId,
7534
- modelId: context.route.model
7535
- })
7536
- );
7537
- }
7538
7517
  this.activePrompt = {
7539
7518
  sessionId,
7540
7519
  text: "",
@@ -8047,9 +8026,6 @@ var AcpConnection = class {
8047
8026
  });
8048
8027
  }
8049
8028
  };
8050
- function isGeminiAcpFallbackCandidate(route, commandLine) {
8051
- return route.transport.id === "gemini-acp" && commandLine.includes("--acp") && !commandLine.includes("--experimental-acp");
8052
- }
8053
8029
  function cacheKeyForConnection(route, runtime, options) {
8054
8030
  const envEntries = Object.entries(options.env).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("");
8055
8031
  return [
@@ -8296,18 +8272,7 @@ function createAcpTransportManager(options) {
8296
8272
  }
8297
8273
  async function drivePrompt(context, onDelta) {
8298
8274
  const commandLine = resolveAcpCommand(context.route, options);
8299
- try {
8300
- return await runWithCommand(context, commandLine, onDelta);
8301
- } catch (error) {
8302
- if (!isGeminiAcpFallbackCandidate(context.route, commandLine)) {
8303
- throw error;
8304
- }
8305
- return await runWithCommand(
8306
- context,
8307
- commandLine.replace("--acp", "--experimental-acp"),
8308
- onDelta
8309
- );
8310
- }
8275
+ return await runWithCommand(context, commandLine, onDelta);
8311
8276
  }
8312
8277
  return {
8313
8278
  async runPrompt(context) {
@@ -8376,17 +8341,7 @@ function createAcpTransportManager(options) {
8376
8341
  },
8377
8342
  async discoverModels(context) {
8378
8343
  const commandLine = resolveAcpCommand(context.route, options);
8379
- try {
8380
- return await discoverWithCommand(context, commandLine);
8381
- } catch (error) {
8382
- if (!isGeminiAcpFallbackCandidate(context.route, commandLine)) {
8383
- throw error;
8384
- }
8385
- return await discoverWithCommand(
8386
- context,
8387
- commandLine.replace("--acp", "--experimental-acp")
8388
- );
8389
- }
8344
+ return await discoverWithCommand(context, commandLine);
8390
8345
  },
8391
8346
  async dispose() {
8392
8347
  const values = [...connectionPools.values()].flat();
@@ -8410,150 +8365,7 @@ import { spawn as spawn2 } from "node:child_process";
8410
8365
  import fs2 from "node:fs/promises";
8411
8366
  import os2 from "node:os";
8412
8367
  import path2 from "node:path";
8413
-
8414
- // src/cli-presets.ts
8415
- var AI_CONNECT_DEFAULT_CLI_PRESETS = {
8416
- gemini: {
8417
- id: "gemini",
8418
- label: "Gemini CLI",
8419
- command: "gemini",
8420
- transportId: "gemini-cli",
8421
- options: {
8422
- preset: "gemini",
8423
- argsTemplate: ["-p", "{prompt}", "--output-format", "json", "--model", "{model}"],
8424
- discovery: {
8425
- via: "acp",
8426
- acp: {
8427
- transportId: "gemini-acp"
8428
- }
8429
- },
8430
- parser: {
8431
- kind: "json",
8432
- textPath: "response",
8433
- usagePath: "stats",
8434
- errorPath: "error"
8435
- }
8436
- }
8437
- },
8438
- qwen: {
8439
- id: "qwen",
8440
- label: "Qwen CLI",
8441
- command: "qwen",
8442
- transportId: "qwen-cli",
8443
- options: {
8444
- preset: "qwen",
8445
- argsTemplate: ["-p", "{prompt}", "--output-format", "json", "--model", "{model}"],
8446
- discovery: {
8447
- via: "acp",
8448
- acp: {
8449
- transportId: "qwen-acp"
8450
- }
8451
- },
8452
- parser: {
8453
- kind: "json",
8454
- textPath: "__preset__:qwen.result",
8455
- usagePath: "__preset__:qwen.stats",
8456
- errorPath: "__preset__:qwen.error"
8457
- }
8458
- }
8459
- },
8460
- claude: {
8461
- id: "claude",
8462
- label: "Claude CLI",
8463
- command: "claude",
8464
- transportId: "claude-cli",
8465
- options: {
8466
- preset: "claude",
8467
- argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "{prompt}"],
8468
- discovery: {
8469
- via: "acp",
8470
- acp: {
8471
- transportId: "claude-code-acp"
8472
- }
8473
- },
8474
- parser: {
8475
- kind: "json",
8476
- textPath: "__preset__:claude.result",
8477
- usagePath: "usage",
8478
- errorPath: "__preset__:claude.error"
8479
- }
8480
- }
8481
- },
8482
- openclaude: {
8483
- id: "openclaude",
8484
- label: "OpenClaude CLI",
8485
- command: "openclaude",
8486
- transportId: "openclaude-cli",
8487
- options: {
8488
- preset: "openclaude",
8489
- argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "{prompt}"],
8490
- parser: {
8491
- kind: "json",
8492
- textPath: "__preset__:claude.result",
8493
- usagePath: "usage",
8494
- errorPath: "__preset__:claude.error"
8495
- }
8496
- }
8497
- },
8498
- codex: {
8499
- id: "codex",
8500
- label: "Codex CLI",
8501
- command: "codex",
8502
- transportId: "codex-cli",
8503
- options: {
8504
- preset: "codex",
8505
- argsTemplate: [
8506
- "exec",
8507
- "--json",
8508
- "--output-last-message",
8509
- "{output_file}",
8510
- "--model",
8511
- "{model}",
8512
- "{prompt}"
8513
- ],
8514
- discovery: {
8515
- via: "acp",
8516
- acp: {
8517
- transportId: "codex-acp"
8518
- }
8519
- },
8520
- parser: {
8521
- kind: "jsonl",
8522
- text: {
8523
- path: "__preset__:codex.text"
8524
- },
8525
- usage: {
8526
- path: "__preset__:codex.usage"
8527
- },
8528
- error: {
8529
- path: "__preset__:codex.error"
8530
- }
8531
- }
8532
- }
8533
- }
8534
- };
8535
- var AI_CONNECT_DEFAULT_CLI_COMMANDS = {
8536
- "anthropic:claude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.claude.command,
8537
- "anthropic:openclaude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.openclaude.command,
8538
- "openclaude:openclaude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.openclaude.command,
8539
- "openai:codex-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.codex.command,
8540
- "gemini:gemini-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.gemini.command,
8541
- "qwen:qwen-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.qwen.command
8542
- };
8543
- function getCliTransportPreset(presetId) {
8544
- return AI_CONNECT_DEFAULT_CLI_PRESETS[presetId];
8545
- }
8546
-
8547
- // src/cli.ts
8548
8368
  var CLI_PRESET_ACP_DISCOVERY_DEFAULTS = {
8549
- gemini: {
8550
- transportId: "gemini-acp",
8551
- providerId: "gemini"
8552
- },
8553
- qwen: {
8554
- transportId: "qwen-acp",
8555
- providerId: "qwen"
8556
- },
8557
8369
  claude: {
8558
8370
  transportId: "claude-code-acp",
8559
8371
  providerId: "anthropic"
@@ -8583,25 +8395,26 @@ function splitCommandLine2(commandLine) {
8583
8395
  function quoteArg(value) {
8584
8396
  return /\s/.test(value) ? JSON.stringify(value) : value;
8585
8397
  }
8586
- function buildCliPrompt(context) {
8398
+ function buildCliPrompt(context, fileInputEnabled) {
8587
8399
  if (context.request.operation !== "text") {
8588
8400
  throw new AiConnectError(
8589
8401
  "not_supported",
8590
8402
  `CLI transport "${context.route.transport.id}" only supports text requests.`
8591
8403
  );
8592
8404
  }
8593
- if (context.request.image || context.request.attachments.some(
8594
- (file) => portableFileCategory(file) === "image" || portableFileCategory(file) === "document"
8595
- )) {
8405
+ if (context.request.image) {
8596
8406
  throw new AiConnectError(
8597
8407
  "unsupported_capability",
8598
- `CLI transport "${context.route.transport.id}" does not support image or document inputs.`
8408
+ `CLI transport "${context.route.transport.id}" does not support image output.`
8599
8409
  );
8600
8410
  }
8601
- if (context.request.attachments.length > 0) {
8411
+ if (!fileInputEnabled && context.request.attachments.length > 0) {
8412
+ const hasBinary = context.request.attachments.some(
8413
+ (file) => portableFileCategory(file) === "image" || portableFileCategory(file) === "document"
8414
+ );
8602
8415
  throw new AiConnectError(
8603
- "not_supported",
8604
- `CLI transport "${context.route.transport.id}" does not support attachments.`
8416
+ hasBinary ? "unsupported_capability" : "not_supported",
8417
+ hasBinary ? `CLI transport "${context.route.transport.id}" does not support image or document inputs.` : `CLI transport "${context.route.transport.id}" does not support attachments.`
8605
8418
  );
8606
8419
  }
8607
8420
  return context.request.messages.map((message) => {
@@ -8660,10 +8473,8 @@ function buildCliCwd(context, options) {
8660
8473
  }
8661
8474
  function defaultCliPresetIdForRoute(route) {
8662
8475
  switch (route.transport.id) {
8663
- case "gemini-cli":
8664
- return "gemini";
8665
- case "qwen-cli":
8666
- return "qwen";
8476
+ case "pi-cli":
8477
+ return "pi";
8667
8478
  case "claude-cli":
8668
8479
  return "claude";
8669
8480
  case "openclaude-cli":
@@ -8748,11 +8559,16 @@ function createCliDiscoveryAcpRoute(route) {
8748
8559
  function resolveCliTransportOptions(route) {
8749
8560
  const presetId = route.transport.cli?.preset ?? defaultCliPresetIdForRoute(route);
8750
8561
  const preset = presetId ? getCliTransportPreset(presetId) : void 0;
8562
+ const mergedFileInput = preset?.options.fileInput || route.transport.cli?.fileInput ? {
8563
+ ...preset?.options.fileInput ?? {},
8564
+ ...route.transport.cli?.fileInput ?? {}
8565
+ } : void 0;
8751
8566
  const merged = {
8752
8567
  ...preset?.options ?? {},
8753
8568
  ...route.transport.cli ?? {},
8754
8569
  ...route.transport.cli?.argsTemplate ? { argsTemplate: route.transport.cli.argsTemplate } : {},
8755
8570
  ...route.transport.cli?.parser ? { parser: route.transport.cli.parser } : {},
8571
+ ...mergedFileInput ? { fileInput: mergedFileInput } : {},
8756
8572
  ...presetId ? { preset: presetId } : {}
8757
8573
  };
8758
8574
  if (!merged.argsTemplate?.length) {
@@ -8763,15 +8579,73 @@ function resolveCliTransportOptions(route) {
8763
8579
  }
8764
8580
  return merged;
8765
8581
  }
8766
- async function createOutputFile() {
8582
+ async function createCliTempDir(baseDir, prefix) {
8767
8583
  const tempDir = await fs2.mkdtemp(
8768
- path2.join(os2.tmpdir(), "ai-connect-cli-output-")
8584
+ path2.join(baseDir ?? os2.tmpdir(), prefix ?? "ai-connect-cli-")
8769
8585
  );
8770
8586
  return {
8771
8587
  tempDir,
8772
8588
  outputFile: path2.join(tempDir, "last-message.txt")
8773
8589
  };
8774
8590
  }
8591
+ function safeStagedBasename(name, index, mimeType) {
8592
+ const base = path2.basename(name.replace(/\\/g, "/"));
8593
+ if (base === "" || base === "." || base === "..") {
8594
+ return `attachment-${index}.${extensionForMime(mimeType)}`;
8595
+ }
8596
+ return base;
8597
+ }
8598
+ async function stageCliAttachments(attachments, attachDir, fileInput) {
8599
+ const categories = new Set(
8600
+ fileInput.categories ?? ["image", "document", "text", "other"]
8601
+ );
8602
+ await fs2.mkdir(attachDir, { recursive: true });
8603
+ const refs = [];
8604
+ const usedNames = /* @__PURE__ */ new Map();
8605
+ for (let i = 0; i < attachments.length; i += 1) {
8606
+ const file = preparePortableFile(attachments[i]);
8607
+ const payload = await materializePortableFile(file);
8608
+ if (!categories.has(payload.category)) {
8609
+ continue;
8610
+ }
8611
+ let destName = safeStagedBasename(payload.name, i, payload.mimeType);
8612
+ const seen = usedNames.get(destName);
8613
+ if (seen !== void 0) {
8614
+ destName = `${i}-${destName}`;
8615
+ }
8616
+ usedNames.set(destName, i);
8617
+ const dest = path2.join(attachDir, destName);
8618
+ if (payload.category === "text") {
8619
+ if (payload.text !== void 0) {
8620
+ await fs2.writeFile(dest, payload.text, "utf8");
8621
+ refs.push(dest);
8622
+ continue;
8623
+ }
8624
+ } else {
8625
+ const bytes = payload.base64 ? Buffer.from(payload.base64, "base64") : await portableFileToBytes(file);
8626
+ if (bytes !== void 0) {
8627
+ await fs2.writeFile(dest, bytes);
8628
+ refs.push(dest);
8629
+ continue;
8630
+ }
8631
+ }
8632
+ if (payload.uri) {
8633
+ refs.push(payload.uri);
8634
+ }
8635
+ }
8636
+ return refs;
8637
+ }
8638
+ function expandFileArgs(staged, fileInput) {
8639
+ const perFileArgs = fileInput?.perFileArgs ?? ["@{path}"];
8640
+ if (fileInput?.placement === "prompt") {
8641
+ return [];
8642
+ }
8643
+ return staged.flatMap(
8644
+ (ref) => perFileArgs.map(
8645
+ (tpl) => tpl.replace("{path}", ref).replace("{name}", path2.basename(ref))
8646
+ )
8647
+ );
8648
+ }
8775
8649
  function materializeTemplateArg(templateArg, values, parameterKeys) {
8776
8650
  switch (templateArg) {
8777
8651
  case "{prompt}":
@@ -8798,31 +8672,62 @@ function materializeTemplateArg(templateArg, values, parameterKeys) {
8798
8672
  }
8799
8673
  async function buildCliInvocation(context, options) {
8800
8674
  const route = context.route;
8801
- const prompt = buildCliPrompt(context);
8802
8675
  const cliOptions = resolveCliTransportOptions(route);
8676
+ const fileInput = cliOptions.fileInput;
8677
+ const attachments = context.request.attachments;
8678
+ const hasStageable = Boolean(fileInput) && attachments.length > 0;
8679
+ let prompt = buildCliPrompt(context, Boolean(fileInput));
8803
8680
  const commandLine = resolveCliCommand(route, options);
8804
8681
  const resolved = splitCommandLine2(commandLine);
8805
8682
  const parameterKeys = [];
8806
8683
  let tempDir;
8807
8684
  let outputFile;
8808
- if (cliOptions.argsTemplate.includes("{output_file}")) {
8809
- const created = await createOutputFile();
8685
+ const needsOutputFile = cliOptions.argsTemplate.includes("{output_file}");
8686
+ if (needsOutputFile || hasStageable) {
8687
+ const created = await createCliTempDir(
8688
+ fileInput?.stagingDir ?? options?.staging?.dir,
8689
+ options?.staging?.prefix
8690
+ );
8810
8691
  tempDir = created.tempDir;
8811
8692
  outputFile = created.outputFile;
8812
8693
  }
8694
+ let staged = [];
8695
+ if (hasStageable && tempDir && fileInput) {
8696
+ staged = await stageCliAttachments(
8697
+ attachments,
8698
+ path2.join(tempDir, "attachments"),
8699
+ fileInput
8700
+ );
8701
+ if (fileInput.placement === "prompt" && staged.length > 0) {
8702
+ const separator = fileInput.separator ?? " ";
8703
+ const mentionTemplate = fileInput.mentionTemplate ?? "@{path}";
8704
+ const mentions = staged.map(
8705
+ (ref) => mentionTemplate.replace("{path}", ref).replace("{name}", path2.basename(ref))
8706
+ ).join(separator);
8707
+ prompt = `${prompt}${separator}${mentions}`;
8708
+ }
8709
+ }
8813
8710
  const args = [
8814
8711
  ...resolved.args,
8815
- ...cliOptions.argsTemplate.map(
8816
- (part) => materializeTemplateArg(
8817
- part,
8818
- {
8819
- prompt,
8820
- model: route.model,
8821
- ...outputFile ? { outputFile } : {}
8822
- },
8823
- parameterKeys
8824
- )
8825
- )
8712
+ ...cliOptions.argsTemplate.flatMap((part) => {
8713
+ if (part === "{files}") {
8714
+ if (staged.length > 0) {
8715
+ parameterKeys.push("{files}");
8716
+ }
8717
+ return expandFileArgs(staged, fileInput);
8718
+ }
8719
+ return [
8720
+ materializeTemplateArg(
8721
+ part,
8722
+ {
8723
+ prompt,
8724
+ model: route.model,
8725
+ ...outputFile ? { outputFile } : {}
8726
+ },
8727
+ parameterKeys
8728
+ )
8729
+ ];
8730
+ })
8826
8731
  ];
8827
8732
  return {
8828
8733
  command: resolved.command,
@@ -8831,6 +8736,7 @@ async function buildCliInvocation(context, options) {
8831
8736
  env: buildCliEnvironment(options),
8832
8737
  ...tempDir ? { tempDir } : {},
8833
8738
  ...outputFile ? { outputFile } : {},
8739
+ ...options?.staging?.keep ? { keepStaging: true } : {},
8834
8740
  parameterKeys
8835
8741
  };
8836
8742
  }
@@ -9042,60 +8948,6 @@ function parseCliModelList(stdout, parser, selector) {
9042
8948
  }
9043
8949
  return models;
9044
8950
  }
9045
- function parseGeminiCli(stdout) {
9046
- const payload = JSON.parse(stdout);
9047
- if (typeof payload.error === "string") {
9048
- throw new AiConnectError("temporary_unavailable", payload.error);
9049
- }
9050
- if (typeof payload.response !== "string") {
9051
- throw new AiConnectError(
9052
- "temporary_unavailable",
9053
- "Gemini CLI JSON output did not include response text."
9054
- );
9055
- }
9056
- const usage = payload.stats ? statsToUsage(payload.stats) : void 0;
9057
- return {
9058
- text: payload.response,
9059
- ...usage ? { usage } : {},
9060
- data: payload
9061
- };
9062
- }
9063
- function parseQwenCli(stdout) {
9064
- const payload = JSON.parse(stdout);
9065
- if (!Array.isArray(payload)) {
9066
- throw new AiConnectError(
9067
- "temporary_unavailable",
9068
- "Qwen CLI JSON output did not return a message array."
9069
- );
9070
- }
9071
- const resultMessage = payload.find(
9072
- (entry) => entry && typeof entry === "object" && entry.type === "result"
9073
- );
9074
- if (!resultMessage) {
9075
- throw new AiConnectError(
9076
- "temporary_unavailable",
9077
- "Qwen CLI JSON output did not contain a result message."
9078
- );
9079
- }
9080
- if (resultMessage.is_error === true) {
9081
- throw new AiConnectError(
9082
- "temporary_unavailable",
9083
- typeof resultMessage.result === "string" ? resultMessage.result : "Qwen CLI returned an error result."
9084
- );
9085
- }
9086
- if (typeof resultMessage.result !== "string") {
9087
- throw new AiConnectError(
9088
- "temporary_unavailable",
9089
- "Qwen CLI result message did not contain text output."
9090
- );
9091
- }
9092
- const usage = resultMessage.stats ? statsToUsage(resultMessage.stats) : void 0;
9093
- return {
9094
- text: resultMessage.result,
9095
- ...usage ? { usage } : {},
9096
- data: payload
9097
- };
9098
- }
9099
8951
  function parseClaudeCli(stdout) {
9100
8952
  const payload = JSON.parse(stdout);
9101
8953
  const text = typeof payload.result === "string" ? payload.result : typeof payload.response === "string" ? payload.response : typeof payload.text === "string" ? payload.text : void 0;
@@ -9181,10 +9033,8 @@ function parseCliResult(route, result, outputFileContent) {
9181
9033
  }
9182
9034
  }
9183
9035
  switch (cliOptions.preset) {
9184
- case "gemini":
9185
- return parseGeminiCli(stdout);
9186
- case "qwen":
9187
- return parseQwenCli(stdout);
9036
+ case "pi":
9037
+ return parseTextCli(stdout, { kind: "text" });
9188
9038
  case "claude":
9189
9039
  case "openclaude":
9190
9040
  return parseClaudeCli(stdout);
@@ -9275,7 +9125,7 @@ async function executeCliInvocation(invocation, timeoutMs, phases, signal) {
9275
9125
  });
9276
9126
  }
9277
9127
  async function cleanupCliInvocation(invocation) {
9278
- if (invocation.tempDir) {
9128
+ if (invocation.tempDir && !invocation.keepStaging) {
9279
9129
  await fs2.rm(invocation.tempDir, { recursive: true, force: true });
9280
9130
  }
9281
9131
  }