@yoqa/runner 0.3.17 → 0.3.19

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.
Files changed (2) hide show
  1. package/dist/index.js +773 -478
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -16801,7 +16801,7 @@ function getCatalogDb() {
16801
16801
  // package.json
16802
16802
  var package_default = {
16803
16803
  name: "@yoqa/runner",
16804
- version: "0.3.17",
16804
+ version: "0.3.19",
16805
16805
  description: "Yoqa local runner \u2014 Appium + catalog HTTP API (Bun). Start with yoqa serve or the yoqa-runner bin.",
16806
16806
  license: "UNLICENSED",
16807
16807
  type: "module",
@@ -16896,11 +16896,458 @@ function abortAllMjpegProxies() {
16896
16896
  import { mkdir as mkdir3 } from "fs/promises";
16897
16897
  import { createServer as createServer2 } from "net";
16898
16898
  import { homedir as homedir6 } from "os";
16899
- import { join as join6 } from "path";
16899
+ import { join as join7 } from "path";
16900
16900
  import { remote } from "webdriverio";
16901
- var YOQA_ROOT4 = join6(homedir6(), ".yoqa");
16901
+
16902
+ // src/domains/devices/application.ts
16903
+ import { mkdtemp as mkdtemp2, readFile, rm as rm2 } from "fs/promises";
16904
+ import { tmpdir as tmpdir2 } from "os";
16905
+ import { join as join6 } from "path";
16906
+
16907
+ // src/domains/devices/android-identity.ts
16908
+ function matchAndroidAppiumIdentity(deviceId, connected, knownAvds = []) {
16909
+ const id = deviceId.trim();
16910
+ if (!id)
16911
+ return {};
16912
+ const bySerial = connected.find((device) => device.serial === id);
16913
+ if (bySerial) {
16914
+ return {
16915
+ udid: bySerial.serial,
16916
+ ...bySerial.avdName ? { avd: bySerial.avdName } : {}
16917
+ };
16918
+ }
16919
+ const byAvd = connected.find((device) => device.avdName === id);
16920
+ if (byAvd) {
16921
+ return { udid: byAvd.serial, avd: byAvd.avdName ?? id };
16922
+ }
16923
+ if (knownAvds.includes(id)) {
16924
+ return { avd: id };
16925
+ }
16926
+ return { udid: id };
16927
+ }
16928
+ function parseAdbEmuAvdName(stdout) {
16929
+ const name = stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line && line !== "OK");
16930
+ return name || null;
16931
+ }
16932
+
16933
+ // src/domains/devices/application.ts
16934
+ async function runCommand3(command, options) {
16935
+ try {
16936
+ const proc = Bun.spawn(command, {
16937
+ env: options?.env ? { ...process.env, ...options.env } : process.env,
16938
+ stdout: "pipe",
16939
+ stderr: "pipe"
16940
+ });
16941
+ const [stdout, stderr, exitCode] = await Promise.all([
16942
+ new Response(proc.stdout).text(),
16943
+ new Response(proc.stderr).text(),
16944
+ proc.exited
16945
+ ]);
16946
+ return { stdout, stderr, exitCode };
16947
+ } catch {
16948
+ return { stdout: "", stderr: `failed to spawn: ${command[0]}`, exitCode: 127 };
16949
+ }
16950
+ }
16951
+ function androidSdkRoot() {
16952
+ return resolveAndroidSdkRoot();
16953
+ }
16954
+ async function resolveTool(candidates) {
16955
+ for (const candidate of candidates) {
16956
+ if (!candidate)
16957
+ continue;
16958
+ const { exitCode } = await runCommand3(["test", "-x", candidate]);
16959
+ if (exitCode === 0)
16960
+ return candidate;
16961
+ }
16962
+ return null;
16963
+ }
16964
+ async function resolveAdb() {
16965
+ const sdk = androidSdkRoot();
16966
+ return resolveTool([...sdk ? [join6(sdk, "platform-tools/adb")] : [], "adb"]);
16967
+ }
16968
+ async function resolveEmulator() {
16969
+ const sdk = androidSdkRoot();
16970
+ return resolveTool([...sdk ? [join6(sdk, "emulator/emulator")] : [], "emulator"]);
16971
+ }
16972
+ function formatIosVersion(version2) {
16973
+ const trimmed = version2.trim();
16974
+ if (!trimmed)
16975
+ return "iOS";
16976
+ if (/^iOS\b/i.test(trimmed))
16977
+ return trimmed;
16978
+ return `iOS ${trimmed}`;
16979
+ }
16980
+ function formatAndroidVersion(apiOrVersion) {
16981
+ const trimmed = apiOrVersion.trim();
16982
+ if (!trimmed)
16983
+ return "Android";
16984
+ if (/^Android\b/i.test(trimmed))
16985
+ return trimmed;
16986
+ if (/^API\b/i.test(trimmed))
16987
+ return trimmed;
16988
+ if (/^\d+(\.\d+)?$/.test(trimmed))
16989
+ return `API ${trimmed}`;
16990
+ return `Android ${trimmed}`;
16991
+ }
16992
+ function titleCaseWords(value) {
16993
+ return value.split(/\s+/).filter(Boolean).map((word) => {
16994
+ if (/^[a-z]$/i.test(word))
16995
+ return word.toUpperCase();
16996
+ if (/^\d/.test(word))
16997
+ return word;
16998
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
16999
+ }).join(" ");
17000
+ }
17001
+ function runtimeVersionFromIdentifier(runtimeId) {
17002
+ const match = runtimeId.match(/iOS-([\d-]+)$/i);
17003
+ if (!match?.[1])
17004
+ return "iOS";
17005
+ return formatIosVersion(match[1].replace(/-/g, "."));
17006
+ }
17007
+ async function listIosSimulators(includeUnavailable) {
17008
+ const { stdout, exitCode } = await runCommand3([
17009
+ "xcrun",
17010
+ "simctl",
17011
+ "list",
17012
+ "devices",
17013
+ "available",
17014
+ "-j"
17015
+ ]);
17016
+ if (exitCode !== 0)
17017
+ return [];
17018
+ let parsed;
17019
+ try {
17020
+ parsed = JSON.parse(stdout);
17021
+ } catch {
17022
+ return [];
17023
+ }
17024
+ const devices = [];
17025
+ for (const [runtimeId, runtimeDevices] of Object.entries(parsed.devices ?? {})) {
17026
+ if (!/iOS/i.test(runtimeId))
17027
+ continue;
17028
+ const osVersion = runtimeVersionFromIdentifier(runtimeId);
17029
+ for (const device of runtimeDevices) {
17030
+ if (!device.udid || !device.name)
17031
+ continue;
17032
+ if (device.isAvailable === false)
17033
+ continue;
17034
+ const state = device.state ?? "Unknown";
17035
+ if (!includeUnavailable && state !== "Booted")
17036
+ continue;
17037
+ devices.push({
17038
+ id: device.udid,
17039
+ name: device.name,
17040
+ osVersion,
17041
+ platform: "ios",
17042
+ kind: "simulator",
17043
+ state,
17044
+ model: device.deviceTypeIdentifier?.split(".").pop()?.replace(/-/g, " ")
17045
+ });
17046
+ }
17047
+ }
17048
+ return devices.sort((a, b) => {
17049
+ if (a.state === "Booted" && b.state !== "Booted")
17050
+ return -1;
17051
+ if (b.state === "Booted" && a.state !== "Booted")
17052
+ return 1;
17053
+ return a.name.localeCompare(b.name);
17054
+ });
17055
+ }
17056
+ function isPhoneOrTablet(deviceType) {
17057
+ if (!deviceType)
17058
+ return false;
17059
+ return /^(iPhone|iPad)$/i.test(deviceType);
17060
+ }
17061
+ function physicalIosState(connection) {
17062
+ const tunnel = connection?.tunnelState?.toLowerCase() ?? "";
17063
+ if (tunnel.includes("connected"))
17064
+ return "connected";
17065
+ if (tunnel.includes("unavailable"))
17066
+ return "unavailable";
17067
+ if (tunnel.includes("disconnected"))
17068
+ return "disconnected";
17069
+ if (connection?.pairingState === "paired")
17070
+ return "paired";
17071
+ return tunnel || connection?.pairingState || "unknown";
17072
+ }
17073
+ async function listIosPhysicalDevices(includeUnavailable) {
17074
+ const dir = await mkdtemp2(join6(tmpdir2(), "yoqa-devices-"));
17075
+ const jsonPath = join6(dir, "devices.json");
17076
+ try {
17077
+ const { exitCode } = await runCommand3([
17078
+ "xcrun",
17079
+ "devicectl",
17080
+ "list",
17081
+ "devices",
17082
+ "--json-output",
17083
+ jsonPath
17084
+ ]);
17085
+ if (exitCode !== 0)
17086
+ return [];
17087
+ let parsed;
17088
+ try {
17089
+ parsed = JSON.parse(await readFile(jsonPath, "utf8"));
17090
+ } catch {
17091
+ return [];
17092
+ }
17093
+ const devices = [];
17094
+ for (const item of parsed.result?.devices ?? []) {
17095
+ const hardware = item.hardwareProperties;
17096
+ const props = item.deviceProperties;
17097
+ if (!hardware?.udid)
17098
+ continue;
17099
+ if (hardware.reality && hardware.reality !== "physical")
17100
+ continue;
17101
+ if (!isPhoneOrTablet(hardware.deviceType))
17102
+ continue;
17103
+ if (hardware.platform && !/^iOS$/i.test(hardware.platform))
17104
+ continue;
17105
+ const state = physicalIosState(item.connectionProperties);
17106
+ if (!includeUnavailable && state === "unavailable")
17107
+ continue;
17108
+ const modelName = hardware.marketingName ?? hardware.productType ?? "iPhone";
17109
+ const owner = props?.name?.trim() || undefined;
17110
+ devices.push({
17111
+ id: hardware.udid,
17112
+ name: modelName,
17113
+ owner,
17114
+ osVersion: formatIosVersion(props?.osVersionNumber ?? ""),
17115
+ platform: "ios",
17116
+ kind: "physical",
17117
+ state,
17118
+ model: hardware.productType
17119
+ });
17120
+ }
17121
+ return devices.sort((a, b) => a.name.localeCompare(b.name));
17122
+ } finally {
17123
+ await rm2(dir, { recursive: true, force: true });
17124
+ }
17125
+ }
17126
+ async function listIosDevices(options = {}) {
17127
+ const includeUnavailable = options.includeUnavailable ?? true;
17128
+ const [physical, simulators] = await Promise.all([
17129
+ listIosPhysicalDevices(includeUnavailable),
17130
+ listIosSimulators(includeUnavailable)
17131
+ ]);
17132
+ return [...physical, ...simulators];
17133
+ }
17134
+ function parseAdbDevices(output) {
17135
+ const lines = output.split(`
17136
+ `).map((line) => line.trim()).filter(Boolean);
17137
+ const results = [];
17138
+ for (const line of lines) {
17139
+ if (line.startsWith("List of devices"))
17140
+ continue;
17141
+ const parts = line.split(/\s+/);
17142
+ const serial = parts[0];
17143
+ const state = parts[1];
17144
+ if (!serial || !state)
17145
+ continue;
17146
+ const meta = {};
17147
+ for (const part of parts.slice(2)) {
17148
+ const eq2 = part.indexOf(":");
17149
+ if (eq2 <= 0)
17150
+ continue;
17151
+ meta[part.slice(0, eq2)] = part.slice(eq2 + 1);
17152
+ }
17153
+ results.push({
17154
+ serial,
17155
+ state,
17156
+ product: meta.product,
17157
+ model: meta.model,
17158
+ device: meta.device,
17159
+ transportId: meta.transport_id
17160
+ });
17161
+ }
17162
+ return results;
17163
+ }
17164
+ function humanizeAvdName(avdName) {
17165
+ return titleCaseWords(avdName.replace(/_/g, " "));
17166
+ }
17167
+ function humanizeModel(model) {
17168
+ if (!model)
17169
+ return;
17170
+ return titleCaseWords(model.replace(/_/g, " "));
17171
+ }
17172
+ async function findAvdConfigPath(avdName) {
17173
+ const home = process.env.HOME ?? "";
17174
+ const roots = [join6(home, ".android/avd"), join6(home, "Library/Android/sdk/.android/avd")];
17175
+ for (const root of roots) {
17176
+ const direct = join6(root, `${avdName}.avd`, "config.ini");
17177
+ try {
17178
+ await readFile(direct, "utf8");
17179
+ return direct;
17180
+ } catch {}
17181
+ try {
17182
+ const glob = new Bun.Glob("*.avd/config.ini");
17183
+ for await (const match of glob.scan({ cwd: root, onlyFiles: true })) {
17184
+ const path = join6(root, match);
17185
+ const text2 = await readFile(path, "utf8");
17186
+ const idLine = text2.split(`
17187
+ `).find((line) => line.startsWith("AvdId="));
17188
+ const id = idLine?.slice("AvdId=".length).trim();
17189
+ if (id === avdName)
17190
+ return path;
17191
+ }
17192
+ } catch {}
17193
+ }
17194
+ return null;
17195
+ }
17196
+ async function readAvdConfig(avdName) {
17197
+ const path = await findAvdConfigPath(avdName);
17198
+ if (!path)
17199
+ return { api: null, deviceName: null };
17200
+ try {
17201
+ const text2 = await readFile(path, "utf8");
17202
+ const imageLine = text2.split(`
17203
+ `).find((line) => line.includes("system-images") && line.includes("android-"));
17204
+ const apiMatch = imageLine?.match(/android-([\d.]+)/);
17205
+ const deviceLine = text2.split(`
17206
+ `).find((entry) => entry.startsWith("hw.device.name="));
17207
+ const deviceRaw = deviceLine?.slice("hw.device.name=".length).trim();
17208
+ return {
17209
+ api: apiMatch?.[1] ?? null,
17210
+ deviceName: humanizeModel(deviceRaw) ?? null
17211
+ };
17212
+ } catch {
17213
+ return { api: null, deviceName: null };
17214
+ }
17215
+ }
17216
+ async function adbAvdName(adb, serial) {
17217
+ const fromProp = await adbGetProp(adb, serial, "ro.boot.qemu.avd_name") ?? await adbGetProp(adb, serial, "ro.kernel.qemu.avd_name");
17218
+ if (fromProp)
17219
+ return fromProp;
17220
+ const { stdout, exitCode } = await runCommand3([adb, "-s", serial, "emu", "avd", "name"]);
17221
+ if (exitCode !== 0)
17222
+ return null;
17223
+ return parseAdbEmuAvdName(stdout);
17224
+ }
17225
+ async function adbGetProp(adb, serial, prop) {
17226
+ const { stdout, exitCode } = await runCommand3([adb, "-s", serial, "shell", "getprop", prop]);
17227
+ if (exitCode !== 0)
17228
+ return null;
17229
+ const value = stdout.trim();
17230
+ return value || null;
17231
+ }
17232
+ async function listAndroidAdbDevices(adb) {
17233
+ const { stdout, exitCode } = await runCommand3([adb, "devices", "-l"]);
17234
+ if (exitCode !== 0) {
17235
+ return { physical: [], runningEmulators: [], runningAvdNames: new Set };
17236
+ }
17237
+ const entries = parseAdbDevices(stdout);
17238
+ const physical = [];
17239
+ const runningEmulators = [];
17240
+ const runningAvdNames = new Set;
17241
+ for (const entry of entries) {
17242
+ const isEmulator = entry.serial.startsWith("emulator-") || entry.product === "sdk_gphone";
17243
+ if (isEmulator) {
17244
+ const avdName = await adbAvdName(adb, entry.serial);
17245
+ if (avdName)
17246
+ runningAvdNames.add(avdName);
17247
+ const avdConfig = avdName ? await readAvdConfig(avdName) : null;
17248
+ const release2 = await adbGetProp(adb, entry.serial, "ro.build.version.release") ?? avdConfig?.api ?? null;
17249
+ const model2 = humanizeModel(entry.model) ?? avdConfig?.deviceName ?? (avdName ? humanizeAvdName(avdName) : entry.serial);
17250
+ runningEmulators.push({
17251
+ id: avdName ?? entry.serial,
17252
+ name: model2,
17253
+ osVersion: formatAndroidVersion(release2 ?? ""),
17254
+ platform: "android",
17255
+ kind: "emulator",
17256
+ state: entry.state === "device" ? "online" : entry.state,
17257
+ model: avdName ?? entry.model
17258
+ });
17259
+ continue;
17260
+ }
17261
+ const release = await adbGetProp(adb, entry.serial, "ro.build.version.release");
17262
+ const model = humanizeModel(entry.model) ?? await adbGetProp(adb, entry.serial, "ro.product.model") ?? entry.serial;
17263
+ physical.push({
17264
+ id: entry.serial,
17265
+ name: model,
17266
+ osVersion: formatAndroidVersion(release ?? ""),
17267
+ platform: "android",
17268
+ kind: "physical",
17269
+ state: entry.state,
17270
+ model: entry.model
17271
+ });
17272
+ }
17273
+ return { physical, runningEmulators, runningAvdNames };
17274
+ }
17275
+ async function listAndroidAvds(emulatorBin, runningAvdNames, includeUnavailable) {
17276
+ const { stdout, exitCode } = await runCommand3([emulatorBin, "-list-avds"]);
17277
+ if (exitCode !== 0)
17278
+ return [];
17279
+ const names = stdout.split(`
17280
+ `).map((line) => line.trim()).filter(Boolean);
17281
+ const devices = [];
17282
+ for (const avdName of names) {
17283
+ const isRunning = runningAvdNames.has(avdName);
17284
+ if (!includeUnavailable && !isRunning)
17285
+ continue;
17286
+ if (isRunning)
17287
+ continue;
17288
+ const { api, deviceName } = await readAvdConfig(avdName);
17289
+ devices.push({
17290
+ id: avdName,
17291
+ name: deviceName ?? humanizeAvdName(avdName),
17292
+ osVersion: formatAndroidVersion(api ?? ""),
17293
+ platform: "android",
17294
+ kind: "emulator",
17295
+ state: "offline",
17296
+ model: avdName
17297
+ });
17298
+ }
17299
+ return devices;
17300
+ }
17301
+ async function resolveAndroidAppiumIdentity(deviceId) {
17302
+ const [adb, emulatorBin] = await Promise.all([resolveAdb(), resolveEmulator()]);
17303
+ const connected = [];
17304
+ if (adb) {
17305
+ const { stdout, exitCode } = await runCommand3([adb, "devices", "-l"]);
17306
+ if (exitCode === 0) {
17307
+ for (const entry of parseAdbDevices(stdout)) {
17308
+ const isEmulator = entry.serial.startsWith("emulator-") || entry.product === "sdk_gphone";
17309
+ connected.push({
17310
+ serial: entry.serial,
17311
+ avdName: isEmulator ? await adbAvdName(adb, entry.serial) : null
17312
+ });
17313
+ }
17314
+ }
17315
+ }
17316
+ const knownAvds = [];
17317
+ if (emulatorBin) {
17318
+ const { stdout, exitCode } = await runCommand3([emulatorBin, "-list-avds"]);
17319
+ if (exitCode === 0) {
17320
+ knownAvds.push(...stdout.split(`
17321
+ `).map((line) => line.trim()).filter(Boolean));
17322
+ }
17323
+ }
17324
+ return matchAndroidAppiumIdentity(deviceId, connected, knownAvds);
17325
+ }
17326
+ async function listAndroidDevices(options = {}) {
17327
+ const includeUnavailable = options.includeUnavailable ?? true;
17328
+ const [adb, emulatorBin] = await Promise.all([resolveAdb(), resolveEmulator()]);
17329
+ const adbResult = adb ? await listAndroidAdbDevices(adb) : { physical: [], runningEmulators: [], runningAvdNames: new Set };
17330
+ const offlineAvds = emulatorBin != null ? await listAndroidAvds(emulatorBin, adbResult.runningAvdNames, includeUnavailable) : [];
17331
+ const physical = includeUnavailable ? adbResult.physical : adbResult.physical.filter((device) => device.state === "device");
17332
+ const emulators = [...adbResult.runningEmulators, ...offlineAvds].sort((a, b) => {
17333
+ const aOnline = a.state === "online" ? 0 : 1;
17334
+ const bOnline = b.state === "online" ? 0 : 1;
17335
+ if (aOnline !== bOnline)
17336
+ return aOnline - bOnline;
17337
+ return a.name.localeCompare(b.name);
17338
+ });
17339
+ return [...physical, ...emulators];
17340
+ }
17341
+ async function listDevices(platform, options = {}) {
17342
+ if (platform === "ios")
17343
+ return listIosDevices(options);
17344
+ return listAndroidDevices(options);
17345
+ }
17346
+
17347
+ // src/domains/devices/session.ts
17348
+ var YOQA_ROOT4 = join7(homedir6(), ".yoqa");
16902
17349
  var DEFAULT_MJPEG_PORT = Number(process.env.YOQA_MJPEG_PORT ?? "9100");
16903
- var SCREENSHOT_DIR = join6(YOQA_ROOT4, "runs", "screenshots");
17350
+ var SCREENSHOT_DIR = join7(YOQA_ROOT4, "runs", "screenshots");
16904
17351
  var MJPEG_SETTINGS_BASE = {
16905
17352
  mjpegServerScreenshotQuality: 35,
16906
17353
  mjpegScalingFactor: 50
@@ -17022,7 +17469,7 @@ function looksLikePhysicalIosUdid(udid) {
17022
17469
  }
17023
17470
  var PHYSICAL_IOS_SESSION_TIMEOUT_MS = 60000;
17024
17471
  var SIMULATOR_WDA_SESSION_TIMEOUT_MS = 600000;
17025
- var SIMULATOR_WDA_DERIVED_DATA = join6(YOQA_ROOT4, "wda-sim");
17472
+ var SIMULATOR_WDA_DERIVED_DATA = join7(YOQA_ROOT4, "wda-sim");
17026
17473
  function iosSessionCreateTimeoutMs(deviceId) {
17027
17474
  return looksLikePhysicalIosUdid(deviceId) ? PHYSICAL_IOS_SESSION_TIMEOUT_MS : SIMULATOR_WDA_SESSION_TIMEOUT_MS;
17028
17475
  }
@@ -17033,18 +17480,17 @@ async function buildW3cCapabilities(options, mjpegPort) {
17033
17480
  const caps = {
17034
17481
  platformName,
17035
17482
  "appium:automationName": automationName,
17036
- "appium:udid": options.deviceId,
17037
17483
  "appium:newCommandTimeout": 3600,
17038
17484
  "appium:mjpegServerPort": mjpegPort,
17039
17485
  ...Object.fromEntries(Object.entries(merged).map(([key, value]) => key.includes(":") ? [key, value] : [`appium:${key}`, value]))
17040
17486
  };
17041
- if (options.platform === "ios" && options.bundleId && !caps["appium:bundleId"]) {
17042
- caps["appium:bundleId"] = options.bundleId;
17043
- }
17044
- if (options.platform === "android" && options.appPackage && !caps["appium:appPackage"]) {
17045
- caps["appium:appPackage"] = options.appPackage;
17046
- }
17047
17487
  if (options.platform === "ios") {
17488
+ if (caps["appium:udid"] === undefined) {
17489
+ caps["appium:udid"] = options.deviceId;
17490
+ }
17491
+ if (options.bundleId && !caps["appium:bundleId"]) {
17492
+ caps["appium:bundleId"] = options.bundleId;
17493
+ }
17048
17494
  const physical = looksLikePhysicalIosUdid(options.deviceId);
17049
17495
  if (physical) {
17050
17496
  const prep = await loadDevicePrep(options.deviceId);
@@ -17079,6 +17525,19 @@ async function buildW3cCapabilities(options, mjpegPort) {
17079
17525
  caps["appium:derivedDataPath"] = SIMULATOR_WDA_DERIVED_DATA;
17080
17526
  }
17081
17527
  }
17528
+ if (options.platform === "android") {
17529
+ const requested = String(caps["appium:udid"] ?? options.deviceId);
17530
+ const identity = await resolveAndroidAppiumIdentity(requested);
17531
+ if (identity.udid) {
17532
+ caps["appium:udid"] = identity.udid;
17533
+ }
17534
+ if (identity.avd && caps["appium:avd"] === undefined) {
17535
+ caps["appium:avd"] = identity.avd;
17536
+ }
17537
+ if (options.appPackage && !caps["appium:appPackage"]) {
17538
+ caps["appium:appPackage"] = options.appPackage;
17539
+ }
17540
+ }
17082
17541
  return caps;
17083
17542
  }
17084
17543
 
@@ -17369,7 +17828,7 @@ async function createDeviceSession(options) {
17369
17828
  const screenshot = async () => {
17370
17829
  await mkdir3(SCREENSHOT_DIR, { recursive: true });
17371
17830
  const frame = await captureFrame();
17372
- const path = join6(SCREENSHOT_DIR, `shot_${Date.now()}_${crypto.randomUUID()}.png`);
17831
+ const path = join7(SCREENSHOT_DIR, `shot_${Date.now()}_${crypto.randomUUID()}.png`);
17373
17832
  await Bun.write(path, Uint8Array.from(Buffer.from(frame.base64, "base64")));
17374
17833
  return { path, base64: frame.base64 };
17375
17834
  };
@@ -23799,7 +24258,7 @@ var probeProviderResponseSchema = exports_external.object({
23799
24258
  var providerModelSchema = exports_external.object({
23800
24259
  id: exports_external.string().min(1),
23801
24260
  name: exports_external.string().min(1),
23802
- tier: exports_external.enum(["free", "paid"]).optional()
24261
+ provider: exports_external.string().min(1).optional()
23803
24262
  });
23804
24263
  var listProviderModelsResponseSchema = exports_external.object({
23805
24264
  models: exports_external.array(providerModelSchema),
@@ -24193,8 +24652,13 @@ async function installBuildOnDevice(options) {
24193
24652
  }
24194
24653
  throw new Error(`Cannot install iOS build at ${build.path}. Use a .app on simulator or install .ipa via ideviceinstaller.`);
24195
24654
  }
24655
+ const identity = await resolveAndroidAppiumIdentity(deviceId);
24656
+ const serial = identity.udid;
24657
+ if (!serial) {
24658
+ throw new Error(`Android emulator ${deviceId} is not running. Boot it, then retry the install.`);
24659
+ }
24196
24660
  const adb = Bun.which("adb") ?? "adb";
24197
- const proc = Bun.spawn([adb, "-s", deviceId, "install", "-r", build.path], {
24661
+ const proc = Bun.spawn([adb, "-s", serial, "install", "-r", build.path], {
24198
24662
  stdout: "pipe",
24199
24663
  stderr: "pipe"
24200
24664
  });
@@ -25017,397 +25481,6 @@ function createCatalogRoutes() {
25017
25481
  return app;
25018
25482
  }
25019
25483
 
25020
- // src/domains/devices/application.ts
25021
- import { mkdtemp as mkdtemp2, readFile, rm as rm2 } from "fs/promises";
25022
- import { tmpdir as tmpdir2 } from "os";
25023
- import { join as join7 } from "path";
25024
- async function runCommand3(command, options) {
25025
- try {
25026
- const proc = Bun.spawn(command, {
25027
- env: options?.env ? { ...process.env, ...options.env } : process.env,
25028
- stdout: "pipe",
25029
- stderr: "pipe"
25030
- });
25031
- const [stdout, stderr, exitCode] = await Promise.all([
25032
- new Response(proc.stdout).text(),
25033
- new Response(proc.stderr).text(),
25034
- proc.exited
25035
- ]);
25036
- return { stdout, stderr, exitCode };
25037
- } catch {
25038
- return { stdout: "", stderr: `failed to spawn: ${command[0]}`, exitCode: 127 };
25039
- }
25040
- }
25041
- function androidSdkRoot() {
25042
- return resolveAndroidSdkRoot();
25043
- }
25044
- async function resolveTool(candidates) {
25045
- for (const candidate of candidates) {
25046
- if (!candidate)
25047
- continue;
25048
- const { exitCode } = await runCommand3(["test", "-x", candidate]);
25049
- if (exitCode === 0)
25050
- return candidate;
25051
- }
25052
- return null;
25053
- }
25054
- async function resolveAdb() {
25055
- const sdk = androidSdkRoot();
25056
- return resolveTool([...sdk ? [join7(sdk, "platform-tools/adb")] : [], "adb"]);
25057
- }
25058
- async function resolveEmulator() {
25059
- const sdk = androidSdkRoot();
25060
- return resolveTool([...sdk ? [join7(sdk, "emulator/emulator")] : [], "emulator"]);
25061
- }
25062
- function formatIosVersion(version2) {
25063
- const trimmed = version2.trim();
25064
- if (!trimmed)
25065
- return "iOS";
25066
- if (/^iOS\b/i.test(trimmed))
25067
- return trimmed;
25068
- return `iOS ${trimmed}`;
25069
- }
25070
- function formatAndroidVersion(apiOrVersion) {
25071
- const trimmed = apiOrVersion.trim();
25072
- if (!trimmed)
25073
- return "Android";
25074
- if (/^Android\b/i.test(trimmed))
25075
- return trimmed;
25076
- if (/^API\b/i.test(trimmed))
25077
- return trimmed;
25078
- if (/^\d+(\.\d+)?$/.test(trimmed))
25079
- return `API ${trimmed}`;
25080
- return `Android ${trimmed}`;
25081
- }
25082
- function titleCaseWords(value) {
25083
- return value.split(/\s+/).filter(Boolean).map((word) => {
25084
- if (/^[a-z]$/i.test(word))
25085
- return word.toUpperCase();
25086
- if (/^\d/.test(word))
25087
- return word;
25088
- return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
25089
- }).join(" ");
25090
- }
25091
- function runtimeVersionFromIdentifier(runtimeId) {
25092
- const match2 = runtimeId.match(/iOS-([\d-]+)$/i);
25093
- if (!match2?.[1])
25094
- return "iOS";
25095
- return formatIosVersion(match2[1].replace(/-/g, "."));
25096
- }
25097
- async function listIosSimulators(includeUnavailable) {
25098
- const { stdout, exitCode } = await runCommand3([
25099
- "xcrun",
25100
- "simctl",
25101
- "list",
25102
- "devices",
25103
- "available",
25104
- "-j"
25105
- ]);
25106
- if (exitCode !== 0)
25107
- return [];
25108
- let parsed;
25109
- try {
25110
- parsed = JSON.parse(stdout);
25111
- } catch {
25112
- return [];
25113
- }
25114
- const devices = [];
25115
- for (const [runtimeId, runtimeDevices] of Object.entries(parsed.devices ?? {})) {
25116
- if (!/iOS/i.test(runtimeId))
25117
- continue;
25118
- const osVersion = runtimeVersionFromIdentifier(runtimeId);
25119
- for (const device of runtimeDevices) {
25120
- if (!device.udid || !device.name)
25121
- continue;
25122
- if (device.isAvailable === false)
25123
- continue;
25124
- const state = device.state ?? "Unknown";
25125
- if (!includeUnavailable && state !== "Booted")
25126
- continue;
25127
- devices.push({
25128
- id: device.udid,
25129
- name: device.name,
25130
- osVersion,
25131
- platform: "ios",
25132
- kind: "simulator",
25133
- state,
25134
- model: device.deviceTypeIdentifier?.split(".").pop()?.replace(/-/g, " ")
25135
- });
25136
- }
25137
- }
25138
- return devices.sort((a, b) => {
25139
- if (a.state === "Booted" && b.state !== "Booted")
25140
- return -1;
25141
- if (b.state === "Booted" && a.state !== "Booted")
25142
- return 1;
25143
- return a.name.localeCompare(b.name);
25144
- });
25145
- }
25146
- function isPhoneOrTablet(deviceType) {
25147
- if (!deviceType)
25148
- return false;
25149
- return /^(iPhone|iPad)$/i.test(deviceType);
25150
- }
25151
- function physicalIosState(connection) {
25152
- const tunnel = connection?.tunnelState?.toLowerCase() ?? "";
25153
- if (tunnel.includes("connected"))
25154
- return "connected";
25155
- if (tunnel.includes("unavailable"))
25156
- return "unavailable";
25157
- if (tunnel.includes("disconnected"))
25158
- return "disconnected";
25159
- if (connection?.pairingState === "paired")
25160
- return "paired";
25161
- return tunnel || connection?.pairingState || "unknown";
25162
- }
25163
- async function listIosPhysicalDevices(includeUnavailable) {
25164
- const dir = await mkdtemp2(join7(tmpdir2(), "yoqa-devices-"));
25165
- const jsonPath = join7(dir, "devices.json");
25166
- try {
25167
- const { exitCode } = await runCommand3([
25168
- "xcrun",
25169
- "devicectl",
25170
- "list",
25171
- "devices",
25172
- "--json-output",
25173
- jsonPath
25174
- ]);
25175
- if (exitCode !== 0)
25176
- return [];
25177
- let parsed;
25178
- try {
25179
- parsed = JSON.parse(await readFile(jsonPath, "utf8"));
25180
- } catch {
25181
- return [];
25182
- }
25183
- const devices = [];
25184
- for (const item of parsed.result?.devices ?? []) {
25185
- const hardware = item.hardwareProperties;
25186
- const props = item.deviceProperties;
25187
- if (!hardware?.udid)
25188
- continue;
25189
- if (hardware.reality && hardware.reality !== "physical")
25190
- continue;
25191
- if (!isPhoneOrTablet(hardware.deviceType))
25192
- continue;
25193
- if (hardware.platform && !/^iOS$/i.test(hardware.platform))
25194
- continue;
25195
- const state = physicalIosState(item.connectionProperties);
25196
- if (!includeUnavailable && state === "unavailable")
25197
- continue;
25198
- const modelName = hardware.marketingName ?? hardware.productType ?? "iPhone";
25199
- const owner = props?.name?.trim() || undefined;
25200
- devices.push({
25201
- id: hardware.udid,
25202
- name: modelName,
25203
- owner,
25204
- osVersion: formatIosVersion(props?.osVersionNumber ?? ""),
25205
- platform: "ios",
25206
- kind: "physical",
25207
- state,
25208
- model: hardware.productType
25209
- });
25210
- }
25211
- return devices.sort((a, b) => a.name.localeCompare(b.name));
25212
- } finally {
25213
- await rm2(dir, { recursive: true, force: true });
25214
- }
25215
- }
25216
- async function listIosDevices(options = {}) {
25217
- const includeUnavailable = options.includeUnavailable ?? true;
25218
- const [physical, simulators] = await Promise.all([
25219
- listIosPhysicalDevices(includeUnavailable),
25220
- listIosSimulators(includeUnavailable)
25221
- ]);
25222
- return [...physical, ...simulators];
25223
- }
25224
- function parseAdbDevices(output) {
25225
- const lines = output.split(`
25226
- `).map((line) => line.trim()).filter(Boolean);
25227
- const results = [];
25228
- for (const line of lines) {
25229
- if (line.startsWith("List of devices"))
25230
- continue;
25231
- const parts = line.split(/\s+/);
25232
- const serial = parts[0];
25233
- const state = parts[1];
25234
- if (!serial || !state)
25235
- continue;
25236
- const meta = {};
25237
- for (const part of parts.slice(2)) {
25238
- const eq2 = part.indexOf(":");
25239
- if (eq2 <= 0)
25240
- continue;
25241
- meta[part.slice(0, eq2)] = part.slice(eq2 + 1);
25242
- }
25243
- results.push({
25244
- serial,
25245
- state,
25246
- product: meta.product,
25247
- model: meta.model,
25248
- device: meta.device,
25249
- transportId: meta.transport_id
25250
- });
25251
- }
25252
- return results;
25253
- }
25254
- function humanizeAvdName(avdName) {
25255
- return titleCaseWords(avdName.replace(/_/g, " "));
25256
- }
25257
- function humanizeModel(model) {
25258
- if (!model)
25259
- return;
25260
- return titleCaseWords(model.replace(/_/g, " "));
25261
- }
25262
- async function findAvdConfigPath(avdName) {
25263
- const home = process.env.HOME ?? "";
25264
- const roots = [join7(home, ".android/avd"), join7(home, "Library/Android/sdk/.android/avd")];
25265
- for (const root of roots) {
25266
- const direct = join7(root, `${avdName}.avd`, "config.ini");
25267
- try {
25268
- await readFile(direct, "utf8");
25269
- return direct;
25270
- } catch {}
25271
- try {
25272
- const glob = new Bun.Glob("*.avd/config.ini");
25273
- for await (const match2 of glob.scan({ cwd: root, onlyFiles: true })) {
25274
- const path = join7(root, match2);
25275
- const text2 = await readFile(path, "utf8");
25276
- const idLine = text2.split(`
25277
- `).find((line) => line.startsWith("AvdId="));
25278
- const id = idLine?.slice("AvdId=".length).trim();
25279
- if (id === avdName)
25280
- return path;
25281
- }
25282
- } catch {}
25283
- }
25284
- return null;
25285
- }
25286
- async function readAvdConfig(avdName) {
25287
- const path = await findAvdConfigPath(avdName);
25288
- if (!path)
25289
- return { api: null, deviceName: null };
25290
- try {
25291
- const text2 = await readFile(path, "utf8");
25292
- const imageLine = text2.split(`
25293
- `).find((line) => line.includes("system-images") && line.includes("android-"));
25294
- const apiMatch = imageLine?.match(/android-([\d.]+)/);
25295
- const deviceLine = text2.split(`
25296
- `).find((entry) => entry.startsWith("hw.device.name="));
25297
- const deviceRaw = deviceLine?.slice("hw.device.name=".length).trim();
25298
- return {
25299
- api: apiMatch?.[1] ?? null,
25300
- deviceName: humanizeModel(deviceRaw) ?? null
25301
- };
25302
- } catch {
25303
- return { api: null, deviceName: null };
25304
- }
25305
- }
25306
- async function adbAvdName(adb, serial) {
25307
- const { stdout, exitCode } = await runCommand3([adb, "-s", serial, "emu", "avd", "name"]);
25308
- if (exitCode !== 0)
25309
- return null;
25310
- const name = stdout.trim().split(`
25311
- `)[0]?.trim();
25312
- return name || null;
25313
- }
25314
- async function adbGetProp(adb, serial, prop) {
25315
- const { stdout, exitCode } = await runCommand3([adb, "-s", serial, "shell", "getprop", prop]);
25316
- if (exitCode !== 0)
25317
- return null;
25318
- const value = stdout.trim();
25319
- return value || null;
25320
- }
25321
- async function listAndroidAdbDevices(adb) {
25322
- const { stdout, exitCode } = await runCommand3([adb, "devices", "-l"]);
25323
- if (exitCode !== 0) {
25324
- return { physical: [], runningEmulators: [], runningAvdNames: new Set };
25325
- }
25326
- const entries = parseAdbDevices(stdout);
25327
- const physical = [];
25328
- const runningEmulators = [];
25329
- const runningAvdNames = new Set;
25330
- for (const entry of entries) {
25331
- const isEmulator = entry.serial.startsWith("emulator-") || entry.product === "sdk_gphone";
25332
- if (isEmulator) {
25333
- const avdName = await adbAvdName(adb, entry.serial);
25334
- if (avdName)
25335
- runningAvdNames.add(avdName);
25336
- const avdConfig = avdName ? await readAvdConfig(avdName) : null;
25337
- const release2 = await adbGetProp(adb, entry.serial, "ro.build.version.release") ?? avdConfig?.api ?? null;
25338
- const model2 = humanizeModel(entry.model) ?? avdConfig?.deviceName ?? (avdName ? humanizeAvdName(avdName) : entry.serial);
25339
- runningEmulators.push({
25340
- id: avdName ?? entry.serial,
25341
- name: model2,
25342
- osVersion: formatAndroidVersion(release2 ?? ""),
25343
- platform: "android",
25344
- kind: "emulator",
25345
- state: entry.state === "device" ? "online" : entry.state,
25346
- model: avdName ?? entry.model
25347
- });
25348
- continue;
25349
- }
25350
- const release = await adbGetProp(adb, entry.serial, "ro.build.version.release");
25351
- const model = humanizeModel(entry.model) ?? await adbGetProp(adb, entry.serial, "ro.product.model") ?? entry.serial;
25352
- physical.push({
25353
- id: entry.serial,
25354
- name: model,
25355
- osVersion: formatAndroidVersion(release ?? ""),
25356
- platform: "android",
25357
- kind: "physical",
25358
- state: entry.state,
25359
- model: entry.model
25360
- });
25361
- }
25362
- return { physical, runningEmulators, runningAvdNames };
25363
- }
25364
- async function listAndroidAvds(emulatorBin, runningAvdNames, includeUnavailable) {
25365
- const { stdout, exitCode } = await runCommand3([emulatorBin, "-list-avds"]);
25366
- if (exitCode !== 0)
25367
- return [];
25368
- const names = stdout.split(`
25369
- `).map((line) => line.trim()).filter(Boolean);
25370
- const devices = [];
25371
- for (const avdName of names) {
25372
- const isRunning = runningAvdNames.has(avdName);
25373
- if (!includeUnavailable && !isRunning)
25374
- continue;
25375
- if (isRunning)
25376
- continue;
25377
- const { api, deviceName } = await readAvdConfig(avdName);
25378
- devices.push({
25379
- id: avdName,
25380
- name: deviceName ?? humanizeAvdName(avdName),
25381
- osVersion: formatAndroidVersion(api ?? ""),
25382
- platform: "android",
25383
- kind: "emulator",
25384
- state: "offline",
25385
- model: avdName
25386
- });
25387
- }
25388
- return devices;
25389
- }
25390
- async function listAndroidDevices(options = {}) {
25391
- const includeUnavailable = options.includeUnavailable ?? true;
25392
- const [adb, emulatorBin] = await Promise.all([resolveAdb(), resolveEmulator()]);
25393
- const adbResult = adb ? await listAndroidAdbDevices(adb) : { physical: [], runningEmulators: [], runningAvdNames: new Set };
25394
- const offlineAvds = emulatorBin != null ? await listAndroidAvds(emulatorBin, adbResult.runningAvdNames, includeUnavailable) : [];
25395
- const physical = includeUnavailable ? adbResult.physical : adbResult.physical.filter((device) => device.state === "device");
25396
- const emulators = [...adbResult.runningEmulators, ...offlineAvds].sort((a, b) => {
25397
- const aOnline = a.state === "online" ? 0 : 1;
25398
- const bOnline = b.state === "online" ? 0 : 1;
25399
- if (aOnline !== bOnline)
25400
- return aOnline - bOnline;
25401
- return a.name.localeCompare(b.name);
25402
- });
25403
- return [...physical, ...emulators];
25404
- }
25405
- async function listDevices(platform, options = {}) {
25406
- if (platform === "ios")
25407
- return listIosDevices(options);
25408
- return listAndroidDevices(options);
25409
- }
25410
-
25411
25484
  // src/interfaces/http/devices.ts
25412
25485
  function createDevicesRoutes() {
25413
25486
  const app = new Hono2;
@@ -57472,6 +57545,9 @@ function formatProviderHttpError(label, status, body) {
57472
57545
  if (label === "OpenCode" && (body.includes("unknown variant `image_url`") || body.includes("No endpoints found that support image input") || body.includes("expected `text`"))) {
57473
57546
  return `This OpenCode model does not accept screenshots (text-only). In Settings \u2192 Provider, set the default model to ${EXAMPLE_OPENCODE_VISION_MODEL} (or another vision-capable model). deepseek-v4-flash-free and big-pickle are text-only on Zen.`;
57474
57547
  }
57548
+ if (label === "OpenCode" && (body.includes("output_config.format") || body.includes("BedrockException") && body.includes("Extra inputs are not permitted"))) {
57549
+ return "LiteLLM/Bedrock rejected JSON schema mode for this model (output_config.format). Restart the Yoqa runner and retry \u2014 LiteLLM vision now asks for JSON in the prompt only.";
57550
+ }
57475
57551
  if (label === "OpenCode" && status >= 500 && (body.includes("Internal server error") || body.includes('"type":"error"'))) {
57476
57552
  return `OpenCode Zen returned an internal error for this vision request. Many free models reject screenshots. In Settings \u2192 Provider, set the default model to ${EXAMPLE_OPENCODE_VISION_MODEL} (or another vision-capable model).`;
57477
57553
  }
@@ -57505,6 +57581,16 @@ function mapProviderError(label, error40) {
57505
57581
  }
57506
57582
  throw new AgentProviderError(String(error40));
57507
57583
  }
57584
+ function repairModelJsonText(text3) {
57585
+ const trimmed = text3.trim();
57586
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
57587
+ const candidate = fenced?.[1]?.trim() ?? trimmed;
57588
+ const start = candidate.indexOf("{");
57589
+ const end = candidate.lastIndexOf("}");
57590
+ if (start < 0 || end <= start)
57591
+ return trimmed;
57592
+ return candidate.slice(start, end + 1);
57593
+ }
57508
57594
  async function completeWithAiSdk(input) {
57509
57595
  const run = async (prompt) => {
57510
57596
  try {
@@ -57513,6 +57599,7 @@ async function completeWithAiSdk(input) {
57513
57599
  schema: input.schema,
57514
57600
  system: input.system,
57515
57601
  maxOutputTokens: VISION_MAX_TOKENS,
57602
+ experimental_repairText: async ({ text: text3 }) => repairModelJsonText(text3),
57516
57603
  messages: [
57517
57604
  {
57518
57605
  role: "user",
@@ -57529,6 +57616,11 @@ async function completeWithAiSdk(input) {
57529
57616
  });
57530
57617
  return object3;
57531
57618
  } catch (error40) {
57619
+ if (NoObjectGeneratedError.isInstance(error40) && error40.text?.trim()) {
57620
+ try {
57621
+ return input.schema.parse(JSON.parse(repairModelJsonText(error40.text)));
57622
+ } catch {}
57623
+ }
57532
57624
  mapProviderError(input.label, error40);
57533
57625
  }
57534
57626
  };
@@ -57571,41 +57663,58 @@ function openCodeAuthPaths() {
57571
57663
  paths.push(join9(homedir8(), ".local", "share", "opencode", "auth.json"));
57572
57664
  return paths;
57573
57665
  }
57574
- async function readOpenCodeCliAuthKey() {
57666
+ function apiKeyFromOpenCodeAuthEntry(value) {
57667
+ if (!value || typeof value !== "object")
57668
+ return null;
57669
+ const entry = value;
57670
+ for (const field of ["key", "apiKey", "token"]) {
57671
+ const candidate = entry[field];
57672
+ if (typeof candidate === "string" && candidate.trim())
57673
+ return candidate.trim();
57674
+ }
57675
+ return null;
57676
+ }
57677
+ async function readOpenCodeCliAuthJson() {
57575
57678
  for (const path of openCodeAuthPaths()) {
57576
57679
  try {
57577
57680
  const file2 = Bun.file(path);
57578
57681
  if (!await file2.exists())
57579
57682
  continue;
57580
57683
  const json3 = await file2.json();
57581
- if (!json3 || typeof json3 !== "object")
57684
+ if (!json3 || typeof json3 !== "object" || Array.isArray(json3))
57582
57685
  continue;
57583
- const record2 = json3;
57584
- const opencode = record2.opencode;
57585
- if (opencode && typeof opencode === "object") {
57586
- const entry = opencode;
57587
- for (const field of ["key", "apiKey", "token"]) {
57588
- const value = entry[field];
57589
- if (typeof value === "string" && value.trim())
57590
- return value.trim();
57591
- }
57592
- }
57593
- for (const value of Object.values(record2)) {
57594
- if (!value || typeof value !== "object")
57595
- continue;
57596
- const entry = value;
57597
- const key = entry.key ?? entry.apiKey ?? entry.token;
57598
- if (typeof key === "string" && key.trim()) {
57599
- const type = typeof entry.type === "string" ? entry.type.toLowerCase() : "";
57600
- if (!type || type.includes("api") || type.includes("key") || type === "opencode") {
57601
- return key.trim();
57602
- }
57603
- }
57604
- }
57686
+ return json3;
57605
57687
  } catch {}
57606
57688
  }
57607
57689
  return null;
57608
57690
  }
57691
+ async function readOpenCodeCliAuthKeyForProvider(providerId) {
57692
+ const record2 = await readOpenCodeCliAuthJson();
57693
+ if (!record2)
57694
+ return null;
57695
+ return apiKeyFromOpenCodeAuthEntry(record2[providerId]);
57696
+ }
57697
+ async function readOpenCodeCliAuthKey() {
57698
+ const record2 = await readOpenCodeCliAuthJson();
57699
+ if (!record2)
57700
+ return null;
57701
+ const zen = apiKeyFromOpenCodeAuthEntry(record2.opencode);
57702
+ if (zen)
57703
+ return zen;
57704
+ for (const value of Object.values(record2)) {
57705
+ const key = apiKeyFromOpenCodeAuthEntry(value);
57706
+ if (!key)
57707
+ continue;
57708
+ if (!value || typeof value !== "object")
57709
+ continue;
57710
+ const entry = value;
57711
+ const type = typeof entry.type === "string" ? entry.type.toLowerCase() : "";
57712
+ if (!type || type.includes("api") || type.includes("key") || type === "opencode") {
57713
+ return key;
57714
+ }
57715
+ }
57716
+ return null;
57717
+ }
57609
57718
  async function resolveOpenAiCompatibleKey(auth) {
57610
57719
  if (auth.apiKey?.trim())
57611
57720
  return auth.apiKey.trim();
@@ -57648,18 +57757,34 @@ function withOpenCodeRequestHooks(opts) {
57648
57757
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
57649
57758
  return async (input, init) => {
57650
57759
  const headers = new Headers(init?.headers);
57651
- if (opts.authHeaders?.Authorization) {
57652
- headers.set("Authorization", opts.authHeaders.Authorization);
57760
+ if (opts.authHeaders) {
57761
+ for (const [key, value] of Object.entries(opts.authHeaders)) {
57762
+ if (value)
57763
+ headers.set(key, value);
57764
+ }
57765
+ }
57766
+ if (opts.extraHeaders) {
57767
+ for (const [key, value] of Object.entries(opts.extraHeaders)) {
57768
+ if (value)
57769
+ headers.set(key, value);
57770
+ }
57653
57771
  }
57654
- if (!opts.disableThinking || !init?.body || typeof init.body !== "string") {
57772
+ const rawBody = init?.body;
57773
+ const shouldRewriteBody = (opts.disableThinking || Boolean(opts.stripResponseFormat)) && typeof rawBody === "string";
57774
+ if (!shouldRewriteBody || typeof rawBody !== "string") {
57655
57775
  return fetchImpl(input, { ...init, headers });
57656
57776
  }
57657
57777
  try {
57658
- const parsed = JSON.parse(init.body);
57778
+ const parsed = JSON.parse(rawBody);
57659
57779
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
57660
57780
  return fetchImpl(input, { ...init, headers });
57661
57781
  }
57662
- const body = { ...parsed, thinking: { type: "disabled" } };
57782
+ const parsedBody = { ...parsed };
57783
+ if (opts.disableThinking) {
57784
+ parsedBody.thinking = { type: "disabled" };
57785
+ }
57786
+ const { response_format: _responseFormat, ...withoutFormat } = parsedBody;
57787
+ const body = opts.stripResponseFormat ? withoutFormat : parsedBody;
57663
57788
  return fetchImpl(input, {
57664
57789
  ...init,
57665
57790
  headers,
@@ -96163,12 +96288,144 @@ var openaiDriver = {
96163
96288
  }
96164
96289
  };
96165
96290
 
96291
+ // src/domains/providers/drivers/opencode-config.ts
96292
+ import { homedir as homedir10 } from "os";
96293
+ import { join as join13 } from "path";
96294
+ var OPENAI_COMPATIBLE_PACKAGES = new Set([
96295
+ "@ai-sdk/openai-compatible",
96296
+ "@ai-sdk/openai",
96297
+ "@ai-sdk/azure"
96298
+ ]);
96299
+ function asRecord3(value) {
96300
+ if (!value || typeof value !== "object" || Array.isArray(value))
96301
+ return null;
96302
+ return value;
96303
+ }
96304
+ function isOpenAiCompatibleNpm(npm) {
96305
+ const trimmed = npm.trim();
96306
+ if (!trimmed)
96307
+ return true;
96308
+ if (OPENAI_COMPATIBLE_PACKAGES.has(trimmed))
96309
+ return true;
96310
+ return trimmed.includes("openai-compatible");
96311
+ }
96312
+ function interpolateOpenCodeTemplate(value, env) {
96313
+ return value.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_full, name31) => {
96314
+ return env[name31]?.trim() || process.env[name31]?.trim() || "";
96315
+ }).trim();
96316
+ }
96317
+ function openCodeConfigPaths() {
96318
+ const paths = [];
96319
+ const xdg = process.env.XDG_CONFIG_HOME?.trim();
96320
+ if (xdg)
96321
+ paths.push(join13(xdg, "opencode", "opencode.json"));
96322
+ paths.push(join13(homedir10(), ".config", "opencode", "opencode.json"));
96323
+ return paths;
96324
+ }
96325
+ function parseOpenCodeCompatibleProvider(config2, providerId) {
96326
+ const root = asRecord3(config2);
96327
+ const providers2 = asRecord3(root?.provider);
96328
+ const entry = asRecord3(providers2?.[providerId]);
96329
+ if (!entry)
96330
+ return null;
96331
+ const npm = typeof entry.npm === "string" ? entry.npm : "";
96332
+ if (!isOpenAiCompatibleNpm(npm))
96333
+ return null;
96334
+ const options = asRecord3(entry.options);
96335
+ const baseURLRaw = typeof options?.baseURL === "string" ? options.baseURL.trim() : "";
96336
+ if (!baseURLRaw)
96337
+ return null;
96338
+ const apiKeyTemplate = typeof options?.apiKey === "string" ? options.apiKey : null;
96339
+ const headers = {};
96340
+ const headerBag = asRecord3(options?.headers);
96341
+ if (headerBag) {
96342
+ for (const [key, value] of Object.entries(headerBag)) {
96343
+ if (typeof value === "string" && value.trim())
96344
+ headers[key] = value;
96345
+ }
96346
+ }
96347
+ return {
96348
+ providerId,
96349
+ baseURL: baseURLRaw.replace(/\/$/, ""),
96350
+ apiKeyTemplate,
96351
+ headers
96352
+ };
96353
+ }
96354
+ function resolveOpenCodeCompatibleAuth(input) {
96355
+ const parsed = parseOpenCodeCompatibleProvider(input.config, input.providerId);
96356
+ const override = input.overrideBaseUrl?.trim().replace(/\/$/, "") || null;
96357
+ if (!parsed && !(override && input.providerId === "litellm"))
96358
+ return null;
96359
+ const baseURL = override || parsed?.baseURL || null;
96360
+ if (!baseURL)
96361
+ return null;
96362
+ const headers = {};
96363
+ if (parsed) {
96364
+ for (const [key, value] of Object.entries(parsed.headers)) {
96365
+ const interpolated = interpolateOpenCodeTemplate(value, input.env);
96366
+ if (interpolated)
96367
+ headers[key] = interpolated;
96368
+ }
96369
+ }
96370
+ const fromTemplate = parsed?.apiKeyTemplate ? interpolateOpenCodeTemplate(parsed.apiKeyTemplate, input.env) : "";
96371
+ const envKeyName = `${input.providerId.replace(/-/g, "_").toUpperCase()}_API_KEY`;
96372
+ const apiKey = fromTemplate || input.env[envKeyName]?.trim() || process.env[envKeyName]?.trim() || input.storedKey?.trim() || null;
96373
+ return { baseURL, apiKey, headers };
96374
+ }
96375
+ async function loadOpenCodeUserConfig() {
96376
+ for (const path of openCodeConfigPaths()) {
96377
+ try {
96378
+ const file2 = Bun.file(path);
96379
+ if (!await file2.exists())
96380
+ continue;
96381
+ return await file2.json();
96382
+ } catch {}
96383
+ }
96384
+ return null;
96385
+ }
96386
+
96166
96387
  // src/domains/providers/drivers/opencode.ts
96167
- var OPENCODE_FREE_MODEL_IDS = new Set(["big-pickle"]);
96168
96388
  var OPENCODE_DEFAULT_VISION_MODEL = "mimo-v2.5-free";
96169
- function isOpenCodeFreeModel(id) {
96170
- const normalized = id.trim().toLowerCase();
96171
- return OPENCODE_FREE_MODEL_IDS.has(normalized) || normalized.endsWith("-free");
96389
+ var OPENCODE_PROVIDER_LABELS = {
96390
+ "amazon-bedrock": "Amazon Bedrock",
96391
+ anthropic: "Anthropic",
96392
+ azure: "Azure OpenAI",
96393
+ "github-copilot": "GitHub Copilot",
96394
+ google: "Google",
96395
+ "google-vertex": "Google Vertex AI",
96396
+ groq: "Groq",
96397
+ litellm: "LiteLLM",
96398
+ ollama: "Ollama",
96399
+ openai: "OpenAI",
96400
+ opencode: "OpenCode Zen",
96401
+ "opencode-go": "OpenCode Go",
96402
+ openrouter: "OpenRouter",
96403
+ xai: "xAI",
96404
+ zenmux: "ZenMux"
96405
+ };
96406
+ function humanizeOpenCodeProvider(providerId) {
96407
+ const known = OPENCODE_PROVIDER_LABELS[providerId];
96408
+ if (known)
96409
+ return known;
96410
+ return providerId.split(/[-_]/).filter(Boolean).map((part) => {
96411
+ if (part.toLowerCase() === "ai")
96412
+ return "AI";
96413
+ return part.charAt(0).toUpperCase() + part.slice(1);
96414
+ }).join(" ");
96415
+ }
96416
+ function parseOpenCodeModelSlug(slug) {
96417
+ const trimmed = slug.trim();
96418
+ const sep = trimmed.indexOf("/");
96419
+ if (sep <= 0 || sep === trimmed.length - 1) {
96420
+ return { providerId: "opencode", modelId: trimmed };
96421
+ }
96422
+ return {
96423
+ providerId: trimmed.slice(0, sep),
96424
+ modelId: trimmed.slice(sep + 1)
96425
+ };
96426
+ }
96427
+ function isOpenCodeZenModel(slug) {
96428
+ return parseOpenCodeModelSlug(slug).providerId === "opencode";
96172
96429
  }
96173
96430
  function humanizeModelId(id) {
96174
96431
  return id.split(/[-_]/).filter(Boolean).map((part) => {
@@ -96179,11 +96436,13 @@ function humanizeModelId(id) {
96179
96436
  return part.charAt(0).toUpperCase() + part.slice(1);
96180
96437
  }).join(" ");
96181
96438
  }
96182
- function toOpenCodeModelEntry(id) {
96439
+ function toOpenCodeModelEntry(slug) {
96440
+ const trimmed = slug.trim();
96441
+ const { providerId, modelId } = parseOpenCodeModelSlug(trimmed);
96183
96442
  return {
96184
- id,
96185
- name: humanizeModelId(id),
96186
- tier: isOpenCodeFreeModel(id) ? "free" : "paid"
96443
+ id: trimmed,
96444
+ name: humanizeModelId(modelId),
96445
+ provider: humanizeOpenCodeProvider(providerId)
96187
96446
  };
96188
96447
  }
96189
96448
  function openCodeServerAuthHeaders(password) {
@@ -96197,18 +96456,14 @@ function resolveOpenCodeServerPassword(env) {
96197
96456
  return env.OPENCODE_SERVER_PASSWORD?.trim() || env.SERVER_PASSWORD?.trim() || process.env.OPENCODE_SERVER_PASSWORD?.trim() || process.env.SERVER_PASSWORD?.trim() || null;
96198
96457
  }
96199
96458
  function stripOpenCodeModelSlug(slug) {
96200
- const trimmed = slug.trim();
96201
- const sep = trimmed.indexOf("/");
96202
- if (sep <= 0 || sep === trimmed.length - 1)
96203
- return trimmed;
96204
- return trimmed.slice(sep + 1);
96459
+ return parseOpenCodeModelSlug(slug).modelId;
96205
96460
  }
96206
96461
  function parseModelsPayload(json3) {
96207
96462
  const models = [];
96208
96463
  if (json3 && typeof json3 === "object" && "data" in json3 && Array.isArray(json3.data)) {
96209
96464
  for (const item of json3.data) {
96210
96465
  if (item && typeof item === "object" && "id" in item && typeof item.id === "string") {
96211
- models.push(toOpenCodeModelEntry(stripOpenCodeModelSlug(item.id)));
96466
+ models.push(toOpenCodeModelEntry(item.id));
96212
96467
  }
96213
96468
  }
96214
96469
  }
@@ -96237,44 +96492,84 @@ async function listOpenCodeModelsFromCli(binaryPath) {
96237
96492
  const slug = line.trim();
96238
96493
  if (!slug || /\s/.test(slug))
96239
96494
  continue;
96240
- const id = stripOpenCodeModelSlug(slug);
96241
- if (!id || seen.has(id))
96495
+ if (seen.has(slug))
96242
96496
  continue;
96243
- seen.add(id);
96244
- models.push(toOpenCodeModelEntry(id));
96497
+ seen.add(slug);
96498
+ models.push(toOpenCodeModelEntry(slug));
96245
96499
  }
96246
96500
  return {
96247
96501
  models,
96248
96502
  detail: `${models.length} models from opencode CLI`
96249
96503
  };
96250
96504
  }
96505
+ async function resolveCompatibleOpenCodeVision(providerId, auth) {
96506
+ const config2 = await loadOpenCodeUserConfig();
96507
+ const storedKey = await readOpenCodeCliAuthKeyForProvider(providerId);
96508
+ return resolveOpenCodeCompatibleAuth({
96509
+ providerId,
96510
+ config: config2,
96511
+ env: auth.env,
96512
+ storedKey,
96513
+ overrideBaseUrl: auth.baseUrl
96514
+ });
96515
+ }
96251
96516
  var opencodeDriver = {
96252
96517
  kind: "opencode",
96253
96518
  label: "OpenCode",
96254
- description: "OpenCode Zen (OpenAI-compatible). Paste a Zen API key for vision runs.",
96519
+ description: "OpenCode Zen or LiteLLM (OpenAI-compatible). Paste a Zen API key for Zen; LiteLLM uses opencode.json.",
96255
96520
  defaultBinary: "opencode",
96256
96521
  authModes: ["cli", "api_key"],
96257
- envHints: ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY", "OPENCODE_SERVER_PASSWORD"],
96258
- loginInstructions: "Paste a Zen API key from https://opencode.ai for vision. Local `opencode serve` is not OpenAI-compatible.",
96522
+ envHints: [
96523
+ "OPENCODE_API_KEY",
96524
+ "OPENCODE_ZEN_API_KEY",
96525
+ "OPENCODE_SERVER_PASSWORD",
96526
+ "LITELLM_API_KEY"
96527
+ ],
96528
+ loginInstructions: "Paste a Zen API key from https://opencode.ai for Zen vision. LiteLLM uses ~/.config/opencode/opencode.json plus CLI auth or LITELLM_API_KEY. Local `opencode serve` is not OpenAI-compatible.",
96259
96529
  capabilities: { vision: true },
96260
96530
  vision: createSdkVisionPort({
96261
96531
  label: "OpenCode",
96262
96532
  defaultModel: OPENCODE_DEFAULT_VISION_MODEL,
96263
96533
  createModel: async (auth, modelId) => {
96264
- const apiKey = await resolveOpenAiCompatibleKey(auth);
96265
- if (!apiKey) {
96266
- throw new AgentProviderError("OpenCode vision needs a Zen API key. Local `opencode serve` returns a web UI for /v1 (not chat completions). Paste a key from https://opencode.ai in Settings \u2192 Provider, or set OPENCODE_API_KEY.");
96534
+ if (isOpenCodeZenModel(modelId)) {
96535
+ const apiKey = await resolveOpenAiCompatibleKey(auth);
96536
+ if (!apiKey) {
96537
+ throw new AgentProviderError("OpenCode vision needs a Zen API key. Local `opencode serve` returns a web UI for /v1 (not chat completions). Paste a key from https://opencode.ai in Settings \u2192 Provider, or set OPENCODE_API_KEY.");
96538
+ }
96539
+ const baseURL = auth.baseUrl?.trim().replace(/\/$/, "") || "https://opencode.ai/zen/v1";
96540
+ return createOpenAI({
96541
+ apiKey,
96542
+ baseURL,
96543
+ name: "opencode",
96544
+ fetch: withOpenCodeRequestHooks({
96545
+ disableThinking: true,
96546
+ authHeaders: null
96547
+ })
96548
+ }).chat(stripOpenCodeModelSlug(modelId));
96549
+ }
96550
+ const { providerId } = parseOpenCodeModelSlug(modelId);
96551
+ const providerLabel = humanizeOpenCodeProvider(providerId);
96552
+ const compatible = await resolveCompatibleOpenCodeVision(providerId, auth);
96553
+ if (compatible?.apiKey) {
96554
+ return createOpenAI({
96555
+ apiKey: compatible.apiKey,
96556
+ baseURL: compatible.baseURL,
96557
+ name: providerId,
96558
+ fetch: withOpenCodeRequestHooks({
96559
+ disableThinking: false,
96560
+ stripResponseFormat: true,
96561
+ authHeaders: null,
96562
+ extraHeaders: Object.keys(compatible.headers).length > 0 ? compatible.headers : null
96563
+ })
96564
+ }).chat(stripOpenCodeModelSlug(modelId));
96267
96565
  }
96268
- const baseURL = auth.baseUrl?.trim().replace(/\/$/, "") || "https://opencode.ai/zen/v1";
96269
- return createOpenAI({
96270
- apiKey,
96271
- baseURL,
96272
- name: "opencode",
96273
- fetch: withOpenCodeRequestHooks({
96274
- disableThinking: true,
96275
- authHeaders: null
96276
- })
96277
- }).chat(modelId);
96566
+ if (compatible && !compatible.apiKey) {
96567
+ throw new AgentProviderError(`${providerLabel} vision needs an API key. OpenCode stores it in auth.json after login, or set ${providerId.replace(/-/g, "_").toUpperCase()}_API_KEY in Settings \u2192 Provider.`);
96568
+ }
96569
+ if (providerId === "litellm") {
96570
+ throw new AgentProviderError("Yoqa vision can use LiteLLM via OpenCode. Add provider.litellm.options.baseURL in ~/.config/opencode/opencode.json, then pick litellm/\u2026 in Settings \u2192 Provider.");
96571
+ }
96572
+ throw new AgentProviderError(`Yoqa vision uses OpenCode Zen or LiteLLM. "${modelId}" is a ${providerLabel} model. Pick an OpenCode Zen or LiteLLM model in Settings \u2192 Provider.`);
96278
96573
  }
96279
96574
  }),
96280
96575
  async probe(binaryPath) {
@@ -96365,6 +96660,10 @@ var opencodeDriver = {
96365
96660
  async listModels(input) {
96366
96661
  const key = resolveOpenCodeKey(input);
96367
96662
  const serverUrl = input.serverUrl?.trim() || null;
96663
+ const cli = await listOpenCodeModelsFromCli(input.binaryPath);
96664
+ if (cli.models.length > 0) {
96665
+ return { models: cli.models, message: cli.detail };
96666
+ }
96368
96667
  if (serverUrl) {
96369
96668
  try {
96370
96669
  const password = resolveOpenCodeServerPassword(input.env);
@@ -96390,14 +96689,10 @@ var opencodeDriver = {
96390
96689
  label: "OpenCode"
96391
96690
  });
96392
96691
  return {
96393
- models: (result.models ?? []).map((id) => toOpenCodeModelEntry(stripOpenCodeModelSlug(id))),
96692
+ models: (result.models ?? []).map((id) => toOpenCodeModelEntry(id)),
96394
96693
  message: result.ok ? `${result.models?.length ?? 0} models available` : result.message
96395
96694
  };
96396
96695
  }
96397
- const cli = await listOpenCodeModelsFromCli(input.binaryPath);
96398
- if (cli.models.length > 0) {
96399
- return { models: cli.models, message: cli.detail };
96400
- }
96401
96696
  return {
96402
96697
  models: [],
96403
96698
  message: "Add OPENCODE_API_KEY, a server URL, or install the opencode CLI to list models"
@@ -96470,10 +96765,10 @@ import {
96470
96765
  randomBytes
96471
96766
  } from "crypto";
96472
96767
  import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
96473
- import { homedir as homedir10 } from "os";
96474
- import { dirname as dirname3, join as join13 } from "path";
96475
- var YOQA_ROOT5 = join13(homedir10(), ".yoqa");
96476
- var KEY_PATH = join13(YOQA_ROOT5, ".provider-key");
96768
+ import { homedir as homedir11 } from "os";
96769
+ import { dirname as dirname3, join as join14 } from "path";
96770
+ var YOQA_ROOT5 = join14(homedir11(), ".yoqa");
96771
+ var KEY_PATH = join14(YOQA_ROOT5, ".provider-key");
96477
96772
  var ALGORITHM = "aes-256-gcm";
96478
96773
  var IV_LENGTH = 12;
96479
96774
  var KEY_LENGTH = 32;