@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/local.js CHANGED
@@ -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() ? {
@@ -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);
@@ -1785,6 +1975,8 @@ function fillConfiguredContextLength(route, models) {
1785
1975
  return {
1786
1976
  ...entry,
1787
1977
  contextLength: configured,
1978
+ // FT-002: typed provenance (canonical) + metadata alias (back-compat).
1979
+ contextWindowSource: "configured",
1788
1980
  metadata: { ...entry.metadata ?? {}, contextWindowSource: "configured" }
1789
1981
  };
1790
1982
  });
@@ -3114,7 +3306,8 @@ function createClient(configOrInput, options = {}) {
3114
3306
  ...catalog.canonicalModelId ? { canonicalModelId: catalog.canonicalModelId } : {},
3115
3307
  ...catalog.resolvedModelId ? { resolvedModelId: catalog.resolvedModelId } : {},
3116
3308
  ...catalog.currentModelId ? { currentModelId: catalog.currentModelId } : {},
3117
- availableModels: catalog.availableModels
3309
+ availableModels: catalog.availableModels,
3310
+ ...catalog.warnings?.length ? { warnings: catalog.warnings } : {}
3118
3311
  });
3119
3312
  wideAttempts.push({
3120
3313
  index: wideAttempts.length + 1,
@@ -4799,7 +4992,7 @@ function parseOpenAiModelCatalog(route, payload) {
4799
4992
  modelId: entry.id,
4800
4993
  name: typeof entry.display_name === "string" ? entry.display_name : typeof entry.name === "string" ? entry.name : entry.id,
4801
4994
  ...typeof entry.description === "string" ? { description: entry.description } : {},
4802
- ...contextLength !== void 0 ? { contextLength } : {},
4995
+ ...contextLength !== void 0 ? { contextLength, contextWindowSource: "discovered" } : {},
4803
4996
  ...pricing ? { pricing } : {},
4804
4997
  ...free ? { free } : {},
4805
4998
  ...metadata ? {
@@ -8175,114 +8368,6 @@ import { spawn as spawn2 } from "node:child_process";
8175
8368
  import fs2 from "node:fs/promises";
8176
8369
  import os2 from "node:os";
8177
8370
  import path2 from "node:path";
8178
-
8179
- // src/cli-presets.ts
8180
- var AI_CONNECT_DEFAULT_CLI_PRESETS = {
8181
- pi: {
8182
- id: "pi",
8183
- label: "pi (coding agent)",
8184
- command: "pi",
8185
- transportId: "pi-cli",
8186
- options: {
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}"],
8191
- discovery: {
8192
- via: "none"
8193
- },
8194
- parser: {
8195
- kind: "text"
8196
- }
8197
- }
8198
- },
8199
- claude: {
8200
- id: "claude",
8201
- label: "Claude CLI",
8202
- command: "claude",
8203
- transportId: "claude-cli",
8204
- options: {
8205
- preset: "claude",
8206
- argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "{prompt}"],
8207
- discovery: {
8208
- via: "acp",
8209
- acp: {
8210
- transportId: "claude-code-acp"
8211
- }
8212
- },
8213
- parser: {
8214
- kind: "json",
8215
- textPath: "__preset__:claude.result",
8216
- usagePath: "usage",
8217
- errorPath: "__preset__:claude.error"
8218
- }
8219
- }
8220
- },
8221
- openclaude: {
8222
- id: "openclaude",
8223
- label: "OpenClaude CLI",
8224
- command: "openclaude",
8225
- transportId: "openclaude-cli",
8226
- options: {
8227
- preset: "openclaude",
8228
- argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "{prompt}"],
8229
- parser: {
8230
- kind: "json",
8231
- textPath: "__preset__:claude.result",
8232
- usagePath: "usage",
8233
- errorPath: "__preset__:claude.error"
8234
- }
8235
- }
8236
- },
8237
- codex: {
8238
- id: "codex",
8239
- label: "Codex CLI",
8240
- command: "codex",
8241
- transportId: "codex-cli",
8242
- options: {
8243
- preset: "codex",
8244
- argsTemplate: [
8245
- "exec",
8246
- "--json",
8247
- "--output-last-message",
8248
- "{output_file}",
8249
- "--model",
8250
- "{model}",
8251
- "{prompt}"
8252
- ],
8253
- discovery: {
8254
- via: "acp",
8255
- acp: {
8256
- transportId: "codex-acp"
8257
- }
8258
- },
8259
- parser: {
8260
- kind: "jsonl",
8261
- text: {
8262
- path: "__preset__:codex.text"
8263
- },
8264
- usage: {
8265
- path: "__preset__:codex.usage"
8266
- },
8267
- error: {
8268
- path: "__preset__:codex.error"
8269
- }
8270
- }
8271
- }
8272
- }
8273
- };
8274
- var AI_CONNECT_DEFAULT_CLI_COMMANDS = {
8275
- "anthropic:claude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.claude.command,
8276
- "anthropic:openclaude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.openclaude.command,
8277
- "openclaude:openclaude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.openclaude.command,
8278
- "openai:codex-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.codex.command,
8279
- "pi:pi-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.pi.command
8280
- };
8281
- function getCliTransportPreset(presetId) {
8282
- return AI_CONNECT_DEFAULT_CLI_PRESETS[presetId];
8283
- }
8284
-
8285
- // src/cli.ts
8286
8371
  var CLI_PRESET_ACP_DISCOVERY_DEFAULTS = {
8287
8372
  claude: {
8288
8373
  transportId: "claude-code-acp",
@@ -8313,25 +8398,26 @@ function splitCommandLine2(commandLine) {
8313
8398
  function quoteArg(value) {
8314
8399
  return /\s/.test(value) ? JSON.stringify(value) : value;
8315
8400
  }
8316
- function buildCliPrompt(context) {
8401
+ function buildCliPrompt(context, fileInputEnabled) {
8317
8402
  if (context.request.operation !== "text") {
8318
8403
  throw new AiConnectError(
8319
8404
  "not_supported",
8320
8405
  `CLI transport "${context.route.transport.id}" only supports text requests.`
8321
8406
  );
8322
8407
  }
8323
- if (context.request.image || context.request.attachments.some(
8324
- (file) => portableFileCategory(file) === "image" || portableFileCategory(file) === "document"
8325
- )) {
8408
+ if (context.request.image) {
8326
8409
  throw new AiConnectError(
8327
8410
  "unsupported_capability",
8328
- `CLI transport "${context.route.transport.id}" does not support image or document inputs.`
8411
+ `CLI transport "${context.route.transport.id}" does not support image output.`
8329
8412
  );
8330
8413
  }
8331
- if (context.request.attachments.length > 0) {
8414
+ if (!fileInputEnabled && context.request.attachments.length > 0) {
8415
+ const hasBinary = context.request.attachments.some(
8416
+ (file) => portableFileCategory(file) === "image" || portableFileCategory(file) === "document"
8417
+ );
8332
8418
  throw new AiConnectError(
8333
- "not_supported",
8334
- `CLI transport "${context.route.transport.id}" does not support attachments.`
8419
+ hasBinary ? "unsupported_capability" : "not_supported",
8420
+ hasBinary ? `CLI transport "${context.route.transport.id}" does not support image or document inputs.` : `CLI transport "${context.route.transport.id}" does not support attachments.`
8335
8421
  );
8336
8422
  }
8337
8423
  return context.request.messages.map((message) => {
@@ -8476,11 +8562,16 @@ function createCliDiscoveryAcpRoute(route) {
8476
8562
  function resolveCliTransportOptions(route) {
8477
8563
  const presetId = route.transport.cli?.preset ?? defaultCliPresetIdForRoute(route);
8478
8564
  const preset = presetId ? getCliTransportPreset(presetId) : void 0;
8565
+ const mergedFileInput = preset?.options.fileInput || route.transport.cli?.fileInput ? {
8566
+ ...preset?.options.fileInput ?? {},
8567
+ ...route.transport.cli?.fileInput ?? {}
8568
+ } : void 0;
8479
8569
  const merged = {
8480
8570
  ...preset?.options ?? {},
8481
8571
  ...route.transport.cli ?? {},
8482
8572
  ...route.transport.cli?.argsTemplate ? { argsTemplate: route.transport.cli.argsTemplate } : {},
8483
8573
  ...route.transport.cli?.parser ? { parser: route.transport.cli.parser } : {},
8574
+ ...mergedFileInput ? { fileInput: mergedFileInput } : {},
8484
8575
  ...presetId ? { preset: presetId } : {}
8485
8576
  };
8486
8577
  if (!merged.argsTemplate?.length) {
@@ -8491,15 +8582,73 @@ function resolveCliTransportOptions(route) {
8491
8582
  }
8492
8583
  return merged;
8493
8584
  }
8494
- async function createOutputFile() {
8585
+ async function createCliTempDir(baseDir, prefix) {
8495
8586
  const tempDir = await fs2.mkdtemp(
8496
- path2.join(os2.tmpdir(), "ai-connect-cli-output-")
8587
+ path2.join(baseDir ?? os2.tmpdir(), prefix ?? "ai-connect-cli-")
8497
8588
  );
8498
8589
  return {
8499
8590
  tempDir,
8500
8591
  outputFile: path2.join(tempDir, "last-message.txt")
8501
8592
  };
8502
8593
  }
8594
+ function safeStagedBasename(name, index, mimeType) {
8595
+ const base = path2.basename(name.replace(/\\/g, "/"));
8596
+ if (base === "" || base === "." || base === "..") {
8597
+ return `attachment-${index}.${extensionForMime(mimeType)}`;
8598
+ }
8599
+ return base;
8600
+ }
8601
+ async function stageCliAttachments(attachments, attachDir, fileInput) {
8602
+ const categories = new Set(
8603
+ fileInput.categories ?? ["image", "document", "text", "other"]
8604
+ );
8605
+ await fs2.mkdir(attachDir, { recursive: true });
8606
+ const refs = [];
8607
+ const usedNames = /* @__PURE__ */ new Map();
8608
+ for (let i = 0; i < attachments.length; i += 1) {
8609
+ const file = preparePortableFile(attachments[i]);
8610
+ const payload = await materializePortableFile(file);
8611
+ if (!categories.has(payload.category)) {
8612
+ continue;
8613
+ }
8614
+ let destName = safeStagedBasename(payload.name, i, payload.mimeType);
8615
+ const seen = usedNames.get(destName);
8616
+ if (seen !== void 0) {
8617
+ destName = `${i}-${destName}`;
8618
+ }
8619
+ usedNames.set(destName, i);
8620
+ const dest = path2.join(attachDir, destName);
8621
+ if (payload.category === "text") {
8622
+ if (payload.text !== void 0) {
8623
+ await fs2.writeFile(dest, payload.text, "utf8");
8624
+ refs.push(dest);
8625
+ continue;
8626
+ }
8627
+ } else {
8628
+ const bytes = payload.base64 ? Buffer.from(payload.base64, "base64") : await portableFileToBytes(file);
8629
+ if (bytes !== void 0) {
8630
+ await fs2.writeFile(dest, bytes);
8631
+ refs.push(dest);
8632
+ continue;
8633
+ }
8634
+ }
8635
+ if (payload.uri) {
8636
+ refs.push(payload.uri);
8637
+ }
8638
+ }
8639
+ return refs;
8640
+ }
8641
+ function expandFileArgs(staged, fileInput) {
8642
+ const perFileArgs = fileInput?.perFileArgs ?? ["@{path}"];
8643
+ if (fileInput?.placement === "prompt") {
8644
+ return [];
8645
+ }
8646
+ return staged.flatMap(
8647
+ (ref) => perFileArgs.map(
8648
+ (tpl) => tpl.replace("{path}", ref).replace("{name}", path2.basename(ref))
8649
+ )
8650
+ );
8651
+ }
8503
8652
  function materializeTemplateArg(templateArg, values, parameterKeys) {
8504
8653
  switch (templateArg) {
8505
8654
  case "{prompt}":
@@ -8526,31 +8675,62 @@ function materializeTemplateArg(templateArg, values, parameterKeys) {
8526
8675
  }
8527
8676
  async function buildCliInvocation(context, options) {
8528
8677
  const route = context.route;
8529
- const prompt = buildCliPrompt(context);
8530
8678
  const cliOptions = resolveCliTransportOptions(route);
8679
+ const fileInput = cliOptions.fileInput;
8680
+ const attachments = context.request.attachments;
8681
+ const hasStageable = Boolean(fileInput) && attachments.length > 0;
8682
+ let prompt = buildCliPrompt(context, Boolean(fileInput));
8531
8683
  const commandLine = resolveCliCommand(route, options);
8532
8684
  const resolved = splitCommandLine2(commandLine);
8533
8685
  const parameterKeys = [];
8534
8686
  let tempDir;
8535
8687
  let outputFile;
8536
- if (cliOptions.argsTemplate.includes("{output_file}")) {
8537
- const created = await createOutputFile();
8688
+ const needsOutputFile = cliOptions.argsTemplate.includes("{output_file}");
8689
+ if (needsOutputFile || hasStageable) {
8690
+ const created = await createCliTempDir(
8691
+ fileInput?.stagingDir ?? options?.staging?.dir,
8692
+ options?.staging?.prefix
8693
+ );
8538
8694
  tempDir = created.tempDir;
8539
8695
  outputFile = created.outputFile;
8540
8696
  }
8697
+ let staged = [];
8698
+ if (hasStageable && tempDir && fileInput) {
8699
+ staged = await stageCliAttachments(
8700
+ attachments,
8701
+ path2.join(tempDir, "attachments"),
8702
+ fileInput
8703
+ );
8704
+ if (fileInput.placement === "prompt" && staged.length > 0) {
8705
+ const separator = fileInput.separator ?? " ";
8706
+ const mentionTemplate = fileInput.mentionTemplate ?? "@{path}";
8707
+ const mentions = staged.map(
8708
+ (ref) => mentionTemplate.replace("{path}", ref).replace("{name}", path2.basename(ref))
8709
+ ).join(separator);
8710
+ prompt = `${prompt}${separator}${mentions}`;
8711
+ }
8712
+ }
8541
8713
  const args = [
8542
8714
  ...resolved.args,
8543
- ...cliOptions.argsTemplate.map(
8544
- (part) => materializeTemplateArg(
8545
- part,
8546
- {
8547
- prompt,
8548
- model: route.model,
8549
- ...outputFile ? { outputFile } : {}
8550
- },
8551
- parameterKeys
8552
- )
8553
- )
8715
+ ...cliOptions.argsTemplate.flatMap((part) => {
8716
+ if (part === "{files}") {
8717
+ if (staged.length > 0) {
8718
+ parameterKeys.push("{files}");
8719
+ }
8720
+ return expandFileArgs(staged, fileInput);
8721
+ }
8722
+ return [
8723
+ materializeTemplateArg(
8724
+ part,
8725
+ {
8726
+ prompt,
8727
+ model: route.model,
8728
+ ...outputFile ? { outputFile } : {}
8729
+ },
8730
+ parameterKeys
8731
+ )
8732
+ ];
8733
+ })
8554
8734
  ];
8555
8735
  return {
8556
8736
  command: resolved.command,
@@ -8559,6 +8739,7 @@ async function buildCliInvocation(context, options) {
8559
8739
  env: buildCliEnvironment(options),
8560
8740
  ...tempDir ? { tempDir } : {},
8561
8741
  ...outputFile ? { outputFile } : {},
8742
+ ...options?.staging?.keep ? { keepStaging: true } : {},
8562
8743
  parameterKeys
8563
8744
  };
8564
8745
  }
@@ -8587,16 +8768,20 @@ function buildCliDiscoveryInvocation(route, command, options) {
8587
8768
  parameterKeys
8588
8769
  };
8589
8770
  }
8590
- function buildStaticCliCatalog(route) {
8771
+ function buildStaticCliCatalog(route, warning) {
8591
8772
  const models = route.advertisedModels.map((id) => ({
8592
8773
  modelId: id,
8593
8774
  name: id
8594
8775
  }));
8595
- return buildModelCatalog(
8776
+ const catalog = buildModelCatalog(
8596
8777
  route,
8597
8778
  models,
8598
8779
  currentModelIdForRoute(route, route.advertisedModels)
8599
8780
  );
8781
+ if (warning) {
8782
+ catalog.warnings = [...catalog.warnings ?? [], warning];
8783
+ }
8784
+ return catalog;
8600
8785
  }
8601
8786
  function statsToUsage(stats) {
8602
8787
  if (!stats || typeof stats !== "object") {
@@ -8739,7 +8924,9 @@ function modelInfoFromRecord(record, selector) {
8739
8924
  modelId,
8740
8925
  name,
8741
8926
  ...description ? { description } : {},
8742
- ...contextLength !== void 0 ? { contextLength } : {}
8927
+ // FT-002: a contextLength read from the list-command output is a discovered
8928
+ // value (consistent with the API parsers), so tag its provenance.
8929
+ ...contextLength !== void 0 ? { contextLength, contextWindowSource: "discovered" } : {}
8743
8930
  };
8744
8931
  }
8745
8932
  function parseCliModelList(stdout, parser, selector) {
@@ -8947,7 +9134,7 @@ async function executeCliInvocation(invocation, timeoutMs, phases, signal) {
8947
9134
  });
8948
9135
  }
8949
9136
  async function cleanupCliInvocation(invocation) {
8950
- if (invocation.tempDir) {
9137
+ if (invocation.tempDir && !invocation.keepStaging) {
8951
9138
  await fs2.rm(invocation.tempDir, { recursive: true, force: true });
8952
9139
  }
8953
9140
  }
@@ -9010,13 +9197,20 @@ function createCliTransportManager(options) {
9010
9197
  throw error;
9011
9198
  }
9012
9199
  if (source.fallback === "static") {
9013
- return buildStaticCliCatalog(context.route);
9200
+ const detail = error instanceof Error ? error.message : String(error);
9201
+ return buildStaticCliCatalog(
9202
+ context.route,
9203
+ `CLI discovery command for "${context.route.transport.id}" failed (${detail}); fell back to the static models[] catalog.`
9204
+ );
9014
9205
  }
9015
9206
  throw error;
9016
9207
  }
9017
9208
  if (models.length === 0) {
9018
9209
  if (source.fallback === "static") {
9019
- return buildStaticCliCatalog(context.route);
9210
+ return buildStaticCliCatalog(
9211
+ context.route,
9212
+ `CLI discovery command for "${context.route.transport.id}" returned no models; fell back to the static models[] catalog.`
9213
+ );
9020
9214
  }
9021
9215
  throw new AiConnectError(
9022
9216
  "temporary_unavailable",