@jacobbd/relay-ai 0.4.1 → 0.4.4

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
@@ -15,15 +15,11 @@
15
15
  >
16
16
  > <a href="https://buymeacoffee.com/jacobbd"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="42"></a>
17
17
 
18
- <p align="center">
19
- <a href="https://youtu.be/IvsUPHLhX0o">
20
- <img src="assets/demo-part1-thumbnail.png" alt="relay-ai demo — installation, configuration, Claude Code, Claude Cowork & Claude Code Desktop (Part 1)" width="100%">
21
- </a>
22
- </p>
18
+ 📺 **Watch the Demos**
23
19
 
24
- > 🎥 **Watch the Demos:**
25
- > - **Demo (Part 1):** Installation · Configuration · Claude Code · Claude Cowork & Claude Code Desktop — [watch on YouTube](https://youtu.be/IvsUPHLhX0o)
26
- > - **Demo (Part 2):** OpenAI Codex CLI & Codex Desktop App [watch on YouTube](https://youtu.be/42oiOB8IAu4)
20
+ | **Claude Code / Cowork / Desktop** | **Codex CLI & Desktop App** | **Gemini CLI** | **v0.4.1: UI & Antigravity** |
21
+ |:---:|:---:|:---:|:---:|
22
+ | [![Claude Demo](https://img.youtube.com/vi/IvsUPHLhX0o/mqdefault.jpg)](https://youtu.be/IvsUPHLhX0o) | [![Codex Demo](https://img.youtube.com/vi/42oiOB8IAu4/mqdefault.jpg)](https://youtu.be/42oiOB8IAu4) | [![Gemini Demo](https://img.youtube.com/vi/g7JKvqOHJl4/mqdefault.jpg)](https://www.youtube.com/watch?v=g7JKvqOHJl4) | [![UI & Antigravity Demo](https://img.youtube.com/vi/8vXJ0LfpdoY/mqdefault.jpg)](https://www.youtube.com/watch?v=8vXJ0LfpdoY) |
27
23
 
28
24
  **relay-ai** is an interactive CLI — and now a **visual launcher** — that connects AI coding tools to any provider and runs local API gateways on your machine. It supports **Claude Code**, **Claude Desktop (Cowork + Code)**, the **OpenAI Codex CLI**, the **ChatGPT desktop app in Codex mode (macOS + Windows)**, **Google Gemini CLI**, and experimental **Antigravity CLI / IDE** support.
29
25
 
@@ -11,7 +11,7 @@ import { join } from "path";
11
11
  // package.json
12
12
  var package_default = {
13
13
  name: "@jacobbd/relay-ai",
14
- version: "0.4.1",
14
+ version: "0.4.4",
15
15
  publishConfig: {
16
16
  access: "public"
17
17
  },
@@ -7287,10 +7287,12 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
7287
7287
  sendJson(res, 200, anthropicResponse);
7288
7288
  }
7289
7289
  } catch (err) {
7290
- const message = err instanceof Error ? err.message : String(err);
7290
+ const message = formatUpstreamError(err);
7291
7291
  plog(`sdk error npm=${model.npm} upstream=${upstreamModelId(model)}: ${message}`);
7292
- if (!res.headersSent) sendJson(res, 502, { error: { message } });
7293
- else res.end();
7292
+ if (!res.headersSent) {
7293
+ const status = upstreamHttpStatus(err, message);
7294
+ sendJson(res, status === 500 ? 502 : status, { error: { message } });
7295
+ } else res.end();
7294
7296
  }
7295
7297
  return;
7296
7298
  }
@@ -7340,10 +7342,12 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
7340
7342
  sendJson(res, 200, response);
7341
7343
  }
7342
7344
  } catch (err) {
7343
- const message = err instanceof Error ? err.message : String(err);
7345
+ const message = formatUpstreamError(err);
7344
7346
  plog(`sdk error npm=${model.npm} upstream=${upstreamModelId(model)}: ${message}`);
7345
- if (!res.headersSent) sendJson(res, 502, { error: { message } });
7346
- else res.end();
7347
+ if (!res.headersSent) {
7348
+ const status = upstreamHttpStatus(err, message);
7349
+ sendJson(res, status === 500 ? 502 : status, { error: { message } });
7350
+ } else res.end();
7347
7351
  }
7348
7352
  }
7349
7353
  function lookupModel(res, catalog, modelId) {
@@ -8075,6 +8079,128 @@ async function runServerCommand(options = {}) {
8075
8079
  return 0;
8076
8080
  }
8077
8081
 
8082
+ // src/update-check.ts
8083
+ import {
8084
+ chmodSync as chmodSync5,
8085
+ mkdirSync as mkdirSync6,
8086
+ readFileSync as readFileSync10,
8087
+ renameSync as renameSync3,
8088
+ unlinkSync as unlinkSync2,
8089
+ writeFileSync as writeFileSync5
8090
+ } from "fs";
8091
+ import { join as join10 } from "path";
8092
+ var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
8093
+ var UPDATE_CHECK_TIMEOUT_MS = 2e3;
8094
+ var UPDATE_COMMAND = "npm install -g @jacobbd/relay-ai@latest";
8095
+ var REGISTRY_URL = "https://registry.npmjs.org/@jacobbd%2Frelay-ai/latest";
8096
+ var SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
8097
+ function parseVersion(version) {
8098
+ const match = SEMVER_PATTERN.exec(version);
8099
+ if (!match) return null;
8100
+ return {
8101
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
8102
+ prerelease: match[4]?.split(".") ?? []
8103
+ };
8104
+ }
8105
+ function comparePrerelease(current, latest) {
8106
+ if (current.length === 0 || latest.length === 0) {
8107
+ if (current.length === latest.length) return 0;
8108
+ return current.length === 0 ? -1 : 1;
8109
+ }
8110
+ const length = Math.max(current.length, latest.length);
8111
+ for (let i = 0; i < length; i++) {
8112
+ const currentPart = current[i];
8113
+ const latestPart = latest[i];
8114
+ if (currentPart === void 0) return 1;
8115
+ if (latestPart === void 0) return -1;
8116
+ if (currentPart === latestPart) continue;
8117
+ const currentNumber = /^\d+$/.test(currentPart) ? Number(currentPart) : null;
8118
+ const latestNumber = /^\d+$/.test(latestPart) ? Number(latestPart) : null;
8119
+ if (currentNumber !== null && latestNumber !== null) return latestNumber > currentNumber ? 1 : -1;
8120
+ if (currentNumber !== null) return 1;
8121
+ if (latestNumber !== null) return -1;
8122
+ return latestPart > currentPart ? 1 : -1;
8123
+ }
8124
+ return 0;
8125
+ }
8126
+ function isNewerVersion(currentVersion, latestVersion) {
8127
+ const current = parseVersion(currentVersion);
8128
+ const latest = parseVersion(latestVersion);
8129
+ if (!current || !latest) return false;
8130
+ for (let i = 0; i < current.core.length; i++) {
8131
+ if (current.core[i] === latest.core[i]) continue;
8132
+ return latest.core[i] > current.core[i];
8133
+ }
8134
+ return comparePrerelease(current.prerelease, latest.prerelease) > 0;
8135
+ }
8136
+ function cachePath() {
8137
+ return join10(getAppHome(), "update-check.json");
8138
+ }
8139
+ function readFreshCache(now) {
8140
+ try {
8141
+ const parsed = JSON.parse(readFileSync10(cachePath(), "utf8"));
8142
+ if (typeof parsed.latestVersion !== "string" || !parseVersion(parsed.latestVersion)) return null;
8143
+ if (typeof parsed.checkedAt !== "number" || !Number.isFinite(parsed.checkedAt)) return null;
8144
+ const age = now - parsed.checkedAt;
8145
+ if (age < 0 || age >= UPDATE_CHECK_TTL_MS) return null;
8146
+ return { latestVersion: parsed.latestVersion, checkedAt: parsed.checkedAt };
8147
+ } catch {
8148
+ return null;
8149
+ }
8150
+ }
8151
+ function writeCache(cache) {
8152
+ const directory = getAppHome();
8153
+ const path = cachePath();
8154
+ const temporaryPath = `${path}.${process.pid}.tmp`;
8155
+ try {
8156
+ mkdirSync6(directory, { recursive: true, mode: 448 });
8157
+ writeFileSync5(temporaryPath, `${JSON.stringify(cache)}
8158
+ `, { mode: 384 });
8159
+ renameSync3(temporaryPath, path);
8160
+ try {
8161
+ chmodSync5(path, 384);
8162
+ } catch {
8163
+ }
8164
+ } catch {
8165
+ try {
8166
+ unlinkSync2(temporaryPath);
8167
+ } catch {
8168
+ }
8169
+ }
8170
+ }
8171
+ function statusFor(latestVersion) {
8172
+ return {
8173
+ currentVersion: VERSION,
8174
+ latestVersion,
8175
+ updateAvailable: latestVersion !== null && isNewerVersion(VERSION, latestVersion)
8176
+ };
8177
+ }
8178
+ async function checkForUpdates(options = {}) {
8179
+ const now = options.now ?? Date.now();
8180
+ const cached = readFreshCache(now);
8181
+ if (cached) return statusFor(cached.latestVersion);
8182
+ const controller = new AbortController();
8183
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? UPDATE_CHECK_TIMEOUT_MS);
8184
+ try {
8185
+ const response = await (options.fetchImpl ?? fetch)(REGISTRY_URL, {
8186
+ headers: { Accept: "application/json", "User-Agent": `relay-ai/${VERSION}` },
8187
+ signal: controller.signal
8188
+ });
8189
+ if (!response.ok) return statusFor(null);
8190
+ const body = await response.json();
8191
+ if (typeof body.version !== "string" || !parseVersion(body.version)) return statusFor(null);
8192
+ writeCache({ latestVersion: body.version, checkedAt: now });
8193
+ return statusFor(body.version);
8194
+ } catch {
8195
+ return statusFor(null);
8196
+ } finally {
8197
+ clearTimeout(timer);
8198
+ }
8199
+ }
8200
+ function formatUpdateNotification(currentVersion, latestVersion) {
8201
+ return `\u{1F514} Update available: ${currentVersion} \u2192 ${latestVersion}. Run ${UPDATE_COMMAND} to update.`;
8202
+ }
8203
+
8078
8204
  // src/favorite-provider-display.ts
8079
8205
  var OAUTH_FAVORITE_NAMES = {
8080
8206
  "claude-code": "Claude Code OAuth (Anthropic subscription)",
@@ -8095,16 +8221,16 @@ function favoriteProviderDisplayName(provider) {
8095
8221
  import { execSync as execSync2, spawn as spawn2 } from "child_process";
8096
8222
  import { existsSync as existsSync10 } from "fs";
8097
8223
  import { homedir as homedir7 } from "os";
8098
- import { join as join10 } from "path";
8224
+ import { join as join11 } from "path";
8099
8225
  var isWindows2 = process.platform === "win32";
8100
8226
  var OPENCODE_FALLBACK_PATHS = isWindows2 ? [
8101
- join10(process.env["APPDATA"] ?? homedir7(), "npm", "opencode.cmd"),
8102
- join10(process.env["APPDATA"] ?? homedir7(), "npm", "opencode"),
8103
- join10(homedir7(), "AppData", "Roaming", "npm", "opencode.cmd")
8227
+ join11(process.env["APPDATA"] ?? homedir7(), "npm", "opencode.cmd"),
8228
+ join11(process.env["APPDATA"] ?? homedir7(), "npm", "opencode"),
8229
+ join11(homedir7(), "AppData", "Roaming", "npm", "opencode.cmd")
8104
8230
  ] : [
8105
- join10(homedir7(), ".opencode", "bin", "opencode"),
8106
- join10(homedir7(), ".local", "bin", "opencode"),
8107
- join10(homedir7(), ".npm", "bin", "opencode"),
8231
+ join11(homedir7(), ".opencode", "bin", "opencode"),
8232
+ join11(homedir7(), ".local", "bin", "opencode"),
8233
+ join11(homedir7(), ".npm", "bin", "opencode"),
8108
8234
  "/usr/local/bin/opencode",
8109
8235
  "/opt/homebrew/bin/opencode"
8110
8236
  ];
@@ -9785,7 +9911,7 @@ ${pc6.bold("Device code (works on SSH/VPS):")}
9785
9911
  import { execSync as execSync3, spawn as spawn4 } from "child_process";
9786
9912
  import { existsSync as existsSync11, readdirSync, statSync as statSync3 } from "fs";
9787
9913
  import { homedir as homedir8 } from "os";
9788
- import { join as join11 } from "path";
9914
+ import { join as join12 } from "path";
9789
9915
  import * as p6 from "@clack/prompts";
9790
9916
  var CODEX_BUNDLE_ID = "com.openai.codex";
9791
9917
  var DARWIN_APP_NAMES = ["ChatGPT", "Codex"];
@@ -9804,33 +9930,33 @@ function runPowerShell(script) {
9804
9930
  function darwinAppCandidates() {
9805
9931
  return DARWIN_APP_NAMES.flatMap((name) => [
9806
9932
  `/Applications/${name}.app`,
9807
- join11(homedir8(), "Applications", `${name}.app`)
9933
+ join12(homedir8(), "Applications", `${name}.app`)
9808
9934
  ]);
9809
9935
  }
9810
9936
  function winLocalAppData() {
9811
- return process.env.LOCALAPPDATA ?? join11(homedir8(), "AppData", "Local");
9937
+ return process.env.LOCALAPPDATA ?? join12(homedir8(), "AppData", "Local");
9812
9938
  }
9813
9939
  function winCodexExeCandidates() {
9814
9940
  const local = winLocalAppData();
9815
9941
  const bases = WIN_APP_NAMES.flatMap((name) => [
9816
- join11(local, "Programs", name),
9817
- join11(local, "Programs", `OpenAI ${name}`),
9818
- join11(local, name),
9819
- join11(local, `OpenAI ${name}`),
9820
- join11(local, "OpenAI", name)
9942
+ join12(local, "Programs", name),
9943
+ join12(local, "Programs", `OpenAI ${name}`),
9944
+ join12(local, name),
9945
+ join12(local, `OpenAI ${name}`),
9946
+ join12(local, "OpenAI", name)
9821
9947
  ]);
9822
- bases.push(join11(local, "openai-codex-electron"), join11(local, "openai-chatgpt-electron"));
9948
+ bases.push(join12(local, "openai-codex-electron"), join12(local, "openai-chatgpt-electron"));
9823
9949
  const out = [];
9824
9950
  for (const base of bases) {
9825
9951
  for (const name of WIN_APP_NAMES) {
9826
- out.push(join11(base, `${name}.exe`));
9952
+ out.push(join12(base, `${name}.exe`));
9827
9953
  }
9828
9954
  try {
9829
9955
  if (existsSync11(base)) {
9830
9956
  for (const dir of readdirSync(base)) {
9831
9957
  if (dir.startsWith("app-")) {
9832
9958
  for (const name of WIN_APP_NAMES) {
9833
- out.push(join11(base, dir, `${name}.exe`));
9959
+ out.push(join12(base, dir, `${name}.exe`));
9834
9960
  }
9835
9961
  }
9836
9962
  }
@@ -10007,7 +10133,7 @@ function codexAppInstallHint() {
10007
10133
  import { execSync as execSync4, spawn as spawn5 } from "child_process";
10008
10134
  import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as statSync4 } from "fs";
10009
10135
  import { homedir as homedir9 } from "os";
10010
- import { join as join12 } from "path";
10136
+ import { join as join13 } from "path";
10011
10137
  import * as p7 from "@clack/prompts";
10012
10138
  var CLAUDE_BUNDLE_ID = "com.anthropic.claudefordesktop";
10013
10139
  function claudeAppSupported() {
@@ -10024,26 +10150,26 @@ function runPowerShell2(script) {
10024
10150
  function darwinAppCandidates2() {
10025
10151
  return [
10026
10152
  "/Applications/Claude.app",
10027
- join12(homedir9(), "Applications", "Claude.app")
10153
+ join13(homedir9(), "Applications", "Claude.app")
10028
10154
  ];
10029
10155
  }
10030
10156
  function winLocalAppData2() {
10031
- return process.env.LOCALAPPDATA ?? join12(homedir9(), "AppData", "Local");
10157
+ return process.env.LOCALAPPDATA ?? join13(homedir9(), "AppData", "Local");
10032
10158
  }
10033
10159
  function winClaudeExeCandidates() {
10034
10160
  const local = winLocalAppData2();
10035
10161
  const bases = [
10036
- join12(local, "Programs", "Claude"),
10037
- join12(local, "Claude")
10162
+ join13(local, "Programs", "Claude"),
10163
+ join13(local, "Claude")
10038
10164
  ];
10039
10165
  const out = [];
10040
10166
  for (const base of bases) {
10041
- out.push(join12(base, "Claude.exe"));
10167
+ out.push(join13(base, "Claude.exe"));
10042
10168
  try {
10043
10169
  if (existsSync12(base)) {
10044
10170
  for (const name of readdirSync2(base)) {
10045
10171
  if (name.startsWith("app-")) {
10046
- out.push(join12(base, name, "Claude.exe"));
10172
+ out.push(join13(base, name, "Claude.exe"));
10047
10173
  }
10048
10174
  }
10049
10175
  }
@@ -10381,6 +10507,8 @@ export {
10381
10507
  loadServerModels,
10382
10508
  resolveServerUpstreamApiKey,
10383
10509
  runServerCommand,
10510
+ checkForUpdates,
10511
+ formatUpdateNotification,
10384
10512
  favoriteProviderDisplayName,
10385
10513
  addProviderFromTemplate,
10386
10514
  removeProviderFromRegistry,
@@ -10404,4 +10532,4 @@ export {
10404
10532
  quitClaudeAppGracefully,
10405
10533
  launchOrRestartClaudeApp
10406
10534
  };
10407
- //# sourceMappingURL=chunk-2GUFTECU.js.map
10535
+ //# sourceMappingURL=chunk-XCB2K4GI.js.map