@vedmalex/ai-connect 0.5.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.
package/README.md CHANGED
@@ -432,6 +432,65 @@ Current local transport scope:
432
432
  - `cli`: text generation, plus model discovery via a list `command`, a `static` config catalog, or an ACP sidecar
433
433
  - `server`: text generation plus provider-native model discovery
434
434
 
435
+ ### CLI File and Image Input
436
+
437
+ CLI routes can stage local attachment files into a temp directory and pass them to the subprocess as argv tokens or inline prompt references.
438
+
439
+ **Client-level staging** is configured under `cli.staging`:
440
+
441
+ ```ts
442
+ const client = createLocalClient(config, {
443
+ cli: {
444
+ staging: {
445
+ dir: "/tmp/my-staging", // default: os.tmpdir()
446
+ prefix: "ai-connect-", // temp dir name prefix
447
+ keep: true, // retain per-invocation temp dir (debug only)
448
+ },
449
+ },
450
+ });
451
+ ```
452
+
453
+ Staged files are written under `<stagingDir>/attachments/` with sanitized basenames (path-traversal safe) and removed after the call completes (unless `keep: true`). Attachments that carry only a remote URI and no bytes degrade to the raw URI reference.
454
+
455
+ **Per-route file input** is declared under `transport.cli.fileInput`:
456
+
457
+ ```ts
458
+ transport: {
459
+ kind: "cli",
460
+ id: "my-route",
461
+ cli: {
462
+ fileInput: {
463
+ placement: "args", // "args" (default) | "prompt"
464
+ perFileArgs: ["@{path}"], // argv tokens per file; {path} and {name} placeholders
465
+ // mentionTemplate: "@{path}", // prompt placement: inserted per file (default "@{path}")
466
+ // separator: " ",
467
+ categories: ["image", "document", "text", "other"], // accepted categories (default: all)
468
+ stagingDir: "/custom/dir", // per-route override of cli.staging.dir
469
+ },
470
+ },
471
+ }
472
+ ```
473
+
474
+ A `{files}` placeholder in `argsTemplate` expands to the full per-file argv block; it records a single telemetry key, not the staged absolute paths.
475
+
476
+ **Capability gate.** A route advertises `supportsImageInput` only when its `fileInput` stages the `image` category, and `supportsFileUpload` only when it stages the `document` category. A route that does not accept a category rejects the attachment with `unsupported_capability` at routing time, before any subprocess is spawned.
477
+
478
+ **Built-in preset behavior:**
479
+
480
+ - **`pi` preset** — accepts attachments from all categories (`image`, `document`, `text`, `other`). Each file is passed as an `@{path}` argv token (`perFileArgs: ["@{path}"]`). Pass an image attachment and `pi` receives `@/tmp/.../attachments/photo.png` as an argument. PDFs are staged as-is with no document extraction; `pi` receives raw bytes — prefer images or text files for reliable results.
481
+
482
+ ```ts
483
+ const result = await client.generate({
484
+ messages: [{ role: "user", content: "Describe this image." }],
485
+ attachments: ["/path/to/photo.png"],
486
+ // route is pi:cli:local or routeHints selects pi-cli
487
+ });
488
+ ```
489
+
490
+ - **`codex` preset** — accepts images only (`categories: ["image"]`). Each image is passed as `--image {path}` (`perFileArgs: ["--image", "{path}"]`). A non-image attachment (e.g. a PDF) on a codex route is rejected with `unsupported_capability` before any spawn.
491
+
492
+ - **`claude` / `openclaude` presets** — do not accept CLI file input by default. File input is opt-in: add an explicit `transport.cli.fileInput` block to your route config.
493
+
435
494
  ## Mock Gateway
436
495
 
437
496
  For API-level debugging you can run a local mock backend that behaves like a small OpenAI/Anthropic/Gemini proxy and captures the real finalized wire payloads:
@@ -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,
@@ -6339,112 +6525,6 @@ var AI_CONNECT_DEFAULT_ACP_COMMANDS = {
6339
6525
  "opencode:opencode-acp": "opencode acp"
6340
6526
  };
6341
6527
 
6342
- // src/cli-presets.ts
6343
- var AI_CONNECT_DEFAULT_CLI_PRESETS = {
6344
- pi: {
6345
- id: "pi",
6346
- label: "pi (coding agent)",
6347
- command: "pi",
6348
- transportId: "pi-cli",
6349
- options: {
6350
- preset: "pi",
6351
- // pi print mode emits the answer as plain text on stdout (no JSON wrapper),
6352
- // and has no ACP mode — so it uses the raw `text` parser and no discovery sidecar.
6353
- argsTemplate: ["--print", "--model", "{model}", "{prompt}"],
6354
- discovery: {
6355
- via: "none"
6356
- },
6357
- parser: {
6358
- kind: "text"
6359
- }
6360
- }
6361
- },
6362
- claude: {
6363
- id: "claude",
6364
- label: "Claude CLI",
6365
- command: "claude",
6366
- transportId: "claude-cli",
6367
- options: {
6368
- preset: "claude",
6369
- argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "{prompt}"],
6370
- discovery: {
6371
- via: "acp",
6372
- acp: {
6373
- transportId: "claude-code-acp"
6374
- }
6375
- },
6376
- parser: {
6377
- kind: "json",
6378
- textPath: "__preset__:claude.result",
6379
- usagePath: "usage",
6380
- errorPath: "__preset__:claude.error"
6381
- }
6382
- }
6383
- },
6384
- openclaude: {
6385
- id: "openclaude",
6386
- label: "OpenClaude CLI",
6387
- command: "openclaude",
6388
- transportId: "openclaude-cli",
6389
- options: {
6390
- preset: "openclaude",
6391
- argsTemplate: ["--print", "--output-format", "json", "--model", "{model}", "{prompt}"],
6392
- parser: {
6393
- kind: "json",
6394
- textPath: "__preset__:claude.result",
6395
- usagePath: "usage",
6396
- errorPath: "__preset__:claude.error"
6397
- }
6398
- }
6399
- },
6400
- codex: {
6401
- id: "codex",
6402
- label: "Codex CLI",
6403
- command: "codex",
6404
- transportId: "codex-cli",
6405
- options: {
6406
- preset: "codex",
6407
- argsTemplate: [
6408
- "exec",
6409
- "--json",
6410
- "--output-last-message",
6411
- "{output_file}",
6412
- "--model",
6413
- "{model}",
6414
- "{prompt}"
6415
- ],
6416
- discovery: {
6417
- via: "acp",
6418
- acp: {
6419
- transportId: "codex-acp"
6420
- }
6421
- },
6422
- parser: {
6423
- kind: "jsonl",
6424
- text: {
6425
- path: "__preset__:codex.text"
6426
- },
6427
- usage: {
6428
- path: "__preset__:codex.usage"
6429
- },
6430
- error: {
6431
- path: "__preset__:codex.error"
6432
- }
6433
- }
6434
- }
6435
- }
6436
- };
6437
- var AI_CONNECT_DEFAULT_CLI_COMMANDS = {
6438
- "anthropic:claude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.claude.command,
6439
- "anthropic:openclaude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.openclaude.command,
6440
- "openclaude:openclaude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.openclaude.command,
6441
- "openai:codex-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.codex.command,
6442
- "pi:pi-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.pi.command
6443
- };
6444
- function getCliTransportPreset(presetId) {
6445
- return AI_CONNECT_DEFAULT_CLI_PRESETS[presetId];
6446
- }
6447
-
6448
6528
  // src/server-presets.ts
6449
6529
  var AI_CONNECT_DEFAULT_SERVER_PRESETS = {
6450
6530
  opencode: {