@telefonica/ai-sdlc-opencode-plugin-v1-dev 1.0.0 → 1.0.1

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
@@ -17,4 +17,7 @@ opencode plugin -g @telefonica/ai-sdlc-opencode-plugin-v1-dev
17
17
  Restart OpenCode, run `/connect`, choose **CDO.AI**, and enter the gateway URL
18
18
  and API key. Restart OpenCode once more after connecting so the model list loads.
19
19
 
20
+ The sidebar displays usage, average spend per elapsed workday, and workdays remaining
21
+ this month on a separate line.
22
+
20
23
  Using OpenCode 2.x? Install `@telefonica/ai-sdlc-opencode-plugin-v2` instead.
@@ -0,0 +1,405 @@
1
+ //#region ../common/dist/model-name.js
2
+ const SPECIAL_LABELS = {
3
+ cdo: "CDO",
4
+ deepseek: "DeepSeek",
5
+ "d-seek": "DeepSeek",
6
+ gemi: "Gemini",
7
+ gpt: "GPT",
8
+ km: "Kimi"
9
+ };
10
+ function words(value) {
11
+ return value.trim().split(/[\s-]+/).filter(Boolean);
12
+ }
13
+ function joinAdjacentNumbers(parts) {
14
+ const result = [];
15
+ for (let index = 0; index < parts.length; index += 1) if (/^v\d+(?:\.\d+)*$/i.test(parts[index])) {
16
+ let version = `v${parts[index].slice(1)}`;
17
+ while (index + 1 < parts.length && /^\d+$/.test(parts[index + 1])) {
18
+ version += `.${parts[index + 1]}`;
19
+ index += 1;
20
+ }
21
+ result.push(version);
22
+ } else if (/^\d+(?:\.\d+)*$/.test(parts[index])) {
23
+ let number = parts[index];
24
+ while (index + 1 < parts.length && /^\d+$/.test(parts[index + 1])) {
25
+ number += `.${parts[index + 1]}`;
26
+ index += 1;
27
+ }
28
+ result.push(number);
29
+ } else result.push(parts[index]);
30
+ return result;
31
+ }
32
+ function titleCase(word) {
33
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
34
+ }
35
+ /** Converts a provider model ID into a readable label. */
36
+ function unsanitizeModelId(modelId) {
37
+ const parts = words(modelId.replace(/^claude-/i, ""));
38
+ const normalized = [];
39
+ for (let index = 0; index < parts.length; index += 1) {
40
+ const current = parts[index].toLowerCase();
41
+ const next = parts[index + 1]?.toLowerCase();
42
+ if (current === "g" && next === "p" && parts[index + 2]?.toLowerCase() === "t") {
43
+ normalized.push("GPT");
44
+ index += 2;
45
+ } else if (current === "gp" && next === "t") {
46
+ normalized.push("GPT");
47
+ index += 1;
48
+ } else if (current === "gemi" && next === "n") {
49
+ normalized.push("Gemini");
50
+ index += 1;
51
+ } else if (current === "d" && next === "seek") {
52
+ normalized.push("DeepSeek");
53
+ index += 1;
54
+ } else normalized.push(Object.hasOwn(SPECIAL_LABELS, current) ? SPECIAL_LABELS[current] : current);
55
+ }
56
+ return joinAdjacentNumbers(normalized.at(-1)?.toLowerCase() === "az" ? normalized.slice(0, -1) : normalized).map((part) => {
57
+ if (/^(CDO|GPT|Gemini|Kimi|DeepSeek)$/.test(part) || /^v\d/i.test(part) || /^\d/.test(part)) return part;
58
+ return titleCase(part);
59
+ }).join(" ");
60
+ }
61
+ //#endregion
62
+ //#region ../common/dist/copy.js
63
+ const PLUGIN_UI = {
64
+ barWidth: 20,
65
+ warningPercent: 80,
66
+ exceededPercent: 100
67
+ };
68
+ const formatMoney = (amount) => `$${amount.toFixed(2)}`;
69
+ const PLUGIN_COPY = {
70
+ apiKeyLabel: (displayName) => `${displayName} API key`,
71
+ proxyURLLabel: (displayName) => `${displayName} Proxy URL`,
72
+ proxyURLPlaceholder: "https://litellm.example.com",
73
+ proxyURLRequired: "Proxy URL is required",
74
+ connectCommand: (integrationID) => `/connect ${integrationID}`,
75
+ usageUnavailable: (message) => `Usage unavailable: ${message}`,
76
+ notConnected: (command) => `not connected — run ${command}`,
77
+ formatMoney,
78
+ usageValues: (spend, budget) => `${formatMoney(spend)} / ${budget == null ? "∞" : formatMoney(budget)}`,
79
+ formatPercent: (percent) => `${percent.toFixed(1)}%`,
80
+ budgetSourceLabels: {
81
+ key: "Personal API key budget",
82
+ user: "User budget",
83
+ "team-member": "Team member budget",
84
+ "team-member-default": "Team default budget",
85
+ none: "No budget set"
86
+ },
87
+ progressBar: (percent, width = PLUGIN_UI.barWidth) => {
88
+ const filled = Math.round(Math.max(0, Math.min(100, percent)) / 100 * width);
89
+ return "█".repeat(filled) + "░".repeat(width - filled);
90
+ },
91
+ perWorkday: (amount) => `${amount} per work day`,
92
+ workdaysRemaining: (count) => `${count} work days remaining`
93
+ };
94
+ //#endregion
95
+ //#region ../common/dist/flavors.js
96
+ function defineFlavor(environment, providerID, displayName) {
97
+ const serverPluginID = `ai-sdlc-opencode-${providerID}-plugin`;
98
+ return {
99
+ environment,
100
+ providerID,
101
+ integrationID: providerID,
102
+ rpcID: providerID,
103
+ serverPluginID,
104
+ tuiPluginID: `${serverPluginID}.tui`,
105
+ displayName,
106
+ connectLabel: PLUGIN_COPY.apiKeyLabel(displayName),
107
+ keyEnvironmentPrefix: environment === "dev" ? "CDO_AI_DEV" : "CDO_AI",
108
+ debugFile: `/tmp/${providerID}-debug.json`,
109
+ budgetStorageKey: `${providerID}.budget`
110
+ };
111
+ }
112
+ const PLUGIN_FLAVORS = {
113
+ prod: defineFlavor("prod", "cdo-ai", "CDO.AI"),
114
+ dev: defineFlavor("dev", "cdo-ai-dev", "CDO.AI (dev)")
115
+ };
116
+ //#endregion
117
+ //#region ../common/dist/reasoning-efforts.js
118
+ const EFFORT_ORDER = [
119
+ "none",
120
+ "minimal",
121
+ "low",
122
+ "medium",
123
+ "high",
124
+ "xhigh",
125
+ "max"
126
+ ];
127
+ const BASELINE_EFFORTS = [
128
+ "low",
129
+ "medium",
130
+ "high"
131
+ ];
132
+ const RESTRICTED_EFFORTS = /* @__PURE__ */ new Map([
133
+ ["gpt-5-pro", ["high"]],
134
+ ["gpt-5.2-pro", [
135
+ "medium",
136
+ "high",
137
+ "xhigh"
138
+ ]],
139
+ ["gpt-5.4-pro", [
140
+ "medium",
141
+ "high",
142
+ "xhigh"
143
+ ]]
144
+ ]);
145
+ function restrictedEfforts(...identifiers) {
146
+ for (const identifier of identifiers) {
147
+ if (typeof identifier !== "string") continue;
148
+ const model = identifier.split("/").pop()?.replace(/-\d{4}-\d{2}-\d{2}$/, "");
149
+ const efforts = model ? RESTRICTED_EFFORTS.get(model) : void 0;
150
+ if (efforts) return efforts;
151
+ }
152
+ }
153
+ function isRecord(value) {
154
+ return value !== null && typeof value === "object" && !Array.isArray(value);
155
+ }
156
+ function resolveReasoningEfforts(info, params) {
157
+ const metadata = { ...params ?? {} };
158
+ for (const [key, value] of Object.entries(info)) if (value != null) metadata[key] = value;
159
+ if (metadata.supports_reasoning === false) return [];
160
+ for (const key of ["reasoning_effort_levels", "supports_reasoning_efforts"]) {
161
+ const levels = metadata[key];
162
+ if (Array.isArray(levels)) {
163
+ const explicit = [...new Set(levels.filter((level) => typeof level === "string"))];
164
+ const restricted = restrictedEfforts(metadata.key, metadata.model, metadata.base_model);
165
+ return restricted ? explicit.filter((effort) => restricted.includes(effort)) : explicit;
166
+ }
167
+ }
168
+ const supportedParams = metadata.supported_openai_params;
169
+ if (Array.isArray(supportedParams) && !supportedParams.includes("reasoning_effort") && !supportedParams.includes("reasoning")) return [];
170
+ const flags = /* @__PURE__ */ new Map();
171
+ for (const [key, value] of Object.entries(metadata)) {
172
+ const match = key.match(/^supports_([a-z]+)_reasoning_effort$/);
173
+ if (match && typeof value === "boolean") flags.set(match[1], value);
174
+ }
175
+ if (flags.size === 0 || metadata.supports_reasoning !== true && ![...flags.values()].includes(true)) return;
176
+ const efforts = /* @__PURE__ */ new Set();
177
+ for (const effort of BASELINE_EFFORTS) if (flags.get(effort) !== false) efforts.add(effort);
178
+ for (const [effort, supported] of flags) if (supported) efforts.add(effort);
179
+ const restricted = restrictedEfforts(metadata.key, metadata.model, metadata.base_model);
180
+ if (restricted) {
181
+ for (const effort of [...efforts]) if (!restricted.includes(effort)) efforts.delete(effort);
182
+ }
183
+ return [...EFFORT_ORDER.filter((effort) => efforts.has(effort)), ...[...efforts].filter((effort) => !EFFORT_ORDER.includes(effort))];
184
+ }
185
+ //#endregion
186
+ //#region ../common/dist/models.js
187
+ function finiteNumber(value) {
188
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
189
+ }
190
+ function booleanValue(value) {
191
+ return typeof value === "boolean" ? value : void 0;
192
+ }
193
+ function normalizeModel(entry, info, params) {
194
+ const value = {
195
+ ...entry,
196
+ ...info ?? {}
197
+ };
198
+ const reasoning = resolveReasoningEfforts(info ?? {}, params);
199
+ const mode = typeof value.mode === "string" ? value.mode : void 0;
200
+ return Object.fromEntries(Object.entries({
201
+ id: entry.id,
202
+ name: unsanitizeModelId(entry.id),
203
+ mode,
204
+ contextTokens: finiteNumber(value.max_input_tokens) ?? finiteNumber(value.max_tokens),
205
+ inputTokens: finiteNumber(value.max_input_tokens),
206
+ outputTokens: finiteNumber(value.max_output_tokens),
207
+ inputCostPerToken: finiteNumber(value.input_cost_per_token),
208
+ outputCostPerToken: finiteNumber(value.output_cost_per_token),
209
+ cacheReadCostPerToken: finiteNumber(value.cache_read_input_token_cost),
210
+ cacheWriteCostPerToken: finiteNumber(value.cache_creation_input_token_cost),
211
+ supportsTools: booleanValue(value.supports_function_calling),
212
+ supportsReasoning: booleanValue(info?.supports_reasoning) ?? booleanValue(params?.supports_reasoning),
213
+ supportsVision: booleanValue(value.supports_vision),
214
+ supportsPdf: booleanValue(value.supports_pdf_input),
215
+ supportsAudio: booleanValue(value.supports_audio_input) ?? booleanValue(params?.supports_audio_input),
216
+ reasoningEfforts: reasoning
217
+ }).filter(([, value]) => value !== void 0));
218
+ }
219
+ function invalid(message) {
220
+ return {
221
+ ok: false,
222
+ error: {
223
+ code: "invalid-response",
224
+ message
225
+ }
226
+ };
227
+ }
228
+ function httpError(status) {
229
+ return {
230
+ ok: false,
231
+ error: {
232
+ code: "http",
233
+ status,
234
+ message: `HTTP ${status}`
235
+ }
236
+ };
237
+ }
238
+ function safeEndpoint(endpoint) {
239
+ try {
240
+ const url = new URL(endpoint);
241
+ url.username = "";
242
+ url.password = "";
243
+ url.search = "";
244
+ url.hash = "";
245
+ return url.toString();
246
+ } catch {
247
+ return endpoint.split(/[?#]/, 1)[0] ?? endpoint;
248
+ }
249
+ }
250
+ function emitGatewayLog(options, event) {
251
+ const level = event.ok ? "info" : "error";
252
+ const message = `${event.operation} ${event.phase} ${event.ok ? "succeeded" : "failed"}`;
253
+ try {
254
+ const result = options.logger?.({
255
+ ...event,
256
+ level,
257
+ message,
258
+ endpoint: safeEndpoint(event.endpoint)
259
+ });
260
+ if (result && typeof result.then === "function") result.catch(() => {});
261
+ } catch {}
262
+ }
263
+ async function fetchJSON(url, options, operation = "models.discover") {
264
+ const controller = new AbortController();
265
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 8e3);
266
+ const startedAt = Date.now();
267
+ let status;
268
+ let result;
269
+ try {
270
+ const response = await (options.fetchImpl ?? fetch)(url, {
271
+ headers: options.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
272
+ signal: controller.signal
273
+ });
274
+ status = response.status;
275
+ if (!response.ok) result = httpError(response.status);
276
+ else try {
277
+ result = {
278
+ ok: true,
279
+ value: await response.json()
280
+ };
281
+ } catch {
282
+ result = invalid(`Invalid JSON from ${url}`);
283
+ }
284
+ } catch (error) {
285
+ result = {
286
+ ok: false,
287
+ error: {
288
+ code: controller.signal.aborted ? "timeout" : "network",
289
+ message: error instanceof Error ? error.message : "Network error"
290
+ }
291
+ };
292
+ } finally {
293
+ clearTimeout(timer);
294
+ }
295
+ emitGatewayLog(options, {
296
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
297
+ operation,
298
+ phase: "request",
299
+ endpoint: url,
300
+ ok: result.ok,
301
+ status,
302
+ durationMs: Date.now() - startedAt,
303
+ ...!result.ok ? { errorCode: result.error.code } : {}
304
+ });
305
+ return result;
306
+ }
307
+ function modelRows(payload) {
308
+ if (!isRecord(payload) || !Array.isArray(payload.data) || payload.data.length === 0) return void 0;
309
+ if (!payload.data.every((entry) => isRecord(entry) && typeof entry.id === "string" && entry.id.length > 0)) return void 0;
310
+ return payload.data;
311
+ }
312
+ function infoRows(payload) {
313
+ if (!isRecord(payload) || !Array.isArray(payload.data)) return [];
314
+ return payload.data.filter(isRecord);
315
+ }
316
+ function usableModel(model) {
317
+ return !model.mode || model.mode === "chat";
318
+ }
319
+ async function discoverModelsResult(options) {
320
+ const startedAt = Date.now();
321
+ const baseURL = options.baseURL.trim().replace(/\/+$/, "").replace(/\/v1$/, "");
322
+ const [modelsResponse, infoResponse] = await Promise.all([fetchJSON(`${baseURL}/v1/models`, options, "models.discover"), fetchJSON(`${baseURL}/v1/model/info`, options, "models.discover")]);
323
+ let result;
324
+ if (!modelsResponse.ok) result = modelsResponse;
325
+ else {
326
+ const rows = modelRows(modelsResponse.value);
327
+ if (!rows) result = invalid("Invalid or empty /v1/models response");
328
+ else {
329
+ const infos = /* @__PURE__ */ new Map();
330
+ if (infoResponse.ok) {
331
+ for (const row of infoRows(infoResponse.value)) if (typeof row.model_name === "string") infos.set(row.model_name, row);
332
+ }
333
+ const models = rows.map((row) => {
334
+ const metadata = infos.get(row.id);
335
+ return normalizeModel(row, metadata?.model_info, metadata?.litellm_params);
336
+ }).filter(usableModel);
337
+ result = models.length > 0 ? {
338
+ ok: true,
339
+ value: models
340
+ } : invalid("No usable chat models in /v1/models response");
341
+ }
342
+ }
343
+ emitGatewayLog(options, {
344
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
345
+ operation: "models.discover",
346
+ phase: "operation",
347
+ endpoint: `${baseURL}/v1/models`,
348
+ ok: result.ok,
349
+ durationMs: Date.now() - startedAt,
350
+ ...!result.ok ? { errorCode: result.error.code } : {},
351
+ ...result.ok ? { details: { modelCount: result.value.length } } : {}
352
+ });
353
+ return result;
354
+ }
355
+ async function discoverModels(options) {
356
+ return discoverModelsResult(options);
357
+ }
358
+ //#endregion
359
+ //#region ../common/dist/presentation.js
360
+ function normalizeProviderURL(url) {
361
+ const root = url.trim().replace(/\/+$/, "").replace(/(?:\/v1)+$/i, "");
362
+ return root ? `${root}/v1` : "";
363
+ }
364
+ function resolveUsagePercent(spend, budget) {
365
+ const thresholdPercent = budget != null && budget > 0 ? spend / budget * 100 : null;
366
+ return {
367
+ percent: thresholdPercent == null ? null : Math.round(thresholdPercent * 10) / 10,
368
+ thresholdPercent
369
+ };
370
+ }
371
+ const BUDGET_SOURCE_LABELS = PLUGIN_COPY.budgetSourceLabels;
372
+ function calendarDate(value) {
373
+ return new Date(value.getFullYear(), value.getMonth(), value.getDate());
374
+ }
375
+ function monthlyUsageMetrics(spend, now = /* @__PURE__ */ new Date()) {
376
+ const today = calendarDate(now);
377
+ const monthEnd = new Date(today.getFullYear(), today.getMonth() + 1, 0);
378
+ let elapsedWeekdays = 0;
379
+ let remainingWorkdays = 0;
380
+ for (let day = 1; day <= monthEnd.getDate(); day++) {
381
+ const weekday = new Date(today.getFullYear(), today.getMonth(), day).getDay();
382
+ if (weekday === 0 || weekday === 6) continue;
383
+ if (day <= today.getDate()) elapsedWeekdays++;
384
+ else remainingWorkdays++;
385
+ }
386
+ return {
387
+ workdayAverage: elapsedWeekdays > 0 ? spend / elapsedWeekdays : null,
388
+ remainingWorkdays
389
+ };
390
+ }
391
+ //#endregion
392
+ //#region src/flavor.ts
393
+ function withV1Behavior(flavor, matchesPanelProvider) {
394
+ const apiKeyEnvVars = [`${flavor.keyEnvironmentPrefix}_API_KEY`, ...flavor.environment === "prod" ? ["LITELLM_API_KEY", "LITELLM_MASTER_KEY"] : []];
395
+ return {
396
+ ...flavor,
397
+ connectLabel: PLUGIN_COPY.apiKeyLabel(flavor.displayName),
398
+ apiKeyEnvVars,
399
+ matchesPanelProvider
400
+ };
401
+ }
402
+ withV1Behavior(PLUGIN_FLAVORS.prod, (id) => id === PLUGIN_FLAVORS.prod.providerID || id.includes("litellm"));
403
+ const DEV = withV1Behavior(PLUGIN_FLAVORS.dev, (id) => id === PLUGIN_FLAVORS.dev.providerID);
404
+ //#endregion
405
+ export { resolveUsagePercent as a, fetchJSON as c, PLUGIN_UI as d, unsanitizeModelId as f, normalizeProviderURL as i, isRecord as l, BUDGET_SOURCE_LABELS as n, discoverModels as o, monthlyUsageMetrics as r, emitGatewayLog as s, DEV as t, PLUGIN_COPY as u };
@@ -0,0 +1,7 @@
1
+ //#region src/entry-dev.d.ts
2
+ declare const _default: {
3
+ id: string;
4
+ server: import("@opencode-ai/plugin").Plugin;
5
+ };
6
+ //#endregion
7
+ export { _default as default };
package/dist/index.js CHANGED
@@ -1,8 +1,116 @@
1
- import { t as DEV } from "./flavor-CMZWvD--.js";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { f as unsanitizeModelId, i as normalizeProviderURL, o as discoverModels, t as DEV, u as PLUGIN_COPY } from "./flavor-8QVaVpzl.js";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
3
4
  import { homedir } from "node:os";
4
5
  import { join } from "node:path";
5
- import { createHash } from "node:crypto";
6
+ //#region ../common/dist/cache.js
7
+ function cacheDirectory() {
8
+ const root = process.env["XDG_CACHE_HOME"] || join(homedir(), ".cache");
9
+ return join(root, "opencode");
10
+ }
11
+ function cachePath() {
12
+ return join(cacheDirectory(), "cdo-ai-sdlc-models.json");
13
+ }
14
+ function cacheKey(scope) {
15
+ const baseURL = scope.baseURL.trim().replace(/\/+$/, "").replace(/\/v1$/, "");
16
+ const gateway = createHash("sha256").update(baseURL).digest("hex").slice(0, 16);
17
+ const credential = createHash("sha256").update(scope.apiKey ?? "").digest("hex").slice(0, 16);
18
+ return `${scope.environment}:${gateway}:${credential}`;
19
+ }
20
+ async function readEntries() {
21
+ try {
22
+ const parsed = JSON.parse(await readFile(cachePath(), "utf8"));
23
+ if (!parsed.entries || typeof parsed.entries !== "object" || Array.isArray(parsed.entries)) return {};
24
+ return parsed.entries;
25
+ } catch {
26
+ return {};
27
+ }
28
+ }
29
+ function validModels(value) {
30
+ return Array.isArray(value) && value.length > 0 && value.every((model) => {
31
+ if (!model || typeof model !== "object" || Array.isArray(model)) return false;
32
+ const entry = model;
33
+ return typeof entry.id === "string" && entry.id.length > 0 && [
34
+ "contextTokens",
35
+ "inputTokens",
36
+ "outputTokens",
37
+ "inputCostPerToken",
38
+ "outputCostPerToken",
39
+ "cacheReadCostPerToken",
40
+ "cacheWriteCostPerToken"
41
+ ].every((key) => entry[key] === void 0 || typeof entry[key] === "number" && Number.isFinite(entry[key])) && [
42
+ "supportsTools",
43
+ "supportsReasoning",
44
+ "supportsVision",
45
+ "supportsPdf",
46
+ "supportsAudio"
47
+ ].every((key) => entry[key] === void 0 || typeof entry[key] === "boolean") && (entry.reasoningEfforts === void 0 || Array.isArray(entry.reasoningEfforts) && entry.reasoningEfforts.every((effort) => typeof effort === "string"));
48
+ });
49
+ }
50
+ async function readModelCache(scope) {
51
+ const models = (await readEntries())[cacheKey(scope)]?.models;
52
+ return validModels(models) ? models.map((model) => ({
53
+ ...model,
54
+ name: unsanitizeModelId(model.id)
55
+ })) : void 0;
56
+ }
57
+ async function acquireLock(lockPath) {
58
+ const deadline = Date.now() + 2e3;
59
+ while (Date.now() < deadline) try {
60
+ await mkdir(lockPath);
61
+ return () => rm(lockPath, {
62
+ recursive: true,
63
+ force: true
64
+ });
65
+ } catch {
66
+ try {
67
+ if (Date.now() - (await stat(lockPath)).mtimeMs > 3e4) {
68
+ await rm(lockPath, {
69
+ recursive: true,
70
+ force: true
71
+ });
72
+ continue;
73
+ }
74
+ } catch {
75
+ continue;
76
+ }
77
+ await new Promise((resolve) => setTimeout(resolve, 25));
78
+ }
79
+ }
80
+ const pendingWrites = /* @__PURE__ */ new Map();
81
+ async function writeModelCache(scope, models) {
82
+ if (!validModels(models)) return;
83
+ const path = cachePath();
84
+ const current = (pendingWrites.get(path) ?? Promise.resolve()).catch(() => {}).then(async () => {
85
+ let release;
86
+ try {
87
+ await mkdir(cacheDirectory(), {
88
+ recursive: true,
89
+ mode: 448
90
+ });
91
+ release = await acquireLock(`${path}.lock`);
92
+ if (!release) return;
93
+ const entries = await readEntries();
94
+ const cachedModels = models.map(({ name: _name, ...model }) => model);
95
+ entries[cacheKey(scope)] = {
96
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
97
+ models: cachedModels
98
+ };
99
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
100
+ await writeFile(temp, JSON.stringify({ entries }), {
101
+ encoding: "utf8",
102
+ mode: 384
103
+ });
104
+ await rename(temp, path);
105
+ } catch {} finally {
106
+ await release?.().catch(() => {});
107
+ }
108
+ });
109
+ pendingWrites.set(path, current);
110
+ await current;
111
+ if (pendingWrites.get(path) === current) pendingWrites.delete(path);
112
+ }
113
+ //#endregion
6
114
  //#region src/server/auth-hook.ts
7
115
  function createAuthHook(flavor) {
8
116
  return {
@@ -10,8 +118,9 @@ function createAuthHook(flavor) {
10
118
  async loader(auth) {
11
119
  const creds = await auth();
12
120
  if (!creds || creds.type !== "api") return {};
121
+ const url = creds.metadata?.["url"];
13
122
  return {
14
- baseURL: creds.metadata?.["url"],
123
+ baseURL: url ? normalizeProviderURL(url) : void 0,
15
124
  apiKey: creds.key
16
125
  };
17
126
  },
@@ -21,8 +130,9 @@ function createAuthHook(flavor) {
21
130
  prompts: [{
22
131
  type: "text",
23
132
  key: "url",
24
- message: `${flavor.displayName} proxy URL`,
25
- placeholder: "https://litellm.example.com/v1"
133
+ message: PLUGIN_COPY.proxyURLLabel(flavor.displayName),
134
+ placeholder: PLUGIN_COPY.proxyURLPlaceholder,
135
+ validate: (value) => value.trim() ? void 0 : PLUGIN_COPY.proxyURLRequired
26
136
  }]
27
137
  }]
28
138
  };
@@ -30,13 +140,8 @@ function createAuthHook(flavor) {
30
140
  //#endregion
31
141
  //#region src/server/constants.ts
32
142
  const PROVIDER_NPM = "@ai-sdk/openai-compatible";
33
- const FETCH_TIMEOUT_MS = 8e3;
34
- const CACHE_TTL_MS = 6048e5;
35
143
  //#endregion
36
144
  //#region src/server/credentials.ts
37
- function normalizeRoot(url) {
38
- return url.trim().replace(/\/+$/, "").replace(/\/v1$/, "");
39
- }
40
145
  function authFilePath() {
41
146
  const dataHome = process.env["XDG_DATA_HOME"] || join(homedir(), ".local", "share");
42
147
  return join(dataHome, "opencode", "auth.json");
@@ -66,109 +171,129 @@ async function resolveCredentials(flavor, options) {
66
171
  const baseURL = (typeof options.baseURL === "string" ? options.baseURL : void 0) || stored.baseURL;
67
172
  return {
68
173
  apiKey,
69
- baseURL: baseURL ? normalizeRoot(baseURL) : void 0
174
+ baseURL: baseURL ? normalizeProviderURL(baseURL) : void 0
70
175
  };
71
176
  }
72
177
  //#endregion
73
178
  //#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");
179
+ function scope(root, flavor, apiKey) {
180
+ return {
181
+ environment: flavor.environment,
182
+ baseURL: root,
183
+ apiKey
184
+ };
77
185
  }
78
- function cachePath(root) {
79
- const key = createHash("sha256").update(root).digest("hex").slice(0, 16);
80
- return join(cacheDir(), `models-${key}.json`);
186
+ function toCanonical(models) {
187
+ return Object.entries(models).map(([id, model]) => ({
188
+ id,
189
+ contextTokens: model.limit?.context,
190
+ outputTokens: model.limit?.output,
191
+ supportsTools: model.tool_call,
192
+ supportsReasoning: model.reasoning,
193
+ supportsVision: model.attachment,
194
+ reasoningEfforts: model.variants ? Object.keys(model.variants) : void 0,
195
+ inputCostPerToken: model.cost?.input == null ? void 0 : model.cost.input / 1e6,
196
+ outputCostPerToken: model.cost?.output == null ? void 0 : model.cost.output / 1e6,
197
+ cacheReadCostPerToken: model.cost?.cache_read == null ? void 0 : model.cost.cache_read / 1e6,
198
+ cacheWriteCostPerToken: model.cost?.cache_write == null ? void 0 : model.cost.cache_write / 1e6
199
+ }));
81
200
  }
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
- }
201
+ function fromCanonical(models) {
202
+ return Object.fromEntries(models.map((model) => {
203
+ const cost = model.inputCostPerToken != null || model.outputCostPerToken != null ? {
204
+ input: (model.inputCostPerToken ?? 0) * 1e6,
205
+ output: (model.outputCostPerToken ?? 0) * 1e6,
206
+ ...model.cacheReadCostPerToken == null ? {} : { cache_read: model.cacheReadCostPerToken * 1e6 },
207
+ ...model.cacheWriteCostPerToken == null ? {} : { cache_write: model.cacheWriteCostPerToken * 1e6 }
208
+ } : void 0;
209
+ const input = ["text"];
210
+ if (model.supportsVision) input.push("image");
211
+ if (model.supportsPdf) input.push("pdf");
212
+ if (model.supportsAudio) input.push("audio");
213
+ return [model.id, {
214
+ name: unsanitizeModelId(model.id),
215
+ limit: {
216
+ context: model.contextTokens ?? 128e3,
217
+ output: model.outputTokens ?? 16384
218
+ },
219
+ tool_call: model.supportsTools ?? true,
220
+ reasoning: model.supportsReasoning ?? false,
221
+ attachment: model.supportsVision ?? false,
222
+ ...model.reasoningEfforts?.length ? { variants: Object.fromEntries(model.reasoningEfforts.map((effort) => [effort, { reasoningEffort: effort }])) } : {},
223
+ ...cost ? { cost } : {},
224
+ modalities: {
225
+ input,
226
+ output: ["text"]
227
+ }
228
+ }];
229
+ }));
93
230
  }
94
- async function writeCache(root, models) {
231
+ async function readCache(root, flavor, apiKey) {
232
+ const models = await readModelCache(scope(root, flavor, apiKey));
233
+ return models?.length ? fromCanonical(models) : void 0;
234
+ }
235
+ async function writeCache(root, flavor, apiKey, models) {
236
+ await writeModelCache(scope(root, flavor, apiKey), toCanonical(models));
237
+ }
238
+ async function resolveCachedModels(root, flavor, apiKey, discover) {
95
239
  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");
240
+ const models = await discover();
241
+ if (Object.keys(models).length > 0) {
242
+ await writeCache(root, flavor, apiKey, models);
243
+ return models;
244
+ }
102
245
  } catch {}
246
+ return readCache(root, flavor, apiKey);
103
247
  }
104
248
  //#endregion
105
249
  //#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) {
250
+ function toModelEntry(model) {
251
+ const cost = model.inputCostPerToken != null || model.outputCostPerToken != null ? {
252
+ input: (model.inputCostPerToken ?? 0) * 1e6,
253
+ output: (model.outputCostPerToken ?? 0) * 1e6,
254
+ ...model.cacheReadCostPerToken == null ? {} : { cache_read: model.cacheReadCostPerToken * 1e6 },
255
+ ...model.cacheWriteCostPerToken == null ? {} : { cache_write: model.cacheWriteCostPerToken * 1e6 }
256
+ } : void 0;
257
+ const input = ["text"];
258
+ if (model.supportsVision) input.push("image");
259
+ if (model.supportsPdf) input.push("pdf");
260
+ if (model.supportsAudio) input.push("audio");
133
261
  return {
134
- name: id,
262
+ name: model.name ?? model.id,
135
263
  limit: {
136
- context: info?.max_input_tokens ?? info?.max_tokens ?? 128e3,
137
- output: info?.max_output_tokens ?? 16384
264
+ context: model.contextTokens ?? 128e3,
265
+ output: model.outputTokens ?? 16384
138
266
  },
139
- tool_call: info?.supports_function_calling ?? true,
140
- reasoning: info?.supports_reasoning ?? false,
141
- attachment: info?.supports_vision ?? false
267
+ tool_call: model.supportsTools ?? true,
268
+ reasoning: model.supportsReasoning ?? false,
269
+ attachment: model.supportsVision ?? false,
270
+ ...model.reasoningEfforts?.length ? { variants: Object.fromEntries(model.reasoningEfforts.map((effort) => [effort, { reasoningEffort: effort }])) } : {},
271
+ ...cost ? { cost } : {},
272
+ modalities: {
273
+ input,
274
+ output: ["text"]
275
+ }
142
276
  };
143
277
  }
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;
278
+ async function discoverModelsForV1(root, apiKey, logger) {
279
+ const result = await discoverModels({
280
+ baseURL: root,
281
+ apiKey,
282
+ logger
283
+ });
284
+ if (!result.ok) throw new Error(result.error.message);
285
+ return Object.fromEntries(result.value.map((model) => [model.id, toModelEntry(model)]));
156
286
  }
157
287
  //#endregion
158
288
  //#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
- }
289
+ async function resolveModels(root, apiKey, flavor, logger) {
290
+ return resolveCachedModels(root, flavor, apiKey, async () => {
291
+ const models = await discoverModelsForV1(root, apiKey, logger);
292
+ if (Object.keys(models).length === 0) throw new Error("No usable chat models");
293
+ return models;
294
+ });
170
295
  }
171
- function createConfigHook(flavor) {
296
+ function createConfigHook(flavor, logger) {
172
297
  return async function configHook(config) {
173
298
  try {
174
299
  config.provider ??= {};
@@ -179,9 +304,9 @@ function createConfigHook(flavor) {
179
304
  provider.models ??= { ["_"]: {} };
180
305
  const { baseURL, apiKey } = await resolveCredentials(flavor, provider.options);
181
306
  if (!baseURL || !apiKey) return;
182
- provider.options.baseURL ??= `${baseURL}/v1`;
307
+ provider.options.baseURL = baseURL;
183
308
  provider.options.apiKey ??= apiKey;
184
- const models = await resolveModels(baseURL, apiKey);
309
+ const models = await resolveModels(baseURL, apiKey, flavor, logger);
185
310
  if (!models || Object.keys(models).length === 0) return;
186
311
  provider.models = models;
187
312
  } catch {}
@@ -190,10 +315,20 @@ function createConfigHook(flavor) {
190
315
  //#endregion
191
316
  //#region src/server/plugin.ts
192
317
  function createServer(flavor) {
193
- return async (_ctx) => ({
194
- config: createConfigHook(flavor),
195
- auth: createAuthHook(flavor)
196
- });
318
+ return async (ctx) => {
319
+ const logger = async (event) => {
320
+ await ctx.client.app.log({ body: {
321
+ service: "ai-sdlc-opencode",
322
+ level: event.level,
323
+ message: event.message,
324
+ extra: { ...event }
325
+ } });
326
+ };
327
+ return {
328
+ config: createConfigHook(flavor, logger),
329
+ auth: createAuthHook(flavor)
330
+ };
331
+ };
197
332
  }
198
333
  //#endregion
199
334
  //#region src/entry-dev.ts
package/dist/tui.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ //#region src/tui-dev.d.ts
2
+ declare const _default: {
3
+ id: string;
4
+ tui: import("@opencode-ai/plugin/tui").TuiPlugin;
5
+ };
6
+ //#endregion
7
+ export { _default as default };
package/dist/tui.js CHANGED
@@ -1,76 +1,127 @@
1
- import { t as DEV } from "./flavor-CMZWvD--.js";
1
+ import { a as resolveUsagePercent, c as fetchJSON, d as PLUGIN_UI, l as isRecord, n as BUDGET_SOURCE_LABELS, r as monthlyUsageMetrics, s as emitGatewayLog, t as DEV, u as PLUGIN_COPY } from "./flavor-8QVaVpzl.js";
2
2
  import { For, createSignal } from "solid-js";
3
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");
4
+ import { Fragment, jsx, jsxs } from "@opentui/solid/jsx-runtime";
5
+ //#region ../common/dist/usage.js
6
+ function deriveUsageURL(baseURL) {
7
+ return `${baseURL.replace(/^(https?:\/\/)litellm(\.[^/]+).*/, "$1api$2").replace(/\/+$/, "").replace(/\/v1$/, "")}/v1/usage`;
8
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);
9
+ async function fetchUsage$1(options) {
10
+ const startedAt = Date.now();
11
+ const endpoint = deriveUsageURL(options.baseURL);
12
+ const response = await fetchJSON(endpoint, options, "usage.fetch");
13
+ let result;
14
+ if (!response.ok) result = response;
15
+ else if (!isRecord(response.value)) result = {
16
+ ok: false,
17
+ error: {
18
+ code: "invalid-response",
19
+ message: "Invalid /v1/usage response"
20
+ }
21
+ };
22
+ else {
23
+ const { spend, budget, type } = response.value;
24
+ result = typeof spend !== "number" || !Number.isFinite(spend) || !(budget === null || typeof budget === "number" && Number.isFinite(budget)) || typeof type !== "string" || ![
25
+ "key",
26
+ "user",
27
+ "team-member",
28
+ "team-member-default",
29
+ "none"
30
+ ].includes(type) ? {
31
+ ok: false,
32
+ error: {
33
+ code: "invalid-response",
34
+ message: "Invalid /v1/usage response fields"
35
+ }
36
+ } : {
37
+ ok: true,
38
+ value: {
39
+ spend,
40
+ budget,
41
+ type
42
+ }
43
+ };
23
44
  }
45
+ emitGatewayLog(options, {
46
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
47
+ operation: "usage.fetch",
48
+ phase: "operation",
49
+ endpoint,
50
+ ok: result.ok,
51
+ durationMs: Date.now() - startedAt,
52
+ ...!result.ok ? { errorCode: result.error.code } : {}
53
+ });
54
+ return result;
24
55
  }
25
- function round1(n) {
26
- return Math.round(n * 10) / 10;
56
+ //#endregion
57
+ //#region src/usage-client.ts
58
+ async function fetchUsage(baseURL, apiKey, timeoutMs, logger) {
59
+ const result = await fetchUsage$1({
60
+ baseURL,
61
+ apiKey,
62
+ timeoutMs,
63
+ logger
64
+ });
65
+ return result.ok ? result.value : result.error.message;
27
66
  }
28
67
  function toEntry(providerName, result) {
29
68
  if (typeof result === "string") return {
30
69
  providerName,
31
- error: result
70
+ error: result,
71
+ connected: true
32
72
  };
33
- const percent = result.budget != null ? round1(result.spend / result.budget * 100) : null;
73
+ const { percent, thresholdPercent } = resolveUsagePercent(result.spend, result.budget);
34
74
  return {
35
75
  providerName,
36
76
  ...result,
37
- percent
77
+ ...monthlyUsageMetrics(result.spend),
78
+ percent,
79
+ thresholdPercent,
80
+ connected: true
38
81
  };
39
82
  }
40
83
  //#endregion
41
84
  //#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
85
  function barColor(entry, theme) {
49
- const pct = entry.percent ?? 0;
50
- return pct >= 100 ? theme.error : pct >= 80 ? theme.warning : theme.success;
86
+ const pct = entry.thresholdPercent ?? 0;
87
+ return pct >= PLUGIN_UI.exceededPercent ? theme.error : pct >= PLUGIN_UI.warningPercent ? theme.warning : theme.success;
51
88
  }
52
89
  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)} / ∞`;
90
+ if (entry.error != null) return PLUGIN_COPY.usageUnavailable(entry.error);
91
+ return PLUGIN_COPY.usageValues(entry.spend ?? 0, entry.budget ?? null);
55
92
  }
56
93
  function UsagePanel(props) {
57
- return /* @__PURE__ */ jsx(For, {
94
+ return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(For, {
58
95
  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
- });
96
+ children: (entry) => /* @__PURE__ */ jsxs("box", {
97
+ flexDirection: "column",
98
+ children: [
99
+ /* @__PURE__ */ jsx("text", {
100
+ attributes: createTextAttributes({ bold: true }),
101
+ children: entry.providerName
102
+ }),
103
+ /* @__PURE__ */ jsx("text", {
104
+ fg: !entry.connected || entry.error != null ? props.theme.error : void 0,
105
+ children: !entry.connected ? PLUGIN_COPY.notConnected(props.connectCommand) : valuesLine(entry)
106
+ }),
107
+ entry.connected && entry.error == null && entry.percent != null && /* @__PURE__ */ jsx("text", {
108
+ fg: barColor(entry, props.theme),
109
+ children: `${PLUGIN_COPY.progressBar(entry.percent, PLUGIN_UI.barWidth)} ${PLUGIN_COPY.formatPercent(entry.percent)}`
110
+ }),
111
+ entry.connected && entry.error == null && entry.type != null && /* @__PURE__ */ jsx("text", {
112
+ fg: props.theme.textMuted,
113
+ children: BUDGET_SOURCE_LABELS[entry.type]
114
+ }),
115
+ entry.connected && entry.error == null && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("text", {
116
+ fg: props.theme.textMuted,
117
+ children: PLUGIN_COPY.perWorkday(entry.workdayAverage == null ? "—" : PLUGIN_COPY.formatMoney(entry.workdayAverage))
118
+ }), /* @__PURE__ */ jsx("text", {
119
+ fg: props.theme.textMuted,
120
+ children: PLUGIN_COPY.workdaysRemaining(entry.remainingWorkdays ?? 0)
121
+ })] })
122
+ ]
123
+ })
124
+ }) });
74
125
  }
75
126
  //#endregion
76
127
  //#region src/tui.tsx
@@ -79,16 +130,37 @@ const TIMEOUT_MS = 5e3;
79
130
  function createTui(flavor) {
80
131
  return (api) => {
81
132
  const [entries, setEntries] = createSignal([]);
133
+ const logger = async (event) => {
134
+ await api.client.app.log({
135
+ service: "ai-sdlc-opencode",
136
+ level: event.level,
137
+ message: event.message,
138
+ extra: { ...event }
139
+ });
140
+ };
82
141
  async function refresh() {
83
142
  try {
84
143
  const providers = api.state.provider.filter((p) => flavor.matchesPanelProvider(p.id));
144
+ if (providers.length === 0) {
145
+ setEntries([{
146
+ providerName: flavor.displayName,
147
+ connected: false
148
+ }]);
149
+ return;
150
+ }
85
151
  const results = await Promise.all(providers.map(async (p) => {
86
152
  const options = p.options ?? {};
87
153
  const baseURL = options["baseURL"];
88
154
  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));
155
+ if (!apiKey) return {
156
+ ...toEntry(p.name, ""),
157
+ connected: false
158
+ };
159
+ if (!baseURL) return {
160
+ ...toEntry(p.name, ""),
161
+ connected: false
162
+ };
163
+ return toEntry(p.name, await fetchUsage(baseURL, apiKey, TIMEOUT_MS, logger));
92
164
  }));
93
165
  setEntries(results);
94
166
  } catch {}
@@ -101,7 +173,8 @@ function createTui(flavor) {
101
173
  slots: { sidebar_content(ctx) {
102
174
  return /* @__PURE__ */ jsx(UsagePanel, {
103
175
  entries: entries(),
104
- theme: ctx.theme.current
176
+ theme: ctx.theme.current,
177
+ connectCommand: PLUGIN_COPY.connectCommand(flavor.providerID)
105
178
  });
106
179
  } }
107
180
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telefonica/ai-sdlc-opencode-plugin-v1-dev",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "OpenCode 1.x plugin (dev environment): CDO.AI provider registration, model discovery, and budget panel",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,13 +0,0 @@
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 };
@@ -1,9 +0,0 @@
1
- import * as _opencode_ai_plugin0 from "@opencode-ai/plugin";
2
-
3
- //#region src/entry-dev.d.ts
4
- declare const _default: {
5
- id: string;
6
- server: _opencode_ai_plugin0.Plugin;
7
- };
8
- //#endregion
9
- export { _default as default };
@@ -1,9 +0,0 @@
1
- import * as _opencode_ai_plugin_tui0 from "@opencode-ai/plugin/tui";
2
-
3
- //#region src/tui-dev.d.ts
4
- declare const _default: {
5
- id: string;
6
- tui: _opencode_ai_plugin_tui0.TuiPlugin;
7
- };
8
- //#endregion
9
- export { _default as default };