@telefonica/ai-sdlc-opencode-plugin-v1-dev 0.1.0-snapshot.35846538366
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 +124 -0
- package/dist/flavor-CMZWvD--.js +13 -0
- package/dist/index-T2VhrgAK.d.ts +9 -0
- package/dist/index.js +205 -0
- package/dist/tui-BTWQZHT5.d.ts +9 -0
- package/dist/tui.js +118 -0
- package/package.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# @telefonica/ai-sdlc-opencode-plugin-v1
|
|
2
|
+
|
|
3
|
+
CDO.AI gateway plugin for **OpenCode 1.x**. Registers the gateway as a provider,
|
|
4
|
+
discovers its models, and renders remaining budget in the sidebar.
|
|
5
|
+
|
|
6
|
+
For installation and the `/connect` walkthrough, see the
|
|
7
|
+
[plugins README](../../README.md). This file covers the internals.
|
|
8
|
+
|
|
9
|
+
> Using OpenCode 2.x? Install [`@telefonica/ai-sdlc-opencode-plugin-v2`](../v2)
|
|
10
|
+
> instead — the two major versions have incompatible plugin APIs.
|
|
11
|
+
|
|
12
|
+
## What it provides
|
|
13
|
+
|
|
14
|
+
Two flavors are published as separate packages, each built into
|
|
15
|
+
`plugin-dist/<flavor>/` with its own generated manifest — the same layout the v2
|
|
16
|
+
plugin uses.
|
|
17
|
+
|
|
18
|
+
| Package | Provider id | Shown in `/connect` as |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| `@telefonica/ai-sdlc-opencode-plugin-v1` | `cdo-ai` | CDO.AI |
|
|
21
|
+
| `@telefonica/ai-sdlc-opencode-plugin-v1-dev` | `cdo-ai-dev` | CDO.AI (dev) |
|
|
22
|
+
|
|
23
|
+
Each package exposes two entry points, which OpenCode resolves independently:
|
|
24
|
+
|
|
25
|
+
| Export | Built file | Role |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| `./server` | `dist/index.js` | Provider registration, `/connect` auth, model discovery |
|
|
28
|
+
| `./tui` | `dist/tui.js` | Sidebar budget panel |
|
|
29
|
+
|
|
30
|
+
Only `exports["./server"]` and `exports["./tui"]` are consulted. OpenCode never
|
|
31
|
+
reads `exports["."]`, and a `main` field would be picked up as an additional
|
|
32
|
+
server entry point, so neither is declared.
|
|
33
|
+
|
|
34
|
+
The production panel also covers the hand-written `litellm*` providers from the
|
|
35
|
+
older manual setup, so the budget display keeps working while users migrate. The
|
|
36
|
+
dev panel matches only `cdo-ai-dev`, so installing both packages does not produce
|
|
37
|
+
two panels for the same gateway.
|
|
38
|
+
|
|
39
|
+
## Architecture notes
|
|
40
|
+
|
|
41
|
+
These are load-bearing constraints of the OpenCode 1.x plugin API. They look
|
|
42
|
+
arbitrary in isolation, so they are recorded here.
|
|
43
|
+
|
|
44
|
+
**Models come from the `config` hook, not the `provider.models` hook.** OpenCode
|
|
45
|
+
invokes `provider.models` before config-declared providers exist, so it never
|
|
46
|
+
fires for a provider that is not in the models.dev catalog. Discovery therefore
|
|
47
|
+
happens in the `config` hook, which is async and may perform network calls.
|
|
48
|
+
|
|
49
|
+
**A provider with no models is discarded.** OpenCode deletes any provider whose
|
|
50
|
+
model map is empty before `/connect` renders, so the config hook seeds a
|
|
51
|
+
placeholder entry that is replaced once discovery succeeds.
|
|
52
|
+
|
|
53
|
+
**`/connect` does not call `authorize()` for API-key methods.** Values collected
|
|
54
|
+
by `prompts` are stored as credential metadata and the key itself is collected by
|
|
55
|
+
a separate built-in step. The auth method therefore prompts only for the gateway
|
|
56
|
+
URL, and `auth.loader` recombines the stored URL and key into provider options.
|
|
57
|
+
|
|
58
|
+
**Credentials are read from disk.** Stored auth is not exposed in-process when
|
|
59
|
+
the config hook runs, so it reads `auth.json` directly. Resolution order is
|
|
60
|
+
config `options`, then environment variables, then `auth.json`. Resolved values
|
|
61
|
+
are written back into `options` because a fully custom provider builds its
|
|
62
|
+
completion client from them directly.
|
|
63
|
+
|
|
64
|
+
**One environment per package.** `Hooks.auth` binds a single provider id, so each
|
|
65
|
+
environment needs its own plugin instance. Rather than smuggling both into one
|
|
66
|
+
module, each flavor is built and published separately, which keeps the plain
|
|
67
|
+
default-export plugin module shape and matches the v2 plugin.
|
|
68
|
+
|
|
69
|
+
## Layout
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
src/
|
|
73
|
+
├── flavor.ts # prod/dev ids and labels, mirroring the v2 plugin
|
|
74
|
+
├── entry-prod.ts # server entry, prod -> dist/index.js
|
|
75
|
+
├── entry-dev.ts # server entry, dev -> dist/index.js
|
|
76
|
+
├── tui-prod.tsx # TUI entry, prod -> dist/tui.js
|
|
77
|
+
├── tui-dev.tsx # TUI entry, dev -> dist/tui.js
|
|
78
|
+
├── tui.tsx # sidebar slot registration and budget polling
|
|
79
|
+
├── components.tsx # budget panel rendering
|
|
80
|
+
├── usage-client.ts # /v1/usage fetch for the budget panel
|
|
81
|
+
└── server/
|
|
82
|
+
├── plugin.ts # assembles the hooks for a flavor
|
|
83
|
+
├── config-hook.ts # provider registration and model discovery
|
|
84
|
+
├── auth-hook.ts # /connect prompts and credential loading
|
|
85
|
+
├── credentials.ts # config / env / auth.json resolution
|
|
86
|
+
├── models-client.ts # /v1/models and /v1/model/info
|
|
87
|
+
├── model-cache.ts # 7-day on-disk model cache
|
|
88
|
+
└── constants.ts
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`FLAVOR=dev|prod` is the only build flag. Every identity string lives in
|
|
92
|
+
`src/flavor.ts`; there is no runtime environment lookup.
|
|
93
|
+
|
|
94
|
+
## Develop
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npm ci
|
|
98
|
+
npm run build:prod # writes plugin-dist/prod/dist/
|
|
99
|
+
npm run build:dev # writes plugin-dist/dev/dist/
|
|
100
|
+
npm run build # both
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Point OpenCode at a built flavor to try it:
|
|
104
|
+
|
|
105
|
+
```jsonc
|
|
106
|
+
{ "plugin": ["/absolute/path/to/plugins/opencode/v1/plugin-dist/prod"] }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
For OpenCode 1.x, put this entry in both `opencode.jsonc` and `tui.jsonc`.
|
|
110
|
+
The server and TUI plugin loaders use separate config files.
|
|
111
|
+
|
|
112
|
+
Build before launching — OpenCode installs plugins with scripts disabled and will
|
|
113
|
+
not build the package for you. Quit OpenCode fully between attempts, since a
|
|
114
|
+
plugin that fails to load stays cached for the lifetime of the process.
|
|
115
|
+
|
|
116
|
+
## Release
|
|
117
|
+
|
|
118
|
+
Published by `.github/workflows/opencode-plugin-release.yml` via
|
|
119
|
+
`semantic-release`, tagged `opencode-v1@<version>`.
|
|
120
|
+
|
|
121
|
+
`plugin-dist/` is a build output directory and is not committed. The release
|
|
122
|
+
`prepareCmd` builds both flavors and then runs `scripts/write-manifests.js`,
|
|
123
|
+
which generates each publish manifest; `publishCmd` publishes both. The root
|
|
124
|
+
package is `private` and exists only to hold the sources and release config.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/flavor.ts
|
|
2
|
+
const DEV = {
|
|
3
|
+
environment: "dev",
|
|
4
|
+
serverPluginID: "ai-sdlc-opencode-cdo-ai-dev-plugin",
|
|
5
|
+
tuiPluginID: "ai-sdlc-opencode-cdo-ai-dev-plugin.tui",
|
|
6
|
+
providerID: "cdo-ai-dev",
|
|
7
|
+
displayName: "CDO.AI (dev)",
|
|
8
|
+
connectLabel: "CDO.AI (dev) API key",
|
|
9
|
+
apiKeyEnvVars: ["CDO_AI_DEV_API_KEY"],
|
|
10
|
+
matchesPanelProvider: (id) => id === "cdo-ai-dev"
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { DEV as t };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { t as DEV } from "./flavor-CMZWvD--.js";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
//#region src/server/auth-hook.ts
|
|
7
|
+
function createAuthHook(flavor) {
|
|
8
|
+
return {
|
|
9
|
+
provider: flavor.providerID,
|
|
10
|
+
async loader(auth) {
|
|
11
|
+
const creds = await auth();
|
|
12
|
+
if (!creds || creds.type !== "api") return {};
|
|
13
|
+
return {
|
|
14
|
+
baseURL: creds.metadata?.["url"],
|
|
15
|
+
apiKey: creds.key
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
methods: [{
|
|
19
|
+
type: "api",
|
|
20
|
+
label: flavor.connectLabel,
|
|
21
|
+
prompts: [{
|
|
22
|
+
type: "text",
|
|
23
|
+
key: "url",
|
|
24
|
+
message: `${flavor.displayName} proxy URL`,
|
|
25
|
+
placeholder: "https://litellm.example.com/v1"
|
|
26
|
+
}]
|
|
27
|
+
}]
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/server/constants.ts
|
|
32
|
+
const PROVIDER_NPM = "@ai-sdk/openai-compatible";
|
|
33
|
+
const FETCH_TIMEOUT_MS = 8e3;
|
|
34
|
+
const CACHE_TTL_MS = 6048e5;
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/server/credentials.ts
|
|
37
|
+
function normalizeRoot(url) {
|
|
38
|
+
return url.trim().replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
39
|
+
}
|
|
40
|
+
function authFilePath() {
|
|
41
|
+
const dataHome = process.env["XDG_DATA_HOME"] || join(homedir(), ".local", "share");
|
|
42
|
+
return join(dataHome, "opencode", "auth.json");
|
|
43
|
+
}
|
|
44
|
+
async function fromAuthFile(providerID) {
|
|
45
|
+
try {
|
|
46
|
+
const raw = await readFile(authFilePath(), "utf8");
|
|
47
|
+
const entry = JSON.parse(raw)[providerID];
|
|
48
|
+
if (!entry || entry.type !== "api") return {};
|
|
49
|
+
return {
|
|
50
|
+
apiKey: entry.key,
|
|
51
|
+
baseURL: entry.metadata?.["url"]
|
|
52
|
+
};
|
|
53
|
+
} catch {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function fromEnv(flavor) {
|
|
58
|
+
for (const name of flavor.apiKeyEnvVars) {
|
|
59
|
+
const value = process.env[name];
|
|
60
|
+
if (value) return value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function resolveCredentials(flavor, options) {
|
|
64
|
+
const stored = await fromAuthFile(flavor.providerID);
|
|
65
|
+
const apiKey = (typeof options.apiKey === "string" ? options.apiKey : void 0) || fromEnv(flavor) || stored.apiKey;
|
|
66
|
+
const baseURL = (typeof options.baseURL === "string" ? options.baseURL : void 0) || stored.baseURL;
|
|
67
|
+
return {
|
|
68
|
+
apiKey,
|
|
69
|
+
baseURL: baseURL ? normalizeRoot(baseURL) : void 0
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/server/model-cache.ts
|
|
74
|
+
function cacheDir() {
|
|
75
|
+
const cacheHome = process.env["XDG_CACHE_HOME"] || join(homedir(), ".cache");
|
|
76
|
+
return join(cacheHome, "opencode-cdo-ai");
|
|
77
|
+
}
|
|
78
|
+
function cachePath(root) {
|
|
79
|
+
const key = createHash("sha256").update(root).digest("hex").slice(0, 16);
|
|
80
|
+
return join(cacheDir(), `models-${key}.json`);
|
|
81
|
+
}
|
|
82
|
+
async function readCache(root) {
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(await readFile(cachePath(root), "utf8"));
|
|
85
|
+
if (!parsed.models || Object.keys(parsed.models).length === 0) return void 0;
|
|
86
|
+
return {
|
|
87
|
+
models: parsed.models,
|
|
88
|
+
fresh: Date.now() - parsed.savedAt < CACHE_TTL_MS
|
|
89
|
+
};
|
|
90
|
+
} catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function writeCache(root, models) {
|
|
95
|
+
try {
|
|
96
|
+
await mkdir(cacheDir(), { recursive: true });
|
|
97
|
+
const payload = {
|
|
98
|
+
savedAt: Date.now(),
|
|
99
|
+
models
|
|
100
|
+
};
|
|
101
|
+
await writeFile(cachePath(root), JSON.stringify(payload), "utf8");
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/server/models-client.ts
|
|
106
|
+
async function getJSON(url, apiKey) {
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch(url, {
|
|
111
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
112
|
+
signal: controller.signal
|
|
113
|
+
});
|
|
114
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
115
|
+
return await res.json();
|
|
116
|
+
} finally {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function parseIds(payload) {
|
|
121
|
+
const data = payload?.data;
|
|
122
|
+
if (!Array.isArray(data)) return [];
|
|
123
|
+
return data.map((m) => m.id).filter((id) => typeof id === "string" && id.length > 0);
|
|
124
|
+
}
|
|
125
|
+
function parseInfo(payload) {
|
|
126
|
+
const data = payload?.data;
|
|
127
|
+
if (!Array.isArray(data)) return {};
|
|
128
|
+
const out = {};
|
|
129
|
+
for (const row of data) if (typeof row.model_name === "string" && row.model_info) out[row.model_name] = row.model_info;
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
function toEntry(id, info) {
|
|
133
|
+
return {
|
|
134
|
+
name: id,
|
|
135
|
+
limit: {
|
|
136
|
+
context: info?.max_input_tokens ?? info?.max_tokens ?? 128e3,
|
|
137
|
+
output: info?.max_output_tokens ?? 16384
|
|
138
|
+
},
|
|
139
|
+
tool_call: info?.supports_function_calling ?? true,
|
|
140
|
+
reasoning: info?.supports_reasoning ?? false,
|
|
141
|
+
attachment: info?.supports_vision ?? false
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async function discoverModels(root, apiKey) {
|
|
145
|
+
const [idsResult, infoResult] = await Promise.allSettled([getJSON(`${root}/v1/models`, apiKey), getJSON(`${root}/v1/model/info`, apiKey)]);
|
|
146
|
+
if (idsResult.status !== "fulfilled") throw idsResult.reason;
|
|
147
|
+
const ids = parseIds(idsResult.value);
|
|
148
|
+
const info = infoResult.status === "fulfilled" ? parseInfo(infoResult.value) : {};
|
|
149
|
+
const models = {};
|
|
150
|
+
for (const id of ids) {
|
|
151
|
+
const meta = info[id];
|
|
152
|
+
if (meta?.mode && meta.mode !== "chat") continue;
|
|
153
|
+
models[id] = toEntry(id, meta);
|
|
154
|
+
}
|
|
155
|
+
return models;
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/server/config-hook.ts
|
|
159
|
+
async function resolveModels(root, apiKey) {
|
|
160
|
+
const cached = await readCache(root);
|
|
161
|
+
if (cached?.fresh) return cached.models;
|
|
162
|
+
try {
|
|
163
|
+
const discovered = await discoverModels(root, apiKey);
|
|
164
|
+
if (Object.keys(discovered).length === 0) return cached?.models;
|
|
165
|
+
await writeCache(root, discovered);
|
|
166
|
+
return discovered;
|
|
167
|
+
} catch {
|
|
168
|
+
return cached?.models;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function createConfigHook(flavor) {
|
|
172
|
+
return async function configHook(config) {
|
|
173
|
+
try {
|
|
174
|
+
config.provider ??= {};
|
|
175
|
+
const provider = config.provider[flavor.providerID] ??= {};
|
|
176
|
+
provider.name ??= flavor.displayName;
|
|
177
|
+
provider.npm ??= PROVIDER_NPM;
|
|
178
|
+
provider.options ??= {};
|
|
179
|
+
provider.models ??= { ["_"]: {} };
|
|
180
|
+
const { baseURL, apiKey } = await resolveCredentials(flavor, provider.options);
|
|
181
|
+
if (!baseURL || !apiKey) return;
|
|
182
|
+
provider.options.baseURL ??= `${baseURL}/v1`;
|
|
183
|
+
provider.options.apiKey ??= apiKey;
|
|
184
|
+
const models = await resolveModels(baseURL, apiKey);
|
|
185
|
+
if (!models || Object.keys(models).length === 0) return;
|
|
186
|
+
provider.models = models;
|
|
187
|
+
} catch {}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/server/plugin.ts
|
|
192
|
+
function createServer(flavor) {
|
|
193
|
+
return async (_ctx) => ({
|
|
194
|
+
config: createConfigHook(flavor),
|
|
195
|
+
auth: createAuthHook(flavor)
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/entry-dev.ts
|
|
200
|
+
var entry_dev_default = {
|
|
201
|
+
id: DEV.serverPluginID,
|
|
202
|
+
server: createServer(DEV)
|
|
203
|
+
};
|
|
204
|
+
//#endregion
|
|
205
|
+
export { entry_dev_default as default };
|
package/dist/tui.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { t as DEV } from "./flavor-CMZWvD--.js";
|
|
2
|
+
import { For, createSignal } from "solid-js";
|
|
3
|
+
import { createTextAttributes } from "@opentui/core";
|
|
4
|
+
import { jsx, jsxs } from "@opentui/solid/jsx-runtime";
|
|
5
|
+
//#region src/usage-client.ts
|
|
6
|
+
function deriveApiUrl(litellmBaseURL) {
|
|
7
|
+
return litellmBaseURL.replace(/^(https?:\/\/)litellm(\.[^/]+).*/, "$1api$2");
|
|
8
|
+
}
|
|
9
|
+
async function fetchUsage(baseURL, apiKey, timeoutMs) {
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch(`${deriveApiUrl(baseURL)}/v1/usage`, {
|
|
14
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
15
|
+
signal: controller.signal
|
|
16
|
+
});
|
|
17
|
+
if (!res.ok) return `HTTP ${res.status}`;
|
|
18
|
+
return await res.json();
|
|
19
|
+
} catch (e) {
|
|
20
|
+
return e instanceof Error ? e.message : "network error";
|
|
21
|
+
} finally {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function round1(n) {
|
|
26
|
+
return Math.round(n * 10) / 10;
|
|
27
|
+
}
|
|
28
|
+
function toEntry(providerName, result) {
|
|
29
|
+
if (typeof result === "string") return {
|
|
30
|
+
providerName,
|
|
31
|
+
error: result
|
|
32
|
+
};
|
|
33
|
+
const percent = result.budget != null ? round1(result.spend / result.budget * 100) : null;
|
|
34
|
+
return {
|
|
35
|
+
providerName,
|
|
36
|
+
...result,
|
|
37
|
+
percent
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/components.tsx
|
|
42
|
+
const BAR_WIDTH = 20;
|
|
43
|
+
function bar(percent) {
|
|
44
|
+
if (percent == null) return "∞";
|
|
45
|
+
const filled = Math.min(Math.round(percent / 100 * BAR_WIDTH), BAR_WIDTH);
|
|
46
|
+
return "█".repeat(filled) + "░".repeat(BAR_WIDTH - filled);
|
|
47
|
+
}
|
|
48
|
+
function barColor(entry, theme) {
|
|
49
|
+
const pct = entry.percent ?? 0;
|
|
50
|
+
return pct >= 100 ? theme.error : pct >= 80 ? theme.warning : theme.success;
|
|
51
|
+
}
|
|
52
|
+
function valuesLine(entry) {
|
|
53
|
+
if (entry.error != null) return ` ${entry.error}`;
|
|
54
|
+
return entry.budget != null ? ` $${entry.spend.toFixed(2)} / $${entry.budget.toFixed(2)} (${entry.percent?.toFixed(1)}%)` : ` $${entry.spend.toFixed(2)} / ∞`;
|
|
55
|
+
}
|
|
56
|
+
function UsagePanel(props) {
|
|
57
|
+
return /* @__PURE__ */ jsx(For, {
|
|
58
|
+
each: props.entries,
|
|
59
|
+
children: (entry) => /* @__PURE__ */ jsxs("box", { children: [
|
|
60
|
+
/* @__PURE__ */ jsx("text", {
|
|
61
|
+
attributes: createTextAttributes({ bold: true }),
|
|
62
|
+
children: entry.providerName
|
|
63
|
+
}),
|
|
64
|
+
/* @__PURE__ */ jsx("text", {
|
|
65
|
+
fg: entry.error != null ? props.theme.error : void 0,
|
|
66
|
+
children: valuesLine(entry)
|
|
67
|
+
}),
|
|
68
|
+
/* @__PURE__ */ jsx("text", {
|
|
69
|
+
fg: barColor(entry, props.theme),
|
|
70
|
+
children: entry.error == null ? ` ${bar(entry.percent ?? null)}` : ""
|
|
71
|
+
})
|
|
72
|
+
] })
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/tui.tsx
|
|
77
|
+
const REFRESH_MS = 6e4;
|
|
78
|
+
const TIMEOUT_MS = 5e3;
|
|
79
|
+
function createTui(flavor) {
|
|
80
|
+
return (api) => {
|
|
81
|
+
const [entries, setEntries] = createSignal([]);
|
|
82
|
+
async function refresh() {
|
|
83
|
+
try {
|
|
84
|
+
const providers = api.state.provider.filter((p) => flavor.matchesPanelProvider(p.id));
|
|
85
|
+
const results = await Promise.all(providers.map(async (p) => {
|
|
86
|
+
const options = p.options ?? {};
|
|
87
|
+
const baseURL = options["baseURL"];
|
|
88
|
+
const apiKey = p.key ?? options["apiKey"];
|
|
89
|
+
if (!apiKey) return toEntry(p.name, "no API key configured");
|
|
90
|
+
if (!baseURL) return toEntry(p.name, "no base URL configured");
|
|
91
|
+
return toEntry(p.name, await fetchUsage(baseURL, apiKey, TIMEOUT_MS));
|
|
92
|
+
}));
|
|
93
|
+
setEntries(results);
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
refresh();
|
|
97
|
+
const interval = setInterval(refresh, REFRESH_MS);
|
|
98
|
+
api.lifecycle.onDispose(() => clearInterval(interval));
|
|
99
|
+
api.slots.register({
|
|
100
|
+
order: 101,
|
|
101
|
+
slots: { sidebar_content(ctx) {
|
|
102
|
+
return /* @__PURE__ */ jsx(UsagePanel, {
|
|
103
|
+
entries: entries(),
|
|
104
|
+
theme: ctx.theme.current
|
|
105
|
+
});
|
|
106
|
+
} }
|
|
107
|
+
});
|
|
108
|
+
return Promise.resolve();
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/tui-dev.tsx
|
|
113
|
+
var tui_dev_default = {
|
|
114
|
+
id: DEV.tuiPluginID,
|
|
115
|
+
tui: createTui(DEV)
|
|
116
|
+
};
|
|
117
|
+
//#endregion
|
|
118
|
+
export { tui_dev_default as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@telefonica/ai-sdlc-opencode-plugin-v1-dev",
|
|
3
|
+
"version": "0.1.0-snapshot.35846538366",
|
|
4
|
+
"description": "OpenCode 1.x plugin (dev environment): CDO.AI provider registration, model discovery, and budget panel",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./server": "./dist/index.js",
|
|
8
|
+
"./tui": "./dist/tui.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
]
|
|
13
|
+
}
|