@slkiser/opencode-quota 3.1.4 → 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 (133) hide show
  1. package/README.md +256 -559
  2. package/dist/lib/anthropic.js +1 -1
  3. package/dist/lib/anthropic.js.map +1 -1
  4. package/dist/lib/cache.d.ts +17 -27
  5. package/dist/lib/cache.d.ts.map +1 -1
  6. package/dist/lib/cache.js +62 -65
  7. package/dist/lib/cache.js.map +1 -1
  8. package/dist/lib/config-file-utils.d.ts +12 -0
  9. package/dist/lib/config-file-utils.d.ts.map +1 -1
  10. package/dist/lib/config-file-utils.js +23 -0
  11. package/dist/lib/config-file-utils.js.map +1 -1
  12. package/dist/lib/config.d.ts +16 -3
  13. package/dist/lib/config.d.ts.map +1 -1
  14. package/dist/lib/config.js +448 -214
  15. package/dist/lib/config.js.map +1 -1
  16. package/dist/lib/copilot.d.ts.map +1 -1
  17. package/dist/lib/copilot.js +3 -2
  18. package/dist/lib/copilot.js.map +1 -1
  19. package/dist/lib/entries.d.ts +6 -2
  20. package/dist/lib/entries.d.ts.map +1 -1
  21. package/dist/lib/format-utils.d.ts.map +1 -1
  22. package/dist/lib/format-utils.js +3 -2
  23. package/dist/lib/format-utils.js.map +1 -1
  24. package/dist/lib/format.d.ts +2 -1
  25. package/dist/lib/format.d.ts.map +1 -1
  26. package/dist/lib/format.js +12 -6
  27. package/dist/lib/format.js.map +1 -1
  28. package/dist/lib/google-gemini-cli-companion.d.ts +29 -0
  29. package/dist/lib/google-gemini-cli-companion.d.ts.map +1 -0
  30. package/dist/lib/google-gemini-cli-companion.js +166 -0
  31. package/dist/lib/google-gemini-cli-companion.js.map +1 -0
  32. package/dist/lib/google-gemini-cli.d.ts +48 -0
  33. package/dist/lib/google-gemini-cli.d.ts.map +1 -0
  34. package/dist/lib/google-gemini-cli.js +404 -0
  35. package/dist/lib/google-gemini-cli.js.map +1 -0
  36. package/dist/lib/init-installer.d.ts +2 -1
  37. package/dist/lib/init-installer.d.ts.map +1 -1
  38. package/dist/lib/init-installer.js +10 -6
  39. package/dist/lib/init-installer.js.map +1 -1
  40. package/dist/lib/opencode-go.js +1 -1
  41. package/dist/lib/opencode-go.js.map +1 -1
  42. package/dist/lib/provider-metadata.d.ts +2 -1
  43. package/dist/lib/provider-metadata.d.ts.map +1 -1
  44. package/dist/lib/provider-metadata.js +27 -0
  45. package/dist/lib/provider-metadata.js.map +1 -1
  46. package/dist/lib/quota-format-style.d.ts +21 -0
  47. package/dist/lib/quota-format-style.d.ts.map +1 -0
  48. package/dist/lib/quota-format-style.js +38 -0
  49. package/dist/lib/quota-format-style.js.map +1 -0
  50. package/dist/lib/quota-render-data.d.ts +4 -12
  51. package/dist/lib/quota-render-data.d.ts.map +1 -1
  52. package/dist/lib/quota-render-data.js +83 -70
  53. package/dist/lib/quota-render-data.js.map +1 -1
  54. package/dist/lib/quota-runtime-context.d.ts +43 -0
  55. package/dist/lib/quota-runtime-context.d.ts.map +1 -0
  56. package/dist/lib/quota-runtime-context.js +61 -0
  57. package/dist/lib/quota-runtime-context.js.map +1 -0
  58. package/dist/lib/quota-state.d.ts +21 -0
  59. package/dist/lib/quota-state.d.ts.map +1 -0
  60. package/dist/lib/quota-state.js +228 -0
  61. package/dist/lib/quota-state.js.map +1 -0
  62. package/dist/lib/quota-status.d.ts +16 -0
  63. package/dist/lib/quota-status.d.ts.map +1 -1
  64. package/dist/lib/quota-status.js +63 -17
  65. package/dist/lib/quota-status.js.map +1 -1
  66. package/dist/lib/toast-format-grouped.d.ts.map +1 -1
  67. package/dist/lib/toast-format-grouped.js +5 -3
  68. package/dist/lib/toast-format-grouped.js.map +1 -1
  69. package/dist/lib/tui-config-diagnostics.d.ts +7 -2
  70. package/dist/lib/tui-config-diagnostics.d.ts.map +1 -1
  71. package/dist/lib/tui-config-diagnostics.js +27 -8
  72. package/dist/lib/tui-config-diagnostics.js.map +1 -1
  73. package/dist/lib/tui-runtime.d.ts +1 -2
  74. package/dist/lib/tui-runtime.d.ts.map +1 -1
  75. package/dist/lib/tui-runtime.js +24 -17
  76. package/dist/lib/tui-runtime.js.map +1 -1
  77. package/dist/lib/tui-sidebar-format.d.ts.map +1 -1
  78. package/dist/lib/tui-sidebar-format.js +2 -10
  79. package/dist/lib/tui-sidebar-format.js.map +1 -1
  80. package/dist/lib/types.d.ts +51 -9
  81. package/dist/lib/types.d.ts.map +1 -1
  82. package/dist/lib/types.js +2 -1
  83. package/dist/lib/types.js.map +1 -1
  84. package/dist/plugin.d.ts.map +1 -1
  85. package/dist/plugin.js +448 -242
  86. package/dist/plugin.js.map +1 -1
  87. package/dist/providers/alibaba-coding-plan.d.ts.map +1 -1
  88. package/dist/providers/alibaba-coding-plan.js +0 -16
  89. package/dist/providers/alibaba-coding-plan.js.map +1 -1
  90. package/dist/providers/anthropic.d.ts.map +1 -1
  91. package/dist/providers/anthropic.js +15 -29
  92. package/dist/providers/anthropic.js.map +1 -1
  93. package/dist/providers/copilot.d.ts.map +1 -1
  94. package/dist/providers/copilot.js +27 -53
  95. package/dist/providers/copilot.js.map +1 -1
  96. package/dist/providers/cursor.d.ts.map +1 -1
  97. package/dist/providers/cursor.js +38 -79
  98. package/dist/providers/cursor.js.map +1 -1
  99. package/dist/providers/google-gemini-cli.d.ts +3 -0
  100. package/dist/providers/google-gemini-cli.d.ts.map +1 -0
  101. package/dist/providers/google-gemini-cli.js +83 -0
  102. package/dist/providers/google-gemini-cli.js.map +1 -0
  103. package/dist/providers/kimi-code.d.ts.map +1 -1
  104. package/dist/providers/kimi-code.js +3 -15
  105. package/dist/providers/kimi-code.js.map +1 -1
  106. package/dist/providers/minimax-coding-plan.d.ts.map +1 -1
  107. package/dist/providers/minimax-coding-plan.js +3 -17
  108. package/dist/providers/minimax-coding-plan.js.map +1 -1
  109. package/dist/providers/nanogpt.d.ts.map +1 -1
  110. package/dist/providers/nanogpt.js +23 -42
  111. package/dist/providers/nanogpt.js.map +1 -1
  112. package/dist/providers/openai.d.ts.map +1 -1
  113. package/dist/providers/openai.js +6 -23
  114. package/dist/providers/openai.js.map +1 -1
  115. package/dist/providers/qwen-code.d.ts.map +1 -1
  116. package/dist/providers/qwen-code.js +7 -23
  117. package/dist/providers/qwen-code.js.map +1 -1
  118. package/dist/providers/registry.d.ts.map +1 -1
  119. package/dist/providers/registry.js +2 -0
  120. package/dist/providers/registry.js.map +1 -1
  121. package/dist/providers/result-helpers.d.ts +2 -2
  122. package/dist/providers/result-helpers.d.ts.map +1 -1
  123. package/dist/providers/result-helpers.js +7 -2
  124. package/dist/providers/result-helpers.js.map +1 -1
  125. package/dist/providers/synthetic.d.ts.map +1 -1
  126. package/dist/providers/synthetic.js +5 -14
  127. package/dist/providers/synthetic.js.map +1 -1
  128. package/dist/providers/zai.d.ts.map +1 -1
  129. package/dist/providers/zai.js +6 -28
  130. package/dist/providers/zai.js.map +1 -1
  131. package/dist/tui.d.ts.map +1 -1
  132. package/dist/tui.tsx +1 -15
  133. package/package.json +2 -1
@@ -3,24 +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";
10
+ import { isQuotaFormatStyle, resolveQuotaFormatStyle } from "./quota-format-style.js";
11
11
  import { parseJsonOrJsonc } from "./jsonc.js";
12
- import { normalizeQuotaProviderId } from "./provider-metadata.js";
12
+ import { getQuotaProviderShape, normalizeQuotaProviderId } from "./provider-metadata.js";
13
13
  import { existsSync } from "fs";
14
14
  import { readFile } from "fs/promises";
15
15
  import { join } from "path";
16
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
+ ];
17
44
  export function createLoadConfigMeta() {
18
- return { source: "defaults", paths: [], networkSettingSources: {} };
45
+ return {
46
+ source: "defaults",
47
+ paths: [],
48
+ globalConfigPaths: [],
49
+ workspaceConfigPaths: [],
50
+ settingSources: {},
51
+ networkSettingSources: {},
52
+ configIssues: [],
53
+ };
19
54
  }
20
- const NETWORK_AFFECTING_KEYS = [
55
+ const CONFIG_FILENAMES = ["opencode.json", "opencode.jsonc"];
56
+ const NETWORK_SETTING_SOURCE_KEYS = [
21
57
  "enabled",
22
58
  "enabledProviders",
23
59
  "minIntervalMs",
60
+ "pricingSnapshot.source",
61
+ "pricingSnapshot.autoRefresh",
24
62
  "showOnIdle",
25
63
  "showOnQuestion",
26
64
  "showOnCompact",
@@ -39,8 +77,7 @@ function isValidGoogleModelId(id) {
39
77
  return typeof id === "string" && ["G3PRO", "G3FLASH", "CLAUDE", "G3IMAGE"].includes(id);
40
78
  }
41
79
  function isValidCursorQuotaPlan(plan) {
42
- return (typeof plan === "string" &&
43
- ["none", "pro", "pro-plus", "ultra"].includes(plan));
80
+ return (typeof plan === "string" && ["none", "pro", "pro-plus", "ultra"].includes(plan));
44
81
  }
45
82
  function isValidPricingSnapshotSource(source) {
46
83
  return typeof source === "string" && ["auto", "bundled", "runtime"].includes(source);
@@ -48,12 +85,18 @@ function isValidPricingSnapshotSource(source) {
48
85
  function isValidPricingSnapshotAutoRefresh(value) {
49
86
  return typeof value === "number" && Number.isInteger(value) && value > 0;
50
87
  }
51
- function isValidFormatStyle(value) {
52
- return value === "classic" || value === "grouped";
53
- }
54
88
  function isValidPercentDisplayMode(value) {
55
89
  return value === "remaining" || value === "used";
56
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
+ }
57
100
  function normalizeOptionalString(value) {
58
101
  if (typeof value !== "string") {
59
102
  return undefined;
@@ -61,59 +104,350 @@ function normalizeOptionalString(value) {
61
104
  const trimmed = value.trim();
62
105
  return trimmed ? trimmed : undefined;
63
106
  }
107
+ function getConfiguredFormatStyle(quotaToastConfig) {
108
+ if (!quotaToastConfig) {
109
+ return undefined;
110
+ }
111
+ if (isQuotaFormatStyle(quotaToastConfig.formatStyle)) {
112
+ return resolveQuotaFormatStyle(quotaToastConfig.formatStyle);
113
+ }
114
+ const legacyFormatStyle = quotaToastConfig.toastStyle;
115
+ if (isQuotaFormatStyle(legacyFormatStyle)) {
116
+ return resolveQuotaFormatStyle(legacyFormatStyle);
117
+ }
118
+ return undefined;
119
+ }
64
120
  /**
65
121
  * Remove duplicates from an array while preserving order
66
122
  */
67
123
  function dedupe(list) {
68
124
  return [...new Set(list)];
69
125
  }
70
- const NETWORK_SETTING_SOURCE_KEYS = [
71
- "enabled",
72
- "enabledProviders",
73
- "minIntervalMs",
74
- "pricingSnapshot.source",
75
- "pricingSnapshot.autoRefresh",
76
- "showOnIdle",
77
- "showOnQuestion",
78
- "showOnCompact",
79
- "showOnBothFail",
80
- ];
81
- function resolveEffectiveNetworkSettingSources(params) {
82
- const resolved = {};
83
- for (const key of NETWORK_SETTING_SOURCE_KEYS) {
84
- if (typeof params.globalSources[key] === "string" && params.globalSources[key].length > 0) {
85
- 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);
86
242
  }
87
- else if (typeof params.localSources[key] === "string" && params.localSources[key].length > 0) {
88
- resolved[key] = params.localSources[key];
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;
254
+ }
255
+ }
256
+ if (hasOwnKey(quotaToastConfig, "googleModels")) {
257
+ const googleModels = normalizeGoogleModels(quotaToastConfig.googleModels);
258
+ if (googleModels !== undefined) {
259
+ patch.googleModels = googleModels;
89
260
  }
90
261
  }
91
- return resolved;
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;
314
+ }
315
+ }
316
+ return patch;
317
+ }
318
+ function applySettingSource(settingSources, key, sourcePath) {
319
+ settingSources[key] = sourcePath;
92
320
  }
93
- function recordNetworkSettingSource(sources, quotaToast, sourcePath) {
94
- for (const key of [
95
- "enabled",
96
- "enabledProviders",
97
- "minIntervalMs",
98
- "showOnIdle",
99
- "showOnQuestion",
100
- "showOnCompact",
101
- "showOnBothFail",
102
- ]) {
103
- if (key in quotaToast) {
104
- sources[key] = sourcePath;
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);
105
385
  }
106
386
  }
107
- const pricingSnapshot = quotaToast.pricingSnapshot;
108
- if (pricingSnapshot && typeof pricingSnapshot === "object") {
109
- const pricingSnapshotRecord = pricingSnapshot;
110
- if ("source" in pricingSnapshotRecord) {
111
- 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);
112
423
  }
113
- if ("autoRefresh" in pricingSnapshotRecord) {
114
- sources["pricingSnapshot.autoRefresh"] = sourcePath;
424
+ if (hasOwnKey(patch.layout, "tinyAt")) {
425
+ config.layout.tinyAt = patch.layout.tinyAt;
426
+ applySettingSource(settingSources, "layout.tinyAt", sourcePath);
427
+ }
428
+ }
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;
115
436
  }
116
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
+ ];
117
451
  }
118
452
  /**
119
453
  * Load plugin configuration from OpenCode config
@@ -122,109 +456,6 @@ function recordNetworkSettingSource(sources, quotaToast, sourcePath) {
122
456
  * @returns Merged configuration with defaults
123
457
  */
124
458
  export async function loadConfig(client, meta, options) {
125
- function normalize(quotaToastConfig) {
126
- if (!quotaToastConfig)
127
- return DEFAULT_CONFIG;
128
- const config = {
129
- enabled: typeof quotaToastConfig.enabled === "boolean"
130
- ? quotaToastConfig.enabled
131
- : DEFAULT_CONFIG.enabled,
132
- enableToast: typeof quotaToastConfig.enableToast === "boolean"
133
- ? quotaToastConfig.enableToast
134
- : DEFAULT_CONFIG.enableToast,
135
- formatStyle: isValidFormatStyle(quotaToastConfig.formatStyle)
136
- ? quotaToastConfig.formatStyle
137
- : isValidFormatStyle(quotaToastConfig.toastStyle)
138
- ? quotaToastConfig.toastStyle
139
- : DEFAULT_CONFIG.formatStyle,
140
- percentDisplayMode: isValidPercentDisplayMode(quotaToastConfig.percentDisplayMode)
141
- ? quotaToastConfig.percentDisplayMode
142
- : DEFAULT_CONFIG.percentDisplayMode,
143
- minIntervalMs: typeof quotaToastConfig.minIntervalMs === "number" && quotaToastConfig.minIntervalMs > 0
144
- ? quotaToastConfig.minIntervalMs
145
- : DEFAULT_CONFIG.minIntervalMs,
146
- debug: typeof quotaToastConfig.debug === "boolean" ? quotaToastConfig.debug : DEFAULT_CONFIG.debug,
147
- enabledProviders: quotaToastConfig.enabledProviders === "auto"
148
- ? "auto"
149
- : Array.isArray(quotaToastConfig.enabledProviders)
150
- ? dedupe(quotaToastConfig.enabledProviders
151
- .filter((p) => typeof p === "string")
152
- .map(normalizeQuotaProviderId)
153
- .filter(Boolean))
154
- : DEFAULT_CONFIG.enabledProviders,
155
- anthropicBinaryPath: normalizeOptionalString(quotaToastConfig.anthropicBinaryPath) ??
156
- DEFAULT_CONFIG.anthropicBinaryPath,
157
- googleModels: Array.isArray(quotaToastConfig.googleModels)
158
- ? quotaToastConfig.googleModels.filter(isValidGoogleModelId)
159
- : DEFAULT_CONFIG.googleModels,
160
- alibabaCodingPlanTier: quotaToastConfig.alibabaCodingPlanTier === "lite" ||
161
- quotaToastConfig.alibabaCodingPlanTier === "pro"
162
- ? quotaToastConfig.alibabaCodingPlanTier
163
- : DEFAULT_CONFIG.alibabaCodingPlanTier,
164
- cursorPlan: isValidCursorQuotaPlan(quotaToastConfig.cursorPlan)
165
- ? quotaToastConfig.cursorPlan
166
- : DEFAULT_CONFIG.cursorPlan,
167
- cursorIncludedApiUsd: typeof quotaToastConfig.cursorIncludedApiUsd === "number" &&
168
- Number.isFinite(quotaToastConfig.cursorIncludedApiUsd) &&
169
- quotaToastConfig.cursorIncludedApiUsd > 0
170
- ? quotaToastConfig.cursorIncludedApiUsd
171
- : undefined,
172
- cursorBillingCycleStartDay: typeof quotaToastConfig.cursorBillingCycleStartDay === "number" &&
173
- Number.isInteger(quotaToastConfig.cursorBillingCycleStartDay) &&
174
- quotaToastConfig.cursorBillingCycleStartDay >= 1 &&
175
- quotaToastConfig.cursorBillingCycleStartDay <= 28
176
- ? quotaToastConfig.cursorBillingCycleStartDay
177
- : undefined,
178
- pricingSnapshot: {
179
- source: isValidPricingSnapshotSource(quotaToastConfig.pricingSnapshot?.source)
180
- ? quotaToastConfig.pricingSnapshot.source
181
- : DEFAULT_CONFIG.pricingSnapshot.source,
182
- autoRefresh: isValidPricingSnapshotAutoRefresh(quotaToastConfig.pricingSnapshot?.autoRefresh)
183
- ? quotaToastConfig.pricingSnapshot.autoRefresh
184
- : DEFAULT_CONFIG.pricingSnapshot.autoRefresh,
185
- },
186
- showOnIdle: typeof quotaToastConfig.showOnIdle === "boolean"
187
- ? quotaToastConfig.showOnIdle
188
- : DEFAULT_CONFIG.showOnIdle,
189
- showOnQuestion: typeof quotaToastConfig.showOnQuestion === "boolean"
190
- ? quotaToastConfig.showOnQuestion
191
- : DEFAULT_CONFIG.showOnQuestion,
192
- showOnCompact: typeof quotaToastConfig.showOnCompact === "boolean"
193
- ? quotaToastConfig.showOnCompact
194
- : DEFAULT_CONFIG.showOnCompact,
195
- showOnBothFail: typeof quotaToastConfig.showOnBothFail === "boolean"
196
- ? quotaToastConfig.showOnBothFail
197
- : DEFAULT_CONFIG.showOnBothFail,
198
- toastDurationMs: typeof quotaToastConfig.toastDurationMs === "number" && quotaToastConfig.toastDurationMs > 0
199
- ? quotaToastConfig.toastDurationMs
200
- : DEFAULT_CONFIG.toastDurationMs,
201
- onlyCurrentModel: typeof quotaToastConfig.onlyCurrentModel === "boolean"
202
- ? quotaToastConfig.onlyCurrentModel
203
- : DEFAULT_CONFIG.onlyCurrentModel,
204
- showSessionTokens: typeof quotaToastConfig.showSessionTokens === "boolean"
205
- ? quotaToastConfig.showSessionTokens
206
- : DEFAULT_CONFIG.showSessionTokens,
207
- layout: {
208
- maxWidth: typeof quotaToastConfig.layout?.maxWidth === "number" &&
209
- quotaToastConfig.layout.maxWidth > 0
210
- ? quotaToastConfig.layout.maxWidth
211
- : DEFAULT_CONFIG.layout.maxWidth,
212
- narrowAt: typeof quotaToastConfig.layout?.narrowAt === "number" &&
213
- quotaToastConfig.layout.narrowAt > 0
214
- ? quotaToastConfig.layout.narrowAt
215
- : DEFAULT_CONFIG.layout.narrowAt,
216
- tinyAt: typeof quotaToastConfig.layout?.tinyAt === "number" && quotaToastConfig.layout.tinyAt > 0
217
- ? quotaToastConfig.layout.tinyAt
218
- : DEFAULT_CONFIG.layout.tinyAt,
219
- },
220
- };
221
- // enabledProviders: "auto" means auto-detect; explicit array means user-specified.
222
- // Ensure at least one Google model is configured
223
- if (config.googleModels.length === 0) {
224
- config.googleModels = DEFAULT_CONFIG.googleModels;
225
- }
226
- return config;
227
- }
228
459
  async function readJson(path) {
229
460
  try {
230
461
  const content = await readFile(path, "utf-8");
@@ -234,72 +465,58 @@ export async function loadConfig(client, meta, options) {
234
465
  return null;
235
466
  }
236
467
  }
237
- async function loadQuotaToastFromLocations(locations) {
238
- const quota = {};
239
- const usedPaths = [];
240
- const networkSettingSources = {};
241
- for (const dir of locations) {
242
- for (const filename of ["opencode.json", "opencode.jsonc"]) {
243
- const p = join(dir, filename);
244
- if (!existsSync(p))
245
- continue;
246
- const parsed = await readJson(p);
247
- if (!parsed || typeof parsed !== "object")
248
- continue;
249
- const root = parsed;
250
- const rawQuotaToast = root?.experimental?.quotaToast;
251
- if (!rawQuotaToast || typeof rawQuotaToast !== "object")
252
- continue;
253
- Object.assign(quota, rawQuotaToast);
254
- const sourcePath = `${p} (experimental.quotaToast)`;
255
- usedPaths.push(sourcePath);
256
- recordNetworkSettingSource(networkSettingSources, rawQuotaToast, sourcePath);
257
- }
258
- }
259
- return { quota, usedPaths, networkSettingSources };
260
- }
261
468
  async function loadFromFiles() {
262
- const cwd = options?.cwd ?? process.cwd();
469
+ const configRootDir = options?.configRootDir ?? options?.cwd ?? process.cwd();
263
470
  const { configDirs } = getOpencodeRuntimeDirCandidates();
264
- const globalConfig = await loadQuotaToastFromLocations(configDirs);
265
- const localConfig = await loadQuotaToastFromLocations([cwd]);
266
- const usedPaths = [...globalConfig.usedPaths, ...localConfig.usedPaths];
267
- const networkSettingSources = resolveEffectiveNetworkSettingSources({
268
- globalSources: globalConfig.networkSettingSources,
269
- localSources: localConfig.networkSettingSources,
270
- });
271
- if (usedPaths.length === 0) {
272
- return { config: null, usedPaths: [], networkSettingSources: {} };
273
- }
274
- const quota = {
275
- ...globalConfig.quota,
276
- ...localConfig.quota,
277
- };
278
- for (const key of NETWORK_AFFECTING_KEYS) {
279
- if (hasOwnKey(globalConfig.quota, key)) {
280
- 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;
281
480
  }
282
- else if (hasOwnKey(localConfig.quota, key)) {
283
- quota[key] = localConfig.quota[key];
481
+ const parsed = await readJson(candidate.path);
482
+ if (!isPlainObject(parsed) || !isPlainObject(parsed.experimental)) {
483
+ continue;
284
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);
285
500
  }
286
- const mergedPricingSnapshot = {};
287
- let hasMergedPricingSnapshot = false;
288
- if (isPlainObject(localConfig.quota.pricingSnapshot)) {
289
- Object.assign(mergedPricingSnapshot, localConfig.quota.pricingSnapshot);
290
- hasMergedPricingSnapshot = true;
291
- }
292
- if (isPlainObject(globalConfig.quota.pricingSnapshot)) {
293
- Object.assign(mergedPricingSnapshot, globalConfig.quota.pricingSnapshot);
294
- hasMergedPricingSnapshot = true;
295
- }
296
- if (hasMergedPricingSnapshot) {
297
- quota.pricingSnapshot = mergedPricingSnapshot;
501
+ if (usedPaths.length === 0) {
502
+ return {
503
+ config: null,
504
+ usedPaths: [],
505
+ globalConfigPaths: [],
506
+ workspaceConfigPaths: [],
507
+ settingSources: {},
508
+ networkSettingSources: {},
509
+ configIssues: [],
510
+ };
298
511
  }
299
512
  return {
300
- config: normalize(quota),
513
+ config,
301
514
  usedPaths,
302
- networkSettingSources,
515
+ globalConfigPaths,
516
+ workspaceConfigPaths,
517
+ settingSources,
518
+ networkSettingSources: projectNetworkSettingSources(settingSources),
519
+ configIssues,
303
520
  };
304
521
  }
305
522
  const fileConfig = await loadFromFiles();
@@ -307,7 +524,11 @@ export async function loadConfig(client, meta, options) {
307
524
  if (meta) {
308
525
  meta.source = "files";
309
526
  meta.paths = fileConfig.usedPaths;
527
+ meta.globalConfigPaths = fileConfig.globalConfigPaths;
528
+ meta.workspaceConfigPaths = fileConfig.workspaceConfigPaths;
529
+ meta.settingSources = fileConfig.settingSources;
310
530
  meta.networkSettingSources = fileConfig.networkSettingSources;
531
+ meta.configIssues = fileConfig.configIssues;
311
532
  }
312
533
  return fileConfig.config;
313
534
  }
@@ -317,14 +538,23 @@ export async function loadConfig(client, meta, options) {
317
538
  // OpenCode config schema is strict; plugin-specific config must live under
318
539
  // experimental.* to avoid "unrecognized key" validation errors.
319
540
  const quotaToastConfig = response.data?.experimental?.quotaToast;
320
- 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);
321
548
  if (meta) {
322
549
  meta.source = "sdk";
323
550
  meta.paths = ["client.config.get"];
324
- meta.networkSettingSources = {};
325
- 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;
326
556
  }
327
- return normalize(quotaToastConfig);
557
+ return config;
328
558
  }
329
559
  }
330
560
  catch {
@@ -334,7 +564,11 @@ export async function loadConfig(client, meta, options) {
334
564
  if (meta) {
335
565
  meta.source = "defaults";
336
566
  meta.paths = [];
567
+ meta.globalConfigPaths = [];
568
+ meta.workspaceConfigPaths = [];
569
+ meta.settingSources = {};
337
570
  meta.networkSettingSources = {};
571
+ meta.configIssues = [];
338
572
  }
339
573
  return DEFAULT_CONFIG;
340
574
  }