@vedmalex/ai-connect 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bun/index.js CHANGED
@@ -1,3 +1,125 @@
1
+ // src/cli-presets.ts
2
+ var AI_CONNECT_DEFAULT_CLI_PRESETS = {
3
+ pi: {
4
+ id: "pi",
5
+ label: "pi (coding agent)",
6
+ command: "pi",
7
+ transportId: "pi-cli",
8
+ options: {
9
+ preset: "pi",
10
+ // pi print mode emits the answer as plain text on stdout (no JSON wrapper),
11
+ // and has no ACP mode — so it uses the raw `text` parser and no discovery sidecar.
12
+ // pi takes file/image attachments as separate `@<path>` argv tokens (REQ-024);
13
+ // {files} expands one `@<staged-path>` token per attachment. pi stages all
14
+ // categories (PDFs as-is, no extraction guarantee).
15
+ argsTemplate: ["--print", "--model", "{model}", "{files}", "{prompt}"],
16
+ discovery: {
17
+ via: "none"
18
+ },
19
+ parser: {
20
+ kind: "text"
21
+ },
22
+ fileInput: {
23
+ placement: "args",
24
+ perFileArgs: ["@{path}"]
25
+ }
26
+ }
27
+ },
28
+ claude: {
29
+ id: "claude",
30
+ label: "Claude CLI",
31
+ command: "claude",
32
+ transportId: "claude-cli",
33
+ options: {
34
+ preset: "claude",
35
+ argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "{prompt}"],
36
+ discovery: {
37
+ via: "acp",
38
+ acp: {
39
+ transportId: "claude-code-acp"
40
+ }
41
+ },
42
+ parser: {
43
+ kind: "json",
44
+ textPath: "__preset__:claude.result",
45
+ usagePath: "usage",
46
+ errorPath: "__preset__:claude.error"
47
+ }
48
+ }
49
+ },
50
+ openclaude: {
51
+ id: "openclaude",
52
+ label: "OpenClaude CLI",
53
+ command: "openclaude",
54
+ transportId: "openclaude-cli",
55
+ options: {
56
+ preset: "openclaude",
57
+ argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "{prompt}"],
58
+ parser: {
59
+ kind: "json",
60
+ textPath: "__preset__:claude.result",
61
+ usagePath: "usage",
62
+ errorPath: "__preset__:claude.error"
63
+ }
64
+ }
65
+ },
66
+ codex: {
67
+ id: "codex",
68
+ label: "Codex CLI",
69
+ command: "codex",
70
+ transportId: "codex-cli",
71
+ options: {
72
+ preset: "codex",
73
+ // codex exec takes image attachments via `-i/--image <FILE>` flags
74
+ // (REQ-024); {files} expands a `--image <staged-path>` pair per image.
75
+ // Images only — a non-image attachment cleanly rejects pre-spawn.
76
+ argsTemplate: [
77
+ "exec",
78
+ "--json",
79
+ "--output-last-message",
80
+ "{output_file}",
81
+ "--model",
82
+ "{model}",
83
+ "{files}",
84
+ "{prompt}"
85
+ ],
86
+ discovery: {
87
+ via: "acp",
88
+ acp: {
89
+ transportId: "codex-acp"
90
+ }
91
+ },
92
+ fileInput: {
93
+ placement: "args",
94
+ perFileArgs: ["--image", "{path}"],
95
+ categories: ["image"]
96
+ },
97
+ parser: {
98
+ kind: "jsonl",
99
+ text: {
100
+ path: "__preset__:codex.text"
101
+ },
102
+ usage: {
103
+ path: "__preset__:codex.usage"
104
+ },
105
+ error: {
106
+ path: "__preset__:codex.error"
107
+ }
108
+ }
109
+ }
110
+ }
111
+ };
112
+ var AI_CONNECT_DEFAULT_CLI_COMMANDS = {
113
+ "anthropic:claude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.claude.command,
114
+ "anthropic:openclaude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.openclaude.command,
115
+ "openclaude:openclaude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.openclaude.command,
116
+ "openai:codex-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.codex.command,
117
+ "pi:pi-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.pi.command
118
+ };
119
+ function getCliTransportPreset(presetId) {
120
+ return AI_CONNECT_DEFAULT_CLI_PRESETS[presetId];
121
+ }
122
+
1
123
  // src/errors.ts
2
124
  var AiConnectError = class extends Error {
3
125
  code;
@@ -345,9 +467,38 @@ function normalizeTransport(providerId, input) {
345
467
  ...fallback !== void 0 ? { fallback } : {}
346
468
  };
347
469
  })();
470
+ const fileInput = (() => {
471
+ const fi = descriptor.cli?.fileInput;
472
+ if (!fi) {
473
+ return void 0;
474
+ }
475
+ const validCategories = ["image", "document", "text", "other"];
476
+ if (fi.categories) {
477
+ for (const c of fi.categories) {
478
+ assert(
479
+ validCategories.includes(c),
480
+ `Unsupported CLI fileInput category "${String(c)}" for provider "${providerId}".`
481
+ );
482
+ }
483
+ }
484
+ const placement = fi.placement ?? "args";
485
+ assert(
486
+ placement === "args" || placement === "prompt",
487
+ `Unsupported CLI fileInput placement "${String(placement)}" for provider "${providerId}".`
488
+ );
489
+ return {
490
+ placement,
491
+ ...fi.perFileArgs ? { perFileArgs: fi.perFileArgs.map((part) => String(part)) } : {},
492
+ ...fi.mentionTemplate?.trim() ? { mentionTemplate: fi.mentionTemplate.trim() } : {},
493
+ ...fi.separator !== void 0 ? { separator: fi.separator } : {},
494
+ ...fi.categories ? { categories: [...fi.categories] } : {},
495
+ ...fi.stagingDir?.trim() ? { stagingDir: fi.stagingDir.trim() } : {}
496
+ };
497
+ })();
348
498
  return {
349
499
  ...normalized,
350
- ...discovery ? { discovery } : {}
500
+ ...discovery ? { discovery } : {},
501
+ ...fileInput ? { fileInput } : {}
351
502
  };
352
503
  })();
353
504
  const normalizedAuth = descriptor.auth?.methodId?.trim() ? {
@@ -435,7 +586,40 @@ function normalizeRuntime(transport, runtime) {
435
586
  }
436
587
  return "universal";
437
588
  }
589
+ function cliPresetIdForTransportId(transportId) {
590
+ switch (transportId) {
591
+ case "pi-cli":
592
+ return "pi";
593
+ case "claude-cli":
594
+ return "claude";
595
+ case "openclaude-cli":
596
+ return "openclaude";
597
+ case "codex-cli":
598
+ return "codex";
599
+ default:
600
+ return void 0;
601
+ }
602
+ }
603
+ function cliFileInputCategories(transport) {
604
+ if (transport.kind !== "cli") {
605
+ return /* @__PURE__ */ new Set();
606
+ }
607
+ const presetId = transport.cli?.preset ?? cliPresetIdForTransportId(transport.id);
608
+ const presetFileInput = presetId ? AI_CONNECT_DEFAULT_CLI_PRESETS[presetId]?.options.fileInput : void 0;
609
+ const routeFileInput = transport.cli?.fileInput;
610
+ if (!presetFileInput && !routeFileInput) {
611
+ return /* @__PURE__ */ new Set();
612
+ }
613
+ const merged = {
614
+ ...presetFileInput ?? {},
615
+ ...routeFileInput ?? {}
616
+ };
617
+ return new Set(
618
+ merged.categories ?? ["image", "document", "text", "other"]
619
+ );
620
+ }
438
621
  function defaultCapabilities(transport, runtime) {
622
+ const cliFileCategories = cliFileInputCategories(transport);
439
623
  const localOnly = runtime === "local" || transport.kind === "acp" || transport.kind === "cli" || transport.kind === "server";
440
624
  const browserSafe = !localOnly;
441
625
  return {
@@ -450,10 +634,12 @@ function defaultCapabilities(transport, runtime) {
450
634
  supportsClientToolExecution: transport.kind === "api",
451
635
  // C6 / F-P-4: api routes now advertise document + image input parity with acp.
452
636
  // The per-account `capabilities` override remains the escape hatch for
453
- // text-only api proxies that must opt out.
454
- supportsFileUpload: transport.kind === "acp" || transport.kind === "api",
637
+ // text-only api proxies that must opt out. REQ-024: a cli route advertises
638
+ // document/image input ONLY for the categories its fileInput actually stages
639
+ // (category-aware so e.g. an image-only cli cleanly rejects a PDF pre-spawn).
640
+ supportsFileUpload: transport.kind === "acp" || transport.kind === "api" || cliFileCategories.has("document"),
455
641
  supportsFileOutput: transport.kind === "acp",
456
- supportsImageInput: transport.kind === "acp" || transport.kind === "api",
642
+ supportsImageInput: transport.kind === "acp" || transport.kind === "api" || cliFileCategories.has("image"),
457
643
  supportsImageOutput: transport.kind === "acp",
458
644
  supportsProfileVerification: true,
459
645
  supportsCredentialRotation: false,
@@ -1004,6 +1190,10 @@ function extension(value) {
1004
1190
  }
1005
1191
  return name.slice(index + 1).toLowerCase();
1006
1192
  }
1193
+ function extensionForMime(mimeType) {
1194
+ const subtype = mimeType.split("/")[1]?.toLowerCase().replace(/[^a-z0-9.+-]/g, "");
1195
+ return subtype && subtype.length > 0 ? subtype : "bin";
1196
+ }
1007
1197
  function isRemoteUrl(value) {
1008
1198
  try {
1009
1199
  const url = new URL(value);
@@ -1794,6 +1984,8 @@ function fillConfiguredContextLength(route, models) {
1794
1984
  return {
1795
1985
  ...entry,
1796
1986
  contextLength: configured,
1987
+ // FT-002: typed provenance (canonical) + metadata alias (back-compat).
1988
+ contextWindowSource: "configured",
1797
1989
  metadata: { ...entry.metadata ?? {}, contextWindowSource: "configured" }
1798
1990
  };
1799
1991
  });
@@ -3126,7 +3318,8 @@ function createClient(configOrInput, options = {}) {
3126
3318
  ...catalog.canonicalModelId ? { canonicalModelId: catalog.canonicalModelId } : {},
3127
3319
  ...catalog.resolvedModelId ? { resolvedModelId: catalog.resolvedModelId } : {},
3128
3320
  ...catalog.currentModelId ? { currentModelId: catalog.currentModelId } : {},
3129
- availableModels: catalog.availableModels
3321
+ availableModels: catalog.availableModels,
3322
+ ...catalog.warnings?.length ? { warnings: catalog.warnings } : {}
3130
3323
  });
3131
3324
  wideAttempts.push({
3132
3325
  index: wideAttempts.length + 1,
@@ -4829,7 +5022,7 @@ function parseOpenAiModelCatalog(route, payload) {
4829
5022
  modelId: entry.id,
4830
5023
  name: typeof entry.display_name === "string" ? entry.display_name : typeof entry.name === "string" ? entry.name : entry.id,
4831
5024
  ...typeof entry.description === "string" ? { description: entry.description } : {},
4832
- ...contextLength !== void 0 ? { contextLength } : {},
5025
+ ...contextLength !== void 0 ? { contextLength, contextWindowSource: "discovered" } : {},
4833
5026
  ...pricing ? { pricing } : {},
4834
5027
  ...free ? { free } : {},
4835
5028
  ...metadata ? {
@@ -6342,112 +6535,6 @@ var AI_CONNECT_DEFAULT_ACP_COMMANDS = {
6342
6535
  "opencode:opencode-acp": "opencode acp"
6343
6536
  };
6344
6537
 
6345
- // src/cli-presets.ts
6346
- var AI_CONNECT_DEFAULT_CLI_PRESETS = {
6347
- pi: {
6348
- id: "pi",
6349
- label: "pi (coding agent)",
6350
- command: "pi",
6351
- transportId: "pi-cli",
6352
- options: {
6353
- preset: "pi",
6354
- // pi print mode emits the answer as plain text on stdout (no JSON wrapper),
6355
- // and has no ACP mode — so it uses the raw `text` parser and no discovery sidecar.
6356
- argsTemplate: ["--print", "--model", "{model}", "{prompt}"],
6357
- discovery: {
6358
- via: "none"
6359
- },
6360
- parser: {
6361
- kind: "text"
6362
- }
6363
- }
6364
- },
6365
- claude: {
6366
- id: "claude",
6367
- label: "Claude CLI",
6368
- command: "claude",
6369
- transportId: "claude-cli",
6370
- options: {
6371
- preset: "claude",
6372
- argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "{prompt}"],
6373
- discovery: {
6374
- via: "acp",
6375
- acp: {
6376
- transportId: "claude-code-acp"
6377
- }
6378
- },
6379
- parser: {
6380
- kind: "json",
6381
- textPath: "__preset__:claude.result",
6382
- usagePath: "usage",
6383
- errorPath: "__preset__:claude.error"
6384
- }
6385
- }
6386
- },
6387
- openclaude: {
6388
- id: "openclaude",
6389
- label: "OpenClaude CLI",
6390
- command: "openclaude",
6391
- transportId: "openclaude-cli",
6392
- options: {
6393
- preset: "openclaude",
6394
- argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "{prompt}"],
6395
- parser: {
6396
- kind: "json",
6397
- textPath: "__preset__:claude.result",
6398
- usagePath: "usage",
6399
- errorPath: "__preset__:claude.error"
6400
- }
6401
- }
6402
- },
6403
- codex: {
6404
- id: "codex",
6405
- label: "Codex CLI",
6406
- command: "codex",
6407
- transportId: "codex-cli",
6408
- options: {
6409
- preset: "codex",
6410
- argsTemplate: [
6411
- "exec",
6412
- "--json",
6413
- "--output-last-message",
6414
- "{output_file}",
6415
- "--model",
6416
- "{model}",
6417
- "{prompt}"
6418
- ],
6419
- discovery: {
6420
- via: "acp",
6421
- acp: {
6422
- transportId: "codex-acp"
6423
- }
6424
- },
6425
- parser: {
6426
- kind: "jsonl",
6427
- text: {
6428
- path: "__preset__:codex.text"
6429
- },
6430
- usage: {
6431
- path: "__preset__:codex.usage"
6432
- },
6433
- error: {
6434
- path: "__preset__:codex.error"
6435
- }
6436
- }
6437
- }
6438
- }
6439
- };
6440
- var AI_CONNECT_DEFAULT_CLI_COMMANDS = {
6441
- "anthropic:claude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.claude.command,
6442
- "anthropic:openclaude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.openclaude.command,
6443
- "openclaude:openclaude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.openclaude.command,
6444
- "openai:codex-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.codex.command,
6445
- "pi:pi-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.pi.command
6446
- };
6447
- function getCliTransportPreset(presetId) {
6448
- return AI_CONNECT_DEFAULT_CLI_PRESETS[presetId];
6449
- }
6450
-
6451
6538
  // src/server-presets.ts
6452
6539
  var AI_CONNECT_DEFAULT_SERVER_PRESETS = {
6453
6540
  opencode: {
@@ -8561,25 +8648,26 @@ function splitCommandLine2(commandLine) {
8561
8648
  function quoteArg(value) {
8562
8649
  return /\s/.test(value) ? JSON.stringify(value) : value;
8563
8650
  }
8564
- function buildCliPrompt(context) {
8651
+ function buildCliPrompt(context, fileInputEnabled) {
8565
8652
  if (context.request.operation !== "text") {
8566
8653
  throw new AiConnectError(
8567
8654
  "not_supported",
8568
8655
  `CLI transport "${context.route.transport.id}" only supports text requests.`
8569
8656
  );
8570
8657
  }
8571
- if (context.request.image || context.request.attachments.some(
8572
- (file) => portableFileCategory(file) === "image" || portableFileCategory(file) === "document"
8573
- )) {
8658
+ if (context.request.image) {
8574
8659
  throw new AiConnectError(
8575
8660
  "unsupported_capability",
8576
- `CLI transport "${context.route.transport.id}" does not support image or document inputs.`
8661
+ `CLI transport "${context.route.transport.id}" does not support image output.`
8577
8662
  );
8578
8663
  }
8579
- if (context.request.attachments.length > 0) {
8664
+ if (!fileInputEnabled && context.request.attachments.length > 0) {
8665
+ const hasBinary = context.request.attachments.some(
8666
+ (file) => portableFileCategory(file) === "image" || portableFileCategory(file) === "document"
8667
+ );
8580
8668
  throw new AiConnectError(
8581
- "not_supported",
8582
- `CLI transport "${context.route.transport.id}" does not support attachments.`
8669
+ hasBinary ? "unsupported_capability" : "not_supported",
8670
+ hasBinary ? `CLI transport "${context.route.transport.id}" does not support image or document inputs.` : `CLI transport "${context.route.transport.id}" does not support attachments.`
8583
8671
  );
8584
8672
  }
8585
8673
  return context.request.messages.map((message) => {
@@ -8724,11 +8812,16 @@ function createCliDiscoveryAcpRoute(route) {
8724
8812
  function resolveCliTransportOptions(route) {
8725
8813
  const presetId = route.transport.cli?.preset ?? defaultCliPresetIdForRoute(route);
8726
8814
  const preset = presetId ? getCliTransportPreset(presetId) : void 0;
8815
+ const mergedFileInput = preset?.options.fileInput || route.transport.cli?.fileInput ? {
8816
+ ...preset?.options.fileInput ?? {},
8817
+ ...route.transport.cli?.fileInput ?? {}
8818
+ } : void 0;
8727
8819
  const merged = {
8728
8820
  ...preset?.options ?? {},
8729
8821
  ...route.transport.cli ?? {},
8730
8822
  ...route.transport.cli?.argsTemplate ? { argsTemplate: route.transport.cli.argsTemplate } : {},
8731
8823
  ...route.transport.cli?.parser ? { parser: route.transport.cli.parser } : {},
8824
+ ...mergedFileInput ? { fileInput: mergedFileInput } : {},
8732
8825
  ...presetId ? { preset: presetId } : {}
8733
8826
  };
8734
8827
  if (!merged.argsTemplate?.length) {
@@ -8739,15 +8832,73 @@ function resolveCliTransportOptions(route) {
8739
8832
  }
8740
8833
  return merged;
8741
8834
  }
8742
- async function createOutputFile() {
8835
+ async function createCliTempDir(baseDir, prefix) {
8743
8836
  const tempDir = await fs2.mkdtemp(
8744
- path2.join(os2.tmpdir(), "ai-connect-cli-output-")
8837
+ path2.join(baseDir ?? os2.tmpdir(), prefix ?? "ai-connect-cli-")
8745
8838
  );
8746
8839
  return {
8747
8840
  tempDir,
8748
8841
  outputFile: path2.join(tempDir, "last-message.txt")
8749
8842
  };
8750
8843
  }
8844
+ function safeStagedBasename(name, index, mimeType) {
8845
+ const base = path2.basename(name.replace(/\\/g, "/"));
8846
+ if (base === "" || base === "." || base === "..") {
8847
+ return `attachment-${index}.${extensionForMime(mimeType)}`;
8848
+ }
8849
+ return base;
8850
+ }
8851
+ async function stageCliAttachments(attachments, attachDir, fileInput) {
8852
+ const categories = new Set(
8853
+ fileInput.categories ?? ["image", "document", "text", "other"]
8854
+ );
8855
+ await fs2.mkdir(attachDir, { recursive: true });
8856
+ const refs = [];
8857
+ const usedNames = /* @__PURE__ */ new Map();
8858
+ for (let i = 0; i < attachments.length; i += 1) {
8859
+ const file = preparePortableFile(attachments[i]);
8860
+ const payload = await materializePortableFile(file);
8861
+ if (!categories.has(payload.category)) {
8862
+ continue;
8863
+ }
8864
+ let destName = safeStagedBasename(payload.name, i, payload.mimeType);
8865
+ const seen = usedNames.get(destName);
8866
+ if (seen !== void 0) {
8867
+ destName = `${i}-${destName}`;
8868
+ }
8869
+ usedNames.set(destName, i);
8870
+ const dest = path2.join(attachDir, destName);
8871
+ if (payload.category === "text") {
8872
+ if (payload.text !== void 0) {
8873
+ await fs2.writeFile(dest, payload.text, "utf8");
8874
+ refs.push(dest);
8875
+ continue;
8876
+ }
8877
+ } else {
8878
+ const bytes = payload.base64 ? Buffer.from(payload.base64, "base64") : await portableFileToBytes(file);
8879
+ if (bytes !== void 0) {
8880
+ await fs2.writeFile(dest, bytes);
8881
+ refs.push(dest);
8882
+ continue;
8883
+ }
8884
+ }
8885
+ if (payload.uri) {
8886
+ refs.push(payload.uri);
8887
+ }
8888
+ }
8889
+ return refs;
8890
+ }
8891
+ function expandFileArgs(staged, fileInput) {
8892
+ const perFileArgs = fileInput?.perFileArgs ?? ["@{path}"];
8893
+ if (fileInput?.placement === "prompt") {
8894
+ return [];
8895
+ }
8896
+ return staged.flatMap(
8897
+ (ref) => perFileArgs.map(
8898
+ (tpl) => tpl.replace("{path}", ref).replace("{name}", path2.basename(ref))
8899
+ )
8900
+ );
8901
+ }
8751
8902
  function materializeTemplateArg(templateArg, values, parameterKeys) {
8752
8903
  switch (templateArg) {
8753
8904
  case "{prompt}":
@@ -8774,31 +8925,62 @@ function materializeTemplateArg(templateArg, values, parameterKeys) {
8774
8925
  }
8775
8926
  async function buildCliInvocation(context, options) {
8776
8927
  const route = context.route;
8777
- const prompt = buildCliPrompt(context);
8778
8928
  const cliOptions = resolveCliTransportOptions(route);
8929
+ const fileInput = cliOptions.fileInput;
8930
+ const attachments = context.request.attachments;
8931
+ const hasStageable = Boolean(fileInput) && attachments.length > 0;
8932
+ let prompt = buildCliPrompt(context, Boolean(fileInput));
8779
8933
  const commandLine = resolveCliCommand(route, options);
8780
8934
  const resolved = splitCommandLine2(commandLine);
8781
8935
  const parameterKeys = [];
8782
8936
  let tempDir;
8783
8937
  let outputFile;
8784
- if (cliOptions.argsTemplate.includes("{output_file}")) {
8785
- const created = await createOutputFile();
8938
+ const needsOutputFile = cliOptions.argsTemplate.includes("{output_file}");
8939
+ if (needsOutputFile || hasStageable) {
8940
+ const created = await createCliTempDir(
8941
+ fileInput?.stagingDir ?? options?.staging?.dir,
8942
+ options?.staging?.prefix
8943
+ );
8786
8944
  tempDir = created.tempDir;
8787
8945
  outputFile = created.outputFile;
8788
8946
  }
8947
+ let staged = [];
8948
+ if (hasStageable && tempDir && fileInput) {
8949
+ staged = await stageCliAttachments(
8950
+ attachments,
8951
+ path2.join(tempDir, "attachments"),
8952
+ fileInput
8953
+ );
8954
+ if (fileInput.placement === "prompt" && staged.length > 0) {
8955
+ const separator = fileInput.separator ?? " ";
8956
+ const mentionTemplate = fileInput.mentionTemplate ?? "@{path}";
8957
+ const mentions = staged.map(
8958
+ (ref) => mentionTemplate.replace("{path}", ref).replace("{name}", path2.basename(ref))
8959
+ ).join(separator);
8960
+ prompt = `${prompt}${separator}${mentions}`;
8961
+ }
8962
+ }
8789
8963
  const args = [
8790
8964
  ...resolved.args,
8791
- ...cliOptions.argsTemplate.map(
8792
- (part) => materializeTemplateArg(
8793
- part,
8794
- {
8795
- prompt,
8796
- model: route.model,
8797
- ...outputFile ? { outputFile } : {}
8798
- },
8799
- parameterKeys
8800
- )
8801
- )
8965
+ ...cliOptions.argsTemplate.flatMap((part) => {
8966
+ if (part === "{files}") {
8967
+ if (staged.length > 0) {
8968
+ parameterKeys.push("{files}");
8969
+ }
8970
+ return expandFileArgs(staged, fileInput);
8971
+ }
8972
+ return [
8973
+ materializeTemplateArg(
8974
+ part,
8975
+ {
8976
+ prompt,
8977
+ model: route.model,
8978
+ ...outputFile ? { outputFile } : {}
8979
+ },
8980
+ parameterKeys
8981
+ )
8982
+ ];
8983
+ })
8802
8984
  ];
8803
8985
  return {
8804
8986
  command: resolved.command,
@@ -8807,6 +8989,7 @@ async function buildCliInvocation(context, options) {
8807
8989
  env: buildCliEnvironment(options),
8808
8990
  ...tempDir ? { tempDir } : {},
8809
8991
  ...outputFile ? { outputFile } : {},
8992
+ ...options?.staging?.keep ? { keepStaging: true } : {},
8810
8993
  parameterKeys
8811
8994
  };
8812
8995
  }
@@ -8835,16 +9018,20 @@ function buildCliDiscoveryInvocation(route, command, options) {
8835
9018
  parameterKeys
8836
9019
  };
8837
9020
  }
8838
- function buildStaticCliCatalog(route) {
9021
+ function buildStaticCliCatalog(route, warning) {
8839
9022
  const models = route.advertisedModels.map((id) => ({
8840
9023
  modelId: id,
8841
9024
  name: id
8842
9025
  }));
8843
- return buildModelCatalog(
9026
+ const catalog = buildModelCatalog(
8844
9027
  route,
8845
9028
  models,
8846
9029
  currentModelIdForRoute(route, route.advertisedModels)
8847
9030
  );
9031
+ if (warning) {
9032
+ catalog.warnings = [...catalog.warnings ?? [], warning];
9033
+ }
9034
+ return catalog;
8848
9035
  }
8849
9036
  function statsToUsage(stats) {
8850
9037
  if (!stats || typeof stats !== "object") {
@@ -8987,7 +9174,9 @@ function modelInfoFromRecord(record, selector) {
8987
9174
  modelId,
8988
9175
  name,
8989
9176
  ...description ? { description } : {},
8990
- ...contextLength !== void 0 ? { contextLength } : {}
9177
+ // FT-002: a contextLength read from the list-command output is a discovered
9178
+ // value (consistent with the API parsers), so tag its provenance.
9179
+ ...contextLength !== void 0 ? { contextLength, contextWindowSource: "discovered" } : {}
8991
9180
  };
8992
9181
  }
8993
9182
  function parseCliModelList(stdout, parser, selector) {
@@ -9195,7 +9384,7 @@ async function executeCliInvocation(invocation, timeoutMs, phases, signal) {
9195
9384
  });
9196
9385
  }
9197
9386
  async function cleanupCliInvocation(invocation) {
9198
- if (invocation.tempDir) {
9387
+ if (invocation.tempDir && !invocation.keepStaging) {
9199
9388
  await fs2.rm(invocation.tempDir, { recursive: true, force: true });
9200
9389
  }
9201
9390
  }
@@ -9258,13 +9447,20 @@ function createCliTransportManager(options) {
9258
9447
  throw error;
9259
9448
  }
9260
9449
  if (source.fallback === "static") {
9261
- return buildStaticCliCatalog(context.route);
9450
+ const detail = error instanceof Error ? error.message : String(error);
9451
+ return buildStaticCliCatalog(
9452
+ context.route,
9453
+ `CLI discovery command for "${context.route.transport.id}" failed (${detail}); fell back to the static models[] catalog.`
9454
+ );
9262
9455
  }
9263
9456
  throw error;
9264
9457
  }
9265
9458
  if (models.length === 0) {
9266
9459
  if (source.fallback === "static") {
9267
- return buildStaticCliCatalog(context.route);
9460
+ return buildStaticCliCatalog(
9461
+ context.route,
9462
+ `CLI discovery command for "${context.route.transport.id}" returned no models; fell back to the static models[] catalog.`
9463
+ );
9268
9464
  }
9269
9465
  throw new AiConnectError(
9270
9466
  "temporary_unavailable",