dsh-agy 0.4.4 → 0.4.6
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 +8 -3
- package/lib/{accounts-DW-BL9c3.mjs → accounts-DtIsWCeF.mjs} +15 -11
- package/lib/{accounts-DW-BL9c3.mjs.map → accounts-DtIsWCeF.mjs.map} +1 -1
- package/lib/cli/index.mjs +6 -6
- package/lib/client.js +181 -23
- package/lib/{constants-CmA9wXNe.mjs → constants-AdsD4sKh.mjs} +3 -3
- package/lib/{constants-CmA9wXNe.mjs.map → constants-AdsD4sKh.mjs.map} +1 -1
- package/lib/{exchange-C-tndv7j.mjs → exchange-B_rHhU8K.mjs} +3 -3
- package/lib/{exchange-C-tndv7j.mjs.map → exchange-B_rHhU8K.mjs.map} +1 -1
- package/lib/{import-SxPDjH3u.mjs → import-BqyA9tzt.mjs} +4 -4
- package/lib/{import-SxPDjH3u.mjs.map → import-BqyA9tzt.mjs.map} +1 -1
- package/lib/index.mjs +2 -2
- package/lib/{models-CjB0rXtI.mjs → models-D_54Ro5z.mjs} +3 -3
- package/lib/{models-CjB0rXtI.mjs.map → models-D_54Ro5z.mjs.map} +1 -1
- package/lib/{parse-38V1zT1B.mjs → parse-CeJc7cHo.mjs} +141 -19
- package/lib/parse-CeJc7cHo.mjs.map +1 -0
- package/lib/{plugin-common-CKTYXxSc.mjs → plugin-common-BlGbNHxX.mjs} +426 -73
- package/lib/plugin-common-BlGbNHxX.mjs.map +1 -0
- package/lib/{proxy-BbuQX5U9.mjs → proxy-B7VnbAfO.mjs} +5 -2
- package/lib/proxy-B7VnbAfO.mjs.map +1 -0
- package/lib/{quota-summary-DEA3P3Zh.mjs → quota-summary-DarIZkmo.mjs} +3 -3
- package/lib/{quota-summary-DEA3P3Zh.mjs.map → quota-summary-DarIZkmo.mjs.map} +1 -1
- package/lib/{translate-BR1kuCaz.mjs → translate-vh3M5H_9.mjs} +51 -9
- package/lib/translate-vh3M5H_9.mjs.map +1 -0
- package/lib/web/plugin.d.mts +23 -1
- package/lib/web/plugin.d.mts.map +1 -1
- package/lib/web/plugin.mjs +86 -19
- package/lib/web/plugin.mjs.map +1 -1
- package/package.json +1 -1
- package/lib/parse-38V1zT1B.mjs.map +0 -1
- package/lib/plugin-common-CKTYXxSc.mjs.map +0 -1
- package/lib/proxy-BbuQX5U9.mjs.map +0 -1
- package/lib/translate-BR1kuCaz.mjs.map +0 -1
package/lib/web/plugin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.mjs","names":["body"],"sources":["../../src/ui-prefs.ts","../../src/web/management.ts","../../src/web/i18n.ts","../../src/web/page.ts","../../src/web/plugin.ts"],"sourcesContent":["/**\n * UI preferences persistence: per-installation UI toggles stored in\n * `$DSH_HOME/agy/agy-ui-prefs.json`.\n *\n * Persisted to agy's own JSON file rather than `ctx.settings` to avoid\n * schemastery coupling and keep standalone access clean.\n */\n\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { agyDataFile } from './store/paths.ts'\nimport type { SetUiPrefsPayload, UiPrefsDocument, UiPrefsView } from './ui-prefs-types.ts'\n\nexport * from './ui-prefs-types.ts'\n\nexport const UI_PREFS_VERSION = 1\nexport const UI_PREFS_FILE = 'agy-ui-prefs.json'\n\nexport function sanitizeUiPrefs(raw: unknown): UiPrefsDocument {\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n return { version: UI_PREFS_VERSION }\n }\n const obj = raw as Record<string, unknown>\n const doc: UiPrefsDocument = { version: UI_PREFS_VERSION }\n if (typeof obj.conversationBadge === 'boolean') {\n doc.conversationBadge = obj.conversationBadge\n }\n return doc\n}\n\nexport function parseUiPrefs(text: string): UiPrefsDocument {\n let raw: unknown\n try {\n raw = JSON.parse(text)\n } catch {\n return { version: UI_PREFS_VERSION }\n }\n return sanitizeUiPrefs(raw)\n}\n\nexport class UiPrefsStore {\n readonly file: string\n private doc: UiPrefsDocument\n private raw: string\n\n constructor(file: string = agyDataFile(UI_PREFS_FILE)) {\n this.file = file\n const { doc, raw } = this.readWithRaw()\n this.doc = doc\n this.raw = raw\n }\n\n get(): UiPrefsView {\n this.reloadIfChanged()\n return {\n conversationBadge: this.doc.conversationBadge === true,\n }\n }\n\n set(prefs: Partial<SetUiPrefsPayload>): UiPrefsView {\n this.reload()\n const next: UiPrefsDocument = {\n version: UI_PREFS_VERSION,\n ...(this.doc.conversationBadge !== undefined ? { conversationBadge: this.doc.conversationBadge } : {}),\n }\n if (typeof prefs.conversationBadge === 'boolean') {\n next.conversationBadge = prefs.conversationBadge\n }\n this.write(next)\n this.doc = next\n this.raw = UiPrefsStore.serialize(next)\n return {\n conversationBadge: next.conversationBadge === true,\n }\n }\n\n reload(): void {\n const fresh = this.readWithRaw()\n this.doc = fresh.doc\n this.raw = fresh.raw\n }\n\n private reloadIfChanged(): void {\n let current: string\n try {\n current = readFileSync(this.file, 'utf8')\n } catch {\n current = ''\n }\n if (current === this.raw) return\n this.reload()\n }\n\n private readWithRaw(): { doc: UiPrefsDocument; raw: string } {\n let text: string\n try {\n text = readFileSync(this.file, 'utf8')\n } catch {\n return { doc: { version: UI_PREFS_VERSION }, raw: '' }\n }\n return { doc: parseUiPrefs(text), raw: text }\n }\n\n private write(doc: UiPrefsDocument): void {\n const serialized = UiPrefsStore.serialize(doc)\n const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`\n mkdirSync(dirname(this.file), { recursive: true })\n writeFileSync(tmp, serialized, { mode: 0o600 })\n renameSync(tmp, this.file)\n }\n\n static serialize(doc: UiPrefsDocument): string {\n return `${JSON.stringify(doc, null, 2)}\\n`\n }\n}\n","/**\n * agy management API: one method dispatcher shared by the RPC transport.\n *\n * Every method is a pure `(payload) => result` function so the transport stays\n * thin and the handlers stay testable without HTTP. Failures throw; the\n * transport converts a thrown error into the RPC failure envelope.\n *\n * This module owns the OAuth pending-authorization state because `auth.url`\n * issues it and the callback route consumes it — they must share one map, and\n * splitting them would let a verifier and its callback drift apart.\n */\n\nimport { authorizeAntigravity } from '../oauth/authorize.ts'\nimport { exchangeAntigravity } from '../oauth/exchange.ts'\nimport { importManySources, upsertImportedAccount } from '../cli/import.ts'\nimport { generateFingerprint, recordFingerprintVersion } from '../runtime/fingerprint.ts'\nimport { currentAgyVersion } from '../oauth/constants.ts'\nimport { maskProxyUrl } from '../store/accounts.ts'\nimport { accountFetch, isProxyReachable, normalizeProxyUrl, proxyUrlForLogs, withTotalTimeout } from '../proxy.ts'\nimport { AGY_PROVIDER } from '../adapter/models.ts'\nimport type { DiscoveredModelEntry } from '../adapter/models.ts'\nimport { clearExpiredState } from '../runtime/rotation.ts'\nimport { foldWindowBreakdown } from '../stats.ts'\nimport { THINKING_BUDGET_MAX, THINKING_BUDGET_MIN } from '../thinking-budget.ts'\nimport { CLAUDE_BUDGET_MAX, CLAUDE_BUDGET_MIN } from '../thinking-types.ts'\nimport { dayKey } from '../stats.ts'\nimport type { RecentActivity, UsageCounters, UsageSource } from '../stats.ts'\nimport type { AccountStore } from '../store/accounts.ts'\nimport type { AgySessionManager } from '../session.ts'\nimport type { ModelVisibility } from '../model-visibility.ts'\nimport type { UsageStats } from '../stats.ts'\nimport { UiPrefsStore } from '../ui-prefs.ts'\nimport type {\n AccountState,\n AccountUsageView,\n AccountView,\n AgyRpcMethod,\n ModelView,\n RangeBreakdown,\n StatsView,\n ThinkingBudgets,\n} from '../rpc-contract.ts'\n\n/** One pending OAuth authorization, keyed by its raw state. */\ninterface PendingAuth {\n verifier: string\n expiresAt: number\n}\n\nexport interface AgyManagementOptions {\n store: AccountStore\n sessions: AgySessionManager\n stats: UsageStats\n /**\n * The persisted recent-activity ring (see `recent-store.ts`).\n *\n * A plain accessor rather than the store instance, matching `thinkingBudget`:\n * this module needs exactly one operation and cannot reach the rest.\n */\n recentRequests: () => RecentActivity[]\n modelVisibility: ModelVisibility\n uiPrefs?: UiPrefsStore\n /**\n * The global reasoning-level budget map (see `thinking-budget.ts`).\n *\n * A plain accessor pair rather than the store instance, so this module depends\n * only on the two operations it exposes and cannot reach the rest of the store.\n */\n thinkingBudget: {\n all: () => ThinkingBudgets\n set: (level: string, value: number | undefined) => ThinkingBudgets\n claude: () => number | undefined\n setClaude: (value: number | undefined) => number | undefined\n tiered: () => number | undefined\n setTiered: (value: number | undefined) => number | undefined\n }\n /**\n * The adapter's *unfiltered* model catalog.\n *\n * A function rather than the adapter itself so this module depends on the one\n * capability it needs. It must not be `llm.listModels()`, which already has\n * the hidden set applied — see the `model.list` handler.\n */\n listAllModels: () => Promise<readonly { id: string; name: string }[]>\n /**\n * Drop the adapter's cached model list.\n *\n * The cache is keyed to no account (see `AgyAdapter.invalidateModelCache`), so\n * the events that switch the account discovery rides must drop it explicitly —\n * `account.activate` is the one such event reachable from this surface.\n */\n invalidateModelCache: () => void\n /**\n * Harness web-server base URL, used to build the loopback OAuth redirect.\n * A function because the port in it is the one the server bound, known only\n * once it is listening — see `webBaseUrl` in `plugin.ts`.\n */\n baseUrl: () => string\n /**\n * Tell DSH the model catalog changed.\n *\n * The hidden-model list lives in agy's own file, so nothing in DSH emits a\n * change when it is written. The client refreshes the picker on\n * `llm/adapters-updated` alone, so without this a toggle would only take\n * effect after a page reload.\n */\n notifyModelsChanged: () => void\n}\n\nexport interface AgyManagement {\n /** Dispatch one method; rejects with a legible message on failure. */\n call(method: string, payload: unknown): Promise<unknown>\n /** Consume an OAuth callback (the one endpoint that stays a real HTTP route). */\n handleCallback(params: URLSearchParams): Promise<{ ok: boolean; email?: string | null; error?: string }>\n}\n\n/** Fail one RPC with a message the UI can show verbatim. */\nfunction fail(message: string): never {\n throw new Error(message)\n}\n\nfunction asIndex(payload: unknown): number {\n const index = Number((payload as { index?: unknown })?.index)\n if (!Number.isInteger(index) || index < 0) fail('invalid index')\n return index\n}\n\n/**\n * Flatten one account's ledger entry for transport.\n *\n * `models` is deliberately NOT carried: the per-model table that read it was\n * removed (the cumulative metric strip states the same figures better), and\n * sending fields no client reads is a wire surface with no consumer, which\n * reads as intentional to the next person and invites a stale assumption.\n * `lastUsedAt` DOES travel — the account row's activity fragment reads it.\n */\nfunction toAccountUsageView(\n usage: { totals: UsageCounters; sources: Record<UsageSource, number>; lastUsedAt?: number } | undefined,\n): AccountUsageView | null {\n if (usage === undefined) return null\n return {\n totals: usage.totals,\n sources: usage.sources,\n lastUsedAt: typeof usage.lastUsedAt === 'number' && Number.isFinite(usage.lastUsedAt)\n ? usage.lastUsedAt\n : null,\n }\n}\n\nexport function createAgyManagement(options: AgyManagementOptions): AgyManagement {\n const { store, sessions, stats, recentRequests, modelVisibility, listAllModels, invalidateModelCache, baseUrl, notifyModelsChanged } = options\n const thinkingBudget = options.thinkingBudget\n const uiPrefs = options.uiPrefs ?? new UiPrefsStore()\n\n /** Authorizations issued by `auth.url`, keyed by raw state. */\n const pendingAuth = new Map<string, PendingAuth>()\n const PENDING_AUTH_TTL_MS = 10 * 60 * 1000\n\n const prunePendingAuth = (now = Date.now()): void => {\n for (const [key, entry] of pendingAuth) {\n if (entry.expiresAt <= now) pendingAuth.delete(key)\n }\n // Bound the map even under abuse: drop the soonest-to-expire entries.\n while (pendingAuth.size > 100) {\n let oldestKey: string | null = null\n let oldestExpiry = Infinity\n for (const [key, entry] of pendingAuth) {\n if (entry.expiresAt < oldestExpiry) {\n oldestExpiry = entry.expiresAt\n oldestKey = key\n }\n }\n if (oldestKey === null) break\n pendingAuth.delete(oldestKey)\n }\n }\n\n const listAccounts = async (): Promise<AccountView[]> => {\n const storage = await store.load()\n const now = Date.now()\n // Expire stale cooldown state BEFORE rendering. `clearExpiredState` otherwise\n // only runs inside `pickAccount`, i.e. only when a request is actually made,\n // so a management read showed a reason for a window that had already lapsed —\n // the badge said \"active\" while the detail row still said \"network-error\",\n // contradicting itself on one screen. Purely in-memory here: the next real\n // mutation persists it, and a read must not write.\n for (const account of storage.accounts) clearExpiredState(account, now)\n const ledger = stats.snapshot()\n const rows: AccountView[] = []\n for (const [index, account] of storage.accounts.entries()) {\n const state: AccountState = account.enabled === false\n ? 'disabled'\n : account.verificationRequired === true\n ? 'verification-required'\n : account.coolingDownUntil !== undefined && account.coolingDownUntil > now\n ? 'cooling'\n : 'active'\n const key = account.email ?? account.id\n rows.push({\n index,\n email: account.email ?? null,\n projectId: account.projectId ?? null,\n active: index === storage.activeIndex && account.enabled !== false,\n state,\n // Disabled is terminal until a human acts (verify / re-import), so the\n // time is the actionable half: \"minutes ago\" makes an immediate verify\n // worth trying, \"weeks ago\" says re-login. Both disable writers persist\n // `verificationRequiredAt` in the same mutation as `enabled = false`,\n // and the parked-but-enabled path must NOT read as a disable time.\n disabledAt: account.enabled === false && account.verificationRequiredAt !== undefined\n ? new Date(account.verificationRequiredAt).toISOString()\n : null,\n cooldownUntil: account.coolingDownUntil !== undefined && account.coolingDownUntil > now\n ? new Date(account.coolingDownUntil).toISOString()\n : null,\n cooldownReason: account.cooldownReason ?? null,\n // Only while the window is live: `clearExpiredState` above already drops\n // both fields for an expired one, so this cannot outlive its reason.\n cooldownSetAt: account.cooldownSetAt === undefined\n ? null\n : new Date(account.cooldownSetAt).toISOString(),\n /**\n * Appeal link from an upstream verification challenge. Surfaced, never\n * followed automatically: only the account owner can complete it, and the\n * account returns to service on its own once the wall clears.\n */\n verificationUrl: account.verificationUrl ?? null,\n verificationRequired:\n account.verificationRequired === true && account.enabled !== false,\n rateLimits: account.rateLimitResetTimes ?? null,\n fingerprint: account.fingerprint\n ? {\n userAgent: account.fingerprint.userAgent,\n deviceId: account.fingerprint.deviceId,\n createdAt: account.fingerprint.createdAt,\n }\n : null,\n fingerprintHistory: (account.fingerprintHistory ?? []).length,\n proxy: account.proxy ? maskProxyUrl(account.proxy) : null,\n // Read from the cache the session manager already refreshes alongside the\n // per-model quota, so showing the windows costs no request. Null means\n // \"never measured\" and renders as an explicit placeholder rather than a\n // zero, which would read as an exhausted account.\n limits: account.cachedLimits?.groups ?? null,\n limitsUpdatedAt: account.cachedLimits?.updatedAt ?? null,\n // The burn rate arrives via `account.limits` (it exists only after the\n // sampling path has run twice); `account.list` stays probe-free.\n limitBurn: null,\n usage: key === undefined ? null : toAccountUsageView(ledger.accounts[key]),\n })\n }\n // Per-model quota is deliberately NOT fetched here (nor anywhere else): it\n // costs an upstream round trip, and embedding it in this reply once meant the\n // accounts list could not render until that probe finished — on a slow\n // network the panel showed \"no accounts\" behind a permanent spinner,\n // indistinguishable from data loss. The grouped 5h/weekly windows are the\n // surviving quota display, and they arrive via `account.limits`.\n return rows\n }\n\n /** Fold the ledger into the Usage tab's view. */\n const statsView = (): StatsView => {\n const doc = stats.snapshot()\n const now = Date.now()\n\n // Per-model totals across accounts: sum the same model id from every account.\n const byModel = new Map<string, UsageCounters>()\n for (const usage of Object.values(doc.accounts)) {\n for (const [model, counters] of Object.entries(usage.models)) {\n const current = byModel.get(model)\n if (current === undefined) {\n byModel.set(model, { ...counters })\n } else {\n for (const key of Object.keys(current) as Array<keyof UsageCounters>) {\n current[key] += counters[key]\n }\n }\n }\n }\n const allModels = [...byModel.entries()]\n .map(([model, counters]) => ({ model, counters }))\n .sort((a, b) => b.counters.requests - a.counters.requests)\n\n // Each range carries its OWN breakdown, so the tables follow the range\n // selection instead of showing all-time rows beneath range-scoped headline\n // figures (30 requests above a 164-request row, on one screen). The\n // all-time range reads the account maps directly, which hold every request\n // ever recorded rather than only the retained day window.\n const allAccounts = Object.entries(doc.accounts)\n .map(([account, usage]) => ({ account, counters: usage.totals }))\n .sort((a, b) => b.counters.requests - a.counters.requests)\n const breakdown = (days: number): RangeBreakdown => {\n const { totals, models, accounts } = foldWindowBreakdown(doc, days, now)\n return { counters: totals, models, accounts }\n }\n\n // Last 7 LOCAL calendar days, zeros filled: the trend table's job is to\n // show shape over time, and a gap day reads as data loss rather than idle.\n // Same local keying as the ledger's own buckets (dayKey).\n const daySeries = Array.from({ length: 7 }, (_, i) => {\n const key = dayKey(now - (6 - i) * 86_400_000)\n const bucket = doc.days[key]\n return {\n day: key,\n requests: bucket?.totals.requests ?? 0,\n failed: bucket?.totals.failed ?? 0,\n rateLimited: bucket?.totals.rateLimited ?? 0,\n rotations: bucket?.totals.rotations ?? 0,\n }\n })\n return {\n since: doc.totals.requests > 0 ? doc.since : null,\n all: { counters: doc.totals, models: allModels, accounts: allAccounts },\n today: breakdown(1),\n week: breakdown(7),\n month: breakdown(30),\n days: daySeries,\n }\n }\n\n const methods: Record<AgyRpcMethod, (payload: unknown) => Promise<unknown>> = {\n 'account.list': async () => ({ accounts: await listAccounts() }),\n\n 'pool.status': async () => ({ busy: await sessions.inFlightAccounts() }),\n\n 'pool.recent': async () => ({ recent: recentRequests() }),\n\n 'account.activate': async (payload) => {\n const index = asIndex(payload)\n await sessions.activateAccount(index)\n // The activated account may see a different catalog, and the model-list\n // cache is keyed to no account — drop it or the picker serves the\n // previous account's list until the TTL expires.\n invalidateModelCache()\n return { ok: true, index }\n },\n\n 'account.delete': async (payload) => {\n const index = asIndex(payload)\n await store.mutate((storage) => {\n if (index >= storage.accounts.length) fail('account not found')\n storage.accounts.splice(index, 1)\n if (storage.activeIndex >= storage.accounts.length) storage.activeIndex = 0\n })\n return { ok: true }\n },\n\n 'account.verify': async (payload) => sessions.verifyAccount(asIndex(payload)),\n\n 'account.health': async (payload) => {\n const raw = (payload as { indices?: unknown })?.indices\n const indices = Array.isArray(raw) ? raw.map((value) => Number(value)) : undefined\n return { results: await sessions.checkAccounts(indices) }\n },\n\n /**\n * Refresh and return the 5h/weekly windows for every enabled account.\n *\n * A SEPARATE call rather than a field on `account.list`, because that reply\n * is deliberately probe-free (see the note above its return): folding an\n * upstream call into it made the whole accounts+usage view wait on the\n * network and show \"no accounts\" behind a spinner. Here the client renders\n * the list from `account.list` immediately and merges these in when they\n * arrive, so a slow probe costs a placeholder, not the page.\n *\n * Server-side this is TTL-gated, so the upstream call happens at most once\n * per window rather than once per view. `force` bypasses that gate for an\n * EXPLICIT refresh — without it the toolbar's Refresh button could not\n * deliver \"the latest numbers now\", which is the only reason to click it\n * while the snapshot is still inside its TTL.\n */\n 'account.limits': async (payload) => {\n const force = (payload as { force?: unknown } | undefined)?.force === true\n const storage = await store.load()\n // Best-effort: a failed refresh leaves whatever was cached, so this reply\n // is always the current best knowledge rather than an error. The OUTCOME\n // is still reported, because \"probed and failed\" and \"was still fresh\"\n // both leave the numbers identical and the user needs to know which.\n const result = await sessions\n .refreshLimits(storage, { force })\n .catch(() => ({ measured: [], failed: [], skipped: 0 }))\n const fresh = await store.load()\n // Burn rates ride the same reply (the sampling path runs inside\n // refreshLimits); a join failure degrades to null rates, not an error.\n const burn = await sessions.limitBurnRates().catch(() => [])\n const burnByIndex = new Map(burn.map((entry) => [entry.index, entry.perHour]))\n return {\n limits: fresh.accounts.map((account, index) => ({\n index,\n groups: account.cachedLimits?.groups ?? null,\n updatedAt: account.cachedLimits?.updatedAt ?? null,\n burn: burnByIndex.get(index) ?? null,\n })),\n measured: result.measured.length,\n failed: result.failed.length,\n skipped: result.skipped,\n }\n },\n\n 'account.test': async (payload) => {\n const model = (payload as { model?: unknown })?.model\n if (typeof model !== 'string' || model === '') fail('model is required')\n // The account row the user clicked. Optional so a caller that only wants\n // \"test whatever the pool picks\" still works, but a row click always\n // supplies it — otherwise the probe silently ran on the affinity account\n // and reported its result as this account's.\n const rawIndex = (payload as { index?: unknown })?.index\n return sessions.testCall(model, {\n ...(rawIndex === undefined ? {} : { accountIndex: asIndex(payload) }),\n })\n },\n\n 'account.export': async (payload) => sessions.exportBlob(asIndex(payload)),\n\n 'account.exportAll': async () => {\n const storage = await store.load()\n const blobs: Array<{ index: number; blob: string }> = []\n for (let index = 0; index < storage.accounts.length; index++) {\n const result = await sessions.exportBlob(index)\n if (result.blob !== undefined) blobs.push({ index, blob: result.blob })\n }\n return { blobs }\n },\n\n 'account.import': async (payload) => {\n const body = payload as { kind?: unknown; sources?: unknown } | undefined\n const kind = body?.kind === 'blob' ? 'blob' : 'json'\n const sources = Array.isArray(body?.sources) ? body.sources : []\n if (sources.length === 0) fail('nothing to import')\n const result = await importManySources(\n sources.map((source) => ({ source, kind })),\n store,\n { overwriteExisting: true },\n )\n // Per-source failures are part of the result, not a thrown error: a batch\n // where 2 of 5 sources were malformed is a partial success, and the caller\n // must be able to say which ones failed.\n return {\n imported: result.imported,\n replaced: result.replaced,\n errors: result.errors.map((error) => String(error)),\n }\n },\n\n 'account.fingerprint': async (payload) => {\n const index = asIndex(payload)\n const action = (payload as { action?: unknown })?.action === 'regenerate' ? 'regenerate' : 'show'\n return store.mutate(async (storage) => {\n const account = storage.accounts[index]\n if (!account) fail('account not found')\n if (action === 'regenerate') {\n // Same version source as first-use creation: the published value the\n // resolver maintains, never a per-call network probe whose result the\n // pool-randomizing default would silently discard anyway.\n const fresh = generateFingerprint(undefined, currentAgyVersion())\n account.fingerprint = fresh\n account.fingerprintHistory = recordFingerprintVersion(account.fingerprintHistory, fresh, 'regenerated')\n }\n return {\n action,\n fingerprint: account.fingerprint\n ? {\n userAgent: account.fingerprint.userAgent,\n deviceId: account.fingerprint.deviceId,\n createdAt: account.fingerprint.createdAt,\n }\n : null,\n history: account.fingerprintHistory?.length ?? 0,\n }\n })\n },\n\n 'account.proxy': async (payload) => {\n const index = asIndex(payload)\n const raw = (payload as { proxy?: unknown })?.proxy\n const proxy = typeof raw === 'string' ? raw : ''\n if (proxy.trim() === '') {\n await store.mutate((storage) => {\n const account = storage.accounts[index]\n if (!account) fail('account not found')\n delete account.proxy\n })\n return { ok: true as const, proxy: null, proxyMasked: null, rawLogs: null }\n }\n let normalized: string\n try {\n normalized = normalizeProxyUrl(proxy)\n } catch (error) {\n fail(error instanceof Error ? error.message : String(error))\n }\n await store.mutate((storage) => {\n const account = storage.accounts[index]\n if (!account) fail('account not found')\n account.proxy = normalized\n })\n return {\n ok: true as const,\n proxy: maskProxyUrl(normalized),\n proxyMasked: maskProxyUrl(normalized),\n rawLogs: proxyUrlForLogs(normalized),\n }\n },\n\n 'account.proxyTest': async (payload) => {\n const body = payload as { index?: unknown; proxy?: unknown } | undefined\n const hasExplicit = typeof body?.proxy === 'string' && body.proxy.trim() !== ''\n let target: string\n if (hasExplicit) {\n try {\n target = normalizeProxyUrl(body.proxy as string)\n } catch (error) {\n fail(error instanceof Error ? error.message : String(error))\n }\n } else {\n const index = asIndex(payload)\n const storage = await store.load()\n const account = storage.accounts[index]\n if (!account) fail('account not found')\n if (!account.proxy) fail('no proxy configured for this account')\n target = account.proxy\n }\n // Masked for display only; the raw URL never leaves this function.\n const masked = maskProxyUrl(target) ?? proxyUrlForLogs(target)\n try {\n const ok = await isProxyReachable(target, 2000)\n return ok ? { ok, masked } : { ok, masked, error: `proxy unreachable: ${masked}` }\n } catch (error) {\n return { ok: false, masked, error: error instanceof Error ? error.message : String(error) }\n }\n },\n\n 'auth.url': async () => {\n const authorization = await authorizeAntigravity(`${baseUrl()}/agy/oauth-callback`)\n prunePendingAuth()\n pendingAuth.set(authorization.state, {\n verifier: authorization.verifier,\n expiresAt: Date.now() + PENDING_AUTH_TTL_MS,\n })\n return { url: authorization.url }\n },\n\n 'model.list': async () => {\n const session = await sessions.getSession().catch(() => undefined)\n if (session === undefined) fail('No agy account configured — run `dsh-agy login` first.')\n // The adapter's unfiltered catalog: reading the filtered `listModels()`\n // would hide a disabled model from this very list, leaving no switch to\n // turn it back on.\n const models = await listAllModels()\n const hidden = modelVisibility.disabledFor(AGY_PROVIDER)\n const rows: ModelView[] = models.map((model) => ({\n id: model.id,\n name: model.name,\n disabled: hidden.has(model.id),\n }))\n return { account: session.account.email ?? null, models: rows }\n },\n\n 'model.setDisabled': async (payload) => {\n const body = payload as { modelId?: unknown; disabled?: unknown } | undefined\n const modelId = body?.modelId\n if (typeof modelId !== 'string' || modelId === '') fail('modelId is required')\n const disabled = body?.disabled === true\n modelVisibility.setDisabled(AGY_PROVIDER, modelId, disabled)\n // Push the change to every open client so the picker updates live.\n notifyModelsChanged()\n return { modelId, disabled }\n },\n\n 'thinking.get': async () => ({\n budgets: thinkingBudget.all(),\n min: THINKING_BUDGET_MIN,\n max: THINKING_BUDGET_MAX,\n tieredBudget: thinkingBudget.tiered() ?? null,\n claudeBudget: thinkingBudget.claude() ?? null,\n claudeMin: CLAUDE_BUDGET_MIN,\n claudeMax: CLAUDE_BUDGET_MAX,\n }),\n\n 'thinking.setTiered': async (payload) => {\n const body = payload as { budget?: unknown } | undefined\n const raw = body?.budget\n if (raw !== undefined && raw !== null && typeof raw !== 'number') fail('budget must be a number')\n try {\n const value = thinkingBudget.setTiered(raw === undefined || raw === null ? undefined : raw)\n return { tieredBudget: value ?? null }\n } catch (error) {\n fail(error instanceof Error ? error.message : String(error))\n }\n },\n\n 'thinking.setClaude': async (payload) => {\n const body = payload as { budget?: unknown } | undefined\n const raw = body?.budget\n if (raw !== undefined && raw !== null && typeof raw !== 'number') fail('budget must be a number')\n try {\n const value = thinkingBudget.setClaude(raw === undefined || raw === null ? undefined : raw)\n return { claudeBudget: value ?? null }\n } catch (error) {\n fail(error instanceof Error ? error.message : String(error))\n }\n },\n\n 'thinking.set': async (payload) => {\n const body = payload as { level?: unknown; budget?: unknown } | undefined\n const level = body?.level\n if (typeof level !== 'string' || level === '') fail('level is required')\n // `undefined` and explicit null BOTH clear the entry: the field means \"let\n // upstream choose\", and a JSON null is how a cleared input arrives.\n const raw = body?.budget\n if (raw !== undefined && raw !== null && typeof raw !== 'number') fail('budget must be a number')\n const value = raw === undefined || raw === null ? undefined : raw\n try {\n const budgets = thinkingBudget.set(level, value)\n return { budgets }\n } catch (error) {\n // Surface the range/name violation as a caller-correctable message\n // rather than an unlabelled RPC failure.\n fail(error instanceof Error ? error.message : String(error))\n }\n },\n\n 'stats.get': async () => statsView(),\n\n 'ui.prefs.get': async () => uiPrefs.get(),\n\n 'ui.prefs.set': async (payload) => {\n const body = payload as { conversationBadge?: unknown } | undefined\n if (body?.conversationBadge !== undefined && typeof body.conversationBadge !== 'boolean') {\n fail('conversationBadge must be a boolean')\n }\n return uiPrefs.set({\n conversationBadge: body?.conversationBadge as boolean | undefined,\n })\n },\n }\n\n return {\n async call(method, payload) {\n const handler = methods[method as AgyRpcMethod]\n if (handler === undefined) fail(`unknown method: ${method}`)\n return handler(payload)\n },\n\n async handleCallback(params) {\n const code = params.get('code')\n const state = params.get('state')\n if (!code || !state) return { ok: false, error: 'Missing code or state parameter' }\n const expected = pendingAuth.get(state)\n if (expected === undefined) {\n return { ok: false, error: 'Unknown or expired authorization — please start a new login.' }\n }\n // One-time use: consume the issued state regardless of the exchange result.\n pendingAuth.delete(state)\n const redirectUri = `${baseUrl()}/agy/oauth-callback`\n const result = await exchangeAntigravity(code, state, redirectUri, expected.verifier)\n if (result.type === 'failed') return { ok: false, error: result.error }\n await upsertImportedAccount(store, {\n accessToken: result.access,\n refreshToken: result.refresh.split('|')[0]!,\n tokenType: 'Bearer',\n expiresAt: new Date(result.expires).toISOString(),\n authMethod: 'oauth',\n email: result.email ?? null,\n projectId: result.projectId || null,\n clientId: result.clientId || null,\n }, { overwriteExisting: true })\n return { ok: true, email: result.email ?? null }\n },\n }\n}\n","/**\n * Bilingual UI dictionary for the dsh-agy web dashboard and OAuth callbacks.\n */\n\nexport interface I18nStrings {\n loginSuccessTitle: string\n loginSuccessDesc: string\n loginFailedTitle: string\n windowClosing: string\n}\n\nexport const I18N_DICT: { en: I18nStrings; zh: I18nStrings } = {\n en: {\n loginSuccessTitle: 'Sign-in Successful',\n loginSuccessDesc: 'Your Antigravity account has been authorized and saved.',\n loginFailedTitle: 'Sign-in Failed',\n windowClosing: 'This window will close automatically in a moment...',\n },\n zh: {\n loginSuccessTitle: '授权登录成功',\n loginSuccessDesc: 'Antigravity 账号已成功接入并保存。',\n loginFailedTitle: '授权登录失败',\n windowClosing: '此窗口即将在 2 秒内自动关闭...',\n },\n}\n","/**\n * Browser-facing HTML for the agy OAuth callback.\n *\n * This is the only page agy serves. The account/statistics dashboard that used\n * to live at `/agy` is gone: that surface is now the Settings section rendered\n * by `src/client/` over the `/api/agy` RPC, so there is no longer a second UI to\n * keep in step with it.\n *\n * The callback stays a real page because Google redirects the browser here.\n */\n\nimport { I18N_DICT } from './i18n.ts'\n\n/**\n * Escape text for an HTML text/attribute position.\n *\n * Every interpolation below except the static markup is attacker-influenced or\n * upstream-influenced: `error` is the token endpoint's raw response body, and\n * `email` comes from Google's userinfo. This page is served by the SAME web\n * server, and therefore the same origin, as the DSH GUI — so injected script\n * here would run with the GUI's session and could reach `/api/agy`\n * (`account.exportAll` returns live credential blobs). That chain is why the\n * escaping matters more than the narrow trigger suggests.\n *\n * Text in markup goes through {@link escapeHtml}; text inside the inline\n * `<script>` goes through {@link jsonForInlineScript}. Neither is optional.\n */\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\n/**\n * Serialize a value for interpolation into the inline `<script>`.\n *\n * `JSON.stringify` escapes for a JS string context only, NOT for the HTML script\n * data state: it leaves `<` alone, so an email or error body containing\n * `</script>` terminates the element early and everything after it is parsed as\n * markup. Escaping the three HTML-significant characters as `\\uXXXX` keeps the\n * JSON value identical while making the byte sequence unrepresentable in the\n * source. (The `JSON.stringify` output above is a valid JS string either way,\n * because a `\\u003c` escape and a literal `<` denote the same character.)\n */\nfunction jsonForInlineScript(value: unknown): string {\n return JSON.stringify(value)\n .replace(/</g, '\\\\u003c')\n .replace(/>/g, '\\\\u003e')\n .replace(/&/g, '\\\\u0026')\n}\n\nexport function renderCallbackHtml(options: { ok: boolean; error?: string; email?: string | null; baseUrl: string }): string {\n const { ok, error, email, baseUrl } = options\n const i18nJson = jsonForInlineScript(I18N_DICT)\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Antigravity Sign-in</title>\n <style>\n :root {\n color-scheme: light dark;\n --dsw-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;\n --bg-page: #0f1115;\n --bg-surface: #171a21;\n --border-l2: rgba(255, 255, 255, 0.12);\n --text-primary: #f8fafc;\n --text-secondary: #94a3b8;\n --brand-primary: #5686fe;\n --state-success: #34d399;\n --state-error: #f87171;\n }\n @media (prefers-color-scheme: light) {\n :root {\n --bg-page: #f8fafc;\n --bg-surface: #ffffff;\n --border-l2: rgba(0, 0, 0, 0.12);\n --text-primary: #0f172a;\n --text-secondary: #475569;\n --brand-primary: #4176e6;\n --state-success: #22c55e;\n --state-error: #ef4444;\n }\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: var(--dsw-font-family);\n background-color: var(--bg-page);\n color: var(--text-primary);\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n padding: 16px;\n }\n .card {\n background: var(--bg-surface);\n border: 1px solid var(--border-l2);\n border-radius: 12px;\n padding: 24px;\n max-width: 440px;\n width: 100%;\n text-align: center;\n box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);\n }\n .icon-wrap {\n width: 48px;\n height: 48px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 16px;\n font-size: 24px;\n }\n .icon-success { background: rgba(52, 211, 153, 0.15); color: var(--state-success); }\n .icon-error { background: rgba(248, 113, 113, 0.15); color: var(--state-error); }\n h1 { font-size: 18px; font-weight: 600; margin-bottom: 8px; }\n p { font-size: 13.5px; color: var(--text-secondary); margin-bottom: 16px; line-height: 1.5; }\n .btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-family: inherit;\n font-size: 13px;\n font-weight: 500;\n height: 34px;\n padding: 0 16px;\n border-radius: 17px;\n border: 1px solid var(--border-l2);\n background: transparent;\n color: var(--text-primary);\n cursor: pointer;\n text-decoration: none;\n }\n .btn:hover { background: rgba(128, 128, 128, 0.1); }\n .btn-primary { background: var(--brand-primary); color: #ffffff; border-color: transparent; }\n </style>\n</head>\n<body>\n <div class=\"card\">\n ${ok ? `\n <div class=\"icon-wrap icon-success\">✓</div>\n <h1 id=\"title\">Sign-in Successful</h1>\n <p id=\"desc\">Your Antigravity account ${email ? '<strong>' + escapeHtml(email) + '</strong> ' : ''}has been authorized and saved.</p>\n <p style=\"font-size:12px;opacity:0.8;\" id=\"closing\">This window will close automatically...</p>\n <button class=\"btn\" onclick=\"window.close()\">Close Window</button>\n ` : `\n <div class=\"icon-wrap icon-error\">✕</div>\n <h1 id=\"title\">Sign-in Failed</h1>\n <p id=\"desc\">Error details: ${escapeHtml(error || 'Unknown error')}</p>\n <a class=\"btn btn-primary\" href=\"${escapeHtml(baseUrl)}/\">Return to Settings</a>\n `}\n </div>\n <script>\n const I18N = ${i18nJson};\n const lang = localStorage.getItem('agy_lang') || (navigator.language.startsWith('zh') ? 'zh' : 'en');\n const dict = I18N[lang] || I18N.en;\n\n if (${ok ? 'true' : 'false'}) {\n document.getElementById('title').textContent = dict.loginSuccessTitle;\n // textContent, not innerHTML: an email containing markup must never be\n // parsed as HTML. The string itself is serialized for the script data\n // state by jsonForInlineScript, so it cannot terminate this element.\n document.getElementById('desc').textContent = dict.loginSuccessDesc + (${jsonForInlineScript(email ? ' (' + email + ')' : '')});\n document.getElementById('closing').textContent = dict.windowClosing;\n \n // Notify parent window & close\n try {\n if (window.opener) {\n // Target this page's own origin rather than '*', so the message cannot\n // be delivered to an unrelated opener.\n window.opener.postMessage({ type: 'agy_login_success' }, window.location.origin);\n }\n } catch (e) {}\n setTimeout(() => {\n window.close();\n }, 1800);\n } else {\n document.getElementById('title').textContent = dict.loginFailedTitle;\n }\n </script>\n</body>\n</html>`\n}\n","/**\n * dsh-agy web entry.\n *\n * Two things are registered, and they deliberately use different transports:\n *\n * 1. **Management RPC at `/api/agy`** (`connection.fetch.register`). The inline\n * Settings UI calls methods over DSH's own RPC channel. This replaces the\n * previous bare `ctx.webServer.register` route table, which had no\n * authentication of its own: `/api/*` sits behind the host's browser-trust\n * fence and BrowserAuth, so the management surface is no longer reachable by\n * anything that can merely open a socket to a loopback port.\n *\n * 2. **The OAuth callback at `/agy/oauth-callback`** stays a real HTTP route.\n * Google redirects a browser to it with a GET, which the management RPC\n * channel (POST-only) cannot carry.\n *\n * BOTH `webServer` and `connection` are deliberately reached lazily rather than\n * statically injected. Each exists only in a Web composition, and a statically\n * injected service that never appears leaves this entry permanently pending —\n * and the loader treats a pending entry as a FAILED PROFILE, not a skipped one:\n *\n * dsh: plugin tree failed to load: dsh: 1 entry did not activate\n * dsh-agy/web: pending (waiting for service: webServer)\n *\n * Measured on a real TUI profile, where no provider of `webServer` is mounted.\n * `ctx.inject([...])` keeps this entry active and inert instead, so a TUI or\n * headless profile boots with no management surface rather than not booting.\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\n// Type-only: merges the `llm/adapters-updated` event into the Cordis Events map.\nimport type {} from '@deepseek-ai/dsh-llm/types'\nimport { createAgyRuntime } from '../plugin-common.ts'\nimport { isAgyDisabled } from '../runtime/risk.ts'\nimport { createAgyManagement } from './management.ts'\nimport { renderCallbackHtml } from './page.ts'\n\nexport const name = 'dsh-agy-web'\n\n/** The one service every composition provides; the rest are resolved lazily. */\nexport const inject = ['llm']\n\n/** The slice of the host's web-server service this entry uses. */\ninterface WebServerLike {\n register(route: {\n kind: 'exact'\n path: string\n handler: (req: IncomingMessage, res: ServerResponse) => void\n }): () => void\n host?: string\n /** The port actually listened on: the OS-assigned one when `--port 0`. */\n readonly port?: number\n}\n\n/**\n * The loopback base URL the OAuth redirect and the callback page point at.\n *\n * The port is the one the server BOUND, not the one it was asked for.\n * `webStartup.port` is the `--port` flag verbatim, and `--port 0` — which asks\n * the OS for any free port — would send Google's redirect to\n * `http://127.0.0.1:0/agy/oauth-callback`, which nothing answers.\n * The requested port and DSH's own 3080 default only stand in while the\n * server has not reported one.\n * @param host - loopback host the URL names.\n * @param webServer - the host web server (source of the bound port).\n * @param requestedPort - `webStartup.port`, the `--port` flag if one was given.\n * @returns `http://<host>:<port>`, no trailing slash.\n */\nexport function webBaseUrl(host: string, webServer: Pick<WebServerLike, 'port'>, requestedPort?: number): string {\n return `http://${host}:${webServer.port || requestedPort || 3080}`\n}\n\nexport function apply(ctx: Context): void {\n if (isAgyDisabled()) {\n ctx.logger.warn('[dsh-agy] disabled by DSH_AGY_DISABLE=1 — skipping web registration')\n return\n }\n // Lazily, like `connection` below: see the module docblock for why a static\n // `webServer` is a profile-breaking bug rather than a nicety.\n ctx.inject(['webServer'], (webCtx) => {\n const webServer = webCtx.get('webServer') as WebServerLike | undefined\n if (!webServer) return\n webCtx.effect(() => registerAgyWeb(webCtx, webServer))\n })\n}\n\n/**\n * Register the OAuth callback route, and the management RPC once `connection`\n * appears.\n * @param ctx - the web-server-bearing context (owns both registrations).\n * @param webServer - the host web server.\n * @returns disposer for every registration this function made.\n */\nasync function registerAgyWeb(ctx: Context, webServer: WebServerLike): Promise<() => void> {\n // Host comes from the webStartup provider (CLI args), falling back to DSH's\n // own web-app default (loopback), never a user override. The port does not:\n // see `webBaseUrl`.\n const webStartup = ctx.get('webStartup') as { host?: string; port?: number } | undefined\n const host = webStartup?.host ?? '127.0.0.1'\n\n // The OAuth callback manages credentials with no authentication of its own,\n // so it must never be reachable from the network. When the web server binds\n // a non-loopback interface, refuse to register it (the loopback-only OAuth\n // redirect would be unusable there anyway).\n const bindHost = webServer.host ?? host\n if (!['127.0.0.1', 'localhost', '::1'].includes(bindHost)) {\n ctx.logger.warn(\n '[dsh-agy] web server bound to \"' + bindHost + '\" (non-loopback): not registering the agy routes ' +\n '(they manage account credentials and must stay loopback-only). Bind the web server to 127.0.0.1 to enable them.',\n )\n return () => {}\n }\n\n const { store, sessions, adapter, stats, recentStore, modelVisibility, thinkingBudget } = await createAgyRuntime(ctx)\n // Read per use rather than once here: the bound port is only known after the\n // server's listen callback has run.\n const baseUrl = (): string => webBaseUrl(host, webServer, webStartup?.port)\n const management = createAgyManagement({\n store,\n sessions,\n stats,\n // The persisted ring, not just this boot's in-memory one.\n recentRequests: () => recentStore.recentRequests(),\n modelVisibility,\n // Expose only the two operations the RPC needs, not the whole store.\n thinkingBudget: {\n all: () => thinkingBudget.all(),\n set: (level, value) => thinkingBudget.setBudget(level, value),\n claude: () => thinkingBudget.claudeBudget(),\n setClaude: (value) => thinkingBudget.setClaudeBudget(value).claudeBudget,\n tiered: () => thinkingBudget.tieredBudget(),\n setTiered: (value) => thinkingBudget.setTieredBudget(value).tieredBudget,\n },\n // The adapter's *unfiltered* catalog, so a hidden model still appears in\n // the settings list alongside the switch that un-hides it.\n listAllModels: () => adapter.listAllModels(),\n // Activating an account can switch the account model discovery rides; the\n // cache carries no account key, so the RPC drops it (see\n // `AgyAdapter.invalidateModelCache`).\n invalidateModelCache: () => adapter.invalidateModelCache(),\n baseUrl,\n // DSH refreshes the model picker on `llm/adapters-updated`; the hidden\n // list lives in agy's own file, so nothing else would announce the change\n // and the toggle would appear to do nothing until a page reload.\n // (`llm/adapters-updated` is declared `@mode emit` for exactly this kind\n // of registry notification.)\n notifyModelsChanged: () => { ctx.emit('llm/adapters-updated') },\n })\n const disposers: Array<() => void> = []\n\n // The one endpoint that cannot ride the RPC channel: Google sends the\n // browser here with a GET redirect.\n disposers.push(webServer.register({\n kind: 'exact',\n path: '/agy/oauth-callback',\n handler: async (req: IncomingMessage, res: ServerResponse) => {\n const base = baseUrl()\n const url = new URL(req.url ?? '/', base)\n const result = await management.handleCallback(url.searchParams).catch((error: unknown) => ({\n ok: false as const,\n error: error instanceof Error ? error.message : String(error),\n }))\n res.writeHead(result.ok ? 200 : 400, { 'content-type': 'text/html; charset=utf-8' })\n res.end(result.ok\n ? renderCallbackHtml({ ok: true, email: result.email ?? null, baseUrl: base })\n : renderCallbackHtml({ ok: false, error: result.error ?? 'Unknown error', baseUrl: base }))\n },\n }))\n\n // Lazy injection: see the module docblock for why this is not static.\n ctx.inject(['connection'], (connectionCtx) => {\n const connection = connectionCtx.get('connection') as\n | {\n fetch: {\n register(route: {\n path: string\n methods: string[]\n requestBody: 'buffered' | 'streaming'\n fetch: (request: Request) => Promise<Response>\n }): () => void\n }\n }\n | undefined\n if (!connection || typeof connection.fetch?.register !== 'function') {\n connectionCtx.logger.warn('[dsh-agy] connection.fetch unavailable — management RPC not registered')\n return\n }\n connectionCtx.effect(() => connection.fetch.register({\n path: '/api/agy',\n methods: ['POST'],\n requestBody: 'buffered',\n async fetch(request: Request): Promise<Response> {\n if (request.method !== 'POST') return new Response('method not allowed', { status: 405 })\n const contentType = request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase()\n if (contentType !== 'application/json') {\n return new Response('content type must be application/json', { status: 415 })\n }\n let message: Record<string, unknown>\n try {\n message = await request.json() as Record<string, unknown>\n } catch {\n return new Response('body is not JSON', { status: 400 })\n }\n const rpcId = typeof message.rpcId === 'string' ? message.rpcId : 'invalid-request'\n const call = message.payload as { method?: unknown; payload?: unknown } | undefined\n if (\n message.type !== 'client-request'\n || typeof message.rpcId !== 'string'\n || typeof call?.method !== 'string'\n ) {\n return reply(rpcId, {\n ok: false,\n error: { code: 'agy/bad-request', message: 'Invalid agy management request.' },\n })\n }\n try {\n const value = await management.call(call.method, call.payload)\n return reply(rpcId, { ok: true, value })\n } catch (error) {\n const text = error instanceof Error ? error.message : String(error)\n connectionCtx.logger.warn(`[dsh-agy] ${call.method} failed: ${text}`)\n // A structured failure, not a bare 500: the client's unwrap needs a\n // parseable envelope or a failed call looks like \"nothing happened\".\n return reply(rpcId, { ok: false, error: { code: 'agy/handler-failed', message: text } })\n }\n },\n }), 'dsh-agy: /api/agy management RPC')\n })\n\n return () => {\n for (const dispose of disposers) dispose()\n }\n}\n\n/**\n * Wrap one result in the Connection RPC response envelope.\n *\n * A failure MUST carry `error.details` as an object: the client's envelope\n * parser rejects a failure without it as \"invalid server-response\", which hides\n * the real message behind a transport-sounding error.\n */\nfunction reply(rpcId: string, result: unknown): Response {\n const normalized = isFailure(result)\n ? { ...result, error: { details: {}, ...result.error } }\n : result\n return Response.json({ type: 'server-response', rpcId, result: normalized })\n}\n\n/** Whether one result is an RPC failure envelope. */\nfunction isFailure(value: unknown): value is {\n ok: false\n error: { code?: string, message?: string, details?: unknown }\n} {\n return typeof value === 'object' && value !== null\n && (value as { ok?: unknown }).ok === false\n && typeof (value as { error?: unknown }).error === 'object'\n && (value as { error?: unknown }).error !== null\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin.mjs","names":["body"],"sources":["../../src/ui-prefs.ts","../../src/web/management.ts","../../src/web/i18n.ts","../../src/web/page.ts","../../src/web/plugin.ts"],"sourcesContent":["/**\n * UI preferences persistence: per-installation UI toggles stored in\n * `$DSH_HOME/agy/agy-ui-prefs.json`.\n *\n * Persisted to agy's own JSON file rather than `ctx.settings` to avoid\n * schemastery coupling and keep standalone access clean.\n */\n\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { agyDataFile } from './store/paths.ts'\nimport type { SetUiPrefsPayload, UiPrefsDocument, UiPrefsView } from './ui-prefs-types.ts'\n\nexport * from './ui-prefs-types.ts'\n\nexport const UI_PREFS_VERSION = 1\nexport const UI_PREFS_FILE = 'agy-ui-prefs.json'\n\nexport function sanitizeUiPrefs(raw: unknown): UiPrefsDocument {\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n return { version: UI_PREFS_VERSION }\n }\n const obj = raw as Record<string, unknown>\n const doc: UiPrefsDocument = { version: UI_PREFS_VERSION }\n if (typeof obj.conversationBadge === 'boolean') {\n doc.conversationBadge = obj.conversationBadge\n }\n return doc\n}\n\nexport function parseUiPrefs(text: string): UiPrefsDocument {\n let raw: unknown\n try {\n raw = JSON.parse(text)\n } catch {\n return { version: UI_PREFS_VERSION }\n }\n return sanitizeUiPrefs(raw)\n}\n\nexport class UiPrefsStore {\n readonly file: string\n private doc: UiPrefsDocument\n private raw: string\n\n constructor(file: string = agyDataFile(UI_PREFS_FILE)) {\n this.file = file\n const { doc, raw } = this.readWithRaw()\n this.doc = doc\n this.raw = raw\n }\n\n get(): UiPrefsView {\n this.reloadIfChanged()\n return {\n conversationBadge: this.doc.conversationBadge === true,\n }\n }\n\n set(prefs: Partial<SetUiPrefsPayload>): UiPrefsView {\n this.reload()\n const next: UiPrefsDocument = {\n version: UI_PREFS_VERSION,\n ...(this.doc.conversationBadge !== undefined ? { conversationBadge: this.doc.conversationBadge } : {}),\n }\n if (typeof prefs.conversationBadge === 'boolean') {\n next.conversationBadge = prefs.conversationBadge\n }\n this.write(next)\n this.doc = next\n this.raw = UiPrefsStore.serialize(next)\n return {\n conversationBadge: next.conversationBadge === true,\n }\n }\n\n reload(): void {\n const fresh = this.readWithRaw()\n this.doc = fresh.doc\n this.raw = fresh.raw\n }\n\n private reloadIfChanged(): void {\n let current: string\n try {\n current = readFileSync(this.file, 'utf8')\n } catch {\n current = ''\n }\n if (current === this.raw) return\n this.reload()\n }\n\n private readWithRaw(): { doc: UiPrefsDocument; raw: string } {\n let text: string\n try {\n text = readFileSync(this.file, 'utf8')\n } catch {\n return { doc: { version: UI_PREFS_VERSION }, raw: '' }\n }\n return { doc: parseUiPrefs(text), raw: text }\n }\n\n private write(doc: UiPrefsDocument): void {\n const serialized = UiPrefsStore.serialize(doc)\n const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`\n mkdirSync(dirname(this.file), { recursive: true })\n writeFileSync(tmp, serialized, { mode: 0o600 })\n renameSync(tmp, this.file)\n }\n\n static serialize(doc: UiPrefsDocument): string {\n return `${JSON.stringify(doc, null, 2)}\\n`\n }\n}\n","/**\n * agy management API: one method dispatcher shared by the RPC transport.\n *\n * Every method is a pure `(payload) => result` function so the transport stays\n * thin and the handlers stay testable without HTTP. Failures throw; the\n * transport converts a thrown error into the RPC failure envelope.\n *\n * This module owns the OAuth pending-authorization state because `auth.url`\n * issues it and the callback route consumes it — they must share one map, and\n * splitting them would let a verifier and its callback drift apart.\n */\n\nimport { authorizeAntigravity } from '../oauth/authorize.ts'\nimport { exchangeAntigravity } from '../oauth/exchange.ts'\nimport { importManySources, upsertImportedAccount } from '../cli/import.ts'\nimport { generateFingerprint, recordFingerprintVersion } from '../runtime/fingerprint.ts'\nimport { currentAgyVersion } from '../oauth/constants.ts'\nimport { maskProxyUrl } from '../store/accounts.ts'\nimport { accountFetch, isProxyReachable, normalizeProxyUrl, proxyUrlForLogs, withTotalTimeout } from '../proxy.ts'\nimport { AGY_PROVIDER } from '../adapter/models.ts'\nimport type { DiscoveredModelEntry } from '../adapter/models.ts'\nimport { clearExpiredState } from '../runtime/rotation.ts'\nimport { foldWindowBreakdown } from '../stats.ts'\nimport { THINKING_BUDGET_MAX, THINKING_BUDGET_MIN } from '../thinking-budget.ts'\nimport { CLAUDE_BUDGET_MAX, CLAUDE_BUDGET_MIN } from '../thinking-types.ts'\nimport { dayKey } from '../stats.ts'\nimport type { RecentActivity, UsageCounters, UsageSource } from '../stats.ts'\nimport type { AccountStore } from '../store/accounts.ts'\nimport type { AgySessionManager } from '../session.ts'\nimport type { ModelVisibility } from '../model-visibility.ts'\nimport type { UsageStats } from '../stats.ts'\nimport { UiPrefsStore } from '../ui-prefs.ts'\nimport type {\n AccountState,\n AccountUsageView,\n AccountView,\n AgyRpcMethod,\n ModelView,\n MultimodalView,\n RangeBreakdown,\n StatsView,\n ThinkingBudgets,\n} from '../rpc-contract.ts'\n\n/** One pending OAuth authorization, keyed by its raw state. */\ninterface PendingAuth {\n verifier: string\n expiresAt: number\n}\n\nexport interface AgyManagementOptions {\n store: AccountStore\n sessions: AgySessionManager\n stats: UsageStats\n /**\n * The persisted recent-activity ring (see `recent-store.ts`).\n *\n * A plain accessor rather than the store instance, matching `thinkingBudget`:\n * this module needs exactly one operation and cannot reach the rest.\n */\n recentRequests: () => RecentActivity[]\n modelVisibility: ModelVisibility\n uiPrefs?: UiPrefsStore\n /**\n * The global reasoning-level budget map (see `thinking-budget.ts`).\n *\n * A plain accessor pair rather than the store instance, so this module depends\n * only on the two operations it exposes and cannot reach the rest of the store.\n */\n thinkingBudget: {\n all: () => ThinkingBudgets\n set: (level: string, value: number | undefined) => ThinkingBudgets\n claude: () => number | undefined\n setClaude: (value: number | undefined) => number | undefined\n tiered: () => number | undefined\n setTiered: (value: number | undefined) => number | undefined\n }\n /**\n * The per-file inline cap for non-image multimodal files (see\n * `multimodal-config.ts`).\n *\n * A duck-typed accessor pair, like `thinkingBudget` above, and REQUIRED rather\n * than optional: this one has exactly two call sites in the codebase (the web\n * entry and the test harness), so an omitted wiring would silently leave the\n * card reading a second store's default instead of the runtime's — a mistake\n * the compiler catches only while the field is required.\n */\n multimodal: {\n get: () => MultimodalView\n set: (value: number | undefined) => MultimodalView\n }\n /**\n * The adapter's *unfiltered* model catalog.\n *\n * A function rather than the adapter itself so this module depends on the one\n * capability it needs. It must not be `llm.listModels()`, which already has\n * the hidden set applied — see the `model.list` handler.\n */\n listAllModels: () => Promise<readonly { id: string; name: string }[]>\n /**\n * Drop the adapter's cached model list.\n *\n * The cache is keyed to no account (see `AgyAdapter.invalidateModelCache`), so\n * the events that switch the account discovery rides must drop it explicitly —\n * `account.activate` is the one such event reachable from this surface.\n */\n invalidateModelCache: () => void\n /**\n * Harness web-server base URL, used to build the loopback OAuth redirect.\n * A function because the port in it is the one the server bound, known only\n * once it is listening — see `webBaseUrl` in `plugin.ts`.\n */\n baseUrl: () => string\n /**\n * Tell DSH the model catalog changed.\n *\n * The hidden-model list lives in agy's own file, so nothing in DSH emits a\n * change when it is written. The client refreshes the picker on\n * `llm/adapters-updated` alone, so without this a toggle would only take\n * effect after a page reload.\n */\n notifyModelsChanged: () => void\n}\n\nexport interface AgyManagement {\n /** Dispatch one method; rejects with a legible message on failure. */\n call(method: string, payload: unknown): Promise<unknown>\n /** Consume an OAuth callback (the one endpoint that stays a real HTTP route). */\n handleCallback(params: URLSearchParams): Promise<{ ok: boolean; email?: string | null; error?: string }>\n}\n\n/** Fail one RPC with a message the UI can show verbatim. */\nfunction fail(message: string): never {\n throw new Error(message)\n}\n\nfunction asIndex(payload: unknown): number {\n const index = Number((payload as { index?: unknown })?.index)\n if (!Number.isInteger(index) || index < 0) fail('invalid index')\n return index\n}\n\n/**\n * Flatten one account's ledger entry for transport.\n *\n * `models` is deliberately NOT carried: the per-model table that read it was\n * removed (the cumulative metric strip states the same figures better), and\n * sending fields no client reads is a wire surface with no consumer, which\n * reads as intentional to the next person and invites a stale assumption.\n * `lastUsedAt` DOES travel — the account row's activity fragment reads it.\n */\nfunction toAccountUsageView(\n usage: { totals: UsageCounters; sources: Record<UsageSource, number>; lastUsedAt?: number } | undefined,\n): AccountUsageView | null {\n if (usage === undefined) return null\n return {\n totals: usage.totals,\n sources: usage.sources,\n lastUsedAt: typeof usage.lastUsedAt === 'number' && Number.isFinite(usage.lastUsedAt)\n ? usage.lastUsedAt\n : null,\n }\n}\n\nexport function createAgyManagement(options: AgyManagementOptions): AgyManagement {\n const { store, sessions, stats, recentRequests, modelVisibility, listAllModels, invalidateModelCache, baseUrl, notifyModelsChanged } = options\n const thinkingBudget = options.thinkingBudget\n const multimodal = options.multimodal\n const uiPrefs = options.uiPrefs ?? new UiPrefsStore()\n\n /** Authorizations issued by `auth.url`, keyed by raw state. */\n const pendingAuth = new Map<string, PendingAuth>()\n const PENDING_AUTH_TTL_MS = 10 * 60 * 1000\n\n const prunePendingAuth = (now = Date.now()): void => {\n for (const [key, entry] of pendingAuth) {\n if (entry.expiresAt <= now) pendingAuth.delete(key)\n }\n // Bound the map even under abuse: drop the soonest-to-expire entries.\n while (pendingAuth.size > 100) {\n let oldestKey: string | null = null\n let oldestExpiry = Infinity\n for (const [key, entry] of pendingAuth) {\n if (entry.expiresAt < oldestExpiry) {\n oldestExpiry = entry.expiresAt\n oldestKey = key\n }\n }\n if (oldestKey === null) break\n pendingAuth.delete(oldestKey)\n }\n }\n\n const listAccounts = async (): Promise<AccountView[]> => {\n const storage = await store.load()\n const now = Date.now()\n // Expire stale cooldown state BEFORE rendering. `clearExpiredState` otherwise\n // only runs inside `pickAccount`, i.e. only when a request is actually made,\n // so a management read showed a reason for a window that had already lapsed —\n // the badge said \"active\" while the detail row still said \"network-error\",\n // contradicting itself on one screen. Purely in-memory here: the next real\n // mutation persists it, and a read must not write.\n for (const account of storage.accounts) clearExpiredState(account, now)\n const ledger = stats.snapshot()\n const rows: AccountView[] = []\n for (const [index, account] of storage.accounts.entries()) {\n const state: AccountState = account.enabled === false\n ? 'disabled'\n : account.verificationRequired === true\n ? 'verification-required'\n : account.coolingDownUntil !== undefined && account.coolingDownUntil > now\n ? 'cooling'\n : 'active'\n const key = account.email ?? account.id\n rows.push({\n index,\n email: account.email ?? null,\n projectId: account.projectId ?? null,\n active: index === storage.activeIndex && account.enabled !== false,\n state,\n // Disabled is terminal until a human acts (verify / re-import), so the\n // time is the actionable half: \"minutes ago\" makes an immediate verify\n // worth trying, \"weeks ago\" says re-login. Both disable writers persist\n // `verificationRequiredAt` in the same mutation as `enabled = false`,\n // and the parked-but-enabled path must NOT read as a disable time.\n disabledAt: account.enabled === false && account.verificationRequiredAt !== undefined\n ? new Date(account.verificationRequiredAt).toISOString()\n : null,\n cooldownUntil: account.coolingDownUntil !== undefined && account.coolingDownUntil > now\n ? new Date(account.coolingDownUntil).toISOString()\n : null,\n cooldownReason: account.cooldownReason ?? null,\n // Only while the window is live: `clearExpiredState` above already drops\n // both fields for an expired one, so this cannot outlive its reason.\n cooldownSetAt: account.cooldownSetAt === undefined\n ? null\n : new Date(account.cooldownSetAt).toISOString(),\n /**\n * Appeal link from an upstream verification challenge. Surfaced, never\n * followed automatically: only the account owner can complete it, and the\n * account returns to service on its own once the wall clears.\n */\n verificationUrl: account.verificationUrl ?? null,\n verificationRequired:\n account.verificationRequired === true && account.enabled !== false,\n rateLimits: account.rateLimitResetTimes ?? null,\n fingerprint: account.fingerprint\n ? {\n userAgent: account.fingerprint.userAgent,\n deviceId: account.fingerprint.deviceId,\n createdAt: account.fingerprint.createdAt,\n }\n : null,\n fingerprintHistory: (account.fingerprintHistory ?? []).length,\n proxy: account.proxy ? maskProxyUrl(account.proxy) : null,\n // Read from the cache the session manager already refreshes alongside the\n // per-model quota, so showing the windows costs no request. Null means\n // \"never measured\" and renders as an explicit placeholder rather than a\n // zero, which would read as an exhausted account.\n limits: account.cachedLimits?.groups ?? null,\n limitsUpdatedAt: account.cachedLimits?.updatedAt ?? null,\n // The burn rate arrives via `account.limits` (it exists only after the\n // sampling path has run twice); `account.list` stays probe-free.\n limitBurn: null,\n usage: key === undefined ? null : toAccountUsageView(ledger.accounts[key]),\n })\n }\n // Per-model quota is deliberately NOT fetched here (nor anywhere else): it\n // costs an upstream round trip, and embedding it in this reply once meant the\n // accounts list could not render until that probe finished — on a slow\n // network the panel showed \"no accounts\" behind a permanent spinner,\n // indistinguishable from data loss. The grouped 5h/weekly windows are the\n // surviving quota display, and they arrive via `account.limits`.\n return rows\n }\n\n /** Fold the ledger into the Usage tab's view. */\n const statsView = (): StatsView => {\n const doc = stats.snapshot()\n const now = Date.now()\n\n // Per-model totals across accounts: sum the same model id from every account.\n const byModel = new Map<string, UsageCounters>()\n for (const usage of Object.values(doc.accounts)) {\n for (const [model, counters] of Object.entries(usage.models)) {\n const current = byModel.get(model)\n if (current === undefined) {\n byModel.set(model, { ...counters })\n } else {\n for (const key of Object.keys(current) as Array<keyof UsageCounters>) {\n current[key] += counters[key]\n }\n }\n }\n }\n const allModels = [...byModel.entries()]\n .map(([model, counters]) => ({ model, counters }))\n .sort((a, b) => b.counters.requests - a.counters.requests)\n\n // Each range carries its OWN breakdown, so the tables follow the range\n // selection instead of showing all-time rows beneath range-scoped headline\n // figures (30 requests above a 164-request row, on one screen). The\n // all-time range reads the account maps directly, which hold every request\n // ever recorded rather than only the retained day window.\n const allAccounts = Object.entries(doc.accounts)\n .map(([account, usage]) => ({ account, counters: usage.totals }))\n .sort((a, b) => b.counters.requests - a.counters.requests)\n const breakdown = (days: number): RangeBreakdown => {\n const { totals, models, accounts } = foldWindowBreakdown(doc, days, now)\n return { counters: totals, models, accounts }\n }\n\n // Last 7 LOCAL calendar days, zeros filled: the trend table's job is to\n // show shape over time, and a gap day reads as data loss rather than idle.\n // Same local keying as the ledger's own buckets (dayKey).\n const daySeries = Array.from({ length: 7 }, (_, i) => {\n const key = dayKey(now - (6 - i) * 86_400_000)\n const bucket = doc.days[key]\n return {\n day: key,\n requests: bucket?.totals.requests ?? 0,\n failed: bucket?.totals.failed ?? 0,\n rateLimited: bucket?.totals.rateLimited ?? 0,\n rotations: bucket?.totals.rotations ?? 0,\n }\n })\n return {\n since: doc.totals.requests > 0 ? doc.since : null,\n all: { counters: doc.totals, models: allModels, accounts: allAccounts },\n today: breakdown(1),\n week: breakdown(7),\n month: breakdown(30),\n days: daySeries,\n }\n }\n\n const methods: Record<AgyRpcMethod, (payload: unknown) => Promise<unknown>> = {\n 'account.list': async () => ({ accounts: await listAccounts() }),\n\n 'pool.status': async () => ({ busy: await sessions.inFlightAccounts() }),\n\n 'pool.recent': async () => ({ recent: recentRequests() }),\n\n 'account.activate': async (payload) => {\n const index = asIndex(payload)\n await sessions.activateAccount(index)\n // The activated account may see a different catalog, and the model-list\n // cache is keyed to no account — drop it or the picker serves the\n // previous account's list until the TTL expires.\n invalidateModelCache()\n return { ok: true, index }\n },\n\n 'account.delete': async (payload) => {\n const index = asIndex(payload)\n await store.mutate((storage) => {\n if (index >= storage.accounts.length) fail('account not found')\n storage.accounts.splice(index, 1)\n if (storage.activeIndex >= storage.accounts.length) storage.activeIndex = 0\n })\n return { ok: true }\n },\n\n 'account.verify': async (payload) => sessions.verifyAccount(asIndex(payload)),\n\n 'account.health': async (payload) => {\n const raw = (payload as { indices?: unknown })?.indices\n const indices = Array.isArray(raw) ? raw.map((value) => Number(value)) : undefined\n return { results: await sessions.checkAccounts(indices) }\n },\n\n /**\n * Refresh and return the 5h/weekly windows for every enabled account.\n *\n * A SEPARATE call rather than a field on `account.list`, because that reply\n * is deliberately probe-free (see the note above its return): folding an\n * upstream call into it made the whole accounts+usage view wait on the\n * network and show \"no accounts\" behind a spinner. Here the client renders\n * the list from `account.list` immediately and merges these in when they\n * arrive, so a slow probe costs a placeholder, not the page.\n *\n * Server-side this is TTL-gated, so the upstream call happens at most once\n * per window rather than once per view. `force` bypasses that gate for an\n * EXPLICIT refresh — without it the toolbar's Refresh button could not\n * deliver \"the latest numbers now\", which is the only reason to click it\n * while the snapshot is still inside its TTL.\n */\n 'account.limits': async (payload) => {\n const force = (payload as { force?: unknown } | undefined)?.force === true\n const storage = await store.load()\n // Best-effort: a failed refresh leaves whatever was cached, so this reply\n // is always the current best knowledge rather than an error. The OUTCOME\n // is still reported, because \"probed and failed\" and \"was still fresh\"\n // both leave the numbers identical and the user needs to know which.\n const result = await sessions\n .refreshLimits(storage, { force })\n .catch(() => ({ measured: [], failed: [], skipped: 0 }))\n const fresh = await store.load()\n // Burn rates ride the same reply (the sampling path runs inside\n // refreshLimits); a join failure degrades to null rates, not an error.\n const burn = await sessions.limitBurnRates().catch(() => [])\n const burnByIndex = new Map(burn.map((entry) => [entry.index, entry.perHour]))\n return {\n limits: fresh.accounts.map((account, index) => ({\n index,\n groups: account.cachedLimits?.groups ?? null,\n updatedAt: account.cachedLimits?.updatedAt ?? null,\n burn: burnByIndex.get(index) ?? null,\n })),\n measured: result.measured.length,\n failed: result.failed.length,\n skipped: result.skipped,\n }\n },\n\n 'account.test': async (payload) => {\n const model = (payload as { model?: unknown })?.model\n if (typeof model !== 'string' || model === '') fail('model is required')\n // The account row the user clicked. Optional so a caller that only wants\n // \"test whatever the pool picks\" still works, but a row click always\n // supplies it — otherwise the probe silently ran on the affinity account\n // and reported its result as this account's.\n const rawIndex = (payload as { index?: unknown })?.index\n return sessions.testCall(model, {\n ...(rawIndex === undefined ? {} : { accountIndex: asIndex(payload) }),\n })\n },\n\n 'account.export': async (payload) => sessions.exportBlob(asIndex(payload)),\n\n 'account.exportAll': async () => {\n const storage = await store.load()\n const blobs: Array<{ index: number; blob: string }> = []\n for (let index = 0; index < storage.accounts.length; index++) {\n const result = await sessions.exportBlob(index)\n if (result.blob !== undefined) blobs.push({ index, blob: result.blob })\n }\n return { blobs }\n },\n\n 'account.import': async (payload) => {\n const body = payload as { kind?: unknown; sources?: unknown } | undefined\n const kind = body?.kind === 'blob' ? 'blob' : 'json'\n const sources = Array.isArray(body?.sources) ? body.sources : []\n if (sources.length === 0) fail('nothing to import')\n const result = await importManySources(\n sources.map((source) => ({ source, kind })),\n store,\n { overwriteExisting: true },\n )\n // Per-source failures are part of the result, not a thrown error: a batch\n // where 2 of 5 sources were malformed is a partial success, and the caller\n // must be able to say which ones failed.\n return {\n imported: result.imported,\n replaced: result.replaced,\n errors: result.errors.map((error) => String(error)),\n }\n },\n\n 'account.fingerprint': async (payload) => {\n const index = asIndex(payload)\n const action = (payload as { action?: unknown })?.action === 'regenerate' ? 'regenerate' : 'show'\n return store.mutate(async (storage) => {\n const account = storage.accounts[index]\n if (!account) fail('account not found')\n if (action === 'regenerate') {\n // Same version source as first-use creation: the published value the\n // resolver maintains, never a per-call network probe whose result the\n // pool-randomizing default would silently discard anyway.\n const fresh = generateFingerprint(undefined, currentAgyVersion())\n account.fingerprint = fresh\n account.fingerprintHistory = recordFingerprintVersion(account.fingerprintHistory, fresh, 'regenerated')\n }\n return {\n action,\n fingerprint: account.fingerprint\n ? {\n userAgent: account.fingerprint.userAgent,\n deviceId: account.fingerprint.deviceId,\n createdAt: account.fingerprint.createdAt,\n }\n : null,\n history: account.fingerprintHistory?.length ?? 0,\n }\n })\n },\n\n 'account.proxy': async (payload) => {\n const index = asIndex(payload)\n const raw = (payload as { proxy?: unknown })?.proxy\n const proxy = typeof raw === 'string' ? raw : ''\n if (proxy.trim() === '') {\n await store.mutate((storage) => {\n const account = storage.accounts[index]\n if (!account) fail('account not found')\n delete account.proxy\n })\n return { ok: true as const, proxy: null, proxyMasked: null, rawLogs: null }\n }\n let normalized: string\n try {\n normalized = normalizeProxyUrl(proxy)\n } catch (error) {\n fail(error instanceof Error ? error.message : String(error))\n }\n await store.mutate((storage) => {\n const account = storage.accounts[index]\n if (!account) fail('account not found')\n account.proxy = normalized\n })\n return {\n ok: true as const,\n proxy: maskProxyUrl(normalized),\n proxyMasked: maskProxyUrl(normalized),\n rawLogs: proxyUrlForLogs(normalized),\n }\n },\n\n 'account.proxyTest': async (payload) => {\n const body = payload as { index?: unknown; proxy?: unknown } | undefined\n const hasExplicit = typeof body?.proxy === 'string' && body.proxy.trim() !== ''\n let target: string\n if (hasExplicit) {\n try {\n target = normalizeProxyUrl(body.proxy as string)\n } catch (error) {\n fail(error instanceof Error ? error.message : String(error))\n }\n } else {\n const index = asIndex(payload)\n const storage = await store.load()\n const account = storage.accounts[index]\n if (!account) fail('account not found')\n if (!account.proxy) fail('no proxy configured for this account')\n target = account.proxy\n }\n // Masked for display only; the raw URL never leaves this function.\n const masked = maskProxyUrl(target) ?? proxyUrlForLogs(target)\n try {\n const ok = await isProxyReachable(target, 2000)\n return ok ? { ok, masked } : { ok, masked, error: `proxy unreachable: ${masked}` }\n } catch (error) {\n return { ok: false, masked, error: error instanceof Error ? error.message : String(error) }\n }\n },\n\n 'auth.url': async () => {\n const authorization = await authorizeAntigravity(`${baseUrl()}/agy/oauth-callback`)\n prunePendingAuth()\n pendingAuth.set(authorization.state, {\n verifier: authorization.verifier,\n expiresAt: Date.now() + PENDING_AUTH_TTL_MS,\n })\n return { url: authorization.url }\n },\n\n 'model.list': async () => {\n const session = await sessions.getSession().catch(() => undefined)\n if (session === undefined) fail('No agy account configured — run `dsh-agy login` first.')\n // The adapter's unfiltered catalog: reading the filtered `listModels()`\n // would hide a disabled model from this very list, leaving no switch to\n // turn it back on.\n const models = await listAllModels()\n const hidden = modelVisibility.disabledFor(AGY_PROVIDER)\n const rows: ModelView[] = models.map((model) => ({\n id: model.id,\n name: model.name,\n disabled: hidden.has(model.id),\n }))\n return { account: session.account.email ?? null, models: rows }\n },\n\n 'model.setDisabled': async (payload) => {\n const body = payload as { modelId?: unknown; disabled?: unknown } | undefined\n const modelId = body?.modelId\n if (typeof modelId !== 'string' || modelId === '') fail('modelId is required')\n const disabled = body?.disabled === true\n modelVisibility.setDisabled(AGY_PROVIDER, modelId, disabled)\n // Push the change to every open client so the picker updates live.\n notifyModelsChanged()\n return { modelId, disabled }\n },\n\n 'thinking.get': async () => ({\n budgets: thinkingBudget.all(),\n min: THINKING_BUDGET_MIN,\n max: THINKING_BUDGET_MAX,\n tieredBudget: thinkingBudget.tiered() ?? null,\n claudeBudget: thinkingBudget.claude() ?? null,\n claudeMin: CLAUDE_BUDGET_MIN,\n claudeMax: CLAUDE_BUDGET_MAX,\n }),\n\n 'thinking.setTiered': async (payload) => {\n const body = payload as { budget?: unknown } | undefined\n const raw = body?.budget\n if (raw !== undefined && raw !== null && typeof raw !== 'number') fail('budget must be a number')\n try {\n const value = thinkingBudget.setTiered(raw === undefined || raw === null ? undefined : raw)\n return { tieredBudget: value ?? null }\n } catch (error) {\n fail(error instanceof Error ? error.message : String(error))\n }\n },\n\n 'thinking.setClaude': async (payload) => {\n const body = payload as { budget?: unknown } | undefined\n const raw = body?.budget\n if (raw !== undefined && raw !== null && typeof raw !== 'number') fail('budget must be a number')\n try {\n const value = thinkingBudget.setClaude(raw === undefined || raw === null ? undefined : raw)\n return { claudeBudget: value ?? null }\n } catch (error) {\n fail(error instanceof Error ? error.message : String(error))\n }\n },\n\n 'thinking.set': async (payload) => {\n const body = payload as { level?: unknown; budget?: unknown } | undefined\n const level = body?.level\n if (typeof level !== 'string' || level === '') fail('level is required')\n // `undefined` and explicit null BOTH clear the entry: the field means \"let\n // upstream choose\", and a JSON null is how a cleared input arrives.\n const raw = body?.budget\n if (raw !== undefined && raw !== null && typeof raw !== 'number') fail('budget must be a number')\n const value = raw === undefined || raw === null ? undefined : raw\n try {\n const budgets = thinkingBudget.set(level, value)\n return { budgets }\n } catch (error) {\n // Surface the range/name violation as a caller-correctable message\n // rather than an unlabelled RPC failure.\n fail(error instanceof Error ? error.message : String(error))\n }\n },\n\n 'stats.get': async () => statsView(),\n\n 'multimodal.get': async () => multimodal.get(),\n\n 'multimodal.set': async (payload) => {\n const body = payload as { maxInlineMb?: unknown } | undefined\n // `undefined` and explicit null BOTH clear: the field means \"no stored\n // override\", and a JSON null is how a cleared input arrives.\n const raw = body?.maxInlineMb\n if (raw !== undefined && raw !== null && typeof raw !== 'number') fail('maxInlineMb must be a number')\n const value = raw === undefined || raw === null ? undefined : raw\n try {\n return multimodal.set(value)\n } catch (error) {\n // Surface the interval violation verbatim, like `thinking.set*`: the\n // message names the accepted range, so the caller can correct it.\n fail(error instanceof Error ? error.message : String(error))\n }\n },\n\n 'ui.prefs.get': async () => uiPrefs.get(),\n\n 'ui.prefs.set': async (payload) => {\n const body = payload as { conversationBadge?: unknown } | undefined\n if (body?.conversationBadge !== undefined && typeof body.conversationBadge !== 'boolean') {\n fail('conversationBadge must be a boolean')\n }\n return uiPrefs.set({\n conversationBadge: body?.conversationBadge as boolean | undefined,\n })\n },\n }\n\n return {\n async call(method, payload) {\n const handler = methods[method as AgyRpcMethod]\n if (handler === undefined) fail(`unknown method: ${method}`)\n return handler(payload)\n },\n\n async handleCallback(params) {\n const code = params.get('code')\n const state = params.get('state')\n if (!code || !state) return { ok: false, error: 'Missing code or state parameter' }\n const expected = pendingAuth.get(state)\n if (expected === undefined) {\n return { ok: false, error: 'Unknown or expired authorization — please start a new login.' }\n }\n // One-time use: consume the issued state regardless of the exchange result.\n pendingAuth.delete(state)\n const redirectUri = `${baseUrl()}/agy/oauth-callback`\n const result = await exchangeAntigravity(code, state, redirectUri, expected.verifier)\n if (result.type === 'failed') return { ok: false, error: result.error }\n await upsertImportedAccount(store, {\n accessToken: result.access,\n refreshToken: result.refresh.split('|')[0]!,\n tokenType: 'Bearer',\n expiresAt: new Date(result.expires).toISOString(),\n authMethod: 'oauth',\n email: result.email ?? null,\n projectId: result.projectId || null,\n clientId: result.clientId || null,\n }, { overwriteExisting: true })\n return { ok: true, email: result.email ?? null }\n },\n }\n}\n","/**\n * Bilingual UI dictionary for the dsh-agy web dashboard and OAuth callbacks.\n */\n\nexport interface I18nStrings {\n loginSuccessTitle: string\n loginSuccessDesc: string\n loginFailedTitle: string\n windowClosing: string\n}\n\nexport const I18N_DICT: { en: I18nStrings; zh: I18nStrings } = {\n en: {\n loginSuccessTitle: 'Sign-in Successful',\n loginSuccessDesc: 'Your Antigravity account has been authorized and saved.',\n loginFailedTitle: 'Sign-in Failed',\n windowClosing: 'This window will close automatically in a moment...',\n },\n zh: {\n loginSuccessTitle: '授权登录成功',\n loginSuccessDesc: 'Antigravity 账号已成功接入并保存。',\n loginFailedTitle: '授权登录失败',\n windowClosing: '此窗口即将在 2 秒内自动关闭...',\n },\n}\n","/**\n * Browser-facing HTML for the agy OAuth callback.\n *\n * This is the only page agy serves. The account/statistics dashboard that used\n * to live at `/agy` is gone: that surface is now the Settings section rendered\n * by `src/client/` over the `/api/agy` RPC, so there is no longer a second UI to\n * keep in step with it.\n *\n * The callback stays a real page because Google redirects the browser here.\n */\n\nimport { I18N_DICT } from './i18n.ts'\n\n/**\n * Escape text for an HTML text/attribute position.\n *\n * Every interpolation below except the static markup is attacker-influenced or\n * upstream-influenced: `error` is the token endpoint's raw response body, and\n * `email` comes from Google's userinfo. This page is served by the SAME web\n * server, and therefore the same origin, as the DSH GUI — so injected script\n * here would run with the GUI's session and could reach `/api/agy`\n * (`account.exportAll` returns live credential blobs). That chain is why the\n * escaping matters more than the narrow trigger suggests.\n *\n * Text in markup goes through {@link escapeHtml}; text inside the inline\n * `<script>` goes through {@link jsonForInlineScript}. Neither is optional.\n */\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\n/**\n * Serialize a value for interpolation into the inline `<script>`.\n *\n * `JSON.stringify` escapes for a JS string context only, NOT for the HTML script\n * data state: it leaves `<` alone, so an email or error body containing\n * `</script>` terminates the element early and everything after it is parsed as\n * markup. Escaping the three HTML-significant characters as `\\uXXXX` keeps the\n * JSON value identical while making the byte sequence unrepresentable in the\n * source. (The `JSON.stringify` output above is a valid JS string either way,\n * because a `\\u003c` escape and a literal `<` denote the same character.)\n */\nfunction jsonForInlineScript(value: unknown): string {\n return JSON.stringify(value)\n .replace(/</g, '\\\\u003c')\n .replace(/>/g, '\\\\u003e')\n .replace(/&/g, '\\\\u0026')\n}\n\nexport function renderCallbackHtml(options: { ok: boolean; error?: string; email?: string | null; baseUrl: string }): string {\n const { ok, error, email, baseUrl } = options\n const i18nJson = jsonForInlineScript(I18N_DICT)\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Antigravity Sign-in</title>\n <style>\n :root {\n color-scheme: light dark;\n --dsw-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;\n --bg-page: #0f1115;\n --bg-surface: #171a21;\n --border-l2: rgba(255, 255, 255, 0.12);\n --text-primary: #f8fafc;\n --text-secondary: #94a3b8;\n --brand-primary: #5686fe;\n --state-success: #34d399;\n --state-error: #f87171;\n }\n @media (prefers-color-scheme: light) {\n :root {\n --bg-page: #f8fafc;\n --bg-surface: #ffffff;\n --border-l2: rgba(0, 0, 0, 0.12);\n --text-primary: #0f172a;\n --text-secondary: #475569;\n --brand-primary: #4176e6;\n --state-success: #22c55e;\n --state-error: #ef4444;\n }\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: var(--dsw-font-family);\n background-color: var(--bg-page);\n color: var(--text-primary);\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n padding: 16px;\n }\n .card {\n background: var(--bg-surface);\n border: 1px solid var(--border-l2);\n border-radius: 12px;\n padding: 24px;\n max-width: 440px;\n width: 100%;\n text-align: center;\n box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);\n }\n .icon-wrap {\n width: 48px;\n height: 48px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 16px;\n font-size: 24px;\n }\n .icon-success { background: rgba(52, 211, 153, 0.15); color: var(--state-success); }\n .icon-error { background: rgba(248, 113, 113, 0.15); color: var(--state-error); }\n h1 { font-size: 18px; font-weight: 600; margin-bottom: 8px; }\n p { font-size: 13.5px; color: var(--text-secondary); margin-bottom: 16px; line-height: 1.5; }\n .btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-family: inherit;\n font-size: 13px;\n font-weight: 500;\n height: 34px;\n padding: 0 16px;\n border-radius: 17px;\n border: 1px solid var(--border-l2);\n background: transparent;\n color: var(--text-primary);\n cursor: pointer;\n text-decoration: none;\n }\n .btn:hover { background: rgba(128, 128, 128, 0.1); }\n .btn-primary { background: var(--brand-primary); color: #ffffff; border-color: transparent; }\n </style>\n</head>\n<body>\n <div class=\"card\">\n ${ok ? `\n <div class=\"icon-wrap icon-success\">✓</div>\n <h1 id=\"title\">Sign-in Successful</h1>\n <p id=\"desc\">Your Antigravity account ${email ? '<strong>' + escapeHtml(email) + '</strong> ' : ''}has been authorized and saved.</p>\n <p style=\"font-size:12px;opacity:0.8;\" id=\"closing\">This window will close automatically...</p>\n <button class=\"btn\" onclick=\"window.close()\">Close Window</button>\n ` : `\n <div class=\"icon-wrap icon-error\">✕</div>\n <h1 id=\"title\">Sign-in Failed</h1>\n <p id=\"desc\">Error details: ${escapeHtml(error || 'Unknown error')}</p>\n <a class=\"btn btn-primary\" href=\"${escapeHtml(baseUrl)}/\">Return to Settings</a>\n `}\n </div>\n <script>\n const I18N = ${i18nJson};\n const lang = localStorage.getItem('agy_lang') || (navigator.language.startsWith('zh') ? 'zh' : 'en');\n const dict = I18N[lang] || I18N.en;\n\n if (${ok ? 'true' : 'false'}) {\n document.getElementById('title').textContent = dict.loginSuccessTitle;\n // textContent, not innerHTML: an email containing markup must never be\n // parsed as HTML. The string itself is serialized for the script data\n // state by jsonForInlineScript, so it cannot terminate this element.\n document.getElementById('desc').textContent = dict.loginSuccessDesc + (${jsonForInlineScript(email ? ' (' + email + ')' : '')});\n document.getElementById('closing').textContent = dict.windowClosing;\n \n // Notify parent window & close\n try {\n if (window.opener) {\n // Target this page's own origin rather than '*', so the message cannot\n // be delivered to an unrelated opener.\n window.opener.postMessage({ type: 'agy_login_success' }, window.location.origin);\n }\n } catch (e) {}\n setTimeout(() => {\n window.close();\n }, 1800);\n } else {\n document.getElementById('title').textContent = dict.loginFailedTitle;\n }\n </script>\n</body>\n</html>`\n}\n","/**\n * dsh-agy web entry.\n *\n * Two things are registered, and they deliberately use different transports:\n *\n * 1. **Management RPC at `/api/agy`** (`connection.fetch.register`). The inline\n * Settings UI calls methods over DSH's own RPC channel. This replaces the\n * previous bare `ctx.webServer.register` route table, which had no\n * authentication of its own: `/api/*` sits behind the host's browser-trust\n * fence and BrowserAuth, so the management surface is no longer reachable by\n * anything that can merely open a socket to a loopback port.\n *\n * 2. **The OAuth callback at `/agy/oauth-callback`** stays a real HTTP route.\n * Google redirects a browser to it with a GET, which the management RPC\n * channel (POST-only) cannot carry.\n *\n * BOTH `webServer` and `connection` are deliberately reached lazily rather than\n * statically injected. Each exists only in a Web composition, and a statically\n * injected service that never appears leaves this entry permanently pending —\n * and the loader treats a pending entry as a FAILED PROFILE, not a skipped one:\n *\n * dsh: plugin tree failed to load: dsh: 1 entry did not activate\n * dsh-agy/web: pending (waiting for service: webServer)\n *\n * Measured on a real TUI profile, where no provider of `webServer` is mounted.\n * `ctx.inject([...])` keeps this entry active and inert instead, so a TUI or\n * headless profile boots with no management surface rather than not booting.\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\n// Type-only: merges the `llm/adapters-updated` event into the Cordis Events map.\nimport type {} from '@deepseek-ai/dsh-llm/types'\nimport { createAgyRuntime } from '../plugin-common.ts'\nimport { isAgyDisabled } from '../runtime/risk.ts'\nimport { createAgyManagement } from './management.ts'\nimport { renderCallbackHtml } from './page.ts'\n\nexport const name = 'dsh-agy-web'\n\n/** The one service every composition provides; the rest are resolved lazily. */\nexport const inject = ['llm']\n\n/** Bind hosts that mean \"this machine only\". */\nconst LOOPBACK_HOSTS = ['127.0.0.1', 'localhost', '::1', '[::1]']\n\n/** Listen-any addresses. */\nconst WILDCARD_HOSTS = ['0.0.0.0', '::']\n\n/**\n * Whether a socket peer is on the loopback interface.\n *\n * The OAuth callback is gated on the PEER rather than on the bind address: a\n * LAN-bound server must still answer the redirect that completes a login\n * started in the local GUI, and `0.0.0.0` listeners see loopback peers as\n * `127.0.0.1` (or `::ffff:127.0.0.1` when the socket is dual-stack).\n * @param address - `req.socket.remoteAddress`, undefined when unavailable.\n * @returns true only for a loopback peer; an unknown address is refused.\n */\nexport function isLoopbackPeer(address: string | undefined): boolean {\n if (address === undefined) return false\n return address === '::1'\n || address.startsWith('127.')\n || address.startsWith('::ffff:127.')\n}\n\n/**\n * Format a host for an HTTP URL, mapping wildcard listeners to loopback.\n *\n * An OAuth redirect must name a reachable loopback address. Wildcard listen\n * addresses (`0.0.0.0`, or IPv6 `::` in either bracketed or bare form) map to\n * loopback (`127.0.0.1` and `[::1]`). Bare IPv6 addresses (e.g. `::1`) are\n * wrapped in brackets so `new URL` accepts them.\n * @param host - the bind host.\n * @returns the host to build the redirect from.\n */\nexport function redirectHostFor(host: string): string {\n if (host === '0.0.0.0') return '127.0.0.1'\n if (host === '::' || host === '[::]' || host === '::1') return '[::1]'\n if (host.includes(':') && !host.startsWith('[')) return `[${host}]`\n return host\n}\n\n/** The slice of the host's web-server service this entry uses. */\ninterface WebServerLike {\n register(route: {\n kind: 'exact'\n path: string\n handler: (req: IncomingMessage, res: ServerResponse) => void\n }): () => void\n host?: string\n /** The port actually listened on: the OS-assigned one when `--port 0`. */\n readonly port?: number\n}\n\n/**\n * The loopback base URL the OAuth redirect and the callback page point at.\n *\n * The port is the one the server BOUND, not the one it was asked for.\n * `webStartup.port` is the `--port` flag verbatim, and `--port 0` — which asks\n * the OS for any free port — would send Google's redirect to\n * `http://127.0.0.1:0/agy/oauth-callback`, which nothing answers.\n * The requested port and DSH's own 3080 default only stand in while the\n * server has not reported one.\n * @param host - loopback host the URL names.\n * @param webServer - the host web server (source of the bound port).\n * @param requestedPort - `webStartup.port`, the `--port` flag if one was given.\n * @returns `http://<host>:<port>`, no trailing slash.\n */\nexport function webBaseUrl(host: string, webServer: Pick<WebServerLike, 'port'>, requestedPort?: number): string {\n return `http://${host}:${webServer.port || requestedPort || 3080}`\n}\n\nexport function apply(ctx: Context): void {\n if (isAgyDisabled()) {\n ctx.logger.warn('[dsh-agy] disabled by DSH_AGY_DISABLE=1 — skipping web registration')\n return\n }\n // Lazily, like `connection` below: see the module docblock for why a static\n // `webServer` is a profile-breaking bug rather than a nicety.\n ctx.inject(['webServer'], (webCtx) => {\n const webServer = webCtx.get('webServer') as WebServerLike | undefined\n if (!webServer) return\n webCtx.effect(() => registerAgyWeb(webCtx, webServer))\n })\n}\n\n/**\n * Register the OAuth callback route, and the management RPC once `connection`\n * appears.\n * @param ctx - the web-server-bearing context (owns both registrations).\n * @param webServer - the host web server.\n * @returns disposer for every registration this function made.\n */\nasync function registerAgyWeb(ctx: Context, webServer: WebServerLike): Promise<() => void> {\n // Host comes from the webStartup provider (CLI args), falling back to DSH's\n // own web-app default (loopback), never a user override. The port does not:\n // see `webBaseUrl`.\n const webStartup = ctx.get('webStartup') as { host?: string; port?: number } | undefined\n const host = webStartup?.host ?? '127.0.0.1'\n\n // Two transports, two trust anchors — neither of them the bind address.\n //\n // The management surface rides `/api/agy` (DSH's Connection RPC), which sits\n // behind the host's browser-trust fence and BrowserAuth: anything that can\n // reach it can already drive the Settings UI that exports the same blobs, and\n // anything that cannot is refused there. Gating that on a LOOPBACK BIND\n // instead shipped a Settings section whose every call failed with\n // `transport failure for /api/agy: HTTP 404` on every LAN-bound profile\n // (0.0.0.0 / NetBird), so it now registers on any bind.\n //\n // The OAuth callback is the one route with no authentication of its own, so\n // ITS gate is the peer's address rather than the server's: the route has to\n // exist for the browser redirect that completes a login started in the GUI on\n // this machine, and the handler refuses every non-loopback peer.\n const bindHost = webServer.host ?? host\n if (!LOOPBACK_HOSTS.includes(bindHost)) {\n ctx.logger.warn(\n '[dsh-agy] web server bound to \"' + bindHost + '\" (non-loopback): management RPC registered via /api/*, ' +\n 'but OAuth callback accepts requests from loopback peers only.',\n )\n }\n\n const { store, sessions, adapter, stats, recentStore, modelVisibility, thinkingBudget, multimodalConfig } = await createAgyRuntime(ctx)\n // Read per use rather than once here: the bound port is only known after the\n // server's listen callback has run. The host goes through `redirectHostFor`\n // so wildcard listeners map to loopback and bare IPv6 literals are bracketed.\n const baseUrl = (): string =>\n webBaseUrl(redirectHostFor(bindHost), webServer, webStartup?.port)\n const management = createAgyManagement({\n store,\n sessions,\n stats,\n // The persisted ring, not just this boot's in-memory one.\n recentRequests: () => recentStore.recentRequests(),\n modelVisibility,\n // Expose only the two operations the RPC needs, not the whole store.\n thinkingBudget: {\n all: () => thinkingBudget.all(),\n set: (level, value) => thinkingBudget.setBudget(level, value),\n claude: () => thinkingBudget.claudeBudget(),\n setClaude: (value) => thinkingBudget.setClaudeBudget(value).claudeBudget,\n tiered: () => thinkingBudget.tieredBudget(),\n setTiered: (value) => thinkingBudget.setTieredBudget(value).tieredBudget,\n },\n // Same shape as `thinkingBudget`: the two operations the RPC needs, not the\n // store. The setter returns the fresh snapshot so the card re-renders from\n // what was stored rather than from its own optimistic guess.\n multimodal: {\n get: () => multimodalConfig.snapshot(),\n set: (value) => multimodalConfig.setMaxInlineMb(value),\n },\n // The adapter's *unfiltered* catalog, so a hidden model still appears in\n // the settings list alongside the switch that un-hides it.\n listAllModels: () => adapter.listAllModels(),\n // Activating an account can switch the account model discovery rides; the\n // cache carries no account key, so the RPC drops it (see\n // `AgyAdapter.invalidateModelCache`).\n invalidateModelCache: () => adapter.invalidateModelCache(),\n baseUrl,\n // DSH refreshes the model picker on `llm/adapters-updated`; the hidden\n // list lives in agy's own file, so nothing else would announce the change\n // and the toggle would appear to do nothing until a page reload.\n // (`llm/adapters-updated` is declared `@mode emit` for exactly this kind\n // of registry notification.)\n notifyModelsChanged: () => { ctx.emit('llm/adapters-updated') },\n })\n const disposers: Array<() => void> = []\n\n // The one endpoint that cannot ride the RPC channel: Google sends the\n // browser here with a GET redirect.\n disposers.push(webServer.register({\n kind: 'exact',\n path: '/agy/oauth-callback',\n handler: async (req: IncomingMessage, res: ServerResponse) => {\n const base = baseUrl()\n // Loopback peers only: this route carries no authentication of its own,\n // so a LAN-bound server must not answer it for the network. The local\n // browser still completes a login (the wildcard listener accepts\n // loopback), which is the only case where the loopback redirect works at\n // all.\n if (!isLoopbackPeer(req.socket.remoteAddress)) {\n ctx.logger.warn(\n `[dsh-agy] rejected OAuth callback request from non-loopback peer ${req.socket.remoteAddress ?? 'unknown'}`,\n )\n res.writeHead(403, { 'content-type': 'text/html; charset=utf-8' })\n res.end(renderCallbackHtml({\n ok: false,\n error: 'Forbidden: OAuth callback only accepts requests from loopback peers.',\n baseUrl: base,\n }))\n return\n }\n let url: URL\n try {\n url = new URL(req.url ?? '/', base)\n } catch {\n res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' })\n res.end(renderCallbackHtml({ ok: false, error: 'Invalid callback URL', baseUrl: base }))\n return\n }\n const result = await management.handleCallback(url.searchParams).catch((error: unknown) => ({\n ok: false as const,\n error: error instanceof Error ? error.message : String(error),\n }))\n res.writeHead(result.ok ? 200 : 400, { 'content-type': 'text/html; charset=utf-8' })\n res.end(result.ok\n ? renderCallbackHtml({ ok: true, email: result.email ?? null, baseUrl: base })\n : renderCallbackHtml({ ok: false, error: result.error ?? 'Unknown error', baseUrl: base }))\n },\n }))\n\n // Lazy injection: see the module docblock for why this is not static.\n ctx.inject(['connection'], (connectionCtx) => {\n const connection = connectionCtx.get('connection') as\n | {\n fetch: {\n register(route: {\n path: string\n methods: string[]\n requestBody: 'buffered' | 'streaming'\n fetch: (request: Request) => Promise<Response>\n }): () => void\n }\n }\n | undefined\n if (!connection || typeof connection.fetch?.register !== 'function') {\n connectionCtx.logger.warn('[dsh-agy] connection.fetch unavailable — management RPC not registered')\n return\n }\n connectionCtx.effect(() => connection.fetch.register({\n path: '/api/agy',\n methods: ['POST'],\n requestBody: 'buffered',\n async fetch(request: Request): Promise<Response> {\n if (request.method !== 'POST') return new Response('method not allowed', { status: 405 })\n const contentType = request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase()\n if (contentType !== 'application/json') {\n return new Response('content type must be application/json', { status: 415 })\n }\n let message: Record<string, unknown>\n try {\n message = await request.json() as Record<string, unknown>\n } catch {\n return new Response('body is not JSON', { status: 400 })\n }\n const rpcId = typeof message.rpcId === 'string' ? message.rpcId : 'invalid-request'\n const call = message.payload as { method?: unknown; payload?: unknown } | undefined\n if (\n message.type !== 'client-request'\n || typeof message.rpcId !== 'string'\n || typeof call?.method !== 'string'\n ) {\n return reply(rpcId, {\n ok: false,\n error: { code: 'agy/bad-request', message: 'Invalid agy management request.' },\n })\n }\n try {\n const value = await management.call(call.method, call.payload)\n return reply(rpcId, { ok: true, value })\n } catch (error) {\n const text = error instanceof Error ? error.message : String(error)\n connectionCtx.logger.warn(`[dsh-agy] ${call.method} failed: ${text}`)\n // A structured failure, not a bare 500: the client's unwrap needs a\n // parseable envelope or a failed call looks like \"nothing happened\".\n return reply(rpcId, { ok: false, error: { code: 'agy/handler-failed', message: text } })\n }\n },\n }), 'dsh-agy: /api/agy management RPC')\n })\n\n return () => {\n for (const dispose of disposers) dispose()\n }\n}\n\n/**\n * Wrap one result in the Connection RPC response envelope.\n *\n * A failure MUST carry `error.details` as an object: the client's envelope\n * parser rejects a failure without it as \"invalid server-response\", which hides\n * the real message behind a transport-sounding error.\n */\nfunction reply(rpcId: string, result: unknown): Response {\n const normalized = isFailure(result)\n ? { ...result, error: { details: {}, ...result.error } }\n : result\n return Response.json({ type: 'server-response', rpcId, result: normalized })\n}\n\n/** Whether one result is an RPC failure envelope. */\nfunction isFailure(value: unknown): value is {\n ok: false\n error: { code?: string, message?: string, details?: unknown }\n} {\n return typeof value === 'object' && value !== null\n && (value as { ok?: unknown }).ok === false\n && typeof (value as { error?: unknown }).error === 'object'\n && (value as { error?: unknown }).error !== null\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agy",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"packageManager": "pnpm@11.28.3",
|
|
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",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"parse-38V1zT1B.mjs","names":[],"sources":["../src/adapter/parse.ts"],"sourcesContent":["/**\n * Parse Antigravity SSE responses (Gemini-style `candidates[]` events) into the\n * DSH StreamChunk protocol: block-start / text-delta / reasoning-delta /\n * tool-call-delta / block-end / usage / finish.\n *\n * Wire shape: each `data:` payload is a JSON array of candidate objects with\n * `content.parts[]` (text / {thought:true} / functionCall), `usageMetadata`,\n * and `finishReason`. MEASURED (2026-10-03, daily endpoint, 3/3): a normal\n * stream terminates via the candidate `finishReason` alone — the endpoint\n * sends NO `data: [DONE]`; the SSE `[DONE]` convention is accepted but never\n * observed on the wire.\n *\n * Robustness contract: a cut-short stream must FAIL, never masquerade as a\n * completed turn. Three cases throw:\n * - an error body in-band (`data:` prefixed) or BARE (the upstream emits its\n * `{\"error\":{...}}` vocabulary without the prefix right before dropping the\n * connection), including in the final unterminated line;\n * - a clean EOF carrying neither `data: [DONE]` nor any `finishReason`\n * (either signal alone completes the stream);\n * - an explicit `finishReason` this parser does not map\n * ({@link UnmappedFinishReasonError}).\n */\n\nimport type { FinishReason, StreamChunk } from '@deepseek-ai/dsh-llm'\nimport * as DshLlm from '@deepseek-ai/dsh-llm'\n\n// ponytail: DSH 0.0.1-rc.1 exports CallId, 0.1.x renames to ToolCallId — pick whichever exists at runtime\nconst CallId = ((DshLlm as unknown as { ToolCallId?: (id: string) => unknown }).ToolCallId\n ?? (DshLlm as unknown as { CallId?: (id: string) => unknown }).CallId) as (id: string) => never\nif (!CallId) throw new Error('dsh-llm: neither ToolCallId nor CallId found')\n\nexport interface SsePart {\n text?: string\n thought?: boolean\n thoughtSignature?: string\n functionCall?: { id?: string; name?: string; args?: unknown }\n}\n\nexport interface SseCandidate {\n index?: number\n content?: {\n role?: string\n parts?: SsePart[]\n }\n finishReason?: string\n}\n\n/** The upstream error-vocabulary shape, in-band or bare. */\nexport interface SseError {\n code?: number\n status?: string\n message?: string\n}\n\nexport interface SsePayload {\n candidates?: SseCandidate[]\n usageMetadata?: {\n promptTokenCount?: number\n candidatesTokenCount?: number\n totalTokenCount?: number\n cachedContentTokenCount?: number\n }\n error?: SseError\n}\n\n/**\n * Parse one SSE `data:` line; returns null for `[DONE]` or empty lines.\n * Accepts the `{\"response\": {...}}` envelope (daily endpoint wire shape) and\n * the bare array/object shapes older clients emitted.\n */\nexport function parseSseDataLine(line: string): SsePayload | null {\n const trimmed = line.trim()\n if (!trimmed.startsWith('data:')) return null\n const data = trimmed.slice(5).trim()\n if (data === '' || data === '[DONE]') return null\n const parsed = JSON.parse(data) as unknown\n const root = (parsed as { response?: unknown })?.response ?? parsed\n const payload = (Array.isArray(root) ? root[0] : root) as SsePayload | undefined\n return payload ?? null\n}\n\n/**\n * Extract an error body from a line that does not carry the SSE `data:`\n * prefix. The upstream occasionally drops its `{\"error\":{...}}` vocabulary as\n * a bare JSON line right before terminating the stream — the socket closes\n * cleanly and nothing else surfaces the failure. Returns null for anything\n * that is not a JSON object carrying an `error` object; SSE comments and\n * event/id/retry fields stay skipped.\n */\nfunction extractBareJsonError(line: string): SseError | null {\n const trimmed = line.trim()\n if (!trimmed.startsWith('{')) return null\n try {\n const parsed = JSON.parse(trimmed) as { error?: SseError }\n if (parsed.error && typeof parsed.error === 'object') return parsed.error\n } catch {\n // not JSON (or truncated) — nothing to extract\n }\n return null\n}\n\n/**\n * Thrown when the upstream ends a stream with a `finishReason` this parser\n * does not map (SAFETY, RECITATION, MALFORMED_FUNCTION_CALL, ...). A silent\n * default to `stop` presented policy-blocked and malformed-call turns as\n * completed. This is a CONTENT-level verdict, not account health: the adapter\n * reports it as `request-error` (no cooldown, no rotation), never\n * `network-error`.\n */\nexport class UnmappedFinishReasonError extends Error {\n constructor(readonly reason: string) {\n super(`agy upstream ended the stream with unrecognized finishReason: ${reason}`)\n this.name = 'UnmappedFinishReasonError'\n }\n}\n\n/**\n * Map the upstream `finishReason` vocabulary onto DSH's. WHITELIST, not\n * blacklist: the completable reasons map to their kinds and every other\n * explicit reason throws. `FINISH_REASON_UNSPECIFIED` is granted `stop` — it\n * carries no information, exactly like the absent field.\n */\nfunction mapFinishReason(reason: string): FinishReason {\n switch (reason) {\n case 'MAX_TOKENS':\n return { kind: 'max-tokens' }\n case 'STOP':\n case 'FINISH_REASON_UNSPECIFIED':\n return { kind: 'stop' }\n case 'TOOL_CALLS':\n case 'FUNCTION_CALL':\n return { kind: 'tool-calls' }\n default:\n throw new UnmappedFinishReasonError(reason)\n }\n}\n\n/**\n * Consume an SSE text stream and yield StreamChunks. One accumulating block is\n * kept open at a time; tool-call argument deltas accumulate until a different\n * part kind, usage metadata, or stream end closes it.\n */\nexport interface ParseAgySseOptions {\n signal?: AbortSignal\n /**\n * Invoked when a functionCall part carries a sibling thoughtSignature, and\n * when a thought part carries one, keyed by the functionCall id (or the\n * thought signature is ignored unless paired). The adapter persists these\n * for replay on the next turn (see signature-cache.ts).\n */\n onToolSignature?(toolCallId: string, signature: string): void\n}\n\nexport async function* parseAgySse(\n body: ReadableStream<Uint8Array>,\n options: ParseAgySseOptions = {},\n): AsyncGenerator<StreamChunk> {\n const { signal } = options\n const reader = body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n let blockIndex = 0\n let finishReason: FinishReason = { kind: 'stop' }\n let sawDone = false\n let sawFinishReason = false\n let sawUsage = false\n let lastUsage: { inputTokens: number; outputTokens: number; cacheReadTokens?: number } | null = null\n\n interface OpenBlock {\n kind: 'text' | 'reasoning' | 'tool-call'\n id?: string\n name?: string\n arguments: string\n text: string\n }\n let open: OpenBlock | null = null\n\n const closeBlock = (): StreamChunk | null => {\n if (!open) return null\n const block: StreamChunk = open.kind === 'tool-call'\n ? {\n type: 'block-end',\n index: blockIndex,\n block: {\n type: 'tool-call',\n id: CallId(open.id ?? `call-${blockIndex}`),\n name: open.name ?? '',\n arguments: open.arguments,\n },\n }\n : {\n type: 'block-end',\n index: blockIndex,\n block: { type: open.kind, text: open.text },\n }\n open = null\n blockIndex += 1\n return block\n }\n\n /**\n * Ensure a block of the given kind is open, switching when needed.\n * Returns chunks to yield (a closed block's end, then the new block's start).\n * Callers MUST yield everything returned — dropping the end silently corrupts\n * the block stream for DSH (verified: multi-tool turns and text→tool\n * transitions lost their block-end).\n */\n const ensureBlock = (kind: OpenBlock['kind'], meta: { id?: string; name?: string } = {}): StreamChunk[] => {\n const out: StreamChunk[] = []\n if (open && open.kind !== kind) {\n const end = closeBlock()\n if (end) out.push(end)\n }\n if (!open) {\n open = { kind, arguments: '', text: '', id: meta.id, name: meta.name }\n const blockType = kind === 'tool-call' ? 'tool-call' : kind\n out.push({ type: 'block-start', index: blockIndex, blockType })\n }\n return out\n }\n\n /**\n * Handle one wire line and return the chunks to yield. Shared by the read\n * loop and the EOF residual so a final line without a trailing newline is\n * processed exactly like a newline-terminated one — dropping it would lose\n * content AND, since the completeness guard landed, falsify a premature\n * termination on streams that end bare.\n */\n const handleLine = (line: string): StreamChunk[] => {\n const out: StreamChunk[] = []\n const trimmed = line.trim()\n if (!trimmed.startsWith('data:')) {\n const bare = extractBareJsonError(trimmed)\n if (bare) {\n const message = bare.message ?? bare.status ?? 'upstream error'\n throw new Error(`agy stream error (${bare.code ?? 'unknown'}): ${message}`)\n }\n return out\n }\n const data = trimmed.slice(5).trim()\n if (data === '[DONE]') {\n sawDone = true\n return out\n }\n const payload = parseSseDataLine(trimmed)\n if (!payload) return out\n if (payload.error) {\n const message = payload.error.message ?? payload.error.status ?? 'upstream error'\n throw new Error(`agy stream error (${payload.error.code ?? 'unknown'}): ${message}`)\n }\n for (const candidate of payload.candidates ?? []) {\n if (candidate.finishReason) {\n sawFinishReason = true\n finishReason = mapFinishReason(candidate.finishReason)\n }\n for (const part of candidate.content?.parts ?? []) {\n // A zero-length text part is a routine wire shape (the daily endpoint\n // ends turns with `{thoughtSignature, text: \"\"}`) — fabricating an\n // empty block from it poisoned session history for stricter\n // downstream serializers (issue #77). Skip it without touching an\n // open block, so \"Hel\" + \"\" + \"lo\" stays one block; `!part.functionCall`\n // keeps a text+functionCall part on its current branch. Signatures\n // ride functionCall parts only, so the skip loses nothing.\n if (part.text === '' && !part.functionCall) continue\n if (part.text !== undefined && part.thought !== true) {\n out.push(...ensureBlock('text'))\n open!.text += part.text\n out.push({ type: 'text-delta', index: blockIndex, text: part.text })\n } else if (part.text !== undefined && part.thought === true) {\n out.push(...ensureBlock('reasoning'))\n open!.text += part.text\n out.push({ type: 'reasoning-delta', index: blockIndex, text: part.text })\n } else if (part.functionCall) {\n // Use the upstream functionCall id when present so the signature\n // captured on this part can be replayed for the same id next turn.\n const upstreamId = part.functionCall.id || String(blockIndex)\n // Each functionCall part is an ATOMIC block: a stream can carry\n // several functionCall parts in one turn (multi-tool responses),\n // and they share kind \"tool-call\" — ensureBlock alone would not\n // switch between them, concatenating their args JSON into one\n // invalid string. Close any open block (yielding its end) first.\n if (open) {\n const end = closeBlock()\n if (end) out.push(end)\n }\n const start = ensureBlock('tool-call', {\n id: upstreamId,\n name: part.functionCall.name,\n })\n out.push(...start)\n if (part.thoughtSignature) {\n options.onToolSignature?.(upstreamId, part.thoughtSignature)\n }\n const argsJson = typeof part.functionCall.args === 'string'\n ? part.functionCall.args\n : JSON.stringify(part.functionCall.args ?? {})\n open!.arguments += argsJson\n out.push({\n type: 'tool-call-delta',\n index: blockIndex,\n id: CallId(open!.id ?? ''),\n name: open!.name,\n argumentsDelta: argsJson,\n })\n }\n }\n }\n if (payload.usageMetadata) {\n // The upstream sends usageMetadata on EVERY SSE event (cumulative).\n // Do NOT close the block here: closing per event would split one\n // continuous text stream into a block per chunk (frontend renders\n // block boundaries as line breaks). Stash the last (full) totals\n // and emit one usage chunk at stream end.\n sawUsage = true\n // DSH TokenUsage buckets are DISJOINT: inputTokens must be the\n // uncached portion only; cache reads are reported separately.\n // Reporting the full promptTokenCount here double-counts cached\n // tokens in the stats line's cache-hit percentage (it divides by\n // uncached + cacheRead + cacheWrite).\n const promptTokens = payload.usageMetadata.promptTokenCount ?? 0\n const cachedTokens = payload.usageMetadata.cachedContentTokenCount ?? 0\n lastUsage = {\n inputTokens: Math.max(0, promptTokens - cachedTokens),\n outputTokens: payload.usageMetadata.candidatesTokenCount ?? 0,\n cacheReadTokens: cachedTokens,\n }\n }\n return out\n }\n\n try {\n while (true) {\n if (signal?.aborted) {\n throw new DOMException('aborted', 'AbortError')\n }\n const { done, value } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n let newlineIndex: number\n while ((newlineIndex = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, newlineIndex)\n buffer = buffer.slice(newlineIndex + 1)\n for (const chunk of handleLine(line)) yield chunk\n }\n }\n // Flush the decoder and process a final line that arrived without its\n // trailing newline: content, usage, finishReason, `[DONE]`, and bare\n // error bodies all ride it.\n buffer += decoder.decode()\n if (buffer.trim() !== '') {\n const line = buffer\n buffer = ''\n for (const chunk of handleLine(line)) yield chunk\n }\n // Completeness guard: a CLEAN close carrying neither `[DONE]` nor any\n // `finishReason` is a cut-short stream, not a completed turn — yielding\n // the default `stop` here presented truncated replies as successful\n // (issue #85). Either signal alone completes the stream.\n if (!sawDone && !sawFinishReason) {\n throw new Error('agy stream terminated prematurely without [DONE] or finishReason')\n }\n const closed = closeBlock()\n if (closed) yield closed\n if (lastUsage) {\n yield { type: 'usage', usage: lastUsage }\n } else if (sawUsage) {\n yield { type: 'usage', usage: { inputTokens: 0, outputTokens: 0 } }\n }\n yield { type: 'finish', reason: finishReason }\n } finally {\n reader.releaseLock()\n }\n}\n"],"mappings":"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"}
|