@route_33/opencode 0.4.6 → 0.4.7-rc.11
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 +74 -88
- package/dist/cli/production-smoke/production-smoke.js +1665 -0
- package/dist/index.js +3886 -1232
- package/dist/tui-entry/index.js +7935 -0
- package/dist/v2-entry/index.js +4304 -0
- package/package.json +38 -42
- package/LICENSE +0 -21
- package/dist/index.d.ts +0 -23
- package/dist/tui/index.d.ts +0 -5
- package/dist/tui/index.js +0 -542
- package/docs/gateway-limits-variants-broadcast.md +0 -146
- package/docs/gateway-pricing-proposal.md +0 -96
- package/docs/getting-started.html +0 -570
package/package.json
CHANGED
|
@@ -1,59 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@route_33/opencode",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7-rc.11",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
-
"description": "opencode provider plugin for the route33.ai AI gateway \u2014 key-scoped model discovery, cost/limit enrichment, auth, and the key-setup wizard.",
|
|
7
5
|
"repository": {
|
|
8
6
|
"type": "git",
|
|
9
|
-
"url": "
|
|
10
|
-
"directory": "packages/
|
|
7
|
+
"url": "https://github.com/Cronos-AI-Lab/r33-tooling.git",
|
|
8
|
+
"directory": "packages/opencode-route33"
|
|
11
9
|
},
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"route33"
|
|
16
|
-
],
|
|
17
|
-
"exports": {
|
|
18
|
-
".": {
|
|
19
|
-
"types": "./dist/index.d.ts",
|
|
20
|
-
"import": "./dist/index.js"
|
|
21
|
-
},
|
|
22
|
-
"./server": {
|
|
23
|
-
"types": "./dist/index.d.ts",
|
|
24
|
-
"import": "./dist/index.js"
|
|
25
|
-
},
|
|
26
|
-
"./tui": {
|
|
27
|
-
"types": "./dist/tui/index.d.ts",
|
|
28
|
-
"import": "./dist/tui/index.js"
|
|
29
|
-
}
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"registry": "https://registry.npmjs.org/",
|
|
12
|
+
"access": "public"
|
|
30
13
|
},
|
|
31
|
-
"main": "./dist/index.js",
|
|
32
|
-
"types": "./dist/index.d.ts",
|
|
33
14
|
"files": [
|
|
34
15
|
"dist",
|
|
35
|
-
"
|
|
16
|
+
"README.md"
|
|
36
17
|
],
|
|
37
|
-
"
|
|
38
|
-
"
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./dist/index.js",
|
|
20
|
+
"./server": "./dist/v2-entry/index.js",
|
|
21
|
+
"./tui": "./dist/tui-entry/index.js",
|
|
22
|
+
"./v2": "./dist/v2-entry/index.js"
|
|
39
23
|
},
|
|
40
24
|
"peerDependencies": {
|
|
41
|
-
"@opencode-ai/plugin": ">=1.
|
|
25
|
+
"@opencode-ai/plugin": ">=1.18 <2",
|
|
26
|
+
"@opencode/plugin": ">=2.1 <3"
|
|
27
|
+
},
|
|
28
|
+
"peerDependenciesMeta": {
|
|
29
|
+
"@opencode-ai/plugin": {
|
|
30
|
+
"optional": true
|
|
31
|
+
},
|
|
32
|
+
"@opencode/plugin": {
|
|
33
|
+
"optional": true
|
|
34
|
+
}
|
|
42
35
|
},
|
|
43
36
|
"devDependencies": {
|
|
44
|
-
"@opencode-ai/plugin": "^1.
|
|
45
|
-
"@
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
37
|
+
"@opencode-ai/plugin": "^1.18.0",
|
|
38
|
+
"@opencode-ai/plugin-v2": "npm:@opencode/plugin@^2.1.0",
|
|
39
|
+
"@total-typescript/ts-reset": "0.6.1",
|
|
40
|
+
"@types/node": "26.6.5",
|
|
41
|
+
"esbuild": "0.28.2",
|
|
42
|
+
"eslint": "10.13.0",
|
|
43
|
+
"typescript": "6.0.3",
|
|
44
|
+
"vitest": "5.0.5",
|
|
45
|
+
"@cronos-ai-lab/eslint-config": "0.2.0",
|
|
46
|
+
"@cronos-ai-lab/typescript-config": "0.2.0"
|
|
49
47
|
},
|
|
50
48
|
"scripts": {
|
|
51
|
-
"build": "
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"test": "vitest run && bun run verify:bundle",
|
|
57
|
-
"typecheck": "tsc --noEmit"
|
|
49
|
+
"build": "esbuild src/index.ts src/v2-entry/index.ts src/tui-entry/index.ts src/cli/production-smoke/production-smoke.ts --bundle --format=esm --platform=node --packages=external --outbase=src --outdir=dist --alias:@opencode-ai/plugin-v2=@opencode/plugin --define:__ROUTE33_DEFAULT_CLIENT_ID__=\\\"${ROUTE33_DEFAULT_CLIENT_ID:-}\\\"",
|
|
50
|
+
"lint": "eslint . --max-warnings 0",
|
|
51
|
+
"smoke:production": "node dist/cli/production-smoke/production-smoke.js",
|
|
52
|
+
"test": "ROUTE33_DEFAULT_CLIENT_ID=e2e-injected-client-id pnpm run build && vitest run --passWithNoTests",
|
|
53
|
+
"type:check": "tsc --noEmit"
|
|
58
54
|
}
|
|
59
|
-
}
|
|
55
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
route33.ai opencode plugin — Proprietary License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Tom Van Damme. All rights reserved.
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted to any person obtaining a copy of this package
|
|
6
|
-
through the public npm registry (the "Package") to install and use the
|
|
7
|
-
Package, unmodified, as a plugin for opencode, for any purpose including
|
|
8
|
-
commercial purposes.
|
|
9
|
-
|
|
10
|
-
You may NOT copy, modify, merge, publish, distribute, sublicense, sell, or
|
|
11
|
-
create derivative works of the Package or any part of it, beyond the
|
|
12
|
-
download and installation performed by npm/opencode as part of normal use.
|
|
13
|
-
The source code of the Package is not licensed for reuse.
|
|
14
|
-
|
|
15
|
-
THE PACKAGE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
20
|
-
FROM, OUT OF OR IN CONNECTION WITH THE PACKAGE OR THE USE OR OTHER DEALINGS
|
|
21
|
-
IN THE PACKAGE.
|
package/dist/index.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { Plugin } from '@opencode-ai/plugin';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* opencode provider plugin for the route33.ai AI gateway — the single
|
|
5
|
-
* plugin export (R4: one provider, one auth hook, one entry file).
|
|
6
|
-
*
|
|
7
|
-
* Registers the `config` hook — key-scoped model discovery, cost/limit
|
|
8
|
-
* enrichment, add-only injection into the single `route_33` provider block
|
|
9
|
-
* (per-model native-SDK routing via `provider.npm`) and strict model-reference
|
|
10
|
-
* validation — plus the `route_33` auth method and options loader.
|
|
11
|
-
* Discovery runs once at startup; restart opencode after a key or config
|
|
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.
|
|
20
|
-
*/
|
|
21
|
-
declare const Route33Plugin: Plugin;
|
|
22
|
-
|
|
23
|
-
export { Route33Plugin };
|
package/dist/tui/index.d.ts
DELETED
package/dist/tui/index.js
DELETED
|
@@ -1,542 +0,0 @@
|
|
|
1
|
-
// src/shared/env-api-key.ts
|
|
2
|
-
var ENV_API_KEY = "ROUTE33_API_KEY";
|
|
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
|
-
|
|
21
|
-
// src/shared/logger.ts
|
|
22
|
-
var logger = {
|
|
23
|
-
info: (...args) => {
|
|
24
|
-
console.log("[route_33]", ...args);
|
|
25
|
-
},
|
|
26
|
-
warn: (...args) => {
|
|
27
|
-
console.warn("[route_33]", ...args);
|
|
28
|
-
},
|
|
29
|
-
error: (...args) => {
|
|
30
|
-
console.error("[route_33]", ...args);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// src/shared/native-sdk-api-key-placeholder.ts
|
|
35
|
-
var NATIVE_SDK_API_KEY_PLACEHOLDER = "__route33_fetch_shim_auth__";
|
|
36
|
-
|
|
37
|
-
// src/shared/resolve-cache-dir.ts
|
|
38
|
-
import * as os from "os";
|
|
39
|
-
import * as path2 from "path";
|
|
40
|
-
|
|
41
|
-
// src/auth/auth-json.ts
|
|
42
|
-
import * as fs from "fs";
|
|
43
|
-
import * as os2 from "os";
|
|
44
|
-
import * as path3 from "path";
|
|
45
|
-
var PROVIDER_ID = "route_33";
|
|
46
|
-
function usable(value) {
|
|
47
|
-
if (typeof value !== "string") return void 0;
|
|
48
|
-
const trimmed = value.trim();
|
|
49
|
-
return trimmed ? trimmed : void 0;
|
|
50
|
-
}
|
|
51
|
-
function isRecord(value) {
|
|
52
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
53
|
-
}
|
|
54
|
-
function readAuthEntry(env = process.env) {
|
|
55
|
-
try {
|
|
56
|
-
const dataHome = usable(env.XDG_DATA_HOME) ?? path3.join(os2.homedir(), ".local", "share");
|
|
57
|
-
const authPath = path3.join(dataHome, "opencode", "auth.json");
|
|
58
|
-
const parsed = JSON.parse(fs.readFileSync(authPath, "utf8"));
|
|
59
|
-
if (!isRecord(parsed)) return {};
|
|
60
|
-
const entry = parsed[PROVIDER_ID];
|
|
61
|
-
if (!isRecord(entry) || entry.type !== "api") return {};
|
|
62
|
-
const result = {};
|
|
63
|
-
const key = usable(entry.key);
|
|
64
|
-
if (key !== void 0) result.key = key;
|
|
65
|
-
const baseURL = isRecord(entry.metadata) ? usable(entry.metadata.baseURL) : void 0;
|
|
66
|
-
if (baseURL !== void 0) result.baseURL = baseURL;
|
|
67
|
-
return result;
|
|
68
|
-
} catch {
|
|
69
|
-
return {};
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// src/auth/workspace-key-file.ts
|
|
74
|
-
import * as fs2 from "fs";
|
|
75
|
-
import * as path4 from "path";
|
|
76
|
-
var WORKSPACE_KEY_RELATIVE_PATH = path4.join(".opencode", "route33.key");
|
|
77
|
-
var WORKSPACE_GITIGNORE_RELATIVE_PATH = path4.join(".opencode", ".gitignore");
|
|
78
|
-
var GITIGNORE_ENTRIES = ["route33.key", "tui.json"];
|
|
79
|
-
function usable2(value) {
|
|
80
|
-
if (typeof value !== "string") return void 0;
|
|
81
|
-
const trimmed = value.trim();
|
|
82
|
-
return trimmed ? trimmed : void 0;
|
|
83
|
-
}
|
|
84
|
-
function isRecord2(value) {
|
|
85
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
86
|
-
}
|
|
87
|
-
function readWorkspaceKey(worktree) {
|
|
88
|
-
if (worktree === void 0) return void 0;
|
|
89
|
-
try {
|
|
90
|
-
const raw = fs2.readFileSync(
|
|
91
|
-
path4.join(worktree, WORKSPACE_KEY_RELATIVE_PATH),
|
|
92
|
-
"utf8"
|
|
93
|
-
);
|
|
94
|
-
const parsed = JSON.parse(raw);
|
|
95
|
-
if (!isRecord2(parsed) || parsed.type !== "api") return void 0;
|
|
96
|
-
return usable2(parsed.key);
|
|
97
|
-
} catch {
|
|
98
|
-
return void 0;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
function ensureWorkspaceKeyGitignore(worktree) {
|
|
102
|
-
if (worktree === void 0 || worktree.trim() === "") return;
|
|
103
|
-
if (path4.resolve(worktree) === "/") return;
|
|
104
|
-
const gitignorePath = path4.join(worktree, WORKSPACE_GITIGNORE_RELATIVE_PATH);
|
|
105
|
-
try {
|
|
106
|
-
fs2.mkdirSync(path4.dirname(gitignorePath), { recursive: true });
|
|
107
|
-
let content = "";
|
|
108
|
-
try {
|
|
109
|
-
content = fs2.readFileSync(gitignorePath, "utf8");
|
|
110
|
-
} catch {
|
|
111
|
-
}
|
|
112
|
-
const lines = content.split("\n");
|
|
113
|
-
const missing = GITIGNORE_ENTRIES.filter(
|
|
114
|
-
(entry) => !lines.some(
|
|
115
|
-
(line) => line.trim() === entry || line.trim() === `/${entry}`
|
|
116
|
-
)
|
|
117
|
-
);
|
|
118
|
-
if (missing.length === 0) return;
|
|
119
|
-
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
120
|
-
fs2.writeFileSync(gitignorePath, `${content}${prefix}${missing.join("\n")}
|
|
121
|
-
`);
|
|
122
|
-
} catch (error) {
|
|
123
|
-
logger.warn(
|
|
124
|
-
`opencode-route33: could not update ${gitignorePath} (${String(error)}) \u2014 verify route33.key and tui.json are git-ignored before committing`
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
function writeWorkspaceKey(worktree, key) {
|
|
129
|
-
ensureWorkspaceKeyGitignore(worktree);
|
|
130
|
-
const filePath = path4.join(worktree, WORKSPACE_KEY_RELATIVE_PATH);
|
|
131
|
-
fs2.mkdirSync(path4.dirname(filePath), { recursive: true });
|
|
132
|
-
fs2.writeFileSync(
|
|
133
|
-
filePath,
|
|
134
|
-
`${JSON.stringify({ type: "api", key }, null, 2)}
|
|
135
|
-
`,
|
|
136
|
-
{ mode: 384 }
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// src/auth/resolve-key.ts
|
|
141
|
-
var UNRESOLVED_TEMPLATE_MARKER = "{env:";
|
|
142
|
-
function usable3(value) {
|
|
143
|
-
const trimmed = value?.trim();
|
|
144
|
-
return trimmed ? trimmed : void 0;
|
|
145
|
-
}
|
|
146
|
-
function keyFromConfig(cfg) {
|
|
147
|
-
const literal = usable3(cfg?.provider?.route_33?.options?.apiKey);
|
|
148
|
-
if (literal === void 0) return void 0;
|
|
149
|
-
if (literal.includes(UNRESOLVED_TEMPLATE_MARKER)) return void 0;
|
|
150
|
-
if (literal === NATIVE_SDK_API_KEY_PLACEHOLDER) return void 0;
|
|
151
|
-
return literal;
|
|
152
|
-
}
|
|
153
|
-
function resolveKey(cfg, env = process.env, worktree) {
|
|
154
|
-
return usable3(env[ENV_API_KEY]) ?? keyFromConfig(cfg) ?? readWorkspaceKey(worktree) ?? (worktree !== void 0 && isInsideGitWorktree(worktree) ? void 0 : readAuthEntry(env).key);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// src/tui/key-prompt-ui.ts
|
|
158
|
-
var KEY_SELECT_TITLE = "route33.ai project key";
|
|
159
|
-
var KEY_SELECT_ENTER_LABEL = "Enter a project key";
|
|
160
|
-
var KEY_SELECT_ENTER_DESCRIPTION = "Per workspace on purpose \u2014 one shared key would lose the per-project metrics";
|
|
161
|
-
var KEY_SELECT_SKIP_LABEL = "Skip";
|
|
162
|
-
var KEY_SELECT_SKIP_DESCRIPTION = "Ask again in 7 days \u2014 this workspace only";
|
|
163
|
-
var KEY_SELECT_SKIP_ALL_LABEL = "Skip for all workspaces";
|
|
164
|
-
var KEY_SELECT_SKIP_ALL_DESCRIPTION = "Ask again in 7 days \u2014 everywhere";
|
|
165
|
-
var KEY_SELECT_PLACEHOLDER = "No route33.ai key found for this workspace yet.";
|
|
166
|
-
var KEY_PROMPT_TITLE = "Paste your route33.ai project key";
|
|
167
|
-
var KEY_SAVED_MESSAGE = "route33.ai project key saved to .opencode/route33.key \u2014 live immediately, no restart needed.";
|
|
168
|
-
var KEY_SAVE_FAILED_MESSAGE = "Could not save the key \u2014 will ask again next start.";
|
|
169
|
-
var GLOBAL_SELECT_TITLE = "route33.ai global token";
|
|
170
|
-
var GLOBAL_SELECT_ENTER_LABEL = "Enter a global token";
|
|
171
|
-
var GLOBAL_SELECT_ENTER_DESCRIPTION = "One token for every workspace without its own project key";
|
|
172
|
-
var GLOBAL_SELECT_SKIP_LABEL = "Skip";
|
|
173
|
-
var GLOBAL_SELECT_SKIP_DESCRIPTION = "Ask again in 7 days";
|
|
174
|
-
var GLOBAL_SELECT_PLACEHOLDER = "No project folder detected \u2014 save a token for all workspaces?";
|
|
175
|
-
var GLOBAL_PROMPT_TITLE = "Paste your route33.ai global token";
|
|
176
|
-
var GLOBAL_TOKEN_SAVED_MESSAGE = "route33.ai global token saved \u2014 applies to all workspaces without a project key.";
|
|
177
|
-
var GLOBAL_TOKEN_SAVE_FAILED_MESSAGE = "Could not save the token \u2014 will ask again next start.";
|
|
178
|
-
|
|
179
|
-
// src/tui/global-token-flow.ts
|
|
180
|
-
async function runGlobalTokenPromptFlow({
|
|
181
|
-
ui,
|
|
182
|
-
globalStore,
|
|
183
|
-
dismissal,
|
|
184
|
-
now = Date.now
|
|
185
|
-
}) {
|
|
186
|
-
if (await ui.askChoice() === "skip") {
|
|
187
|
-
dismissal.dismiss(now());
|
|
188
|
-
ui.close();
|
|
189
|
-
return "skipped";
|
|
190
|
-
}
|
|
191
|
-
while (true) {
|
|
192
|
-
const value = await ui.askKey();
|
|
193
|
-
if (value === void 0) {
|
|
194
|
-
dismissal.dismiss(now());
|
|
195
|
-
ui.close();
|
|
196
|
-
return "skipped";
|
|
197
|
-
}
|
|
198
|
-
const key = value.trim();
|
|
199
|
-
if (key.length === 0) continue;
|
|
200
|
-
try {
|
|
201
|
-
await globalStore.save(key);
|
|
202
|
-
ui.toastSuccess(GLOBAL_TOKEN_SAVED_MESSAGE);
|
|
203
|
-
ui.close();
|
|
204
|
-
return "saved";
|
|
205
|
-
} catch {
|
|
206
|
-
ui.toastError(GLOBAL_TOKEN_SAVE_FAILED_MESSAGE);
|
|
207
|
-
ui.close();
|
|
208
|
-
return "failed";
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// src/tui/prompt-flow.ts
|
|
214
|
-
async function runProjectKeyPromptFlow({
|
|
215
|
-
ui,
|
|
216
|
-
keyStore,
|
|
217
|
-
dismissal,
|
|
218
|
-
now = Date.now
|
|
219
|
-
}) {
|
|
220
|
-
const choice = await ui.askChoice();
|
|
221
|
-
if (choice === "skip" || choice === "skip-all") {
|
|
222
|
-
if (choice === "skip-all") dismissal.dismissGlobally(now());
|
|
223
|
-
else dismissal.dismiss(now());
|
|
224
|
-
ui.close();
|
|
225
|
-
return "skipped";
|
|
226
|
-
}
|
|
227
|
-
while (true) {
|
|
228
|
-
const value = await ui.askKey();
|
|
229
|
-
if (value === void 0) {
|
|
230
|
-
dismissal.dismiss(now());
|
|
231
|
-
ui.close();
|
|
232
|
-
return "skipped";
|
|
233
|
-
}
|
|
234
|
-
const key = value.trim();
|
|
235
|
-
if (key.length === 0) continue;
|
|
236
|
-
try {
|
|
237
|
-
await keyStore.save(key);
|
|
238
|
-
ui.toastSuccess(KEY_SAVED_MESSAGE);
|
|
239
|
-
ui.close();
|
|
240
|
-
return "saved";
|
|
241
|
-
} catch {
|
|
242
|
-
ui.toastError(KEY_SAVE_FAILED_MESSAGE);
|
|
243
|
-
ui.close();
|
|
244
|
-
return "failed";
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// src/tui/activate.ts
|
|
250
|
-
async function runActivation(deps) {
|
|
251
|
-
try {
|
|
252
|
-
if (!await deps.waitUntilReady()) return "state-unready";
|
|
253
|
-
if (deps.resolveKey() !== void 0) return "key-present";
|
|
254
|
-
if (deps.isGitWorkspace) {
|
|
255
|
-
if (deps.dismissal.isDismissed(Date.now())) return "dismissed";
|
|
256
|
-
return await runProjectKeyPromptFlow({
|
|
257
|
-
ui: deps.ui,
|
|
258
|
-
keyStore: deps.keyStore,
|
|
259
|
-
dismissal: deps.dismissal
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
if (deps.globalDismissal.isDismissed(Date.now())) return "dismissed";
|
|
263
|
-
return await runGlobalTokenPromptFlow({
|
|
264
|
-
ui: deps.globalUi,
|
|
265
|
-
globalStore: deps.globalStore,
|
|
266
|
-
dismissal: deps.globalDismissal
|
|
267
|
-
});
|
|
268
|
-
} catch {
|
|
269
|
-
return "state-unready";
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// src/tui/dismissal-store.ts
|
|
274
|
-
import { resolve as resolve3 } from "path";
|
|
275
|
-
|
|
276
|
-
// src/tui/dismissal-cooldown.ts
|
|
277
|
-
var DISMISSAL_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
278
|
-
function isWithinCooldown(dismissedAt, now) {
|
|
279
|
-
return dismissedAt !== void 0 && now - dismissedAt < DISMISSAL_COOLDOWN_MS;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// src/tui/dismissal-store.ts
|
|
283
|
-
var PROJECT_KEY_PROMPT_DISMISSED_AT_KEY = "project-key-prompt.dismissed-at";
|
|
284
|
-
var GLOBAL_TOKEN_PROMPT_DISMISSED_AT_KEY = "global-token-prompt.dismissed-at";
|
|
285
|
-
function readDismissedAt(kv, key) {
|
|
286
|
-
try {
|
|
287
|
-
const value = kv.get(key);
|
|
288
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
289
|
-
} catch {
|
|
290
|
-
return void 0;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
function readByWorkspace(kv, key) {
|
|
294
|
-
try {
|
|
295
|
-
const value = kv.get(key);
|
|
296
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
297
|
-
return {};
|
|
298
|
-
}
|
|
299
|
-
const entries = {};
|
|
300
|
-
for (const [path5, at] of Object.entries(value)) {
|
|
301
|
-
if (typeof at === "number" && Number.isFinite(at)) entries[path5] = at;
|
|
302
|
-
}
|
|
303
|
-
return entries;
|
|
304
|
-
} catch {
|
|
305
|
-
return {};
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
function createDismissalStore(kv, key = PROJECT_KEY_PROMPT_DISMISSED_AT_KEY, worktree) {
|
|
309
|
-
const scoped = worktree !== void 0 ? resolve3(worktree) : void 0;
|
|
310
|
-
const byWorkspaceKey = `${key}.by-workspace`;
|
|
311
|
-
const dismissGlobally = (now) => {
|
|
312
|
-
try {
|
|
313
|
-
kv.set(key, now);
|
|
314
|
-
} catch {
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
return {
|
|
318
|
-
isDismissed: (now) => {
|
|
319
|
-
if (isWithinCooldown(readDismissedAt(kv, key), now)) return true;
|
|
320
|
-
if (scoped === void 0) return false;
|
|
321
|
-
return isWithinCooldown(readByWorkspace(kv, byWorkspaceKey)[scoped], now);
|
|
322
|
-
},
|
|
323
|
-
dismiss: (now) => {
|
|
324
|
-
if (scoped === void 0) {
|
|
325
|
-
dismissGlobally(now);
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
try {
|
|
329
|
-
const entries = readByWorkspace(kv, byWorkspaceKey);
|
|
330
|
-
const pruned = {};
|
|
331
|
-
for (const [path5, at] of Object.entries(entries)) {
|
|
332
|
-
if (isWithinCooldown(at, now)) pruned[path5] = at;
|
|
333
|
-
}
|
|
334
|
-
pruned[scoped] = now;
|
|
335
|
-
kv.set(byWorkspaceKey, pruned);
|
|
336
|
-
} catch {
|
|
337
|
-
}
|
|
338
|
-
},
|
|
339
|
-
dismissGlobally
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// src/tui/key-prompt-copy.ts
|
|
344
|
-
var PROJECT_COPY = {
|
|
345
|
-
selectTitle: KEY_SELECT_TITLE,
|
|
346
|
-
enterLabel: KEY_SELECT_ENTER_LABEL,
|
|
347
|
-
enterDescription: KEY_SELECT_ENTER_DESCRIPTION,
|
|
348
|
-
skipLabel: KEY_SELECT_SKIP_LABEL,
|
|
349
|
-
skipDescription: KEY_SELECT_SKIP_DESCRIPTION,
|
|
350
|
-
skipAllLabel: KEY_SELECT_SKIP_ALL_LABEL,
|
|
351
|
-
skipAllDescription: KEY_SELECT_SKIP_ALL_DESCRIPTION,
|
|
352
|
-
selectPlaceholder: KEY_SELECT_PLACEHOLDER,
|
|
353
|
-
promptTitle: KEY_PROMPT_TITLE
|
|
354
|
-
};
|
|
355
|
-
var GLOBAL_COPY = {
|
|
356
|
-
selectTitle: GLOBAL_SELECT_TITLE,
|
|
357
|
-
enterLabel: GLOBAL_SELECT_ENTER_LABEL,
|
|
358
|
-
enterDescription: GLOBAL_SELECT_ENTER_DESCRIPTION,
|
|
359
|
-
skipLabel: GLOBAL_SELECT_SKIP_LABEL,
|
|
360
|
-
skipDescription: GLOBAL_SELECT_SKIP_DESCRIPTION,
|
|
361
|
-
selectPlaceholder: GLOBAL_SELECT_PLACEHOLDER,
|
|
362
|
-
promptTitle: GLOBAL_PROMPT_TITLE
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
// src/tui/tui-api-adapter.ts
|
|
366
|
-
var READY_POLL_INTERVAL_MS = 100;
|
|
367
|
-
function askChoice(api, copy) {
|
|
368
|
-
return new Promise((resolve4) => {
|
|
369
|
-
let settled = false;
|
|
370
|
-
const settle = (choice) => {
|
|
371
|
-
if (settled) return;
|
|
372
|
-
settled = true;
|
|
373
|
-
resolve4(choice);
|
|
374
|
-
};
|
|
375
|
-
api.ui.dialog.replace(
|
|
376
|
-
() => api.ui.DialogSelect({
|
|
377
|
-
title: copy.selectTitle,
|
|
378
|
-
placeholder: copy.selectPlaceholder,
|
|
379
|
-
options: [
|
|
380
|
-
{
|
|
381
|
-
title: copy.enterLabel,
|
|
382
|
-
value: "enter",
|
|
383
|
-
description: copy.enterDescription
|
|
384
|
-
},
|
|
385
|
-
{
|
|
386
|
-
title: copy.skipLabel,
|
|
387
|
-
value: "skip",
|
|
388
|
-
description: copy.skipDescription
|
|
389
|
-
},
|
|
390
|
-
// Only the project copy defines a skip-all scope; the global
|
|
391
|
-
// prompt's plain skip is global already.
|
|
392
|
-
...copy.skipAllLabel !== void 0 ? [
|
|
393
|
-
{
|
|
394
|
-
title: copy.skipAllLabel,
|
|
395
|
-
value: "skip-all",
|
|
396
|
-
description: copy.skipAllDescription ?? ""
|
|
397
|
-
}
|
|
398
|
-
] : []
|
|
399
|
-
],
|
|
400
|
-
onSelect: (option) => {
|
|
401
|
-
settle(
|
|
402
|
-
option.value === "enter" ? "enter" : option.value === "skip-all" ? "skip-all" : "skip"
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
}),
|
|
406
|
-
() => {
|
|
407
|
-
settle("skip");
|
|
408
|
-
}
|
|
409
|
-
);
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
function askKey(api, copy) {
|
|
413
|
-
return new Promise((resolve4) => {
|
|
414
|
-
let settled = false;
|
|
415
|
-
const settle = (value) => {
|
|
416
|
-
if (settled) return;
|
|
417
|
-
settled = true;
|
|
418
|
-
resolve4(value);
|
|
419
|
-
};
|
|
420
|
-
api.ui.dialog.replace(
|
|
421
|
-
() => api.ui.DialogPrompt({
|
|
422
|
-
title: copy.promptTitle,
|
|
423
|
-
// No description prop: the opencode TUI renders it as a JSX element,
|
|
424
|
-
// and a bare string crashes the renderer ("orphan text"). The
|
|
425
|
-
// informational line lives on the DialogSelect placeholder instead.
|
|
426
|
-
onConfirm: (value) => {
|
|
427
|
-
settle(value);
|
|
428
|
-
},
|
|
429
|
-
onCancel: () => {
|
|
430
|
-
settle(void 0);
|
|
431
|
-
}
|
|
432
|
-
}),
|
|
433
|
-
() => {
|
|
434
|
-
settle(void 0);
|
|
435
|
-
}
|
|
436
|
-
);
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
function waitUntilReady(api) {
|
|
440
|
-
return new Promise((resolve4) => {
|
|
441
|
-
if (api.state.ready) {
|
|
442
|
-
resolve4(true);
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
const timer = setInterval(() => {
|
|
446
|
-
if (api.lifecycle.signal.aborted) {
|
|
447
|
-
clearInterval(timer);
|
|
448
|
-
resolve4(false);
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
if (api.state.ready) {
|
|
452
|
-
clearInterval(timer);
|
|
453
|
-
resolve4(true);
|
|
454
|
-
}
|
|
455
|
-
}, READY_POLL_INTERVAL_MS);
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
function createUi(api, copy) {
|
|
459
|
-
return {
|
|
460
|
-
askChoice: () => askChoice(api, copy),
|
|
461
|
-
askKey: () => askKey(api, copy),
|
|
462
|
-
toastSuccess: (message) => {
|
|
463
|
-
api.ui.toast({ variant: "success", message });
|
|
464
|
-
},
|
|
465
|
-
toastError: (message) => {
|
|
466
|
-
api.ui.toast({ variant: "error", message });
|
|
467
|
-
},
|
|
468
|
-
close: () => {
|
|
469
|
-
api.ui.dialog.clear();
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
function createGlobalStore(api) {
|
|
474
|
-
return {
|
|
475
|
-
// The sdk client runs with ThrowOnError=false, so a failed auth.set
|
|
476
|
-
// RESOLVES `{ data, error }` instead of rejecting — inspect the result
|
|
477
|
-
// and throw on a carried error so the flow can toast + retry.
|
|
478
|
-
save: async (key) => {
|
|
479
|
-
const result = await api.client.auth.set({
|
|
480
|
-
providerID: "route_33",
|
|
481
|
-
auth: { type: "api", key }
|
|
482
|
-
});
|
|
483
|
-
if (result != null && typeof result === "object" && "error" in result && result.error != null) {
|
|
484
|
-
throw new Error("route_33 auth.set failed");
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
function createTuiAdapter(api) {
|
|
490
|
-
return {
|
|
491
|
-
ui: createUi(api, PROJECT_COPY),
|
|
492
|
-
globalUi: createUi(api, GLOBAL_COPY),
|
|
493
|
-
keyStore: {
|
|
494
|
-
// The workspace file is the only store needed: the live-auth fetch in
|
|
495
|
-
// the provider options re-resolves it per request, so the key applies
|
|
496
|
-
// immediately — even in the session that is open right now.
|
|
497
|
-
save: async (key) => {
|
|
498
|
-
writeWorkspaceKey(process.cwd(), key);
|
|
499
|
-
}
|
|
500
|
-
},
|
|
501
|
-
globalStore: createGlobalStore(api),
|
|
502
|
-
kv: api.kv,
|
|
503
|
-
waitUntilReady: () => waitUntilReady(api)
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// src/tui/index.ts
|
|
508
|
-
async function activate(api) {
|
|
509
|
-
const { ui, keyStore, globalUi, globalStore, kv, waitUntilReady: waitUntilReady2 } = createTuiAdapter(api);
|
|
510
|
-
await runActivation({
|
|
511
|
-
ui,
|
|
512
|
-
keyStore,
|
|
513
|
-
dismissal: createDismissalStore(
|
|
514
|
-
kv,
|
|
515
|
-
PROJECT_KEY_PROMPT_DISMISSED_AT_KEY,
|
|
516
|
-
process.cwd()
|
|
517
|
-
),
|
|
518
|
-
globalUi,
|
|
519
|
-
globalStore,
|
|
520
|
-
globalDismissal: createDismissalStore(
|
|
521
|
-
kv,
|
|
522
|
-
GLOBAL_TOKEN_PROMPT_DISMISSED_AT_KEY
|
|
523
|
-
),
|
|
524
|
-
resolveKey: () => resolveKey(api.state.config, process.env, process.cwd()),
|
|
525
|
-
// api.state.vcs populates asynchronously and arrives too late in real
|
|
526
|
-
// workspaces; a direct .git walk-up from the launch cwd is deterministic.
|
|
527
|
-
isGitWorkspace: isInsideGitWorktree(process.cwd()),
|
|
528
|
-
waitUntilReady: waitUntilReady2
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
var Route33TuiPlugin = {
|
|
532
|
-
id: "route33",
|
|
533
|
-
tui: async (api) => {
|
|
534
|
-
if (process.env.ROUTE33_TUI_PROMPT === "0") return;
|
|
535
|
-
void activate(api).catch(() => {
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
};
|
|
539
|
-
var tui_default = Route33TuiPlugin;
|
|
540
|
-
export {
|
|
541
|
-
tui_default as default
|
|
542
|
-
};
|