dsh-agy 0.2.12 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -11
- package/cordis.patch.yml +4 -2
- package/lib/{accounts-DB1tT9Gy.mjs → accounts-CkaHhb5H.mjs} +1155 -646
- package/lib/accounts-CkaHhb5H.mjs.map +1 -0
- package/lib/{catalog-bfGIyoba.mjs → catalog-BgdDVJEB.mjs} +17 -2
- package/lib/{catalog-bfGIyoba.mjs.map → catalog-BgdDVJEB.mjs.map} +1 -1
- package/lib/cli/index.d.mts +10 -1
- package/lib/cli/index.d.mts.map +1 -1
- package/lib/cli/index.mjs +21 -8
- package/lib/cli/index.mjs.map +1 -1
- package/lib/client.js +2168 -14
- package/lib/{constants-BFKYjOmh.mjs → constants-Db4tmyfb.mjs} +79 -14
- package/lib/constants-Db4tmyfb.mjs.map +1 -0
- package/lib/{exchange-dQm2mjJ5.mjs → exchange-yZPIZumk.mjs} +27 -12
- package/lib/exchange-yZPIZumk.mjs.map +1 -0
- package/lib/identity-Ci8EGc1F.mjs +96 -0
- package/lib/identity-Ci8EGc1F.mjs.map +1 -0
- package/lib/{import-D7R0GpRr.mjs → import-BaFULQnH.mjs} +11 -7
- package/lib/import-BaFULQnH.mjs.map +1 -0
- package/lib/index.mjs +2 -2
- package/lib/{models-ClGTQBLY.mjs → models-BGuwl50d.mjs} +35 -22
- package/lib/models-BGuwl50d.mjs.map +1 -0
- package/lib/plugin-common-B1MG00JT.mjs +1313 -0
- package/lib/plugin-common-B1MG00JT.mjs.map +1 -0
- package/lib/{proxy-DLvX8yxv.mjs → proxy-DfaL73mL.mjs} +52 -3
- package/lib/proxy-DfaL73mL.mjs.map +1 -0
- package/lib/quota-summary-5CypVffd.mjs +127 -0
- package/lib/quota-summary-5CypVffd.mjs.map +1 -0
- package/lib/{translate-D3GFUpw4.mjs → translate-BQu8HLvB.mjs} +25 -6
- package/lib/translate-BQu8HLvB.mjs.map +1 -0
- package/lib/web/plugin.d.mts +1 -0
- package/lib/web/plugin.d.mts.map +1 -1
- package/lib/web/plugin.mjs +651 -1701
- package/lib/web/plugin.mjs.map +1 -1
- package/package.json +16 -4
- package/lib/accounts-DB1tT9Gy.mjs.map +0 -1
- package/lib/constants-BFKYjOmh.mjs.map +0 -1
- package/lib/exchange-dQm2mjJ5.mjs.map +0 -1
- package/lib/identity-DsftHTJC.mjs +0 -35
- package/lib/identity-DsftHTJC.mjs.map +0 -1
- package/lib/import-D7R0GpRr.mjs.map +0 -1
- package/lib/models-ClGTQBLY.mjs.map +0 -1
- package/lib/plugin-common-B63uavj7.mjs +0 -254
- package/lib/plugin-common-B63uavj7.mjs.map +0 -1
- package/lib/proxy-DLvX8yxv.mjs.map +0 -1
- package/lib/translate-D3GFUpw4.mjs.map +0 -1
package/lib/web/plugin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.mjs","names":[],"sources":["../../src/web/i18n.ts","../../src/web/page.ts","../../src/web/routes.ts","../../src/web/plugin.ts"],"sourcesContent":["/**\n * Bilingual UI dictionary for the dsh-agy web dashboard and OAuth callbacks.\n */\n\nexport interface I18nStrings {\n title: string\n subtitle: string\n refresh: string\n loginWithGoogle: string\n switchLang: string\n accountsList: string\n noAccounts: string\n noAccountsHint: string\n activeBadge: string\n stateActive: string\n stateCooling: string\n stateRateLimited: string\n stateVerificationRequired: string\n stateDisabled: string\n project: string\n defaultProject: string\n activate: string\n verify: string\n verifyCta: string\n testAll: string\n exportBlob: string\n fingerprint: string\n deleteAccount: string\n confirmDelete: string\n confirmTestAll: string\n confirmRegenerateFp: string\n quotaTitle: string\n quotaUnavailable: string\n noQuotaReported: string\n testModel: string\n testing: string\n copied: string\n importTitle: string\n importDesc: string\n importPlaceholder: string\n importJson: string\n importBlobBtn: string\n importSuccess: string\n exportAll: string\n importBatchResult: string\n copyAllMsg: string\n fingerprintTitle: string\n fingerprintDevice: string\n fingerprintUserAgent: string\n fingerprintCreated: string\n fingerprintHistory: string\n regenerateFp: string\n copyBlobMsg: string\n copyFpMsg: string\n allModelsOk: string\n modelsTestedSummary: string\n loginSuccessTitle: string\n loginSuccessDesc: string\n loginFailedTitle: string\n loginFailedDesc: string\n windowClosing: string\n closeWindow: string\n retryLogin: string\n resetIn: string\n resetAt: string\n}\n\nexport const I18N_DICT: { en: I18nStrings; zh: I18nStrings } = {\n en: {\n title: 'Antigravity Account Pool',\n subtitle: 'Manage multi-account rotation, quota monitoring, and credentials.',\n refresh: 'Refresh',\n loginWithGoogle: 'Login with Google',\n switchLang: '中文',\n accountsList: 'Accounts',\n noAccounts: 'No accounts configured',\n noAccountsHint: 'Click \"Login with Google\" or import credentials below to get started.',\n activeBadge: 'Active',\n stateActive: 'Healthy',\n stateCooling: 'Cooling Down',\n stateRateLimited: 'Rate Limited',\n stateVerificationRequired: 'Verification Required',\n stateDisabled: 'Disabled',\n project: 'Project',\n defaultProject: 'default',\n activate: 'Set Active',\n verify: 'Verify',\n verifyCta: 'Verify Now',\n testAll: 'Test All Models',\n exportBlob: 'Export Blob',\n fingerprint: 'Fingerprint',\n deleteAccount: 'Delete Account',\n confirmDelete: 'Are you sure you want to delete this account? This action cannot be undone.',\n confirmTestAll: 'Test All will execute real API calls on all models, consuming actual quota. Continue?',\n confirmRegenerateFp: 'Generate a fresh device fingerprint and user agent for this account?',\n quotaTitle: 'Model Quotas',\n quotaUnavailable: 'Quota information unavailable for this account',\n noQuotaReported: 'No quota reported by upstream',\n testModel: 'Test',\n testing: 'Testing...',\n copied: 'Copied to clipboard!',\n importTitle: 'Import Credentials',\n importDesc: 'Paste an agy auth.json token document, or a credential blob (from dsh-agy login --blob). Paste multiple blobs, one per line, for a batch import:',\n importPlaceholder: '{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} or dsh-agy-cred-v1.... (one blob per line for batch)',\n importJson: 'Import JSON',\n importBlobBtn: 'Import Blob',\n importSuccess: 'Successfully imported account: ',\n exportAll: 'Export All',\n importBatchResult: 'Imported {imported}, replaced {replaced}',\n copyAllMsg: 'All credential blobs exported. Copied to clipboard!',\n fingerprintTitle: 'Device Fingerprint',\n fingerprintDevice: 'Device ID',\n fingerprintUserAgent: 'User Agent',\n fingerprintCreated: 'Generated At',\n fingerprintHistory: 'Fingerprint Versions',\n regenerateFp: 'Regenerate Fingerprint',\n copyBlobMsg: 'Credential blob exported. Copied to clipboard!',\n copyFpMsg: 'Device fingerprint details loaded.',\n allModelsOk: 'All model tests passed!',\n modelsTestedSummary: 'Completed testing {total} models: {ok} passed, {failed} failed.',\n loginSuccessTitle: 'Sign-in Successful',\n loginSuccessDesc: 'Your Antigravity account has been authorized and saved.',\n loginFailedTitle: 'Sign-in Failed',\n loginFailedDesc: 'Unable to complete authorization. Error details:',\n windowClosing: 'This window will close automatically in a moment...',\n closeWindow: 'Close Window',\n retryLogin: 'Return to Dashboard',\n resetIn: 'Resets in ',\n resetAt: 'Resets at ',\n },\n zh: {\n title: 'Antigravity 账号池管理',\n subtitle: '多账号智能轮换、配额实时监控与凭据指纹管理。',\n refresh: '刷新',\n loginWithGoogle: 'Google 账号登录',\n switchLang: 'EN',\n accountsList: '账号列表',\n noAccounts: '暂无配置账号',\n noAccountsHint: '点击上方“Google 账号登录”或在下方粘贴凭据以添加账号。',\n activeBadge: '当前使用',\n stateActive: '正常',\n stateCooling: '冷却中',\n stateRateLimited: '已限流',\n stateVerificationRequired: '需要验证',\n stateDisabled: '已禁用',\n project: '项目 ID',\n defaultProject: '默认项目',\n activate: '设为默认',\n verify: '验证账号',\n verifyCta: '立即验证',\n testAll: '测试全部模型',\n exportBlob: '导出凭据 Blob',\n fingerprint: '设备指纹',\n deleteAccount: '删除账号',\n confirmDelete: '确定要删除此账号吗?该操作不可撤销。',\n confirmTestAll: '批量测试将对所有模型发起真实调用并消耗 API 配额,确认继续?',\n confirmRegenerateFp: '确定要为该账号重新生成全新的设备指纹与 User-Agent 吗?',\n quotaTitle: '模型配额池',\n quotaUnavailable: '无法获取该账号的模型配额信息',\n noQuotaReported: '上游未返回配额信息',\n testModel: '测试',\n testing: '测试中...',\n copied: '已复制到剪贴板!',\n importTitle: '导入凭据',\n importDesc: '粘贴 agy auth.json 文件内容,或输入凭据 blob(来自 dsh-agy login --blob)。多个 blob 每行一个即可批量导入:',\n importPlaceholder: '{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} 或 dsh-agy-cred-v1....(批量:每行一个 blob)',\n importJson: '导入 JSON',\n importBlobBtn: '导入 Blob',\n importSuccess: '成功导入账号:',\n exportAll: '导出全部',\n importBatchResult: '导入 {imported} 个,替换 {replaced} 个',\n copyAllMsg: '全部凭据 Blob 已导出并复制到剪贴板!',\n fingerprintTitle: '设备指纹信息',\n fingerprintDevice: '设备 ID (DeviceId)',\n fingerprintUserAgent: '用户代理 (User-Agent)',\n fingerprintCreated: '生成时间',\n fingerprintHistory: '历史版本数',\n regenerateFp: '重新生成指纹',\n copyBlobMsg: '凭据 Blob 已成功导出并复制到剪贴板!',\n copyFpMsg: '设备指纹信息已加载。',\n allModelsOk: '所有模型可用性测试通过!',\n modelsTestedSummary: '完成 {total} 个模型测试:{ok} 个可用,{failed} 个失败。',\n loginSuccessTitle: '授权登录成功',\n loginSuccessDesc: 'Antigravity 账号已成功接入并保存。',\n loginFailedTitle: '授权登录失败',\n loginFailedDesc: '无法完成 OAuth 授权,错误详情:',\n windowClosing: '此窗口即将在 2 秒内自动关闭...',\n closeWindow: '立即关闭窗口',\n retryLogin: '返回控制面板',\n resetIn: '将在 ',\n resetAt: '重置时间:',\n },\n}\n","/**\n * Web dashboard HTML/CSS/JS generator for dsh-agy.\n * Follows DeepSeek Harness (DSH) design system tokens and UX conventions.\n */\n\nimport { I18N_DICT } from './i18n.ts'\n\nexport function renderDashboardHtml(): string {\n const i18nJson = JSON.stringify(I18N_DICT)\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>agy — Antigravity Accounts</title>\n <style>\n /* DSH Design System Tokens snapshot (ui-theme / design-platform) */\n :root {\n color-scheme: light dark;\n --dsw-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;\n --ds-font-family-code: 'SF Mono', 'JetBrains Mono', 'Fira Code', Consolas, 'Liberation Mono', Menlo, Courier, monospace;\n --ds-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);\n --ds-transition-fast: 0.12s var(--ds-ease-in-out);\n --ds-transition-normal: 0.2s var(--ds-ease-in-out);\n\n /* Light Theme Defaults */\n --bg-page: #f8fafc;\n --bg-surface: #ffffff;\n --bg-surface-elevated: #f1f5f9;\n --bg-surface-hover: #f1f5f9;\n --bg-input: #ffffff;\n --border-l1: rgba(0, 0, 0, 0.06);\n --border-l2: rgba(0, 0, 0, 0.12);\n --border-l3: rgba(0, 0, 0, 0.2);\n --text-primary: #0f172a;\n --text-secondary: #475569;\n --text-tertiary: #94a3b8;\n --text-dimmed: #cbd5e1;\n\n --brand-primary: #4176e6;\n --brand-hover: #3362c4;\n --brand-surface: rgba(65, 118, 230, 0.1);\n \n --state-success: #22c55e;\n --state-success-bg: rgba(34, 197, 94, 0.12);\n --state-warn: #f59e0b;\n --state-warn-bg: rgba(245, 158, 11, 0.12);\n --state-error: #ef4444;\n --state-error-bg: rgba(239, 68, 68, 0.12);\n \n --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);\n --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);\n }\n\n @media (prefers-color-scheme: dark) {\n :root {\n --bg-page: #0f1115;\n --bg-surface: #171a21;\n --bg-surface-elevated: #20242e;\n --bg-surface-hover: #262b37;\n --bg-input: #12141a;\n --border-l1: rgba(255, 255, 255, 0.06);\n --border-l2: rgba(255, 255, 255, 0.12);\n --border-l3: rgba(255, 255, 255, 0.22);\n --text-primary: #f8fafc;\n --text-secondary: #94a3b8;\n --text-tertiary: #64748b;\n --text-dimmed: #475569;\n\n --brand-primary: #5686fe;\n --brand-hover: #4176e6;\n --brand-surface: rgba(86, 134, 254, 0.15);\n\n --state-success: #34d399;\n --state-success-bg: rgba(52, 211, 153, 0.15);\n --state-warn: #fbbf24;\n --state-warn-bg: rgba(251, 191, 36, 0.15);\n --state-error: #f87171;\n --state-error-bg: rgba(248, 113, 113, 0.15);\n\n --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.4);\n --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.5);\n --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.6);\n }\n }\n\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: var(--dsw-font-family);\n background-color: var(--bg-page);\n color: var(--text-primary);\n line-height: 1.5;\n font-size: 14px;\n padding: 24px 16px;\n -webkit-font-smoothing: antialiased;\n }\n .container {\n max-width: 1080px;\n margin: 0 auto;\n display: flex;\n flex-direction: column;\n gap: 20px;\n }\n\n /* Header */\n .header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n flex-wrap: wrap;\n gap: 12px;\n padding-bottom: 16px;\n border-bottom: 1px solid var(--border-l1);\n }\n .header-title-group h1 {\n font-size: 20px;\n font-weight: 600;\n letter-spacing: -0.01em;\n color: var(--text-primary);\n display: flex;\n align-items: center;\n gap: 8px;\n }\n .header-title-group p {\n font-size: 13px;\n color: var(--text-secondary);\n margin-top: 2px;\n }\n .header-actions {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n /* Buttons (DSH Capsule Geometry) */\n .btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n font-family: inherit;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n height: 32px;\n padding: 0 12px;\n border-radius: 16px;\n border: 1px solid transparent;\n cursor: pointer;\n transition: background-color var(--ds-transition-fast), border-color var(--ds-transition-fast), color var(--ds-transition-fast), opacity var(--ds-transition-fast);\n white-space: nowrap;\n user-select: none;\n }\n .btn:disabled {\n cursor: not-allowed;\n opacity: 0.45 !important;\n }\n .btn-sm {\n height: 26px;\n font-size: 12px;\n padding: 0 9px;\n border-radius: 13px;\n }\n .btn-primary {\n background: var(--brand-primary);\n color: #ffffff;\n }\n .btn-primary:hover:not(:disabled) {\n background: var(--brand-hover);\n }\n .btn-secondary {\n background: transparent;\n border-color: var(--border-l2);\n color: var(--text-primary);\n }\n .btn-secondary:hover:not(:disabled) {\n background: var(--bg-surface-hover);\n border-color: var(--border-l3);\n }\n .btn-cta {\n background: var(--state-warn-bg);\n border-color: var(--state-warn);\n color: var(--state-warn);\n font-weight: 600;\n animation: pulse-border 2s infinite;\n }\n .btn-cta:hover:not(:disabled) {\n background: var(--state-warn);\n color: #ffffff;\n }\n @keyframes pulse-border {\n 0%, 100% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4); }\n 50% { box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.15); }\n }\n .btn-danger {\n background: transparent;\n border-color: var(--border-l2);\n color: var(--state-error);\n }\n .btn-danger:hover:not(:disabled) {\n background: var(--state-error-bg);\n border-color: var(--state-error);\n }\n\n /* Master-Detail Layout */\n .main-grid {\n display: grid;\n grid-template-columns: 290px 1fr;\n gap: 20px;\n align-items: start;\n }\n @media (max-width: 780px) {\n .main-grid {\n grid-template-columns: 1fr;\n }\n }\n\n /* Cards */\n .card {\n background: var(--bg-surface);\n border: 1px solid var(--border-l2);\n border-radius: 12px;\n padding: 16px;\n box-shadow: var(--shadow-sm);\n }\n .card-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--text-primary);\n margin-bottom: 12px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n\n /* Account List (Master) */\n .account-list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n }\n .account-item {\n padding: 10px 12px;\n border-radius: 8px;\n border: 1px solid var(--border-l1);\n background: var(--bg-surface);\n cursor: pointer;\n transition: all var(--ds-transition-fast);\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n .account-item:hover {\n background: var(--bg-surface-hover);\n border-color: var(--border-l2);\n }\n .account-item.active-item {\n background: var(--brand-surface);\n border-color: var(--brand-primary);\n }\n .account-item-top {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 6px;\n }\n .account-email {\n font-weight: 500;\n font-size: 13px;\n color: var(--text-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .account-item-bottom {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n }\n\n /* Badges & Pills */\n .badge {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 11px;\n font-weight: 500;\n line-height: 16px;\n padding: 1px 7px;\n border-radius: 10px;\n }\n .badge-primary { background: var(--brand-surface); color: var(--brand-primary); }\n .badge-success { background: var(--state-success-bg); color: var(--state-success); }\n .badge-warn { background: var(--state-warn-bg); color: var(--state-warn); }\n .badge-error { background: var(--state-error-bg); color: var(--state-error); }\n .badge-neutral { background: var(--bg-surface-elevated); color: var(--text-secondary); }\n\n .dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n display: inline-block;\n flex-shrink: 0;\n }\n\n /* Account Detail View */\n .detail-head {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n gap: 12px;\n flex-wrap: wrap;\n padding-bottom: 14px;\n margin-bottom: 14px;\n border-bottom: 1px solid var(--border-l1);\n }\n .detail-meta h2 {\n font-size: 16px;\n font-weight: 600;\n display: flex;\n align-items: center;\n gap: 8px;\n }\n .detail-meta p {\n font-size: 12px;\n color: var(--text-secondary);\n margin-top: 4px;\n }\n .detail-actions {\n display: flex;\n gap: 6px;\n flex-wrap: wrap;\n }\n\n /* Quota Section */\n .quota-table-wrap {\n overflow-x: auto;\n margin-top: 8px;\n }\n .quota-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n }\n .quota-table th, .quota-table td {\n padding: 8px 10px;\n text-align: left;\n border-bottom: 1px solid var(--border-l1);\n }\n .quota-table th {\n font-size: 12px;\n font-weight: 500;\n color: var(--text-tertiary);\n text-transform: uppercase;\n letter-spacing: 0.03em;\n }\n .model-name {\n font-family: var(--ds-font-family-code);\n font-weight: 500;\n font-size: 12.5px;\n color: var(--text-primary);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n }\n .model-name:hover {\n color: var(--brand-primary);\n text-decoration: underline;\n }\n .progress-bar {\n background: var(--bg-surface-elevated);\n border-radius: 4px;\n height: 8px;\n width: 110px;\n overflow: hidden;\n display: inline-block;\n vertical-align: middle;\n margin-right: 8px;\n }\n .progress-fill {\n height: 100%;\n border-radius: 4px;\n transition: width var(--ds-transition-normal);\n }\n .reset-time {\n font-size: 12px;\n color: var(--text-tertiary);\n white-space: nowrap;\n }\n\n /* Collapsible diagnostics */\n details.diagnostics {\n margin-top: 14px;\n border: 1px solid var(--border-l1);\n border-radius: 8px;\n background: var(--bg-surface-elevated);\n overflow: hidden;\n }\n details.diagnostics summary {\n padding: 8px 12px;\n font-size: 12.5px;\n font-weight: 500;\n color: var(--text-secondary);\n cursor: pointer;\n user-select: none;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n details.diagnostics summary:hover {\n color: var(--text-primary);\n }\n .diagnostics-body {\n padding: 12px;\n border-top: 1px solid var(--border-l1);\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n pre.code-view {\n font-family: var(--ds-font-family-code);\n font-size: 12px;\n background: var(--bg-input);\n border: 1px solid var(--border-l1);\n border-radius: 6px;\n padding: 10px;\n overflow-x: auto;\n word-break: break-all;\n white-space: pre-wrap;\n color: var(--text-secondary);\n }\n\n /* Import Form */\n .import-section {\n margin-top: 4px;\n }\n .import-textarea {\n width: 100%;\n font-family: var(--ds-font-family-code);\n font-size: 12.5px;\n padding: 10px 12px;\n background: var(--bg-input);\n border: 1px solid var(--border-l2);\n border-radius: 8px;\n color: var(--text-primary);\n resize: vertical;\n min-height: 70px;\n outline: none;\n transition: border-color var(--ds-transition-fast);\n margin-bottom: 8px;\n }\n .import-textarea:focus {\n border-color: var(--brand-primary);\n box-shadow: 0 0 0 2px var(--brand-surface);\n }\n\n /* Toast Notification System */\n #toast-container {\n position: fixed;\n bottom: 20px;\n right: 20px;\n z-index: 1000;\n display: flex;\n flex-direction: column;\n gap: 8px;\n max-width: 380px;\n pointer-events: none;\n }\n .toast {\n background: var(--bg-surface);\n color: var(--text-primary);\n border: 1px solid var(--border-l2);\n border-radius: 8px;\n padding: 10px 14px;\n font-size: 13px;\n box-shadow: var(--shadow-lg);\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n pointer-events: auto;\n animation: toast-in 0.2s var(--ds-ease-in-out);\n transition: opacity 0.2s, transform 0.2s;\n }\n .toast-success { border-left: 4px solid var(--state-success); }\n .toast-error { border-left: 4px solid var(--state-error); }\n .toast-info { border-left: 4px solid var(--brand-primary); }\n @keyframes toast-in {\n from { transform: translateY(12px); opacity: 0; }\n to { transform: translateY(0); opacity: 1; }\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <!-- Header -->\n <header class=\"header\">\n <div class=\"header-title-group\">\n <h1>\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"color:var(--brand-primary)\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle><path d=\"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20\"></path><path d=\"M2 12h20\"></path></svg>\n <span id=\"txt-title\">Antigravity Account Pool</span>\n </h1>\n <p id=\"txt-subtitle\">Manage multi-account rotation, quota monitoring, and credentials.</p>\n </div>\n <div class=\"header-actions\">\n <button id=\"btn-lang\" class=\"btn btn-secondary btn-sm\" title=\"Toggle Language\">中文</button>\n <button id=\"btn-refresh\" class=\"btn btn-secondary\" title=\"Refresh accounts\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\"></path><path d=\"M21 3v5h-5\"></path><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\"></path><path d=\"M8 16H3v5\"></path></svg>\n <span id=\"txt-refresh\">Refresh</span>\n </button>\n <button id=\"btn-login\" class=\"btn btn-primary\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\" fill=\"#4285F4\"/><path d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\" fill=\"#34A853\"/><path d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.06H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.94l2.85-2.22.81-.63z\" fill=\"#FBBC05\"/><path d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.06l3.66 2.84c.87-2.6 3.3-4.52 6.16-4.52z\" fill=\"#EA4335\"/></svg>\n <span id=\"txt-login\">Login with Google</span>\n </button>\n </div>\n </header>\n\n <!-- Main Workspace -->\n <main class=\"main-grid\">\n <!-- Master: Account List -->\n <section class=\"card\">\n <div class=\"card-title\">\n <span id=\"txt-accounts-list\">Accounts</span>\n <span id=\"account-total-badge\" class=\"badge badge-neutral\">0</span>\n </div>\n <ul id=\"account-list\" class=\"account-list\">\n <li style=\"color:var(--text-tertiary);font-size:13px;padding:8px 0;\" id=\"txt-loading\">Loading accounts...</li>\n </ul>\n </section>\n\n <!-- Detail: Selected Account Details -->\n <section class=\"card\" id=\"detail-card\">\n <div id=\"detail-content\">\n <p style=\"color:var(--text-tertiary);\" id=\"txt-no-accounts-hint\">Select or add an account to view details.</p>\n </div>\n </section>\n </main>\n\n <!-- Bottom: Import Section -->\n <section class=\"card import-section\">\n <div class=\"card-title\">\n <span id=\"txt-import-title\">Import Credentials</span>\n </div>\n <p style=\"font-size:12.5px;color:var(--text-secondary);margin-bottom:8px;\" id=\"txt-import-desc\">\n Paste an agy auth.json token document, or a credential blob (from dsh-agy login --blob). Paste multiple blobs, one per line, for a batch import:\n </p>\n <textarea id=\"input-import\" class=\"import-textarea\" placeholder='{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} or dsh-agy-cred-v1.... (one blob per line for batch)'></textarea>\n <div style=\"display:flex;gap:8px;\">\n <button id=\"btn-import-json\" class=\"btn btn-secondary\">\n <span id=\"txt-import-json\">Import JSON</span>\n </button>\n <button id=\"btn-import-blob\" class=\"btn btn-secondary\">\n <span id=\"txt-import-blob\">Import Blob</span>\n </button>\n <span style=\"flex:1\"></span>\n <button id=\"btn-export-all\" class=\"btn btn-secondary\">\n <span id=\"txt-export-all\">Export All</span>\n </button>\n </div>\n </section>\n </div>\n\n <div id=\"toast-container\"></div>\n\n <script>\n const I18N = ${i18nJson};\n let currentLang = localStorage.getItem('agy_lang') || (navigator.language.startsWith('zh') ? 'zh' : 'en');\n let accountData = [];\n let selectedIndex = 0;\n let isBusy = false;\n\n function t(key) {\n const dict = I18N[currentLang] || I18N.en;\n return dict[key] || I18N.en[key] || key;\n }\n\n const $ = (id) => document.getElementById(id);\n\n function esc(s) {\n if (s == null) return '';\n return String(s).replace(/[&<>\"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[c]));\n }\n\n function toast(text, type = 'info') {\n const container = $('toast-container');\n const el = document.createElement('div');\n el.className = 'toast toast-' + type;\n el.innerHTML = '<span>' + esc(text) + '</span><span style=\"cursor:pointer;opacity:0.6;font-weight:600;\" onclick=\"this.parentElement.remove()\">✕</span>';\n container.appendChild(el);\n setTimeout(() => {\n el.style.opacity = '0';\n el.style.transform = 'translateY(8px)';\n setTimeout(() => el.remove(), 200);\n }, 3500);\n }\n\n function copyToClipboard(text, msg) {\n if (navigator.clipboard && window.isSecureContext) {\n navigator.clipboard.writeText(text).then(() => toast(msg || t('copied'), 'success')).catch(() => fallbackCopy(text, msg));\n } else {\n fallbackCopy(text, msg);\n }\n }\n\n function fallbackCopy(text, msg) {\n const ta = document.createElement('textarea');\n ta.value = text;\n ta.style.position = 'fixed';\n ta.style.left = '-9999px';\n document.body.appendChild(ta);\n ta.select();\n try {\n document.execCommand('copy');\n toast(msg || t('copied'), 'success');\n } catch {\n toast('Failed to copy', 'error');\n }\n document.body.removeChild(ta);\n }\n\n function formatRelativeTime(dateStr) {\n if (!dateStr) return '';\n const target = new Date(dateStr).getTime();\n const diffMs = target - Date.now();\n if (diffMs <= 0) return '';\n const diffMin = Math.round(diffMs / 60000);\n if (diffMin < 60) return t('resetIn') + diffMin + 'm';\n const diffHours = (diffMs / 3600000).toFixed(1);\n if (diffHours < 24) return t('resetIn') + diffHours + 'h';\n return t('resetAt') + new Date(target).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });\n }\n\n function quotaColor(fraction) {\n if (fraction == null) return 'var(--text-dimmed)';\n return fraction > 0.7 ? 'var(--state-success)' : fraction >= 0.3 ? 'var(--state-warn)' : 'var(--state-error)';\n }\n\n function getWorstQuotaFraction(acc) {\n const models = (acc.quota && acc.quota.models) || [];\n let worst = 1;\n let hasQuota = false;\n for (const m of models) {\n if (m.remainingFraction != null) {\n worst = Math.min(worst, m.remainingFraction);\n hasQuota = true;\n }\n }\n return hasQuota ? worst : null;\n }\n\n function applyI18nLabels() {\n $('txt-title').textContent = t('title');\n $('txt-subtitle').textContent = t('subtitle');\n $('txt-refresh').textContent = t('refresh');\n $('txt-login').textContent = t('loginWithGoogle');\n $('btn-lang').textContent = t('switchLang');\n $('txt-accounts-list').textContent = t('accountsList');\n $('txt-import-title').textContent = t('importTitle');\n $('txt-import-desc').textContent = t('importDesc');\n $('txt-import-json').textContent = t('importJson');\n $('txt-import-blob').textContent = t('importBlobBtn');\n $('txt-export-all').textContent = t('exportAll');\n $('input-import').placeholder = t('importPlaceholder');\n }\n\n async function api(path, init) {\n const res = await fetch('/agy/api' + path, {\n headers: { 'content-type': 'application/json' },\n ...init\n });\n const data = await res.json().catch(() => ({}));\n if (!res.ok) throw new Error(data.error || res.statusText || 'Request failed');\n return data;\n }\n\n function renderMasterList() {\n const listEl = $('account-list');\n $('account-total-badge').textContent = String(accountData.length);\n\n if (accountData.length === 0) {\n listEl.innerHTML = '<li style=\"color:var(--text-tertiary);font-size:13px;padding:12px 0;\">' + esc(t('noAccounts')) + '</li>';\n return;\n }\n\n listEl.innerHTML = accountData.map((a, i) => {\n const isSelected = i === selectedIndex;\n const worstFrac = getWorstQuotaFraction(a);\n const dotColor = quotaColor(worstFrac);\n\n let stateBadge = '';\n if (a.state === 'disabled') {\n stateBadge = '<span class=\"badge badge-error\">' + esc(t('stateDisabled')) + '</span>';\n } else if (a.state === 'verification-required') {\n stateBadge = '<span class=\"badge badge-warn\">' + esc(t('stateVerificationRequired')) + '</span>';\n } else if (a.state === 'cooling') {\n stateBadge = '<span class=\"badge badge-warn\">' + esc(t('stateCooling')) + '</span>';\n } else if (a.rateLimits && Object.values(a.rateLimits).some((t) => t > Date.now())) {\n stateBadge = '<span class=\"badge badge-warn\">' + esc(t('stateRateLimited')) + '</span>';\n } else {\n stateBadge = '<span class=\"badge badge-success\">' + esc(t('stateActive')) + '</span>';\n }\n\n const activeMark = a.active ? '<span class=\"badge badge-primary\">★ ' + esc(t('activeBadge')) + '</span>' : '';\n\n return '<li class=\"account-item ' + (isSelected ? 'active-item' : '') + '\" onclick=\"selectAccount(' + i + ')\">' +\n '<div class=\"account-item-top\">' +\n '<span class=\"account-email\" title=\"' + esc(a.email || '(no email)') + '\">' + esc(a.email || '(no email)') + '</span>' +\n '<span class=\"dot\" style=\"background:' + dotColor + '\" title=\"Quota Health\"></span>' +\n '</div>' +\n '<div class=\"account-item-bottom\">' + activeMark + stateBadge + '</div>' +\n '</li>';\n }).join('');\n }\n\n function renderDetailView() {\n const container = $('detail-content');\n if (accountData.length === 0) {\n container.innerHTML = '<div style=\"text-align:center;padding:32px 12px;color:var(--text-tertiary);\">' +\n '<p style=\"font-size:15px;font-weight:500;margin-bottom:4px;\">' + esc(t('noAccounts')) + '</p>' +\n '<p style=\"font-size:13px;\">' + esc(t('noAccountsHint')) + '</p>' +\n '</div>';\n return;\n }\n\n const a = accountData[selectedIndex] || accountData[0];\n const isVerificationRequired = a.state === 'verification-required';\n const isCooling = a.state === 'cooling' && a.cooldownUntil;\n const coolInfo = isCooling ? ' (' + formatRelativeTime(a.cooldownUntil) + ')' : '';\n\n const models = (a.quota && a.quota.models) || [];\n const quotaRows = models.map((m) => {\n const pct = m.remainingFraction != null ? Math.round(m.remainingFraction * 100) : null;\n const color = quotaColor(m.remainingFraction);\n const resetText = m.resetTime ? formatRelativeTime(m.resetTime) : '';\n const exactReset = m.resetTime ? new Date(m.resetTime).toLocaleString() : '';\n\n return '<tr>' +\n '<td><span class=\"model-name\" onclick=\"copyToClipboard(\\\\'' + esc(m.id) + '\\\\')\" title=\"Click to copy ID\">' + esc(m.id) + '</span></td>' +\n '<td>' +\n (pct != null\n ? '<div class=\"progress-bar\"><div class=\"progress-fill\" style=\"width:' + pct + '%;background:' + color + '\"></div></div><span style=\"font-weight:500;font-size:12px;\">' + pct + '%</span>'\n : '<span style=\"color:var(--text-tertiary);font-size:12px;\">' + esc(t('noQuotaReported')) + '</span>') +\n '</td>' +\n '<td><span class=\"reset-time\" title=\"' + esc(exactReset) + '\">' + esc(resetText) + '</span></td>' +\n '<td style=\"text-align:right;\">' +\n '<button class=\"btn btn-secondary btn-sm\" data-test-model=\"' + esc(m.id) + '\">' + esc(t('testModel')) + '</button>' +\n '</td>' +\n '</tr>';\n }).join('');\n\n container.innerHTML = \n '<div class=\"detail-head\">' +\n '<div class=\"detail-meta\">' +\n '<h2>' + esc(a.email || '(no email)') + (a.active ? ' <span class=\"badge badge-primary\">★ ' + esc(t('activeBadge')) + '</span>' : '') + '</h2>' +\n '<p>' + esc(t('project')) + ': <code>' + esc(a.projectId || t('defaultProject')) + '</code>' + esc(coolInfo) + '</p>' +\n '</div>' +\n '<div class=\"detail-actions\">' +\n (isVerificationRequired ? '<button class=\"btn btn-cta btn-sm\" id=\"btn-action-verify\">⚡ ' + esc(t('verifyCta')) + '</button>' : '') +\n (!a.active ? '<button class=\"btn btn-secondary btn-sm\" id=\"btn-action-activate\">' + esc(t('activate')) + '</button>' : '') +\n (!isVerificationRequired ? '<button class=\"btn btn-secondary btn-sm\" id=\"btn-action-verify\">' + esc(t('verify')) + '</button>' : '') +\n '<button class=\"btn btn-secondary btn-sm\" id=\"btn-action-export\">' + esc(t('exportBlob')) + '</button>' +\n '<button class=\"btn btn-secondary btn-sm\" id=\"btn-action-fp\">' + esc(t('fingerprint')) + '</button>' +\n '<button class=\"btn btn-danger btn-sm\" id=\"btn-action-delete\">' + esc(t('deleteAccount')) + '</button>' +\n '</div>' +\n '</div>' +\n\n '<div style=\"margin-top:14px;padding:10px 12px;border:1px solid var(--border-l1);border-radius:8px;background:var(--bg-surface-elevated);display:flex;flex-wrap:wrap;gap:8px;align-items:center;\">' +\n '<span style=\"font-size:13px;font-weight:500;white-space:nowrap;\">Proxy:</span>' +\n '<input id=\"proxy-input-' + selectedIndex + '\" placeholder=\"socks5://user:pass@host:1080\" style=\"flex:1;min-width:220px;font-family:var(--ds-font-family-code);font-size:12.5px;padding:6px 10px;background:var(--bg-input);border:1px solid var(--border-l2);border-radius:6px;color:var(--text-primary);outline:none;\">' +\n '<button class=\"btn btn-primary btn-sm\" id=\"btn-proxy-save\">Save</button>' +\n '<button class=\"btn btn-secondary btn-sm\" id=\"btn-proxy-clear\">Clear</button>' +\n '<button class=\"btn btn-secondary btn-sm\" id=\"btn-proxy-test\">Test</button>' +\n '<span id=\"proxy-status\" style=\"font-size:12px;color:var(--text-secondary);\">' + ((a.proxy || a.proxyMasked) ? 'Current: ' + esc(a.proxy || a.proxyMasked) : 'No proxy (using env)') + '</span>' +\n '</div>' +\n\n '<div style=\"margin-top:16px;\">' +\n '<div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;\">' +\n '<h3 style=\"font-size:13.5px;font-weight:600;\">' + esc(t('quotaTitle')) + ' (' + models.length + ')</h3>' +\n (models.length > 0 ? '<button class=\"btn btn-secondary btn-sm\" id=\"btn-action-test-all\">⚡ ' + esc(t('testAll')) + '</button>' : '') +\n '</div>' +\n '<div class=\"quota-table-wrap\">' +\n '<table class=\"quota-table\">' +\n '<thead><tr><th>Model</th><th>Remaining</th><th>Reset</th><th style=\"text-align:right;\">Action</th></tr></thead>' +\n '<tbody>' + (quotaRows || '<tr><td colspan=\"4\" style=\"color:var(--text-tertiary);text-align:center;padding:16px 0;\">' + esc(t('quotaUnavailable')) + '</td></tr>') + '</tbody>' +\n '</table>' +\n '</div>' +\n '</div>' +\n\n '<details class=\"diagnostics\" id=\"diagnostics-panel\">' +\n '<summary><span>' + esc(t('fingerprintTitle')) + ' & Output</span><span>▼</span></summary>' +\n '<div class=\"diagnostics-body\">' +\n '<div style=\"display:flex;justify-content:space-between;align-items:center;\">' +\n '<span style=\"font-size:12px;color:var(--text-secondary);\">' + esc(t('fingerprintHistory')) + ': ' + (a.fingerprintHistory || 0) + '</span>' +\n '<button class=\"btn btn-secondary btn-sm\" id=\"btn-action-regen-fp\">' + esc(t('regenerateFp')) + '</button>' +\n '</div>' +\n '<pre class=\"code-view\" id=\"diagnostics-output\">' + (a.fingerprint ? esc(JSON.stringify(a.fingerprint, null, 2)) : 'No diagnostics generated yet.') + '</pre>' +\n '</div>' +\n '</details>';\n\n bindDetailActions(selectedIndex);\n }\n\n function selectAccount(index) {\n selectedIndex = index;\n renderMasterList();\n renderDetailView();\n }\n\n function bindDetailActions(index) {\n const btnActivate = $('btn-action-activate');\n if (btnActivate) btnActivate.onclick = async () => {\n try {\n await api('/activate', { method: 'POST', body: JSON.stringify({ index }) });\n toast(t('activeBadge') + ' -> #' + (index + 1), 'success');\n render();\n } catch (e) { toast(String(e), 'error'); }\n };\n\n const btnVerify = $('btn-action-verify');\n if (btnVerify) btnVerify.onclick = async () => {\n btnVerify.disabled = true;\n try {\n await api('/verify', { method: 'POST', body: JSON.stringify({ index }) });\n toast('Account #' + (index + 1) + ' verified successfully', 'success');\n render();\n } catch (e) { toast(String(e), 'error'); }\n finally { btnVerify.disabled = false; }\n };\n\n const btnDelete = $('btn-action-delete');\n if (btnDelete) btnDelete.onclick = async () => {\n if (!confirm(t('confirmDelete'))) return;\n try {\n await api('/delete', { method: 'POST', body: JSON.stringify({ index }) });\n toast('Account deleted', 'success');\n if (selectedIndex >= accountData.length - 1) selectedIndex = Math.max(0, accountData.length - 2);\n render();\n } catch (e) { toast(String(e), 'error'); }\n };\n\n const btnExport = $('btn-action-export');\n if (btnExport) btnExport.onclick = async () => {\n try {\n const r = await api('/export', { method: 'POST', body: JSON.stringify({ index }) });\n copyToClipboard(r.blob, t('copyBlobMsg'));\n $('diagnostics-panel').open = true;\n $('diagnostics-output').textContent = r.blob;\n } catch (e) { toast(String(e), 'error'); }\n };\n\n const btnFp = $('btn-action-fp');\n if (btnFp) btnFp.onclick = async () => {\n try {\n const r = await api('/fingerprint', { method: 'POST', body: JSON.stringify({ index }) });\n $('diagnostics-panel').open = true;\n $('diagnostics-output').textContent = JSON.stringify(r.fingerprint, null, 2);\n toast(t('copyFpMsg'), 'info');\n } catch (e) { toast(String(e), 'error'); }\n };\n\n const btnRegenFp = $('btn-action-regen-fp');\n if (btnRegenFp) btnRegenFp.onclick = async () => {\n if (!confirm(t('confirmRegenerateFp'))) return;\n try {\n const r = await api('/fingerprint', { method: 'POST', body: JSON.stringify({ index, action: 'regenerate' }) });\n $('diagnostics-panel').open = true;\n $('diagnostics-output').textContent = JSON.stringify(r.fingerprint, null, 2);\n toast('Fingerprint regenerated', 'success');\n render();\n } catch (e) { toast(String(e), 'error'); }\n };\n\n const btnTestAll = $('btn-action-test-all');\n if (btnTestAll) btnTestAll.onclick = async () => {\n if (!confirm(t('confirmTestAll'))) return;\n btnTestAll.disabled = true;\n const a = accountData[index];\n const models = ((a.quota && a.quota.models) || []).map((m) => m.id);\n toast(t('testing') + ' (' + models.length + ')', 'info');\n let okCount = 0;\n let failCount = 0;\n const lines = [];\n\n for (const model of models) {\n try {\n const r = await api('/test', { method: 'POST', body: JSON.stringify({ model }) });\n if (r.ok) {\n okCount++;\n lines.push('OK ' + model);\n } else {\n failCount++;\n lines.push('FAIL ' + model + ' - ' + r.error);\n }\n } catch (err) {\n failCount++;\n lines.push('ERR ' + model + ' - ' + String(err));\n }\n }\n $('diagnostics-panel').open = true;\n $('diagnostics-output').textContent = lines.join('\\\\n');\n toast(t('modelsTestedSummary').replace('{total}', String(models.length)).replace('{ok}', String(okCount)).replace('{failed}', String(failCount)), failCount === 0 ? 'success' : 'warn');\n btnTestAll.disabled = false;\n };\n\n // Proxy actions\n const btnProxySave = $('btn-proxy-save');\n if (btnProxySave) btnProxySave.onclick = async () => {\n const input = $('proxy-input-' + index);\n const proxy = input ? input.value.trim() : '';\n btnProxySave.disabled = true;\n try {\n const r = await api('/proxy', { method: 'POST', body: JSON.stringify({ index, proxy }) });\n const masked = r.proxy || r.proxyMasked || '';\n const statusEl = $('proxy-status');\n if (statusEl) statusEl.textContent = masked ? 'Current: ' + masked : 'No proxy (using env)';\n toast(masked ? 'Proxy saved: ' + masked : 'Proxy cleared (using env)', 'success');\n render();\n } catch (e) { toast(String(e), 'error'); }\n finally { btnProxySave.disabled = false; }\n };\n const btnProxyClear = $('btn-proxy-clear');\n if (btnProxyClear) btnProxyClear.onclick = async () => {\n btnProxyClear.disabled = true;\n try {\n await api('/proxy', { method: 'POST', body: JSON.stringify({ index, proxy: '' }) });\n const statusEl = $('proxy-status');\n if (statusEl) statusEl.textContent = 'No proxy (using env)';\n const input = $('proxy-input-' + index);\n if (input) input.value = '';\n toast('Proxy cleared (using env)', 'success');\n render();\n } catch (e) { toast(String(e), 'error'); }\n finally { btnProxyClear.disabled = false; }\n };\n const btnProxyTest = $('btn-proxy-test');\n if (btnProxyTest) btnProxyTest.onclick = async () => {\n const input = $('proxy-input-' + index);\n const val = input ? input.value.trim() : '';\n const body = val ? { index, proxy: val } : { index };\n btnProxyTest.disabled = true;\n btnProxyTest.textContent = '...';\n try {\n const r = await api('/proxy/test', { method: 'POST', body: JSON.stringify(body) });\n const masked = r.masked ? esc(r.masked) : '';\n const statusEl = $('proxy-status');\n if (r.ok) {\n toast('Proxy reachable: ' + (r.masked || ''), 'success');\n if (statusEl) statusEl.textContent = 'Reachable: ' + (r.masked || '');\n } else {\n toast('Proxy unreachable: ' + (r.masked || '') + (r.error ? ' - ' + r.error : ''), 'error');\n if (statusEl) statusEl.textContent = 'Unreachable: ' + (r.masked || '') + (r.error ? ' (' + r.error + ')' : '');\n }\n } catch (e) { toast(String(e), 'error'); }\n finally { btnProxyTest.disabled = false; btnProxyTest.textContent = 'Test'; }\n };\n\n // Single model tests\n document.querySelectorAll('[data-test-model]').forEach((btn) => {\n btn.onclick = async () => {\n const model = btn.getAttribute('data-test-model');\n btn.disabled = true;\n btn.textContent = '...';\n try {\n const r = await api('/test', { method: 'POST', body: JSON.stringify({ model }) });\n if (r.ok) {\n toast('OK: ' + model, 'success');\n } else {\n toast('FAIL: ' + model + ' - ' + r.error, 'error');\n }\n } catch (err) {\n toast('ERR: ' + model + ' - ' + String(err), 'error');\n } finally {\n btn.disabled = false;\n btn.textContent = t('testModel');\n }\n };\n });\n }\n\n async function render() {\n if (isBusy) return;\n isBusy = true;\n try {\n const data = await api('/accounts');\n accountData = data.accounts || [];\n if (selectedIndex >= accountData.length) selectedIndex = Math.max(0, accountData.length - 1);\n renderMasterList();\n renderDetailView();\n } catch (e) {\n toast(String(e), 'error');\n } finally {\n isBusy = false;\n }\n }\n\n // Event Handlers\n $('btn-refresh').onclick = render;\n\n $('btn-lang').onclick = () => {\n currentLang = currentLang === 'zh' ? 'en' : 'zh';\n localStorage.setItem('agy_lang', currentLang);\n applyI18nLabels();\n renderMasterList();\n renderDetailView();\n };\n\n $('btn-login').onclick = async () => {\n try {\n const { url } = await api('/auth-url', { method: 'POST' });\n window.open(url, '_blank');\n toast(t('windowClosing'), 'info');\n } catch (e) { toast(String(e), 'error'); }\n };\n\n const importLines = () => $('input-import').value.split('\\\\n')\n .map((s) => s.trim())\n .filter(Boolean);\n\n const doImport = async (kind) => {\n const sources = importLines();\n if (sources.length === 0) return;\n try {\n const r = await api('/import', { method: 'POST', body: JSON.stringify({ kind, sources }) });\n if (r.errors && r.errors.length > 0) toast(r.errors[0], 'error');\n toast(t('importBatchResult').replace('{imported}', r.imported).replace('{replaced}', r.replaced), 'success');\n $('input-import').value = '';\n render();\n } catch (e) { toast(String(e), 'error'); }\n };\n\n $('btn-import-json').onclick = () => doImport('json');\n $('btn-import-blob').onclick = () => doImport('blob');\n\n const btnExportAll = $('btn-export-all');\n if (btnExportAll) btnExportAll.onclick = async () => {\n try {\n const r = await api('/export-all', { method: 'POST' });\n if (!r.blobs || r.blobs.length === 0) { toast('No accounts to export', 'info'); return; }\n const text = r.blobs.map((b) => b.blob).join('\\\\n');\n copyToClipboard(text, t('copyAllMsg'));\n $('diagnostics-panel').open = true;\n $('diagnostics-output').textContent = text;\n } catch (e) { toast(String(e), 'error'); }\n };\n\n // Auto-refresh when tab receives focus or receives postMessage from login popup\n window.addEventListener('focus', render);\n window.addEventListener('message', (ev) => {\n if (ev.data && ev.data.type === 'agy_login_success') {\n toast(t('loginSuccessTitle'), 'success');\n render();\n }\n });\n\n // Init\n applyI18nLabels();\n render();\n </script>\n</body>\n</html>`\n}\n\nexport function renderCallbackHtml(options: { ok: boolean; error?: string; email?: string | null; baseUrl: string }): string {\n const { ok, error, email, baseUrl } = options\n const i18nJson = JSON.stringify(I18N_DICT)\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Antigravity Sign-in</title>\n <style>\n :root {\n color-scheme: light dark;\n --dsw-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;\n --bg-page: #0f1115;\n --bg-surface: #171a21;\n --border-l2: rgba(255, 255, 255, 0.12);\n --text-primary: #f8fafc;\n --text-secondary: #94a3b8;\n --brand-primary: #5686fe;\n --state-success: #34d399;\n --state-error: #f87171;\n }\n @media (prefers-color-scheme: light) {\n :root {\n --bg-page: #f8fafc;\n --bg-surface: #ffffff;\n --border-l2: rgba(0, 0, 0, 0.12);\n --text-primary: #0f172a;\n --text-secondary: #475569;\n --brand-primary: #4176e6;\n --state-success: #22c55e;\n --state-error: #ef4444;\n }\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: var(--dsw-font-family);\n background-color: var(--bg-page);\n color: var(--text-primary);\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n padding: 16px;\n }\n .card {\n background: var(--bg-surface);\n border: 1px solid var(--border-l2);\n border-radius: 12px;\n padding: 24px;\n max-width: 440px;\n width: 100%;\n text-align: center;\n box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);\n }\n .icon-wrap {\n width: 48px;\n height: 48px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 16px;\n font-size: 24px;\n }\n .icon-success { background: rgba(52, 211, 153, 0.15); color: var(--state-success); }\n .icon-error { background: rgba(248, 113, 113, 0.15); color: var(--state-error); }\n h1 { font-size: 18px; font-weight: 600; margin-bottom: 8px; }\n p { font-size: 13.5px; color: var(--text-secondary); margin-bottom: 16px; line-height: 1.5; }\n .btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-family: inherit;\n font-size: 13px;\n font-weight: 500;\n height: 34px;\n padding: 0 16px;\n border-radius: 17px;\n border: 1px solid var(--border-l2);\n background: transparent;\n color: var(--text-primary);\n cursor: pointer;\n text-decoration: none;\n }\n .btn:hover { background: rgba(128, 128, 128, 0.1); }\n .btn-primary { background: var(--brand-primary); color: #ffffff; border-color: transparent; }\n </style>\n</head>\n<body>\n <div class=\"card\">\n ${ok ? `\n <div class=\"icon-wrap icon-success\">✓</div>\n <h1 id=\"title\">Sign-in Successful</h1>\n <p id=\"desc\">Your Antigravity account ${email ? '<strong>' + email + '</strong> ' : ''}has been authorized and saved.</p>\n <p style=\"font-size:12px;opacity:0.8;\" id=\"closing\">This window will close automatically...</p>\n <button class=\"btn\" onclick=\"window.close()\">Close Window</button>\n ` : `\n <div class=\"icon-wrap icon-error\">✕</div>\n <h1 id=\"title\">Sign-in Failed</h1>\n <p id=\"desc\">Error details: ${error || 'Unknown error'}</p>\n <a class=\"btn btn-primary\" href=\"${baseUrl}/agy\">Return to Dashboard</a>\n `}\n </div>\n <script>\n const I18N = ${i18nJson};\n const lang = localStorage.getItem('agy_lang') || (navigator.language.startsWith('zh') ? 'zh' : 'en');\n const dict = I18N[lang] || I18N.en;\n\n if (${ok ? 'true' : 'false'}) {\n document.getElementById('title').textContent = dict.loginSuccessTitle;\n document.getElementById('desc').innerHTML = dict.loginSuccessDesc + (${JSON.stringify(email ? ' (' + email + ')' : '')});\n document.getElementById('closing').textContent = dict.windowClosing;\n \n // Notify parent window & close\n try {\n if (window.opener) {\n window.opener.postMessage({ type: 'agy_login_success' }, '*');\n }\n } catch (e) {}\n setTimeout(() => {\n window.close();\n }, 1800);\n } else {\n document.getElementById('title').textContent = dict.loginFailedTitle;\n }\n </script>\n</body>\n</html>`\n}\n","/**\n * In-harness web UI for agy: routes registered on ctx.webServer at /agy.\n * Serves a self-contained HTML dashboard with account management, OAuth login\n * via a popup, paste-blob import, and account verification.\n *\n * The OAuth callback is a webServer route on the same loopback origin, so the\n * firstparty/nativeapp consent can complete locally; remote hosts use the\n * paste-blob form instead.\n */\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { AccountStore } from '../store/accounts.ts'\nimport type { AgySessionManager } from '../session.ts'\nimport { authorizeAntigravity } from '../oauth/authorize.ts'\nimport { exchangeAntigravity } from '../oauth/exchange.ts'\nimport { decodeCredentialBlob } from '../oauth/blob.ts'\nimport { importManySources, upsertImportedAccount } from '../cli/import.ts'\nimport { generateFingerprint, recordFingerprintVersion } from '../runtime/fingerprint.ts'\nimport { resolveAntigravityVersionBounded } from '../runtime/version.ts'\nimport { renderDashboardHtml, renderCallbackHtml } from './page.ts'\nimport { maskProxyUrl } from '../store/accounts.ts'\nimport { accountFetch, isProxyReachable, normalizeProxyUrl, proxyUrlForLogs } from '../proxy.ts'\n\nexport interface WebRoute {\n kind: 'exact' | 'prefix'\n path: string\n handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>\n}\n\nexport interface AgyWebOptions {\n store: AccountStore\n sessions: AgySessionManager\n /** Base URL of the harness web server, e.g. http://127.0.0.1:3080 */\n baseUrl: string\n}\n\nfunction sendJson(res: ServerResponse, status: number, data: unknown): void {\n res.writeHead(status, { 'content-type': 'application/json' })\n res.end(JSON.stringify(data))\n}\n\nasync function readJson(req: IncomingMessage): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = []\n for await (const chunk of req) chunks.push(chunk as Buffer)\n const text = Buffer.concat(chunks).toString('utf8')\n return text ? (JSON.parse(text) as Record<string, unknown>) : {}\n}\n\nexport function createAgyWebRoutes(options: AgyWebOptions): WebRoute[] {\n const { store, sessions, baseUrl } = options\n\n // Authorizations issued by /auth-url, keyed by their raw state so the\n // callback can bind the exchange to the verifier we actually issued.\n const pendingAuth = new Map<string, { verifier: string; expiresAt: number }>()\n const PENDING_AUTH_TTL_MS = 10 * 60 * 1000\n const prunePendingAuth = (now = Date.now()) => {\n for (const [key, entry] of pendingAuth) {\n if (entry.expiresAt <= now) pendingAuth.delete(key)\n }\n while (pendingAuth.size > 100) {\n let oldestKey: string | null = null\n let oldestExpiry = Infinity\n for (const [key, entry] of pendingAuth) {\n if (entry.expiresAt < oldestExpiry) {\n oldestExpiry = entry.expiresAt\n oldestKey = key\n }\n }\n if (oldestKey) pendingAuth.delete(oldestKey)\n else break\n }\n }\n\n const accountsList = async () => {\n const storage = await store.load()\n const now = Date.now()\n const list = []\n for (const [index, account] of storage.accounts.entries()) {\n const entry: Record<string, unknown> = {\n index,\n email: account.email ?? null,\n projectId: account.projectId ?? null,\n active: index === storage.activeIndex && account.enabled !== false,\n state: account.enabled === false ? 'disabled'\n : account.verificationRequired ? 'verification-required'\n : account.coolingDownUntil && account.coolingDownUntil > now ? 'cooling'\n : 'active',\n cooldownUntil: account.coolingDownUntil && account.coolingDownUntil > now ? new Date(account.coolingDownUntil).toISOString() : null,\n cooldownReason: account.cooldownReason ?? null,\n rateLimits: account.rateLimitResetTimes ?? null,\n fingerprint: account.fingerprint\n ? { userAgent: account.fingerprint.userAgent, deviceId: account.fingerprint.deviceId, createdAt: account.fingerprint.createdAt }\n : null,\n fingerprintHistory: (account.fingerprintHistory ?? []).length,\n quota: null as Record<string, unknown> | null,\n proxy: account.proxy ? maskProxyUrl(account.proxy) : null,\n proxyMasked: account.proxy ? maskProxyUrl(account.proxy) : null,\n }\n list.push(entry)\n }\n // Best-effort per-account quota via fetchAvailableModels (fresh token).\n for (const entry of list) {\n const session = await sessions.getSession().catch(() => undefined)\n if (!session || session.index !== entry.index) continue\n try {\n const { fetchAvailableModels } = await import('../adapter/models.ts')\n // Account-scoped: route through the account's proxy (the quota panel\n // must not reveal the host's real IP for a proxied account).\n const discovered = await fetchAvailableModels(\n session.auth.access,\n session.account.projectId,\n accountFetch({ proxyUrl: session.account.proxy }),\n )\n const models = Object.entries(discovered.models ?? {})\n if (models.length > 0) {\n const quotaRows = models\n .map(([id, m]) => ({\n id,\n remainingFraction: typeof m.quotaInfo?.remainingFraction === 'number'\n ? Math.max(0, Math.min(1, m.quotaInfo.remainingFraction))\n : null,\n resetTime: m.quotaInfo?.resetTime ?? null,\n }))\n .sort((a, b) => (a.remainingFraction ?? -1) - (b.remainingFraction ?? -1))\n entry.quota = {\n modelCount: models.length,\n models: quotaRows,\n }\n }\n } catch {\n // quota stays null\n }\n }\n return list\n }\n\n const handleAuthUrl = async (res: ServerResponse) => {\n const redirectUri = `${baseUrl}/agy/oauth-callback`\n const authorization = await authorizeAntigravity(redirectUri)\n prunePendingAuth()\n pendingAuth.set(authorization.state, {\n verifier: authorization.verifier,\n expiresAt: Date.now() + PENDING_AUTH_TTL_MS,\n })\n sendJson(res, 200, { url: authorization.url })\n }\n\n const handleCallback = async (req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url ?? '/', baseUrl)\n const code = url.searchParams.get('code')\n const state = url.searchParams.get('state')\n if (!code || !state) {\n res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' })\n res.end(renderCallbackHtml({ ok: false, error: 'Missing code or state parameter', baseUrl }))\n return\n }\n const expected = pendingAuth.get(state)\n if (!expected) {\n res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' })\n res.end(renderCallbackHtml({ ok: false, error: 'Unknown or expired authorization — please start a new login.', baseUrl }))\n return\n }\n // One-time use: consume the issued state regardless of the exchange result.\n pendingAuth.delete(state)\n const redirectUri = `${baseUrl}/agy/oauth-callback`\n const result = await exchangeAntigravity(code, state, redirectUri, expected.verifier)\n if (result.type === 'failed') {\n res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' })\n res.end(renderCallbackHtml({ ok: false, error: result.error, baseUrl }))\n return\n }\n await upsertImportedAccount(store, {\n accessToken: result.access,\n refreshToken: result.refresh.split('|')[0]!,\n tokenType: 'Bearer',\n expiresAt: new Date(result.expires).toISOString(),\n authMethod: 'oauth',\n email: result.email ?? null,\n projectId: result.projectId || null,\n clientId: result.clientId || null,\n }, { overwriteExisting: true })\n\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })\n res.end(renderCallbackHtml({ ok: true, email: result.email ?? null, baseUrl }))\n }\n\n const handleImport = async (req: IncomingMessage, res: ServerResponse) => {\n try {\n const body = await readJson(req)\n const kind = body.kind === 'blob' ? 'blob' : 'json'\n // Batch mode: `sources` is an array of strings (multi-line paste / bulk files);\n // single mode: `source` (backwards compatible).\n const sources: unknown[] = Array.isArray(body.sources)\n ? body.sources\n : body.source !== undefined ? [body.source] : []\n if (sources.length === 0) throw new Error('nothing to import')\n const result = await importManySources(\n sources.map((source) => ({ source, kind })),\n store,\n { overwriteExisting: true },\n )\n sendJson(res, 200, result)\n } catch (error) {\n sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n const handleExportAll = async (_req: IncomingMessage, res: ServerResponse) => {\n try {\n const storage = await store.load()\n const blobs: Array<{ index: number; blob: string }> = []\n for (let index = 0; index < storage.accounts.length; index++) {\n const result = await sessions.exportBlob(index)\n if (result.blob) blobs.push({ index, blob: result.blob })\n }\n sendJson(res, 200, { blobs })\n } catch (error) {\n sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n const handleVerify = async (req: IncomingMessage, res: ServerResponse) => {\n try {\n const body = await readJson(req)\n const index = Number(body.index)\n const result = await sessions.verifyAccount(index)\n sendJson(res, result.ok ? 200 : 400, result)\n } catch (error) {\n sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n const handleHealth = async (req: IncomingMessage, res: ServerResponse) => {\n try {\n const body = await readJson(req)\n const indices = Array.isArray(body.indices)\n ? (body.indices as unknown[]).map((value) => Number(value))\n : undefined\n const results = await sessions.checkAccounts(indices)\n sendJson(res, 200, { results })\n } catch (error) {\n sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n const handleDelete = async (req: IncomingMessage, res: ServerResponse) => {\n try {\n const body = await readJson(req)\n const index = Number(body.index)\n await store.mutate((storage) => {\n if (index < 0 || index >= storage.accounts.length) throw new Error('account not found')\n storage.accounts.splice(index, 1)\n if (storage.activeIndex >= storage.accounts.length) storage.activeIndex = 0\n })\n sendJson(res, 200, { ok: true })\n } catch (error) {\n sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n const handleActivate = async (req: IncomingMessage, res: ServerResponse) => {\n try {\n const body = await readJson(req)\n const index = Number(body.index)\n await store.mutate((storage) => {\n if (index < 0 || index >= storage.accounts.length) throw new Error('account not found')\n storage.activeIndex = index\n })\n sendJson(res, 200, { ok: true, index })\n } catch (error) {\n sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n const handleModels = async (_req: IncomingMessage, res: ServerResponse) => {\n const session = await sessions.getSession().catch(() => undefined)\n if (!session) {\n sendJson(res, 400, { error: 'No agy account configured — run `dsh-agy login` first.' })\n return\n }\n try {\n const { listAgyModels } = await import('../adapter/models.ts')\n const models = await listAgyModels(\n session.auth.access,\n session.account.projectId,\n accountFetch({ proxyUrl: session.account.proxy }),\n )\n sendJson(res, 200, { account: session.account.email ?? null, models: models.map((m) => ({\n id: m.id,\n name: m.name,\n })) })\n } catch (error) {\n sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n const handleTest = async (req: IncomingMessage, res: ServerResponse) => {\n try {\n const body = await readJson(req)\n const model = typeof body.model === 'string' ? body.model : ''\n if (!model) {\n sendJson(res, 400, { error: 'model is required' })\n return\n }\n const result = await sessions.testCall(model)\n sendJson(res, result.ok ? 200 : 400, result)\n } catch (error) {\n sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n const handleExport = async (req: IncomingMessage, res: ServerResponse) => {\n try {\n const body = await readJson(req)\n const index = Number(body.index)\n const result = await sessions.exportBlob(index)\n if (result.error) {\n sendJson(res, 400, result)\n return\n }\n sendJson(res, 200, { blob: result.blob })\n } catch (error) {\n sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n const handleFingerprint = async (req: IncomingMessage, res: ServerResponse) => {\n try {\n const body = await readJson(req)\n const index = Number(body.index)\n const action = body.action === 'regenerate' ? 'regenerate' : 'show'\n const result = await store.mutate(async (storage) => {\n const account = storage.accounts[index]\n if (!account) throw new Error('account not found')\n if (action === 'regenerate') {\n const fresh = generateFingerprint(undefined, await resolveAntigravityVersionBounded())\n account.fingerprint = fresh\n account.fingerprintHistory = recordFingerprintVersion(account.fingerprintHistory, fresh, 'regenerated')\n }\n return {\n action,\n fingerprint: account.fingerprint\n ? { userAgent: account.fingerprint.userAgent, deviceId: account.fingerprint.deviceId, createdAt: account.fingerprint.createdAt }\n : null,\n history: account.fingerprintHistory?.length ?? 0,\n }\n })\n sendJson(res, 200, result)\n } catch (error) {\n sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n const handleProxy = async (req: IncomingMessage, res: ServerResponse) => {\n try {\n const body = await readJson(req)\n const rawIdx = Number(body.index)\n if (!Number.isInteger(rawIdx) || !Number.isFinite(rawIdx) || rawIdx < 0) {\n sendJson(res, 400, { error: 'invalid index' })\n return\n }\n const index = rawIdx\n const raw = typeof body.proxy === 'string' ? body.proxy : ''\n if (!raw.trim()) {\n // clear\n await store.mutate((storage) => {\n const account = storage.accounts[index]\n if (!account) throw new Error('account not found')\n delete account.proxy\n })\n sendJson(res, 200, { ok: true, proxy: null, proxyMasked: null, rawLogs: null })\n return\n }\n let normalized: string\n try {\n normalized = normalizeProxyUrl(raw)\n } catch (e) {\n sendJson(res, 400, { error: e instanceof Error ? e.message : String(e) })\n return\n }\n await store.mutate((storage) => {\n const account = storage.accounts[index]\n if (!account) throw new Error('account not found')\n account.proxy = normalized\n })\n sendJson(res, 200, { ok: true, proxy: maskProxyUrl(normalized), proxyMasked: maskProxyUrl(normalized), rawLogs: proxyUrlForLogs(normalized) })\n } catch (error) {\n sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n const handleProxyTest = async (req: IncomingMessage, res: ServerResponse) => {\n try {\n const body = await readJson(req)\n const rawIdx2 = body.index !== undefined ? Number(body.index) : NaN\n const hasExplicitProxy = typeof body.proxy === 'string' && body.proxy.trim() !== ''\n if (!hasExplicitProxy) {\n if (!Number.isInteger(rawIdx2) || !Number.isFinite(rawIdx2) || rawIdx2 < 0) {\n sendJson(res, 400, { error: 'invalid index' })\n return\n }\n } else if (body.index !== undefined) {\n if (!Number.isInteger(rawIdx2) || !Number.isFinite(rawIdx2) || rawIdx2 < 0) {\n sendJson(res, 400, { error: 'invalid index' })\n return\n }\n }\n const index = hasExplicitProxy && body.index === undefined ? -1 : rawIdx2\n let target: string | undefined\n if (hasExplicitProxy) {\n try {\n target = normalizeProxyUrl(body.proxy as string)\n } catch (e) {\n sendJson(res, 400, { error: e instanceof Error ? e.message : String(e) })\n return\n }\n } else {\n const storage = await store.load()\n const account = storage.accounts[index]\n if (!account) {\n sendJson(res, 400, { error: 'account not found' })\n return\n }\n if (!account.proxy) {\n sendJson(res, 400, { error: 'no proxy configured for this account' })\n return\n }\n target = account.proxy\n }\n // mask for response, never raw\n const masked = maskProxyUrl(target!) ?? proxyUrlForLogs(target!)\n let ok = false\n let errMsg: string | undefined\n try {\n ok = await isProxyReachable(target, 2000)\n if (!ok) errMsg = 'proxy unreachable: ' + masked\n } catch (e) {\n ok = false\n errMsg = e instanceof Error ? e.message : String(e)\n }\n sendJson(res, 200, { ok, masked, ...(errMsg ? { error: errMsg } : {}) })\n } catch (error) {\n sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n return [\n { kind: 'exact', path: '/agy', handler: (req, res) => {\n if (req.method === 'GET') {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })\n res.end(renderDashboardHtml())\n return\n }\n res.writeHead(405)\n res.end()\n } },\n { kind: 'exact', path: '/agy/oauth-callback', handler: handleCallback },\n { kind: 'exact', path: '/agy/api/accounts', handler: async (_req, res) => {\n sendJson(res, 200, { accounts: await accountsList() })\n } },\n { kind: 'exact', path: '/agy/api/auth-url', handler: async (_req, res) => { await handleAuthUrl(res) } },\n { kind: 'exact', path: '/agy/api/import', handler: handleImport },\n { kind: 'exact', path: '/agy/api/export-all', handler: handleExportAll },\n { kind: 'exact', path: '/agy/api/verify', handler: handleVerify },\n { kind: 'exact', path: '/agy/api/health', handler: handleHealth },\n { kind: 'exact', path: '/agy/api/delete', handler: handleDelete },\n { kind: 'exact', path: '/agy/api/activate', handler: handleActivate },\n { kind: 'exact', path: '/agy/api/models', handler: handleModels },\n { kind: 'exact', path: '/agy/api/test', handler: handleTest },\n { kind: 'exact', path: '/agy/api/export', handler: handleExport },\n { kind: 'exact', path: '/agy/api/fingerprint', handler: handleFingerprint },\n { kind: 'exact', path: '/agy/api/proxy', handler: handleProxy },\n { kind: 'exact', path: '/agy/api/proxy/test', handler: handleProxyTest },\n ]\n}\n\nexport { decodeCredentialBlob }\n","/**\n * dsh-agy web entry: registers the /agy routes on ctx.webServer. Declared as a\n * separate plugin (inject: ['webServer']) because the web server activates\n * after the llm service (it waits on webStartup); a strict ctx.get in the main\n * plugin's apply would return undefined. In profiles without a web server this\n * entry simply stays pending.\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport { createAgyRuntime } from '../plugin-common.ts'\nimport { isAgyDisabled } from '../runtime/risk.ts'\nimport { createAgyWebRoutes, type WebRoute } from './routes.ts'\n\nexport const name = 'dsh-agy-web'\n\nexport const inject = ['llm', 'webServer']\n\nexport function apply(ctx: Context): void {\n if (isAgyDisabled()) {\n ctx.logger.warn('[dsh-agy] disabled by DSH_AGY_DISABLE=1 — skipping /agy registration')\n return\n }\n ctx.effect(async () => {\n const webServer = ctx.get('webServer') as\n | { register(route: WebRoute): () => void; host?: string }\n | undefined\n if (!webServer) return () => {}\n\n // Host/port come from the webStartup provider (CLI args); the fallbacks are\n // DSH's own web-app defaults (loopback + 3080), never a user override.\n const webStartup = ctx.get('webStartup') as { host?: string; port?: number } | undefined\n const host = webStartup?.host ?? '127.0.0.1'\n const port = webStartup?.port ?? 3080\n\n // The /agy routes manage account credentials with no authentication of\n // their own; they must never be reachable from the network. When the web\n // server binds a non-loopback interface, refuse to register them at all\n // (the loopback-only OAuth redirect would be unusable there anyway).\n const bindHost = webServer.host ?? host\n if (!['127.0.0.1', 'localhost', '::1'].includes(bindHost)) {\n ctx.logger.warn(\n '[dsh-agy] web server bound to \"' + bindHost + '\" (non-loopback): not registering the /agy routes ' +\n '(they manage account credentials and must stay loopback-only). Bind the web server to 127.0.0.1 to enable them.',\n )\n return () => {}\n }\n\n const { store, sessions } = await createAgyRuntime(ctx)\n const baseUrl = `http://${host}:${port}`\n\n const disposers = createAgyWebRoutes({ store, sessions, baseUrl }).map((route) =>\n webServer.register(route),\n )\n return () => {\n for (const dispose of disposers) dispose()\n }\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin.mjs","names":["body"],"sources":["../../src/web/management.ts","../../src/web/i18n.ts","../../src/web/page.ts","../../src/web/plugin.ts"],"sourcesContent":["/**\n * agy management API: one method dispatcher shared by the RPC transport.\n *\n * Every method is a pure `(payload) => result` function so the transport stays\n * thin and the handlers stay testable without HTTP. Failures throw; the\n * transport converts a thrown error into the RPC failure envelope.\n *\n * This module owns the OAuth pending-authorization state because `auth.url`\n * issues it and the callback route consumes it — they must share one map, and\n * splitting them would let a verifier and its callback drift apart.\n */\n\nimport { authorizeAntigravity } from '../oauth/authorize.ts'\nimport { exchangeAntigravity } from '../oauth/exchange.ts'\nimport { importManySources, upsertImportedAccount } from '../cli/import.ts'\nimport { generateFingerprint, recordFingerprintVersion } from '../runtime/fingerprint.ts'\nimport { currentAgyVersion } from '../oauth/constants.ts'\nimport { maskProxyUrl } from '../store/accounts.ts'\nimport { accountFetch, isProxyReachable, normalizeProxyUrl, proxyUrlForLogs, withTotalTimeout } from '../proxy.ts'\nimport { AGY_PROVIDER } from '../adapter/models.ts'\nimport type { DiscoveredModelEntry } from '../adapter/models.ts'\nimport { clearExpiredState } from '../runtime/rotation.ts'\nimport { foldWindowBreakdown } from '../stats.ts'\nimport { THINKING_BUDGET_MAX, THINKING_BUDGET_MIN } from '../thinking-budget.ts'\nimport { CLAUDE_BUDGET_MAX, CLAUDE_BUDGET_MIN } from '../thinking-types.ts'\nimport type { UsageCounters, UsageSource } from '../stats.ts'\nimport type { AccountStore } from '../store/accounts.ts'\nimport type { AgySessionManager } from '../session.ts'\nimport type { ModelVisibility } from '../model-visibility.ts'\nimport type { UsageStats } from '../stats.ts'\nimport type {\n AccountState,\n AccountUsageView,\n AccountView,\n AgyRpcMethod,\n ModelView,\n RangeBreakdown,\n StatsView,\n ThinkingBudgets,\n} from '../rpc-contract.ts'\n\n/** One pending OAuth authorization, keyed by its raw state. */\ninterface PendingAuth {\n verifier: string\n expiresAt: number\n}\n\nexport interface AgyManagementOptions {\n store: AccountStore\n sessions: AgySessionManager\n stats: UsageStats\n modelVisibility: ModelVisibility\n /**\n * The global reasoning-level budget map (see `thinking-budget.ts`).\n *\n * A plain accessor pair rather than the store instance, so this module depends\n * only on the two operations it exposes and cannot reach the rest of the store.\n */\n thinkingBudget: {\n all: () => ThinkingBudgets\n set: (level: string, value: number | undefined) => ThinkingBudgets\n claude: () => number | undefined\n setClaude: (value: number | undefined) => number | undefined\n tiered: () => number | undefined\n setTiered: (value: number | undefined) => number | undefined\n }\n /**\n * The adapter's *unfiltered* model catalog.\n *\n * A function rather than the adapter itself so this module depends on the one\n * capability it needs. It must not be `llm.listModels()`, which already has\n * the hidden set applied — see the `model.list` handler.\n */\n listAllModels: () => Promise<readonly { id: string; name: string }[]>\n /** Harness web-server base URL, used to build the loopback OAuth redirect. */\n baseUrl: string\n /**\n * Tell DSH the model catalog changed.\n *\n * The hidden-model list lives in agy's own file, so nothing in DSH emits a\n * change when it is written. The client refreshes the picker on\n * `llm/adapters-updated` alone, so without this a toggle would only take\n * effect after a page reload.\n */\n notifyModelsChanged: () => void\n}\n\nexport interface AgyManagement {\n /** Dispatch one method; rejects with a legible message on failure. */\n call(method: string, payload: unknown): Promise<unknown>\n /** Consume an OAuth callback (the one endpoint that stays a real HTTP route). */\n handleCallback(params: URLSearchParams): Promise<{ ok: boolean; email?: string | null; error?: string }>\n}\n\n/** Fail one RPC with a message the UI can show verbatim. */\nfunction fail(message: string): never {\n throw new Error(message)\n}\n\nfunction asIndex(payload: unknown): number {\n const index = Number((payload as { index?: unknown })?.index)\n if (!Number.isInteger(index) || index < 0) fail('invalid index')\n return index\n}\n\n/**\n * Flatten one account's ledger entry for transport.\n *\n * `models` and `lastUsedAt` are deliberately NOT carried: the per-model table\n * that read `models` was removed (the cumulative metric strip states the same\n * figures better), and the `lastUsedAt` ordering rule went with it. Sending\n * fields no client reads is a wire surface with no consumer, which reads as\n * intentional to the next person and invites a stale assumption.\n */\nfunction toAccountUsageView(\n usage: { totals: UsageCounters; sources: Record<UsageSource, number> } | undefined,\n): AccountUsageView | null {\n if (usage === undefined) return null\n return {\n totals: usage.totals,\n sources: usage.sources,\n }\n}\n\nexport function createAgyManagement(options: AgyManagementOptions): AgyManagement {\n const { store, sessions, stats, modelVisibility, listAllModels, baseUrl, notifyModelsChanged } = options\n const thinkingBudget = options.thinkingBudget\n\n /** Authorizations issued by `auth.url`, keyed by raw state. */\n const pendingAuth = new Map<string, PendingAuth>()\n const PENDING_AUTH_TTL_MS = 10 * 60 * 1000\n\n const prunePendingAuth = (now = Date.now()): void => {\n for (const [key, entry] of pendingAuth) {\n if (entry.expiresAt <= now) pendingAuth.delete(key)\n }\n // Bound the map even under abuse: drop the soonest-to-expire entries.\n while (pendingAuth.size > 100) {\n let oldestKey: string | null = null\n let oldestExpiry = Infinity\n for (const [key, entry] of pendingAuth) {\n if (entry.expiresAt < oldestExpiry) {\n oldestExpiry = entry.expiresAt\n oldestKey = key\n }\n }\n if (oldestKey === null) break\n pendingAuth.delete(oldestKey)\n }\n }\n\n const listAccounts = async (): Promise<AccountView[]> => {\n const storage = await store.load()\n const now = Date.now()\n // Expire stale cooldown state BEFORE rendering. `clearExpiredState` otherwise\n // only runs inside `pickAccount`, i.e. only when a request is actually made,\n // so a management read showed a reason for a window that had already lapsed —\n // the badge said \"active\" while the detail row still said \"network-error\",\n // contradicting itself on one screen. Purely in-memory here: the next real\n // mutation persists it, and a read must not write.\n for (const account of storage.accounts) clearExpiredState(account, now)\n const ledger = stats.snapshot()\n const rows: AccountView[] = []\n for (const [index, account] of storage.accounts.entries()) {\n const state: AccountState = account.enabled === false\n ? 'disabled'\n : account.verificationRequired === true\n ? 'verification-required'\n : account.coolingDownUntil !== undefined && account.coolingDownUntil > now\n ? 'cooling'\n : 'active'\n const key = account.email ?? account.id\n rows.push({\n index,\n email: account.email ?? null,\n projectId: account.projectId ?? null,\n active: index === storage.activeIndex && account.enabled !== false,\n state,\n cooldownUntil: account.coolingDownUntil !== undefined && account.coolingDownUntil > now\n ? new Date(account.coolingDownUntil).toISOString()\n : null,\n cooldownReason: account.cooldownReason ?? null,\n // Only while the window is live: `clearExpiredState` above already drops\n // both fields for an expired one, so this cannot outlive its reason.\n cooldownSetAt: account.cooldownSetAt === undefined\n ? null\n : new Date(account.cooldownSetAt).toISOString(),\n /**\n * Appeal link from an upstream verification challenge. Surfaced, never\n * followed automatically: only the account owner can complete it, and the\n * account returns to service on its own once the wall clears.\n */\n verificationUrl: account.verificationUrl ?? null,\n verificationRequired:\n account.verificationRequired === true && account.enabled !== false,\n rateLimits: account.rateLimitResetTimes ?? null,\n fingerprint: account.fingerprint\n ? {\n userAgent: account.fingerprint.userAgent,\n deviceId: account.fingerprint.deviceId,\n createdAt: account.fingerprint.createdAt,\n }\n : null,\n fingerprintHistory: (account.fingerprintHistory ?? []).length,\n proxy: account.proxy ? maskProxyUrl(account.proxy) : null,\n // Read from the cache the session manager already refreshes alongside the\n // per-model quota, so showing the windows costs no request. Null means\n // \"never measured\" and renders as an explicit placeholder rather than a\n // zero, which would read as an exhausted account.\n limits: account.cachedLimits?.groups ?? null,\n limitsUpdatedAt: account.cachedLimits?.updatedAt ?? null,\n usage: key === undefined ? null : toAccountUsageView(ledger.accounts[key]),\n })\n }\n // Per-model quota is deliberately NOT fetched here (nor anywhere else): it\n // costs an upstream round trip, and embedding it in this reply once meant the\n // accounts list could not render until that probe finished — on a slow\n // network the panel showed \"no accounts\" behind a permanent spinner,\n // indistinguishable from data loss. The grouped 5h/weekly windows are the\n // surviving quota display, and they arrive via `account.limits`.\n return rows\n }\n\n /** Fold the ledger into the Usage tab's view. */\n const statsView = (): StatsView => {\n const doc = stats.snapshot()\n const now = Date.now()\n\n // Per-model totals across accounts: sum the same model id from every account.\n const byModel = new Map<string, UsageCounters>()\n for (const usage of Object.values(doc.accounts)) {\n for (const [model, counters] of Object.entries(usage.models)) {\n const current = byModel.get(model)\n if (current === undefined) {\n byModel.set(model, { ...counters })\n } else {\n for (const key of Object.keys(current) as Array<keyof UsageCounters>) {\n current[key] += counters[key]\n }\n }\n }\n }\n const allModels = [...byModel.entries()]\n .map(([model, counters]) => ({ model, counters }))\n .sort((a, b) => b.counters.requests - a.counters.requests)\n\n // Each range carries its OWN breakdown, so the tables follow the range\n // selection instead of showing all-time rows beneath range-scoped headline\n // figures (30 requests above a 164-request row, on one screen). The\n // all-time range reads the account maps directly, which hold every request\n // ever recorded rather than only the retained day window.\n const allAccounts = Object.entries(doc.accounts)\n .map(([account, usage]) => ({ account, counters: usage.totals }))\n .sort((a, b) => b.counters.requests - a.counters.requests)\n const breakdown = (days: number): RangeBreakdown => {\n const { totals, models, accounts } = foldWindowBreakdown(doc, days, now)\n return { counters: totals, models, accounts }\n }\n\n return {\n since: doc.totals.requests > 0 ? doc.since : null,\n all: { counters: doc.totals, models: allModels, accounts: allAccounts },\n today: breakdown(1),\n week: breakdown(7),\n month: breakdown(30),\n }\n }\n\n const methods: Record<AgyRpcMethod, (payload: unknown) => Promise<unknown>> = {\n 'account.list': async () => ({ accounts: await listAccounts() }),\n\n 'account.activate': async (payload) => {\n const index = asIndex(payload)\n await store.mutate((storage) => {\n if (index >= storage.accounts.length) fail('account not found')\n storage.activeIndex = index\n })\n return { ok: true, index }\n },\n\n 'account.delete': async (payload) => {\n const index = asIndex(payload)\n await store.mutate((storage) => {\n if (index >= storage.accounts.length) fail('account not found')\n storage.accounts.splice(index, 1)\n if (storage.activeIndex >= storage.accounts.length) storage.activeIndex = 0\n })\n return { ok: true }\n },\n\n 'account.verify': async (payload) => sessions.verifyAccount(asIndex(payload)),\n\n 'account.health': async (payload) => {\n const raw = (payload as { indices?: unknown })?.indices\n const indices = Array.isArray(raw) ? raw.map((value) => Number(value)) : undefined\n return { results: await sessions.checkAccounts(indices) }\n },\n\n /**\n * Refresh and return the 5h/weekly windows for every enabled account.\n *\n * A SEPARATE call rather than a field on `account.list`, because that reply\n * is deliberately probe-free (see the note above its return): folding an\n * upstream call into it made the whole accounts+usage view wait on the\n * network and show \"no accounts\" behind a spinner. Here the client renders\n * the list from `account.list` immediately and merges these in when they\n * arrive, so a slow probe costs a placeholder, not the page.\n *\n * Server-side this is TTL-gated, so the upstream call happens at most once\n * per window rather than once per view. `force` bypasses that gate for an\n * EXPLICIT refresh — without it the toolbar's Refresh button could not\n * deliver \"the latest numbers now\", which is the only reason to click it\n * while the snapshot is still inside its TTL.\n */\n 'account.limits': async (payload) => {\n const force = (payload as { force?: unknown } | undefined)?.force === true\n const storage = await store.load()\n // Best-effort: a failed refresh leaves whatever was cached, so this reply\n // is always the current best knowledge rather than an error. The OUTCOME\n // is still reported, because \"probed and failed\" and \"was still fresh\"\n // both leave the numbers identical and the user needs to know which.\n const result = await sessions\n .refreshLimits(storage, { force })\n .catch(() => ({ measured: [], failed: [], skipped: 0 }))\n const fresh = await store.load()\n return {\n limits: fresh.accounts.map((account, index) => ({\n index,\n groups: account.cachedLimits?.groups ?? null,\n updatedAt: account.cachedLimits?.updatedAt ?? null,\n })),\n measured: result.measured.length,\n failed: result.failed.length,\n skipped: result.skipped,\n }\n },\n\n 'account.test': async (payload) => {\n const model = (payload as { model?: unknown })?.model\n if (typeof model !== 'string' || model === '') fail('model is required')\n // The account row the user clicked. Optional so a caller that only wants\n // \"test whatever the pool picks\" still works, but a row click always\n // supplies it — otherwise the probe silently ran on the affinity account\n // and reported its result as this account's.\n const rawIndex = (payload as { index?: unknown })?.index\n return sessions.testCall(model, {\n ...(rawIndex === undefined ? {} : { accountIndex: asIndex(payload) }),\n })\n },\n\n 'account.export': async (payload) => sessions.exportBlob(asIndex(payload)),\n\n 'account.exportAll': async () => {\n const storage = await store.load()\n const blobs: Array<{ index: number; blob: string }> = []\n for (let index = 0; index < storage.accounts.length; index++) {\n const result = await sessions.exportBlob(index)\n if (result.blob !== undefined) blobs.push({ index, blob: result.blob })\n }\n return { blobs }\n },\n\n 'account.import': async (payload) => {\n const body = payload as { kind?: unknown; sources?: unknown } | undefined\n const kind = body?.kind === 'blob' ? 'blob' : 'json'\n const sources = Array.isArray(body?.sources) ? body.sources : []\n if (sources.length === 0) fail('nothing to import')\n const result = await importManySources(\n sources.map((source) => ({ source, kind })),\n store,\n { overwriteExisting: true },\n )\n // Per-source failures are part of the result, not a thrown error: a batch\n // where 2 of 5 sources were malformed is a partial success, and the caller\n // must be able to say which ones failed.\n return {\n imported: result.imported,\n replaced: result.replaced,\n errors: result.errors.map((error) => String(error)),\n }\n },\n\n 'account.fingerprint': async (payload) => {\n const index = asIndex(payload)\n const action = (payload as { action?: unknown })?.action === 'regenerate' ? 'regenerate' : 'show'\n return store.mutate(async (storage) => {\n const account = storage.accounts[index]\n if (!account) fail('account not found')\n if (action === 'regenerate') {\n // Same version source as first-use creation: the published value the\n // resolver maintains, never a per-call network probe whose result the\n // pool-randomizing default would silently discard anyway.\n const fresh = generateFingerprint(undefined, currentAgyVersion())\n account.fingerprint = fresh\n account.fingerprintHistory = recordFingerprintVersion(account.fingerprintHistory, fresh, 'regenerated')\n }\n return {\n action,\n fingerprint: account.fingerprint\n ? {\n userAgent: account.fingerprint.userAgent,\n deviceId: account.fingerprint.deviceId,\n createdAt: account.fingerprint.createdAt,\n }\n : null,\n history: account.fingerprintHistory?.length ?? 0,\n }\n })\n },\n\n 'account.proxy': async (payload) => {\n const index = asIndex(payload)\n const raw = (payload as { proxy?: unknown })?.proxy\n const proxy = typeof raw === 'string' ? raw : ''\n if (proxy.trim() === '') {\n await store.mutate((storage) => {\n const account = storage.accounts[index]\n if (!account) fail('account not found')\n delete account.proxy\n })\n return { ok: true as const, proxy: null, proxyMasked: null, rawLogs: null }\n }\n let normalized: string\n try {\n normalized = normalizeProxyUrl(proxy)\n } catch (error) {\n fail(error instanceof Error ? error.message : String(error))\n }\n await store.mutate((storage) => {\n const account = storage.accounts[index]\n if (!account) fail('account not found')\n account.proxy = normalized\n })\n return {\n ok: true as const,\n proxy: maskProxyUrl(normalized),\n proxyMasked: maskProxyUrl(normalized),\n rawLogs: proxyUrlForLogs(normalized),\n }\n },\n\n 'account.proxyTest': async (payload) => {\n const body = payload as { index?: unknown; proxy?: unknown } | undefined\n const hasExplicit = typeof body?.proxy === 'string' && body.proxy.trim() !== ''\n let target: string\n if (hasExplicit) {\n try {\n target = normalizeProxyUrl(body.proxy as string)\n } catch (error) {\n fail(error instanceof Error ? error.message : String(error))\n }\n } else {\n const index = asIndex(payload)\n const storage = await store.load()\n const account = storage.accounts[index]\n if (!account) fail('account not found')\n if (!account.proxy) fail('no proxy configured for this account')\n target = account.proxy\n }\n // Masked for display only; the raw URL never leaves this function.\n const masked = maskProxyUrl(target) ?? proxyUrlForLogs(target)\n try {\n const ok = await isProxyReachable(target, 2000)\n return ok ? { ok, masked } : { ok, masked, error: `proxy unreachable: ${masked}` }\n } catch (error) {\n return { ok: false, masked, error: error instanceof Error ? error.message : String(error) }\n }\n },\n\n 'auth.url': async () => {\n const authorization = await authorizeAntigravity(`${baseUrl}/agy/oauth-callback`)\n prunePendingAuth()\n pendingAuth.set(authorization.state, {\n verifier: authorization.verifier,\n expiresAt: Date.now() + PENDING_AUTH_TTL_MS,\n })\n return { url: authorization.url }\n },\n\n 'model.list': async () => {\n const session = await sessions.getSession().catch(() => undefined)\n if (session === undefined) fail('No agy account configured — run `dsh-agy login` first.')\n // The adapter's unfiltered catalog: reading the filtered `listModels()`\n // would hide a disabled model from this very list, leaving no switch to\n // turn it back on.\n const models = await listAllModels()\n const hidden = modelVisibility.disabledFor(AGY_PROVIDER)\n const rows: ModelView[] = models.map((model) => ({\n id: model.id,\n name: model.name,\n disabled: hidden.has(model.id),\n }))\n return { account: session.account.email ?? null, models: rows }\n },\n\n 'model.setDisabled': async (payload) => {\n const body = payload as { modelId?: unknown; disabled?: unknown } | undefined\n const modelId = body?.modelId\n if (typeof modelId !== 'string' || modelId === '') fail('modelId is required')\n const disabled = body?.disabled === true\n modelVisibility.setDisabled(AGY_PROVIDER, modelId, disabled)\n // Push the change to every open client so the picker updates live.\n notifyModelsChanged()\n return { modelId, disabled }\n },\n\n 'thinking.get': async () => ({\n budgets: thinkingBudget.all(),\n min: THINKING_BUDGET_MIN,\n max: THINKING_BUDGET_MAX,\n tieredBudget: thinkingBudget.tiered() ?? null,\n claudeBudget: thinkingBudget.claude() ?? null,\n claudeMin: CLAUDE_BUDGET_MIN,\n claudeMax: CLAUDE_BUDGET_MAX,\n }),\n\n 'thinking.setTiered': async (payload) => {\n const body = payload as { budget?: unknown } | undefined\n const raw = body?.budget\n if (raw !== undefined && raw !== null && typeof raw !== 'number') fail('budget must be a number')\n try {\n const value = thinkingBudget.setTiered(raw === undefined || raw === null ? undefined : raw)\n return { tieredBudget: value ?? null }\n } catch (error) {\n fail(error instanceof Error ? error.message : String(error))\n }\n },\n\n 'thinking.setClaude': async (payload) => {\n const body = payload as { budget?: unknown } | undefined\n const raw = body?.budget\n if (raw !== undefined && raw !== null && typeof raw !== 'number') fail('budget must be a number')\n try {\n const value = thinkingBudget.setClaude(raw === undefined || raw === null ? undefined : raw)\n return { claudeBudget: value ?? null }\n } catch (error) {\n fail(error instanceof Error ? error.message : String(error))\n }\n },\n\n 'thinking.set': async (payload) => {\n const body = payload as { level?: unknown; budget?: unknown } | undefined\n const level = body?.level\n if (typeof level !== 'string' || level === '') fail('level is required')\n // `undefined` and explicit null BOTH clear the entry: the field means \"let\n // upstream choose\", and a JSON null is how a cleared input arrives.\n const raw = body?.budget\n if (raw !== undefined && raw !== null && typeof raw !== 'number') fail('budget must be a number')\n const value = raw === undefined || raw === null ? undefined : raw\n try {\n const budgets = thinkingBudget.set(level, value)\n return { budgets }\n } catch (error) {\n // Surface the range/name violation as a caller-correctable message\n // rather than an unlabelled RPC failure.\n fail(error instanceof Error ? error.message : String(error))\n }\n },\n\n 'stats.get': async () => statsView(),\n }\n\n return {\n async call(method, payload) {\n const handler = methods[method as AgyRpcMethod]\n if (handler === undefined) fail(`unknown method: ${method}`)\n return handler(payload)\n },\n\n async handleCallback(params) {\n const code = params.get('code')\n const state = params.get('state')\n if (!code || !state) return { ok: false, error: 'Missing code or state parameter' }\n const expected = pendingAuth.get(state)\n if (expected === undefined) {\n return { ok: false, error: 'Unknown or expired authorization — please start a new login.' }\n }\n // One-time use: consume the issued state regardless of the exchange result.\n pendingAuth.delete(state)\n const redirectUri = `${baseUrl}/agy/oauth-callback`\n const result = await exchangeAntigravity(code, state, redirectUri, expected.verifier)\n if (result.type === 'failed') return { ok: false, error: result.error }\n await upsertImportedAccount(store, {\n accessToken: result.access,\n refreshToken: result.refresh.split('|')[0]!,\n tokenType: 'Bearer',\n expiresAt: new Date(result.expires).toISOString(),\n authMethod: 'oauth',\n email: result.email ?? null,\n projectId: result.projectId || null,\n clientId: result.clientId || null,\n }, { overwriteExisting: true })\n return { ok: true, email: result.email ?? null }\n },\n }\n}\n","/**\n * Bilingual UI dictionary for the dsh-agy web dashboard and OAuth callbacks.\n */\n\nexport interface I18nStrings {\n loginSuccessTitle: string\n loginSuccessDesc: string\n loginFailedTitle: string\n windowClosing: string\n}\n\nexport const I18N_DICT: { en: I18nStrings; zh: I18nStrings } = {\n en: {\n loginSuccessTitle: 'Sign-in Successful',\n loginSuccessDesc: 'Your Antigravity account has been authorized and saved.',\n loginFailedTitle: 'Sign-in Failed',\n windowClosing: 'This window will close automatically in a moment...',\n },\n zh: {\n loginSuccessTitle: '授权登录成功',\n loginSuccessDesc: 'Antigravity 账号已成功接入并保存。',\n loginFailedTitle: '授权登录失败',\n windowClosing: '此窗口即将在 2 秒内自动关闭...',\n },\n}\n","/**\n * Browser-facing HTML for the agy OAuth callback.\n *\n * This is the only page agy serves. The account/statistics dashboard that used\n * to live at `/agy` is gone: that surface is now the Settings section rendered\n * by `src/client/` over the `/api/agy` RPC, so there is no longer a second UI to\n * keep in step with it.\n *\n * The callback stays a real page because Google redirects the browser here.\n */\n\nimport { I18N_DICT } from './i18n.ts'\n\n/**\n * Escape text for an HTML text/attribute position.\n *\n * Every interpolation below except the static markup is attacker-influenced or\n * upstream-influenced: `error` is the token endpoint's raw response body, and\n * `email` comes from Google's userinfo. This page is served by the SAME web\n * server, and therefore the same origin, as the DSH GUI — so injected script\n * here would run with the GUI's session and could reach `/api/agy`\n * (`account.exportAll` returns live credential blobs). That chain is why the\n * escaping matters more than the narrow trigger suggests.\n *\n * Text in markup goes through {@link escapeHtml}; text inside the inline\n * `<script>` goes through {@link jsonForInlineScript}. Neither is optional.\n */\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\n/**\n * Serialize a value for interpolation into the inline `<script>`.\n *\n * `JSON.stringify` escapes for a JS string context only, NOT for the HTML script\n * data state: it leaves `<` alone, so an email or error body containing\n * `</script>` terminates the element early and everything after it is parsed as\n * markup. Escaping the three HTML-significant characters as `\\uXXXX` keeps the\n * JSON value identical while making the byte sequence unrepresentable in the\n * source. (The `JSON.stringify` output above is a valid JS string either way,\n * because a `\\u003c` escape and a literal `<` denote the same character.)\n */\nfunction jsonForInlineScript(value: unknown): string {\n return JSON.stringify(value)\n .replace(/</g, '\\\\u003c')\n .replace(/>/g, '\\\\u003e')\n .replace(/&/g, '\\\\u0026')\n}\n\nexport function renderCallbackHtml(options: { ok: boolean; error?: string; email?: string | null; baseUrl: string }): string {\n const { ok, error, email, baseUrl } = options\n const i18nJson = jsonForInlineScript(I18N_DICT)\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Antigravity Sign-in</title>\n <style>\n :root {\n color-scheme: light dark;\n --dsw-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;\n --bg-page: #0f1115;\n --bg-surface: #171a21;\n --border-l2: rgba(255, 255, 255, 0.12);\n --text-primary: #f8fafc;\n --text-secondary: #94a3b8;\n --brand-primary: #5686fe;\n --state-success: #34d399;\n --state-error: #f87171;\n }\n @media (prefers-color-scheme: light) {\n :root {\n --bg-page: #f8fafc;\n --bg-surface: #ffffff;\n --border-l2: rgba(0, 0, 0, 0.12);\n --text-primary: #0f172a;\n --text-secondary: #475569;\n --brand-primary: #4176e6;\n --state-success: #22c55e;\n --state-error: #ef4444;\n }\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: var(--dsw-font-family);\n background-color: var(--bg-page);\n color: var(--text-primary);\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n padding: 16px;\n }\n .card {\n background: var(--bg-surface);\n border: 1px solid var(--border-l2);\n border-radius: 12px;\n padding: 24px;\n max-width: 440px;\n width: 100%;\n text-align: center;\n box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);\n }\n .icon-wrap {\n width: 48px;\n height: 48px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 16px;\n font-size: 24px;\n }\n .icon-success { background: rgba(52, 211, 153, 0.15); color: var(--state-success); }\n .icon-error { background: rgba(248, 113, 113, 0.15); color: var(--state-error); }\n h1 { font-size: 18px; font-weight: 600; margin-bottom: 8px; }\n p { font-size: 13.5px; color: var(--text-secondary); margin-bottom: 16px; line-height: 1.5; }\n .btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-family: inherit;\n font-size: 13px;\n font-weight: 500;\n height: 34px;\n padding: 0 16px;\n border-radius: 17px;\n border: 1px solid var(--border-l2);\n background: transparent;\n color: var(--text-primary);\n cursor: pointer;\n text-decoration: none;\n }\n .btn:hover { background: rgba(128, 128, 128, 0.1); }\n .btn-primary { background: var(--brand-primary); color: #ffffff; border-color: transparent; }\n </style>\n</head>\n<body>\n <div class=\"card\">\n ${ok ? `\n <div class=\"icon-wrap icon-success\">✓</div>\n <h1 id=\"title\">Sign-in Successful</h1>\n <p id=\"desc\">Your Antigravity account ${email ? '<strong>' + escapeHtml(email) + '</strong> ' : ''}has been authorized and saved.</p>\n <p style=\"font-size:12px;opacity:0.8;\" id=\"closing\">This window will close automatically...</p>\n <button class=\"btn\" onclick=\"window.close()\">Close Window</button>\n ` : `\n <div class=\"icon-wrap icon-error\">✕</div>\n <h1 id=\"title\">Sign-in Failed</h1>\n <p id=\"desc\">Error details: ${escapeHtml(error || 'Unknown error')}</p>\n <a class=\"btn btn-primary\" href=\"${escapeHtml(baseUrl)}/\">Return to Settings</a>\n `}\n </div>\n <script>\n const I18N = ${i18nJson};\n const lang = localStorage.getItem('agy_lang') || (navigator.language.startsWith('zh') ? 'zh' : 'en');\n const dict = I18N[lang] || I18N.en;\n\n if (${ok ? 'true' : 'false'}) {\n document.getElementById('title').textContent = dict.loginSuccessTitle;\n // textContent, not innerHTML: an email containing markup must never be\n // parsed as HTML. The string itself is serialized for the script data\n // state by jsonForInlineScript, so it cannot terminate this element.\n document.getElementById('desc').textContent = dict.loginSuccessDesc + (${jsonForInlineScript(email ? ' (' + email + ')' : '')});\n document.getElementById('closing').textContent = dict.windowClosing;\n \n // Notify parent window & close\n try {\n if (window.opener) {\n // Target this page's own origin rather than '*', so the message cannot\n // be delivered to an unrelated opener.\n window.opener.postMessage({ type: 'agy_login_success' }, window.location.origin);\n }\n } catch (e) {}\n setTimeout(() => {\n window.close();\n }, 1800);\n } else {\n document.getElementById('title').textContent = dict.loginFailedTitle;\n }\n </script>\n</body>\n</html>`\n}\n","/**\n * dsh-agy web entry.\n *\n * Two things are registered, and they deliberately use different transports:\n *\n * 1. **Management RPC at `/api/agy`** (`connection.fetch.register`). The inline\n * Settings UI calls methods over DSH's own RPC channel. This replaces the\n * previous bare `ctx.webServer.register` route table, which had no\n * authentication of its own: `/api/*` sits behind the host's browser-trust\n * fence and BrowserAuth, so the management surface is no longer reachable by\n * anything that can merely open a socket to a loopback port.\n *\n * 2. **The OAuth callback at `/agy/oauth-callback`** stays a real HTTP route.\n * Google redirects a browser to it with a GET, which the management RPC\n * channel (POST-only) cannot carry.\n *\n * BOTH `webServer` and `connection` are deliberately reached lazily rather than\n * statically injected. Each exists only in a Web composition, and a statically\n * injected service that never appears leaves this entry permanently pending —\n * and the loader treats a pending entry as a FAILED PROFILE, not a skipped one:\n *\n * dsh: plugin tree failed to load: dsh: 1 entry did not activate\n * dsh-agy/web: pending (waiting for service: webServer)\n *\n * Measured on a real TUI profile, where no provider of `webServer` is mounted.\n * `ctx.inject([...])` keeps this entry active and inert instead, so a TUI or\n * headless profile boots with no management surface rather than not booting.\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\n// Type-only: merges the `llm/adapters-updated` event into the Cordis Events map.\nimport type {} from '@deepseek-ai/dsh-llm/types'\nimport { createAgyRuntime } from '../plugin-common.ts'\nimport { isAgyDisabled } from '../runtime/risk.ts'\nimport { createAgyManagement } from './management.ts'\nimport { renderCallbackHtml } from './page.ts'\n\nexport const name = 'dsh-agy-web'\n\n/** The one service every composition provides; the rest are resolved lazily. */\nexport const inject = ['llm']\n\n/** The slice of the host's web-server service this entry uses. */\ninterface WebServerLike {\n register(route: {\n kind: 'exact'\n path: string\n handler: (req: IncomingMessage, res: ServerResponse) => void\n }): () => void\n host?: string\n}\n\nexport function apply(ctx: Context): void {\n if (isAgyDisabled()) {\n ctx.logger.warn('[dsh-agy] disabled by DSH_AGY_DISABLE=1 — skipping web registration')\n return\n }\n // Lazily, like `connection` below: see the module docblock for why a static\n // `webServer` is a profile-breaking bug rather than a nicety.\n ctx.inject(['webServer'], (webCtx) => {\n const webServer = webCtx.get('webServer') as WebServerLike | undefined\n if (!webServer) return\n webCtx.effect(() => registerAgyWeb(webCtx, webServer))\n })\n}\n\n/**\n * Register the OAuth callback route, and the management RPC once `connection`\n * appears.\n * @param ctx - the web-server-bearing context (owns both registrations).\n * @param webServer - the host web server.\n * @returns disposer for every registration this function made.\n */\nasync function registerAgyWeb(ctx: Context, webServer: WebServerLike): Promise<() => void> {\n // Host/port come from the webStartup provider (CLI args); the fallbacks are\n // DSH's own web-app defaults (loopback + 3080), never a user override.\n const webStartup = ctx.get('webStartup') as { host?: string; port?: number } | undefined\n const host = webStartup?.host ?? '127.0.0.1'\n const port = webStartup?.port ?? 3080\n\n // The OAuth callback manages credentials with no authentication of its own,\n // so it must never be reachable from the network. When the web server binds\n // a non-loopback interface, refuse to register it (the loopback-only OAuth\n // redirect would be unusable there anyway).\n const bindHost = webServer.host ?? host\n if (!['127.0.0.1', 'localhost', '::1'].includes(bindHost)) {\n ctx.logger.warn(\n '[dsh-agy] web server bound to \"' + bindHost + '\" (non-loopback): not registering the agy routes ' +\n '(they manage account credentials and must stay loopback-only). Bind the web server to 127.0.0.1 to enable them.',\n )\n return () => {}\n }\n\n const { store, sessions, adapter, stats, modelVisibility, thinkingBudget } = await createAgyRuntime(ctx)\n const baseUrl = `http://${host}:${port}`\n const management = createAgyManagement({\n store,\n sessions,\n stats,\n modelVisibility,\n // Expose only the two operations the RPC needs, not the whole store.\n thinkingBudget: {\n all: () => thinkingBudget.all(),\n set: (level, value) => thinkingBudget.setBudget(level, value),\n claude: () => thinkingBudget.claudeBudget(),\n setClaude: (value) => thinkingBudget.setClaudeBudget(value).claudeBudget,\n tiered: () => thinkingBudget.tieredBudget(),\n setTiered: (value) => thinkingBudget.setTieredBudget(value).tieredBudget,\n },\n // The adapter's *unfiltered* catalog, so a hidden model still appears in\n // the settings list alongside the switch that un-hides it.\n listAllModels: () => adapter.listAllModels(),\n baseUrl,\n // DSH refreshes the model picker on `llm/adapters-updated`; the hidden\n // list lives in agy's own file, so nothing else would announce the change\n // and the toggle would appear to do nothing until a page reload.\n // (`llm/adapters-updated` is declared `@mode emit` for exactly this kind\n // of registry notification.)\n notifyModelsChanged: () => { ctx.emit('llm/adapters-updated') },\n })\n const disposers: Array<() => void> = []\n\n // The one endpoint that cannot ride the RPC channel: Google sends the\n // browser here with a GET redirect.\n disposers.push(webServer.register({\n kind: 'exact',\n path: '/agy/oauth-callback',\n handler: async (req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url ?? '/', baseUrl)\n const result = await management.handleCallback(url.searchParams).catch((error: unknown) => ({\n ok: false as const,\n error: error instanceof Error ? error.message : String(error),\n }))\n res.writeHead(result.ok ? 200 : 400, { 'content-type': 'text/html; charset=utf-8' })\n res.end(result.ok\n ? renderCallbackHtml({ ok: true, email: result.email ?? null, baseUrl })\n : renderCallbackHtml({ ok: false, error: result.error ?? 'Unknown error', baseUrl }))\n },\n }))\n\n // Lazy injection: see the module docblock for why this is not static.\n ctx.inject(['connection'], (connectionCtx) => {\n const connection = connectionCtx.get('connection') as\n | {\n fetch: {\n register(route: {\n path: string\n methods: string[]\n requestBody: 'buffered' | 'streaming'\n fetch: (request: Request) => Promise<Response>\n }): () => void\n }\n }\n | undefined\n if (!connection || typeof connection.fetch?.register !== 'function') {\n connectionCtx.logger.warn('[dsh-agy] connection.fetch unavailable — management RPC not registered')\n return\n }\n connectionCtx.effect(() => connection.fetch.register({\n path: '/api/agy',\n methods: ['POST'],\n requestBody: 'buffered',\n async fetch(request: Request): Promise<Response> {\n if (request.method !== 'POST') return new Response('method not allowed', { status: 405 })\n const contentType = request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase()\n if (contentType !== 'application/json') {\n return new Response('content type must be application/json', { status: 415 })\n }\n let message: Record<string, unknown>\n try {\n message = await request.json() as Record<string, unknown>\n } catch {\n return new Response('body is not JSON', { status: 400 })\n }\n const rpcId = typeof message.rpcId === 'string' ? message.rpcId : 'invalid-request'\n const call = message.payload as { method?: unknown; payload?: unknown } | undefined\n if (\n message.type !== 'client-request'\n || typeof message.rpcId !== 'string'\n || typeof call?.method !== 'string'\n ) {\n return reply(rpcId, {\n ok: false,\n error: { code: 'agy/bad-request', message: 'Invalid agy management request.' },\n })\n }\n try {\n const value = await management.call(call.method, call.payload)\n return reply(rpcId, { ok: true, value })\n } catch (error) {\n const text = error instanceof Error ? error.message : String(error)\n connectionCtx.logger.warn(`[dsh-agy] ${call.method} failed: ${text}`)\n // A structured failure, not a bare 500: the client's unwrap needs a\n // parseable envelope or a failed call looks like \"nothing happened\".\n return reply(rpcId, { ok: false, error: { code: 'agy/handler-failed', message: text } })\n }\n },\n }), 'dsh-agy: /api/agy management RPC')\n })\n\n return () => {\n for (const dispose of disposers) dispose()\n }\n}\n\n/**\n * Wrap one result in the Connection RPC response envelope.\n *\n * A failure MUST carry `error.details` as an object: the client's envelope\n * parser rejects a failure without it as \"invalid server-response\", which hides\n * the real message behind a transport-sounding error.\n */\nfunction reply(rpcId: string, result: unknown): Response {\n const normalized = isFailure(result)\n ? { ...result, error: { details: {}, ...result.error } }\n : result\n return Response.json({ type: 'server-response', rpcId, result: normalized })\n}\n\n/** Whether one result is an RPC failure envelope. */\nfunction isFailure(value: unknown): value is {\n ok: false\n error: { code?: string, message?: string, details?: unknown }\n} {\n return typeof value === 'object' && value !== null\n && (value as { ok?: unknown }).ok === false\n && typeof (value as { error?: unknown }).error === 'object'\n && (value as { error?: unknown }).error !== null\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"packageManager": "pnpm@11.10.0",
|
|
5
5
|
"description": "Google Antigravity (agy) OAuth auth + model access plugin for DeepSeek Harness: multi-account pool, 429 rotation, device fingerprinting, CLI and web login.",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,12 @@
|
|
|
29
29
|
"client": {
|
|
30
30
|
"platform": "web",
|
|
31
31
|
"inject": [
|
|
32
|
-
"@deepseek-ai/dsh-client-
|
|
32
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
33
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
34
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
35
|
+
],
|
|
36
|
+
"external": [
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-primitives"
|
|
33
38
|
]
|
|
34
39
|
}
|
|
35
40
|
},
|
|
@@ -38,7 +43,7 @@
|
|
|
38
43
|
},
|
|
39
44
|
"scripts": {
|
|
40
45
|
"build": "tsdown",
|
|
41
|
-
"typecheck": "tsc --noEmit",
|
|
46
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.client.json",
|
|
42
47
|
"test": "vitest run",
|
|
43
48
|
"test:watch": "vitest",
|
|
44
49
|
"record:fixtures": "tsx scripts/record-fixture.ts",
|
|
@@ -50,7 +55,12 @@
|
|
|
50
55
|
"verify:blocks": "tsx scripts/verify-blocks.mts",
|
|
51
56
|
"verify:claude-cap": "tsx scripts/verify-claude-cap.mts",
|
|
52
57
|
"verify:proxy-routing": "tsx scripts/verify-proxy-routing.mts",
|
|
53
|
-
"verify:
|
|
58
|
+
"verify:version-freshness": "tsx scripts/verify-version-freshness.mts",
|
|
59
|
+
"verify:official-identity": "tsx scripts/verify-official-identity.mts",
|
|
60
|
+
"verify:metadata-acceptance": "tsx scripts/verify-metadata-acceptance.mts",
|
|
61
|
+
"verify:claude-parts": "tsx scripts/verify-claude-parts.mts",
|
|
62
|
+
"verify:thinking-config": "tsx scripts/verify-thinking-config.mts",
|
|
63
|
+
"verify:thinking-levels": "tsx scripts/verify-thinking-levels.mts"
|
|
54
64
|
},
|
|
55
65
|
"peerDependencies": {
|
|
56
66
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
@@ -74,7 +84,9 @@
|
|
|
74
84
|
"socks-proxy-agent": "^8.0.5"
|
|
75
85
|
},
|
|
76
86
|
"devDependencies": {
|
|
87
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
|
|
77
88
|
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
|
|
89
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.1",
|
|
78
90
|
"@deepseek-ai/dsh-client-ui-settings": "^0.1.1-rc.2",
|
|
79
91
|
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.1-rc.2",
|
|
80
92
|
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
|