@goodandready/dsh-subscriptions 0.5.6 → 0.5.22

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 (50) hide show
  1. package/lib/adapter.js +2 -0
  2. package/lib/atomic-lock.js +35 -0
  3. package/lib/backoff.js +26 -0
  4. package/lib/client.js +211 -15
  5. package/lib/coalesce-stream.js +22 -0
  6. package/lib/code-assist.js +4 -4
  7. package/lib/cookie-bridge.js +8 -0
  8. package/lib/credit-detect.js +22 -0
  9. package/lib/encrypted-bundle.js +46 -0
  10. package/lib/import-auth.js +74 -7
  11. package/lib/importers/coding-agents.js +37 -0
  12. package/lib/index.js +35 -7
  13. package/lib/latency-heatmap.js +26 -0
  14. package/lib/latency-score.js +36 -0
  15. package/lib/multi-tenant.js +16 -0
  16. package/lib/network-benchmark.js +23 -0
  17. package/lib/pkce-store.js +28 -0
  18. package/lib/preflight-tokens.js +23 -0
  19. package/lib/proactive-refresh.js +37 -0
  20. package/lib/prompt-cache-warmer.js +44 -0
  21. package/lib/quarantine.js +38 -0
  22. package/lib/ratelimit-parser.js +24 -0
  23. package/lib/reconnect-stream.js +21 -0
  24. package/lib/reset-toast.js +38 -0
  25. package/lib/revocation.js +25 -0
  26. package/lib/rotate.js +70 -17
  27. package/lib/savings-calculator.js +14 -0
  28. package/lib/session-pin.js +29 -0
  29. package/lib/state-recovery.js +33 -0
  30. package/lib/stream-rotate.js +34 -11
  31. package/lib/telemetry.js +28 -0
  32. package/lib/token-speedometer.js +26 -0
  33. package/lib/vendors/antigravity.js +16 -3
  34. package/lib/vendors/claude-cli.js +24 -0
  35. package/lib/vendors/cody.js +72 -0
  36. package/lib/vendors/copilot.js +194 -0
  37. package/lib/vendors/cursor.js +4 -4
  38. package/lib/vendors/ernie.js +93 -0
  39. package/lib/vendors/glm.js +4 -4
  40. package/lib/vendors/index.js +26 -1
  41. package/lib/vendors/jetbrains.js +79 -0
  42. package/lib/vendors/kimi.js +4 -4
  43. package/lib/vendors/kiro.js +3 -3
  44. package/lib/vendors/perplexity.js +79 -0
  45. package/lib/vendors/qwen.js +89 -0
  46. package/lib/vendors/replit.js +72 -0
  47. package/lib/vendors/spark.js +77 -0
  48. package/lib/wire.js +4 -2
  49. package/lib/zero-trace-logger.js +27 -0
  50. package/package.json +2 -5
package/lib/adapter.js CHANGED
@@ -57,6 +57,8 @@ export class SubscriptionAdapter extends LlmAdapter {
57
57
  if (typeof this.deps.hideDeprecatedModels === 'function' && this.deps.hideDeprecatedModels()) {
58
58
  models = models.filter((m) => !isDeprecatedId(String((m && m.id) || '')))
59
59
  }
60
+ // Defensive: ensure every model has the provider field set
61
+ models = models.map((m) => (m && !m.provider ? { ...m, provider } : m))
60
62
  return models
61
63
  }
62
64
 
@@ -0,0 +1,35 @@
1
+ import { open, unlink } from 'node:fs/promises'
2
+
3
+ export async function withFileLock(filePath, fn, timeoutMs = 5000) {
4
+ const lockPath = `${filePath}.lock`
5
+ const start = Date.now()
6
+ let handle = null
7
+
8
+ while (Date.now() - start < timeoutMs) {
9
+ try {
10
+ handle = await open(lockPath, 'wx')
11
+ break
12
+ } catch (err) {
13
+ if (err.code === 'EEXIST') {
14
+ await new Promise((r) => setTimeout(r, 40))
15
+ continue
16
+ }
17
+ throw err
18
+ }
19
+ }
20
+
21
+ if (!handle) {
22
+ throw new Error(`Failed to acquire lock on ${lockPath} after ${timeoutMs}ms`)
23
+ }
24
+
25
+ try {
26
+ return await fn()
27
+ } finally {
28
+ try {
29
+ await handle.close()
30
+ await unlink(lockPath)
31
+ } catch {
32
+ // ignore unlock errors
33
+ }
34
+ }
35
+ }
package/lib/backoff.js ADDED
@@ -0,0 +1,26 @@
1
+ export async function executeWithRetry(fn, {
2
+ maxRetries = 3,
3
+ initialDelayMs = 200,
4
+ maxDelayMs = 3000,
5
+ factor = 2,
6
+ isRetryable = (err) => true
7
+ } = {}) {
8
+ let attempt = 0
9
+ let delay = initialDelayMs
10
+
11
+ while (true) {
12
+ try {
13
+ return await fn()
14
+ } catch (err) {
15
+ attempt++
16
+ if (attempt > maxRetries || !isRetryable(err)) {
17
+ throw err
18
+ }
19
+ // Exponential backoff with random jitter: delay * (1 + random * 0.3)
20
+ const jitter = 1 + Math.random() * 0.3
21
+ const sleepMs = Math.min(maxDelayMs, Math.round(delay * jitter))
22
+ await new Promise((r) => setTimeout(r, sleepMs))
23
+ delay = Math.min(maxDelayMs, delay * factor)
24
+ }
25
+ }
26
+ }
package/lib/client.js CHANGED
@@ -25,7 +25,12 @@ window.__ModuleLoader__.load({
25
25
  '.dsub-chevOpen{transform:rotate(180deg)}' +
26
26
  '.dsub-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}' +
27
27
  '.dsub-block{margin-top:10px}' +
28
- '.dsub-row{display:flex;align-items:center;gap:8px;margin-top:8px}' +
28
+ '.dsub-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px}' +
29
+ '.dsub-grow{flex:1 1 180px;min-width:140px}' +
30
+ '.dsub-helpBox{margin-top:8px;padding:12px 14px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:1.6}' +
31
+ '.dsub-helpHead{display:flex;align-items:center;justify-content:space-between;font-weight:600;margin-bottom:8px;color:var(--dsw-alias-brand-primary)}' +
32
+ '.dsub-helpStep{margin-bottom:6px;color:var(--dsw-alias-label-primary)}' +
33
+ '.dsub-btnActive{background:var(--dsw-alias-brand-primary);color:#fff;border-color:var(--dsw-alias-brand-primary)}' +
29
34
  '.dsub-h{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
30
35
  '.dsub-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}' +
31
36
  '.dsub-dim{font-size:11.5px;color:var(--dsw-alias-label-tertiary)}' +
@@ -120,6 +125,126 @@ const cssId = 'dsh-subscriptions/settings.module.css'
120
125
  // пакет, не трогая код этого плагина. Английский — язык по умолчанию,
121
126
  // на него же приходится откат, если перевода нет.
122
127
  const NS = 'dsh-subscriptions'
128
+
129
+ const INSTRUCTIONS = {
130
+ codex: {
131
+ title: 'OpenAI / ChatGPT Codex',
132
+ stepsRu: [
133
+ '1. Авторизуйтесь через CLI на сервере: выполните "codex login" в терминале.',
134
+ '2. Нажмите кнопку "📥 Из CLI", чтобы сервер автоматически прочитал ~/.codex/auth.json.',
135
+ '3. Либо нажмите кнопку "Авторизация по коду" (Device Login) выше и введите код на auth.openai.com.',
136
+ ],
137
+ stepsEn: [
138
+ '1. Log in via CLI on server: run "codex login" in terminal.',
139
+ '2. Click "📥 From CLI" to auto-import ~/.codex/auth.json.',
140
+ '3. Or click "Device Login" above and enter the code at auth.openai.com.',
141
+ ],
142
+ link: 'https://chatgpt.com',
143
+ },
144
+ claude: {
145
+ title: 'Claude Code',
146
+ stepsRu: [
147
+ '1. Выполните "claude login" в консоли сервера.',
148
+ '2. Нажмите "📥 Из CLI" — плагин подтянет токен из ~/.claude/credentials.json.',
149
+ '3. Либо скопируйте свой Session Token / API-ключ из консоли Anthropic и нажмите "Сохранить".',
150
+ ],
151
+ stepsEn: [
152
+ '1. Run "claude login" in server console.',
153
+ '2. Click "📥 From CLI" — plugin will import ~/.claude/credentials.json.',
154
+ '3. Or paste your Session Token / API Key from Anthropic Console and click "Save token".',
155
+ ],
156
+ link: 'https://console.anthropic.com/settings/keys',
157
+ },
158
+ cursor: {
159
+ title: 'Cursor IDE',
160
+ stepsRu: [
161
+ '1. Авторизация через CLI: если Cursor установлен на сервере, нажмите "📥 Из CLI" (считывается из ~/.cursor/cli-config.json).',
162
+ '2. Вручную: откройте Настройки Cursor (кнопка "Подключить" или ссылка ниже), скопируйте WorkOS токен сессии или свой API ключ.',
163
+ '3. Вставьте его в поле ввода и нажмите кнопку "Сохранить".',
164
+ ],
165
+ stepsEn: [
166
+ '1. Auto CLI: if Cursor is used on server, click "📥 From CLI" (reads from ~/.cursor/cli-config.json).',
167
+ '2. Manual: open Cursor Settings (Connect button or link below), copy your session token or API key.',
168
+ '3. Paste it into the input field and click "Save token".',
169
+ ],
170
+ link: 'https://cursor.com/settings',
171
+ },
172
+ antigravity: {
173
+ title: 'Google Antigravity / Gemini',
174
+ stepsRu: [
175
+ '1. Самый простой способ: нажмите "📥 Из CLI" — на сервере уже есть авторизованная сессия (~/.gemini/antigravity-cli/antigravity-oauth-token).',
176
+ '2. Если токен обновлен в системе, нажатие "📥 Из CLI" мгновенно подхватит актуальный refresh_token.',
177
+ '3. Ручной ввод: вставьте JSON сессии или OAuth access_token/refresh_token в поле и нажмите "Сохранить".',
178
+ 'Примечание: Кнопка "Подключить" для Google требует свой OAuth client_id в настройках плагина, поэтому для Antigravity рекомендуется использовать "📥 Из CLI"!',
179
+ ],
180
+ stepsEn: [
181
+ '1. Easiest way: click "📥 From CLI" — server already has active token in ~/.gemini/antigravity-cli/antigravity-oauth-token.',
182
+ '2. Clicking "📥 From CLI" instantly loads fresh credentials.',
183
+ '3. Manual: paste session JSON or OAuth tokens and click "Save token".',
184
+ 'Note: "Connect" button requires custom Google client_id in plugin settings; use "📥 From CLI" for seamless login!',
185
+ ],
186
+ link: 'https://antigravity.google',
187
+ },
188
+ kimi: {
189
+ title: 'Moonshot Kimi',
190
+ stepsRu: [
191
+ '1. Откройте консоль разработчика Moonshot (ссылка ниже или по кнопке "Подключить").',
192
+ '2. Создайте и скопируйте свой API-ключ (sk-...).',
193
+ '3. Вставьте ключ в поле слота и нажмите "Сохранить".',
194
+ '4. Если у вас настроен Kimi Code CLI, можно также нажать "📥 Из CLI" (~/.kimi-code).',
195
+ ],
196
+ stepsEn: [
197
+ '1. Open Moonshot developer console (link below or click Connect).',
198
+ '2. Generate and copy your API Key (sk-...).',
199
+ '3. Paste it into the input field and click "Save token".',
200
+ '4. Or click "📥 From CLI" if ~/.kimi-code is configured.',
201
+ ],
202
+ link: 'https://platform.moonshot.cn/console/api-keys',
203
+ },
204
+ glm: {
205
+ title: 'Zhipu GLM (Z.AI)',
206
+ stepsRu: [
207
+ '1. Откройте Центр разработчика Zhipu AI (ссылка ниже или по кнопке "Подключить").',
208
+ '2. Скопируйте ваш API-ключ GLM.',
209
+ '3. Вставьте его в поле ввода и нажмите "Сохранить".',
210
+ '4. Если установлен ZCode CLI (~/.zcode), нажмите "📥 Из CLI" для авто-импорта.',
211
+ ],
212
+ stepsEn: [
213
+ '1. Open Zhipu BigModel API Keys center (link below or click Connect).',
214
+ '2. Copy your GLM API Key.',
215
+ '3. Paste it into the input field and click "Save token".',
216
+ '4. Or click "📥 From CLI" if ZCode CLI is installed (~/.zcode).',
217
+ ],
218
+ link: 'https://open.bigmodel.cn/usercenter/apikeys',
219
+ },
220
+ grok: {
221
+ title: 'xAI Grok',
222
+ stepsRu: [
223
+ '1. Выполните вход через grok-cli или hermes на сервере.',
224
+ '2. Нажмите "📥 Из CLI" (~/.grok/auth.json).',
225
+ '3. Либо вставьте xAI API-ключ в поле и нажмите "Сохранить".',
226
+ ],
227
+ stepsEn: [
228
+ '1. Run grok-cli or hermes login on server.',
229
+ '2. Click "📥 From CLI" (~/.grok/auth.json).',
230
+ '3. Or paste your xAI API Key and click "Save token".',
231
+ ],
232
+ link: 'https://x.ai',
233
+ },
234
+ kiro: {
235
+ title: 'AWS Kiro',
236
+ stepsRu: [
237
+ '1. Нажмите "📥 Из CLI" для импорта из ~/.kiro/credentials.json или ~/.aws/sso.',
238
+ '2. Либо вставьте KIRO_API_KEY в поле и нажмите "Сохранить".',
239
+ ],
240
+ stepsEn: [
241
+ '1. Click "📥 From CLI" to import from ~/.kiro/credentials.json.',
242
+ '2. Or paste KIRO_API_KEY and click "Save token".',
243
+ ],
244
+ link: 'https://aws.amazon.com',
245
+ },
246
+ }
247
+
123
248
  const en = {
124
249
  'notConnected': 'Not connected',
125
250
  'verifyAccount': 'Verify account',
@@ -213,10 +338,15 @@ const cssId = 'dsh-subscriptions/settings.module.css'
213
338
  'resetLabel': 'reset',
214
339
  'check': 'Check',
215
340
  'checking': 'Checking…',
216
- 'importToken': 'Import',
217
- 'importTokenPlace': 'Paste an existing token',
218
- 'importLocalCli': 'Import from local CLI',
219
- 'importLocalSuccess': 'Imported from local CLI!',
341
+ 'importToken': 'Save token',
342
+ 'importTokenPlace': 'Paste token or API key',
343
+ 'importLocalCli': '📥 From CLI',
344
+ 'importLocalCliTitle': 'Auto-import token from locally installed CLI on server',
345
+ 'importLocalSuccess': 'Successfully imported token from local CLI!',
346
+ 'howToGetToken': '❓ Instructions',
347
+ 'howToGetTokenTitle': 'How to get token / API key for this provider',
348
+ 'instructionsTitle': 'How to obtain token',
349
+ 'close': 'Close',
220
350
  'reconnectRequired': 'Reconnect required',
221
351
  'manualTitle': 'If the browser did not come back',
222
352
  'windowPrimary': 'Primary window',
@@ -225,10 +355,15 @@ const cssId = 'dsh-subscriptions/settings.module.css'
225
355
  const ru = {
226
356
  'check': 'Проверить',
227
357
  'checking': 'Проверяю…',
228
- 'importToken': 'Внести',
229
- 'importTokenPlace': 'Вставить уже готовый токен',
230
- 'importLocalCli': 'Импортировать из локального CLI',
231
- 'importLocalSuccess': 'Импортировано из локального CLI!',
358
+ 'importToken': 'Сохранить',
359
+ 'importTokenPlace': 'Вставить токен или API-ключ',
360
+ 'importLocalCli': '📥 Из CLI',
361
+ 'importLocalCliTitle': 'Автоматически считать токен из установленного на сервере CLI',
362
+ 'importLocalSuccess': 'Токен успешно загружен из локального CLI!',
363
+ 'howToGetToken': '❓ Инструкция',
364
+ 'howToGetTokenTitle': 'Как получить токен или API-ключ для этого провайдера',
365
+ 'instructionsTitle': 'Инструкция по получению токена',
366
+ 'close': 'Закрыть',
232
367
  'reconnectRequired': 'Нужно переподключить',
233
368
  'manualTitle': 'Если браузер не вернулся сам',
234
369
  'windowPrimary': 'Основное окно',
@@ -506,7 +641,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
506
641
  const [checking, setChecking] = React.useState({})
507
642
  const [saved, setSaved] = React.useState(false)
508
643
  const [tokenDraft, setTokenDraft] = React.useState({})
644
+ const [openHelp, setOpenHelp] = React.useState({})
509
645
  const [err, setErr] = React.useState('')
646
+ const [info, setInfo] = React.useState('')
510
647
 
511
648
  const applyPayload = (data) => {
512
649
  setDraft(JSON.parse(JSON.stringify((data && data.config) || {})))
@@ -679,6 +816,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
679
816
 
680
817
  const importLocalCli = async (provider, index) => {
681
818
  setErr('')
819
+ setInfo('')
682
820
  const res = await fetch('/dsh-subscriptions/import-local', {
683
821
  method: 'POST',
684
822
  headers: { 'Content-Type': 'application/json' },
@@ -686,8 +824,10 @@ const cssId = 'dsh-subscriptions/settings.module.css'
686
824
  })
687
825
  const data = await res.json().catch(() => ({}))
688
826
  if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
689
- await load()
827
+ if (data.config) applyPayload(data)
828
+ else await reload()
690
829
  setInfo(t('importLocalSuccess'))
830
+ setTimeout(() => setInfo(''), 3000)
691
831
  }
692
832
 
693
833
  const importToken = async (provider, index) => {
@@ -825,6 +965,21 @@ const cssId = 'dsh-subscriptions/settings.module.css'
825
965
  onClick: () => complete(row.slot.provider, row.slot.index).catch((e) => setErr(cleanErrorMessage(e.message || e))),
826
966
  }, t('submitCode')),
827
967
  ),
968
+ React.createElement('div', { className: 'dsub-row', style: { justifyContent: 'space-between' } },
969
+ React.createElement('button', {
970
+ type: 'button',
971
+ className: 'dsub-mini',
972
+ title: t('importLocalCliTitle'),
973
+ style: { display: 'inline-flex', alignItems: 'center', gap: 6, fontWeight: 600 },
974
+ onClick: () => importLocalCli(row.slot.provider, row.slot.index).catch((e) => setErr(cleanErrorMessage(e.message || e))),
975
+ }, t('importLocalCli')),
976
+ React.createElement('button', {
977
+ type: 'button',
978
+ className: 'dsub-mini' + (openHelp[key] ? ' dsub-btnActive' : ''),
979
+ title: t('howToGetTokenTitle'),
980
+ onClick: (e) => { if (e) { e.preventDefault(); e.stopPropagation(); } setOpenHelp((h) => Object.assign({}, h, { [key]: !h[key] })); },
981
+ }, t('howToGetToken')),
982
+ ),
828
983
  React.createElement('div', { className: 'dsub-row' },
829
984
  React.createElement('input', {
830
985
  className: 'dsub-grow',
@@ -834,14 +989,44 @@ const cssId = 'dsh-subscriptions/settings.module.css'
834
989
  }),
835
990
  React.createElement('button', {
836
991
  type: 'button', className: 'dsub-mini',
992
+ title: t('importToken'),
837
993
  onClick: () => importToken(row.slot.provider, row.slot.index).catch((e) => setErr(cleanErrorMessage(e.message || e))),
838
994
  }, t('importToken')),
839
- React.createElement('button', {
840
- type: 'button', className: 'dsub-mini',
841
- title: t('importLocalCli'),
842
- onClick: () => importLocalCli(row.slot.provider, row.slot.index).catch((e) => setErr(cleanErrorMessage(e.message || e))),
843
- }, '📥 CLI'),
844
995
  ),
996
+ (openHelp[key] ? (function renderInlineHelp(){
997
+ const prov = row.slot.provider;
998
+ const info = (typeof INSTRUCTIONS !== 'undefined' && INSTRUCTIONS[prov]) || {
999
+ title: prov,
1000
+ stepsRu: ['Вставьте токен или API-ключ в поле выше и нажмите "Сохранить".'],
1001
+ stepsEn: ['Paste your token or API key into the field above and click "Save token".'],
1002
+ };
1003
+ // Determine language safely from t('instructionsTitle')
1004
+ const isRu = t('instructionsTitle').indexOf('Инструкция') >= 0;
1005
+ const steps = isRu ? info.stepsRu : info.stepsEn;
1006
+ return React.createElement('div', { className: 'dsub-helpBox' },
1007
+ React.createElement('div', { className: 'dsub-helpHead' },
1008
+ React.createElement('span', null, 'ℹ️ ' + info.title + ' — ' + t('instructionsTitle')),
1009
+ React.createElement('button', {
1010
+ type: 'button', className: 'dsub-mini',
1011
+ style: { padding: '1px 6px', fontSize: 11 },
1012
+ onClick: (e) => {
1013
+ if (e) { e.preventDefault(); e.stopPropagation(); }
1014
+ setOpenHelp((h) => Object.assign({}, h, { [key]: false }));
1015
+ },
1016
+ }, '✕')
1017
+ ),
1018
+ steps.map((st, i) => React.createElement('div', { key: i, className: 'dsub-helpStep' }, st)),
1019
+ info.link ? React.createElement('div', { style: { marginTop: 8 } },
1020
+ React.createElement('a', {
1021
+ href: info.link,
1022
+ target: '_blank',
1023
+ rel: 'noopener noreferrer',
1024
+ className: 'dsub-mini',
1025
+ style: { display: 'inline-flex', alignItems: 'center', gap: 4, textDecoration: 'none' },
1026
+ }, '🔗 ' + (isRu ? 'Открыть страницу провайдера' : 'Open provider console'))
1027
+ ) : null
1028
+ );
1029
+ })() : null),
845
1030
  React.createElement('div', { className: 'dsub-row' },
846
1031
  React.createElement('input', {
847
1032
  className: 'dsub-grow',
@@ -1005,6 +1190,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1005
1190
  onClick: () => save().catch((e) => setErr(cleanErrorMessage(e.message || e))),
1006
1191
  }, t('save')),
1007
1192
  saved ? React.createElement('span', { className: 'dsub-ok' }, t('saved')) : null,
1193
+ info ? React.createElement('span', { className: 'dsub-ok' }, info) : null,
1008
1194
  err ? React.createElement('span', { className: 'dsub-bad' }, err) : null,
1009
1195
  ),
1010
1196
  )
@@ -1211,6 +1397,16 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1211
1397
  if (p.includes('antigravity') || p.includes('google') || p.includes('gemini')) return { label: 'AGY', cls: 'dsub-brandAgy', icon: '◆' }
1212
1398
  if (p.includes('kimi')) return { label: 'Kimi', cls: 'dsub-brandKimi', icon: '🌙' }
1213
1399
  if (p.includes('glm') || p.includes('zcode')) return { label: 'GLM', cls: 'dsub-brandGlm', icon: '⚡' }
1400
+ if (p.includes('copilot') || p.includes('github')) return { label: 'Copilot', cls: 'dsub-brandCodex', icon: '🐙' }
1401
+ if (p.includes('cursor')) return { label: 'Cursor', cls: 'dsub-brandCodex', icon: '💻' }
1402
+ if (p.includes('kiro')) return { label: 'Kiro', cls: 'dsub-brandAgy', icon: '☁️' }
1403
+ if (p.includes('qwen')) return { label: 'Qwen', cls: 'dsub-brandKimi', icon: '🌐' }
1404
+ if (p.includes('ernie') || p.includes('baidu')) return { label: 'ERNIE', cls: 'dsub-brandGlm', icon: '🐻' }
1405
+ if (p.includes('spark') || p.includes('xfyun')) return { label: 'Spark', cls: 'dsub-brandGrok', icon: '✨' }
1406
+ if (p.includes('jetbrains')) return { label: 'JetBrains', cls: 'dsub-brandClaude', icon: '🚀' }
1407
+ if (p.includes('perplexity')) return { label: 'Perplexity', cls: 'dsub-brandClaude', icon: '🔮' }
1408
+ if (p.includes('replit')) return { label: 'Replit', cls: 'dsub-brandCodex', icon: '⚡' }
1409
+ if (p.includes('cody') || p.includes('sourcegraph')) return { label: 'Cody', cls: 'dsub-brandAgy', icon: '🔍' }
1214
1410
  if (p.includes('ollama')) return { label: 'Ollama', cls: 'dsub-brandOllama', icon: '🦙' }
1215
1411
  return { label: (p.charAt(0).toUpperCase() || 'P'), cls: 'dsub-brandCodex', icon: '●' }
1216
1412
  }
@@ -0,0 +1,22 @@
1
+ export async function* coalesceStreamChunks(sourceStream, maxWaitMs = 40, maxChunkChars = 80) {
2
+ let buffer = ''
3
+ let lastFlush = Date.now()
4
+
5
+ for await (const chunk of sourceStream) {
6
+ const text = typeof chunk === 'string' ? chunk : (chunk && chunk.text) || ''
7
+ buffer += text
8
+
9
+ const now = Date.now()
10
+ if (buffer.length >= maxChunkChars || now - lastFlush >= maxWaitMs) {
11
+ if (buffer.length > 0) {
12
+ yield buffer
13
+ buffer = ''
14
+ lastFlush = now
15
+ }
16
+ }
17
+ }
18
+
19
+ if (buffer.length > 0) {
20
+ yield buffer
21
+ }
22
+ }
@@ -7,10 +7,10 @@ export const CODE_ASSIST_PROD = 'https://cloudcode-pa.googleapis.com/v1internal'
7
7
  export const CODE_ASSIST_STREAM = 'https://daily-cloudcode-pa.googleapis.com/v1internal'
8
8
  export const CODE_ASSIST = CODE_ASSIST_PROD
9
9
 
10
- function antigravityPlatform() {
11
- if (process.platform === 'win32') return 'WINDOWS'
12
- if (process.platform === 'darwin') return 'MACOS'
13
- return 'LINUX'
10
+ export function antigravityPlatform() {
11
+ if (process.platform === 'win32') return 'WINDOWS_AMD64'
12
+ if (process.platform === 'darwin') return process.arch === 'arm64' ? 'DARWIN_ARM64' : 'DARWIN_AMD64'
13
+ return process.arch === 'arm64' ? 'LINUX_ARM64' : 'LINUX_AMD64'
14
14
  }
15
15
 
16
16
  export function antigravityMetadata(projectId) {
@@ -0,0 +1,8 @@
1
+ export function extractBearerFromCookie(cookieHeader) {
2
+ if (!cookieHeader || typeof cookieHeader !== 'string') return null
3
+ const match = cookieHeader.match(/(?:__Secure-next-auth\.session-token|session_token|auth_token)=([^;]+)/)
4
+ if (match && match[1]) {
5
+ return decodeURIComponent(match[1])
6
+ }
7
+ return null
8
+ }
@@ -0,0 +1,22 @@
1
+ export function detectCodexBillingType(rawBilling) {
2
+ if (!rawBilling || typeof rawBilling !== 'object') {
3
+ return { plan: 'Free / Unknown', isHardLimit: false, isPrepaid: false }
4
+ }
5
+
6
+ const planId = String(rawBilling.plan || rawBilling.subscription_type || '').toLowerCase()
7
+ const hasHardLimit = Boolean(rawBilling.hard_limit_reached || rawBilling.is_blocked)
8
+ const isPrepaid = Boolean(rawBilling.has_payment_method || rawBilling.is_prepaid)
9
+
10
+ let label = 'Free'
11
+ if (planId.includes('team')) label = 'Team'
12
+ else if (planId.includes('pro')) label = 'Pro'
13
+ else if (planId.includes('plus')) label = 'Plus'
14
+ else if (planId.includes('enterprise')) label = 'Enterprise'
15
+
16
+ return {
17
+ plan: label,
18
+ isHardLimit: hasHardLimit,
19
+ isPrepaid,
20
+ remainingCredits: rawBilling.remaining_credits != null ? Number(rawBilling.remaining_credits) : null
21
+ }
22
+ }
@@ -0,0 +1,46 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync } from 'node:crypto'
2
+
3
+ const ALGORITHM = 'aes-256-gcm'
4
+ const SALT_LEN = 16
5
+ const IV_LEN = 12
6
+ const ITERATIONS = 100000
7
+
8
+ export function exportEncryptedBundle(data, password) {
9
+ const salt = randomBytes(SALT_LEN)
10
+ const key = pbkdf2Sync(password, salt, ITERATIONS, 32, 'sha256')
11
+ const iv = randomBytes(IV_LEN)
12
+ const cipher = createCipheriv(ALGORITHM, key, iv)
13
+
14
+ const plaintext = JSON.stringify(data)
15
+ let encrypted = cipher.update(plaintext, 'utf8', 'hex')
16
+ encrypted += cipher.final('hex')
17
+ const authTag = cipher.getAuthTag().toString('hex')
18
+
19
+ return {
20
+ version: 1,
21
+ algorithm: ALGORITHM,
22
+ salt: salt.toString('hex'),
23
+ iv: iv.toString('hex'),
24
+ authTag,
25
+ payload: encrypted
26
+ }
27
+ }
28
+
29
+ export function importEncryptedBundle(bundle, password) {
30
+ if (!bundle || bundle.version !== 1 || !bundle.payload) {
31
+ throw new Error('Invalid backup bundle format')
32
+ }
33
+
34
+ const salt = Buffer.from(bundle.salt, 'hex')
35
+ const key = pbkdf2Sync(password, salt, ITERATIONS, 32, 'sha256')
36
+ const iv = Buffer.from(bundle.iv, 'hex')
37
+ const authTag = Buffer.from(bundle.authTag, 'hex')
38
+
39
+ const decipher = createDecipheriv(ALGORITHM, key, iv)
40
+ decipher.setAuthTag(authTag)
41
+
42
+ let decrypted = decipher.update(bundle.payload, 'hex', 'utf8')
43
+ decrypted += decipher.final('utf8')
44
+
45
+ return JSON.parse(decrypted)
46
+ }
@@ -1,6 +1,8 @@
1
1
  import { readFile } from 'node:fs/promises'
2
2
  import { homedir } from 'node:os'
3
3
  import { join } from 'node:path'
4
+ import { readClaudeCredentials } from './vendors/claude-cli.js'
5
+ import { discoverCodingAgentConfigs } from './importers/coding-agents.js'
4
6
 
5
7
  function homeFile(...parts) {
6
8
  return join(homedir(), ...parts)
@@ -97,8 +99,8 @@ export async function discoverLocalCliSessions() {
97
99
  }
98
100
  }
99
101
 
100
-
101
- // 6. Cursor Token (env or CLI)
102
+ // 6. Cursor Token (env, CLI config or IDE state)
103
+ const cursorCliConfig = await readJson(homeFile('.cursor', 'cli-config.json'))
102
104
  if (process.env.CURSOR_ACCESS_TOKEN) {
103
105
  detected.cursor = {
104
106
  provider: 'cursor',
@@ -106,9 +108,15 @@ export async function discoverLocalCliSessions() {
106
108
  email: 'Cursor IDE User',
107
109
  hasRefreshToken: false,
108
110
  }
111
+ } else if (cursorCliConfig && (cursorCliConfig.authInfo || cursorCliConfig.serverConfigCache)) {
112
+ detected.cursor = {
113
+ provider: 'cursor',
114
+ path: homeFile('.cursor', 'cli-config.json'),
115
+ email: (cursorCliConfig.authInfo && cursorCliConfig.authInfo.email) || 'Cursor CLI User',
116
+ hasRefreshToken: false,
117
+ }
109
118
  }
110
119
 
111
-
112
120
  // 7. AWS Kiro
113
121
  const kiroPaths = [
114
122
  homeFile('.kiro', 'credentials.json'),
@@ -135,6 +143,46 @@ export async function discoverLocalCliSessions() {
135
143
  }
136
144
  }
137
145
 
146
+ // 8. Claude CLI Direct Import
147
+ const claudeCreds = await readClaudeCredentials()
148
+ if (claudeCreds) {
149
+ detected['claude-cli'] = {
150
+ provider: 'claude',
151
+ path: homeFile('.claude', 'credentials.json'),
152
+ email: 'Claude Code CLI User',
153
+ hasRefreshToken: !!claudeCreds.refreshToken,
154
+ }
155
+ }
156
+
157
+ // 9. GitHub Copilot token
158
+ if (process.env.GITHUB_COPILOT_TOKEN || process.env.GH_TOKEN) {
159
+ detected.copilot = {
160
+ provider: 'copilot',
161
+ path: 'GITHUB_COPILOT_TOKEN env',
162
+ email: 'GitHub Copilot User',
163
+ hasRefreshToken: false,
164
+ }
165
+ }
166
+
167
+ // 10. Coding Agents (Aider / Roo-Code)
168
+ const agentConfigs = await discoverCodingAgentConfigs()
169
+ if (agentConfigs.aider) {
170
+ detected.aider = {
171
+ provider: 'aider',
172
+ path: agentConfigs.aider.path,
173
+ email: 'Aider Configured Agent',
174
+ hasRefreshToken: false,
175
+ }
176
+ }
177
+ if (agentConfigs.roocode) {
178
+ detected.roocode = {
179
+ provider: 'roocode',
180
+ path: agentConfigs.roocode.path,
181
+ email: 'Roo-Code Configured Agent',
182
+ hasRefreshToken: false,
183
+ }
184
+ }
185
+
138
186
  return detected
139
187
  }
140
188
 
@@ -143,6 +191,27 @@ export async function loadLocalCliBlob(provider) {
143
191
  const info = discovered[provider]
144
192
  if (!info) throw new Error(`no local CLI session found for ${provider}`)
145
193
 
194
+ if (provider === 'claude-cli' || (provider === 'claude' && info.provider === 'claude')) {
195
+ const creds = await readClaudeCredentials()
196
+ if (!creds) throw new Error('Claude CLI credentials not found')
197
+ return {
198
+ accessToken: creds.accessToken,
199
+ refreshToken: creds.refreshToken,
200
+ expiresAt: creds.expiresAt,
201
+ email: 'Claude Code CLI User'
202
+ }
203
+ }
204
+
205
+ if (provider === 'copilot') {
206
+ const tok = process.env.GITHUB_COPILOT_TOKEN || process.env.GH_TOKEN || ''
207
+ return {
208
+ accessToken: tok,
209
+ refreshToken: tok,
210
+ expiresAt: Date.now() + 30 * 86400 * 1000,
211
+ email: 'GitHub Copilot User'
212
+ }
213
+ }
214
+
146
215
  const raw = await readJson(info.path)
147
216
  if (!raw) throw new Error(`failed to read local CLI file: ${info.path}`)
148
217
 
@@ -197,18 +266,16 @@ export async function loadLocalCliBlob(provider) {
197
266
  }
198
267
  }
199
268
 
200
-
201
269
  if (provider === 'cursor') {
202
- const token = process.env.CURSOR_ACCESS_TOKEN || (raw && (raw.accessToken || raw.access_token)) || ''
270
+ const token = process.env.CURSOR_ACCESS_TOKEN || (raw && (raw.accessToken || raw.access_token || (raw.authInfo && raw.authInfo.authId))) || ''
203
271
  return {
204
272
  accessToken: token,
205
273
  refreshToken: '',
206
274
  expiresAt: Date.now() + 30 * 86400 * 1000,
207
- email: 'Cursor User',
275
+ email: (raw && raw.authInfo && raw.authInfo.email) || 'Cursor User',
208
276
  }
209
277
  }
210
278
 
211
-
212
279
  if (provider === 'kiro') {
213
280
  const token = process.env.KIRO_API_KEY || (raw && (raw.accessToken || raw.access_token || raw.token)) || ''
214
281
  return {