@route_33/opencode 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,17 +4,31 @@ An opencode provider plugin for the [route33.ai](https://route33.ai) AI gateway:
4
4
 
5
5
  ## Install
6
6
 
7
- Add to the `plugin` array of your opencode config and restart opencode:
7
+ ```bash
8
+ opencode plugin @route_33/opencode
9
+ ```
10
+
11
+ The installer patches both configs: the server plugin into `opencode.json` and the key-setup wizard (TUI plugin) into `tui.json`. To install manually instead, declare both entries yourself:
8
12
 
9
- ```json
13
+ ```jsonc
14
+ // opencode.json — server plugin (model discovery, cost/limit enrichment, auth)
10
15
  {
11
- "plugin": ["@route_33/opencode@^0.1"]
16
+ "plugin": ["@route_33/opencode@^0.2"]
12
17
  }
13
18
  ```
14
19
 
20
+ ```jsonc
21
+ // tui.json — TUI plugin (the key-setup wizard)
22
+ {
23
+ "plugin": ["@route_33/opencode@^0.2"]
24
+ }
25
+ ```
26
+
27
+ This plugin never writes your config: opencode has no TUI-plugin auto-discovery, so without a `tui.json` entry the wizard simply never appears — the server plugin works regardless.
28
+
15
29
  ## Auth
16
30
 
17
- Run `opencode auth login` and choose `route_33`, or set the `ROUTE33_API_KEY` environment variable. Without a key, the TUI wizard offers to save one: per-project inside a git workspace, global otherwise. `ROUTE33_BASE_URL` overrides the default gateway URL; `ROUTE33_TUI_PROMPT=0` disables the wizard.
31
+ Run `opencode auth login` and choose `route_33`, or set the `ROUTE33_API_KEY` environment variable. Without a key, the TUI wizard offers to save one: per-project inside a git workspace, global otherwise. A globally stored key applies only outside git workspaces — inside a workspace, keys resolve from the environment, the workspace config, or `.opencode/route33.key` (per-project keys are the metering boundary). `ROUTE33_BASE_URL` overrides the default gateway URL; `ROUTE33_TUI_PROMPT=0` disables the wizard.
18
32
 
19
33
  ## License
20
34
 
package/dist/index.d.ts CHANGED
@@ -10,6 +10,13 @@ import { Plugin } from '@opencode-ai/plugin';
10
10
  * validation — plus the `route_33` auth method and options loader.
11
11
  * Discovery runs once at startup; restart opencode after a key or config
12
12
  * change.
13
+ *
14
+ * The companion TUI plugin (`exports["./tui"]`, the key-setup wizard) is
15
+ * installed by the USER declaring it in `tui.json` — opencode has no TUI
16
+ * plugin auto-discovery, and this plugin never writes user config
17
+ * (upstream calls third-party tui.json mutation undesirable). The
18
+ * sanctioned install is `opencode plugin @route_33/opencode`, which
19
+ * patches both configs.
13
20
  */
14
21
  declare const Route33Plugin: Plugin;
15
22
 
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ function readAuthEntry(env = process.env) {
32
32
 
33
33
  // src/auth/workspace-key-file.ts
34
34
  import * as fs2 from "fs";
35
- import * as path3 from "path";
35
+ import * as path4 from "path";
36
36
 
37
37
  // src/shared/default-base-url.ts
38
38
  var DEFAULT_BASE_URL = "https://api.route33.ai/v1";
@@ -64,6 +64,23 @@ var ENV_ENRICH_CAPS = "ROUTE33_ENRICH_CAPS";
64
64
  // src/shared/env-strict.ts
65
65
  var ENV_STRICT = "ROUTE33_STRICT";
66
66
 
67
+ // src/shared/git-workspace.ts
68
+ import { existsSync } from "fs";
69
+ import * as path2 from "path";
70
+ function isInsideGitWorktree(startDir) {
71
+ try {
72
+ let dir = path2.resolve(startDir);
73
+ for (; ; ) {
74
+ if (existsSync(path2.join(dir, ".git"))) return true;
75
+ const parent = path2.dirname(dir);
76
+ if (parent === dir) return false;
77
+ dir = parent;
78
+ }
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+
67
84
  // src/shared/logger.ts
68
85
  var logger = {
69
86
  info: (...args) => {
@@ -82,18 +99,18 @@ var MODELS_DEV_TTL_MS = 864e5;
82
99
 
83
100
  // src/shared/resolve-cache-dir.ts
84
101
  import * as os2 from "os";
85
- import * as path2 from "path";
102
+ import * as path3 from "path";
86
103
  function resolveCacheDir(env = process.env) {
87
104
  const override = env[ENV_CACHE_DIR];
88
105
  if (override) return override;
89
106
  const xdgCacheHome = env.XDG_CACHE_HOME;
90
- const base = xdgCacheHome ?? path2.join(os2.homedir(), ".cache");
91
- return path2.join(base, "opencode-route33");
107
+ const base = xdgCacheHome ?? path3.join(os2.homedir(), ".cache");
108
+ return path3.join(base, "opencode-route33");
92
109
  }
93
110
 
94
111
  // src/auth/workspace-key-file.ts
95
- var WORKSPACE_KEY_RELATIVE_PATH = path3.join(".opencode", "route33.key");
96
- var WORKSPACE_GITIGNORE_RELATIVE_PATH = path3.join(".opencode", ".gitignore");
112
+ var WORKSPACE_KEY_RELATIVE_PATH = path4.join(".opencode", "route33.key");
113
+ var WORKSPACE_GITIGNORE_RELATIVE_PATH = path4.join(".opencode", ".gitignore");
97
114
  var GITIGNORE_ENTRIES = ["route33.key", "tui.json"];
98
115
  function usable2(value) {
99
116
  if (typeof value !== "string") return void 0;
@@ -107,7 +124,7 @@ function readWorkspaceKey(worktree) {
107
124
  if (worktree === void 0) return void 0;
108
125
  try {
109
126
  const raw = fs2.readFileSync(
110
- path3.join(worktree, WORKSPACE_KEY_RELATIVE_PATH),
127
+ path4.join(worktree, WORKSPACE_KEY_RELATIVE_PATH),
111
128
  "utf8"
112
129
  );
113
130
  const parsed = JSON.parse(raw);
@@ -119,10 +136,10 @@ function readWorkspaceKey(worktree) {
119
136
  }
120
137
  function ensureWorkspaceKeyGitignore(worktree) {
121
138
  if (worktree === void 0 || worktree.trim() === "") return;
122
- if (path3.resolve(worktree) === "/") return;
123
- const gitignorePath = path3.join(worktree, WORKSPACE_GITIGNORE_RELATIVE_PATH);
139
+ if (path4.resolve(worktree) === "/") return;
140
+ const gitignorePath = path4.join(worktree, WORKSPACE_GITIGNORE_RELATIVE_PATH);
124
141
  try {
125
- fs2.mkdirSync(path3.dirname(gitignorePath), { recursive: true });
142
+ fs2.mkdirSync(path4.dirname(gitignorePath), { recursive: true });
126
143
  let content = "";
127
144
  try {
128
145
  content = fs2.readFileSync(gitignorePath, "utf8");
@@ -157,8 +174,8 @@ function urlFromConfig(cfg) {
157
174
  return literal;
158
175
  return void 0;
159
176
  }
160
- function resolveBaseUrl(cfg, env = process.env) {
161
- return usable3(env[ENV_BASE_URL]) ?? urlFromConfig(cfg) ?? readAuthEntry(env).baseURL ?? DEFAULT_BASE_URL;
177
+ function resolveBaseUrl(cfg, env = process.env, worktree) {
178
+ return usable3(env[ENV_BASE_URL]) ?? urlFromConfig(cfg) ?? (worktree !== void 0 && isInsideGitWorktree(worktree) ? void 0 : readAuthEntry(env).baseURL) ?? DEFAULT_BASE_URL;
162
179
  }
163
180
 
164
181
  // src/auth/api-key-method.ts
@@ -208,7 +225,7 @@ function keyFromConfig(cfg) {
208
225
  return void 0;
209
226
  }
210
227
  function resolveKey(cfg, env = process.env, worktree) {
211
- return usable4(env[ENV_API_KEY]) ?? keyFromConfig(cfg) ?? readWorkspaceKey(worktree) ?? readAuthEntry(env).key;
228
+ return usable4(env[ENV_API_KEY]) ?? keyFromConfig(cfg) ?? readWorkspaceKey(worktree) ?? (worktree !== void 0 && isInsideGitWorktree(worktree) ? void 0 : readAuthEntry(env).key);
212
229
  }
213
230
 
214
231
  // src/auth/route33-auth.ts
@@ -228,200 +245,6 @@ function buildRoute33Auth(env = process.env, worktree) {
228
245
  };
229
246
  }
230
247
 
231
- // src/bootstrap/ensure-tui-registration.ts
232
- import * as path7 from "path";
233
-
234
- // src/bootstrap/global-context.ts
235
- import * as os3 from "os";
236
- import * as path4 from "path";
237
- import { fileURLToPath } from "url";
238
- function isGlobalContext(worktree, moduleUrl) {
239
- try {
240
- if (moduleUrl === void 0) return false;
241
- if (worktree === void 0) return true;
242
- const root = path4.resolve(worktree);
243
- if (root === "/") return true;
244
- const modulePath = fileURLToPath(moduleUrl);
245
- return !modulePath.startsWith(root + path4.sep);
246
- } catch {
247
- return false;
248
- }
249
- }
250
- function usable5(value) {
251
- const trimmed = value?.trim();
252
- return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
253
- }
254
- function globalTuiJsonPath(env) {
255
- const configHome = usable5(env.XDG_CONFIG_HOME) ?? path4.join(os3.homedir(), ".config");
256
- return path4.join(configHome, "opencode", "tui.json");
257
- }
258
-
259
- // src/bootstrap/tui-json-upsert.ts
260
- import * as fs4 from "fs";
261
- import * as path6 from "path";
262
- import {
263
- applyEdits,
264
- modify,
265
- parse as parseJsonc
266
- } from "jsonc-parser";
267
-
268
- // src/bootstrap/tui-spec-resolution.ts
269
- import * as fs3 from "fs";
270
- import * as os4 from "os";
271
- import * as path5 from "path";
272
- import { fileURLToPath as fileURLToPath2 } from "url";
273
- var NPM_TUI_SPEC = "@route_33/opencode/tui";
274
- var OLD_NPM_TUI_SPEC = "opencode-route33/tui";
275
- var TUI_BUNDLE_NAME = "route33-tui.js";
276
- var TUI_SCHEMA = "https://opencode.ai/tui.json";
277
- var SERVER_BUNDLE_NAME = "route33.js";
278
- var DIST_TUI_ENTRY_SUFFIX = "/opencode-route33/dist/tui/index.js";
279
- var SCOPED_DIST_TUI_ENTRY_SUFFIX = "/@route_33/opencode/dist/tui/index.js";
280
- function fileExists(candidate) {
281
- try {
282
- return fs3.statSync(candidate).isFile();
283
- } catch {
284
- return false;
285
- }
286
- }
287
- function specFromModuleUrl(moduleUrl) {
288
- try {
289
- if (moduleUrl === void 0) return void 0;
290
- const selfPath = fileURLToPath2(moduleUrl);
291
- const dir = path5.dirname(selfPath);
292
- if (path5.basename(selfPath) === SERVER_BUNDLE_NAME) {
293
- const sibling = path5.join(dir, TUI_BUNDLE_NAME);
294
- return fileExists(sibling) ? sibling : void 0;
295
- }
296
- if (path5.basename(selfPath) === "index.js" && path5.basename(dir) === "dist") {
297
- const tuiEntry = path5.join(dir, "tui", "index.js");
298
- return fileExists(tuiEntry) ? tuiEntry : void 0;
299
- }
300
- return void 0;
301
- } catch {
302
- return void 0;
303
- }
304
- }
305
- function resolveTuiSpec(worktree, env, moduleUrl) {
306
- const fromModule = specFromModuleUrl(moduleUrl);
307
- if (fromModule !== void 0) return fromModule;
308
- if (worktree !== void 0 && worktree.trim() !== "" && path5.resolve(worktree) !== "/") {
309
- const local = path5.join(worktree, ".opencode", "plugins", TUI_BUNDLE_NAME);
310
- if (fileExists(local)) return local;
311
- }
312
- const configHome = env.XDG_CONFIG_HOME?.trim() || path5.join(os4.homedir(), ".config");
313
- const global = path5.join(configHome, "opencode", "plugins", TUI_BUNDLE_NAME);
314
- if (fileExists(global)) return global;
315
- return NPM_TUI_SPEC;
316
- }
317
- function isOurEntry(entry) {
318
- return typeof entry === "string" && (entry === NPM_TUI_SPEC || entry === OLD_NPM_TUI_SPEC || entry.endsWith(`/${TUI_BUNDLE_NAME}`) || entry.endsWith(DIST_TUI_ENTRY_SUFFIX) || entry.endsWith(SCOPED_DIST_TUI_ENTRY_SUFFIX));
319
- }
320
-
321
- // src/bootstrap/tui-json-upsert.ts
322
- function isRecord3(value) {
323
- return typeof value === "object" && value !== null && !Array.isArray(value);
324
- }
325
- function fileExists2(candidate) {
326
- try {
327
- return fs4.statSync(candidate).isFile();
328
- } catch {
329
- return false;
330
- }
331
- }
332
- function upsertTuiSpec(file, spec) {
333
- try {
334
- if (!fileExists2(file)) {
335
- fs4.mkdirSync(path6.dirname(file), { recursive: true });
336
- fs4.writeFileSync(
337
- file,
338
- JSON.stringify({ $schema: TUI_SCHEMA, plugin: [spec] }, null, 2) + "\n",
339
- "utf8"
340
- );
341
- return;
342
- }
343
- const text = fs4.readFileSync(file, "utf8");
344
- const errors = [];
345
- const parsed = parseJsonc(text, errors, {
346
- allowTrailingComma: true,
347
- allowEmptyContent: true
348
- });
349
- if (!isRecord3(parsed)) {
350
- logger.warn(
351
- `tui.json at ${file} did not parse to an object \u2014 skipping TUI registration`
352
- );
353
- return;
354
- }
355
- if (parsed.plugin !== void 0 && !Array.isArray(parsed.plugin)) {
356
- logger.warn(
357
- `tui.json at ${file} has a non-array "plugin" key \u2014 skipping TUI registration`
358
- );
359
- return;
360
- }
361
- const entries = Array.isArray(parsed.plugin) ? parsed.plugin : [];
362
- const ourIndices = entries.flatMap(
363
- (entry, index) => isOurEntry(entry) ? [index] : []
364
- );
365
- const firstOurs = ourIndices[0];
366
- if (ourIndices.length === 1 && firstOurs !== void 0 && entries[firstOurs] === spec)
367
- return;
368
- const formattingOptions = { insertSpaces: true, tabSize: 2 };
369
- let next = text;
370
- if (!Array.isArray(parsed.plugin)) {
371
- next = applyEdits(
372
- next,
373
- modify(next, ["plugin"], [spec], { formattingOptions })
374
- );
375
- } else if (firstOurs === void 0) {
376
- next = applyEdits(
377
- next,
378
- modify(next, ["plugin", -1], spec, {
379
- formattingOptions,
380
- isArrayInsertion: true
381
- })
382
- );
383
- } else {
384
- if (entries[firstOurs] !== spec) {
385
- next = applyEdits(
386
- next,
387
- modify(next, ["plugin", firstOurs], spec, { formattingOptions })
388
- );
389
- }
390
- for (const index of ourIndices.slice(1).sort((a, b) => b - a)) {
391
- next = applyEdits(
392
- next,
393
- modify(next, ["plugin", index], void 0, { formattingOptions })
394
- );
395
- }
396
- }
397
- if (next !== text) fs4.writeFileSync(file, next, "utf8");
398
- } catch (error) {
399
- logger.warn(
400
- `could not register the route33 TUI plugin in tui.json: ${String(error)}`
401
- );
402
- }
403
- }
404
-
405
- // src/bootstrap/ensure-tui-registration.ts
406
- function ensureTuiRegistration(deps) {
407
- try {
408
- const { worktree, env } = deps;
409
- if (env.ROUTE33_TUI_PROMPT === "0") return;
410
- const usableWorktree = worktree !== void 0 && worktree.trim() !== "" && path7.resolve(worktree) !== "/" ? worktree : void 0;
411
- const spec = resolveTuiSpec(usableWorktree, env, deps.moduleUrl);
412
- if (usableWorktree !== void 0) {
413
- upsertTuiSpec(path7.join(usableWorktree, ".opencode", "tui.json"), spec);
414
- }
415
- if (isGlobalContext(worktree, deps.moduleUrl)) {
416
- upsertTuiSpec(globalTuiJsonPath(env), spec);
417
- }
418
- } catch (error) {
419
- logger.warn(
420
- `could not register the route33 TUI plugin in tui.json: ${String(error)}`
421
- );
422
- }
423
- }
424
-
425
248
  // src/config/live-auth-fetch.ts
426
249
  function buildLiveAuthFetch(resolveKeyNow, innerFetch) {
427
250
  return (input, init) => {
@@ -563,24 +386,24 @@ function hashKey(key) {
563
386
  }
564
387
 
565
388
  // src/catalog/catalog-cache/read-disk.ts
566
- import * as fs5 from "fs";
389
+ import * as fs3 from "fs";
567
390
 
568
391
  // src/catalog/catalog-cache/cache-file-path.ts
569
- import * as path8 from "path";
392
+ import * as path5 from "path";
570
393
  function cacheFilePath(keyHash, env) {
571
- return path8.join(resolveCacheDir(env), `catalog-${keyHash}.json`);
394
+ return path5.join(resolveCacheDir(env), `catalog-${keyHash}.json`);
572
395
  }
573
396
 
574
397
  // src/catalog/catalog-cache/read-disk.ts
575
- function isRecord4(value) {
398
+ function isRecord3(value) {
576
399
  return typeof value === "object" && value !== null && !Array.isArray(value);
577
400
  }
578
401
  function readDisk(keyHash, env) {
579
402
  try {
580
403
  const parsed = JSON.parse(
581
- fs5.readFileSync(cacheFilePath(keyHash, env), "utf8")
404
+ fs3.readFileSync(cacheFilePath(keyHash, env), "utf8")
582
405
  );
583
- if (!isRecord4(parsed) || typeof parsed.fetchedAt !== "number" || !isRecord4(parsed.models))
406
+ if (!isRecord3(parsed) || typeof parsed.fetchedAt !== "number" || !isRecord3(parsed.models))
584
407
  return void 0;
585
408
  return {
586
409
  fetchedAt: parsed.fetchedAt,
@@ -622,15 +445,15 @@ function readStaleCatalog(opts) {
622
445
  }
623
446
 
624
447
  // src/catalog/catalog-cache/write-catalog.ts
625
- import * as fs6 from "fs";
448
+ import * as fs4 from "fs";
626
449
  function writeCatalog(opts) {
627
450
  const { key, models, env = process.env, now = Date.now() } = opts;
628
451
  const keyHash = hashKey(key);
629
452
  const record = { fetchedAt: now, keyHash, models };
630
453
  memory.set(keyHash, record);
631
454
  try {
632
- fs6.mkdirSync(resolveCacheDir(env), { recursive: true });
633
- fs6.writeFileSync(cacheFilePath(keyHash, env), JSON.stringify(record));
455
+ fs4.mkdirSync(resolveCacheDir(env), { recursive: true });
456
+ fs4.writeFileSync(cacheFilePath(keyHash, env), JSON.stringify(record));
634
457
  } catch (error) {
635
458
  logger.warn(
636
459
  `could not persist catalog cache: ${error instanceof Error ? error.message : String(error)}`
@@ -654,7 +477,7 @@ var CatalogFetchError = class extends Error {
654
477
  var DEFAULT_FETCH_TIMEOUT_MS = 5e3;
655
478
 
656
479
  // src/catalog/extract-pricing.ts
657
- function isRecord5(value) {
480
+ function isRecord4(value) {
658
481
  return typeof value === "object" && value !== null && !Array.isArray(value);
659
482
  }
660
483
  var OPENROUTER_KEYS = [
@@ -711,7 +534,7 @@ function extractModelsDevPricing(raw) {
711
534
  return cost;
712
535
  }
713
536
  function extractPricing(raw) {
714
- if (!isRecord5(raw)) return void 0;
537
+ if (!isRecord4(raw)) return void 0;
715
538
  const hasOpenRouterKeys = OPENROUTER_KEYS.some((key) => key in raw);
716
539
  const hasModelsDevKeys = MODELS_DEV_KEYS.some((key) => key in raw);
717
540
  if (hasOpenRouterKeys && hasModelsDevKeys) return void 0;
@@ -742,12 +565,12 @@ function normalizeId(raw, env = process.env) {
742
565
  }
743
566
 
744
567
  // src/catalog/fetch-catalog.ts
745
- function isRecord6(value) {
568
+ function isRecord5(value) {
746
569
  return typeof value === "object" && value !== null && !Array.isArray(value);
747
570
  }
748
571
  function parseEntries(payload) {
749
572
  if (Array.isArray(payload)) return payload;
750
- if (isRecord6(payload) && Array.isArray(payload.data)) return payload.data;
573
+ if (isRecord5(payload) && Array.isArray(payload.data)) return payload.data;
751
574
  throw new CatalogFetchError(
752
575
  "parse",
753
576
  "response is neither {data:[\u2026]} nor a bare array"
@@ -756,13 +579,13 @@ function parseEntries(payload) {
756
579
  function toCatalogModels(entries, env) {
757
580
  const models = {};
758
581
  for (const entry of entries) {
759
- if (!isRecord6(entry) || typeof entry.id !== "string") continue;
582
+ if (!isRecord5(entry) || typeof entry.id !== "string") continue;
760
583
  const slug2 = normalizeId(entry.id, env);
761
584
  if (!slug2 || models[slug2]) continue;
762
585
  const model = { id: slug2 };
763
586
  const displayName = typeof entry.display_name === "string" ? entry.display_name.trim() : "";
764
587
  if (displayName !== "") model.displayName = displayName;
765
- if (isRecord6(entry.pricing) && extractPricing(entry.pricing) !== void 0)
588
+ if (isRecord5(entry.pricing) && extractPricing(entry.pricing) !== void 0)
766
589
  model.pricing = entry.pricing;
767
590
  models[slug2] = model;
768
591
  }
@@ -1171,11 +994,11 @@ function enrichModel(slug2, sources) {
1171
994
  }
1172
995
 
1173
996
  // src/enrich/models-dev/load-models-dev.ts
1174
- import * as fs7 from "fs";
1175
- import * as path9 from "path";
997
+ import * as fs5 from "fs";
998
+ import * as path6 from "path";
1176
999
 
1177
1000
  // src/enrich/models-dev/is-record.ts
1178
- function isRecord7(value) {
1001
+ function isRecord6(value) {
1179
1002
  return typeof value === "object" && value !== null && !Array.isArray(value);
1180
1003
  }
1181
1004
 
@@ -1194,7 +1017,7 @@ async function fetchLive(fetchImpl) {
1194
1017
  return void 0;
1195
1018
  }
1196
1019
  const body = await response.json();
1197
- return isRecord7(body) ? body : void 0;
1020
+ return isRecord6(body) ? body : void 0;
1198
1021
  } catch (error) {
1199
1022
  logger.warn("models.dev fetch failed:", error);
1200
1023
  return void 0;
@@ -1211,7 +1034,7 @@ function num(value) {
1211
1034
  // src/enrich/models-dev/normalize-cost.ts
1212
1035
  var EXPRESSIBLE_TIER_SIZE = 2e5;
1213
1036
  function parseRateBlock(value) {
1214
- if (!isRecord7(value)) return void 0;
1037
+ if (!isRecord6(value)) return void 0;
1215
1038
  const input = num(value.input);
1216
1039
  const output = num(value.output);
1217
1040
  if (input === void 0 || output === void 0) return void 0;
@@ -1224,7 +1047,7 @@ function parseRateBlock(value) {
1224
1047
  }
1225
1048
  function isExpressibleContextTier(tier) {
1226
1049
  const marker = tier.tier;
1227
- return isRecord7(marker) && marker.type === "context" && marker.size === EXPRESSIBLE_TIER_SIZE;
1050
+ return isRecord6(marker) && marker.type === "context" && marker.size === EXPRESSIBLE_TIER_SIZE;
1228
1051
  }
1229
1052
  function maxRate(current, candidate) {
1230
1053
  const rate = num(candidate);
@@ -1232,7 +1055,7 @@ function maxRate(current, candidate) {
1232
1055
  return current === void 0 || rate > current ? rate : current;
1233
1056
  }
1234
1057
  function normalizeCost(raw) {
1235
- if (!isRecord7(raw)) return void 0;
1058
+ if (!isRecord6(raw)) return void 0;
1236
1059
  const input = num(raw.input);
1237
1060
  const output = num(raw.output);
1238
1061
  if (input === void 0 || output === void 0) return void 0;
@@ -1243,7 +1066,7 @@ function normalizeCost(raw) {
1243
1066
  if (cacheWrite !== void 0) cost.cache_write = cacheWrite;
1244
1067
  const over200k = parseRateBlock(raw.context_over_200k);
1245
1068
  if (over200k) cost.context_over_200k = over200k;
1246
- const tiers = Array.isArray(raw.tiers) ? raw.tiers.filter(isRecord7) : [];
1069
+ const tiers = Array.isArray(raw.tiers) ? raw.tiers.filter(isRecord6) : [];
1247
1070
  for (const tier of tiers) {
1248
1071
  if (isExpressibleContextTier(tier)) {
1249
1072
  if (cost.context_over_200k === void 0) {
@@ -1273,10 +1096,10 @@ function swapLast(value, from, to) {
1273
1096
  return index === -1 ? value : `${value.slice(0, index)}${to}${value.slice(index + 1)}`;
1274
1097
  }
1275
1098
  function probeCandidates(slug2) {
1276
- const sep2 = slug2.indexOf("/");
1277
- if (sep2 <= 0 || sep2 >= slug2.length - 1) return [];
1278
- const vendor = slug2.slice(0, sep2);
1279
- const model = slug2.slice(sep2 + 1);
1099
+ const sep = slug2.indexOf("/");
1100
+ if (sep <= 0 || sep >= slug2.length - 1) return [];
1101
+ const vendor = slug2.slice(0, sep);
1102
+ const model = slug2.slice(sep + 1);
1280
1103
  const vendors = [vendor];
1281
1104
  const alias = VENDOR_ALIASES[vendor];
1282
1105
  if (alias !== void 0 && alias !== vendor) vendors.push(alias);
@@ -1301,7 +1124,7 @@ function probeCandidates(slug2) {
1301
1124
 
1302
1125
  // src/enrich/models-dev/lookup.ts
1303
1126
  function normalizeLimit(raw) {
1304
- if (!isRecord7(raw)) return void 0;
1127
+ if (!isRecord6(raw)) return void 0;
1305
1128
  const context = num(raw.context);
1306
1129
  const output = num(raw.output);
1307
1130
  if (context === void 0 || output === void 0) return void 0;
@@ -1330,9 +1153,9 @@ function buildLookup(model) {
1330
1153
  function lookupSlug(data, slug2) {
1331
1154
  for (const [providerKey, modelKey] of probeCandidates(slug2)) {
1332
1155
  const provider = data[providerKey];
1333
- if (!isRecord7(provider) || !isRecord7(provider.models)) continue;
1156
+ if (!isRecord6(provider) || !isRecord6(provider.models)) continue;
1334
1157
  const model = provider.models[modelKey];
1335
- if (isRecord7(model)) return buildLookup(model);
1158
+ if (isRecord6(model)) return buildLookup(model);
1336
1159
  }
1337
1160
  return void 0;
1338
1161
  }
@@ -1341,10 +1164,10 @@ function lookupSlug(data, slug2) {
1341
1164
  var CACHE_FILE = "models-dev.json";
1342
1165
  function readCache(cachePath) {
1343
1166
  try {
1344
- const parsed = JSON.parse(fs7.readFileSync(cachePath, "utf8"));
1345
- if (!isRecord7(parsed)) return void 0;
1167
+ const parsed = JSON.parse(fs5.readFileSync(cachePath, "utf8"));
1168
+ if (!isRecord6(parsed)) return void 0;
1346
1169
  const fetchedAt = num(parsed.fetchedAt);
1347
- if (fetchedAt === void 0 || !isRecord7(parsed.data)) return void 0;
1170
+ if (fetchedAt === void 0 || !isRecord6(parsed.data)) return void 0;
1348
1171
  return { fetchedAt, data: parsed.data };
1349
1172
  } catch {
1350
1173
  return void 0;
@@ -1352,8 +1175,8 @@ function readCache(cachePath) {
1352
1175
  }
1353
1176
  function writeCache(cachePath, envelope) {
1354
1177
  try {
1355
- fs7.mkdirSync(path9.dirname(cachePath), { recursive: true });
1356
- fs7.writeFileSync(cachePath, JSON.stringify(envelope), "utf8");
1178
+ fs5.mkdirSync(path6.dirname(cachePath), { recursive: true });
1179
+ fs5.writeFileSync(cachePath, JSON.stringify(envelope), "utf8");
1357
1180
  } catch (error) {
1358
1181
  logger.warn("failed to write models.dev cache:", error);
1359
1182
  }
@@ -1366,7 +1189,7 @@ async function loadModelsDev(options = {}) {
1366
1189
  const cacheDir = options.cacheDir ?? resolveCacheDir();
1367
1190
  const ttlMs = options.ttlMs ?? MODELS_DEV_TTL_MS;
1368
1191
  const now = options.now ?? Date.now;
1369
- const cachePath = path9.join(cacheDir, CACHE_FILE);
1192
+ const cachePath = path6.join(cacheDir, CACHE_FILE);
1370
1193
  const cached = readCache(cachePath);
1371
1194
  if (cached && now() - cached.fetchedAt < ttlMs)
1372
1195
  return makeSource("cache", cached.data);
@@ -1516,10 +1339,10 @@ function mergeConfig(cfg, blocks) {
1516
1339
 
1517
1340
  // src/config/collected-ref.ts
1518
1341
  function parseRef(ref, source) {
1519
- const sep2 = ref.indexOf("/");
1520
- if (sep2 <= 0 || sep2 >= ref.length - 1) return void 0;
1521
- const providerId = ref.slice(0, sep2);
1522
- const suffix = ref.slice(sep2 + 1);
1342
+ const sep = ref.indexOf("/");
1343
+ if (sep <= 0 || sep >= ref.length - 1) return void 0;
1344
+ const providerId = ref.slice(0, sep);
1345
+ const suffix = ref.slice(sep + 1);
1523
1346
  if (providerId === "route_33")
1524
1347
  return { providerId, modelKey: suffix, ref, source };
1525
1348
  if (providerId !== "route_33_anthropic") return void 0;
@@ -1549,34 +1372,34 @@ function collectCfgRefs(cfg) {
1549
1372
  }
1550
1373
 
1551
1374
  // src/config/collect-flavour-refs.ts
1552
- import * as fs8 from "fs";
1553
- import * as os5 from "os";
1554
- import * as path10 from "path";
1555
- import { parse as parseJsonc2 } from "jsonc-parser";
1556
- function isRecord8(value) {
1375
+ import * as fs6 from "fs";
1376
+ import * as os3 from "os";
1377
+ import * as path7 from "path";
1378
+ import { parse as parseJsonc } from "jsonc-parser";
1379
+ function isRecord7(value) {
1557
1380
  return typeof value === "object" && value !== null && !Array.isArray(value);
1558
1381
  }
1559
1382
  function findFlavourPath(env, cwd) {
1560
1383
  const candidates = [];
1561
1384
  const configDir = env.OPENCODE_CONFIG_DIR?.trim();
1562
- if (configDir) candidates.push(path10.join(configDir, "oh-my-openagent.jsonc"));
1563
- candidates.push(path10.join(cwd, ".opencode", "oh-my-openagent.jsonc"));
1564
- const configHome = env.XDG_CONFIG_HOME?.trim() || path10.join(os5.homedir(), ".config");
1565
- candidates.push(path10.join(configHome, "opencode", "oh-my-openagent.jsonc"));
1385
+ if (configDir) candidates.push(path7.join(configDir, "oh-my-openagent.jsonc"));
1386
+ candidates.push(path7.join(cwd, ".opencode", "oh-my-openagent.jsonc"));
1387
+ const configHome = env.XDG_CONFIG_HOME?.trim() || path7.join(os3.homedir(), ".config");
1388
+ candidates.push(path7.join(configHome, "opencode", "oh-my-openagent.jsonc"));
1566
1389
  for (const candidate of candidates) {
1567
1390
  try {
1568
- if (fs8.existsSync(candidate)) return candidate;
1391
+ if (fs6.existsSync(candidate)) return candidate;
1569
1392
  } catch {
1570
1393
  }
1571
1394
  }
1572
1395
  return void 0;
1573
1396
  }
1574
1397
  function collectSectionRefs(refs, section, label) {
1575
- if (!isRecord8(section)) return;
1398
+ if (!isRecord7(section)) return;
1576
1399
  for (const [name, entry] of Object.entries(section)) {
1577
- if (!isRecord8(entry)) continue;
1400
+ if (!isRecord7(entry)) continue;
1578
1401
  pushRef(refs, entry.model, `flavour ${label} "${name}"`);
1579
- if (label === "agent" && isRecord8(entry.ultrawork)) {
1402
+ if (label === "agent" && isRecord7(entry.ultrawork)) {
1580
1403
  pushRef(
1581
1404
  refs,
1582
1405
  entry.ultrawork.model,
@@ -1590,8 +1413,8 @@ function collectFlavourRefs(env, cwd) {
1590
1413
  if (flavourPath === void 0) return [];
1591
1414
  const refs = [];
1592
1415
  try {
1593
- const parsed = parseJsonc2(fs8.readFileSync(flavourPath, "utf8"));
1594
- if (!isRecord8(parsed)) {
1416
+ const parsed = parseJsonc(fs6.readFileSync(flavourPath, "utf8"));
1417
+ if (!isRecord7(parsed)) {
1595
1418
  logger.warn(
1596
1419
  `flavour file ${flavourPath} did not parse to an object \u2014 skipping flavour ref validation`
1597
1420
  );
@@ -1725,7 +1548,7 @@ function createConfigHook(options = {}) {
1725
1548
  let key;
1726
1549
  try {
1727
1550
  key = resolveKey(cfg, env, cwd);
1728
- const baseURL = resolveBaseUrl(cfg, env);
1551
+ const baseURL = resolveBaseUrl(cfg, env, cwd);
1729
1552
  const catalog = await getCatalog({
1730
1553
  key,
1731
1554
  baseURL,
@@ -1770,11 +1593,6 @@ function createConfigHook(options = {}) {
1770
1593
 
1771
1594
  // src/index.ts
1772
1595
  var Route33Plugin = async (input, _options) => {
1773
- ensureTuiRegistration({
1774
- worktree: input?.worktree ?? input?.directory,
1775
- env: process.env,
1776
- moduleUrl: import.meta.url
1777
- });
1778
1596
  ensureWorkspaceKeyGitignore(input?.worktree ?? input?.directory);
1779
1597
  return {
1780
1598
  config: createConfigHook(),
package/dist/tui/index.js CHANGED
@@ -1,6 +1,23 @@
1
1
  // src/shared/env-api-key.ts
2
2
  var ENV_API_KEY = "ROUTE33_API_KEY";
3
3
 
4
+ // src/shared/git-workspace.ts
5
+ import { existsSync } from "fs";
6
+ import * as path from "path";
7
+ function isInsideGitWorktree(startDir) {
8
+ try {
9
+ let dir = path.resolve(startDir);
10
+ for (; ; ) {
11
+ if (existsSync(path.join(dir, ".git"))) return true;
12
+ const parent = path.dirname(dir);
13
+ if (parent === dir) return false;
14
+ dir = parent;
15
+ }
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
4
21
  // src/shared/logger.ts
5
22
  var logger = {
6
23
  info: (...args) => {
@@ -16,12 +33,12 @@ var logger = {
16
33
 
17
34
  // src/shared/resolve-cache-dir.ts
18
35
  import * as os from "os";
19
- import * as path from "path";
36
+ import * as path2 from "path";
20
37
 
21
38
  // src/auth/auth-json.ts
22
39
  import * as fs from "fs";
23
40
  import * as os2 from "os";
24
- import * as path2 from "path";
41
+ import * as path3 from "path";
25
42
  var PROVIDER_ID = "route_33";
26
43
  function usable(value) {
27
44
  if (typeof value !== "string") return void 0;
@@ -33,8 +50,8 @@ function isRecord(value) {
33
50
  }
34
51
  function readAuthEntry(env = process.env) {
35
52
  try {
36
- const dataHome = usable(env.XDG_DATA_HOME) ?? path2.join(os2.homedir(), ".local", "share");
37
- const authPath = path2.join(dataHome, "opencode", "auth.json");
53
+ const dataHome = usable(env.XDG_DATA_HOME) ?? path3.join(os2.homedir(), ".local", "share");
54
+ const authPath = path3.join(dataHome, "opencode", "auth.json");
38
55
  const parsed = JSON.parse(fs.readFileSync(authPath, "utf8"));
39
56
  if (!isRecord(parsed)) return {};
40
57
  const entry = parsed[PROVIDER_ID];
@@ -52,9 +69,9 @@ function readAuthEntry(env = process.env) {
52
69
 
53
70
  // src/auth/workspace-key-file.ts
54
71
  import * as fs2 from "fs";
55
- import * as path3 from "path";
56
- var WORKSPACE_KEY_RELATIVE_PATH = path3.join(".opencode", "route33.key");
57
- var WORKSPACE_GITIGNORE_RELATIVE_PATH = path3.join(".opencode", ".gitignore");
72
+ import * as path4 from "path";
73
+ var WORKSPACE_KEY_RELATIVE_PATH = path4.join(".opencode", "route33.key");
74
+ var WORKSPACE_GITIGNORE_RELATIVE_PATH = path4.join(".opencode", ".gitignore");
58
75
  var GITIGNORE_ENTRIES = ["route33.key", "tui.json"];
59
76
  function usable2(value) {
60
77
  if (typeof value !== "string") return void 0;
@@ -68,7 +85,7 @@ function readWorkspaceKey(worktree) {
68
85
  if (worktree === void 0) return void 0;
69
86
  try {
70
87
  const raw = fs2.readFileSync(
71
- path3.join(worktree, WORKSPACE_KEY_RELATIVE_PATH),
88
+ path4.join(worktree, WORKSPACE_KEY_RELATIVE_PATH),
72
89
  "utf8"
73
90
  );
74
91
  const parsed = JSON.parse(raw);
@@ -80,10 +97,10 @@ function readWorkspaceKey(worktree) {
80
97
  }
81
98
  function ensureWorkspaceKeyGitignore(worktree) {
82
99
  if (worktree === void 0 || worktree.trim() === "") return;
83
- if (path3.resolve(worktree) === "/") return;
84
- const gitignorePath = path3.join(worktree, WORKSPACE_GITIGNORE_RELATIVE_PATH);
100
+ if (path4.resolve(worktree) === "/") return;
101
+ const gitignorePath = path4.join(worktree, WORKSPACE_GITIGNORE_RELATIVE_PATH);
85
102
  try {
86
- fs2.mkdirSync(path3.dirname(gitignorePath), { recursive: true });
103
+ fs2.mkdirSync(path4.dirname(gitignorePath), { recursive: true });
87
104
  let content = "";
88
105
  try {
89
106
  content = fs2.readFileSync(gitignorePath, "utf8");
@@ -107,8 +124,8 @@ function ensureWorkspaceKeyGitignore(worktree) {
107
124
  }
108
125
  function writeWorkspaceKey(worktree, key) {
109
126
  ensureWorkspaceKeyGitignore(worktree);
110
- const filePath = path3.join(worktree, WORKSPACE_KEY_RELATIVE_PATH);
111
- fs2.mkdirSync(path3.dirname(filePath), { recursive: true });
127
+ const filePath = path4.join(worktree, WORKSPACE_KEY_RELATIVE_PATH);
128
+ fs2.mkdirSync(path4.dirname(filePath), { recursive: true });
112
129
  fs2.writeFileSync(
113
130
  filePath,
114
131
  `${JSON.stringify({ type: "api", key }, null, 2)}
@@ -130,7 +147,7 @@ function keyFromConfig(cfg) {
130
147
  return void 0;
131
148
  }
132
149
  function resolveKey(cfg, env = process.env, worktree) {
133
- return usable3(env[ENV_API_KEY]) ?? keyFromConfig(cfg) ?? readWorkspaceKey(worktree) ?? readAuthEntry(env).key;
150
+ return usable3(env[ENV_API_KEY]) ?? keyFromConfig(cfg) ?? readWorkspaceKey(worktree) ?? (worktree !== void 0 && isInsideGitWorktree(worktree) ? void 0 : readAuthEntry(env).key);
134
151
  }
135
152
 
136
153
  // src/tui/key-prompt-ui.ts
@@ -274,23 +291,6 @@ function createDismissalStore(kv, key = PROJECT_KEY_PROMPT_DISMISSED_AT_KEY) {
274
291
  };
275
292
  }
276
293
 
277
- // src/tui/git-workspace.ts
278
- import { existsSync } from "fs";
279
- import * as path4 from "path";
280
- function isInsideGitWorktree(startDir) {
281
- try {
282
- let dir = path4.resolve(startDir);
283
- for (; ; ) {
284
- if (existsSync(path4.join(dir, ".git"))) return true;
285
- const parent = path4.dirname(dir);
286
- if (parent === dir) return false;
287
- dir = parent;
288
- }
289
- } catch {
290
- return false;
291
- }
292
- }
293
-
294
294
  // src/tui/key-prompt-copy.ts
295
295
  var PROJECT_COPY = {
296
296
  selectTitle: KEY_SELECT_TITLE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@route_33/opencode",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "description": "opencode provider plugin for the route33.ai AI gateway — key-scoped model discovery, cost/limit enrichment, auth, and the key-setup wizard.",
@@ -19,6 +19,10 @@
19
19
  "types": "./dist/index.d.ts",
20
20
  "import": "./dist/index.js"
21
21
  },
22
+ "./server": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ },
22
26
  "./tui": {
23
27
  "types": "./dist/tui/index.d.ts",
24
28
  "import": "./dist/tui/index.js"