@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/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);
@@ -8175,114 +8365,6 @@ import { spawn as spawn2 } from "node:child_process";
8175
8365
  import fs2 from "node:fs/promises";
8176
8366
  import os2 from "node:os";
8177
8367
  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
8368
  var CLI_PRESET_ACP_DISCOVERY_DEFAULTS = {
8287
8369
  claude: {
8288
8370
  transportId: "claude-code-acp",
@@ -8313,25 +8395,26 @@ function splitCommandLine2(commandLine) {
8313
8395
  function quoteArg(value) {
8314
8396
  return /\s/.test(value) ? JSON.stringify(value) : value;
8315
8397
  }
8316
- function buildCliPrompt(context) {
8398
+ function buildCliPrompt(context, fileInputEnabled) {
8317
8399
  if (context.request.operation !== "text") {
8318
8400
  throw new AiConnectError(
8319
8401
  "not_supported",
8320
8402
  `CLI transport "${context.route.transport.id}" only supports text requests.`
8321
8403
  );
8322
8404
  }
8323
- if (context.request.image || context.request.attachments.some(
8324
- (file) => portableFileCategory(file) === "image" || portableFileCategory(file) === "document"
8325
- )) {
8405
+ if (context.request.image) {
8326
8406
  throw new AiConnectError(
8327
8407
  "unsupported_capability",
8328
- `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.`
8329
8409
  );
8330
8410
  }
8331
- 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
+ );
8332
8415
  throw new AiConnectError(
8333
- "not_supported",
8334
- `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.`
8335
8418
  );
8336
8419
  }
8337
8420
  return context.request.messages.map((message) => {
@@ -8476,11 +8559,16 @@ function createCliDiscoveryAcpRoute(route) {
8476
8559
  function resolveCliTransportOptions(route) {
8477
8560
  const presetId = route.transport.cli?.preset ?? defaultCliPresetIdForRoute(route);
8478
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;
8479
8566
  const merged = {
8480
8567
  ...preset?.options ?? {},
8481
8568
  ...route.transport.cli ?? {},
8482
8569
  ...route.transport.cli?.argsTemplate ? { argsTemplate: route.transport.cli.argsTemplate } : {},
8483
8570
  ...route.transport.cli?.parser ? { parser: route.transport.cli.parser } : {},
8571
+ ...mergedFileInput ? { fileInput: mergedFileInput } : {},
8484
8572
  ...presetId ? { preset: presetId } : {}
8485
8573
  };
8486
8574
  if (!merged.argsTemplate?.length) {
@@ -8491,15 +8579,73 @@ function resolveCliTransportOptions(route) {
8491
8579
  }
8492
8580
  return merged;
8493
8581
  }
8494
- async function createOutputFile() {
8582
+ async function createCliTempDir(baseDir, prefix) {
8495
8583
  const tempDir = await fs2.mkdtemp(
8496
- path2.join(os2.tmpdir(), "ai-connect-cli-output-")
8584
+ path2.join(baseDir ?? os2.tmpdir(), prefix ?? "ai-connect-cli-")
8497
8585
  );
8498
8586
  return {
8499
8587
  tempDir,
8500
8588
  outputFile: path2.join(tempDir, "last-message.txt")
8501
8589
  };
8502
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
+ }
8503
8649
  function materializeTemplateArg(templateArg, values, parameterKeys) {
8504
8650
  switch (templateArg) {
8505
8651
  case "{prompt}":
@@ -8526,31 +8672,62 @@ function materializeTemplateArg(templateArg, values, parameterKeys) {
8526
8672
  }
8527
8673
  async function buildCliInvocation(context, options) {
8528
8674
  const route = context.route;
8529
- const prompt = buildCliPrompt(context);
8530
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));
8531
8680
  const commandLine = resolveCliCommand(route, options);
8532
8681
  const resolved = splitCommandLine2(commandLine);
8533
8682
  const parameterKeys = [];
8534
8683
  let tempDir;
8535
8684
  let outputFile;
8536
- if (cliOptions.argsTemplate.includes("{output_file}")) {
8537
- 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
+ );
8538
8691
  tempDir = created.tempDir;
8539
8692
  outputFile = created.outputFile;
8540
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
+ }
8541
8710
  const args = [
8542
8711
  ...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
- )
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
+ })
8554
8731
  ];
8555
8732
  return {
8556
8733
  command: resolved.command,
@@ -8559,6 +8736,7 @@ async function buildCliInvocation(context, options) {
8559
8736
  env: buildCliEnvironment(options),
8560
8737
  ...tempDir ? { tempDir } : {},
8561
8738
  ...outputFile ? { outputFile } : {},
8739
+ ...options?.staging?.keep ? { keepStaging: true } : {},
8562
8740
  parameterKeys
8563
8741
  };
8564
8742
  }
@@ -8947,7 +9125,7 @@ async function executeCliInvocation(invocation, timeoutMs, phases, signal) {
8947
9125
  });
8948
9126
  }
8949
9127
  async function cleanupCliInvocation(invocation) {
8950
- if (invocation.tempDir) {
9128
+ if (invocation.tempDir && !invocation.keepStaging) {
8951
9129
  await fs2.rm(invocation.tempDir, { recursive: true, force: true });
8952
9130
  }
8953
9131
  }