@route_33/opencode 0.1.2

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.
@@ -0,0 +1,5 @@
1
+ import { TuiPluginModule } from '@opencode-ai/plugin/tui';
2
+
3
+ declare const Route33TuiPlugin: TuiPluginModule;
4
+
5
+ export { Route33TuiPlugin as default };
@@ -0,0 +1,476 @@
1
+ // src/shared/env-api-key.ts
2
+ var ENV_API_KEY = "ROUTE33_API_KEY";
3
+
4
+ // src/shared/logger.ts
5
+ var logger = {
6
+ info: (...args) => {
7
+ console.log("[route_33]", ...args);
8
+ },
9
+ warn: (...args) => {
10
+ console.warn("[route_33]", ...args);
11
+ },
12
+ error: (...args) => {
13
+ console.error("[route_33]", ...args);
14
+ }
15
+ };
16
+
17
+ // src/shared/resolve-cache-dir.ts
18
+ import * as os from "os";
19
+ import * as path from "path";
20
+
21
+ // src/auth/auth-json.ts
22
+ import * as fs from "fs";
23
+ import * as os2 from "os";
24
+ import * as path2 from "path";
25
+ var PROVIDER_ID = "route_33";
26
+ function usable(value) {
27
+ if (typeof value !== "string") return void 0;
28
+ const trimmed = value.trim();
29
+ return trimmed ? trimmed : void 0;
30
+ }
31
+ function isRecord(value) {
32
+ return typeof value === "object" && value !== null && !Array.isArray(value);
33
+ }
34
+ function readAuthEntry(env = process.env) {
35
+ try {
36
+ const dataHome = usable(env.XDG_DATA_HOME) ?? path2.join(os2.homedir(), ".local", "share");
37
+ const authPath = path2.join(dataHome, "opencode", "auth.json");
38
+ const parsed = JSON.parse(fs.readFileSync(authPath, "utf8"));
39
+ if (!isRecord(parsed)) return {};
40
+ const entry = parsed[PROVIDER_ID];
41
+ if (!isRecord(entry) || entry.type !== "api") return {};
42
+ const result = {};
43
+ const key = usable(entry.key);
44
+ if (key !== void 0) result.key = key;
45
+ const baseURL = isRecord(entry.metadata) ? usable(entry.metadata.baseURL) : void 0;
46
+ if (baseURL !== void 0) result.baseURL = baseURL;
47
+ return result;
48
+ } catch {
49
+ return {};
50
+ }
51
+ }
52
+
53
+ // src/auth/workspace-key-file.ts
54
+ 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");
58
+ var GITIGNORE_ENTRIES = ["route33.key", "tui.json"];
59
+ function usable2(value) {
60
+ if (typeof value !== "string") return void 0;
61
+ const trimmed = value.trim();
62
+ return trimmed ? trimmed : void 0;
63
+ }
64
+ function isRecord2(value) {
65
+ return typeof value === "object" && value !== null && !Array.isArray(value);
66
+ }
67
+ function readWorkspaceKey(worktree) {
68
+ if (worktree === void 0) return void 0;
69
+ try {
70
+ const raw = fs2.readFileSync(
71
+ path3.join(worktree, WORKSPACE_KEY_RELATIVE_PATH),
72
+ "utf8"
73
+ );
74
+ const parsed = JSON.parse(raw);
75
+ if (!isRecord2(parsed) || parsed.type !== "api") return void 0;
76
+ return usable2(parsed.key);
77
+ } catch {
78
+ return void 0;
79
+ }
80
+ }
81
+ function ensureWorkspaceKeyGitignore(worktree) {
82
+ if (worktree === void 0 || worktree.trim() === "") return;
83
+ if (path3.resolve(worktree) === "/") return;
84
+ const gitignorePath = path3.join(worktree, WORKSPACE_GITIGNORE_RELATIVE_PATH);
85
+ try {
86
+ fs2.mkdirSync(path3.dirname(gitignorePath), { recursive: true });
87
+ let content = "";
88
+ try {
89
+ content = fs2.readFileSync(gitignorePath, "utf8");
90
+ } catch {
91
+ }
92
+ const lines = content.split("\n");
93
+ const missing = GITIGNORE_ENTRIES.filter(
94
+ (entry) => !lines.some(
95
+ (line) => line.trim() === entry || line.trim() === `/${entry}`
96
+ )
97
+ );
98
+ if (missing.length === 0) return;
99
+ const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
100
+ fs2.writeFileSync(gitignorePath, `${content}${prefix}${missing.join("\n")}
101
+ `);
102
+ } catch (error) {
103
+ logger.warn(
104
+ `opencode-route33: could not update ${gitignorePath} (${String(error)}) \u2014 verify route33.key and tui.json are git-ignored before committing`
105
+ );
106
+ }
107
+ }
108
+ function writeWorkspaceKey(worktree, key) {
109
+ ensureWorkspaceKeyGitignore(worktree);
110
+ const filePath = path3.join(worktree, WORKSPACE_KEY_RELATIVE_PATH);
111
+ fs2.mkdirSync(path3.dirname(filePath), { recursive: true });
112
+ fs2.writeFileSync(
113
+ filePath,
114
+ `${JSON.stringify({ type: "api", key }, null, 2)}
115
+ `,
116
+ { mode: 384 }
117
+ );
118
+ }
119
+
120
+ // src/auth/resolve-key.ts
121
+ var UNRESOLVED_TEMPLATE_MARKER = "{env:";
122
+ function usable3(value) {
123
+ const trimmed = value?.trim();
124
+ return trimmed ? trimmed : void 0;
125
+ }
126
+ function keyFromConfig(cfg) {
127
+ const literal = usable3(cfg?.provider?.route_33?.options?.apiKey);
128
+ if (literal !== void 0 && !literal.includes(UNRESOLVED_TEMPLATE_MARKER))
129
+ return literal;
130
+ return void 0;
131
+ }
132
+ function resolveKey(cfg, env = process.env, worktree) {
133
+ return usable3(env[ENV_API_KEY]) ?? keyFromConfig(cfg) ?? readWorkspaceKey(worktree) ?? readAuthEntry(env).key;
134
+ }
135
+
136
+ // src/tui/key-prompt-ui.ts
137
+ var KEY_SELECT_TITLE = "route33.ai project key";
138
+ var KEY_SELECT_ENTER_LABEL = "Enter a project key";
139
+ var KEY_SELECT_ENTER_DESCRIPTION = "Per workspace on purpose \u2014 one shared key would lose the per-project metrics";
140
+ var KEY_SELECT_SKIP_LABEL = "Skip";
141
+ var KEY_SELECT_SKIP_DESCRIPTION = "Ask again in 7 days";
142
+ var KEY_SELECT_PLACEHOLDER = "No route33.ai key found for this workspace yet.";
143
+ var KEY_PROMPT_TITLE = "Paste your route33.ai project key";
144
+ var KEY_SAVED_MESSAGE = "route33.ai project key saved to .opencode/route33.key \u2014 live immediately, no restart needed.";
145
+ var KEY_SAVE_FAILED_MESSAGE = "Could not save the key \u2014 will ask again next start.";
146
+ var GLOBAL_SELECT_TITLE = "route33.ai global token";
147
+ var GLOBAL_SELECT_ENTER_LABEL = "Enter a global token";
148
+ var GLOBAL_SELECT_ENTER_DESCRIPTION = "One token for every workspace without its own project key";
149
+ var GLOBAL_SELECT_SKIP_LABEL = "Skip";
150
+ var GLOBAL_SELECT_SKIP_DESCRIPTION = "Ask again in 7 days";
151
+ var GLOBAL_SELECT_PLACEHOLDER = "No project folder detected \u2014 save a token for all workspaces?";
152
+ var GLOBAL_PROMPT_TITLE = "Paste your route33.ai global token";
153
+ var GLOBAL_TOKEN_SAVED_MESSAGE = "route33.ai global token saved \u2014 applies to all workspaces without a project key.";
154
+ var GLOBAL_TOKEN_SAVE_FAILED_MESSAGE = "Could not save the token \u2014 will ask again next start.";
155
+
156
+ // src/tui/global-token-flow.ts
157
+ async function runGlobalTokenPromptFlow({
158
+ ui,
159
+ globalStore,
160
+ dismissal,
161
+ now = Date.now
162
+ }) {
163
+ if (await ui.askChoice() === "skip") {
164
+ dismissal.dismiss(now());
165
+ ui.close();
166
+ return "skipped";
167
+ }
168
+ while (true) {
169
+ const value = await ui.askKey();
170
+ if (value === void 0) {
171
+ dismissal.dismiss(now());
172
+ ui.close();
173
+ return "skipped";
174
+ }
175
+ const key = value.trim();
176
+ if (key.length === 0) continue;
177
+ try {
178
+ await globalStore.save(key);
179
+ ui.toastSuccess(GLOBAL_TOKEN_SAVED_MESSAGE);
180
+ ui.close();
181
+ return "saved";
182
+ } catch {
183
+ ui.toastError(GLOBAL_TOKEN_SAVE_FAILED_MESSAGE);
184
+ ui.close();
185
+ return "failed";
186
+ }
187
+ }
188
+ }
189
+
190
+ // src/tui/prompt-flow.ts
191
+ async function runProjectKeyPromptFlow({
192
+ ui,
193
+ keyStore,
194
+ dismissal,
195
+ now = Date.now
196
+ }) {
197
+ if (await ui.askChoice() === "skip") {
198
+ dismissal.dismiss(now());
199
+ ui.close();
200
+ return "skipped";
201
+ }
202
+ while (true) {
203
+ const value = await ui.askKey();
204
+ if (value === void 0) {
205
+ dismissal.dismiss(now());
206
+ ui.close();
207
+ return "skipped";
208
+ }
209
+ const key = value.trim();
210
+ if (key.length === 0) continue;
211
+ try {
212
+ await keyStore.save(key);
213
+ ui.toastSuccess(KEY_SAVED_MESSAGE);
214
+ ui.close();
215
+ return "saved";
216
+ } catch {
217
+ ui.toastError(KEY_SAVE_FAILED_MESSAGE);
218
+ ui.close();
219
+ return "failed";
220
+ }
221
+ }
222
+ }
223
+
224
+ // src/tui/activate.ts
225
+ async function runActivation(deps) {
226
+ try {
227
+ if (!await deps.waitUntilReady()) return "state-unready";
228
+ if (deps.resolveKey() !== void 0) return "key-present";
229
+ if (deps.isGitWorkspace) {
230
+ if (deps.dismissal.isDismissed(Date.now())) return "dismissed";
231
+ return await runProjectKeyPromptFlow({
232
+ ui: deps.ui,
233
+ keyStore: deps.keyStore,
234
+ dismissal: deps.dismissal
235
+ });
236
+ }
237
+ if (deps.globalDismissal.isDismissed(Date.now())) return "dismissed";
238
+ return await runGlobalTokenPromptFlow({
239
+ ui: deps.globalUi,
240
+ globalStore: deps.globalStore,
241
+ dismissal: deps.globalDismissal
242
+ });
243
+ } catch {
244
+ return "state-unready";
245
+ }
246
+ }
247
+
248
+ // src/tui/dismissal-cooldown.ts
249
+ var DISMISSAL_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1e3;
250
+ function isWithinCooldown(dismissedAt, now) {
251
+ return dismissedAt !== void 0 && now - dismissedAt < DISMISSAL_COOLDOWN_MS;
252
+ }
253
+
254
+ // src/tui/dismissal-store.ts
255
+ var PROJECT_KEY_PROMPT_DISMISSED_AT_KEY = "project-key-prompt.dismissed-at";
256
+ var GLOBAL_TOKEN_PROMPT_DISMISSED_AT_KEY = "global-token-prompt.dismissed-at";
257
+ function readDismissedAt(kv, key) {
258
+ try {
259
+ const value = kv.get(key);
260
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
261
+ } catch {
262
+ return void 0;
263
+ }
264
+ }
265
+ function createDismissalStore(kv, key = PROJECT_KEY_PROMPT_DISMISSED_AT_KEY) {
266
+ return {
267
+ isDismissed: (now) => isWithinCooldown(readDismissedAt(kv, key), now),
268
+ dismiss: (now) => {
269
+ try {
270
+ kv.set(key, now);
271
+ } catch {
272
+ }
273
+ }
274
+ };
275
+ }
276
+
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
+ // src/tui/key-prompt-copy.ts
295
+ var PROJECT_COPY = {
296
+ selectTitle: KEY_SELECT_TITLE,
297
+ enterLabel: KEY_SELECT_ENTER_LABEL,
298
+ enterDescription: KEY_SELECT_ENTER_DESCRIPTION,
299
+ skipLabel: KEY_SELECT_SKIP_LABEL,
300
+ skipDescription: KEY_SELECT_SKIP_DESCRIPTION,
301
+ selectPlaceholder: KEY_SELECT_PLACEHOLDER,
302
+ promptTitle: KEY_PROMPT_TITLE
303
+ };
304
+ var GLOBAL_COPY = {
305
+ selectTitle: GLOBAL_SELECT_TITLE,
306
+ enterLabel: GLOBAL_SELECT_ENTER_LABEL,
307
+ enterDescription: GLOBAL_SELECT_ENTER_DESCRIPTION,
308
+ skipLabel: GLOBAL_SELECT_SKIP_LABEL,
309
+ skipDescription: GLOBAL_SELECT_SKIP_DESCRIPTION,
310
+ selectPlaceholder: GLOBAL_SELECT_PLACEHOLDER,
311
+ promptTitle: GLOBAL_PROMPT_TITLE
312
+ };
313
+
314
+ // src/tui/tui-api-adapter.ts
315
+ var READY_POLL_INTERVAL_MS = 100;
316
+ function askChoice(api, copy) {
317
+ return new Promise((resolve3) => {
318
+ let settled = false;
319
+ const settle = (choice) => {
320
+ if (settled) return;
321
+ settled = true;
322
+ resolve3(choice);
323
+ };
324
+ api.ui.dialog.replace(
325
+ () => api.ui.DialogSelect({
326
+ title: copy.selectTitle,
327
+ placeholder: copy.selectPlaceholder,
328
+ options: [
329
+ {
330
+ title: copy.enterLabel,
331
+ value: "enter",
332
+ description: copy.enterDescription
333
+ },
334
+ {
335
+ title: copy.skipLabel,
336
+ value: "skip",
337
+ description: copy.skipDescription
338
+ }
339
+ ],
340
+ onSelect: (option) => {
341
+ settle(option.value === "enter" ? "enter" : "skip");
342
+ }
343
+ }),
344
+ () => {
345
+ settle("skip");
346
+ }
347
+ );
348
+ });
349
+ }
350
+ function askKey(api, copy) {
351
+ return new Promise((resolve3) => {
352
+ let settled = false;
353
+ const settle = (value) => {
354
+ if (settled) return;
355
+ settled = true;
356
+ resolve3(value);
357
+ };
358
+ api.ui.dialog.replace(
359
+ () => api.ui.DialogPrompt({
360
+ title: copy.promptTitle,
361
+ // No description prop: the opencode TUI renders it as a JSX element,
362
+ // and a bare string crashes the renderer ("orphan text"). The
363
+ // informational line lives on the DialogSelect placeholder instead.
364
+ onConfirm: (value) => {
365
+ settle(value);
366
+ },
367
+ onCancel: () => {
368
+ settle(void 0);
369
+ }
370
+ }),
371
+ () => {
372
+ settle(void 0);
373
+ }
374
+ );
375
+ });
376
+ }
377
+ function waitUntilReady(api) {
378
+ return new Promise((resolve3) => {
379
+ if (api.state.ready) {
380
+ resolve3(true);
381
+ return;
382
+ }
383
+ const timer = setInterval(() => {
384
+ if (api.lifecycle.signal.aborted) {
385
+ clearInterval(timer);
386
+ resolve3(false);
387
+ return;
388
+ }
389
+ if (api.state.ready) {
390
+ clearInterval(timer);
391
+ resolve3(true);
392
+ }
393
+ }, READY_POLL_INTERVAL_MS);
394
+ });
395
+ }
396
+ function createUi(api, copy) {
397
+ return {
398
+ askChoice: () => askChoice(api, copy),
399
+ askKey: () => askKey(api, copy),
400
+ toastSuccess: (message) => {
401
+ api.ui.toast({ variant: "success", message });
402
+ },
403
+ toastError: (message) => {
404
+ api.ui.toast({ variant: "error", message });
405
+ },
406
+ close: () => {
407
+ api.ui.dialog.clear();
408
+ }
409
+ };
410
+ }
411
+ function createGlobalStore(api) {
412
+ return {
413
+ // The sdk client runs with ThrowOnError=false, so a failed auth.set
414
+ // RESOLVES `{ data, error }` instead of rejecting — inspect the result
415
+ // and throw on a carried error so the flow can toast + retry.
416
+ save: async (key) => {
417
+ const result = await api.client.auth.set({
418
+ providerID: "route_33",
419
+ auth: { type: "api", key }
420
+ });
421
+ if (result != null && typeof result === "object" && "error" in result && result.error != null) {
422
+ throw new Error("route_33 auth.set failed");
423
+ }
424
+ }
425
+ };
426
+ }
427
+ function createTuiAdapter(api) {
428
+ return {
429
+ ui: createUi(api, PROJECT_COPY),
430
+ globalUi: createUi(api, GLOBAL_COPY),
431
+ keyStore: {
432
+ // The workspace file is the only store needed: the live-auth fetch in
433
+ // the provider options re-resolves it per request, so the key applies
434
+ // immediately — even in the session that is open right now.
435
+ save: async (key) => {
436
+ writeWorkspaceKey(process.cwd(), key);
437
+ }
438
+ },
439
+ globalStore: createGlobalStore(api),
440
+ kv: api.kv,
441
+ waitUntilReady: () => waitUntilReady(api)
442
+ };
443
+ }
444
+
445
+ // src/tui/index.ts
446
+ async function activate(api) {
447
+ const { ui, keyStore, globalUi, globalStore, kv, waitUntilReady: waitUntilReady2 } = createTuiAdapter(api);
448
+ await runActivation({
449
+ ui,
450
+ keyStore,
451
+ dismissal: createDismissalStore(kv),
452
+ globalUi,
453
+ globalStore,
454
+ globalDismissal: createDismissalStore(
455
+ kv,
456
+ GLOBAL_TOKEN_PROMPT_DISMISSED_AT_KEY
457
+ ),
458
+ resolveKey: () => resolveKey(api.state.config, process.env, process.cwd()),
459
+ // api.state.vcs populates asynchronously and arrives too late in real
460
+ // workspaces; a direct .git walk-up from the launch cwd is deterministic.
461
+ isGitWorkspace: isInsideGitWorktree(process.cwd()),
462
+ waitUntilReady: waitUntilReady2
463
+ });
464
+ }
465
+ var Route33TuiPlugin = {
466
+ id: "route33",
467
+ tui: async (api) => {
468
+ if (process.env.ROUTE33_TUI_PROMPT === "0") return;
469
+ void activate(api).catch(() => {
470
+ });
471
+ }
472
+ };
473
+ var tui_default = Route33TuiPlugin;
474
+ export {
475
+ tui_default as default
476
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@route_33/opencode",
3
+ "version": "0.1.2",
4
+ "type": "module",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "description": "opencode provider plugin for the route33.ai AI gateway — key-scoped model discovery, cost/limit enrichment, auth, and the key-setup wizard.",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Cronos-AI-Lab/harness-bench.git",
10
+ "directory": "packages/integrations/opencode-route33"
11
+ },
12
+ "keywords": [
13
+ "opencode",
14
+ "opencode-plugin",
15
+ "route33"
16
+ ],
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ },
22
+ "./tui": {
23
+ "types": "./dist/tui/index.d.ts",
24
+ "import": "./dist/tui/index.js"
25
+ }
26
+ },
27
+ "main": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "peerDependencies": {
36
+ "@opencode-ai/plugin": ">=1.17.0 <2.0.0"
37
+ },
38
+ "dependencies": {
39
+ "jsonc-parser": "^3.3.1"
40
+ },
41
+ "devDependencies": {
42
+ "@opencode-ai/plugin": "^1.17.12",
43
+ "@types/node": "^22.10.7",
44
+ "tsup": "^8.3.5",
45
+ "typescript": "^5.7.3",
46
+ "vitest": "^3.0.5"
47
+ },
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "prepack": "bun run build",
51
+ "bundle": "tsup",
52
+ "bundle:watch": "tsup --watch",
53
+ "verify:bundle": "bun run scripts/verify-bundle.ts",
54
+ "test": "vitest run && bun run verify:bundle",
55
+ "typecheck": "tsc --noEmit"
56
+ }
57
+ }