@route_33/opencode 0.1.3 → 0.3.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. 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.
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). Skipping the project-key wizard snoozes it for 7 days — "Skip" for that workspace only, "Skip for all workspaces" everywhere (both live in opencode's TUI kv store; remove `project-key-prompt.dismissed-at*` from `~/.local/state/opencode/kv.json` to re-arm sooner). `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
@@ -245,215 +245,6 @@ function buildRoute33Auth(env = process.env, worktree) {
245
245
  };
246
246
  }
247
247
 
248
- // src/bootstrap/ensure-tui-registration.ts
249
- import * as path8 from "path";
250
-
251
- // src/bootstrap/global-context.ts
252
- import * as os3 from "os";
253
- import * as path5 from "path";
254
- import { fileURLToPath } from "url";
255
- function isGlobalContext(worktree, moduleUrl) {
256
- try {
257
- if (moduleUrl === void 0) return false;
258
- if (worktree === void 0) return true;
259
- const root = path5.resolve(worktree);
260
- if (root === "/") return true;
261
- const modulePath = fileURLToPath(moduleUrl);
262
- return !modulePath.startsWith(root + path5.sep);
263
- } catch {
264
- return false;
265
- }
266
- }
267
- function usable5(value) {
268
- const trimmed = value?.trim();
269
- return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
270
- }
271
- function globalTuiJsonPath(env) {
272
- const configHome = usable5(env.XDG_CONFIG_HOME) ?? path5.join(os3.homedir(), ".config");
273
- return path5.join(configHome, "opencode", "tui.json");
274
- }
275
-
276
- // src/bootstrap/tui-json-upsert.ts
277
- import * as fs4 from "fs";
278
- import * as path7 from "path";
279
- import {
280
- applyEdits,
281
- modify,
282
- parse as parseJsonc
283
- } from "jsonc-parser";
284
-
285
- // src/bootstrap/tui-spec-resolution.ts
286
- import * as fs3 from "fs";
287
- import * as os4 from "os";
288
- import * as path6 from "path";
289
- import { fileURLToPath as fileURLToPath2 } from "url";
290
- var NPM_TUI_SPEC = "@route_33/opencode";
291
- var DEAD_SUBPATH_TUI_SPEC = "@route_33/opencode/tui";
292
- var OLD_NPM_TUI_SPEC = "opencode-route33/tui";
293
- var TUI_BUNDLE_NAME = "route33-tui.js";
294
- var TUI_SCHEMA = "https://opencode.ai/tui.json";
295
- var SERVER_BUNDLE_NAME = "route33.js";
296
- var DIST_TUI_ENTRY_SUFFIX = "/opencode-route33/dist/tui/index.js";
297
- var SCOPED_DIST_TUI_ENTRY_SUFFIX = "/@route_33/opencode/dist/tui/index.js";
298
- var NPM_PACKAGE_DIR = path6.join("node_modules", "@route_33", "opencode");
299
- function fileExists(candidate) {
300
- try {
301
- return fs3.statSync(candidate).isFile();
302
- } catch {
303
- return false;
304
- }
305
- }
306
- function npmSpecFromModulePath(selfPath) {
307
- const segments = selfPath.split(path6.sep);
308
- for (let i = 0; i < segments.length - 1; i++) {
309
- const pkgDir = segments[i + 1];
310
- if (segments[i] === "@route_33" && pkgDir !== void 0 && pkgDir.startsWith("opencode@")) {
311
- const pin = pkgDir.slice("opencode@".length);
312
- if (pin !== "") return `${NPM_TUI_SPEC}@${pin}`;
313
- }
314
- }
315
- return selfPath.includes(`${NPM_PACKAGE_DIR}${path6.sep}`) ? NPM_TUI_SPEC : void 0;
316
- }
317
- function specFromModuleUrl(moduleUrl) {
318
- try {
319
- if (moduleUrl === void 0) return void 0;
320
- const selfPath = fileURLToPath2(moduleUrl);
321
- const dir = path6.dirname(selfPath);
322
- if (path6.basename(selfPath) === SERVER_BUNDLE_NAME) {
323
- const sibling = path6.join(dir, TUI_BUNDLE_NAME);
324
- return fileExists(sibling) ? sibling : void 0;
325
- }
326
- if (path6.basename(selfPath) === "index.js" && path6.basename(dir) === "dist") {
327
- const npmSpec = npmSpecFromModulePath(selfPath);
328
- if (npmSpec !== void 0) return npmSpec;
329
- const tuiEntry = path6.join(dir, "tui", "index.js");
330
- return fileExists(tuiEntry) ? tuiEntry : void 0;
331
- }
332
- return void 0;
333
- } catch {
334
- return void 0;
335
- }
336
- }
337
- function resolveTuiSpec(worktree, env, moduleUrl) {
338
- const fromModule = specFromModuleUrl(moduleUrl);
339
- if (fromModule !== void 0) return fromModule;
340
- if (worktree !== void 0 && worktree.trim() !== "" && path6.resolve(worktree) !== "/") {
341
- const local = path6.join(worktree, ".opencode", "plugins", TUI_BUNDLE_NAME);
342
- if (fileExists(local)) return local;
343
- }
344
- const configHome = env.XDG_CONFIG_HOME?.trim() || path6.join(os4.homedir(), ".config");
345
- const global = path6.join(configHome, "opencode", "plugins", TUI_BUNDLE_NAME);
346
- if (fileExists(global)) return global;
347
- return NPM_TUI_SPEC;
348
- }
349
- function isOurEntry(entry) {
350
- return typeof entry === "string" && (entry === NPM_TUI_SPEC || entry.startsWith(`${NPM_TUI_SPEC}@`) || entry === DEAD_SUBPATH_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));
351
- }
352
-
353
- // src/bootstrap/tui-json-upsert.ts
354
- function isRecord3(value) {
355
- return typeof value === "object" && value !== null && !Array.isArray(value);
356
- }
357
- function fileExists2(candidate) {
358
- try {
359
- return fs4.statSync(candidate).isFile();
360
- } catch {
361
- return false;
362
- }
363
- }
364
- function upsertTuiSpec(file, spec) {
365
- try {
366
- if (!fileExists2(file)) {
367
- fs4.mkdirSync(path7.dirname(file), { recursive: true });
368
- fs4.writeFileSync(
369
- file,
370
- JSON.stringify({ $schema: TUI_SCHEMA, plugin: [spec] }, null, 2) + "\n",
371
- "utf8"
372
- );
373
- return;
374
- }
375
- const text = fs4.readFileSync(file, "utf8");
376
- const errors = [];
377
- const parsed = parseJsonc(text, errors, {
378
- allowTrailingComma: true,
379
- allowEmptyContent: true
380
- });
381
- if (!isRecord3(parsed)) {
382
- logger.warn(
383
- `tui.json at ${file} did not parse to an object \u2014 skipping TUI registration`
384
- );
385
- return;
386
- }
387
- if (parsed.plugin !== void 0 && !Array.isArray(parsed.plugin)) {
388
- logger.warn(
389
- `tui.json at ${file} has a non-array "plugin" key \u2014 skipping TUI registration`
390
- );
391
- return;
392
- }
393
- const entries = Array.isArray(parsed.plugin) ? parsed.plugin : [];
394
- const ourIndices = entries.flatMap(
395
- (entry, index) => isOurEntry(entry) ? [index] : []
396
- );
397
- const firstOurs = ourIndices[0];
398
- if (ourIndices.length === 1 && firstOurs !== void 0 && entries[firstOurs] === spec)
399
- return;
400
- const formattingOptions = { insertSpaces: true, tabSize: 2 };
401
- let next = text;
402
- if (!Array.isArray(parsed.plugin)) {
403
- next = applyEdits(
404
- next,
405
- modify(next, ["plugin"], [spec], { formattingOptions })
406
- );
407
- } else if (firstOurs === void 0) {
408
- next = applyEdits(
409
- next,
410
- modify(next, ["plugin", -1], spec, {
411
- formattingOptions,
412
- isArrayInsertion: true
413
- })
414
- );
415
- } else {
416
- if (entries[firstOurs] !== spec) {
417
- next = applyEdits(
418
- next,
419
- modify(next, ["plugin", firstOurs], spec, { formattingOptions })
420
- );
421
- }
422
- for (const index of ourIndices.slice(1).sort((a, b) => b - a)) {
423
- next = applyEdits(
424
- next,
425
- modify(next, ["plugin", index], void 0, { formattingOptions })
426
- );
427
- }
428
- }
429
- if (next !== text) fs4.writeFileSync(file, next, "utf8");
430
- } catch (error) {
431
- logger.warn(
432
- `could not register the route33 TUI plugin in tui.json: ${String(error)}`
433
- );
434
- }
435
- }
436
-
437
- // src/bootstrap/ensure-tui-registration.ts
438
- function ensureTuiRegistration(deps) {
439
- try {
440
- const { worktree, env } = deps;
441
- if (env.ROUTE33_TUI_PROMPT === "0") return;
442
- const usableWorktree = worktree !== void 0 && worktree.trim() !== "" && path8.resolve(worktree) !== "/" ? worktree : void 0;
443
- const spec = resolveTuiSpec(usableWorktree, env, deps.moduleUrl);
444
- if (usableWorktree !== void 0) {
445
- upsertTuiSpec(path8.join(usableWorktree, ".opencode", "tui.json"), spec);
446
- }
447
- if (isGlobalContext(worktree, deps.moduleUrl)) {
448
- upsertTuiSpec(globalTuiJsonPath(env), spec);
449
- }
450
- } catch (error) {
451
- logger.warn(
452
- `could not register the route33 TUI plugin in tui.json: ${String(error)}`
453
- );
454
- }
455
- }
456
-
457
248
  // src/config/live-auth-fetch.ts
458
249
  function buildLiveAuthFetch(resolveKeyNow, innerFetch) {
459
250
  return (input, init) => {
@@ -595,24 +386,24 @@ function hashKey(key) {
595
386
  }
596
387
 
597
388
  // src/catalog/catalog-cache/read-disk.ts
598
- import * as fs5 from "fs";
389
+ import * as fs3 from "fs";
599
390
 
600
391
  // src/catalog/catalog-cache/cache-file-path.ts
601
- import * as path9 from "path";
392
+ import * as path5 from "path";
602
393
  function cacheFilePath(keyHash, env) {
603
- return path9.join(resolveCacheDir(env), `catalog-${keyHash}.json`);
394
+ return path5.join(resolveCacheDir(env), `catalog-${keyHash}.json`);
604
395
  }
605
396
 
606
397
  // src/catalog/catalog-cache/read-disk.ts
607
- function isRecord4(value) {
398
+ function isRecord3(value) {
608
399
  return typeof value === "object" && value !== null && !Array.isArray(value);
609
400
  }
610
401
  function readDisk(keyHash, env) {
611
402
  try {
612
403
  const parsed = JSON.parse(
613
- fs5.readFileSync(cacheFilePath(keyHash, env), "utf8")
404
+ fs3.readFileSync(cacheFilePath(keyHash, env), "utf8")
614
405
  );
615
- if (!isRecord4(parsed) || typeof parsed.fetchedAt !== "number" || !isRecord4(parsed.models))
406
+ if (!isRecord3(parsed) || typeof parsed.fetchedAt !== "number" || !isRecord3(parsed.models))
616
407
  return void 0;
617
408
  return {
618
409
  fetchedAt: parsed.fetchedAt,
@@ -654,15 +445,15 @@ function readStaleCatalog(opts) {
654
445
  }
655
446
 
656
447
  // src/catalog/catalog-cache/write-catalog.ts
657
- import * as fs6 from "fs";
448
+ import * as fs4 from "fs";
658
449
  function writeCatalog(opts) {
659
450
  const { key, models, env = process.env, now = Date.now() } = opts;
660
451
  const keyHash = hashKey(key);
661
452
  const record = { fetchedAt: now, keyHash, models };
662
453
  memory.set(keyHash, record);
663
454
  try {
664
- fs6.mkdirSync(resolveCacheDir(env), { recursive: true });
665
- fs6.writeFileSync(cacheFilePath(keyHash, env), JSON.stringify(record));
455
+ fs4.mkdirSync(resolveCacheDir(env), { recursive: true });
456
+ fs4.writeFileSync(cacheFilePath(keyHash, env), JSON.stringify(record));
666
457
  } catch (error) {
667
458
  logger.warn(
668
459
  `could not persist catalog cache: ${error instanceof Error ? error.message : String(error)}`
@@ -686,7 +477,7 @@ var CatalogFetchError = class extends Error {
686
477
  var DEFAULT_FETCH_TIMEOUT_MS = 5e3;
687
478
 
688
479
  // src/catalog/extract-pricing.ts
689
- function isRecord5(value) {
480
+ function isRecord4(value) {
690
481
  return typeof value === "object" && value !== null && !Array.isArray(value);
691
482
  }
692
483
  var OPENROUTER_KEYS = [
@@ -743,7 +534,7 @@ function extractModelsDevPricing(raw) {
743
534
  return cost;
744
535
  }
745
536
  function extractPricing(raw) {
746
- if (!isRecord5(raw)) return void 0;
537
+ if (!isRecord4(raw)) return void 0;
747
538
  const hasOpenRouterKeys = OPENROUTER_KEYS.some((key) => key in raw);
748
539
  const hasModelsDevKeys = MODELS_DEV_KEYS.some((key) => key in raw);
749
540
  if (hasOpenRouterKeys && hasModelsDevKeys) return void 0;
@@ -774,12 +565,12 @@ function normalizeId(raw, env = process.env) {
774
565
  }
775
566
 
776
567
  // src/catalog/fetch-catalog.ts
777
- function isRecord6(value) {
568
+ function isRecord5(value) {
778
569
  return typeof value === "object" && value !== null && !Array.isArray(value);
779
570
  }
780
571
  function parseEntries(payload) {
781
572
  if (Array.isArray(payload)) return payload;
782
- if (isRecord6(payload) && Array.isArray(payload.data)) return payload.data;
573
+ if (isRecord5(payload) && Array.isArray(payload.data)) return payload.data;
783
574
  throw new CatalogFetchError(
784
575
  "parse",
785
576
  "response is neither {data:[\u2026]} nor a bare array"
@@ -788,13 +579,13 @@ function parseEntries(payload) {
788
579
  function toCatalogModels(entries, env) {
789
580
  const models = {};
790
581
  for (const entry of entries) {
791
- if (!isRecord6(entry) || typeof entry.id !== "string") continue;
582
+ if (!isRecord5(entry) || typeof entry.id !== "string") continue;
792
583
  const slug2 = normalizeId(entry.id, env);
793
584
  if (!slug2 || models[slug2]) continue;
794
585
  const model = { id: slug2 };
795
586
  const displayName = typeof entry.display_name === "string" ? entry.display_name.trim() : "";
796
587
  if (displayName !== "") model.displayName = displayName;
797
- if (isRecord6(entry.pricing) && extractPricing(entry.pricing) !== void 0)
588
+ if (isRecord5(entry.pricing) && extractPricing(entry.pricing) !== void 0)
798
589
  model.pricing = entry.pricing;
799
590
  models[slug2] = model;
800
591
  }
@@ -1203,11 +994,11 @@ function enrichModel(slug2, sources) {
1203
994
  }
1204
995
 
1205
996
  // src/enrich/models-dev/load-models-dev.ts
1206
- import * as fs7 from "fs";
1207
- import * as path10 from "path";
997
+ import * as fs5 from "fs";
998
+ import * as path6 from "path";
1208
999
 
1209
1000
  // src/enrich/models-dev/is-record.ts
1210
- function isRecord7(value) {
1001
+ function isRecord6(value) {
1211
1002
  return typeof value === "object" && value !== null && !Array.isArray(value);
1212
1003
  }
1213
1004
 
@@ -1226,7 +1017,7 @@ async function fetchLive(fetchImpl) {
1226
1017
  return void 0;
1227
1018
  }
1228
1019
  const body = await response.json();
1229
- return isRecord7(body) ? body : void 0;
1020
+ return isRecord6(body) ? body : void 0;
1230
1021
  } catch (error) {
1231
1022
  logger.warn("models.dev fetch failed:", error);
1232
1023
  return void 0;
@@ -1243,7 +1034,7 @@ function num(value) {
1243
1034
  // src/enrich/models-dev/normalize-cost.ts
1244
1035
  var EXPRESSIBLE_TIER_SIZE = 2e5;
1245
1036
  function parseRateBlock(value) {
1246
- if (!isRecord7(value)) return void 0;
1037
+ if (!isRecord6(value)) return void 0;
1247
1038
  const input = num(value.input);
1248
1039
  const output = num(value.output);
1249
1040
  if (input === void 0 || output === void 0) return void 0;
@@ -1256,7 +1047,7 @@ function parseRateBlock(value) {
1256
1047
  }
1257
1048
  function isExpressibleContextTier(tier) {
1258
1049
  const marker = tier.tier;
1259
- return isRecord7(marker) && marker.type === "context" && marker.size === EXPRESSIBLE_TIER_SIZE;
1050
+ return isRecord6(marker) && marker.type === "context" && marker.size === EXPRESSIBLE_TIER_SIZE;
1260
1051
  }
1261
1052
  function maxRate(current, candidate) {
1262
1053
  const rate = num(candidate);
@@ -1264,7 +1055,7 @@ function maxRate(current, candidate) {
1264
1055
  return current === void 0 || rate > current ? rate : current;
1265
1056
  }
1266
1057
  function normalizeCost(raw) {
1267
- if (!isRecord7(raw)) return void 0;
1058
+ if (!isRecord6(raw)) return void 0;
1268
1059
  const input = num(raw.input);
1269
1060
  const output = num(raw.output);
1270
1061
  if (input === void 0 || output === void 0) return void 0;
@@ -1275,7 +1066,7 @@ function normalizeCost(raw) {
1275
1066
  if (cacheWrite !== void 0) cost.cache_write = cacheWrite;
1276
1067
  const over200k = parseRateBlock(raw.context_over_200k);
1277
1068
  if (over200k) cost.context_over_200k = over200k;
1278
- const tiers = Array.isArray(raw.tiers) ? raw.tiers.filter(isRecord7) : [];
1069
+ const tiers = Array.isArray(raw.tiers) ? raw.tiers.filter(isRecord6) : [];
1279
1070
  for (const tier of tiers) {
1280
1071
  if (isExpressibleContextTier(tier)) {
1281
1072
  if (cost.context_over_200k === void 0) {
@@ -1305,10 +1096,10 @@ function swapLast(value, from, to) {
1305
1096
  return index === -1 ? value : `${value.slice(0, index)}${to}${value.slice(index + 1)}`;
1306
1097
  }
1307
1098
  function probeCandidates(slug2) {
1308
- const sep3 = slug2.indexOf("/");
1309
- if (sep3 <= 0 || sep3 >= slug2.length - 1) return [];
1310
- const vendor = slug2.slice(0, sep3);
1311
- const model = slug2.slice(sep3 + 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);
1312
1103
  const vendors = [vendor];
1313
1104
  const alias = VENDOR_ALIASES[vendor];
1314
1105
  if (alias !== void 0 && alias !== vendor) vendors.push(alias);
@@ -1333,7 +1124,7 @@ function probeCandidates(slug2) {
1333
1124
 
1334
1125
  // src/enrich/models-dev/lookup.ts
1335
1126
  function normalizeLimit(raw) {
1336
- if (!isRecord7(raw)) return void 0;
1127
+ if (!isRecord6(raw)) return void 0;
1337
1128
  const context = num(raw.context);
1338
1129
  const output = num(raw.output);
1339
1130
  if (context === void 0 || output === void 0) return void 0;
@@ -1362,9 +1153,9 @@ function buildLookup(model) {
1362
1153
  function lookupSlug(data, slug2) {
1363
1154
  for (const [providerKey, modelKey] of probeCandidates(slug2)) {
1364
1155
  const provider = data[providerKey];
1365
- if (!isRecord7(provider) || !isRecord7(provider.models)) continue;
1156
+ if (!isRecord6(provider) || !isRecord6(provider.models)) continue;
1366
1157
  const model = provider.models[modelKey];
1367
- if (isRecord7(model)) return buildLookup(model);
1158
+ if (isRecord6(model)) return buildLookup(model);
1368
1159
  }
1369
1160
  return void 0;
1370
1161
  }
@@ -1373,10 +1164,10 @@ function lookupSlug(data, slug2) {
1373
1164
  var CACHE_FILE = "models-dev.json";
1374
1165
  function readCache(cachePath) {
1375
1166
  try {
1376
- const parsed = JSON.parse(fs7.readFileSync(cachePath, "utf8"));
1377
- if (!isRecord7(parsed)) return void 0;
1167
+ const parsed = JSON.parse(fs5.readFileSync(cachePath, "utf8"));
1168
+ if (!isRecord6(parsed)) return void 0;
1378
1169
  const fetchedAt = num(parsed.fetchedAt);
1379
- if (fetchedAt === void 0 || !isRecord7(parsed.data)) return void 0;
1170
+ if (fetchedAt === void 0 || !isRecord6(parsed.data)) return void 0;
1380
1171
  return { fetchedAt, data: parsed.data };
1381
1172
  } catch {
1382
1173
  return void 0;
@@ -1384,8 +1175,8 @@ function readCache(cachePath) {
1384
1175
  }
1385
1176
  function writeCache(cachePath, envelope) {
1386
1177
  try {
1387
- fs7.mkdirSync(path10.dirname(cachePath), { recursive: true });
1388
- fs7.writeFileSync(cachePath, JSON.stringify(envelope), "utf8");
1178
+ fs5.mkdirSync(path6.dirname(cachePath), { recursive: true });
1179
+ fs5.writeFileSync(cachePath, JSON.stringify(envelope), "utf8");
1389
1180
  } catch (error) {
1390
1181
  logger.warn("failed to write models.dev cache:", error);
1391
1182
  }
@@ -1398,7 +1189,7 @@ async function loadModelsDev(options = {}) {
1398
1189
  const cacheDir = options.cacheDir ?? resolveCacheDir();
1399
1190
  const ttlMs = options.ttlMs ?? MODELS_DEV_TTL_MS;
1400
1191
  const now = options.now ?? Date.now;
1401
- const cachePath = path10.join(cacheDir, CACHE_FILE);
1192
+ const cachePath = path6.join(cacheDir, CACHE_FILE);
1402
1193
  const cached = readCache(cachePath);
1403
1194
  if (cached && now() - cached.fetchedAt < ttlMs)
1404
1195
  return makeSource("cache", cached.data);
@@ -1548,10 +1339,10 @@ function mergeConfig(cfg, blocks) {
1548
1339
 
1549
1340
  // src/config/collected-ref.ts
1550
1341
  function parseRef(ref, source) {
1551
- const sep3 = ref.indexOf("/");
1552
- if (sep3 <= 0 || sep3 >= ref.length - 1) return void 0;
1553
- const providerId = ref.slice(0, sep3);
1554
- const suffix = ref.slice(sep3 + 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);
1555
1346
  if (providerId === "route_33")
1556
1347
  return { providerId, modelKey: suffix, ref, source };
1557
1348
  if (providerId !== "route_33_anthropic") return void 0;
@@ -1581,34 +1372,34 @@ function collectCfgRefs(cfg) {
1581
1372
  }
1582
1373
 
1583
1374
  // src/config/collect-flavour-refs.ts
1584
- import * as fs8 from "fs";
1585
- import * as os5 from "os";
1586
- import * as path11 from "path";
1587
- import { parse as parseJsonc2 } from "jsonc-parser";
1588
- 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) {
1589
1380
  return typeof value === "object" && value !== null && !Array.isArray(value);
1590
1381
  }
1591
1382
  function findFlavourPath(env, cwd) {
1592
1383
  const candidates = [];
1593
1384
  const configDir = env.OPENCODE_CONFIG_DIR?.trim();
1594
- if (configDir) candidates.push(path11.join(configDir, "oh-my-openagent.jsonc"));
1595
- candidates.push(path11.join(cwd, ".opencode", "oh-my-openagent.jsonc"));
1596
- const configHome = env.XDG_CONFIG_HOME?.trim() || path11.join(os5.homedir(), ".config");
1597
- candidates.push(path11.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"));
1598
1389
  for (const candidate of candidates) {
1599
1390
  try {
1600
- if (fs8.existsSync(candidate)) return candidate;
1391
+ if (fs6.existsSync(candidate)) return candidate;
1601
1392
  } catch {
1602
1393
  }
1603
1394
  }
1604
1395
  return void 0;
1605
1396
  }
1606
1397
  function collectSectionRefs(refs, section, label) {
1607
- if (!isRecord8(section)) return;
1398
+ if (!isRecord7(section)) return;
1608
1399
  for (const [name, entry] of Object.entries(section)) {
1609
- if (!isRecord8(entry)) continue;
1400
+ if (!isRecord7(entry)) continue;
1610
1401
  pushRef(refs, entry.model, `flavour ${label} "${name}"`);
1611
- if (label === "agent" && isRecord8(entry.ultrawork)) {
1402
+ if (label === "agent" && isRecord7(entry.ultrawork)) {
1612
1403
  pushRef(
1613
1404
  refs,
1614
1405
  entry.ultrawork.model,
@@ -1622,8 +1413,8 @@ function collectFlavourRefs(env, cwd) {
1622
1413
  if (flavourPath === void 0) return [];
1623
1414
  const refs = [];
1624
1415
  try {
1625
- const parsed = parseJsonc2(fs8.readFileSync(flavourPath, "utf8"));
1626
- if (!isRecord8(parsed)) {
1416
+ const parsed = parseJsonc(fs6.readFileSync(flavourPath, "utf8"));
1417
+ if (!isRecord7(parsed)) {
1627
1418
  logger.warn(
1628
1419
  `flavour file ${flavourPath} did not parse to an object \u2014 skipping flavour ref validation`
1629
1420
  );
@@ -1802,11 +1593,6 @@ function createConfigHook(options = {}) {
1802
1593
 
1803
1594
  // src/index.ts
1804
1595
  var Route33Plugin = async (input, _options) => {
1805
- ensureTuiRegistration({
1806
- worktree: input?.worktree ?? input?.directory,
1807
- env: process.env,
1808
- moduleUrl: import.meta.url
1809
- });
1810
1596
  ensureWorkspaceKeyGitignore(input?.worktree ?? input?.directory);
1811
1597
  return {
1812
1598
  config: createConfigHook(),
package/dist/tui/index.js CHANGED
@@ -155,7 +155,9 @@ var KEY_SELECT_TITLE = "route33.ai project key";
155
155
  var KEY_SELECT_ENTER_LABEL = "Enter a project key";
156
156
  var KEY_SELECT_ENTER_DESCRIPTION = "Per workspace on purpose \u2014 one shared key would lose the per-project metrics";
157
157
  var KEY_SELECT_SKIP_LABEL = "Skip";
158
- var KEY_SELECT_SKIP_DESCRIPTION = "Ask again in 7 days";
158
+ var KEY_SELECT_SKIP_DESCRIPTION = "Ask again in 7 days \u2014 this workspace only";
159
+ var KEY_SELECT_SKIP_ALL_LABEL = "Skip for all workspaces";
160
+ var KEY_SELECT_SKIP_ALL_DESCRIPTION = "Ask again in 7 days \u2014 everywhere";
159
161
  var KEY_SELECT_PLACEHOLDER = "No route33.ai key found for this workspace yet.";
160
162
  var KEY_PROMPT_TITLE = "Paste your route33.ai project key";
161
163
  var KEY_SAVED_MESSAGE = "route33.ai project key saved to .opencode/route33.key \u2014 live immediately, no restart needed.";
@@ -211,8 +213,10 @@ async function runProjectKeyPromptFlow({
211
213
  dismissal,
212
214
  now = Date.now
213
215
  }) {
214
- if (await ui.askChoice() === "skip") {
215
- dismissal.dismiss(now());
216
+ const choice = await ui.askChoice();
217
+ if (choice === "skip" || choice === "skip-all") {
218
+ if (choice === "skip-all") dismissal.dismissGlobally(now());
219
+ else dismissal.dismiss(now());
216
220
  ui.close();
217
221
  return "skipped";
218
222
  }
@@ -262,6 +266,9 @@ async function runActivation(deps) {
262
266
  }
263
267
  }
264
268
 
269
+ // src/tui/dismissal-store.ts
270
+ import { resolve as resolve3 } from "path";
271
+
265
272
  // src/tui/dismissal-cooldown.ts
266
273
  var DISMISSAL_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1e3;
267
274
  function isWithinCooldown(dismissedAt, now) {
@@ -279,15 +286,53 @@ function readDismissedAt(kv, key) {
279
286
  return void 0;
280
287
  }
281
288
  }
282
- function createDismissalStore(kv, key = PROJECT_KEY_PROMPT_DISMISSED_AT_KEY) {
289
+ function readByWorkspace(kv, key) {
290
+ try {
291
+ const value = kv.get(key);
292
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
293
+ return {};
294
+ }
295
+ const entries = {};
296
+ for (const [path5, at] of Object.entries(value)) {
297
+ if (typeof at === "number" && Number.isFinite(at)) entries[path5] = at;
298
+ }
299
+ return entries;
300
+ } catch {
301
+ return {};
302
+ }
303
+ }
304
+ function createDismissalStore(kv, key = PROJECT_KEY_PROMPT_DISMISSED_AT_KEY, worktree) {
305
+ const scoped = worktree !== void 0 ? resolve3(worktree) : void 0;
306
+ const byWorkspaceKey = `${key}.by-workspace`;
307
+ const dismissGlobally = (now) => {
308
+ try {
309
+ kv.set(key, now);
310
+ } catch {
311
+ }
312
+ };
283
313
  return {
284
- isDismissed: (now) => isWithinCooldown(readDismissedAt(kv, key), now),
314
+ isDismissed: (now) => {
315
+ if (isWithinCooldown(readDismissedAt(kv, key), now)) return true;
316
+ if (scoped === void 0) return false;
317
+ return isWithinCooldown(readByWorkspace(kv, byWorkspaceKey)[scoped], now);
318
+ },
285
319
  dismiss: (now) => {
320
+ if (scoped === void 0) {
321
+ dismissGlobally(now);
322
+ return;
323
+ }
286
324
  try {
287
- kv.set(key, now);
325
+ const entries = readByWorkspace(kv, byWorkspaceKey);
326
+ const pruned = {};
327
+ for (const [path5, at] of Object.entries(entries)) {
328
+ if (isWithinCooldown(at, now)) pruned[path5] = at;
329
+ }
330
+ pruned[scoped] = now;
331
+ kv.set(byWorkspaceKey, pruned);
288
332
  } catch {
289
333
  }
290
- }
334
+ },
335
+ dismissGlobally
291
336
  };
292
337
  }
293
338
 
@@ -298,6 +343,8 @@ var PROJECT_COPY = {
298
343
  enterDescription: KEY_SELECT_ENTER_DESCRIPTION,
299
344
  skipLabel: KEY_SELECT_SKIP_LABEL,
300
345
  skipDescription: KEY_SELECT_SKIP_DESCRIPTION,
346
+ skipAllLabel: KEY_SELECT_SKIP_ALL_LABEL,
347
+ skipAllDescription: KEY_SELECT_SKIP_ALL_DESCRIPTION,
301
348
  selectPlaceholder: KEY_SELECT_PLACEHOLDER,
302
349
  promptTitle: KEY_PROMPT_TITLE
303
350
  };
@@ -314,12 +361,12 @@ var GLOBAL_COPY = {
314
361
  // src/tui/tui-api-adapter.ts
315
362
  var READY_POLL_INTERVAL_MS = 100;
316
363
  function askChoice(api, copy) {
317
- return new Promise((resolve3) => {
364
+ return new Promise((resolve4) => {
318
365
  let settled = false;
319
366
  const settle = (choice) => {
320
367
  if (settled) return;
321
368
  settled = true;
322
- resolve3(choice);
369
+ resolve4(choice);
323
370
  };
324
371
  api.ui.dialog.replace(
325
372
  () => api.ui.DialogSelect({
@@ -335,10 +382,21 @@ function askChoice(api, copy) {
335
382
  title: copy.skipLabel,
336
383
  value: "skip",
337
384
  description: copy.skipDescription
338
- }
385
+ },
386
+ // Only the project copy defines a skip-all scope; the global
387
+ // prompt's plain skip is global already.
388
+ ...copy.skipAllLabel !== void 0 ? [
389
+ {
390
+ title: copy.skipAllLabel,
391
+ value: "skip-all",
392
+ description: copy.skipAllDescription ?? ""
393
+ }
394
+ ] : []
339
395
  ],
340
396
  onSelect: (option) => {
341
- settle(option.value === "enter" ? "enter" : "skip");
397
+ settle(
398
+ option.value === "enter" ? "enter" : option.value === "skip-all" ? "skip-all" : "skip"
399
+ );
342
400
  }
343
401
  }),
344
402
  () => {
@@ -348,12 +406,12 @@ function askChoice(api, copy) {
348
406
  });
349
407
  }
350
408
  function askKey(api, copy) {
351
- return new Promise((resolve3) => {
409
+ return new Promise((resolve4) => {
352
410
  let settled = false;
353
411
  const settle = (value) => {
354
412
  if (settled) return;
355
413
  settled = true;
356
- resolve3(value);
414
+ resolve4(value);
357
415
  };
358
416
  api.ui.dialog.replace(
359
417
  () => api.ui.DialogPrompt({
@@ -375,20 +433,20 @@ function askKey(api, copy) {
375
433
  });
376
434
  }
377
435
  function waitUntilReady(api) {
378
- return new Promise((resolve3) => {
436
+ return new Promise((resolve4) => {
379
437
  if (api.state.ready) {
380
- resolve3(true);
438
+ resolve4(true);
381
439
  return;
382
440
  }
383
441
  const timer = setInterval(() => {
384
442
  if (api.lifecycle.signal.aborted) {
385
443
  clearInterval(timer);
386
- resolve3(false);
444
+ resolve4(false);
387
445
  return;
388
446
  }
389
447
  if (api.state.ready) {
390
448
  clearInterval(timer);
391
- resolve3(true);
449
+ resolve4(true);
392
450
  }
393
451
  }, READY_POLL_INTERVAL_MS);
394
452
  });
@@ -448,7 +506,11 @@ async function activate(api) {
448
506
  await runActivation({
449
507
  ui,
450
508
  keyStore,
451
- dismissal: createDismissalStore(kv),
509
+ dismissal: createDismissalStore(
510
+ kv,
511
+ PROJECT_KEY_PROMPT_DISMISSED_AT_KEY,
512
+ process.cwd()
513
+ ),
452
514
  globalUi,
453
515
  globalStore,
454
516
  globalDismissal: createDismissalStore(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@route_33/opencode",
3
- "version": "0.1.3",
3
+ "version": "0.3.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"