@slkiser/opencode-quota 1.2.0 → 1.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 (83) hide show
  1. package/README.md +171 -74
  2. package/dist/lib/api-key-resolver.d.ts +83 -0
  3. package/dist/lib/api-key-resolver.d.ts.map +1 -0
  4. package/dist/lib/api-key-resolver.js +113 -0
  5. package/dist/lib/api-key-resolver.js.map +1 -0
  6. package/dist/lib/chutes-config.d.ts +8 -7
  7. package/dist/lib/chutes-config.d.ts.map +1 -1
  8. package/dist/lib/chutes-config.js +32 -128
  9. package/dist/lib/chutes-config.js.map +1 -1
  10. package/dist/lib/chutes.d.ts.map +1 -1
  11. package/dist/lib/chutes.js +1 -17
  12. package/dist/lib/chutes.js.map +1 -1
  13. package/dist/lib/config.d.ts.map +1 -1
  14. package/dist/lib/config.js +1 -48
  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 +1 -24
  18. package/dist/lib/copilot.js.map +1 -1
  19. package/dist/lib/env-template.d.ts +25 -0
  20. package/dist/lib/env-template.d.ts.map +1 -0
  21. package/dist/lib/env-template.js +32 -0
  22. package/dist/lib/env-template.js.map +1 -0
  23. package/dist/lib/firmware-config.d.ts +1 -7
  24. package/dist/lib/firmware-config.d.ts.map +1 -1
  25. package/dist/lib/firmware-config.js +26 -148
  26. package/dist/lib/firmware-config.js.map +1 -1
  27. package/dist/lib/firmware.d.ts +22 -0
  28. package/dist/lib/firmware.d.ts.map +1 -1
  29. package/dist/lib/firmware.js +96 -23
  30. package/dist/lib/firmware.js.map +1 -1
  31. package/dist/lib/format-utils.d.ts +56 -0
  32. package/dist/lib/format-utils.d.ts.map +1 -0
  33. package/dist/lib/format-utils.js +101 -0
  34. package/dist/lib/format-utils.js.map +1 -0
  35. package/dist/lib/format.d.ts.map +1 -1
  36. package/dist/lib/format.js +2 -67
  37. package/dist/lib/format.js.map +1 -1
  38. package/dist/lib/google.d.ts.map +1 -1
  39. package/dist/lib/google.js +2 -24
  40. package/dist/lib/google.js.map +1 -1
  41. package/dist/lib/http.d.ts +14 -0
  42. package/dist/lib/http.d.ts.map +1 -0
  43. package/dist/lib/http.js +34 -0
  44. package/dist/lib/http.js.map +1 -0
  45. package/dist/lib/jsonc.d.ts +25 -0
  46. package/dist/lib/jsonc.d.ts.map +1 -0
  47. package/dist/lib/jsonc.js +73 -0
  48. package/dist/lib/jsonc.js.map +1 -0
  49. package/dist/lib/markdown-table.d.ts +7 -0
  50. package/dist/lib/markdown-table.d.ts.map +1 -1
  51. package/dist/lib/markdown-table.js +76 -9
  52. package/dist/lib/markdown-table.js.map +1 -1
  53. package/dist/lib/openai.d.ts.map +1 -1
  54. package/dist/lib/openai.js +1 -17
  55. package/dist/lib/openai.js.map +1 -1
  56. package/dist/lib/opencode-storage.d.ts +27 -0
  57. package/dist/lib/opencode-storage.d.ts.map +1 -1
  58. package/dist/lib/opencode-storage.js +67 -0
  59. package/dist/lib/opencode-storage.js.map +1 -1
  60. package/dist/lib/quota-command-format.d.ts.map +1 -1
  61. package/dist/lib/quota-command-format.js +5 -50
  62. package/dist/lib/quota-command-format.js.map +1 -1
  63. package/dist/lib/quota-stats-format.d.ts.map +1 -1
  64. package/dist/lib/quota-stats-format.js +9 -3
  65. package/dist/lib/quota-stats-format.js.map +1 -1
  66. package/dist/lib/quota-stats.d.ts +1 -0
  67. package/dist/lib/quota-stats.d.ts.map +1 -1
  68. package/dist/lib/quota-stats.js +15 -5
  69. package/dist/lib/quota-stats.js.map +1 -1
  70. package/dist/lib/quota-status.d.ts +7 -0
  71. package/dist/lib/quota-status.d.ts.map +1 -1
  72. package/dist/lib/quota-status.js +10 -0
  73. package/dist/lib/quota-status.js.map +1 -1
  74. package/dist/lib/toast-format-grouped.d.ts.map +1 -1
  75. package/dist/lib/toast-format-grouped.js +1 -66
  76. package/dist/lib/toast-format-grouped.js.map +1 -1
  77. package/dist/plugin.d.ts.map +1 -1
  78. package/dist/plugin.js +116 -162
  79. package/dist/plugin.js.map +1 -1
  80. package/dist/providers/firmware.d.ts.map +1 -1
  81. package/dist/providers/firmware.js +40 -7
  82. package/dist/providers/firmware.js.map +1 -1
  83. package/package.json +1 -1
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # opencode-quota
1
+ # Opencode Quota
2
2
 
3
3
  Quota and token tracking for OpenCode providers via Toasts and Commands with no LLM calls.
4
4
 
@@ -10,7 +10,7 @@ Quota and token tracking for OpenCode providers via Toasts and Commands with no
10
10
 
11
11
  **Token Report Commands** - Track token usage and estimated costs across sessions.
12
12
 
13
- ![Image of /quota and /quota_daily outputs](https://github.com/slkiser/opencode-quota/blob/main/quota.png)
13
+ ![Image of /quota and /tokens_daily outputs](https://github.com/slkiser/opencode-quota/blob/main/quota.png)
14
14
 
15
15
  ## Installation
16
16
 
@@ -22,9 +22,7 @@ Add to your `opencode.json`:
22
22
  }
23
23
  ```
24
24
 
25
- ## Quick Start
26
-
27
- Enable the providers you use:
25
+ Then enable the providers you use:
28
26
 
29
27
  ```jsonc
30
28
  {
@@ -36,54 +34,72 @@ Enable the providers you use:
36
34
  }
37
35
  ```
38
36
 
39
- That's it. Toasts appear automatically after main agent responses.
37
+ That's it. Toasts appear automatically after assistant responses.
38
+
39
+ > **LLM Agents:** Ask your agent to install `https://github.com/slkiser/opencode-quota` with the providers you have enabled. See [LLM Agent Installation Instructions](#llm-agent-installation-instructions) for details.
40
40
 
41
41
  ## Commands
42
42
 
43
- ### Token Reports
43
+ | Command | Description |
44
+ | ------------------------ | ------------------------------------------------ |
45
+ | `/quota` | Show quota toast (verbose) |
46
+ | `/quota_status` | Show diagnostics (config, providers, accounts) |
47
+ | | |
48
+ | `/tokens_today` | Tokens used today (calendar day) |
49
+ | `/tokens_daily` | Tokens used in last 24 hours |
50
+ | `/tokens_weekly` | Tokens used in last 7 days |
51
+ | `/tokens_monthly` | Tokens used in last 30 days |
52
+ | `/tokens_all` | Tokens used all time |
53
+ | `/tokens_session` | Tokens used in current session |
54
+ | `/tokens_between` | Tokens between two dates (YYYY-MM-DD) |
55
+ | | |
56
+ | `/firmware_reset_window` | Resets Firmware 5-hour window (requires confirm) |
44
57
 
45
- | Command | Title |
46
- | ----------------- | -------------------------------------------------------- |
47
- | `/tokens_today` | Tokens used (Today) (/tokens_today) |
48
- | `/tokens_daily` | Tokens used (Last 24 Hours) (/tokens_daily) |
49
- | `/tokens_weekly` | Tokens used (Last 7 Days) (/tokens_weekly) |
50
- | `/tokens_monthly` | Tokens used (Last 30 Days) (/tokens_monthly) |
51
- | `/tokens_all` | Tokens used (All Time) (/tokens_all) |
52
- | `/tokens_session` | Tokens used (Current Session) (/tokens_session) |
53
- | `/tokens_between` | Tokens used (YYYY-MM-DD .. YYYY-MM-DD) (/tokens_between) |
58
+ ## Supported Providers
54
59
 
55
- ### Quota Toast & Diagnostics
60
+ | Provider | Config ID | Auth Source |
61
+ | ------------------ | -------------------- | --------------------------------------------- |
62
+ | GitHub Copilot | `copilot` | OpenCode auth (automatic) |
63
+ | OpenAI (Plus/Pro) | `openai` | OpenCode auth (automatic) |
64
+ | Firmware AI | `firmware` | OpenCode auth or API key |
65
+ | Chutes AI | `chutes` | OpenCode auth or API key |
66
+ | Google Antigravity | `google-antigravity` | Multi-account via `opencode-antigravity-auth` |
56
67
 
57
- | Command | Title |
58
- | --------------- | --------------------------------- |
59
- | `/quota` | Quota Toast (Verbose) (/quota) |
60
- | `/quota_status` | Quota Diagnostics (/quota_status) |
68
+ ### Provider-Specific Setup
61
69
 
62
- ### Legacy Aliases
70
+ <details>
71
+ <summary><strong>GitHub Copilot</strong> (usually no setup needed)</summary>
63
72
 
64
- The following `/quota_*` commands remain as backwards-compatible aliases:
73
+ Copilot works automatically if OpenCode has Copilot configured and logged in.
65
74
 
66
- - `/quota_today` -> `/tokens_today`
67
- - `/quota_daily` -> `/tokens_daily`
68
- - `/quota_weekly` -> `/tokens_weekly`
69
- - `/quota_monthly` -> `/tokens_monthly`
70
- - `/quota_all` -> `/tokens_all`
71
- - `/quota_session` -> `/tokens_session`
72
- - `/quota_between` -> `/tokens_between`
75
+ **Optional:** For more reliable quota reporting, provide a fine-grained PAT:
73
76
 
74
- ## Supported Providers
77
+ 1. Create a fine-grained PAT at GitHub with **Account permissions > Plan > Read**
78
+ 2. Create `~/.config/opencode/copilot-quota-token.json`:
75
79
 
76
- | Provider | Config id | Notes |
77
- | ------------------ | -------------------- | --------------------------------------------- |
78
- | GitHub Copilot | `copilot` | Uses OpenCode auth\* |
79
- | OpenAI (Plus/Pro) | `openai` | Uses OpenCode auth |
80
- | Firmware AI | `firmware` | Uses OpenCode auth or API key |
81
- | Chutes AI | `chutes` | Uses OpenCode auth or API key |
82
- | Google Antigravity | `google-antigravity` | Multi-account via `opencode-antigravity-auth` |
80
+ ```json
81
+ {
82
+ "token": "github_pat_...",
83
+ "username": "your-username",
84
+ "tier": "pro"
85
+ }
86
+ ```
87
+
88
+ Tier options: `free`, `pro`, `pro+`, `business`, `enterprise`
89
+
90
+ </details>
91
+
92
+ <details>
93
+ <summary><strong>OpenAI</strong> (no setup needed)</summary>
94
+
95
+ OpenAI works automatically if OpenCode has OpenAI/ChatGPT configured.
96
+
97
+ </details>
83
98
 
84
- ### Firmware AI Setup
99
+ <details>
100
+ <summary><strong>Firmware AI</strong></summary>
85
101
 
86
- Firmware works automatically if OpenCode has Firmware configured. Alternatively, you can provide an API key in your `opencode.json`:
102
+ Works automatically if OpenCode has Firmware configured. Alternatively, provide an API key:
87
103
 
88
104
  ```jsonc
89
105
  {
@@ -102,11 +118,16 @@ Firmware works automatically if OpenCode has Firmware configured. Alternatively,
102
118
  }
103
119
  ```
104
120
 
105
- The `apiKey` field supports the `{env:VAR_NAME}` syntax to reference environment variables, or you can provide the key directly.
121
+ The `apiKey` field supports `{env:VAR_NAME}` syntax or a direct key.
106
122
 
107
- ### Chutes AI Setup
123
+ **Firmware-specific command:** Use `/firmware_reset_window confirm` to reset your 5-hour spending window (consumes 1 of 2 weekly resets). Running without `confirm` shows a warning first.
108
124
 
109
- Chutes works automatically if OpenCode has Chutes configured. Alternatively, you can provide an API key in your `opencode.json`:
125
+ </details>
126
+
127
+ <details>
128
+ <summary><strong>Chutes AI</strong></summary>
129
+
130
+ Works automatically if OpenCode has Chutes configured. Alternatively, provide an API key:
110
131
 
111
132
  ```jsonc
112
133
  {
@@ -125,64 +146,140 @@ Chutes works automatically if OpenCode has Chutes configured. Alternatively, you
125
146
  }
126
147
  ```
127
148
 
128
- ### GitHub Copilot Setup (optional)
149
+ </details>
129
150
 
130
- Copilot works with no extra setup as long as OpenCode already has Copilot configured and logged in.
151
+ <details>
152
+ <summary><strong>Google Antigravity</strong></summary>
131
153
 
132
- _Optional:_ if Copilot quota does not show up (or you want more reliable quota reporting), you can provide a fine-grained PAT so the plugin can use GitHub's public billing API:
133
-
134
- 1. Create a fine-grained PAT at GitHub with **Account permissions > Plan > Read**
135
- 2. Create `~/.config/opencode/copilot-quota-token.json`:
154
+ Requires the `opencode-antigravity-auth` plugin for multi-account support:
136
155
 
137
156
  ```json
138
157
  {
139
- "token": "github_pat_...",
140
- "username": "your-username",
141
- "tier": "pro"
158
+ "plugin": ["opencode-antigravity-auth", "@slkiser/opencode-quota"]
142
159
  }
143
160
  ```
144
161
 
145
- Tier options: `free`, `pro`, `pro+`, `business`, `enterprise`
162
+ Account credentials are stored in `~/.config/opencode/antigravity-accounts.json`.
146
163
 
147
- \* The plugin reads Copilot auth from OpenCode. The PAT file is only a fallback for reliability.
164
+ </details>
148
165
 
149
166
  ## Configuration Reference
150
167
 
151
168
  All options go under `experimental.quotaToast` in `opencode.json`:
152
169
 
153
- | Option | Default | Description |
154
- | ------------------- | ------------ | ------------------------------------------- |
155
- | `enabled` | `true` | Enable/disable plugin |
156
- | `enableToast` | `true` | Show popup toasts |
157
- | `enabledProviders` | `[]` | Provider ids to query |
158
- | `minIntervalMs` | `300000` | Min ms between fetches (5 min) |
159
- | `toastDurationMs` | `9000` | Toast display time |
160
- | `onlyCurrentModel` | `false` | Only show current model's quota |
161
- | `showSessionTokens` | `true` | Show per-model input/output tokens in toast |
162
- | `googleModels` | `["CLAUDE"]` | Google models: `CLAUDE`, `G3PRO`, `G3FLASH` |
163
- | `debug` | `false` | Show debug info in toasts |
170
+ | Option | Default | Description |
171
+ | ------------------- | ------------ | ----------------------------------------------- |
172
+ | `enabled` | `true` | Enable/disable plugin |
173
+ | `enableToast` | `true` | Show popup toasts |
174
+ | `enabledProviders` | `[]` | Provider IDs to query (see table above) |
175
+ | `minIntervalMs` | `300000` | Minimum ms between API fetches (default: 5 min) |
176
+ | `toastDurationMs` | `9000` | How long toasts display (ms) |
177
+ | `onlyCurrentModel` | `false` | Only show quota for the current model |
178
+ | `showSessionTokens` | `true` | Show per-model input/output tokens in toast |
179
+ | `googleModels` | `["CLAUDE"]` | Google models: `CLAUDE`, `G3PRO`, `G3FLASH` |
180
+ | `debug` | `false` | Show debug info in toasts |
181
+
182
+ ## Troubleshooting
183
+
184
+ Toast not appearing? Run `/quota_status` to check config and provider availability.
164
185
 
165
- ### Example Configuration
186
+ ---
166
187
 
167
- Here's a complete example `opencode.json` with all common options:
188
+ <details id="llm-agent-installation-instructions">
189
+ <summary><strong>LLM Agent Installation Instructions</strong></summary>
190
+
191
+ ### For LLM Agents: How to Install and Configure
192
+
193
+ This section provides instructions for LLM agents to install and configure `opencode-quota` based on the user's current OpenCode setup.
194
+
195
+ #### Step 1: Check Current Configuration
196
+
197
+ First, determine what providers the user has connected. You can:
198
+
199
+ 1. Read the user's `opencode.json` (typically at `~/.config/opencode/opencode.json`)
200
+ 2. Run `/connected` in OpenCode to see active providers
201
+ 3. Ask the user which providers they use
202
+
203
+ #### Step 2: Install the Plugin
204
+
205
+ Add the plugin to the user's `opencode.json`:
206
+
207
+ ```json
208
+ {
209
+ "plugin": ["@slkiser/opencode-quota"]
210
+ }
211
+ ```
212
+
213
+ If the user already has plugins, append to the existing array.
214
+
215
+ #### Step 3: Configure Providers
216
+
217
+ Based on the user's connected providers, add the appropriate `enabledProviders`:
168
218
 
169
219
  ```jsonc
170
220
  {
171
- "plugin": ["@slkiser/opencode-quota"],
172
221
  "experimental": {
173
222
  "quotaToast": {
174
- "enabledProviders": ["copilot", "openai"],
175
- "showSessionTokens": true,
176
- "minIntervalMs": 300000,
177
- "toastDurationMs": 9000,
223
+ "enabledProviders": [
224
+ // Add only the providers the user has configured:
225
+ // "copilot" - if using GitHub Copilot
226
+ // "openai" - if using OpenAI/ChatGPT
227
+ // "firmware" - if using Firmware AI
228
+ // "chutes" - if using Chutes AI
229
+ // "google-antigravity" - if using Google Antigravity (requires opencode-antigravity-auth)
230
+ ],
178
231
  },
179
232
  },
180
233
  }
181
234
  ```
182
235
 
183
- ## Troubleshooting
236
+ #### Provider Detection Guide
184
237
 
185
- Toast not appearing? Run `/quota_status` to check config and provider availability.
238
+ | If user's config has... | Add to enabledProviders |
239
+ | ------------------------------------------- | ----------------------- |
240
+ | `github-copilot` provider or Copilot models | `"copilot"` |
241
+ | `openai` / `chatgpt` provider | `"openai"` |
242
+ | `firmware` / `firmware-ai` provider | `"firmware"` |
243
+ | `chutes` provider | `"chutes"` |
244
+ | `google` provider with antigravity models | `"google-antigravity"` |
245
+ | `opencode-antigravity-auth` in plugins | `"google-antigravity"` |
246
+
247
+ #### Example: Full Configuration
248
+
249
+ For a user with Copilot and Google Antigravity:
250
+
251
+ ```jsonc
252
+ {
253
+ "plugin": [
254
+ "opencode-antigravity-auth", // Required for google-antigravity
255
+ "@slkiser/opencode-quota",
256
+ ],
257
+ "experimental": {
258
+ "quotaToast": {
259
+ "enabledProviders": ["copilot", "google-antigravity"],
260
+ "googleModels": ["CLAUDE"], // Options: CLAUDE, G3PRO, G3FLASH
261
+ "minIntervalMs": 300000, // 5 minutes between fetches
262
+ "toastDurationMs": 9000, // Toast shows for 9 seconds
263
+ },
264
+ },
265
+ }
266
+ ```
267
+
268
+ #### Step 4: Verify Installation
269
+
270
+ After configuration, instruct the user to:
271
+
272
+ 1. Restart OpenCode (or reload the window)
273
+ 2. Run `/quota_status` to verify providers are detected
274
+ 3. Run `/quota` to manually trigger a toast
275
+
276
+ #### Common Issues
277
+
278
+ - **Toast not showing**: Run `/quota_status` to diagnose
279
+ - **Google Antigravity not working**: Ensure `opencode-antigravity-auth` plugin is installed and accounts are configured
280
+ - **Copilot quota unreliable**: Consider setting up a fine-grained PAT (see Provider-Specific Setup above)
281
+
282
+ </details>
186
283
 
187
284
  ## License
188
285
 
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Generic API key resolution from env vars, config files, and auth.json.
3
+ *
4
+ * Used by provider-specific config modules (firmware-config, chutes-config)
5
+ * to resolve API keys with consistent priority and behavior.
6
+ */
7
+ /** A candidate config file path with its format */
8
+ export interface ConfigCandidate {
9
+ path: string;
10
+ isJsonc: boolean;
11
+ }
12
+ /**
13
+ * Get candidate paths for opencode.json/opencode.jsonc files.
14
+ *
15
+ * Order: local (cwd) first, then global (~/.config/opencode).
16
+ * Within each location, .jsonc takes precedence over .json.
17
+ */
18
+ export declare function getOpencodeConfigCandidatePaths(): ConfigCandidate[];
19
+ /**
20
+ * Read and parse an opencode config file.
21
+ *
22
+ * @returns Parsed config with metadata, or null if file doesn't exist or is invalid
23
+ */
24
+ export declare function readOpencodeConfig(filePath: string, isJsonc: boolean): Promise<{
25
+ config: unknown;
26
+ path: string;
27
+ isJsonc: boolean;
28
+ } | null>;
29
+ /** Result of API key resolution */
30
+ export interface ApiKeyResult<Source extends string> {
31
+ key: string;
32
+ source: Source;
33
+ }
34
+ /** Environment variable definition for key resolution */
35
+ export interface EnvVarDef<Source extends string> {
36
+ name: string;
37
+ source: Source;
38
+ }
39
+ /** Configuration for resolving an API key from multiple sources */
40
+ export interface ResolveApiKeyConfig<Source extends string> {
41
+ /** Environment variables to check (in order) */
42
+ envVars: EnvVarDef<Source>[];
43
+ /** Extract API key from parsed config object. Returns null if not found. */
44
+ extractFromConfig: (config: unknown) => string | null;
45
+ /** Source label for opencode.json */
46
+ configJsonSource: Source;
47
+ /** Source label for opencode.jsonc */
48
+ configJsoncSource: Source;
49
+ /** Extract API key from auth.json data. Returns null if not found. */
50
+ extractFromAuth: (auth: unknown) => string | null;
51
+ /** Source label for auth.json */
52
+ authSource: Source;
53
+ }
54
+ /**
55
+ * Resolve an API key from multiple sources with consistent priority.
56
+ *
57
+ * Priority (first wins):
58
+ * 1. Environment variables (in order specified)
59
+ * 2. opencode.json/opencode.jsonc (local first, then global)
60
+ * 3. auth.json
61
+ *
62
+ * @returns API key and source, or null if not found
63
+ */
64
+ export declare function resolveApiKey<Source extends string>(config: ResolveApiKeyConfig<Source>, readAuth: () => Promise<unknown | null>): Promise<ApiKeyResult<Source> | null>;
65
+ /** Configuration for API key diagnostics */
66
+ export interface DiagnosticsConfig<Source extends string> {
67
+ /** Environment variable names to check */
68
+ envVarNames: string[];
69
+ /** Resolver function to get the current key result */
70
+ resolve: () => Promise<ApiKeyResult<Source> | null>;
71
+ }
72
+ /**
73
+ * Get diagnostic info about API key configuration.
74
+ *
75
+ * Reports which sources were checked (env vars that exist, config files that exist)
76
+ * and whether a key was found.
77
+ */
78
+ export declare function getApiKeyDiagnostics<Source extends string>(config: DiagnosticsConfig<Source>): Promise<{
79
+ configured: boolean;
80
+ source: Source | null;
81
+ checkedPaths: string[];
82
+ }>;
83
+ //# sourceMappingURL=api-key-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-key-resolver.d.ts","sourceRoot":"","sources":["../../src/lib/api-key-resolver.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,+BAA+B,IAAI,eAAe,EAAE,CAUnE;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC,CASrE;AAED,mCAAmC;AACnC,MAAM,WAAW,YAAY,CAAC,MAAM,SAAS,MAAM;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,yDAAyD;AACzD,MAAM,WAAW,SAAS,CAAC,MAAM,SAAS,MAAM;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,mEAAmE;AACnE,MAAM,WAAW,mBAAmB,CAAC,MAAM,SAAS,MAAM;IACxD,gDAAgD;IAChD,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;IAE7B,4EAA4E;IAC5E,iBAAiB,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;IAEtD,qCAAqC;IACrC,gBAAgB,EAAE,MAAM,CAAC;IAEzB,sCAAsC;IACtC,iBAAiB,EAAE,MAAM,CAAC;IAE1B,sEAAsE;IACtE,eAAe,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;IAElD,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,MAAM,SAAS,MAAM,EACvD,MAAM,EAAE,mBAAmB,CAAC,MAAM,CAAC,EACnC,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,GACtC,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAgCtC;AAED,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB,CAAC,MAAM,SAAS,MAAM;IACtD,0CAA0C;IAC1C,WAAW,EAAE,MAAM,EAAE,CAAC;IAEtB,sDAAsD;IACtD,OAAO,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;CACrD;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,SAAS,MAAM,EAC9D,MAAM,EAAE,iBAAiB,CAAC,MAAM,CAAC,GAChC,OAAO,CAAC;IACT,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC,CAyBD"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Generic API key resolution from env vars, config files, and auth.json.
3
+ *
4
+ * Used by provider-specific config modules (firmware-config, chutes-config)
5
+ * to resolve API keys with consistent priority and behavior.
6
+ */
7
+ import { existsSync } from "fs";
8
+ import { readFile } from "fs/promises";
9
+ import { homedir } from "os";
10
+ import { join } from "path";
11
+ import { parseJsonOrJsonc } from "./jsonc.js";
12
+ /**
13
+ * Get candidate paths for opencode.json/opencode.jsonc files.
14
+ *
15
+ * Order: local (cwd) first, then global (~/.config/opencode).
16
+ * Within each location, .jsonc takes precedence over .json.
17
+ */
18
+ export function getOpencodeConfigCandidatePaths() {
19
+ const cwd = process.cwd();
20
+ const configBaseDir = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
21
+ return [
22
+ { path: join(cwd, "opencode.jsonc"), isJsonc: true },
23
+ { path: join(cwd, "opencode.json"), isJsonc: false },
24
+ { path: join(configBaseDir, "opencode", "opencode.jsonc"), isJsonc: true },
25
+ { path: join(configBaseDir, "opencode", "opencode.json"), isJsonc: false },
26
+ ];
27
+ }
28
+ /**
29
+ * Read and parse an opencode config file.
30
+ *
31
+ * @returns Parsed config with metadata, or null if file doesn't exist or is invalid
32
+ */
33
+ export async function readOpencodeConfig(filePath, isJsonc) {
34
+ try {
35
+ if (!existsSync(filePath))
36
+ return null;
37
+ const content = await readFile(filePath, "utf-8");
38
+ const config = parseJsonOrJsonc(content, isJsonc);
39
+ return { config, path: filePath, isJsonc };
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ /**
46
+ * Resolve an API key from multiple sources with consistent priority.
47
+ *
48
+ * Priority (first wins):
49
+ * 1. Environment variables (in order specified)
50
+ * 2. opencode.json/opencode.jsonc (local first, then global)
51
+ * 3. auth.json
52
+ *
53
+ * @returns API key and source, or null if not found
54
+ */
55
+ export async function resolveApiKey(config, readAuth) {
56
+ // 1. Check environment variables (highest priority)
57
+ for (const envVar of config.envVars) {
58
+ const value = process.env[envVar.name]?.trim();
59
+ if (value && value.length > 0) {
60
+ return { key: value, source: envVar.source };
61
+ }
62
+ }
63
+ // 2. Check opencode.json/opencode.jsonc files
64
+ const candidates = getOpencodeConfigCandidatePaths();
65
+ for (const candidate of candidates) {
66
+ const result = await readOpencodeConfig(candidate.path, candidate.isJsonc);
67
+ if (!result)
68
+ continue;
69
+ const key = config.extractFromConfig(result.config);
70
+ if (key) {
71
+ return {
72
+ key,
73
+ source: result.isJsonc ? config.configJsoncSource : config.configJsonSource,
74
+ };
75
+ }
76
+ }
77
+ // 3. Fallback to auth.json
78
+ const auth = await readAuth();
79
+ const key = config.extractFromAuth(auth);
80
+ if (key) {
81
+ return { key, source: config.authSource };
82
+ }
83
+ return null;
84
+ }
85
+ /**
86
+ * Get diagnostic info about API key configuration.
87
+ *
88
+ * Reports which sources were checked (env vars that exist, config files that exist)
89
+ * and whether a key was found.
90
+ */
91
+ export async function getApiKeyDiagnostics(config) {
92
+ const checkedPaths = [];
93
+ // Track env vars checked (only if they exist, even if empty)
94
+ for (const envVarName of config.envVarNames) {
95
+ if (process.env[envVarName] !== undefined) {
96
+ checkedPaths.push(`env:${envVarName}`);
97
+ }
98
+ }
99
+ // Track config files checked (only if they exist)
100
+ const candidates = getOpencodeConfigCandidatePaths();
101
+ for (const candidate of candidates) {
102
+ if (existsSync(candidate.path)) {
103
+ checkedPaths.push(candidate.path);
104
+ }
105
+ }
106
+ const result = await config.resolve();
107
+ return {
108
+ configured: result !== null,
109
+ source: result?.source ?? null,
110
+ checkedPaths,
111
+ };
112
+ }
113
+ //# sourceMappingURL=api-key-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-key-resolver.js","sourceRoot":"","sources":["../../src/lib/api-key-resolver.ts"],"names":[],"mappings":"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"}
@@ -14,15 +14,16 @@ export interface ChutesApiKeyResult {
14
14
  }
15
15
  /** Source of the resolved API key */
16
16
  export type ChutesKeySource = "env:CHUTES_API_KEY" | "opencode.json" | "opencode.jsonc" | "auth.json";
17
- /**
18
- * Get candidate paths for opencode.json/opencode.jsonc files
19
- */
20
- export declare function getOpencodeConfigCandidatePaths(): Array<{
21
- path: string;
22
- isJsonc: boolean;
23
- }>;
17
+ export { getOpencodeConfigCandidatePaths } from "./api-key-resolver.js";
24
18
  /**
25
19
  * Resolve Chutes API key from all available sources.
20
+ *
21
+ * Priority (first wins):
22
+ * 1. Environment variable: CHUTES_API_KEY
23
+ * 2. opencode.json/opencode.jsonc: provider.chutes.options.apiKey
24
+ * 3. auth.json: chutes.key
25
+ *
26
+ * @returns API key and source, or null if not found
26
27
  */
27
28
  export declare function resolveChutesApiKey(): Promise<ChutesApiKeyResult | null>;
28
29
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"chutes-config.d.ts","sourceRoot":"","sources":["../../src/lib/chutes-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,0CAA0C;AAC1C,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,eAAe,CAAC;CACzB;AAED,qCAAqC;AACrC,MAAM,MAAM,eAAe,GACvB,oBAAoB,GACpB,eAAe,GACf,gBAAgB,GAChB,WAAW,CAAC;AA8FhB;;GAEG;AACH,wBAAgB,+BAA+B,IAAI,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAU3F;AAmBD;;GAEG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA2B9E;AAED;;GAEG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC,CAGxD;AAED;;GAEG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC;IACvD,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC,CAqBD"}
1
+ {"version":3,"file":"chutes-config.d.ts","sourceRoot":"","sources":["../../src/lib/chutes-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAWH,0CAA0C;AAC1C,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,eAAe,CAAC;CACzB;AAED,qCAAqC;AACrC,MAAM,MAAM,eAAe,GACvB,oBAAoB,GACpB,eAAe,GACf,gBAAgB,GAChB,WAAW,CAAC;AAyChB,OAAO,EAAE,+BAA+B,EAAE,MAAM,uBAAuB,CAAC;AAExE;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAY9E;AAED;;GAEG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC,CAGxD;AAED;;GAEG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC;IACvD,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC,CAKD"}