dsh-agy 0.1.0 → 0.1.1
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 +22 -1
- package/lib/cli/index.d.mts.map +1 -1
- package/lib/cli/index.mjs +49 -11
- package/lib/cli/index.mjs.map +1 -1
- package/lib/{import-DnKzgr-0.mjs → import-DpYDhfy4.mjs} +19 -2
- package/lib/{import-DnKzgr-0.mjs.map → import-DpYDhfy4.mjs.map} +1 -1
- package/lib/web/plugin.mjs +66 -25
- package/lib/web/plugin.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# dsh-agy
|
|
2
2
|
|
|
3
|
+
[](https://github.com/chaos-03x/dsh-agy/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/dsh-agy)
|
|
5
|
+
|
|
3
6
|
Google Antigravity (agy) access for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness):
|
|
4
7
|
OAuth authentication, a multi-account pool with automatic 429 rotation, device
|
|
5
8
|
fingerprinting, and both CLI and web management.
|
|
@@ -21,6 +24,13 @@ fingerprinting, and both CLI and web management.
|
|
|
21
24
|
- **CLI**: `dsh-agy login|status|import|verify|logout` works standalone, with or
|
|
22
25
|
without a harness.
|
|
23
26
|
|
|
27
|
+
## Screenshots
|
|
28
|
+
|
|
29
|
+
The `/agy` dashboard inside DSH Web — account cards, per-model quota bars, and
|
|
30
|
+
one-shot model tests:
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
24
34
|
## Quickstart
|
|
25
35
|
|
|
26
36
|
### Path A: DSH Web GUI Users (Recommended — 100% Web UI, zero CLI commands)
|
|
@@ -57,6 +67,17 @@ dsh-agy import <file> # import agy auth.json or credential blob (--blob)
|
|
|
57
67
|
dsh-agy logout # remove account
|
|
58
68
|
```
|
|
59
69
|
|
|
70
|
+
## CLI reference
|
|
71
|
+
|
|
72
|
+
| Command | Options | Description |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `dsh-agy login` | `--headless` — print the auth URL and wait for a pasted redirect URL<br>`--blob` — print a paste-credential blob instead of storing the account<br>`--port <n>` — loopback callback port (default `51121`)<br>`--project <id>` — bind the login to a specific project<br>`--timeout <ms>` — callback timeout (default `300000`) | Interactive Google OAuth |
|
|
75
|
+
| `dsh-agy status` | — | List accounts + per-model quota summary |
|
|
76
|
+
| `dsh-agy import <files...>` | `--blob` — the pasted value is a credential blob<br>`--email <email>` — set the account email (skips userinfo verification)<br>`--overwrite` — replace an existing account with the same email | Import agy auth.json files or credential blobs (multiple files / multi-line paste = batch import) |
|
|
77
|
+
| `dsh-agy export` | `--index <n>` — export one account by index (default: all)<br>`--out <dir>` — write one `dsh-agy-<index>.blob` per account (default: print to stdout, one blob per line) | Export account credentials as paste blobs |
|
|
78
|
+
| `dsh-agy verify` | `--index <n>` — verify one account by index (default: all) | Refresh + health check |
|
|
79
|
+
| `dsh-agy logout` | `--index <n>` — account index (default: active)<br>`--email <email>` — account email | Remove an account |
|
|
80
|
+
|
|
60
81
|
### Path C: Local Development & Link
|
|
61
82
|
|
|
62
83
|
```sh
|
|
@@ -65,7 +86,7 @@ cd dsh-agy && pnpm install && pnpm run build
|
|
|
65
86
|
dsh plugin --profile web link .
|
|
66
87
|
```
|
|
67
88
|
|
|
68
|
-
Requires Node >=
|
|
89
|
+
Requires Node >= 22.
|
|
69
90
|
|
|
70
91
|
## Uninstall
|
|
71
92
|
|
package/lib/cli/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/oauth/constants.ts","../../src/cli/index.ts"],"mappings":";;;cA6Ba;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/oauth/constants.ts","../../src/cli/index.ts"],"mappings":";;;cA6Ba;;;iBCiQG,iBAAiB"}
|
package/lib/cli/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { c as loadMasterKey, d as resolveMasterKeyCodec, i as JsonAccountStore, o as createAesGcmCodec, s as deriveKey, t as AgySessionManager, u as resolveDshHome, y as AGY_DEFAULT_REDIRECT_URI } from "../session-tawi2_uh.mjs";
|
|
2
2
|
import { t as exchangeAntigravity } from "../exchange-MhFUiBAm.mjs";
|
|
3
|
-
import { n as upsertImportedAccount, r as authorizeAntigravity, t as
|
|
3
|
+
import { n as upsertImportedAccount, r as authorizeAntigravity, t as importManySources } from "../import-DpYDhfy4.mjs";
|
|
4
4
|
import { r as encodeCredentialBlob } from "../blob-D1e7_uT1.mjs";
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
5
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
6
7
|
import { Command } from "commander";
|
|
7
8
|
import { createInterface } from "node:readline/promises";
|
|
8
9
|
import { stdin, stdout } from "node:process";
|
|
@@ -249,16 +250,50 @@ async function statusCommand() {
|
|
|
249
250
|
}
|
|
250
251
|
async function importCommand(options) {
|
|
251
252
|
const store = createStore();
|
|
252
|
-
let
|
|
253
|
-
if (options.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
253
|
+
let items;
|
|
254
|
+
if (options.files && options.files.length > 0) items = options.files.map((file) => {
|
|
255
|
+
const raw = readFileSync(file, "utf8");
|
|
256
|
+
return {
|
|
257
|
+
source: options.blob ? raw : JSON.parse(raw),
|
|
258
|
+
kind: options.blob ? "blob" : "json"
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
else items = [{
|
|
262
|
+
source: await ask("Paste the agy token JSON (or blob with --blob): "),
|
|
263
|
+
kind: options.blob ? "blob" : "json"
|
|
264
|
+
}];
|
|
265
|
+
const result = await importManySources(items, store, {
|
|
258
266
|
email: options.email,
|
|
259
267
|
overwriteExisting: options.overwrite
|
|
260
268
|
});
|
|
261
|
-
console.log(
|
|
269
|
+
console.log(`Imported ${result.imported}, replaced ${result.replaced}${result.errors.length > 0 ? `, ${result.errors.length} failed` : ""}`);
|
|
270
|
+
for (const error of result.errors) console.log(` ! ${error}`);
|
|
271
|
+
}
|
|
272
|
+
async function exportCommand(options) {
|
|
273
|
+
const store = createReadOnlyStoreOrExit();
|
|
274
|
+
const sessions = new AgySessionManager({ store });
|
|
275
|
+
const storage = await store.load();
|
|
276
|
+
const indices = options.index !== void 0 ? [Number(options.index)] : storage.accounts.map((_, i) => i);
|
|
277
|
+
let exported = 0;
|
|
278
|
+
for (const index of indices) {
|
|
279
|
+
const account = storage.accounts[index];
|
|
280
|
+
if (!account) {
|
|
281
|
+
console.log(`[${index}] not found`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const result = await sessions.exportBlob(index);
|
|
285
|
+
if (!result.blob) {
|
|
286
|
+
console.log(`[${index}] ${account.email ?? ""} — FAILED: ${result.error}`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (options.out) {
|
|
290
|
+
const file = join(options.out, `dsh-agy-${index}.blob`);
|
|
291
|
+
writeFileSync(file, result.blob + "\n");
|
|
292
|
+
console.log(`[${index}] ${account.email ?? ""} — wrote ${file}`);
|
|
293
|
+
} else console.log(result.blob);
|
|
294
|
+
exported++;
|
|
295
|
+
}
|
|
296
|
+
if (!options.out) console.log(`\n${exported} blob(s) exported — one line each, paste into a remote import`);
|
|
262
297
|
}
|
|
263
298
|
async function verifyCommand(options) {
|
|
264
299
|
const store = createReadOnlyStoreOrExit();
|
|
@@ -297,12 +332,15 @@ function createProgram() {
|
|
|
297
332
|
program.command("status").description("List accounts and their health").action(async () => {
|
|
298
333
|
await statusCommand();
|
|
299
334
|
});
|
|
300
|
-
program.command("import").description("Import
|
|
335
|
+
program.command("import").description("Import agy token files or paste credentials").argument("[files...]", "paths to agy auth.json token files (multiple allowed)").option("--blob", "the pasted value is a credential blob", false).option("--email <email>", "account email (skips userinfo verification)").option("--overwrite", "replace an existing account with the same email", false).action(async (files, options) => {
|
|
301
336
|
await importCommand({
|
|
302
337
|
...options,
|
|
303
|
-
|
|
338
|
+
files
|
|
304
339
|
});
|
|
305
340
|
});
|
|
341
|
+
program.command("export").description("Export account credentials as paste blobs").option("--index <n>", "export one account by index; default all").option("--out <dir>", "write one dsh-agy-<index>.blob file per account into this directory (default: print to stdout)").action(async (options) => {
|
|
342
|
+
await exportCommand(options);
|
|
343
|
+
});
|
|
306
344
|
program.command("verify").description("Verify account credentials (refresh + userinfo)").option("--index <n>", "verify one account by index; default all").action(async (options) => {
|
|
307
345
|
await verifyCommand(options);
|
|
308
346
|
});
|
package/lib/cli/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/cli/callback-server.ts","../../src/cli/index.ts"],"sourcesContent":["/** Loopback OAuth callback server for the standalone CLI login flow. */\n\nimport { createServer } from 'node:http'\nimport type { Server } from 'node:http'\n\nexport interface CallbackResult {\n code: string\n state: string\n url: string\n}\n\nexport interface CallbackHandle {\n result: Promise<CallbackResult>\n /** Resolves once the loopback listener is bound; rejects on bind errors. */\n ready: Promise<void>\n close(): Promise<void>\n}\n\nconst SUCCESS_HTML = `<!doctype html><html><head><meta charset=\"utf-8\"><title>Login successful</title></head>\n<body style=\"font-family:system-ui;text-align:center;padding:3rem\">\n<h2>✓ Authentication successful</h2>\n<p>You can close this tab and return to the terminal.</p>\n</body></html>`\n\n/** Listen on 127.0.0.1:<port>/oauth-callback and resolve with the code+state. */\nexport function startCallbackServer(options: { port?: number; timeoutMs?: number } = {}): CallbackHandle {\n const port = options.port ?? 51121\n const timeoutMs = options.timeoutMs ?? 300_000\n\n let resolveResult: (value: CallbackResult) => void\n let rejectResult: (reason: Error) => void\n const result = new Promise<CallbackResult>((resolve, reject) => {\n resolveResult = resolve\n rejectResult = reject\n })\n\n let resolveReady: () => void\n let rejectReady: (reason: Error) => void\n const ready = new Promise<void>((resolve, reject) => {\n resolveReady = resolve\n rejectReady = reject\n })\n\n const server: Server = createServer((req, res) => {\n const url = new URL(req.url ?? '/', 'http://localhost')\n if (url.pathname === '/oauth-callback') {\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' })\n res.end('<h2>Missing code or state</h2>')\n return\n }\n res.writeHead(200, { 'Content-Type': 'text/html', connection: 'close' })\n res.end(SUCCESS_HTML)\n resolveResult({ code, state, url: url.toString() })\n // Give the browser a moment to render before the server closes.\n setTimeout(() => server.close(), 1500)\n return\n }\n res.writeHead(404)\n res.end('Not found')\n })\n\n server.on('error', (error: NodeJS.ErrnoException) => {\n if (error.code === 'EADDRINUSE') {\n rejectResult(new Error(`Port ${port} is already in use — pass --port to pick another.`))\n } else {\n rejectResult(error)\n }\n rejectReady(error)\n })\n\n server.listen(port, '127.0.0.1', () => {\n resolveReady()\n })\n\n const timeout = setTimeout(() => {\n rejectResult(new Error('Authentication timed out — no callback received.'))\n server.close()\n }, timeoutMs)\n\n const handle: CallbackHandle = {\n result,\n ready,\n async close() {\n clearTimeout(timeout)\n if (server.listening) {\n // The success page's keep-alive connection would otherwise hold\n // server.close() (and the CLI process) open indefinitely.\n server.closeAllConnections()\n await new Promise<void>((resolve) => server.close(() => resolve()))\n }\n },\n }\n return handle\n}\n\n/** Open a URL in the system browser (best effort). */\nexport async function openBrowser(url: string): Promise<boolean> {\n const { spawn } = await import('node:child_process')\n const platform = process.platform\n const command = platform === 'darwin' ? ['open', url]\n : platform === 'win32' ? ['cmd', '/c', 'start', '', url]\n : ['xdg-open', url]\n return new Promise<boolean>((resolve) => {\n const child = spawn(command[0]!, command.slice(1), { stdio: 'ignore' })\n child.on('error', () => resolve(false))\n child.on('spawn', () => resolve(true))\n })\n}\n","/**\n * dsh-agy CLI: login / status / import / verify / logout.\n * Standalone bin — the dsh launcher has no plugin subcommand slot, and the\n * remote paste-blob flow must run on machines without a harness.\n */\n\nimport { Command } from 'commander'\nimport { readFileSync } from 'node:fs'\nimport { createInterface } from 'node:readline/promises'\nimport { stdin as input, stdout as output } from 'node:process'\nimport { authorizeAntigravity } from '../oauth/authorize.ts'\nimport { exchangeAntigravity } from '../oauth/exchange.ts'\nimport { encodeCredentialBlob } from '../oauth/blob.ts'\nimport { AGY_DEFAULT_REDIRECT_URI } from '../oauth/constants.ts'\nimport { createAesGcmCodec, deriveKey, loadMasterKey, resolveDshHome, resolveMasterKeyCodec } from '../store/keyring.ts'\nimport type { SecretCodec } from '../store/keyring.ts'\nimport { JsonAccountStore } from '../store/accounts.ts'\nimport { AgySessionManager } from '../session.ts'\nimport { startCallbackServer, openBrowser } from './callback-server.ts'\nimport { parseImportSource, upsertImportedAccount } from './import.ts'\n\n/** Package version, read from the shipped package.json — never hard-coded twice. */\nconst { version: PACKAGE_VERSION } = JSON.parse(\n readFileSync(new URL('../../package.json', import.meta.url), 'utf8'),\n) as { version: string }\n\nfunction createStore(options: { readOnly?: boolean } = {}): JsonAccountStore {\n const dshHome = resolveDshHome()\n let codec: SecretCodec\n if (options.readOnly) {\n // Read-only commands must never create the master key or the credentials\n // document: they would fail for no reason on a read-only HOME and write\n // files nobody asked for anywhere else.\n const masterKey = loadMasterKey(dshHome)\n if (!masterKey) {\n throw new Error('No agy account store found — run `dsh-agy login` first.')\n }\n codec = createAesGcmCodec(deriveKey(masterKey))\n } else {\n codec = resolveMasterKeyCodec(dshHome).codec\n }\n return new JsonAccountStore({ file: `${dshHome}/agy-accounts.json`, codec })\n}\n\n/** Read-only store with a friendly error when no credentials exist yet. */\nfunction createReadOnlyStoreOrExit(): JsonAccountStore {\n try {\n return createStore({ readOnly: true })\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error))\n process.exit(1)\n }\n}\n\nasync function ask(question: string): Promise<string> {\n const rl = createInterface({ input, output })\n try {\n return (await rl.question(question)).trim()\n } finally {\n rl.close()\n }\n}\n\n/** Remote/SSH sessions cannot reach a local browser; auto-select headless paste. */\nfunction isRemoteSession(): boolean {\n return !!(process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY)\n}\n\nasync function loginCommand(options: { headless: boolean; blob: boolean; port: number; project?: string; timeout?: string }) {\n const store = createStore()\n const redirectUri = `http://localhost:${options.port}/oauth-callback`\n if (!options.headless && isRemoteSession()) {\n console.log('(SSH session detected — using headless paste flow; pass --headless explicitly to force)')\n options.headless = true\n }\n\n let callback: Awaited<ReturnType<typeof startCallbackServer>> | undefined\n if (!options.headless) {\n // Bind the loopback listener BEFORE the URL is shown: a local process\n // squatting on the fixed port could otherwise capture code+state (the\n // state carries the PKCE verifier) and steal the refresh token.\n callback = startCallbackServer({ port: options.port, timeoutMs: Number(options.timeout) || 300_000 })\n try {\n await callback.ready\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error))\n process.exit(1)\n }\n }\n\n const { url, verifier } = await authorizeAntigravity(redirectUri, options.project ?? '')\n console.log(`\\nOpen this URL in a browser to authorize:\\n\\n ${url}\\n`)\n\n let code: string\n let state: string\n\n if (options.headless) {\n const pasted = await ask('After approving, paste the full redirected URL here: ')\n const parsed = new URL(pasted)\n code = parsed.searchParams.get('code') ?? ''\n state = parsed.searchParams.get('state') ?? ''\n if (!code || !state) {\n console.error('Error: pasted URL is missing code or state.')\n process.exit(1)\n }\n } else {\n const opened = await openBrowser(url)\n if (!opened) console.log('(Could not open a browser automatically — open the URL manually.)')\n let callbackResult: { code: string; state: string; url: string }\n try {\n callbackResult = await callback!.result\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error))\n process.exit(1)\n }\n code = callbackResult.code\n state = callbackResult.state\n }\n\n // Bind the exchange to the verifier we issued: a state from any other login\n // (pasted from another session, or fabricated) must be rejected.\n const result = await exchangeAntigravity(code, state, redirectUri, verifier)\n if (result.type === 'failed') {\n console.error(`Login failed: ${result.error}`)\n process.exit(1)\n }\n\n if (options.blob) {\n const blob = encodeCredentialBlob('agy', {\n access_token: result.access,\n refresh_token: result.refresh.split('|')[0],\n expires_in: Math.max(0, Math.round((result.expires - Date.now()) / 1000)),\n })\n console.log(`\\nPaste this blob into the remote dashboard/CLI:\\n\\n${blob}\\n`)\n } else {\n const { account, created } = 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 }, { overwriteExisting: true })\n console.log(`${created ? 'Added' : 'Updated'} account: ${account.email ?? '(no email)'} (project: ${result.projectId || 'default'})`)\n }\n\n await callback?.close()\n}\n\nasync function statusCommand() {\n const store = createReadOnlyStoreOrExit()\n const storage = await store.load()\n if (storage.accounts.length === 0) {\n console.log('No agy accounts. Run `dsh-agy login` first.')\n return\n }\n console.log(`\\n${storage.accounts.length} account(s), active index ${storage.activeIndex}:\\n`)\n const sessions = new AgySessionManager({ store })\n for (const [index, account] of storage.accounts.entries()) {\n const marker = index === storage.activeIndex ? '★' : ' '\n const state = account.enabled === false ? 'disabled'\n : account.verificationRequired ? 'verification-required'\n : account.coolingDownUntil && account.coolingDownUntil > Date.now() ? 'cooling'\n : 'active'\n console.log(` ${marker} [${index}] ${account.email ?? '(no email)'} — ${state}${account.projectId ? ` (project: ${account.projectId})` : ''}`)\n\n // Best-effort quota summary via fetchAvailableModels (fresh access token).\n const session = await sessions.getSession().catch(() => undefined)\n if (session && session.index === index) {\n try {\n const { fetchAvailableModels } = await import('../adapter/models.ts')\n const discovered = await fetchAvailableModels(session.auth.access, session.account.projectId)\n const entries = Object.entries(discovered.models ?? {})\n if (entries.length > 0) {\n const withQuota = entries\n .map(([id, entry]) => ({ id, ...entry.quotaInfo }))\n .filter((e) => typeof e.remainingFraction === 'number')\n .sort((a, b) => (a.remainingFraction ?? 0) - (b.remainingFraction ?? 0))\n if (withQuota.length > 0) {\n const lowest = withQuota[0]!\n console.log(` models: ${entries.length}, lowest quota: ${Math.round((lowest.remainingFraction ?? 0) * 100)}% (${lowest.id})${lowest.resetTime ? `, resets ${new Date(lowest.resetTime).toISOString()}` : ''}`)\n } else {\n console.log(` models: ${entries.length} (no per-model quota reported)`)\n }\n }\n } catch (error) {\n console.log(` quota: unavailable (${error instanceof Error ? error.message : String(error)})`)\n }\n }\n }\n}\n\nasync function importCommand(options: { blob: boolean; file?: string; email?: string; overwrite: boolean }) {\n const store = createStore()\n let source: unknown\n if (options.file) {\n source = JSON.parse(readFileSync(options.file, 'utf8')) as unknown\n } else {\n const pasted = await ask('Paste the agy token JSON (or blob with --blob): ')\n source = pasted\n }\n const kind = options.blob ? 'blob' : 'json'\n const enriched = await parseImportSource(source, kind)\n const { account, created } = await upsertImportedAccount(store, enriched, {\n email: options.email,\n overwriteExisting: options.overwrite,\n })\n console.log(`${created ? 'Imported' : 'Replaced'} account: ${account.email ?? '(no email)'}${enriched.projectId ? ` (project: ${enriched.projectId})` : ''}`)\n}\n\nasync function verifyCommand(options: { index?: string }) {\n const store = createReadOnlyStoreOrExit()\n const sessions = new AgySessionManager({ store })\n const storage = await store.load()\n const indices = options.index !== undefined\n ? [Number(options.index)]\n : storage.accounts.map((_, i) => i)\n\n for (const index of indices) {\n const account = storage.accounts[index]\n if (!account) {\n console.log(`[${index}] not found`)\n continue\n }\n const result = await sessions.verifyAccount(index)\n if (result.ok) {\n console.log(`[${index}] ${account.email ?? ''} — OK${result.email && result.email !== account.email ? ` (userinfo: ${result.email})` : ''}`)\n } else {\n console.log(`[${index}] ${account.email ?? ''} — FAILED: ${result.error}`)\n }\n }\n}\n\nasync function logoutCommand(options: { index?: string; email?: string }) {\n const store = createReadOnlyStoreOrExit()\n await store.mutate((storage) => {\n const index = options.index !== undefined\n ? Number(options.index)\n : options.email\n ? storage.accounts.findIndex((a) => a.email?.toLowerCase() === options.email!.toLowerCase())\n : storage.activeIndex\n if (index < 0 || index >= storage.accounts.length) {\n throw new Error(`account not found (index ${index})`)\n }\n const [removed] = storage.accounts.splice(index, 1)\n if (storage.activeIndex >= storage.accounts.length) storage.activeIndex = 0\n console.log(`Removed account: ${removed?.email ?? `[${index}]`}`)\n })\n}\n\nexport function createProgram(): Command {\n const program = new Command()\n program\n .name('dsh-agy')\n .description('Google Antigravity (agy) account management for DeepSeek Harness')\n .version(PACKAGE_VERSION)\n\n program\n .command('login')\n .description('OAuth login (browser, or headless paste)')\n .option('--headless', 'print the URL and wait for a pasted redirect URL', false)\n .option('--blob', 'print a paste-credential blob instead of storing the account', false)\n .option('--port <n>', 'loopback callback port', '51121')\n .option('--project <id>', 'bind the login to a specific project')\n .option('--timeout <ms>', 'callback timeout', '300000')\n .action(async (options) => {\n await loginCommand({ ...options, timeout: options.timeout })\n })\n\n program\n .command('status')\n .description('List accounts and their health')\n .action(async () => {\n await statusCommand()\n })\n\n program\n .command('import')\n .description('Import an agy token file or paste credentials')\n .argument('[file]', 'path to an agy auth.json token file')\n .option('--blob', 'the pasted value is a credential blob', false)\n .option('--email <email>', 'account email (skips userinfo verification)')\n .option('--overwrite', 'replace an existing account with the same email', false)\n .action(async (file: string | undefined, options: { blob: boolean; email?: string; overwrite: boolean }) => {\n await importCommand({ ...options, file })\n })\n\n program\n .command('verify')\n .description('Verify account credentials (refresh + userinfo)')\n .option('--index <n>', 'verify one account by index; default all')\n .action(async (options: { index?: string }) => {\n await verifyCommand(options)\n })\n\n program\n .command('logout')\n .description('Remove an account')\n .option('--index <n>', 'account index (default: active)')\n .option('--email <email>', 'account email')\n .action(async (options: { index?: string; email?: string }) => {\n await logoutCommand(options)\n })\n\n return program\n}\n\nexport { AGY_DEFAULT_REDIRECT_URI }\n"],"mappings":";;;;;;;;;;;AAkBA,MAAM,eAAe;;;;;;AAOrB,SAAgB,oBAAoB,UAAiD,CAAC,GAAmB;CACvG,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,YAAY,QAAQ,aAAa;CAEvC,IAAI;CACJ,IAAI;CACJ,MAAM,SAAS,IAAI,SAAyB,SAAS,WAAW;EAC9D,gBAAgB;EAChB,eAAe;CACjB,CAAC;CAED,IAAI;CACJ,IAAI;CACJ,MAAM,QAAQ,IAAI,SAAe,SAAS,WAAW;EACnD,eAAe;EACf,cAAc;CAChB,CAAC;CAED,MAAM,SAAiB,cAAc,KAAK,QAAQ;EAChD,MAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,kBAAkB;EACtD,IAAI,IAAI,aAAa,mBAAmB;GACtC,MAAM,OAAO,IAAI,aAAa,IAAI,MAAM;GACxC,MAAM,QAAQ,IAAI,aAAa,IAAI,OAAO;GAC1C,IAAI,CAAC,QAAQ,CAAC,OAAO;IACnB,IAAI,UAAU,KAAK,EAAE,gBAAgB,YAAY,CAAC;IAClD,IAAI,IAAI,gCAAgC;IACxC;GACF;GACA,IAAI,UAAU,KAAK;IAAE,gBAAgB;IAAa,YAAY;GAAQ,CAAC;GACvE,IAAI,IAAI,YAAY;GACpB,cAAc;IAAE;IAAM;IAAO,KAAK,IAAI,SAAS;GAAE,CAAC;GAElD,iBAAiB,OAAO,MAAM,GAAG,IAAI;GACrC;EACF;EACA,IAAI,UAAU,GAAG;EACjB,IAAI,IAAI,WAAW;CACrB,CAAC;CAED,OAAO,GAAG,UAAU,UAAiC;EACnD,IAAI,MAAM,SAAS,cACjB,6BAAa,IAAI,MAAM,QAAQ,KAAK,kDAAkD,CAAC;OAEvF,aAAa,KAAK;EAEpB,YAAY,KAAK;CACnB,CAAC;CAED,OAAO,OAAO,MAAM,mBAAmB;EACrC,aAAa;CACf,CAAC;CAED,MAAM,UAAU,iBAAiB;EAC/B,6BAAa,IAAI,MAAM,kDAAkD,CAAC;EAC1E,OAAO,MAAM;CACf,GAAG,SAAS;CAeZ,OAAO;EAZL;EACA;EACA,MAAM,QAAQ;GACZ,aAAa,OAAO;GACpB,IAAI,OAAO,WAAW;IAGpB,OAAO,oBAAoB;IAC3B,MAAM,IAAI,SAAe,YAAY,OAAO,YAAY,QAAQ,CAAC,CAAC;GACpE;EACF;CAEU;AACd;;AAGA,eAAsB,YAAY,KAA+B;CAC/D,MAAM,EAAE,UAAU,MAAM,OAAO;CAC/B,MAAM,WAAW,QAAQ;CACzB,MAAM,UAAU,aAAa,WAAW,CAAC,QAAQ,GAAG,IAChD,aAAa,UAAU;EAAC;EAAO;EAAM;EAAS;EAAI;CAAG,IACrD,CAAC,YAAY,GAAG;CACpB,OAAO,IAAI,SAAkB,YAAY;EACvC,MAAM,QAAQ,MAAM,QAAQ,IAAK,QAAQ,MAAM,CAAC,GAAG,EAAE,OAAO,SAAS,CAAC;EACtE,MAAM,GAAG,eAAe,QAAQ,KAAK,CAAC;EACtC,MAAM,GAAG,eAAe,QAAQ,IAAI,CAAC;CACvC,CAAC;AACH;;;;;;;;;ACxFA,MAAM,EAAE,SAAS,oBAAoB,KAAK,MACxC,aAAa,IAAI,IAAI,sBAAsB,YAAY,GAAG,GAAG,MAAM,CACrE;AAEA,SAAS,YAAY,UAAkC,CAAC,GAAqB;CAC3E,MAAM,UAAU,eAAe;CAC/B,IAAI;CACJ,IAAI,QAAQ,UAAU;EAIpB,MAAM,YAAY,cAAc,OAAO;EACvC,IAAI,CAAC,WACH,MAAM,IAAI,MAAM,yDAAyD;EAE3E,QAAQ,kBAAkB,UAAU,SAAS,CAAC;CAChD,OACE,QAAQ,sBAAsB,OAAO,CAAC,CAAC;CAEzC,OAAO,IAAI,iBAAiB;EAAE,MAAM,GAAG,QAAQ;EAAqB;CAAM,CAAC;AAC7E;;AAGA,SAAS,4BAA8C;CACrD,IAAI;EACF,OAAO,YAAY,EAAE,UAAU,KAAK,CAAC;CACvC,SAAS,OAAO;EACd,QAAQ,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;EACpE,QAAQ,KAAK,CAAC;CAChB;AACF;AAEA,eAAe,IAAI,UAAmC;CACpD,MAAM,KAAK,gBAAgB;EAAE,OAAA;EAAO,QAAA;CAAO,CAAC;CAC5C,IAAI;EACF,QAAQ,MAAM,GAAG,SAAS,QAAQ,EAAA,CAAG,KAAK;CAC5C,UAAU;EACR,GAAG,MAAM;CACX;AACF;;AAGA,SAAS,kBAA2B;CAClC,OAAO,CAAC,EAAE,QAAQ,IAAI,kBAAkB,QAAQ,IAAI,cAAc,QAAQ,IAAI;AAChF;AAEA,eAAe,aAAa,SAAiG;CAC3H,MAAM,QAAQ,YAAY;CAC1B,MAAM,cAAc,oBAAoB,QAAQ,KAAK;CACrD,IAAI,CAAC,QAAQ,YAAY,gBAAgB,GAAG;EAC1C,QAAQ,IAAI,yFAAyF;EACrG,QAAQ,WAAW;CACrB;CAEA,IAAI;CACJ,IAAI,CAAC,QAAQ,UAAU;EAIrB,WAAW,oBAAoB;GAAE,MAAM,QAAQ;GAAM,WAAW,OAAO,QAAQ,OAAO,KAAK;EAAQ,CAAC;EACpG,IAAI;GACF,MAAM,SAAS;EACjB,SAAS,OAAO;GACd,QAAQ,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;GACpE,QAAQ,KAAK,CAAC;EAChB;CACF;CAEA,MAAM,EAAE,KAAK,aAAa,MAAM,qBAAqB,aAAa,QAAQ,WAAW,EAAE;CACvF,QAAQ,IAAI,mDAAmD,IAAI,GAAG;CAEtE,IAAI;CACJ,IAAI;CAEJ,IAAI,QAAQ,UAAU;EACpB,MAAM,SAAS,MAAM,IAAI,uDAAuD;EAChF,MAAM,SAAS,IAAI,IAAI,MAAM;EAC7B,OAAO,OAAO,aAAa,IAAI,MAAM,KAAK;EAC1C,QAAQ,OAAO,aAAa,IAAI,OAAO,KAAK;EAC5C,IAAI,CAAC,QAAQ,CAAC,OAAO;GACnB,QAAQ,MAAM,6CAA6C;GAC3D,QAAQ,KAAK,CAAC;EAChB;CACF,OAAO;EAEL,IAAI,CAAC,MADgB,YAAY,GAAG,GACvB,QAAQ,IAAI,mEAAmE;EAC5F,IAAI;EACJ,IAAI;GACF,iBAAiB,MAAM,SAAU;EACnC,SAAS,OAAO;GACd,QAAQ,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;GACpE,QAAQ,KAAK,CAAC;EAChB;EACA,OAAO,eAAe;EACtB,QAAQ,eAAe;CACzB;CAIA,MAAM,SAAS,MAAM,oBAAoB,MAAM,OAAO,aAAa,QAAQ;CAC3E,IAAI,OAAO,SAAS,UAAU;EAC5B,QAAQ,MAAM,iBAAiB,OAAO,OAAO;EAC7C,QAAQ,KAAK,CAAC;CAChB;CAEA,IAAI,QAAQ,MAAM;EAChB,MAAM,OAAO,qBAAqB,OAAO;GACvC,cAAc,OAAO;GACrB,eAAe,OAAO,QAAQ,MAAM,GAAG,CAAC,CAAC;GACzC,YAAY,KAAK,IAAI,GAAG,KAAK,OAAO,OAAO,UAAU,KAAK,IAAI,KAAK,GAAI,CAAC;EAC1E,CAAC;EACD,QAAQ,IAAI,uDAAuD,KAAK,GAAG;CAC7E,OAAO;EACL,MAAM,EAAE,SAAS,YAAY,MAAM,sBAAsB,OAAO;GAC9D,aAAa,OAAO;GACpB,cAAc,OAAO,QAAQ,MAAM,GAAG,CAAC,CAAC;GACxC,WAAW;GACX,WAAW,IAAI,KAAK,OAAO,OAAO,CAAC,CAAC,YAAY;GAChD,YAAY;GACZ,OAAO,OAAO,SAAS;GACvB,WAAW,OAAO,aAAa;EACjC,GAAG,EAAE,mBAAmB,KAAK,CAAC;EAC9B,QAAQ,IAAI,GAAG,UAAU,UAAU,UAAU,YAAY,QAAQ,SAAS,aAAa,aAAa,OAAO,aAAa,UAAU,EAAE;CACtI;CAEA,MAAM,UAAU,MAAM;AACxB;AAEA,eAAe,gBAAgB;CAC7B,MAAM,QAAQ,0BAA0B;CACxC,MAAM,UAAU,MAAM,MAAM,KAAK;CACjC,IAAI,QAAQ,SAAS,WAAW,GAAG;EACjC,QAAQ,IAAI,6CAA6C;EACzD;CACF;CACA,QAAQ,IAAI,KAAK,QAAQ,SAAS,OAAO,4BAA4B,QAAQ,YAAY,IAAI;CAC7F,MAAM,WAAW,IAAI,kBAAkB,EAAE,MAAM,CAAC;CAChD,KAAK,MAAM,CAAC,OAAO,YAAY,QAAQ,SAAS,QAAQ,GAAG;EACzD,MAAM,SAAS,UAAU,QAAQ,cAAc,MAAM;EACrD,MAAM,QAAQ,QAAQ,YAAY,QAAQ,aACtC,QAAQ,uBAAuB,0BAC/B,QAAQ,oBAAoB,QAAQ,mBAAmB,KAAK,IAAI,IAAI,YACpE;EACJ,QAAQ,IAAI,IAAI,OAAO,IAAI,MAAM,IAAI,QAAQ,SAAS,aAAa,KAAK,QAAQ,QAAQ,YAAY,cAAc,QAAQ,UAAU,KAAK,IAAI;EAG7I,MAAM,UAAU,MAAM,SAAS,WAAW,CAAC,CAAC,YAAY,KAAA,CAAS;EACjE,IAAI,WAAW,QAAQ,UAAU,OAC/B,IAAI;GACF,MAAM,EAAE,yBAAyB,MAAM,OAAO,yBAAuB,CAAA,MAAA,MAAA,EAAA,CAAA;GACrE,MAAM,aAAa,MAAM,qBAAqB,QAAQ,KAAK,QAAQ,QAAQ,QAAQ,SAAS;GAC5F,MAAM,UAAU,OAAO,QAAQ,WAAW,UAAU,CAAC,CAAC;GACtD,IAAI,QAAQ,SAAS,GAAG;IACtB,MAAM,YAAY,QACf,KAAK,CAAC,IAAI,YAAY;KAAE;KAAI,GAAG,MAAM;IAAU,EAAE,CAAC,CAClD,QAAQ,MAAM,OAAO,EAAE,sBAAsB,QAAQ,CAAC,CACtD,MAAM,GAAG,OAAO,EAAE,qBAAqB,MAAM,EAAE,qBAAqB,EAAE;IACzE,IAAI,UAAU,SAAS,GAAG;KACxB,MAAM,SAAS,UAAU;KACzB,QAAQ,IAAI,kBAAkB,QAAQ,OAAO,kBAAkB,KAAK,OAAO,OAAO,qBAAqB,KAAK,GAAG,EAAE,KAAK,OAAO,GAAG,GAAG,OAAO,YAAY,YAAY,IAAI,KAAK,OAAO,SAAS,CAAC,CAAC,YAAY,MAAM,IAAI;IACrN,OACE,QAAQ,IAAI,kBAAkB,QAAQ,OAAO,+BAA+B;GAEhF;EACF,SAAS,OAAO;GACd,QAAQ,IAAI,8BAA8B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,EAAE;EACrG;CAEJ;AACF;AAEA,eAAe,cAAc,SAA+E;CAC1G,MAAM,QAAQ,YAAY;CAC1B,IAAI;CACJ,IAAI,QAAQ,MACV,SAAS,KAAK,MAAM,aAAa,QAAQ,MAAM,MAAM,CAAC;MAGtD,SAAS,MADY,IAAI,kDAAkD;CAG7E,MAAM,OAAO,QAAQ,OAAO,SAAS;CACrC,MAAM,WAAW,MAAM,kBAAkB,QAAQ,IAAI;CACrD,MAAM,EAAE,SAAS,YAAY,MAAM,sBAAsB,OAAO,UAAU;EACxE,OAAO,QAAQ;EACf,mBAAmB,QAAQ;CAC7B,CAAC;CACD,QAAQ,IAAI,GAAG,UAAU,aAAa,WAAW,YAAY,QAAQ,SAAS,eAAe,SAAS,YAAY,cAAc,SAAS,UAAU,KAAK,IAAI;AAC9J;AAEA,eAAe,cAAc,SAA6B;CACxD,MAAM,QAAQ,0BAA0B;CACxC,MAAM,WAAW,IAAI,kBAAkB,EAAE,MAAM,CAAC;CAChD,MAAM,UAAU,MAAM,MAAM,KAAK;CACjC,MAAM,UAAU,QAAQ,UAAU,KAAA,IAC9B,CAAC,OAAO,QAAQ,KAAK,CAAC,IACtB,QAAQ,SAAS,KAAK,GAAG,MAAM,CAAC;CAEpC,KAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,UAAU,QAAQ,SAAS;EACjC,IAAI,CAAC,SAAS;GACZ,QAAQ,IAAI,IAAI,MAAM,YAAY;GAClC;EACF;EACA,MAAM,SAAS,MAAM,SAAS,cAAc,KAAK;EACjD,IAAI,OAAO,IACT,QAAQ,IAAI,IAAI,MAAM,IAAI,QAAQ,SAAS,GAAG,OAAO,OAAO,SAAS,OAAO,UAAU,QAAQ,QAAQ,eAAe,OAAO,MAAM,KAAK,IAAI;OAE3I,QAAQ,IAAI,IAAI,MAAM,IAAI,QAAQ,SAAS,GAAG,aAAa,OAAO,OAAO;CAE7E;AACF;AAEA,eAAe,cAAc,SAA6C;CAExE,MADc,0BACJ,CAAC,CAAC,QAAQ,YAAY;EAC9B,MAAM,QAAQ,QAAQ,UAAU,KAAA,IAC5B,OAAO,QAAQ,KAAK,IACpB,QAAQ,QACN,QAAQ,SAAS,WAAW,MAAM,EAAE,OAAO,YAAY,MAAM,QAAQ,MAAO,YAAY,CAAC,IACzF,QAAQ;EACd,IAAI,QAAQ,KAAK,SAAS,QAAQ,SAAS,QACzC,MAAM,IAAI,MAAM,4BAA4B,MAAM,EAAE;EAEtD,MAAM,CAAC,WAAW,QAAQ,SAAS,OAAO,OAAO,CAAC;EAClD,IAAI,QAAQ,eAAe,QAAQ,SAAS,QAAQ,QAAQ,cAAc;EAC1E,QAAQ,IAAI,oBAAoB,SAAS,SAAS,IAAI,MAAM,IAAI;CAClE,CAAC;AACH;AAEA,SAAgB,gBAAyB;CACvC,MAAM,UAAU,IAAI,QAAQ;CAC5B,QACG,KAAK,SAAS,CAAC,CACf,YAAY,kEAAkE,CAAC,CAC/E,QAAQ,eAAe;CAE1B,QACG,QAAQ,OAAO,CAAC,CAChB,YAAY,0CAA0C,CAAC,CACvD,OAAO,cAAc,oDAAoD,KAAK,CAAC,CAC/E,OAAO,UAAU,gEAAgE,KAAK,CAAC,CACvF,OAAO,cAAc,0BAA0B,OAAO,CAAC,CACvD,OAAO,kBAAkB,sCAAsC,CAAC,CAChE,OAAO,kBAAkB,oBAAoB,QAAQ,CAAC,CACtD,OAAO,OAAO,YAAY;EACzB,MAAM,aAAa;GAAE,GAAG;GAAS,SAAS,QAAQ;EAAQ,CAAC;CAC7D,CAAC;CAEH,QACG,QAAQ,QAAQ,CAAC,CACjB,YAAY,gCAAgC,CAAC,CAC7C,OAAO,YAAY;EAClB,MAAM,cAAc;CACtB,CAAC;CAEH,QACG,QAAQ,QAAQ,CAAC,CACjB,YAAY,+CAA+C,CAAC,CAC5D,SAAS,UAAU,qCAAqC,CAAC,CACzD,OAAO,UAAU,yCAAyC,KAAK,CAAC,CAChE,OAAO,mBAAmB,6CAA6C,CAAC,CACxE,OAAO,eAAe,mDAAmD,KAAK,CAAC,CAC/E,OAAO,OAAO,MAA0B,YAAmE;EAC1G,MAAM,cAAc;GAAE,GAAG;GAAS;EAAK,CAAC;CAC1C,CAAC;CAEH,QACG,QAAQ,QAAQ,CAAC,CACjB,YAAY,iDAAiD,CAAC,CAC9D,OAAO,eAAe,0CAA0C,CAAC,CACjE,OAAO,OAAO,YAAgC;EAC7C,MAAM,cAAc,OAAO;CAC7B,CAAC;CAEH,QACG,QAAQ,QAAQ,CAAC,CACjB,YAAY,mBAAmB,CAAC,CAChC,OAAO,eAAe,iCAAiC,CAAC,CACxD,OAAO,mBAAmB,eAAe,CAAC,CAC1C,OAAO,OAAO,YAAgD;EAC7D,MAAM,cAAc,OAAO;CAC7B,CAAC;CAEH,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/cli/callback-server.ts","../../src/cli/index.ts"],"sourcesContent":["/** Loopback OAuth callback server for the standalone CLI login flow. */\n\nimport { createServer } from 'node:http'\nimport type { Server } from 'node:http'\n\nexport interface CallbackResult {\n code: string\n state: string\n url: string\n}\n\nexport interface CallbackHandle {\n result: Promise<CallbackResult>\n /** Resolves once the loopback listener is bound; rejects on bind errors. */\n ready: Promise<void>\n close(): Promise<void>\n}\n\nconst SUCCESS_HTML = `<!doctype html><html><head><meta charset=\"utf-8\"><title>Login successful</title></head>\n<body style=\"font-family:system-ui;text-align:center;padding:3rem\">\n<h2>✓ Authentication successful</h2>\n<p>You can close this tab and return to the terminal.</p>\n</body></html>`\n\n/** Listen on 127.0.0.1:<port>/oauth-callback and resolve with the code+state. */\nexport function startCallbackServer(options: { port?: number; timeoutMs?: number } = {}): CallbackHandle {\n const port = options.port ?? 51121\n const timeoutMs = options.timeoutMs ?? 300_000\n\n let resolveResult: (value: CallbackResult) => void\n let rejectResult: (reason: Error) => void\n const result = new Promise<CallbackResult>((resolve, reject) => {\n resolveResult = resolve\n rejectResult = reject\n })\n\n let resolveReady: () => void\n let rejectReady: (reason: Error) => void\n const ready = new Promise<void>((resolve, reject) => {\n resolveReady = resolve\n rejectReady = reject\n })\n\n const server: Server = createServer((req, res) => {\n const url = new URL(req.url ?? '/', 'http://localhost')\n if (url.pathname === '/oauth-callback') {\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' })\n res.end('<h2>Missing code or state</h2>')\n return\n }\n res.writeHead(200, { 'Content-Type': 'text/html', connection: 'close' })\n res.end(SUCCESS_HTML)\n resolveResult({ code, state, url: url.toString() })\n // Give the browser a moment to render before the server closes.\n setTimeout(() => server.close(), 1500)\n return\n }\n res.writeHead(404)\n res.end('Not found')\n })\n\n server.on('error', (error: NodeJS.ErrnoException) => {\n if (error.code === 'EADDRINUSE') {\n rejectResult(new Error(`Port ${port} is already in use — pass --port to pick another.`))\n } else {\n rejectResult(error)\n }\n rejectReady(error)\n })\n\n server.listen(port, '127.0.0.1', () => {\n resolveReady()\n })\n\n const timeout = setTimeout(() => {\n rejectResult(new Error('Authentication timed out — no callback received.'))\n server.close()\n }, timeoutMs)\n\n const handle: CallbackHandle = {\n result,\n ready,\n async close() {\n clearTimeout(timeout)\n if (server.listening) {\n // The success page's keep-alive connection would otherwise hold\n // server.close() (and the CLI process) open indefinitely.\n server.closeAllConnections()\n await new Promise<void>((resolve) => server.close(() => resolve()))\n }\n },\n }\n return handle\n}\n\n/** Open a URL in the system browser (best effort). */\nexport async function openBrowser(url: string): Promise<boolean> {\n const { spawn } = await import('node:child_process')\n const platform = process.platform\n const command = platform === 'darwin' ? ['open', url]\n : platform === 'win32' ? ['cmd', '/c', 'start', '', url]\n : ['xdg-open', url]\n return new Promise<boolean>((resolve) => {\n const child = spawn(command[0]!, command.slice(1), { stdio: 'ignore' })\n child.on('error', () => resolve(false))\n child.on('spawn', () => resolve(true))\n })\n}\n","/**\n * dsh-agy CLI: login / status / import / verify / logout.\n * Standalone bin — the dsh launcher has no plugin subcommand slot, and the\n * remote paste-blob flow must run on machines without a harness.\n */\n\nimport { Command } from 'commander'\nimport { readFileSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { createInterface } from 'node:readline/promises'\nimport { stdin as input, stdout as output } from 'node:process'\nimport { authorizeAntigravity } from '../oauth/authorize.ts'\nimport { exchangeAntigravity } from '../oauth/exchange.ts'\nimport { encodeCredentialBlob } from '../oauth/blob.ts'\nimport { AGY_DEFAULT_REDIRECT_URI } from '../oauth/constants.ts'\nimport { createAesGcmCodec, deriveKey, loadMasterKey, resolveDshHome, resolveMasterKeyCodec } from '../store/keyring.ts'\nimport type { SecretCodec } from '../store/keyring.ts'\nimport { JsonAccountStore } from '../store/accounts.ts'\nimport { AgySessionManager } from '../session.ts'\nimport { startCallbackServer, openBrowser } from './callback-server.ts'\nimport { importManySources, upsertImportedAccount } from './import.ts'\n\n/** Package version, read from the shipped package.json — never hard-coded twice. */\nconst { version: PACKAGE_VERSION } = JSON.parse(\n readFileSync(new URL('../../package.json', import.meta.url), 'utf8'),\n) as { version: string }\n\nfunction createStore(options: { readOnly?: boolean } = {}): JsonAccountStore {\n const dshHome = resolveDshHome()\n let codec: SecretCodec\n if (options.readOnly) {\n // Read-only commands must never create the master key or the credentials\n // document: they would fail for no reason on a read-only HOME and write\n // files nobody asked for anywhere else.\n const masterKey = loadMasterKey(dshHome)\n if (!masterKey) {\n throw new Error('No agy account store found — run `dsh-agy login` first.')\n }\n codec = createAesGcmCodec(deriveKey(masterKey))\n } else {\n codec = resolveMasterKeyCodec(dshHome).codec\n }\n return new JsonAccountStore({ file: `${dshHome}/agy-accounts.json`, codec })\n}\n\n/** Read-only store with a friendly error when no credentials exist yet. */\nfunction createReadOnlyStoreOrExit(): JsonAccountStore {\n try {\n return createStore({ readOnly: true })\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error))\n process.exit(1)\n }\n}\n\nasync function ask(question: string): Promise<string> {\n const rl = createInterface({ input, output })\n try {\n return (await rl.question(question)).trim()\n } finally {\n rl.close()\n }\n}\n\n/** Remote/SSH sessions cannot reach a local browser; auto-select headless paste. */\nfunction isRemoteSession(): boolean {\n return !!(process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY)\n}\n\nasync function loginCommand(options: { headless: boolean; blob: boolean; port: number; project?: string; timeout?: string }) {\n const store = createStore()\n const redirectUri = `http://localhost:${options.port}/oauth-callback`\n if (!options.headless && isRemoteSession()) {\n console.log('(SSH session detected — using headless paste flow; pass --headless explicitly to force)')\n options.headless = true\n }\n\n let callback: Awaited<ReturnType<typeof startCallbackServer>> | undefined\n if (!options.headless) {\n // Bind the loopback listener BEFORE the URL is shown: a local process\n // squatting on the fixed port could otherwise capture code+state (the\n // state carries the PKCE verifier) and steal the refresh token.\n callback = startCallbackServer({ port: options.port, timeoutMs: Number(options.timeout) || 300_000 })\n try {\n await callback.ready\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error))\n process.exit(1)\n }\n }\n\n const { url, verifier } = await authorizeAntigravity(redirectUri, options.project ?? '')\n console.log(`\\nOpen this URL in a browser to authorize:\\n\\n ${url}\\n`)\n\n let code: string\n let state: string\n\n if (options.headless) {\n const pasted = await ask('After approving, paste the full redirected URL here: ')\n const parsed = new URL(pasted)\n code = parsed.searchParams.get('code') ?? ''\n state = parsed.searchParams.get('state') ?? ''\n if (!code || !state) {\n console.error('Error: pasted URL is missing code or state.')\n process.exit(1)\n }\n } else {\n const opened = await openBrowser(url)\n if (!opened) console.log('(Could not open a browser automatically — open the URL manually.)')\n let callbackResult: { code: string; state: string; url: string }\n try {\n callbackResult = await callback!.result\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error))\n process.exit(1)\n }\n code = callbackResult.code\n state = callbackResult.state\n }\n\n // Bind the exchange to the verifier we issued: a state from any other login\n // (pasted from another session, or fabricated) must be rejected.\n const result = await exchangeAntigravity(code, state, redirectUri, verifier)\n if (result.type === 'failed') {\n console.error(`Login failed: ${result.error}`)\n process.exit(1)\n }\n\n if (options.blob) {\n const blob = encodeCredentialBlob('agy', {\n access_token: result.access,\n refresh_token: result.refresh.split('|')[0],\n expires_in: Math.max(0, Math.round((result.expires - Date.now()) / 1000)),\n })\n console.log(`\\nPaste this blob into the remote dashboard/CLI:\\n\\n${blob}\\n`)\n } else {\n const { account, created } = 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 }, { overwriteExisting: true })\n console.log(`${created ? 'Added' : 'Updated'} account: ${account.email ?? '(no email)'} (project: ${result.projectId || 'default'})`)\n }\n\n await callback?.close()\n}\n\nasync function statusCommand() {\n const store = createReadOnlyStoreOrExit()\n const storage = await store.load()\n if (storage.accounts.length === 0) {\n console.log('No agy accounts. Run `dsh-agy login` first.')\n return\n }\n console.log(`\\n${storage.accounts.length} account(s), active index ${storage.activeIndex}:\\n`)\n const sessions = new AgySessionManager({ store })\n for (const [index, account] of storage.accounts.entries()) {\n const marker = index === storage.activeIndex ? '★' : ' '\n const state = account.enabled === false ? 'disabled'\n : account.verificationRequired ? 'verification-required'\n : account.coolingDownUntil && account.coolingDownUntil > Date.now() ? 'cooling'\n : 'active'\n console.log(` ${marker} [${index}] ${account.email ?? '(no email)'} — ${state}${account.projectId ? ` (project: ${account.projectId})` : ''}`)\n\n // Best-effort quota summary via fetchAvailableModels (fresh access token).\n const session = await sessions.getSession().catch(() => undefined)\n if (session && session.index === index) {\n try {\n const { fetchAvailableModels } = await import('../adapter/models.ts')\n const discovered = await fetchAvailableModels(session.auth.access, session.account.projectId)\n const entries = Object.entries(discovered.models ?? {})\n if (entries.length > 0) {\n const withQuota = entries\n .map(([id, entry]) => ({ id, ...entry.quotaInfo }))\n .filter((e) => typeof e.remainingFraction === 'number')\n .sort((a, b) => (a.remainingFraction ?? 0) - (b.remainingFraction ?? 0))\n if (withQuota.length > 0) {\n const lowest = withQuota[0]!\n console.log(` models: ${entries.length}, lowest quota: ${Math.round((lowest.remainingFraction ?? 0) * 100)}% (${lowest.id})${lowest.resetTime ? `, resets ${new Date(lowest.resetTime).toISOString()}` : ''}`)\n } else {\n console.log(` models: ${entries.length} (no per-model quota reported)`)\n }\n }\n } catch (error) {\n console.log(` quota: unavailable (${error instanceof Error ? error.message : String(error)})`)\n }\n }\n }\n}\n\nasync function importCommand(options: { blob: boolean; files?: string[]; email?: string; overwrite: boolean }) {\n const store = createStore()\n let items: Array<{ source: unknown; kind: 'json' | 'blob' }>\n if (options.files && options.files.length > 0) {\n items = options.files.map((file) => {\n const raw = readFileSync(file, 'utf8')\n return { source: options.blob ? raw : (JSON.parse(raw) as unknown), kind: options.blob ? 'blob' : 'json' }\n })\n } else {\n const pasted = await ask('Paste the agy token JSON (or blob with --blob): ')\n items = [{ source: pasted, kind: options.blob ? 'blob' : 'json' }]\n }\n const result = await importManySources(items, store, {\n email: options.email,\n overwriteExisting: options.overwrite,\n })\n console.log(`Imported ${result.imported}, replaced ${result.replaced}${result.errors.length > 0 ? `, ${result.errors.length} failed` : ''}`)\n for (const error of result.errors) console.log(` ! ${error}`)\n}\n\nasync function exportCommand(options: { index?: string; out?: string }) {\n const store = createReadOnlyStoreOrExit()\n const sessions = new AgySessionManager({ store })\n const storage = await store.load()\n const indices = options.index !== undefined\n ? [Number(options.index)]\n : storage.accounts.map((_, i) => i)\n\n let exported = 0\n for (const index of indices) {\n const account = storage.accounts[index]\n if (!account) {\n console.log(`[${index}] not found`)\n continue\n }\n const result = await sessions.exportBlob(index)\n if (!result.blob) {\n console.log(`[${index}] ${account.email ?? ''} — FAILED: ${result.error}`)\n continue\n }\n if (options.out) {\n const file = join(options.out, `dsh-agy-${index}.blob`)\n writeFileSync(file, result.blob + '\\n')\n console.log(`[${index}] ${account.email ?? ''} — wrote ${file}`)\n } else {\n console.log(result.blob)\n }\n exported++\n }\n if (!options.out) console.log(`\\n${exported} blob(s) exported — one line each, paste into a remote import`)\n}\n\nasync function verifyCommand(options: { index?: string }) {\n const store = createReadOnlyStoreOrExit()\n const sessions = new AgySessionManager({ store })\n const storage = await store.load()\n const indices = options.index !== undefined\n ? [Number(options.index)]\n : storage.accounts.map((_, i) => i)\n\n for (const index of indices) {\n const account = storage.accounts[index]\n if (!account) {\n console.log(`[${index}] not found`)\n continue\n }\n const result = await sessions.verifyAccount(index)\n if (result.ok) {\n console.log(`[${index}] ${account.email ?? ''} — OK${result.email && result.email !== account.email ? ` (userinfo: ${result.email})` : ''}`)\n } else {\n console.log(`[${index}] ${account.email ?? ''} — FAILED: ${result.error}`)\n }\n }\n}\n\nasync function logoutCommand(options: { index?: string; email?: string }) {\n const store = createReadOnlyStoreOrExit()\n await store.mutate((storage) => {\n const index = options.index !== undefined\n ? Number(options.index)\n : options.email\n ? storage.accounts.findIndex((a) => a.email?.toLowerCase() === options.email!.toLowerCase())\n : storage.activeIndex\n if (index < 0 || index >= storage.accounts.length) {\n throw new Error(`account not found (index ${index})`)\n }\n const [removed] = storage.accounts.splice(index, 1)\n if (storage.activeIndex >= storage.accounts.length) storage.activeIndex = 0\n console.log(`Removed account: ${removed?.email ?? `[${index}]`}`)\n })\n}\n\nexport function createProgram(): Command {\n const program = new Command()\n program\n .name('dsh-agy')\n .description('Google Antigravity (agy) account management for DeepSeek Harness')\n .version(PACKAGE_VERSION)\n\n program\n .command('login')\n .description('OAuth login (browser, or headless paste)')\n .option('--headless', 'print the URL and wait for a pasted redirect URL', false)\n .option('--blob', 'print a paste-credential blob instead of storing the account', false)\n .option('--port <n>', 'loopback callback port', '51121')\n .option('--project <id>', 'bind the login to a specific project')\n .option('--timeout <ms>', 'callback timeout', '300000')\n .action(async (options) => {\n await loginCommand({ ...options, timeout: options.timeout })\n })\n\n program\n .command('status')\n .description('List accounts and their health')\n .action(async () => {\n await statusCommand()\n })\n\n program\n .command('import')\n .description('Import agy token files or paste credentials')\n .argument('[files...]', 'paths to agy auth.json token files (multiple allowed)')\n .option('--blob', 'the pasted value is a credential blob', false)\n .option('--email <email>', 'account email (skips userinfo verification)')\n .option('--overwrite', 'replace an existing account with the same email', false)\n .action(async (files: string[] | undefined, options: { blob: boolean; email?: string; overwrite: boolean }) => {\n await importCommand({ ...options, files })\n })\n\n program\n .command('export')\n .description('Export account credentials as paste blobs')\n .option('--index <n>', 'export one account by index; default all')\n .option('--out <dir>', 'write one dsh-agy-<index>.blob file per account into this directory (default: print to stdout)')\n .action(async (options: { index?: string; out?: string }) => {\n await exportCommand(options)\n })\n\n program\n .command('verify')\n .description('Verify account credentials (refresh + userinfo)')\n .option('--index <n>', 'verify one account by index; default all')\n .action(async (options: { index?: string }) => {\n await verifyCommand(options)\n })\n\n program\n .command('logout')\n .description('Remove an account')\n .option('--index <n>', 'account index (default: active)')\n .option('--email <email>', 'account email')\n .action(async (options: { index?: string; email?: string }) => {\n await logoutCommand(options)\n })\n\n return program\n}\n\nexport { AGY_DEFAULT_REDIRECT_URI }\n"],"mappings":"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"}
|
|
@@ -140,6 +140,23 @@ async function parseImportSource(source, kind) {
|
|
|
140
140
|
}
|
|
141
141
|
return enrichWithAntigravityBackend(parseAndValidateAgyToken(source));
|
|
142
142
|
}
|
|
143
|
+
/** Batch-import many sources (CLI multi-file / web multi-line paste). Each item
|
|
144
|
+
* is independent: a failure is collected, the rest still import. */
|
|
145
|
+
async function importManySources(items, store, options = {}) {
|
|
146
|
+
const result = {
|
|
147
|
+
imported: 0,
|
|
148
|
+
replaced: 0,
|
|
149
|
+
errors: []
|
|
150
|
+
};
|
|
151
|
+
for (const item of items) try {
|
|
152
|
+
const { created } = await upsertImportedAccount(store, await parseImportSource(item.source, item.kind), options);
|
|
153
|
+
if (created) result.imported++;
|
|
154
|
+
else result.replaced++;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
143
160
|
/** Upsert the account into the store, deduping by email. */
|
|
144
161
|
async function upsertImportedAccount(store, enriched, options = {}) {
|
|
145
162
|
const resolvedEmail = options.email || enriched.email;
|
|
@@ -182,6 +199,6 @@ async function upsertImportedAccount(store, enriched, options = {}) {
|
|
|
182
199
|
});
|
|
183
200
|
}
|
|
184
201
|
//#endregion
|
|
185
|
-
export { upsertImportedAccount as n, authorizeAntigravity as r,
|
|
202
|
+
export { upsertImportedAccount as n, authorizeAntigravity as r, importManySources as t };
|
|
186
203
|
|
|
187
|
-
//# sourceMappingURL=import-
|
|
204
|
+
//# sourceMappingURL=import-DpYDhfy4.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"import-DnKzgr-0.mjs","names":[],"sources":["../src/oauth/authorize.ts","../src/cli/import.ts"],"sourcesContent":["/** Build the Antigravity OAuth authorization URL with PKCE and project metadata. */\n\nimport {\n AGY_CLIENT_ID,\n AGY_SCOPES,\n OAUTH_AUTHORIZE_URL,\n} from './constants.ts'\nimport { encodeState, generatePkcePair } from './pkce.ts'\n\nexport interface AntigravityAuthorization {\n url: string\n verifier: string\n projectId: string\n /** Raw state payload embedded in the URL (for server-side binding). */\n state: string\n}\n\n/**\n * Build the Google OAuth authorization URL for the Antigravity client.\n * The `state` payload carries the PKCE verifier and an optional project id so a\n * headless callback can be pasted back verbatim.\n * @param redirectUri - loopback callback URI (harness webServer route or CLI listener).\n * @param projectId - optional Antigravity project id to bind this login to.\n */\nexport async function authorizeAntigravity(\n redirectUri: string,\n projectId = '',\n): Promise<AntigravityAuthorization> {\n const { verifier, challenge } = generatePkcePair()\n\n const url = new URL(OAUTH_AUTHORIZE_URL)\n url.searchParams.set('client_id', AGY_CLIENT_ID)\n url.searchParams.set('response_type', 'code')\n url.searchParams.set('redirect_uri', redirectUri)\n url.searchParams.set('scope', AGY_SCOPES.join(' '))\n url.searchParams.set('code_challenge', challenge)\n url.searchParams.set('code_challenge_method', 'S256')\n const state = encodeState({ verifier, projectId: projectId || '' })\n url.searchParams.set('state', state)\n url.searchParams.set('access_type', 'offline')\n url.searchParams.set('prompt', 'consent')\n\n return { url: url.toString(), verifier, projectId: projectId || '', state }\n}\n","/**\n * Import agy credentials: parse the `agy` CLI auth.json token file or a\n * paste credential blob, enrich with the Code Assist backend (email/project),\n * and persist into the account store. Adapted from OmniRoute's agyAuthImport\n * (MIT, see NOTICE.md).\n */\n\nimport { AGY_ENDPOINT_FALLBACKS, getAgyBootstrapClientMetadata, getAgyBootstrapUserAgent } from '../oauth/constants.ts'\nimport { decodeCredentialBlob } from '../oauth/blob.ts'\nimport type { AccountStore } from '../store/accounts.ts'\nimport type { ManagedAccount } from '../types.ts'\n\nexport class AgyAuthFileError extends Error {\n status: number\n code: string\n\n constructor(message: string, status = 400, code = 'invalid_request') {\n super(message)\n this.name = 'AgyAuthFileError'\n this.status = status\n this.code = code\n }\n}\n\nexport interface ParsedAgyAuth {\n accessToken: string\n refreshToken: string\n tokenType: string\n expiresAt: string | null\n authMethod: string | null\n}\n\nexport interface EnrichedAgyAuth extends ParsedAgyAuth {\n email: string | null\n projectId: string | null\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {}\n}\n\nfunction toNonEmptyString(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed ? trimmed : null\n}\n\n/**\n * Parse the agy CLI token file: tokens nest under `.token` with an ISO\n * `expiry` string and no id_token; a flat top-level shape is accepted too.\n */\nexport function parseAndValidateAgyToken(raw: unknown): ParsedAgyAuth {\n const doc = toRecord(raw)\n const token = toRecord(doc.token ?? doc)\n\n const accessToken = toNonEmptyString(token.access_token)\n const refreshToken = toNonEmptyString(token.refresh_token)\n if (!accessToken) throw new AgyAuthFileError('access_token is missing or empty in the agy token file', 400, 'missing_access_token')\n if (!refreshToken) throw new AgyAuthFileError('refresh_token is missing or empty in the agy token file', 400, 'missing_refresh_token')\n\n let expiresAt: string | null = null\n const isoExpiry = toNonEmptyString(token.expiry) ?? toNonEmptyString(token.expires_at)\n if (isoExpiry) {\n const ms = new Date(isoExpiry).getTime()\n expiresAt = Number.isNaN(ms) ? null : new Date(ms).toISOString()\n } else if (typeof token.expiry_date === 'number' && Number.isFinite(token.expiry_date)) {\n expiresAt = new Date(token.expiry_date).toISOString()\n }\n\n return {\n accessToken,\n refreshToken,\n tokenType: toNonEmptyString(token.token_type) ?? 'Bearer',\n expiresAt,\n authMethod: toNonEmptyString(doc.auth_method) ?? toNonEmptyString(token.auth_method),\n }\n}\n\n/** Best-effort enrichment: email (userinfo) + projectId (loadCodeAssist), time-boxed. */\nexport async function enrichWithAntigravityBackend(parsed: ParsedAgyAuth): Promise<EnrichedAgyAuth> {\n let email: string | null = null\n let projectId: string | null = null\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), 8000)\n try {\n const res = await fetch('https://www.googleapis.com/oauth2/v1/userinfo?alt=json', {\n headers: { Authorization: `Bearer ${parsed.accessToken}` },\n signal: controller.signal,\n })\n if (res.ok) {\n email = toNonEmptyString(toRecord(await res.json()).email)\n }\n } catch {\n // best effort — email stays null\n } finally {\n clearTimeout(timer)\n }\n\n const loadController = new AbortController()\n const loadTimer = setTimeout(() => loadController.abort(), 8000)\n try {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${parsed.accessToken}`,\n 'Content-Type': 'application/json',\n 'User-Agent': getAgyBootstrapUserAgent(),\n 'Client-Metadata': getAgyBootstrapClientMetadata(),\n }\n for (const endpoint of AGY_ENDPOINT_FALLBACKS) {\n try {\n const res = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ metadata: { ideType: 'ANTIGRAVITY' } }),\n signal: loadController.signal,\n })\n if (!res.ok) continue\n const data = toRecord(await res.json())\n const project = data.cloudaicompanionProject\n projectId = (typeof project === 'string' ? toNonEmptyString(project) : null)\n ?? toNonEmptyString(toRecord(project).id)\n if (projectId) break\n } catch {\n // try next endpoint\n }\n }\n } catch {\n // best effort — projectId stays null\n } finally {\n clearTimeout(loadTimer)\n }\n\n return { ...parsed, email, projectId }\n}\n\n/** Parse either a raw token document or a paste blob into an enriched account. */\nexport async function parseImportSource(source: unknown, kind: 'json' | 'blob'): Promise<EnrichedAgyAuth> {\n if (kind === 'blob') {\n if (typeof source !== 'string') throw new AgyAuthFileError('blob must be a string', 400, 'invalid_blob')\n const blob = decodeCredentialBlob(source)\n return enrichWithAntigravityBackend(\n parseAndValidateAgyToken({ token: blob.tokens, auth_method: 'paste-blob' }),\n )\n }\n return enrichWithAntigravityBackend(parseAndValidateAgyToken(source))\n}\n\n/** Upsert the account into the store, deduping by email. */\nexport async function upsertImportedAccount(\n store: AccountStore,\n enriched: EnrichedAgyAuth,\n options: { email?: string; overwriteExisting?: boolean } = {},\n): Promise<{ account: ManagedAccount; created: boolean }> {\n const resolvedEmail = options.email || enriched.email\n const refresh = `${enriched.refreshToken}|${enriched.projectId ?? ''}`\n\n return store.mutate((storage) => {\n const existingIndex = resolvedEmail\n ? storage.accounts.findIndex((a) => a.email?.toLowerCase() === resolvedEmail.toLowerCase())\n : -1\n\n if (existingIndex !== -1) {\n if (!options.overwriteExisting) {\n throw new AgyAuthFileError(\n 'An agy account for this email already exists. Pass --overwrite to replace it.',\n 409,\n 'duplicate_account',\n )\n }\n const existing = storage.accounts[existingIndex]!\n storage.accounts[existingIndex] = {\n ...existing,\n refresh,\n email: existing.email ?? resolvedEmail ?? undefined,\n addedAt: existing.addedAt,\n lastUsed: Date.now(),\n enabled: true,\n verificationRequired: false,\n verificationRequiredAt: undefined,\n verificationRequiredReason: undefined,\n }\n return { account: storage.accounts[existingIndex]!, created: false }\n }\n\n const account: ManagedAccount = {\n email: resolvedEmail ?? undefined,\n refresh,\n projectId: enriched.projectId ?? undefined,\n addedAt: Date.now(),\n lastUsed: Date.now(),\n enabled: true,\n }\n storage.accounts.push(account)\n if (storage.activeIndex >= storage.accounts.length - 1 && storage.accounts.length === 1) {\n storage.activeIndex = 0\n }\n return { account, created: true }\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"import-DpYDhfy4.mjs","names":[],"sources":["../src/oauth/authorize.ts","../src/cli/import.ts"],"sourcesContent":["/** Build the Antigravity OAuth authorization URL with PKCE and project metadata. */\n\nimport {\n AGY_CLIENT_ID,\n AGY_SCOPES,\n OAUTH_AUTHORIZE_URL,\n} from './constants.ts'\nimport { encodeState, generatePkcePair } from './pkce.ts'\n\nexport interface AntigravityAuthorization {\n url: string\n verifier: string\n projectId: string\n /** Raw state payload embedded in the URL (for server-side binding). */\n state: string\n}\n\n/**\n * Build the Google OAuth authorization URL for the Antigravity client.\n * The `state` payload carries the PKCE verifier and an optional project id so a\n * headless callback can be pasted back verbatim.\n * @param redirectUri - loopback callback URI (harness webServer route or CLI listener).\n * @param projectId - optional Antigravity project id to bind this login to.\n */\nexport async function authorizeAntigravity(\n redirectUri: string,\n projectId = '',\n): Promise<AntigravityAuthorization> {\n const { verifier, challenge } = generatePkcePair()\n\n const url = new URL(OAUTH_AUTHORIZE_URL)\n url.searchParams.set('client_id', AGY_CLIENT_ID)\n url.searchParams.set('response_type', 'code')\n url.searchParams.set('redirect_uri', redirectUri)\n url.searchParams.set('scope', AGY_SCOPES.join(' '))\n url.searchParams.set('code_challenge', challenge)\n url.searchParams.set('code_challenge_method', 'S256')\n const state = encodeState({ verifier, projectId: projectId || '' })\n url.searchParams.set('state', state)\n url.searchParams.set('access_type', 'offline')\n url.searchParams.set('prompt', 'consent')\n\n return { url: url.toString(), verifier, projectId: projectId || '', state }\n}\n","/**\n * Import agy credentials: parse the `agy` CLI auth.json token file or a\n * paste credential blob, enrich with the Code Assist backend (email/project),\n * and persist into the account store. Adapted from OmniRoute's agyAuthImport\n * (MIT, see NOTICE.md).\n */\n\nimport { AGY_ENDPOINT_FALLBACKS, getAgyBootstrapClientMetadata, getAgyBootstrapUserAgent } from '../oauth/constants.ts'\nimport { decodeCredentialBlob } from '../oauth/blob.ts'\nimport type { AccountStore } from '../store/accounts.ts'\nimport type { ManagedAccount } from '../types.ts'\n\nexport class AgyAuthFileError extends Error {\n status: number\n code: string\n\n constructor(message: string, status = 400, code = 'invalid_request') {\n super(message)\n this.name = 'AgyAuthFileError'\n this.status = status\n this.code = code\n }\n}\n\nexport interface ParsedAgyAuth {\n accessToken: string\n refreshToken: string\n tokenType: string\n expiresAt: string | null\n authMethod: string | null\n}\n\nexport interface EnrichedAgyAuth extends ParsedAgyAuth {\n email: string | null\n projectId: string | null\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {}\n}\n\nfunction toNonEmptyString(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed ? trimmed : null\n}\n\n/**\n * Parse the agy CLI token file: tokens nest under `.token` with an ISO\n * `expiry` string and no id_token; a flat top-level shape is accepted too.\n */\nexport function parseAndValidateAgyToken(raw: unknown): ParsedAgyAuth {\n const doc = toRecord(raw)\n const token = toRecord(doc.token ?? doc)\n\n const accessToken = toNonEmptyString(token.access_token)\n const refreshToken = toNonEmptyString(token.refresh_token)\n if (!accessToken) throw new AgyAuthFileError('access_token is missing or empty in the agy token file', 400, 'missing_access_token')\n if (!refreshToken) throw new AgyAuthFileError('refresh_token is missing or empty in the agy token file', 400, 'missing_refresh_token')\n\n let expiresAt: string | null = null\n const isoExpiry = toNonEmptyString(token.expiry) ?? toNonEmptyString(token.expires_at)\n if (isoExpiry) {\n const ms = new Date(isoExpiry).getTime()\n expiresAt = Number.isNaN(ms) ? null : new Date(ms).toISOString()\n } else if (typeof token.expiry_date === 'number' && Number.isFinite(token.expiry_date)) {\n expiresAt = new Date(token.expiry_date).toISOString()\n }\n\n return {\n accessToken,\n refreshToken,\n tokenType: toNonEmptyString(token.token_type) ?? 'Bearer',\n expiresAt,\n authMethod: toNonEmptyString(doc.auth_method) ?? toNonEmptyString(token.auth_method),\n }\n}\n\n/** Best-effort enrichment: email (userinfo) + projectId (loadCodeAssist), time-boxed. */\nexport async function enrichWithAntigravityBackend(parsed: ParsedAgyAuth): Promise<EnrichedAgyAuth> {\n let email: string | null = null\n let projectId: string | null = null\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), 8000)\n try {\n const res = await fetch('https://www.googleapis.com/oauth2/v1/userinfo?alt=json', {\n headers: { Authorization: `Bearer ${parsed.accessToken}` },\n signal: controller.signal,\n })\n if (res.ok) {\n email = toNonEmptyString(toRecord(await res.json()).email)\n }\n } catch {\n // best effort — email stays null\n } finally {\n clearTimeout(timer)\n }\n\n const loadController = new AbortController()\n const loadTimer = setTimeout(() => loadController.abort(), 8000)\n try {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${parsed.accessToken}`,\n 'Content-Type': 'application/json',\n 'User-Agent': getAgyBootstrapUserAgent(),\n 'Client-Metadata': getAgyBootstrapClientMetadata(),\n }\n for (const endpoint of AGY_ENDPOINT_FALLBACKS) {\n try {\n const res = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ metadata: { ideType: 'ANTIGRAVITY' } }),\n signal: loadController.signal,\n })\n if (!res.ok) continue\n const data = toRecord(await res.json())\n const project = data.cloudaicompanionProject\n projectId = (typeof project === 'string' ? toNonEmptyString(project) : null)\n ?? toNonEmptyString(toRecord(project).id)\n if (projectId) break\n } catch {\n // try next endpoint\n }\n }\n } catch {\n // best effort — projectId stays null\n } finally {\n clearTimeout(loadTimer)\n }\n\n return { ...parsed, email, projectId }\n}\n\n/** Parse either a raw token document or a paste blob into an enriched account. */\nexport async function parseImportSource(source: unknown, kind: 'json' | 'blob'): Promise<EnrichedAgyAuth> {\n if (kind === 'blob') {\n if (typeof source !== 'string') throw new AgyAuthFileError('blob must be a string', 400, 'invalid_blob')\n const blob = decodeCredentialBlob(source)\n return enrichWithAntigravityBackend(\n parseAndValidateAgyToken({ token: blob.tokens, auth_method: 'paste-blob' }),\n )\n }\n return enrichWithAntigravityBackend(parseAndValidateAgyToken(source))\n}\n\n/** Batch-import many sources (CLI multi-file / web multi-line paste). Each item\n * is independent: a failure is collected, the rest still import. */\nexport async function importManySources(\n items: Array<{ source: unknown; kind: 'json' | 'blob' }>,\n store: AccountStore,\n options: { email?: string; overwriteExisting?: boolean } = {},\n): Promise<{ imported: number; replaced: number; errors: string[] }> {\n const result = { imported: 0, replaced: 0, errors: [] as string[] }\n for (const item of items) {\n try {\n const enriched = await parseImportSource(item.source, item.kind)\n const { created } = await upsertImportedAccount(store, enriched, options)\n if (created) result.imported++\n else result.replaced++\n } catch (error) {\n result.errors.push(error instanceof Error ? error.message : String(error))\n }\n }\n return result\n}\n\n/** Upsert the account into the store, deduping by email. */\nexport async function upsertImportedAccount(\n store: AccountStore,\n enriched: EnrichedAgyAuth,\n options: { email?: string; overwriteExisting?: boolean } = {},\n): Promise<{ account: ManagedAccount; created: boolean }> {\n const resolvedEmail = options.email || enriched.email\n const refresh = `${enriched.refreshToken}|${enriched.projectId ?? ''}`\n\n return store.mutate((storage) => {\n const existingIndex = resolvedEmail\n ? storage.accounts.findIndex((a) => a.email?.toLowerCase() === resolvedEmail.toLowerCase())\n : -1\n\n if (existingIndex !== -1) {\n if (!options.overwriteExisting) {\n throw new AgyAuthFileError(\n 'An agy account for this email already exists. Pass --overwrite to replace it.',\n 409,\n 'duplicate_account',\n )\n }\n const existing = storage.accounts[existingIndex]!\n storage.accounts[existingIndex] = {\n ...existing,\n refresh,\n email: existing.email ?? resolvedEmail ?? undefined,\n addedAt: existing.addedAt,\n lastUsed: Date.now(),\n enabled: true,\n verificationRequired: false,\n verificationRequiredAt: undefined,\n verificationRequiredReason: undefined,\n }\n return { account: storage.accounts[existingIndex]!, created: false }\n }\n\n const account: ManagedAccount = {\n email: resolvedEmail ?? undefined,\n refresh,\n projectId: enriched.projectId ?? undefined,\n addedAt: Date.now(),\n lastUsed: Date.now(),\n enabled: true,\n }\n storage.accounts.push(account)\n if (storage.activeIndex >= storage.accounts.length - 1 && storage.accounts.length === 1) {\n storage.activeIndex = 0\n }\n return { account, created: true }\n })\n}\n"],"mappings":"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"}
|
package/lib/web/plugin.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { m as resolveAntigravityVersionBounded, n as generateFingerprint, r as recordFingerprintVersion } from "../session-tawi2_uh.mjs";
|
|
2
2
|
import { t as createAgyRuntime } from "../plugin-common-I56kmGjr.mjs";
|
|
3
3
|
import { t as exchangeAntigravity } from "../exchange-MhFUiBAm.mjs";
|
|
4
|
-
import { n as upsertImportedAccount, r as authorizeAntigravity, t as
|
|
4
|
+
import { n as upsertImportedAccount, r as authorizeAntigravity, t as importManySources } from "../import-DpYDhfy4.mjs";
|
|
5
5
|
//#region src/web/i18n.ts
|
|
6
6
|
const I18N_DICT = {
|
|
7
7
|
en: {
|
|
@@ -38,11 +38,14 @@ const I18N_DICT = {
|
|
|
38
38
|
testing: "Testing...",
|
|
39
39
|
copied: "Copied to clipboard!",
|
|
40
40
|
importTitle: "Import Credentials",
|
|
41
|
-
importDesc: "Paste an agy auth.json token document, or a credential blob (from dsh-agy login --blob):",
|
|
42
|
-
importPlaceholder: "{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} or dsh-agy-cred-v1....",
|
|
41
|
+
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:",
|
|
42
|
+
importPlaceholder: "{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} or dsh-agy-cred-v1.... (one blob per line for batch)",
|
|
43
43
|
importJson: "Import JSON",
|
|
44
44
|
importBlobBtn: "Import Blob",
|
|
45
45
|
importSuccess: "Successfully imported account: ",
|
|
46
|
+
exportAll: "Export All",
|
|
47
|
+
importBatchResult: "Imported {imported}, replaced {replaced}",
|
|
48
|
+
copyAllMsg: "All credential blobs exported. Copied to clipboard!",
|
|
46
49
|
fingerprintTitle: "Device Fingerprint",
|
|
47
50
|
fingerprintDevice: "Device ID",
|
|
48
51
|
fingerprintUserAgent: "User Agent",
|
|
@@ -97,11 +100,14 @@ const I18N_DICT = {
|
|
|
97
100
|
testing: "测试中...",
|
|
98
101
|
copied: "已复制到剪贴板!",
|
|
99
102
|
importTitle: "导入凭据",
|
|
100
|
-
importDesc: "粘贴 agy auth.json 文件内容,或输入凭据 blob(来自 dsh-agy login --blob
|
|
101
|
-
importPlaceholder: "{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} 或 dsh-agy-cred-v1
|
|
103
|
+
importDesc: "粘贴 agy auth.json 文件内容,或输入凭据 blob(来自 dsh-agy login --blob)。多个 blob 每行一个即可批量导入:",
|
|
104
|
+
importPlaceholder: "{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} 或 dsh-agy-cred-v1....(批量:每行一个 blob)",
|
|
102
105
|
importJson: "导入 JSON",
|
|
103
106
|
importBlobBtn: "导入 Blob",
|
|
104
107
|
importSuccess: "成功导入账号:",
|
|
108
|
+
exportAll: "导出全部",
|
|
109
|
+
importBatchResult: "导入 {imported} 个,替换 {replaced} 个",
|
|
110
|
+
copyAllMsg: "全部凭据 Blob 已导出并复制到剪贴板!",
|
|
105
111
|
fingerprintTitle: "设备指纹信息",
|
|
106
112
|
fingerprintDevice: "设备 ID (DeviceId)",
|
|
107
113
|
fingerprintUserAgent: "用户代理 (User-Agent)",
|
|
@@ -669,9 +675,9 @@ function renderDashboardHtml() {
|
|
|
669
675
|
<span id="txt-import-title">Import Credentials</span>
|
|
670
676
|
</div>
|
|
671
677
|
<p style="font-size:12.5px;color:var(--text-secondary);margin-bottom:8px;" id="txt-import-desc">
|
|
672
|
-
Paste an agy auth.json token document, or a credential blob (from dsh-agy login --blob):
|
|
678
|
+
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:
|
|
673
679
|
</p>
|
|
674
|
-
<textarea id="input-import" class="import-textarea" placeholder='{"token":{"access_token":"...","refresh_token":"..."}} or dsh-agy-cred-v1....'></textarea>
|
|
680
|
+
<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>
|
|
675
681
|
<div style="display:flex;gap:8px;">
|
|
676
682
|
<button id="btn-import-json" class="btn btn-secondary">
|
|
677
683
|
<span id="txt-import-json">Import JSON</span>
|
|
@@ -679,6 +685,10 @@ function renderDashboardHtml() {
|
|
|
679
685
|
<button id="btn-import-blob" class="btn btn-secondary">
|
|
680
686
|
<span id="txt-import-blob">Import Blob</span>
|
|
681
687
|
</button>
|
|
688
|
+
<span style="flex:1"></span>
|
|
689
|
+
<button id="btn-export-all" class="btn btn-secondary">
|
|
690
|
+
<span id="txt-export-all">Export All</span>
|
|
691
|
+
</button>
|
|
682
692
|
</div>
|
|
683
693
|
</section>
|
|
684
694
|
</div>
|
|
@@ -782,6 +792,7 @@ function renderDashboardHtml() {
|
|
|
782
792
|
$('txt-import-desc').textContent = t('importDesc');
|
|
783
793
|
$('txt-import-json').textContent = t('importJson');
|
|
784
794
|
$('txt-import-blob').textContent = t('importBlobBtn');
|
|
795
|
+
$('txt-export-all').textContent = t('exportAll');
|
|
785
796
|
$('input-import').placeholder = t('importPlaceholder');
|
|
786
797
|
}
|
|
787
798
|
|
|
@@ -1073,25 +1084,34 @@ function renderDashboardHtml() {
|
|
|
1073
1084
|
} catch (e) { toast(String(e), 'error'); }
|
|
1074
1085
|
};
|
|
1075
1086
|
|
|
1076
|
-
$('
|
|
1077
|
-
|
|
1078
|
-
|
|
1087
|
+
const importLines = () => $('input-import').value.split('\n')
|
|
1088
|
+
.map((s) => s.trim())
|
|
1089
|
+
.filter(Boolean);
|
|
1090
|
+
|
|
1091
|
+
const doImport = async (kind) => {
|
|
1092
|
+
const sources = importLines();
|
|
1093
|
+
if (sources.length === 0) return;
|
|
1079
1094
|
try {
|
|
1080
|
-
const r = await api('/import', { method: 'POST', body: JSON.stringify({ kind
|
|
1081
|
-
|
|
1095
|
+
const r = await api('/import', { method: 'POST', body: JSON.stringify({ kind, sources }) });
|
|
1096
|
+
if (r.errors && r.errors.length > 0) toast(r.errors[0], 'error');
|
|
1097
|
+
toast(t('importBatchResult').replace('{imported}', r.imported).replace('{replaced}', r.replaced), 'success');
|
|
1082
1098
|
$('input-import').value = '';
|
|
1083
1099
|
render();
|
|
1084
1100
|
} catch (e) { toast(String(e), 'error'); }
|
|
1085
1101
|
};
|
|
1086
1102
|
|
|
1087
|
-
$('btn-import-
|
|
1088
|
-
|
|
1089
|
-
|
|
1103
|
+
$('btn-import-json').onclick = () => doImport('json');
|
|
1104
|
+
$('btn-import-blob').onclick = () => doImport('blob');
|
|
1105
|
+
|
|
1106
|
+
const btnExportAll = $('btn-export-all');
|
|
1107
|
+
if (btnExportAll) btnExportAll.onclick = async () => {
|
|
1090
1108
|
try {
|
|
1091
|
-
const r = await api('/
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1109
|
+
const r = await api('/export-all', { method: 'POST' });
|
|
1110
|
+
if (!r.blobs || r.blobs.length === 0) { toast('No accounts to export', 'info'); return; }
|
|
1111
|
+
const text = r.blobs.map((b) => b.blob).join('\n');
|
|
1112
|
+
copyToClipboard(text, t('copyAllMsg'));
|
|
1113
|
+
$('diagnostics-panel').open = true;
|
|
1114
|
+
$('diagnostics-output').textContent = text;
|
|
1095
1115
|
} catch (e) { toast(String(e), 'error'); }
|
|
1096
1116
|
};
|
|
1097
1117
|
|
|
@@ -1381,12 +1401,28 @@ function createAgyWebRoutes(options) {
|
|
|
1381
1401
|
try {
|
|
1382
1402
|
const body = await readJson(req);
|
|
1383
1403
|
const kind = body.kind === "blob" ? "blob" : "json";
|
|
1384
|
-
const
|
|
1385
|
-
|
|
1386
|
-
sendJson(res, 200, {
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
});
|
|
1404
|
+
const sources = Array.isArray(body.sources) ? body.sources : body.source !== void 0 ? [body.source] : [];
|
|
1405
|
+
if (sources.length === 0) throw new Error("nothing to import");
|
|
1406
|
+
sendJson(res, 200, await importManySources(sources.map((source) => ({
|
|
1407
|
+
source,
|
|
1408
|
+
kind
|
|
1409
|
+
})), store, { overwriteExisting: true }));
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
const handleExportAll = async (_req, res) => {
|
|
1415
|
+
try {
|
|
1416
|
+
const storage = await store.load();
|
|
1417
|
+
const blobs = [];
|
|
1418
|
+
for (let index = 0; index < storage.accounts.length; index++) {
|
|
1419
|
+
const result = await sessions.exportBlob(index);
|
|
1420
|
+
if (result.blob) blobs.push({
|
|
1421
|
+
index,
|
|
1422
|
+
blob: result.blob
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
sendJson(res, 200, { blobs });
|
|
1390
1426
|
} catch (error) {
|
|
1391
1427
|
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1392
1428
|
}
|
|
@@ -1544,6 +1580,11 @@ function createAgyWebRoutes(options) {
|
|
|
1544
1580
|
path: "/agy/api/import",
|
|
1545
1581
|
handler: handleImport
|
|
1546
1582
|
},
|
|
1583
|
+
{
|
|
1584
|
+
kind: "exact",
|
|
1585
|
+
path: "/agy/api/export-all",
|
|
1586
|
+
handler: handleExportAll
|
|
1587
|
+
},
|
|
1547
1588
|
{
|
|
1548
1589
|
kind: "exact",
|
|
1549
1590
|
path: "/agy/api/verify",
|
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 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):',\n importPlaceholder: '{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} or dsh-agy-cred-v1....',\n importJson: 'Import JSON',\n importBlobBtn: 'Import Blob',\n importSuccess: 'Successfully imported account: ',\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):',\n importPlaceholder: '{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} 或 dsh-agy-cred-v1....',\n importJson: '导入 JSON',\n importBlobBtn: '导入 Blob',\n importSuccess: '成功导入账号:',\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):\n </p>\n <textarea id=\"input-import\" class=\"import-textarea\" placeholder='{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} or dsh-agy-cred-v1....'></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 </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 $('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: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 // 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 $('btn-import-json').onclick = async () => {\n const source = $('input-import').value.trim();\n if (!source) return;\n try {\n const r = await api('/import', { method: 'POST', body: JSON.stringify({ kind: 'json', source }) });\n toast(t('importSuccess') + (r.email || '(no email)'), 'success');\n $('input-import').value = '';\n render();\n } catch (e) { toast(String(e), 'error'); }\n };\n\n $('btn-import-blob').onclick = async () => {\n const source = $('input-import').value.trim();\n if (!source) return;\n try {\n const r = await api('/import', { method: 'POST', body: JSON.stringify({ kind: 'blob', source }) });\n toast(t('importSuccess') + (r.email || '(no email)'), 'success');\n $('input-import').value = '';\n render();\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 { parseImportSource, upsertImportedAccount } from '../cli/import.ts'\nimport { generateFingerprint, recordFingerprintVersion } from '../runtime/fingerprint.ts'\nimport { resolveAntigravityVersionBounded } from '../runtime/version.ts'\nimport { renderDashboardHtml, renderCallbackHtml } from './page.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 }\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 const discovered = await fetchAvailableModels(session.auth.access, session.account.projectId)\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 }, { 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 const enriched = await parseImportSource(body.source, kind)\n const { account } = await upsertImportedAccount(store, enriched, { overwriteExisting: true })\n sendJson(res, 200, { email: account.email ?? null, projectId: enriched.projectId })\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 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(session.auth.access, session.account.projectId)\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 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/verify', handler: handleVerify },\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 ]\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 { 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 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":[],"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: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 // 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'\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 }\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 const discovered = await fetchAvailableModels(session.auth.access, session.account.projectId)\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 }, { 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 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(session.auth.access, session.account.projectId)\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 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/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 ]\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 { 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 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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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",
|
|
@@ -78,4 +78,4 @@
|
|
|
78
78
|
},
|
|
79
79
|
"./package.json": "./package.json"
|
|
80
80
|
}
|
|
81
|
-
}
|
|
81
|
+
}
|