@hystudio/dsh-quota-pill 0.2.3 → 0.2.4

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.
package/AGENTS.md CHANGED
@@ -33,6 +33,7 @@ There are **two credential families**, and this matters for every step:
33
33
  | DSH Platform sign-in | `deepseek` | **nothing** — works if they are signed in to DeepSeek Platform in DSH |
34
34
  | API key | `kimi`, `opencode`, `commandcode` | a key pasted into `~/.dsh/.credentials.yaml` |
35
35
  | OAuth via CLIProxyAPI | `xai`, `codex`, `agy` | **CLIProxyAPI installed and logged in once** |
36
+ | Console session cookie | `opendesign` | the `__Secure-better-auth.session_token` cookie from the Open Design cloud dashboard |
36
37
 
37
38
  **Check DeepSeek first.** It is the official account and needs no setup beyond a
38
39
  DSH Platform sign-in, so it is usually the one the user actually wants. If it
package/README.md CHANGED
@@ -22,6 +22,7 @@ into the live number.
22
22
  | **xAI (Grok)** | OAuth token from [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) |
23
23
  | **Codex (ChatGPT Plus)** | same — and CLIProxyAPI must be **running** |
24
24
  | **Antigravity** | same |
25
+ | **Open Design** (optional) | `refs.OPENDESIGN_SESSION_COOKIE` — the cloud console's session cookie |
25
26
 
26
27
  **DeepSeek works with no setup at all** if you are signed in to DeepSeek Platform
27
28
  in DSH — that is the official account, and it is the one most users care about.
@@ -30,8 +31,10 @@ The three API-key providers need nothing but the key. The three OAuth providers
30
31
  need CLIProxyAPI installed and logged in, because that is what holds the tokens;
31
32
  this plugin only reads them and never runs an OAuth flow itself.
32
33
 
33
- Every provider is optional. Unconfigured ones say exactly what is missing instead
34
- of showing a fake zero.
34
+ Every provider is optional, and **only the ones you actually have are listed**:
35
+ the pill reads DSH's `session/modelCatalog` to learn which providers the app can
36
+ route to, so removing a provider from DSH removes it from the pill too. A provider
37
+ that is present but has no credential is skipped rather than shown as an empty row.
35
38
 
36
39
  ## Install
37
40
 
@@ -190,6 +193,14 @@ would flicker between models you do not even have configured.
190
193
  `monthlyCap = weekly.cap × 2`, an identity verified against two different plans
191
194
  (31.6 + 38.4 = 70 with `weekly.cap` 35; 0.225 + 9.775 = 10 with `weekly.cap` 6).
192
195
 
196
+ **Open Design** keeps its allowance in the cloud console, not in the API key you
197
+ use for inference. The `amr-link.open-design.ai/v1` gateway has no balance
198
+ endpoint at all; the console's own `amr-api` does, and it authenticates with a
199
+ `__Secure-better-auth.session_token` cookie. That cookie is HttpOnly, so grab it
200
+ from devtools → Application → Cookies. The workspace id is discovered
201
+ automatically. The request goes through system curl because Cloudflare rejects
202
+ Node's TLS fingerprint on that host with `error code: 1010`.
203
+
193
204
  **DeepSeek** is a prepaid CNY wallet, so there is no percentage to show — the pill
194
205
  shows the balance and, separately, any bonus credit. Two traps worth recording:
195
206
  the stored credential is a *Platform login grant*, not an `api.deepseek.com` API
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hystudio/dsh-quota-pill",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Quota pill for DeepSeek Harness: remaining quota for every LLM subscription you have, in one collapsible pill",
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Open Design (open-design.ai) — Cloud coding-plan allowance.
3
+ *
4
+ * Optional provider. Unlike the others it does **not** read an API key: the
5
+ * allowance lives in the cloud console's own API and is authenticated with the
6
+ * console's session cookie. Set it once:
7
+ *
8
+ * ~/.dsh/.credentials.yaml
9
+ * refs:
10
+ * OPENDESIGN_SESSION_COOKIE: <__Secure-better-auth.session_token value>
11
+ *
12
+ * To get that value: open https://open-design.ai/cloud/dashboard while signed in,
13
+ * then in the browser's devtools read the `__Secure-better-auth.session_token`
14
+ * cookie (it is HttpOnly, so `document.cookie` will not show it).
15
+ *
16
+ * Two traps, both verified:
17
+ * · The inference gateway at amr-link.open-design.ai/v1 has NO balance
18
+ * endpoint — it is a chat proxy only. Probing it for quota always 404s.
19
+ * · amr-api sits behind Cloudflare, which rejects Node's TLS fingerprint with
20
+ * `error code: 1010`. The request therefore goes through system curl.
21
+ * That is the same request over a different TLS stack, not an auth bypass.
22
+ */
23
+ import { readRef } from './credentials.js';
24
+ import { getJsonViaCurl } from './http.js';
25
+
26
+ const API = process.env.OPENDESIGN_API_BASE || 'https://amr-api.open-design.ai/api/v1';
27
+
28
+ const round2 = (n) => Math.round(n * 100) / 100;
29
+
30
+ function headers(cookie) {
31
+ return {
32
+ Cookie: '__Secure-better-auth.session_token=' + cookie,
33
+ Origin: 'https://open-design.ai',
34
+ Referer: 'https://open-design.ai/',
35
+ };
36
+ }
37
+
38
+ /** Window label from its length: 18000s is the 5h cap, 604800s the weekly one. */
39
+ function windowKey(seconds) {
40
+ const s = Number(seconds) || 0;
41
+ if (s > 0 && s <= 6 * 3600) return '5h';
42
+ if (s > 0 && s <= 8 * 86400) return 'weekly';
43
+ if (s > 0) return 'monthly';
44
+ return 'credits-all';
45
+ }
46
+
47
+ /**
48
+ * The workspace id is discoverable, so the user only supplies a cookie.
49
+ * `/workspaces/current` already resolves to the default workspace.
50
+ */
51
+ async function currentWorkspace(cookie) {
52
+ const res = await getJsonViaCurl(`${API}/workspaces/current`, { headers: headers(cookie) });
53
+ if (!res.ok) return { error: res.error };
54
+ const id = res.data && res.data.workspaceId;
55
+ return id ? { id } : { error: 'no workspaceId in response' };
56
+ }
57
+
58
+ export async function opendesign() {
59
+ const cookie = readRef('OPENDESIGN_SESSION_COOKIE');
60
+ if (!cookie) {
61
+ return {
62
+ kind: 'none',
63
+ provider: 'opendesign',
64
+ error: 'no OPENDESIGN_SESSION_COOKIE in ~/.dsh/.credentials.yaml',
65
+ setup: {
66
+ kind: 'session-cookie',
67
+ ref: 'OPENDESIGN_SESSION_COOKIE',
68
+ hint: 'Copy the __Secure-better-auth.session_token cookie from the Open Design cloud dashboard.',
69
+ },
70
+ };
71
+ }
72
+
73
+ const ws = await currentWorkspace(cookie);
74
+ if (ws.error) {
75
+ return {
76
+ kind: 'probe-failed',
77
+ provider: 'opendesign',
78
+ error: `workspace lookup failed: ${ws.error}`,
79
+ hint: 'The console session cookie has probably expired — copy a fresh one.',
80
+ };
81
+ }
82
+
83
+ const res = await getJsonViaCurl(
84
+ `${API}/workspaces/${ws.id}/billing/coding-plan-usage`,
85
+ { headers: headers(cookie), timeoutMs: 25000 },
86
+ );
87
+ if (!res.ok) {
88
+ return {
89
+ kind: 'probe-failed',
90
+ provider: 'opendesign',
91
+ error: `coding-plan-usage failed: ${res.error}`,
92
+ hint: res.error && res.error.includes('40')
93
+ ? 'The console session cookie has probably expired — copy a fresh one.'
94
+ : undefined,
95
+ };
96
+ }
97
+
98
+ const body = res.data || {};
99
+ const windows = [];
100
+ for (const w of body.windows || []) {
101
+ const limit = Number(w.limitCredits);
102
+ const remain = Number(w.remainingCredits);
103
+ if (!Number.isFinite(limit) || limit <= 0 || !Number.isFinite(remain)) continue;
104
+ windows.push({
105
+ key: windowKey(w.durationSeconds),
106
+ remainPct: round2((remain / limit) * 100),
107
+ resetAt: w.resetsAt || null,
108
+ credits: { remain: round2(remain), total: round2(limit), unit: 'credits' },
109
+ });
110
+ }
111
+
112
+ if (!windows.length) {
113
+ return {
114
+ kind: 'none',
115
+ provider: 'opendesign',
116
+ error: `coding-plan-usage returned no windows (tier ${body.tier || 'unknown'})`,
117
+ };
118
+ }
119
+
120
+ // The tightest window is what blocks the next request, but the list itself
121
+ // stays in canonical order (5h before weekly) so rows match every other
122
+ // provider instead of reshuffling as the numbers move.
123
+ const tight = windows.reduce((a, b) => (a.remainPct <= b.remainPct ? a : b));
124
+ const ORDER = { '5h': 0, weekly: 1, monthly: 2, 'credits-all': 3 };
125
+ windows.sort((a, b) => (ORDER[a.key] ?? 9) - (ORDER[b.key] ?? 9));
126
+ const products = {};
127
+ for (const w of windows) products[w.key] = round2(100 - w.remainPct);
128
+
129
+ return {
130
+ kind: 'subscription-quota',
131
+ provider: 'opendesign',
132
+ account: body.tier || null,
133
+ remainingPct: tight.remainPct,
134
+ usedPct: round2(100 - tight.remainPct),
135
+ window: tight.key,
136
+ resetAt: tight.resetAt,
137
+ products,
138
+ windows,
139
+ raw: { source: 'coding-plan-usage (console session)', tier: body.tier },
140
+ };
141
+ }
package/src/server.js CHANGED
@@ -25,6 +25,7 @@ import { xai, codex, agy } from './adapters-cliproxy.js';
25
25
  import { deepseek } from './adapters-deepseek.js';
26
26
  import { credentialSource, hasCliProxy, CLI_PROXY_DIR, CREDENTIALS_FILE } from './credentials.js';
27
27
  import { catalogProviderIds } from './catalog.js';
28
+ import { opendesign } from './adapters-opendesign.js';
28
29
 
29
30
  const PORT = Number(process.env.QUOTA_PILL_PORT || 8319);
30
31
  const HOST = process.env.QUOTA_PILL_HOST || '127.0.0.1';
@@ -32,7 +33,7 @@ const CACHE_TTL_MS = Number(process.env.QUOTA_PILL_CACHE_TTL || 60) * 1000;
32
33
 
33
34
  // Order matters: the pill lists providers in this order, and the official
34
35
  // DeepSeek account is the one most people look for first.
35
- const ADAPTERS = { deepseek, kimi, opencode, commandcode, xai, codex, agy };
36
+ const ADAPTERS = { deepseek, kimi, opencode, commandcode, xai, codex, agy, opendesign };
36
37
 
37
38
  /** Display names. Deliberately small: the table stays in sync with ADAPTERS. */
38
39
  const DISPLAY_NAMES = {
@@ -43,6 +44,7 @@ const DISPLAY_NAMES = {
43
44
  xai: 'xAI (Grok)',
44
45
  codex: 'Codex (ChatGPT Plus)',
45
46
  agy: 'Antigravity',
47
+ opendesign: 'Open Design',
46
48
  };
47
49
 
48
50
  /** Which DSH model prefixes map to which adapter. */
@@ -58,6 +60,7 @@ const PREFIX_ALIASES = {
58
60
  antigravity: 'agy',
59
61
  agy: 'agy',
60
62
  deepseek: 'deepseek',
63
+ opendesign: 'opendesign',
61
64
  };
62
65
 
63
66
  /** provider|model → { at, payload }. Keyed by model because readings differ per model. */
@@ -116,6 +119,7 @@ const CATALOG_ALIASES = {
116
119
  xai: ['xai'],
117
120
  codex: ['codex'],
118
121
  commandcode: ['commandcode'],
122
+ opendesign: ['opendesign'],
119
123
  };
120
124
 
121
125
  function inCatalog(provider, ids) {