@slkiser/opencode-quota 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +256 -561
  2. package/dist/lib/anthropic.js +1 -1
  3. package/dist/lib/anthropic.js.map +1 -1
  4. package/dist/lib/config-file-utils.d.ts +12 -0
  5. package/dist/lib/config-file-utils.d.ts.map +1 -1
  6. package/dist/lib/config-file-utils.js +23 -0
  7. package/dist/lib/config-file-utils.js.map +1 -1
  8. package/dist/lib/config.d.ts +16 -3
  9. package/dist/lib/config.d.ts.map +1 -1
  10. package/dist/lib/config.js +434 -216
  11. package/dist/lib/config.js.map +1 -1
  12. package/dist/lib/copilot.d.ts.map +1 -1
  13. package/dist/lib/copilot.js +3 -2
  14. package/dist/lib/copilot.js.map +1 -1
  15. package/dist/lib/entries.d.ts +1 -1
  16. package/dist/lib/entries.d.ts.map +1 -1
  17. package/dist/lib/format-utils.d.ts.map +1 -1
  18. package/dist/lib/format-utils.js +3 -2
  19. package/dist/lib/format-utils.js.map +1 -1
  20. package/dist/lib/format.d.ts.map +1 -1
  21. package/dist/lib/format.js +4 -2
  22. package/dist/lib/format.js.map +1 -1
  23. package/dist/lib/google-gemini-cli-companion.d.ts +29 -0
  24. package/dist/lib/google-gemini-cli-companion.d.ts.map +1 -0
  25. package/dist/lib/google-gemini-cli-companion.js +166 -0
  26. package/dist/lib/google-gemini-cli-companion.js.map +1 -0
  27. package/dist/lib/google-gemini-cli.d.ts +48 -0
  28. package/dist/lib/google-gemini-cli.d.ts.map +1 -0
  29. package/dist/lib/google-gemini-cli.js +404 -0
  30. package/dist/lib/google-gemini-cli.js.map +1 -0
  31. package/dist/lib/opencode-go.js +1 -1
  32. package/dist/lib/opencode-go.js.map +1 -1
  33. package/dist/lib/provider-metadata.d.ts +1 -1
  34. package/dist/lib/provider-metadata.d.ts.map +1 -1
  35. package/dist/lib/provider-metadata.js +19 -0
  36. package/dist/lib/provider-metadata.js.map +1 -1
  37. package/dist/lib/quota-render-data.d.ts +2 -0
  38. package/dist/lib/quota-render-data.d.ts.map +1 -1
  39. package/dist/lib/quota-render-data.js +2 -0
  40. package/dist/lib/quota-render-data.js.map +1 -1
  41. package/dist/lib/quota-runtime-context.d.ts +43 -0
  42. package/dist/lib/quota-runtime-context.d.ts.map +1 -0
  43. package/dist/lib/quota-runtime-context.js +61 -0
  44. package/dist/lib/quota-runtime-context.js.map +1 -0
  45. package/dist/lib/quota-status.d.ts +16 -0
  46. package/dist/lib/quota-status.d.ts.map +1 -1
  47. package/dist/lib/quota-status.js +63 -17
  48. package/dist/lib/quota-status.js.map +1 -1
  49. package/dist/lib/toast-format-grouped.d.ts.map +1 -1
  50. package/dist/lib/toast-format-grouped.js +5 -3
  51. package/dist/lib/toast-format-grouped.js.map +1 -1
  52. package/dist/lib/tui-config-diagnostics.d.ts +7 -2
  53. package/dist/lib/tui-config-diagnostics.d.ts.map +1 -1
  54. package/dist/lib/tui-config-diagnostics.js +27 -8
  55. package/dist/lib/tui-config-diagnostics.js.map +1 -1
  56. package/dist/lib/tui-runtime.d.ts.map +1 -1
  57. package/dist/lib/tui-runtime.js +24 -16
  58. package/dist/lib/tui-runtime.js.map +1 -1
  59. package/dist/lib/types.d.ts +37 -6
  60. package/dist/lib/types.d.ts.map +1 -1
  61. package/dist/lib/types.js.map +1 -1
  62. package/dist/plugin.d.ts.map +1 -1
  63. package/dist/plugin.js +419 -159
  64. package/dist/plugin.js.map +1 -1
  65. package/dist/providers/cursor.js +2 -2
  66. package/dist/providers/cursor.js.map +1 -1
  67. package/dist/providers/google-gemini-cli.d.ts +3 -0
  68. package/dist/providers/google-gemini-cli.d.ts.map +1 -0
  69. package/dist/providers/google-gemini-cli.js +83 -0
  70. package/dist/providers/google-gemini-cli.js.map +1 -0
  71. package/dist/providers/minimax-coding-plan.js +2 -2
  72. package/dist/providers/minimax-coding-plan.js.map +1 -1
  73. package/dist/providers/registry.d.ts.map +1 -1
  74. package/dist/providers/registry.js +2 -0
  75. package/dist/providers/registry.js.map +1 -1
  76. package/package.json +2 -1
@@ -3,25 +3,62 @@
3
3
  *
4
4
  * Precedence model:
5
5
  * - Global/user config provides defaults.
6
- * - Project/workspace config may override display-oriented settings for the current project.
7
- * - Global/user config remains authoritative for automatic/network-affecting settings.
8
- * - SDK config is used only as a fallback when no config files are found.
6
+ * - Workspace config at the resolved config root overrides ordinary settings.
7
+ * - SDK config is used only as a fallback when no file-backed config exists.
9
8
  */
10
9
  import { DEFAULT_CONFIG } from "./types.js";
11
10
  import { isQuotaFormatStyle, resolveQuotaFormatStyle } from "./quota-format-style.js";
12
11
  import { parseJsonOrJsonc } from "./jsonc.js";
13
- import { normalizeQuotaProviderId } from "./provider-metadata.js";
12
+ import { getQuotaProviderShape, normalizeQuotaProviderId } from "./provider-metadata.js";
14
13
  import { existsSync } from "fs";
15
14
  import { readFile } from "fs/promises";
16
15
  import { join } from "path";
17
16
  import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js";
17
+ export const QUOTA_TOAST_SETTING_SOURCE_KEYS = [
18
+ "enabled",
19
+ "enableToast",
20
+ "formatStyle",
21
+ "percentDisplayMode",
22
+ "minIntervalMs",
23
+ "debug",
24
+ "enabledProviders",
25
+ "anthropicBinaryPath",
26
+ "googleModels",
27
+ "alibabaCodingPlanTier",
28
+ "cursorPlan",
29
+ "cursorIncludedApiUsd",
30
+ "cursorBillingCycleStartDay",
31
+ "pricingSnapshot.source",
32
+ "pricingSnapshot.autoRefresh",
33
+ "showOnIdle",
34
+ "showOnQuestion",
35
+ "showOnCompact",
36
+ "showOnBothFail",
37
+ "toastDurationMs",
38
+ "onlyCurrentModel",
39
+ "showSessionTokens",
40
+ "layout.maxWidth",
41
+ "layout.narrowAt",
42
+ "layout.tinyAt",
43
+ ];
18
44
  export function createLoadConfigMeta() {
19
- return { source: "defaults", paths: [], networkSettingSources: {} };
45
+ return {
46
+ source: "defaults",
47
+ paths: [],
48
+ globalConfigPaths: [],
49
+ workspaceConfigPaths: [],
50
+ settingSources: {},
51
+ networkSettingSources: {},
52
+ configIssues: [],
53
+ };
20
54
  }
21
- const NETWORK_AFFECTING_KEYS = [
55
+ const CONFIG_FILENAMES = ["opencode.json", "opencode.jsonc"];
56
+ const NETWORK_SETTING_SOURCE_KEYS = [
22
57
  "enabled",
23
58
  "enabledProviders",
24
59
  "minIntervalMs",
60
+ "pricingSnapshot.source",
61
+ "pricingSnapshot.autoRefresh",
25
62
  "showOnIdle",
26
63
  "showOnQuestion",
27
64
  "showOnCompact",
@@ -40,8 +77,7 @@ function isValidGoogleModelId(id) {
40
77
  return typeof id === "string" && ["G3PRO", "G3FLASH", "CLAUDE", "G3IMAGE"].includes(id);
41
78
  }
42
79
  function isValidCursorQuotaPlan(plan) {
43
- return (typeof plan === "string" &&
44
- ["none", "pro", "pro-plus", "ultra"].includes(plan));
80
+ return (typeof plan === "string" && ["none", "pro", "pro-plus", "ultra"].includes(plan));
45
81
  }
46
82
  function isValidPricingSnapshotSource(source) {
47
83
  return typeof source === "string" && ["auto", "bundled", "runtime"].includes(source);
@@ -52,6 +88,15 @@ function isValidPricingSnapshotAutoRefresh(value) {
52
88
  function isValidPercentDisplayMode(value) {
53
89
  return value === "remaining" || value === "used";
54
90
  }
91
+ function isValidAlibabaCodingPlanTier(value) {
92
+ return value === "lite" || value === "pro";
93
+ }
94
+ function isPositiveNumber(value) {
95
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
96
+ }
97
+ function isValidCursorBillingCycleStartDay(value) {
98
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 28;
99
+ }
55
100
  function normalizeOptionalString(value) {
56
101
  if (typeof value !== "string") {
57
102
  return undefined;
@@ -78,54 +123,332 @@ function getConfiguredFormatStyle(quotaToastConfig) {
78
123
  function dedupe(list) {
79
124
  return [...new Set(list)];
80
125
  }
81
- const NETWORK_SETTING_SOURCE_KEYS = [
82
- "enabled",
83
- "enabledProviders",
84
- "minIntervalMs",
85
- "pricingSnapshot.source",
86
- "pricingSnapshot.autoRefresh",
87
- "showOnIdle",
88
- "showOnQuestion",
89
- "showOnCompact",
90
- "showOnBothFail",
91
- ];
92
- function resolveEffectiveNetworkSettingSources(params) {
93
- const resolved = {};
94
- for (const key of NETWORK_SETTING_SOURCE_KEYS) {
95
- if (typeof params.globalSources[key] === "string" && params.globalSources[key].length > 0) {
96
- resolved[key] = params.globalSources[key];
126
+ function cloneDefaultConfig() {
127
+ return {
128
+ ...DEFAULT_CONFIG,
129
+ enabledProviders: Array.isArray(DEFAULT_CONFIG.enabledProviders)
130
+ ? [...DEFAULT_CONFIG.enabledProviders]
131
+ : DEFAULT_CONFIG.enabledProviders,
132
+ googleModels: [...DEFAULT_CONFIG.googleModels],
133
+ pricingSnapshot: { ...DEFAULT_CONFIG.pricingSnapshot },
134
+ layout: { ...DEFAULT_CONFIG.layout },
135
+ };
136
+ }
137
+ function describeInvalidProviderValue(value) {
138
+ return typeof value === "string" ? value : typeof value;
139
+ }
140
+ function normalizeEnabledProviders(value) {
141
+ if (value === "auto") {
142
+ return { value: "auto", issues: [] };
143
+ }
144
+ if (!Array.isArray(value)) {
145
+ return {
146
+ value: [],
147
+ issues: ["expected \"auto\" or an array of provider ids"],
148
+ invalidEmpty: true,
149
+ };
150
+ }
151
+ if (value.length === 0) {
152
+ return { value: [], issues: [] };
153
+ }
154
+ const validProviders = [];
155
+ const invalidProviders = [];
156
+ for (const provider of value) {
157
+ if (typeof provider !== "string") {
158
+ invalidProviders.push(describeInvalidProviderValue(provider));
159
+ continue;
160
+ }
161
+ const normalized = normalizeQuotaProviderId(provider);
162
+ if (normalized && getQuotaProviderShape(normalized)) {
163
+ validProviders.push(normalized);
164
+ }
165
+ else {
166
+ invalidProviders.push(provider);
167
+ }
168
+ }
169
+ const issues = invalidProviders.length
170
+ ? [`unknown provider id(s): ${dedupe(invalidProviders).join(", ")}`]
171
+ : [];
172
+ const normalizedProviders = dedupe(validProviders);
173
+ return {
174
+ value: normalizedProviders,
175
+ issues,
176
+ invalidEmpty: normalizedProviders.length === 0 && invalidProviders.length > 0,
177
+ };
178
+ }
179
+ function normalizeGoogleModels(value) {
180
+ if (!Array.isArray(value)) {
181
+ return undefined;
182
+ }
183
+ const models = value.filter(isValidGoogleModelId);
184
+ return models.length > 0 ? models : undefined;
185
+ }
186
+ function extractPricingSnapshotPatch(value) {
187
+ if (!isPlainObject(value)) {
188
+ return undefined;
189
+ }
190
+ const patch = {};
191
+ if (hasOwnKey(value, "source") && isValidPricingSnapshotSource(value.source)) {
192
+ patch.source = value.source;
193
+ }
194
+ if (hasOwnKey(value, "autoRefresh") && isValidPricingSnapshotAutoRefresh(value.autoRefresh)) {
195
+ patch.autoRefresh = value.autoRefresh;
196
+ }
197
+ return Object.keys(patch).length > 0 ? patch : undefined;
198
+ }
199
+ function extractLayoutPatch(value) {
200
+ if (!isPlainObject(value)) {
201
+ return undefined;
202
+ }
203
+ const patch = {};
204
+ if (hasOwnKey(value, "maxWidth") && isPositiveNumber(value.maxWidth)) {
205
+ patch.maxWidth = value.maxWidth;
206
+ }
207
+ if (hasOwnKey(value, "narrowAt") && isPositiveNumber(value.narrowAt)) {
208
+ patch.narrowAt = value.narrowAt;
209
+ }
210
+ if (hasOwnKey(value, "tinyAt") && isPositiveNumber(value.tinyAt)) {
211
+ patch.tinyAt = value.tinyAt;
212
+ }
213
+ return Object.keys(patch).length > 0 ? patch : undefined;
214
+ }
215
+ function extractValidatedQuotaToastPatch(quotaToastConfig, reportIssue) {
216
+ const patch = {};
217
+ if (hasOwnKey(quotaToastConfig, "enabled") && typeof quotaToastConfig.enabled === "boolean") {
218
+ patch.enabled = quotaToastConfig.enabled;
219
+ }
220
+ if (hasOwnKey(quotaToastConfig, "enableToast") &&
221
+ typeof quotaToastConfig.enableToast === "boolean") {
222
+ patch.enableToast = quotaToastConfig.enableToast;
223
+ }
224
+ const formatStyle = getConfiguredFormatStyle(quotaToastConfig);
225
+ if (formatStyle) {
226
+ patch.formatStyle = formatStyle;
227
+ }
228
+ if (hasOwnKey(quotaToastConfig, "percentDisplayMode") &&
229
+ isValidPercentDisplayMode(quotaToastConfig.percentDisplayMode)) {
230
+ patch.percentDisplayMode = quotaToastConfig.percentDisplayMode;
231
+ }
232
+ if (hasOwnKey(quotaToastConfig, "minIntervalMs") && isPositiveNumber(quotaToastConfig.minIntervalMs)) {
233
+ patch.minIntervalMs = quotaToastConfig.minIntervalMs;
234
+ }
235
+ if (hasOwnKey(quotaToastConfig, "debug") && typeof quotaToastConfig.debug === "boolean") {
236
+ patch.debug = quotaToastConfig.debug;
237
+ }
238
+ if (hasOwnKey(quotaToastConfig, "enabledProviders")) {
239
+ const enabledProviders = normalizeEnabledProviders(quotaToastConfig.enabledProviders);
240
+ for (const issue of enabledProviders.issues) {
241
+ reportIssue?.("enabledProviders", issue);
242
+ }
243
+ if (enabledProviders.value !== undefined) {
244
+ patch.enabledProviders = enabledProviders.value;
245
+ if (enabledProviders.invalidEmpty) {
246
+ patch.enabledProvidersInvalidEmpty = true;
247
+ }
248
+ }
249
+ }
250
+ if (hasOwnKey(quotaToastConfig, "anthropicBinaryPath")) {
251
+ const anthropicBinaryPath = normalizeOptionalString(quotaToastConfig.anthropicBinaryPath);
252
+ if (anthropicBinaryPath !== undefined) {
253
+ patch.anthropicBinaryPath = anthropicBinaryPath;
97
254
  }
98
- else if (typeof params.localSources[key] === "string" && params.localSources[key].length > 0) {
99
- resolved[key] = params.localSources[key];
255
+ }
256
+ if (hasOwnKey(quotaToastConfig, "googleModels")) {
257
+ const googleModels = normalizeGoogleModels(quotaToastConfig.googleModels);
258
+ if (googleModels !== undefined) {
259
+ patch.googleModels = googleModels;
260
+ }
261
+ }
262
+ if (hasOwnKey(quotaToastConfig, "alibabaCodingPlanTier") &&
263
+ isValidAlibabaCodingPlanTier(quotaToastConfig.alibabaCodingPlanTier)) {
264
+ patch.alibabaCodingPlanTier = quotaToastConfig.alibabaCodingPlanTier;
265
+ }
266
+ if (hasOwnKey(quotaToastConfig, "cursorPlan") && isValidCursorQuotaPlan(quotaToastConfig.cursorPlan)) {
267
+ patch.cursorPlan = quotaToastConfig.cursorPlan;
268
+ }
269
+ if (hasOwnKey(quotaToastConfig, "cursorIncludedApiUsd") &&
270
+ isPositiveNumber(quotaToastConfig.cursorIncludedApiUsd)) {
271
+ patch.cursorIncludedApiUsd = quotaToastConfig.cursorIncludedApiUsd;
272
+ }
273
+ if (hasOwnKey(quotaToastConfig, "cursorBillingCycleStartDay") &&
274
+ isValidCursorBillingCycleStartDay(quotaToastConfig.cursorBillingCycleStartDay)) {
275
+ patch.cursorBillingCycleStartDay = quotaToastConfig.cursorBillingCycleStartDay;
276
+ }
277
+ if (hasOwnKey(quotaToastConfig, "pricingSnapshot")) {
278
+ const pricingSnapshot = extractPricingSnapshotPatch(quotaToastConfig.pricingSnapshot);
279
+ if (pricingSnapshot) {
280
+ patch.pricingSnapshot = pricingSnapshot;
281
+ }
282
+ }
283
+ if (hasOwnKey(quotaToastConfig, "showOnIdle") && typeof quotaToastConfig.showOnIdle === "boolean") {
284
+ patch.showOnIdle = quotaToastConfig.showOnIdle;
285
+ }
286
+ if (hasOwnKey(quotaToastConfig, "showOnQuestion") &&
287
+ typeof quotaToastConfig.showOnQuestion === "boolean") {
288
+ patch.showOnQuestion = quotaToastConfig.showOnQuestion;
289
+ }
290
+ if (hasOwnKey(quotaToastConfig, "showOnCompact") &&
291
+ typeof quotaToastConfig.showOnCompact === "boolean") {
292
+ patch.showOnCompact = quotaToastConfig.showOnCompact;
293
+ }
294
+ if (hasOwnKey(quotaToastConfig, "showOnBothFail") &&
295
+ typeof quotaToastConfig.showOnBothFail === "boolean") {
296
+ patch.showOnBothFail = quotaToastConfig.showOnBothFail;
297
+ }
298
+ if (hasOwnKey(quotaToastConfig, "toastDurationMs") &&
299
+ isPositiveNumber(quotaToastConfig.toastDurationMs)) {
300
+ patch.toastDurationMs = quotaToastConfig.toastDurationMs;
301
+ }
302
+ if (hasOwnKey(quotaToastConfig, "onlyCurrentModel") &&
303
+ typeof quotaToastConfig.onlyCurrentModel === "boolean") {
304
+ patch.onlyCurrentModel = quotaToastConfig.onlyCurrentModel;
305
+ }
306
+ if (hasOwnKey(quotaToastConfig, "showSessionTokens") &&
307
+ typeof quotaToastConfig.showSessionTokens === "boolean") {
308
+ patch.showSessionTokens = quotaToastConfig.showSessionTokens;
309
+ }
310
+ if (hasOwnKey(quotaToastConfig, "layout")) {
311
+ const layout = extractLayoutPatch(quotaToastConfig.layout);
312
+ if (layout) {
313
+ patch.layout = layout;
100
314
  }
101
315
  }
102
- return resolved;
316
+ return patch;
103
317
  }
104
- function recordNetworkSettingSource(sources, quotaToast, sourcePath) {
105
- for (const key of [
106
- "enabled",
107
- "enabledProviders",
108
- "minIntervalMs",
109
- "showOnIdle",
110
- "showOnQuestion",
111
- "showOnCompact",
112
- "showOnBothFail",
113
- ]) {
114
- if (key in quotaToast) {
115
- sources[key] = sourcePath;
318
+ function applySettingSource(settingSources, key, sourcePath) {
319
+ settingSources[key] = sourcePath;
320
+ }
321
+ function applyValidatedQuotaToastPatch(config, patch, sourcePath, settingSources) {
322
+ if (hasOwnKey(patch, "enabled")) {
323
+ config.enabled = patch.enabled;
324
+ applySettingSource(settingSources, "enabled", sourcePath);
325
+ }
326
+ if (hasOwnKey(patch, "enableToast")) {
327
+ config.enableToast = patch.enableToast;
328
+ applySettingSource(settingSources, "enableToast", sourcePath);
329
+ }
330
+ if (hasOwnKey(patch, "formatStyle")) {
331
+ config.formatStyle = patch.formatStyle;
332
+ applySettingSource(settingSources, "formatStyle", sourcePath);
333
+ }
334
+ if (hasOwnKey(patch, "percentDisplayMode")) {
335
+ config.percentDisplayMode = patch.percentDisplayMode;
336
+ applySettingSource(settingSources, "percentDisplayMode", sourcePath);
337
+ }
338
+ if (hasOwnKey(patch, "minIntervalMs")) {
339
+ config.minIntervalMs = patch.minIntervalMs;
340
+ applySettingSource(settingSources, "minIntervalMs", sourcePath);
341
+ }
342
+ if (hasOwnKey(patch, "debug")) {
343
+ config.debug = patch.debug;
344
+ applySettingSource(settingSources, "debug", sourcePath);
345
+ }
346
+ if (hasOwnKey(patch, "enabledProviders")) {
347
+ if (!(patch.enabledProvidersInvalidEmpty && settingSources.enabledProviders)) {
348
+ config.enabledProviders =
349
+ patch.enabledProviders === "auto" ? "auto" : [...patch.enabledProviders];
350
+ applySettingSource(settingSources, "enabledProviders", sourcePath);
351
+ }
352
+ }
353
+ if (hasOwnKey(patch, "anthropicBinaryPath")) {
354
+ config.anthropicBinaryPath = patch.anthropicBinaryPath;
355
+ applySettingSource(settingSources, "anthropicBinaryPath", sourcePath);
356
+ }
357
+ if (hasOwnKey(patch, "googleModels")) {
358
+ config.googleModels = [...patch.googleModels];
359
+ applySettingSource(settingSources, "googleModels", sourcePath);
360
+ }
361
+ if (hasOwnKey(patch, "alibabaCodingPlanTier")) {
362
+ config.alibabaCodingPlanTier = patch.alibabaCodingPlanTier;
363
+ applySettingSource(settingSources, "alibabaCodingPlanTier", sourcePath);
364
+ }
365
+ if (hasOwnKey(patch, "cursorPlan")) {
366
+ config.cursorPlan = patch.cursorPlan;
367
+ applySettingSource(settingSources, "cursorPlan", sourcePath);
368
+ }
369
+ if (hasOwnKey(patch, "cursorIncludedApiUsd")) {
370
+ config.cursorIncludedApiUsd = patch.cursorIncludedApiUsd;
371
+ applySettingSource(settingSources, "cursorIncludedApiUsd", sourcePath);
372
+ }
373
+ if (hasOwnKey(patch, "cursorBillingCycleStartDay")) {
374
+ config.cursorBillingCycleStartDay = patch.cursorBillingCycleStartDay;
375
+ applySettingSource(settingSources, "cursorBillingCycleStartDay", sourcePath);
376
+ }
377
+ if (patch.pricingSnapshot) {
378
+ if (hasOwnKey(patch.pricingSnapshot, "source")) {
379
+ config.pricingSnapshot.source = patch.pricingSnapshot.source;
380
+ applySettingSource(settingSources, "pricingSnapshot.source", sourcePath);
381
+ }
382
+ if (hasOwnKey(patch.pricingSnapshot, "autoRefresh")) {
383
+ config.pricingSnapshot.autoRefresh = patch.pricingSnapshot.autoRefresh;
384
+ applySettingSource(settingSources, "pricingSnapshot.autoRefresh", sourcePath);
116
385
  }
117
386
  }
118
- const pricingSnapshot = quotaToast.pricingSnapshot;
119
- if (pricingSnapshot && typeof pricingSnapshot === "object") {
120
- const pricingSnapshotRecord = pricingSnapshot;
121
- if ("source" in pricingSnapshotRecord) {
122
- sources["pricingSnapshot.source"] = sourcePath;
387
+ if (hasOwnKey(patch, "showOnIdle")) {
388
+ config.showOnIdle = patch.showOnIdle;
389
+ applySettingSource(settingSources, "showOnIdle", sourcePath);
390
+ }
391
+ if (hasOwnKey(patch, "showOnQuestion")) {
392
+ config.showOnQuestion = patch.showOnQuestion;
393
+ applySettingSource(settingSources, "showOnQuestion", sourcePath);
394
+ }
395
+ if (hasOwnKey(patch, "showOnCompact")) {
396
+ config.showOnCompact = patch.showOnCompact;
397
+ applySettingSource(settingSources, "showOnCompact", sourcePath);
398
+ }
399
+ if (hasOwnKey(patch, "showOnBothFail")) {
400
+ config.showOnBothFail = patch.showOnBothFail;
401
+ applySettingSource(settingSources, "showOnBothFail", sourcePath);
402
+ }
403
+ if (hasOwnKey(patch, "toastDurationMs")) {
404
+ config.toastDurationMs = patch.toastDurationMs;
405
+ applySettingSource(settingSources, "toastDurationMs", sourcePath);
406
+ }
407
+ if (hasOwnKey(patch, "onlyCurrentModel")) {
408
+ config.onlyCurrentModel = patch.onlyCurrentModel;
409
+ applySettingSource(settingSources, "onlyCurrentModel", sourcePath);
410
+ }
411
+ if (hasOwnKey(patch, "showSessionTokens")) {
412
+ config.showSessionTokens = patch.showSessionTokens;
413
+ applySettingSource(settingSources, "showSessionTokens", sourcePath);
414
+ }
415
+ if (patch.layout) {
416
+ if (hasOwnKey(patch.layout, "maxWidth")) {
417
+ config.layout.maxWidth = patch.layout.maxWidth;
418
+ applySettingSource(settingSources, "layout.maxWidth", sourcePath);
419
+ }
420
+ if (hasOwnKey(patch.layout, "narrowAt")) {
421
+ config.layout.narrowAt = patch.layout.narrowAt;
422
+ applySettingSource(settingSources, "layout.narrowAt", sourcePath);
123
423
  }
124
- if ("autoRefresh" in pricingSnapshotRecord) {
125
- sources["pricingSnapshot.autoRefresh"] = sourcePath;
424
+ if (hasOwnKey(patch.layout, "tinyAt")) {
425
+ config.layout.tinyAt = patch.layout.tinyAt;
426
+ applySettingSource(settingSources, "layout.tinyAt", sourcePath);
126
427
  }
127
428
  }
128
429
  }
430
+ function projectNetworkSettingSources(settingSources) {
431
+ const projected = {};
432
+ for (const key of NETWORK_SETTING_SOURCE_KEYS) {
433
+ const source = settingSources[key];
434
+ if (typeof source === "string" && source.length > 0) {
435
+ projected[key] = source;
436
+ }
437
+ }
438
+ return projected;
439
+ }
440
+ function buildConfigLayerCandidates(configDirs, configRootDir) {
441
+ const workspaceCandidates = CONFIG_FILENAMES.map((filename) => ({
442
+ path: join(configRootDir, filename),
443
+ scope: "workspace",
444
+ }));
445
+ const workspacePaths = new Set(workspaceCandidates.map((candidate) => candidate.path));
446
+ const globalCandidates = configDirs.flatMap((dir) => CONFIG_FILENAMES.map((filename) => ({ path: join(dir, filename), scope: "global" })));
447
+ return [
448
+ ...globalCandidates.filter((candidate) => !workspacePaths.has(candidate.path)),
449
+ ...workspaceCandidates,
450
+ ];
451
+ }
129
452
  /**
130
453
  * Load plugin configuration from OpenCode config
131
454
  *
@@ -133,106 +456,6 @@ function recordNetworkSettingSource(sources, quotaToast, sourcePath) {
133
456
  * @returns Merged configuration with defaults
134
457
  */
135
458
  export async function loadConfig(client, meta, options) {
136
- function normalize(quotaToastConfig) {
137
- if (!quotaToastConfig)
138
- return DEFAULT_CONFIG;
139
- const formatStyle = getConfiguredFormatStyle(quotaToastConfig) ?? DEFAULT_CONFIG.formatStyle;
140
- const config = {
141
- enabled: typeof quotaToastConfig.enabled === "boolean"
142
- ? quotaToastConfig.enabled
143
- : DEFAULT_CONFIG.enabled,
144
- enableToast: typeof quotaToastConfig.enableToast === "boolean"
145
- ? quotaToastConfig.enableToast
146
- : DEFAULT_CONFIG.enableToast,
147
- formatStyle,
148
- percentDisplayMode: isValidPercentDisplayMode(quotaToastConfig.percentDisplayMode)
149
- ? quotaToastConfig.percentDisplayMode
150
- : DEFAULT_CONFIG.percentDisplayMode,
151
- minIntervalMs: typeof quotaToastConfig.minIntervalMs === "number" && quotaToastConfig.minIntervalMs > 0
152
- ? quotaToastConfig.minIntervalMs
153
- : DEFAULT_CONFIG.minIntervalMs,
154
- debug: typeof quotaToastConfig.debug === "boolean" ? quotaToastConfig.debug : DEFAULT_CONFIG.debug,
155
- enabledProviders: quotaToastConfig.enabledProviders === "auto"
156
- ? "auto"
157
- : Array.isArray(quotaToastConfig.enabledProviders)
158
- ? dedupe(quotaToastConfig.enabledProviders
159
- .filter((p) => typeof p === "string")
160
- .map(normalizeQuotaProviderId)
161
- .filter(Boolean))
162
- : DEFAULT_CONFIG.enabledProviders,
163
- anthropicBinaryPath: normalizeOptionalString(quotaToastConfig.anthropicBinaryPath) ??
164
- DEFAULT_CONFIG.anthropicBinaryPath,
165
- googleModels: Array.isArray(quotaToastConfig.googleModels)
166
- ? quotaToastConfig.googleModels.filter(isValidGoogleModelId)
167
- : DEFAULT_CONFIG.googleModels,
168
- alibabaCodingPlanTier: quotaToastConfig.alibabaCodingPlanTier === "lite" ||
169
- quotaToastConfig.alibabaCodingPlanTier === "pro"
170
- ? quotaToastConfig.alibabaCodingPlanTier
171
- : DEFAULT_CONFIG.alibabaCodingPlanTier,
172
- cursorPlan: isValidCursorQuotaPlan(quotaToastConfig.cursorPlan)
173
- ? quotaToastConfig.cursorPlan
174
- : DEFAULT_CONFIG.cursorPlan,
175
- cursorIncludedApiUsd: typeof quotaToastConfig.cursorIncludedApiUsd === "number" &&
176
- Number.isFinite(quotaToastConfig.cursorIncludedApiUsd) &&
177
- quotaToastConfig.cursorIncludedApiUsd > 0
178
- ? quotaToastConfig.cursorIncludedApiUsd
179
- : undefined,
180
- cursorBillingCycleStartDay: typeof quotaToastConfig.cursorBillingCycleStartDay === "number" &&
181
- Number.isInteger(quotaToastConfig.cursorBillingCycleStartDay) &&
182
- quotaToastConfig.cursorBillingCycleStartDay >= 1 &&
183
- quotaToastConfig.cursorBillingCycleStartDay <= 28
184
- ? quotaToastConfig.cursorBillingCycleStartDay
185
- : undefined,
186
- pricingSnapshot: {
187
- source: isValidPricingSnapshotSource(quotaToastConfig.pricingSnapshot?.source)
188
- ? quotaToastConfig.pricingSnapshot.source
189
- : DEFAULT_CONFIG.pricingSnapshot.source,
190
- autoRefresh: isValidPricingSnapshotAutoRefresh(quotaToastConfig.pricingSnapshot?.autoRefresh)
191
- ? quotaToastConfig.pricingSnapshot.autoRefresh
192
- : DEFAULT_CONFIG.pricingSnapshot.autoRefresh,
193
- },
194
- showOnIdle: typeof quotaToastConfig.showOnIdle === "boolean"
195
- ? quotaToastConfig.showOnIdle
196
- : DEFAULT_CONFIG.showOnIdle,
197
- showOnQuestion: typeof quotaToastConfig.showOnQuestion === "boolean"
198
- ? quotaToastConfig.showOnQuestion
199
- : DEFAULT_CONFIG.showOnQuestion,
200
- showOnCompact: typeof quotaToastConfig.showOnCompact === "boolean"
201
- ? quotaToastConfig.showOnCompact
202
- : DEFAULT_CONFIG.showOnCompact,
203
- showOnBothFail: typeof quotaToastConfig.showOnBothFail === "boolean"
204
- ? quotaToastConfig.showOnBothFail
205
- : DEFAULT_CONFIG.showOnBothFail,
206
- toastDurationMs: typeof quotaToastConfig.toastDurationMs === "number" && quotaToastConfig.toastDurationMs > 0
207
- ? quotaToastConfig.toastDurationMs
208
- : DEFAULT_CONFIG.toastDurationMs,
209
- onlyCurrentModel: typeof quotaToastConfig.onlyCurrentModel === "boolean"
210
- ? quotaToastConfig.onlyCurrentModel
211
- : DEFAULT_CONFIG.onlyCurrentModel,
212
- showSessionTokens: typeof quotaToastConfig.showSessionTokens === "boolean"
213
- ? quotaToastConfig.showSessionTokens
214
- : DEFAULT_CONFIG.showSessionTokens,
215
- layout: {
216
- maxWidth: typeof quotaToastConfig.layout?.maxWidth === "number" &&
217
- quotaToastConfig.layout.maxWidth > 0
218
- ? quotaToastConfig.layout.maxWidth
219
- : DEFAULT_CONFIG.layout.maxWidth,
220
- narrowAt: typeof quotaToastConfig.layout?.narrowAt === "number" &&
221
- quotaToastConfig.layout.narrowAt > 0
222
- ? quotaToastConfig.layout.narrowAt
223
- : DEFAULT_CONFIG.layout.narrowAt,
224
- tinyAt: typeof quotaToastConfig.layout?.tinyAt === "number" && quotaToastConfig.layout.tinyAt > 0
225
- ? quotaToastConfig.layout.tinyAt
226
- : DEFAULT_CONFIG.layout.tinyAt,
227
- },
228
- };
229
- // enabledProviders: "auto" means auto-detect; explicit array means user-specified.
230
- // Ensure at least one Google model is configured
231
- if (config.googleModels.length === 0) {
232
- config.googleModels = DEFAULT_CONFIG.googleModels;
233
- }
234
- return config;
235
- }
236
459
  async function readJson(path) {
237
460
  try {
238
461
  const content = await readFile(path, "utf-8");
@@ -242,80 +465,58 @@ export async function loadConfig(client, meta, options) {
242
465
  return null;
243
466
  }
244
467
  }
245
- async function loadQuotaToastFromLocations(locations) {
246
- const quota = {};
247
- const usedPaths = [];
248
- const networkSettingSources = {};
249
- for (const dir of locations) {
250
- for (const filename of ["opencode.json", "opencode.jsonc"]) {
251
- const p = join(dir, filename);
252
- if (!existsSync(p))
253
- continue;
254
- const parsed = await readJson(p);
255
- if (!parsed || typeof parsed !== "object")
256
- continue;
257
- const root = parsed;
258
- const rawQuotaToast = root?.experimental?.quotaToast;
259
- if (!rawQuotaToast || typeof rawQuotaToast !== "object")
260
- continue;
261
- Object.assign(quota, rawQuotaToast);
262
- const sourcePath = `${p} (experimental.quotaToast)`;
263
- usedPaths.push(sourcePath);
264
- recordNetworkSettingSource(networkSettingSources, rawQuotaToast, sourcePath);
265
- }
266
- }
267
- return { quota, usedPaths, networkSettingSources };
268
- }
269
468
  async function loadFromFiles() {
270
- const cwd = options?.cwd ?? process.cwd();
469
+ const configRootDir = options?.configRootDir ?? options?.cwd ?? process.cwd();
271
470
  const { configDirs } = getOpencodeRuntimeDirCandidates();
272
- const globalConfig = await loadQuotaToastFromLocations(configDirs);
273
- const localConfig = await loadQuotaToastFromLocations([cwd]);
274
- const usedPaths = [...globalConfig.usedPaths, ...localConfig.usedPaths];
275
- const networkSettingSources = resolveEffectiveNetworkSettingSources({
276
- globalSources: globalConfig.networkSettingSources,
277
- localSources: localConfig.networkSettingSources,
278
- });
279
- if (usedPaths.length === 0) {
280
- return { config: null, usedPaths: [], networkSettingSources: {} };
281
- }
282
- const quota = {
283
- ...globalConfig.quota,
284
- ...localConfig.quota,
285
- };
286
- for (const key of NETWORK_AFFECTING_KEYS) {
287
- if (hasOwnKey(globalConfig.quota, key)) {
288
- quota[key] = globalConfig.quota[key];
471
+ const config = cloneDefaultConfig();
472
+ const usedPaths = [];
473
+ const globalConfigPaths = [];
474
+ const workspaceConfigPaths = [];
475
+ const settingSources = {};
476
+ const configIssues = [];
477
+ for (const candidate of buildConfigLayerCandidates(configDirs, configRootDir)) {
478
+ if (!existsSync(candidate.path)) {
479
+ continue;
289
480
  }
290
- else if (hasOwnKey(localConfig.quota, key)) {
291
- quota[key] = localConfig.quota[key];
481
+ const parsed = await readJson(candidate.path);
482
+ if (!isPlainObject(parsed) || !isPlainObject(parsed.experimental)) {
483
+ continue;
292
484
  }
485
+ const rawQuotaToast = parsed.experimental.quotaToast;
486
+ if (!isPlainObject(rawQuotaToast)) {
487
+ continue;
488
+ }
489
+ const sourcePath = `${candidate.path} (experimental.quotaToast)`;
490
+ usedPaths.push(sourcePath);
491
+ if (candidate.scope === "global") {
492
+ globalConfigPaths.push(sourcePath);
493
+ }
494
+ else {
495
+ workspaceConfigPaths.push(sourcePath);
496
+ }
497
+ applyValidatedQuotaToastPatch(config, extractValidatedQuotaToastPatch(rawQuotaToast, (key, message) => {
498
+ configIssues.push({ path: sourcePath, key, message });
499
+ }), sourcePath, settingSources);
293
500
  }
294
- const mergedPricingSnapshot = {};
295
- let hasMergedPricingSnapshot = false;
296
- if (isPlainObject(localConfig.quota.pricingSnapshot)) {
297
- Object.assign(mergedPricingSnapshot, localConfig.quota.pricingSnapshot);
298
- hasMergedPricingSnapshot = true;
299
- }
300
- if (isPlainObject(globalConfig.quota.pricingSnapshot)) {
301
- Object.assign(mergedPricingSnapshot, globalConfig.quota.pricingSnapshot);
302
- hasMergedPricingSnapshot = true;
303
- }
304
- if (hasMergedPricingSnapshot) {
305
- quota.pricingSnapshot = mergedPricingSnapshot;
306
- }
307
- const localFormatStyle = getConfiguredFormatStyle(localConfig.quota);
308
- const globalFormatStyle = getConfiguredFormatStyle(globalConfig.quota);
309
- if (localFormatStyle) {
310
- quota.formatStyle = localFormatStyle;
311
- }
312
- else if (globalFormatStyle) {
313
- quota.formatStyle = globalFormatStyle;
501
+ if (usedPaths.length === 0) {
502
+ return {
503
+ config: null,
504
+ usedPaths: [],
505
+ globalConfigPaths: [],
506
+ workspaceConfigPaths: [],
507
+ settingSources: {},
508
+ networkSettingSources: {},
509
+ configIssues: [],
510
+ };
314
511
  }
315
512
  return {
316
- config: normalize(quota),
513
+ config,
317
514
  usedPaths,
318
- networkSettingSources,
515
+ globalConfigPaths,
516
+ workspaceConfigPaths,
517
+ settingSources,
518
+ networkSettingSources: projectNetworkSettingSources(settingSources),
519
+ configIssues,
319
520
  };
320
521
  }
321
522
  const fileConfig = await loadFromFiles();
@@ -323,7 +524,11 @@ export async function loadConfig(client, meta, options) {
323
524
  if (meta) {
324
525
  meta.source = "files";
325
526
  meta.paths = fileConfig.usedPaths;
527
+ meta.globalConfigPaths = fileConfig.globalConfigPaths;
528
+ meta.workspaceConfigPaths = fileConfig.workspaceConfigPaths;
529
+ meta.settingSources = fileConfig.settingSources;
326
530
  meta.networkSettingSources = fileConfig.networkSettingSources;
531
+ meta.configIssues = fileConfig.configIssues;
327
532
  }
328
533
  return fileConfig.config;
329
534
  }
@@ -333,14 +538,23 @@ export async function loadConfig(client, meta, options) {
333
538
  // OpenCode config schema is strict; plugin-specific config must live under
334
539
  // experimental.* to avoid "unrecognized key" validation errors.
335
540
  const quotaToastConfig = response.data?.experimental?.quotaToast;
336
- if (quotaToastConfig && typeof quotaToastConfig === "object") {
541
+ if (isPlainObject(quotaToastConfig)) {
542
+ const config = cloneDefaultConfig();
543
+ const settingSources = {};
544
+ const configIssues = [];
545
+ applyValidatedQuotaToastPatch(config, extractValidatedQuotaToastPatch(quotaToastConfig, (key, message) => {
546
+ configIssues.push({ path: "client.config.get", key, message });
547
+ }), "client.config.get", settingSources);
337
548
  if (meta) {
338
549
  meta.source = "sdk";
339
550
  meta.paths = ["client.config.get"];
340
- meta.networkSettingSources = {};
341
- recordNetworkSettingSource(meta.networkSettingSources, quotaToastConfig, "client.config.get");
551
+ meta.globalConfigPaths = [];
552
+ meta.workspaceConfigPaths = [];
553
+ meta.settingSources = settingSources;
554
+ meta.networkSettingSources = projectNetworkSettingSources(settingSources);
555
+ meta.configIssues = configIssues;
342
556
  }
343
- return normalize(quotaToastConfig);
557
+ return config;
344
558
  }
345
559
  }
346
560
  catch {
@@ -350,7 +564,11 @@ export async function loadConfig(client, meta, options) {
350
564
  if (meta) {
351
565
  meta.source = "defaults";
352
566
  meta.paths = [];
567
+ meta.globalConfigPaths = [];
568
+ meta.workspaceConfigPaths = [];
569
+ meta.settingSources = {};
353
570
  meta.networkSettingSources = {};
571
+ meta.configIssues = [];
354
572
  }
355
573
  return DEFAULT_CONFIG;
356
574
  }