dsh-subscriptions 0.1.5 → 0.2.2

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 (110) hide show
  1. package/README.md +212 -154
  2. package/lib/auth/claude-code-creds.js +31 -9
  3. package/lib/auth/device-flow.js +16 -9
  4. package/lib/auth/oauth-flow.js +13 -10
  5. package/lib/auth/rpc.d.ts +73 -2
  6. package/lib/auth/rpc.js +124 -23
  7. package/lib/auth/store.js +80 -31
  8. package/lib/client/CursorCard.d.ts +1 -1
  9. package/lib/client/CursorCard.js +50 -16
  10. package/lib/client/ExternalUsageCards.d.ts +1 -1
  11. package/lib/client/ExternalUsageCards.js +37 -16
  12. package/lib/client/ImageGallery.d.ts +2 -2
  13. package/lib/client/ImageGallery.js +92 -33
  14. package/lib/client/ImageGenerateToolview.js +50 -14
  15. package/lib/client/OllamaCard.d.ts +13 -0
  16. package/lib/client/OllamaCard.js +94 -0
  17. package/lib/client/OllamaSettingsManager.d.ts +24 -0
  18. package/lib/client/OllamaSettingsManager.js +329 -0
  19. package/lib/client/ProviderAccountManager.d.ts +1 -1
  20. package/lib/client/ProviderAccountManager.js +164 -49
  21. package/lib/client/ProviderModelEditor.js +109 -26
  22. package/lib/client/SpeedSelect.d.ts +62 -14
  23. package/lib/client/SpeedSelect.js +257 -46
  24. package/lib/client/SubscriptionUsageBadge.d.ts +5 -11
  25. package/lib/client/SubscriptionUsageBadge.js +301 -112
  26. package/lib/client/SubscriptionsSection.d.ts +2 -0
  27. package/lib/client/SubscriptionsSection.js +259 -85
  28. package/lib/client/UsageBadgeDisplaySetting.js +35 -12
  29. package/lib/client/UsageMeter.d.ts +1 -1
  30. package/lib/client/UsageMeter.js +69 -27
  31. package/lib/client/VideoGenerateToolview.js +69 -17
  32. package/lib/client/account-preferences.d.ts +8 -0
  33. package/lib/client/account-preferences.js +20 -5
  34. package/lib/client/host-contract.d.ts +11 -2
  35. package/lib/client/host-contract.js +20 -3
  36. package/lib/client/index.d.ts +5 -5
  37. package/lib/client/index.js +34 -12
  38. package/lib/client/locales.d.ts +78 -0
  39. package/lib/client/locales.js +121 -34
  40. package/lib/client/ollama-draft.d.ts +229 -0
  41. package/lib/client/ollama-draft.js +255 -0
  42. package/lib/client/reset-action.js +6 -4
  43. package/lib/client/reset-credits-view.d.ts +1 -1
  44. package/lib/client/reset-credits-view.js +66 -17
  45. package/lib/client/subscription-card-styles.js +94 -24
  46. package/lib/client/usage-color-preferences.js +4 -2
  47. package/lib/client/usage-pace.d.ts +7 -0
  48. package/lib/client/usage-pace.js +43 -10
  49. package/lib/client.js +1647 -266
  50. package/lib/client.js.map +1 -1
  51. package/lib/http.js +1 -1
  52. package/lib/index.d.ts +28 -3
  53. package/lib/index.js +9329 -8217
  54. package/lib/model-defaults.js +4 -2
  55. package/lib/provider-settings.d.ts +14 -1
  56. package/lib/provider-settings.js +64 -20
  57. package/lib/providers/account-preferences.js +51 -22
  58. package/lib/providers/accounts.js +15 -9
  59. package/lib/providers/antigravity-oauth-client.js +2 -2
  60. package/lib/providers/antigravity.js +141 -68
  61. package/lib/providers/catalog-store.js +120 -41
  62. package/lib/providers/claude-cli-version.js +3 -3
  63. package/lib/providers/claude.d.ts +23 -3
  64. package/lib/providers/claude.js +167 -92
  65. package/lib/providers/codex-client-version.js +3 -3
  66. package/lib/providers/codex-search.js +40 -13
  67. package/lib/providers/codex.js +196 -112
  68. package/lib/providers/common.d.ts +14 -0
  69. package/lib/providers/common.js +118 -38
  70. package/lib/providers/copilot.js +143 -82
  71. package/lib/providers/cursor-adapter.js +42 -18
  72. package/lib/providers/cursor-auth.d.ts +1 -1
  73. package/lib/providers/cursor-auth.js +69 -24
  74. package/lib/providers/cursor-model-settings.js +14 -5
  75. package/lib/providers/cursor-usage.js +27 -13
  76. package/lib/providers/external-usage-controller.d.ts +9 -2
  77. package/lib/providers/external-usage-controller.js +30 -7
  78. package/lib/providers/external-usage.d.ts +8 -0
  79. package/lib/providers/external-usage.js +140 -20
  80. package/lib/providers/grok.js +152 -89
  81. package/lib/providers/image-pool.js +29 -11
  82. package/lib/providers/minimax-usage.js +31 -11
  83. package/lib/providers/npm-cli-version.js +10 -3
  84. package/lib/providers/ollama-settings.d.ts +90 -0
  85. package/lib/providers/ollama-settings.js +213 -0
  86. package/lib/providers/ollama-web.d.ts +46 -0
  87. package/lib/providers/ollama-web.js +231 -0
  88. package/lib/providers/ollama.d.ts +107 -0
  89. package/lib/providers/ollama.js +608 -0
  90. package/lib/providers/pool-family.js +11 -3
  91. package/lib/providers/pool-health.js +28 -8
  92. package/lib/providers/pool-usage.js +16 -9
  93. package/lib/providers/pool.d.ts +8 -0
  94. package/lib/providers/pool.js +81 -42
  95. package/lib/providers/rate-limit.js +8 -2
  96. package/lib/providers/replay.js +40 -17
  97. package/lib/providers/reset-redemption.js +18 -6
  98. package/lib/tools/image-generate.js +123 -65
  99. package/lib/tools/registration.js +5 -2
  100. package/lib/tools/video-generate.js +51 -31
  101. package/lib/tools/x-search.js +50 -22
  102. package/lib/translate/anthropic.js +85 -37
  103. package/lib/translate/antigravity-schema.js +71 -28
  104. package/lib/translate/antigravity-thinking.js +22 -9
  105. package/lib/translate/antigravity.js +141 -50
  106. package/lib/translate/chat-completions.js +67 -23
  107. package/lib/translate/resolved.js +55 -21
  108. package/lib/translate/responses.js +69 -29
  109. package/lib/translate/sse.js +4 -1
  110. package/package.json +26 -4
package/README.md CHANGED
@@ -11,27 +11,45 @@ reported quota in **Settings → Subscriptions**.
11
11
  ## What it adds
12
12
 
13
13
  - **Six subscription model sources:** Connect ChatGPT/Codex, Claude, Grok,
14
- GitHub Copilot, Google Antigravity, or Cursor and choose their models in
15
- DSH's picker. These routes use account sign-in, without provider API keys.
16
- - **Account and model control:** Refresh catalogs and choose visible models.
17
- For Codex, Claude, Grok, Copilot, and Antigravity, manage multiple accounts
18
- and same-provider pools. Pools can use available quota to select an account
19
- and fail over before a reply starts. See [account management](docs/account-management.md).
14
+ GitHub Copilot, Google Antigravity, or Cursor and choose their models in DSH's
15
+ picker. These routes use account sign-in, without provider API keys.
16
+ - **Ollama Cloud key route:** Add the `ollama-cloud` route with an
17
+ `OLLAMA_API_KEY` credential. The route ships no model catalog: configure it on
18
+ the Settings → Subscriptions card, which fetches the cloud list, ticks the
19
+ models to keep, and edits each one's display name, order, default reasoning
20
+ effort, and local context window. The picker shows exactly the ticked list, so
21
+ an unconfigured route offers no model. The card also edits the base URL, the
22
+ credential reference, and the web search/fetch switch, and persists them under
23
+ the DSH home directory. No OAuth sign-in. A hand-written `llm-pi-ai` route
24
+ with the same id collides with this one: delete the manual block when enabling
25
+ the plugin route. Stop sequences are unsupported on this route, consistent
26
+ with the other adapters. Web search/fetch stay off until the card's switch is
27
+ on (or a profile sets `ollama.web`), so untouched installs keep their existing
28
+ web provider; a profile still pins them to `ollama-cloud`. Not ported from the
29
+ standalone plugin: its transient-error reclassification, per-image request
30
+ quotas, and per-model usage counts. See the
31
+ [key-route note](.agents/notes/implemented/provider/2026-10-10-ollama-cloud-key-route.md).
32
+ - **Account and model control:** Refresh catalogs and choose visible models. For
33
+ Codex, Claude, Grok, Copilot, and Antigravity, manage multiple accounts and
34
+ same-provider pools. Pools can use available quota to select an account and
35
+ fail over before a reply starts. See
36
+ [account management](docs/account-management.md).
20
37
  - **Quota while you work:** See reported usage windows and reset times on
21
38
  provider cards and in an optional session-footer pill. The same view reads
22
- OpenCode Go and Kimi Code usage through keys already configured in
23
- **Settings → Models**. Their model routes remain built into DSH. GitHub
24
- Copilot has no usage endpoint.
25
- - **Provider tools:** Use Codex web search, Grok X search, ChatGPT or Grok
26
- image generation and editing, and Grok video generation when the matching
27
- provider is enabled.
39
+ OpenCode Go and Kimi Code usage through keys already configured in **Settings
40
+ → Models** (their model routes remain built into DSH), and Ollama Cloud usage
41
+ through its `OLLAMA_API_KEY` credential (its route ships in this plugin).
42
+ GitHub Copilot has no usage endpoint.
43
+ - **Provider tools:** Use Codex web search, Grok X search, ChatGPT or Grok image
44
+ generation and editing, and Grok video generation when the matching provider
45
+ is enabled.
28
46
 
29
47
  ## Screenshots
30
48
 
31
- Captured against DSH 0.2.0-rc.2 in the web UI with a local stub serving
32
- frozen provider responses, so each image shows a product state rather than
33
- a real account. Identities in the images are fabricated, and the quota
34
- percentages, catalogs, and reset countdowns are sample data.
49
+ Captured against DSH 0.2.0-rc.2 in the web UI with a local stub serving frozen
50
+ provider responses, so each image shows a product state rather than a real
51
+ account. Identities in the images are fabricated, and the quota percentages,
52
+ catalogs, and reset countdowns are sample data.
35
53
 
36
54
  ### Subscriptions
37
55
 
@@ -46,21 +64,23 @@ Logged-in providers join the session model picker with their live catalogs.
46
64
 
47
65
  ![Model picker with subscription models](docs/images/model-picker.png)
48
66
 
49
- Models that advertise reasoning levels get an **Effort** selector in the
50
- same menu. Levels and defaults come from the provider's live catalog;
51
- without discovery Codex falls back to a built-in list, and a configured
52
- default overrides either.
67
+ Models that advertise reasoning levels get an **Effort** selector in the same
68
+ menu. Levels and defaults come from the provider's live catalog; without
69
+ discovery Codex falls back to a built-in list, and a configured default
70
+ overrides either.
53
71
 
54
72
  ![Reasoning effort selector](docs/images/model-effort.png)
55
73
 
56
- Codex models whose catalog advertises the fast tier get a **Speed** toggle
57
- in the composer's tool row: Standard or Fast, per session.
74
+ Codex models whose catalog advertises the fast tier get a **Speed** toggle in
75
+ the composer's tool row: Standard or Fast, per session.
58
76
 
59
77
  ![Speed toggle with the Standard/Fast menu open](docs/images/speed-toggle.png)
60
78
 
61
- **Settings → Subscriptions → provider → Manage** edits account aliases,
62
- pool participation, model allowlists, and the per-model effort, context,
63
- and tool settings.
79
+ **Settings → Subscriptions → provider → Manage** edits account aliases, pool
80
+ participation, model allowlists, and the per-model effort, context, and tool
81
+ settings. A context value only lowers the window a session plans against, never
82
+ above what the provider advertises; Codex and Claude support it, and it does not
83
+ enlarge the API's capacity.
64
84
 
65
85
  ![Model settings](docs/images/model-settings.png)
66
86
 
@@ -68,85 +88,81 @@ and tool settings.
68
88
 
69
89
  ### Quota while you work
70
90
 
71
- The composer's stats row gains a **provider usage** pill showing the
72
- used percentage and reset window for the provider of the session's current
73
- model. It shows at most one provider; switching to a non-subscription model
74
- keeps the most recent subscription selected in the mounted conversation
75
- view, or stays hidden if there is none. **Always show** instead rotates
76
- through the providers that report usage every 10 seconds while the current
77
- model has no quota source, so the pill never disappears.
91
+ The composer's stats row gains a **provider usage** pill showing the used
92
+ percentage and reset window for the provider of the session's current model. It
93
+ shows at most one provider; switching to a non-subscription model keeps the most
94
+ recent subscription selected in the mounted conversation view, or stays hidden
95
+ if there is none. **Always show** instead rotates through the providers that
96
+ report usage every 10 seconds while the current model has no quota source, so
97
+ the pill never disappears.
78
98
 
79
99
  ![Provider usage pill in the stats row](docs/images/usage-pill.png)
80
100
 
81
- Click the pill to expand every provider and account that reports usage —
82
- the default account is starred, and the current provider is listed first.
83
- A provider with no usage endpoint, such as GitHub Copilot, has no row, and
84
- neither do accounts without a usage window. Antigravity previews only the
85
- current model's windows; the other model windows stay in a closed
86
- disclosure.
87
-
88
- Quota bars include a dark elapsed-time cursor whenever the provider's
89
- timing places the window in time, including at 0% used and for stale or old
90
- readings. Compare the colored fill (quota used) with the cursor (time
91
- elapsed); hover for the percentages and reset countdown. Standard coloring
92
- turns the fill orange when a fresh reading leads elapsed time by 10 points,
93
- and Relaxed uses 15; 90% used turns it red and takes priority. Windows
94
- without known timing omit the cursor, and a stale or old reading keeps its
95
- cursor without a pace warning. Choose Standard, Relaxed, or Remaining colors
96
- in **Settings → Subscriptions**.
101
+ Click the pill to expand every provider and account that reports usage — the
102
+ default account is starred, and the current provider is listed first. A provider
103
+ with no usage endpoint, such as GitHub Copilot, has no row, and neither do
104
+ accounts without a usage window. Antigravity previews only the current model's
105
+ windows; the other model windows stay in a closed disclosure.
106
+
107
+ Quota bars include a dark elapsed-time cursor whenever the provider's timing
108
+ places the window in time, including at 0% used and for stale or old readings.
109
+ Compare the colored fill (quota used) with the cursor (time elapsed); hover for
110
+ the percentages and reset countdown. Standard coloring turns the fill orange
111
+ when a fresh reading leads elapsed time by 10 points, and Relaxed uses 15; 90%
112
+ used turns it red and takes priority. Windows without known timing omit the
113
+ cursor, and a stale or old reading keeps its cursor without a pace warning.
114
+ Choose Standard, Relaxed, or Remaining colors in **Settings → Subscriptions**.
97
115
 
98
116
  ![Usage dialog with elapsed-time cursors](docs/images/usage-badge.png)
99
117
 
100
118
  #### Codex usage limit resets
101
119
 
102
- The Codex account card shows banked full-reset credits and their expirations
103
- in a closed disclosure. A failed optional lookup reports an error without
104
- hiding ordinary quota windows. **Use…** selects the earliest-expiring available
105
- credit and requires a fresh unscoped weekly reading of at least 80%, then an
106
- explicit confirmation and acknowledgment. Confirmation expires after 60
107
- seconds; the server rechecks usage and credit availability before submission.
120
+ The Codex account card shows banked full-reset credits and their expirations in
121
+ a closed disclosure. A failed optional lookup reports an error without hiding
122
+ ordinary quota windows. **Use…** selects the earliest-expiring available credit
123
+ and requires a fresh unscoped weekly reading of at least 80%, then an explicit
124
+ confirmation and acknowledgment. Confirmation expires after 60 seconds; the
125
+ server rechecks usage and credit availability before submission.
108
126
 
109
127
  One reset consumes one credit and resets weekly and 5-hour limits. This
110
- interface cannot undo it. An unconfirmed outcome blocks further redemption
111
- for that account in the current host process. Restarting clears that block,
112
- but must not be used to bypass it: check credits and limits in Codex before
113
- retrying, because the first submission may already have spent a credit.
114
- The private endpoint is covered by offline fakes, not live-account checks.
115
- See the [manual reset decision](.agents/notes/implemented/provider/2026-10-04-codex-manual-reset.md)
128
+ interface cannot undo it. An unconfirmed outcome blocks further redemption for
129
+ that account in the current host process. Restarting clears that block, but must
130
+ not be used to bypass it: check credits and limits in Codex before retrying,
131
+ because the first submission may already have spent a credit. The private
132
+ endpoint is covered by offline fakes, not live-account checks. See the
133
+ [manual reset decision](.agents/notes/implemented/provider/2026-10-04-codex-manual-reset.md)
116
134
  for the retry boundary and verification limits.
117
135
 
118
136
  #### Claude usage limit resets
119
137
 
120
- The Claude account card shows banked limit resets from Anthropic's
121
- `cedar_ember` program, in the same closed disclosure Codex uses. It lists one
122
- row per grant, not one per credit, and the summary counts remaining resets by
123
- summing `resetsLeft` over grants that are neither expired nor paused. Each row
124
- states whether that grant is available, not yet usable, paused, exhausted, or
125
- expired, with its remaining count and when it lapses. That time is the grant's
126
- expiry: it is not a window reset and does not move the elapsed-time cursor.
138
+ The Claude account card shows banked limit resets from Anthropic's `cedar_ember`
139
+ program, in the same closed disclosure Codex uses. It lists one row per grant,
140
+ not one per credit, and the summary counts remaining resets by summing
141
+ `resetsLeft` over grants that are neither expired nor paused. Each row states
142
+ whether that grant is available, not yet usable, paused, exhausted, or expired,
143
+ with its remaining count and when it lapses. That time is the grant's expiry: it
144
+ is not a window reset and does not move the elapsed-time cursor.
127
145
 
128
146
  This is **display only**. `claimable` is shown as data on a text row, and the
129
147
  page has no control that spends a Claude reset, because the response shape is
130
148
  not an Anthropic contract. An ineligible account, a grant-less block, or a
131
- rejected optional query leaves ordinary session and weekly windows untouched
132
- and shows no list; a failed lookup shows a fixed sentence rather than the
133
- provider's text. No live account was used to verify what Anthropic returns. See
149
+ rejected optional query leaves ordinary session and weekly windows untouched and
150
+ shows no list; a failed lookup shows a fixed sentence rather than the provider's
151
+ text. No live account was used to verify what Anthropic returns. See
134
152
  [the evidence and limits](docs/claude-banked-resets.md) and the
135
153
  [display decision](.agents/notes/implemented/provider/2026-10-08-claude-banked-reset-display.md).
136
154
 
137
155
  ## Install
138
156
 
139
- Current source is tested with DSH `0.1.7-rc.2`, `0.2.0-rc.1`, and
140
- `0.2.0-rc.2`. The package's peers admit `>=0.1.7-rc.2 <0.3.0-0`: untested
141
- versions inside that range install but are unsupported, and versions
142
- outside it need an explicit `dsh plugin allow-version` exemption.
143
- `dsh plugin add` checks those ranges with `includePrerelease` semantics, so
144
- the admitted set covers every host build from `0.1.7-rc.2` through the
145
- whole `0.2.x` line, prereleases included, and blocks the `0.3.0` line. See
146
- [docs/compatibility.md](docs/compatibility.md)
147
- for the support window and [docs/testing.md](docs/testing.md) for the test
148
- layers. With `dsh` available, install the plugin into the web profile from
149
- npm:
157
+ Current source is tested with DSH `0.1.7-rc.2`, `0.2.0-rc.2`, and `0.2.1-rc.2`.
158
+ The package's peers admit `>=0.1.7-rc.2 <0.3.0-0`: untested versions inside that
159
+ range install but are unsupported, and versions outside it need an explicit
160
+ `dsh plugin allow-version` exemption. `dsh plugin add` checks those ranges with
161
+ `includePrerelease` semantics, so the admitted set covers every host build from
162
+ `0.1.7-rc.2` through the whole `0.2.x` line, prereleases included, and blocks
163
+ the `0.3.0` line. See [docs/compatibility.md](docs/compatibility.md) for the
164
+ support window and [docs/testing.md](docs/testing.md) for the test layers. With
165
+ `dsh` available, install the plugin into the web profile from npm:
150
166
 
151
167
  ```sh
152
168
  dsh plugin --profile web add dsh-subscriptions
@@ -159,47 +175,47 @@ dsh plugin --profile web add github:goodboys-ai/dsh-subscriptions
159
175
  ```
160
176
 
161
177
  Git installs run this package's `prepare` script to build the host and browser
162
- bundles. If a Git install stops with `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`
163
- (DSH `0.1.7-rc.2` does this by default), add this package to the web
164
- profile's `pnpm-workspace.yaml` (`~/.dsh/profiles/web/` unless `DSH_HOME` is
165
- set) and repeat the command:
178
+ bundles. If a Git install stops with `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` (DSH
179
+ `0.1.7-rc.2` does this by default), add this package to the web profile's
180
+ `pnpm-workspace.yaml` (`~/.dsh/profiles/web/` unless `DSH_HOME` is set) and
181
+ repeat the command:
166
182
 
167
183
  ```yaml
168
184
  allowBuilds:
169
185
  'dsh-subscriptions': true
170
186
  ```
171
187
 
172
- (If a previous Git install used the old scoped name, replace its
173
- `allowBuilds` key with the one above.)
188
+ (If a previous Git install used the old scoped name, replace its `allowBuilds`
189
+ key with the one above.)
174
190
 
175
191
  Restart `dsh web` after installation. Open **Settings → Subscriptions** to
176
- connect providers, manage accounts and model lists, and inspect available
177
- quota. The session footer quota pill can be shown or hidden there for the
178
- current browser. OpenCode Go and Kimi Code cards show usage when their API
179
- keys are configured in **Settings → Models**.
192
+ connect providers, manage accounts and model lists, and inspect available quota.
193
+ The session footer quota pill can be shown or hidden there for the current
194
+ browser. OpenCode Go, Kimi Code, and Ollama Cloud cards show usage when their
195
+ API keys are configured.
180
196
 
181
- To update an installation, run the same `dsh plugin --profile web add` command again and restart `dsh web`.
197
+ To update an installation, run the same `dsh plugin --profile web add` command
198
+ again and restart `dsh web`.
182
199
 
183
- Without a version suffix, the npm command installs the `latest` dist-tag and
184
- the GitHub command installs the current default branch at install time. Pin
185
- a release by appending `@<version>` to the npm spec or the tag to the GitHub
186
- spec (`#vX.Y.Z`); see the
187
- [release policy](docs/compatibility.md#plugin-versioning).
200
+ Without a version suffix, the npm command installs the `latest` dist-tag and the
201
+ GitHub command installs the current default branch at install time. Pin a
202
+ release by appending `@<version>` to the npm spec or the tag to the GitHub spec
203
+ (`#vX.Y.Z`); see the [release policy](docs/compatibility.md#plugin-versioning).
188
204
 
189
205
  ## Verification and limits
190
206
 
191
- CI builds, tests, and boots a packed tarball on all supported DSH versions;
192
- it does not run the GitHub source install's `prepare` step. Offline
193
- provider tests check recorded API responses, so a passing CI run cannot prove
194
- that a provider still accepts live sign-in or model requests. See
195
- [testing](docs/testing.md) for the checks and their limits.
207
+ CI builds, tests, and boots a packed tarball on all supported DSH versions; it
208
+ does not run the GitHub source install's `prepare` step. Offline provider tests
209
+ check recorded API responses, so a passing CI run cannot prove that a provider
210
+ still accepts live sign-in or model requests. See [testing](docs/testing.md) for
211
+ the checks and their limits.
196
212
 
197
- - **ChatGPT/Codex, Claude, Grok, GitHub Copilot, and Google Antigravity:**
198
- The port includes the tool-message and icon fixes tracked in
213
+ - **ChatGPT/Codex, Claude, Grok, GitHub Copilot, and Google Antigravity:** The
214
+ port includes the tool-message and icon fixes tracked in
199
215
  [upstream issue #117](https://github.com/V1ki/dsh-plugin-subscriptions/issues/117).
200
- A manual check in an isolated `0.1.7-rc.2` web profile found the client
201
- bundle served and the auth and usage-status RPCs answering. Live sign-in
202
- and model requests on the supported DSH lines remain to be checked.
216
+ A manual check in an isolated `0.1.7-rc.2` web profile found the client bundle
217
+ served and the auth and usage-status RPCs answering. Live sign-in and model
218
+ requests on the supported DSH lines remain to be checked.
203
219
  - **Cursor:** Browser sign-in and live model discovery were verified in an
204
220
  isolated `0.1.7-rc.2` profile. A successful live model request remains to be
205
221
  verified. Its transport comes from
@@ -209,28 +225,36 @@ that a provider still accepts live sign-in or model requests. See
209
225
  - **OpenCode Go and Kimi Code:** Live usage responses were verified. Their API
210
226
  keys stay on the DSH host; the browser receives configured status and quota
211
227
  windows, not the keys.
228
+ - **Ollama Cloud:** Unit and fixture coverage only (chat wire, discovery mapping
229
+ and wire-id normalization, the settings store and its RPC endpoints, usage
230
+ parsing, web providers, host E2E fixtures). The card's rendering has no
231
+ browser test. Live chat, listing, usage, and web calls against `ollama.com`
232
+ remain to be verified with a real key.
212
233
 
213
- Cursor uses one connected account and a live model catalog. Its model
214
- visibility selection persists under the DSH home directory, and automatic
215
- display follows newly discovered models. It ships no fallback model list and
216
- does not import the local `cursor-agent` credential cache. Individual usage
217
- comes from Cursor dashboard endpoints; the documented Admin API covers team
218
- usage.
234
+ Cursor uses one connected account and a live model catalog. Its model visibility
235
+ selection persists under the DSH home directory, and automatic display follows
236
+ newly discovered models. It ships no fallback model list and does not import the
237
+ local `cursor-agent` credential cache. Individual usage comes from Cursor
238
+ dashboard endpoints; the documented Admin API covers team usage.
219
239
 
220
- The plugin follows DSH network settings. Its former proxy configuration and
221
- UI have been retired; an old plugin proxy config file is ignored.
240
+ The plugin follows DSH network settings. Its former proxy configuration and UI
241
+ have been retired; an old plugin proxy config file is ignored.
222
242
 
223
243
  ## Claude prompt-cache TTL
224
244
 
225
245
  Set `claudePromptCacheTtl: '5m'` (default) or `'1h'` in the plugin config and
226
- restart DSH. This affects Claude only; other providers keep their own caching.
227
- Five minutes omits the wire `ttl` field, preserving the existing request. One
228
- hour adds `ttl: "1h"` to the tools+system prefix and up to three conversation
229
- breakpoints. All marks share one TTL: Anthropic rejects a one-hour mark after
230
- a five-minute mark. Requests classified with `purpose` as compaction or
231
- session-title keep five minutes. Unclassified calls, including subagents and
232
- calls from hosts that do not supply `purpose`, follow the configured TTL.
233
- The TTL setting does not require a newer host capability or dependency.
246
+ restart DSH. The Claude **Manage** dialog can override this for the Claude
247
+ route: its "Prompt cache lifetime" picker offers Follow config, 5 minutes, or 1
248
+ hour, and a saved choice applies from the next request without a restart. Follow
249
+ config leaves the config value in charge. This affects Claude only; other
250
+ providers keep their own caching. Five minutes omits the wire `ttl` field,
251
+ preserving the existing request. One hour adds `ttl: "1h"` to the tools+system
252
+ prefix and up to three conversation breakpoints. All marks share one TTL:
253
+ Anthropic rejects a one-hour mark after a five-minute mark. Requests classified
254
+ with `purpose` as compaction or session-title keep five minutes. Unclassified
255
+ calls, including subagents and calls from hosts that do not supply `purpose`,
256
+ follow the configured TTL. The TTL setting does not require a newer host
257
+ capability or dependency.
234
258
 
235
259
  Cache reads have the same API price for either TTL. One-hour writes are priced
236
260
  at 2× base input instead of 1.25×. They may pay off with reusable prefixes and
@@ -251,35 +275,58 @@ live-account verification of this fork.
251
275
 
252
276
  **Settings → Subscriptions → Usage coloring** offers one browser-local dropdown:
253
277
 
254
- - **Standard** (default): yellow when used quota leads elapsed time by at least 10 percentage points.
278
+ - **Standard** (default): yellow when used quota leads elapsed time by at least
279
+ 10 percentage points.
255
280
  - **Relaxed**: yellow at a lead of at least 15 points.
256
281
  - **Remaining quota only**: no yellow pace warnings.
257
282
 
258
- Every preset shows reported usage of 90% or more in red, before considering pace. For example, 40% used after 25% of a window is a 15-point lead, not a consumption forecast. Preferences apply to all meters, survive reloads and synchronize across tabs of the same origin. They change display only, never account routing or quota enforcement.
259
-
260
- A vertical marker shows elapsed time only for an explicit valid start/end interval or a provider-verified fixed duration. A session/weekly label alone does not establish a fixed window; rolling or unknown intervals have no marker. Claude's recognized five-hour/seven-day buckets and Codex's reported quota durations supply fixed-window metadata. Unknown timing falls back to green below 90%, without a marker or yellow warning.
261
-
262
- The marker depends on the window's timing alone, not on the reading: it stays at 0% used, for an invalid percentage, and when a refresh failed or the reading is more than five minutes old. Such a reading is not set against the current time, so it gets no yellow pace warning and its tooltip omits the ahead/behind comparison. It retains the last reported green/red color with an accessible stale label; refresh to confirm it. Once the reset time has passed the window has no position and the marker disappears. Invalid percentages show an unavailable state without a fabricated fill. Tooltips explain remaining quota, time progress and reset countdown where available. See the [decision record](.agents/notes/implemented/architecture/2026-10-01-usage-pace-colors.md) and the [cursor note](.agents/notes/implemented/architecture/2026-10-06-usage-cursor-timing-only.md) for rationale and verification limits.
283
+ Every preset shows reported usage of 90% or more in red, before considering
284
+ pace. For example, 40% used after 25% of a window is a 15-point lead, not a
285
+ consumption forecast. Preferences apply to all meters, survive reloads and
286
+ synchronize across tabs of the same origin. They change display only, never
287
+ account routing or quota enforcement.
288
+
289
+ A vertical marker shows elapsed time only for an explicit valid start/end
290
+ interval or a provider-verified fixed duration. A session/weekly label alone
291
+ does not establish a fixed window; rolling or unknown intervals have no marker.
292
+ Claude's recognized five-hour/seven-day buckets and Codex's reported quota
293
+ durations supply fixed-window metadata. Unknown timing falls back to green below
294
+ 90%, without a marker or yellow warning.
295
+
296
+ The marker depends on the window's timing alone, not on the reading: it stays at
297
+ 0% used, for an invalid percentage, and when a refresh failed or the reading is
298
+ more than five minutes old. Such a reading is not set against the current time,
299
+ so it gets no yellow pace warning and its tooltip omits the ahead/behind
300
+ comparison. It retains the last reported green/red color with an accessible
301
+ stale label; refresh to confirm it. Once the reset time has passed the window
302
+ has no position and the marker disappears. Invalid percentages show an
303
+ unavailable state without a fabricated fill. Tooltips explain remaining quota,
304
+ time progress and reset countdown where available. See the
305
+ [decision record](.agents/notes/implemented/architecture/2026-10-01-usage-pace-colors.md)
306
+ and the
307
+ [cursor note](.agents/notes/implemented/architecture/2026-10-06-usage-cursor-timing-only.md)
308
+ for rationale and verification limits.
263
309
 
264
310
  ### MiniMax subscription usage
265
311
 
266
- Configure `MINIMAX_API_KEY` for the global service or
267
- `MINIMAX_CN_API_KEY` for China, using DSH's standard model-key configuration.
268
- The plugin reuses those credential references; no separate subscription ref is
269
- required. Quota reads require subscription keys, not `sk-api-*` pay-as-you-go keys. The plugin reads the
270
- region's `/v1/token_plan/remains` endpoint and displays finite per-model quotas
271
- with API-provided start/end times. It does not add model routing or OAuth login.
272
- Rows are labelled with the model name, and the plugin shows the windows the API
273
- returns for each model. MiniMax documents a five-hour and a weekly window for
274
- non-video models and only a weekly one for video. Standard (non-video) models
275
- on time-based plans report an explicit remaining percentage beside zero counts,
276
- and that percentage is enough for a row. Unlimited, boosted-above-100% and
277
- unsupported quotas are omitted (a status-3 row also covers a model the plan
278
- does not include); if none remain, the card reports unavailable rather than
279
- inventing a percentage. Explicit remaining percentages take priority; older
280
- responses follow the official CLI's remaining-count compatibility convention. No
281
- live-account response was used in verification, so a missing model may still
282
- come from the account or the plan rather than from this plugin.
312
+ Configure `MINIMAX_API_KEY` for the global service or `MINIMAX_CN_API_KEY` for
313
+ China, using DSH's standard model-key configuration. The plugin reuses those
314
+ credential references; no separate subscription ref is required. Quota reads
315
+ require subscription keys, not `sk-api-*` pay-as-you-go keys. The plugin reads
316
+ the region's `/v1/token_plan/remains` endpoint and displays finite per-model
317
+ quotas with API-provided start/end times. It does not add model routing or OAuth
318
+ login. Rows are labelled with the model name, and the plugin shows the windows
319
+ the API returns for each model. MiniMax documents a five-hour and a weekly
320
+ window for non-video models and only a weekly one for video. Standard
321
+ (non-video) models on time-based plans report an explicit remaining percentage
322
+ beside zero counts, and that percentage is enough for a row. Unlimited,
323
+ boosted-above-100% and unsupported quotas are omitted (a status-3 row also
324
+ covers a model the plan does not include); if none remain, the card reports
325
+ unavailable rather than inventing a percentage. Explicit remaining percentages
326
+ take priority; older responses follow the official CLI's remaining-count
327
+ compatibility convention. No live-account response was used in verification, so
328
+ a missing model may still come from the account or the plan rather than from
329
+ this plugin.
283
330
 
284
331
  ## Development
285
332
 
@@ -289,16 +336,27 @@ pnpm build
289
336
  pnpm test
290
337
  ```
291
338
 
292
- See [CONTRIBUTING.md](CONTRIBUTING.md) for the PR workflow, test boundaries,
293
- and isolated DSH checks.
339
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the PR workflow, test boundaries, and
340
+ isolated DSH checks.
294
341
 
295
342
  The tests and checks (unit and integration tests, the host-export and host
296
- contract checks, the boot smoke, and the host E2E) are documented in [docs/testing.md](docs/testing.md); the DSH
297
- support window and release process in
298
- [docs/compatibility.md](docs/compatibility.md).
299
-
300
- The package is published to npm as `dsh-subscriptions`, starting with the first release under the new name (GitHub source installation remains supported). The `cordis.patch.yml` bundle entry and browser module ID use the same package name. Earlier releases were published as `@goodboys-ai/dsh-subscription-hub`; that name is deprecated and points here. If the old scoped package is installed, remove it before adding the new one — installing both registers the same adapters twice and breaks plugin load.
343
+ contract checks, the boot smoke, and the host E2E) are documented in
344
+ [docs/testing.md](docs/testing.md); the DSH support window and release process
345
+ in [docs/compatibility.md](docs/compatibility.md).
346
+
347
+ The package is published to npm as `dsh-subscriptions`, starting with the first
348
+ release under the new name (GitHub source installation remains supported). The
349
+ `cordis.patch.yml` bundle entry and browser module ID use the same package name.
350
+ Earlier releases were published as `@goodboys-ai/dsh-subscription-hub`; that
351
+ name is deprecated and points here. If the old scoped package is installed,
352
+ remove it before adding the new one — installing both registers the same
353
+ adapters twice and breaks plugin load.
301
354
 
302
355
  ## Origins
303
356
 
304
- This project started from [V1ki/dsh-plugin-subscriptions](https://github.com/V1ki/dsh-plugin-subscriptions) at `090d964` under its MIT license and now includes upstream `0.9.5` (`75a8346`). The upstream English and Chinese READMEs are kept in [`docs/upstream-README.md`](docs/upstream-README.md) and [`docs/upstream-README.zh.md`](docs/upstream-README.zh.md).
357
+ This project started from
358
+ [V1ki/dsh-plugin-subscriptions](https://github.com/V1ki/dsh-plugin-subscriptions)
359
+ at `090d964` under its MIT license and now includes upstream `0.9.5`
360
+ (`75a8346`). The upstream English and Chinese READMEs are kept in
361
+ [`docs/upstream-README.md`](docs/upstream-README.md) and
362
+ [`docs/upstream-README.zh.md`](docs/upstream-README.zh.md).
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
5
  const PRIMARY_SERVICE = 'Claude Code-credentials';
@@ -8,19 +8,30 @@ function toSession(data) {
8
8
  // Empty-string tokens (seen from a corrupted Keychain item left by a Claude
9
9
  // Code logout) pass the typeof gate but are useless and would poison the
10
10
  // auth store — a single such entry fails every provider's status read.
11
- if (typeof data.accessToken !== 'string' || data.accessToken.length === 0
12
- || typeof data.refreshToken !== 'string' || data.refreshToken.length === 0
13
- || typeof data.expiresAt !== 'number' || !Number.isFinite(data.expiresAt)) {
11
+ if (typeof data.accessToken !== 'string' ||
12
+ data.accessToken.length === 0 ||
13
+ typeof data.refreshToken !== 'string' ||
14
+ data.refreshToken.length === 0 ||
15
+ typeof data.expiresAt !== 'number' ||
16
+ !Number.isFinite(data.expiresAt)) {
14
17
  return undefined;
15
18
  }
16
- const scopes = Array.isArray(data.scopes) ? data.scopes.join(' ') : typeof data.scopes === 'string' ? data.scopes : DEFAULT_SCOPES;
19
+ const scopes = Array.isArray(data.scopes)
20
+ ? data.scopes.join(' ')
21
+ : typeof data.scopes === 'string'
22
+ ? data.scopes
23
+ : DEFAULT_SCOPES;
17
24
  return {
18
25
  accessToken: data.accessToken,
19
26
  refreshToken: data.refreshToken,
20
27
  expiresAt: Math.trunc(data.expiresAt),
21
28
  scopes,
22
- ...typeof data.emailAddress === 'string' ? { emailAddress: data.emailAddress } : {},
23
- ...typeof data.subscriptionType === 'string' ? { subscriptionType: data.subscriptionType } : {},
29
+ ...(typeof data.emailAddress === 'string'
30
+ ? { emailAddress: data.emailAddress }
31
+ : {}),
32
+ ...(typeof data.subscriptionType === 'string'
33
+ ? { subscriptionType: data.subscriptionType }
34
+ : {}),
24
35
  };
25
36
  }
26
37
  function parseBlob(raw) {
@@ -117,7 +128,16 @@ export function writeBackClaudeCodeCredentials(next, expectedPriorAccessToken) {
117
128
  return false;
118
129
  const account = getKeychainAccountName() ?? PRIMARY_SERVICE;
119
130
  try {
120
- execFileSync('/usr/bin/security', ['add-generic-password', '-s', PRIMARY_SERVICE, '-a', account, '-w', updated, '-U'], {
131
+ execFileSync('/usr/bin/security', [
132
+ 'add-generic-password',
133
+ '-s',
134
+ PRIMARY_SERVICE,
135
+ '-a',
136
+ account,
137
+ '-w',
138
+ updated,
139
+ '-U',
140
+ ], {
121
141
  timeout: 2000,
122
142
  stdio: 'ignore',
123
143
  });
@@ -163,7 +183,9 @@ export function writeBackClaudeCodeCredentials(next, expectedPriorAccessToken) {
163
183
  */
164
184
  export async function refreshClaudeSynced(session, doRefresh) {
165
185
  const fromSource = readClaudeCodeCredentials();
166
- const base = fromSource !== undefined && fromSource.accessToken !== session.accessToken ? fromSource : session;
186
+ const base = fromSource !== undefined && fromSource.accessToken !== session.accessToken
187
+ ? fromSource
188
+ : session;
167
189
  if (base.expiresAt > Date.now() + 60_000)
168
190
  return base;
169
191
  const next = await doRefresh(base);
@@ -70,18 +70,24 @@ export class DeviceFlowManager {
70
70
  const response = await fetchFn(spec.deviceCodeUrl, {
71
71
  method: 'POST',
72
72
  headers: {
73
- 'accept': 'application/json',
73
+ accept: 'application/json',
74
74
  'content-type': 'application/x-www-form-urlencoded',
75
75
  },
76
- body: new URLSearchParams({ client_id: spec.clientId, scope: spec.scope }).toString(),
76
+ body: new URLSearchParams({
77
+ client_id: spec.clientId,
78
+ scope: spec.scope,
79
+ }).toString(),
77
80
  });
78
81
  if (!response.ok) {
79
82
  throw new Error(`${provider} device-code request failed (HTTP ${String(response.status)})`);
80
83
  }
81
- const wire = await parseProviderJson(response, 'device code request');
82
- if (typeof wire.device_code !== 'string' || wire.device_code.length === 0
83
- || typeof wire.user_code !== 'string' || wire.user_code.length === 0
84
- || typeof wire.verification_uri !== 'string' || wire.verification_uri.length === 0) {
84
+ const wire = (await parseProviderJson(response, 'device code request'));
85
+ if (typeof wire.device_code !== 'string' ||
86
+ wire.device_code.length === 0 ||
87
+ typeof wire.user_code !== 'string' ||
88
+ wire.user_code.length === 0 ||
89
+ typeof wire.verification_uri !== 'string' ||
90
+ wire.verification_uri.length === 0) {
85
91
  throw new Error(`${provider} device-code response is missing device_code/user_code/verification_uri`);
86
92
  }
87
93
  const intervalSec = typeof wire.interval === 'number' && wire.interval > 0
@@ -124,7 +130,7 @@ export class DeviceFlowManager {
124
130
  const pollResponse = await fetchFn(spec.tokenUrl, {
125
131
  method: 'POST',
126
132
  headers: {
127
- 'accept': 'application/json',
133
+ accept: 'application/json',
128
134
  'content-type': 'application/x-www-form-urlencoded',
129
135
  },
130
136
  body: new URLSearchParams({
@@ -134,8 +140,9 @@ export class DeviceFlowManager {
134
140
  }).toString(),
135
141
  signal: controller.signal,
136
142
  });
137
- const result = await parseProviderJson(pollResponse, 'device token poll');
138
- if (typeof result.access_token === 'string' && result.access_token.length > 0) {
143
+ const result = (await parseProviderJson(pollResponse, 'device token poll'));
144
+ if (typeof result.access_token === 'string' &&
145
+ result.access_token.length > 0) {
139
146
  settle(undefined, result.access_token);
140
147
  return;
141
148
  }