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