dsh-agy 0.1.0 → 0.1.2

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.
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { g as generateAntigravityRequestId } from "./session-tawi2_uh.mjs";
2
+ import { g as generateAntigravityRequestId } from "./session-DFFF5_Yg.mjs";
3
3
  const DEFAULT_TTL_MS = 36e5;
4
4
  const MAX_ENTRIES = 2e3;
5
5
  const store = /* @__PURE__ */ new Map();
@@ -55,12 +55,53 @@ function stripTrailingModelTurn(contents) {
55
55
  while (contents.length > 1 && contents[contents.length - 1]?.role === "model") contents.pop();
56
56
  return contents;
57
57
  }
58
- /** Recursively remove `enumDescriptions` from tool schemas (upstream rejects with 400). */
59
- function stripEnumDescriptions(schema) {
58
+ /**
59
+ * The Antigravity backend parses tool `parameters` as a strict protobuf
60
+ * schema and rejects ANY unknown keyword with 400 (verified empirically:
61
+ * `$schema`, `propertyNames`, `pattern`, `minLength`, ... each fail in turn).
62
+ * Denylisting is whack-a-mole, so keep only the keywords the upstream
63
+ * accepts. Container shapes are handled distinctly: `properties` is a
64
+ * name->schema map (keys preserved), `items`/`additionalProperties` are
65
+ * nested schemas, `required`/`enum` are plain arrays.
66
+ */
67
+ const AGY_SCHEMA_ALLOWLIST = /* @__PURE__ */ new Set([
68
+ "type",
69
+ "format",
70
+ "title",
71
+ "description",
72
+ "nullable",
73
+ "items",
74
+ "enum",
75
+ "default",
76
+ "properties",
77
+ "required",
78
+ "additionalProperties"
79
+ ]);
80
+ const AGY_SCHEMA_MAP_KEYS = /* @__PURE__ */ new Set(["properties"]);
81
+ const AGY_SCHEMA_NESTED_KEYS = /* @__PURE__ */ new Set(["items", "additionalProperties"]);
82
+ const AGY_SCHEMA_LIST_KEYS = /* @__PURE__ */ new Set(["required", "enum"]);
83
+ function sanitizeToolSchema(schema) {
60
84
  if (!schema || typeof schema !== "object") return schema;
61
- if (Array.isArray(schema)) return schema.map((entry) => stripEnumDescriptions(entry));
85
+ if (Array.isArray(schema)) return schema.map((entry) => sanitizeToolSchema(entry));
62
86
  const result = {};
63
- for (const [key, value] of Object.entries(schema)) if (key !== "enumDescriptions") result[key] = stripEnumDescriptions(value);
87
+ for (const [key, value] of Object.entries(schema)) {
88
+ if (!AGY_SCHEMA_ALLOWLIST.has(key)) continue;
89
+ if (AGY_SCHEMA_MAP_KEYS.has(key)) {
90
+ const map = {};
91
+ for (const [name, child] of Object.entries(value)) map[name] = sanitizeToolSchema(child);
92
+ result[key] = map;
93
+ continue;
94
+ }
95
+ if (AGY_SCHEMA_NESTED_KEYS.has(key)) {
96
+ result[key] = sanitizeToolSchema(value);
97
+ continue;
98
+ }
99
+ if (AGY_SCHEMA_LIST_KEYS.has(key)) {
100
+ result[key] = value;
101
+ continue;
102
+ }
103
+ result[key] = value;
104
+ }
64
105
  return result;
65
106
  }
66
107
  /** Collect tool-call names by id so tool-result blocks can name their function. */
@@ -77,9 +118,11 @@ function blockToParts(block, toolNames) {
77
118
  text: block.text
78
119
  }];
79
120
  case "tool-call": {
80
- let args = block.arguments;
81
- try {
82
- args = JSON.parse(block.arguments);
121
+ let args = {};
122
+ if (typeof block.arguments === "object" && block.arguments !== null && !Array.isArray(block.arguments)) args = block.arguments;
123
+ else if (typeof block.arguments === "string") try {
124
+ const parsed = JSON.parse(block.arguments);
125
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) args = parsed;
83
126
  } catch {}
84
127
  return [{
85
128
  thoughtSignature: getThoughtSignature(block.id) ?? "skip_thought_signature_validator",
@@ -113,7 +156,7 @@ function toolsToDeclarations(tools) {
113
156
  return [{ functionDeclarations: tools.map((tool) => ({
114
157
  name: tool.name,
115
158
  description: tool.description,
116
- parameters: stripEnumDescriptions(tool.parameters)
159
+ parameters: sanitizeToolSchema(tool.parameters)
117
160
  })) }];
118
161
  }
119
162
  /** Build the wrapped Antigravity request body for one call. */
@@ -145,4 +188,4 @@ function toAgyRequestBody(options, context) {
145
188
  //#endregion
146
189
  export { translate_exports as n, setThoughtSignature as r, toAgyRequestBody as t };
147
190
 
148
- //# sourceMappingURL=translate-h2bjH7ub.mjs.map
191
+ //# sourceMappingURL=translate-DgqRjGFH.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translate-DgqRjGFH.mjs","names":[],"sources":["../src/runtime/signature-cache.ts","../src/adapter/translate.ts"],"sourcesContent":["/**\n * Gemini thought-signature cache for Antigravity tool calls.\n *\n * The Antigravity backend requires every outbound `functionCall` part to carry\n * a `thoughtSignature` (400 \"Function call is missing a thought_signature\"\n * otherwise). Signatures arrive on the *response* side — as a sibling field of\n * functionCall parts, and on thought parts — and must be replayed on the next\n * turn for the same tool call id. When nothing is cached, the translator\n * injects the `skip_thought_signature_validator` sentinel (the established\n * bypass both reference implementations use by default).\n *\n * Process-local with TTL; a cold process falls back to the sentinel, which the\n * upstream accepts, so cross-process persistence is not required for\n * correctness.\n */\n\nexport const THOUGHT_SIGNATURE_SENTINEL = 'skip_thought_signature_validator'\n\ninterface CacheEntry {\n signature: string\n expiresAt: number\n}\n\nconst DEFAULT_TTL_MS = 60 * 60 * 1000\nconst MAX_ENTRIES = 2000\n\nconst store = new Map<string, CacheEntry>()\n\nfunction pruneExpired(now = Date.now()): void {\n if (store.size < MAX_ENTRIES / 2) return\n for (const [key, entry] of store) {\n if (entry.expiresAt <= now) store.delete(key)\n }\n}\n\n/** Store the signature observed for one tool call id (response side). */\nexport function setThoughtSignature(toolCallId: string, signature: string, ttlMs = DEFAULT_TTL_MS): void {\n if (!toolCallId || !signature) return\n pruneExpired()\n if (store.size >= MAX_ENTRIES) {\n // Evict the oldest entry to keep the cache bounded.\n let oldestKey: string | null = null\n let oldestExpiry = Infinity\n for (const [key, entry] of store) {\n if (entry.expiresAt < oldestExpiry) {\n oldestExpiry = entry.expiresAt\n oldestKey = key\n }\n }\n if (oldestKey) store.delete(oldestKey)\n }\n store.set(toolCallId, { signature, expiresAt: Date.now() + ttlMs })\n}\n\n/** Resolve the signature for one tool call id, or null when unknown/expired. */\nexport function getThoughtSignature(toolCallId: string): string | null {\n if (!toolCallId) return null\n const entry = store.get(toolCallId)\n if (!entry) return null\n if (entry.expiresAt <= Date.now()) {\n store.delete(toolCallId)\n return null\n }\n return entry.signature\n}\n","/**\n * Translate a DSH GenerateOptions into the Antigravity wrapped request.\n *\n * Envelope shape follows the actively-maintained OmniRoute wire format\n * (the archived opencode reference predates it): top-level `project`,\n * `requestId`, `model`, `userAgent`, `requestType`, with the Gemini-style\n * body under `request` (contents/systemInstruction/tools/generationConfig/\n * sessionId). `toolConfig` VALIDATED is attached when tools are present, and\n * Claude-path requests strip trailing model turns (Vertex rejects \"assistant\n * message prefill\").\n *\n * Thinking blocks are carried as-is (Gemini `thought` parts); nothing is\n * stripped or re-signed — that signature dance was an artifact of the\n * reference plugin's interception architecture (see docs/ARCHITECTURE.md).\n */\n\nimport type { ContentBlock, GenerateOptions, Message, ToolSchema } from '@deepseek-ai/dsh-llm'\nimport { generateAntigravityRequestId } from '../runtime/identity.ts'\nimport { getThoughtSignature, THOUGHT_SIGNATURE_SENTINEL } from '../runtime/signature-cache.ts'\n\nexport type AgyPart =\n | { text: string }\n | { thought: true; text: string }\n | { thoughtSignature: string; functionCall: { id: string; name: string; args: unknown } }\n | { functionResponse: { name: string; response: unknown } }\n\nexport interface AgyContent {\n role: 'user' | 'model'\n parts: AgyPart[]\n}\n\nexport interface AgyRequestBody {\n project?: string\n requestId?: string\n model: string\n userAgent?: string\n requestType?: 'agent'\n request: {\n contents: AgyContent[]\n systemInstruction?: { parts: Array<{ text: string }> }\n tools?: Array<{ functionDeclarations: Array<{ name: string; description: string; parameters: unknown }> }>\n toolConfig?: { functionCallingConfig: { mode: 'VALIDATED' } }\n generationConfig?: {\n temperature?: number\n maxOutputTokens?: number\n stopSequences?: string[]\n }\n sessionId?: string\n }\n}\n\n/** Whether a model id belongs to a Claude-branded model (Vertex-hosted). */\nexport function isClaudeModel(model: string): boolean {\n return model.startsWith('claude-') || model.includes('/claude')\n}\n\n/**\n * Vertex (the Antigravity Claude backend) rejects conversations ending on an\n * assistant/model turn (\"assistant message prefill\"); never strip to empty.\n */\nexport function stripTrailingModelTurn(contents: AgyContent[]): AgyContent[] {\n while (contents.length > 1 && contents[contents.length - 1]?.role === 'model') {\n contents.pop()\n }\n return contents\n}\n\n/**\n * The Antigravity backend parses tool `parameters` as a strict protobuf\n * schema and rejects ANY unknown keyword with 400 (verified empirically:\n * `$schema`, `propertyNames`, `pattern`, `minLength`, ... each fail in turn).\n * Denylisting is whack-a-mole, so keep only the keywords the upstream\n * accepts. Container shapes are handled distinctly: `properties` is a\n * name->schema map (keys preserved), `items`/`additionalProperties` are\n * nested schemas, `required`/`enum` are plain arrays.\n */\nconst AGY_SCHEMA_ALLOWLIST = new Set([\n 'type', 'format', 'title', 'description', 'nullable',\n 'items', 'enum', 'default', 'properties', 'required', 'additionalProperties',\n])\nconst AGY_SCHEMA_MAP_KEYS = new Set(['properties'])\nconst AGY_SCHEMA_NESTED_KEYS = new Set(['items', 'additionalProperties'])\nconst AGY_SCHEMA_LIST_KEYS = new Set(['required', 'enum'])\n\nfunction sanitizeToolSchema(schema: unknown): unknown {\n if (!schema || typeof schema !== 'object') return schema\n if (Array.isArray(schema)) return schema.map((entry) => sanitizeToolSchema(entry))\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(schema as Record<string, unknown>)) {\n if (!AGY_SCHEMA_ALLOWLIST.has(key)) continue\n if (AGY_SCHEMA_MAP_KEYS.has(key)) {\n const map: Record<string, unknown> = {}\n for (const [name, child] of Object.entries(value as Record<string, unknown>)) {\n map[name] = sanitizeToolSchema(child)\n }\n result[key] = map\n continue\n }\n if (AGY_SCHEMA_NESTED_KEYS.has(key)) {\n result[key] = sanitizeToolSchema(value)\n continue\n }\n if (AGY_SCHEMA_LIST_KEYS.has(key)) {\n result[key] = value\n continue\n }\n result[key] = value\n }\n return result\n}\n\n/** Collect tool-call names by id so tool-result blocks can name their function. */\nfunction buildToolNameIndex(messages: readonly Message[]): Map<string, string> {\n const index = new Map<string, string>()\n for (const message of messages) {\n for (const block of message.content) {\n if (block.type === 'tool-call') {\n index.set(block.id, block.name)\n }\n }\n }\n return index\n}\n\nfunction blockToParts(block: ContentBlock, toolNames: Map<string, string>): AgyPart[] {\n switch (block.type) {\n case 'text':\n return [{ text: block.text }]\n case 'reasoning':\n return [{ thought: true, text: block.text }]\n case 'tool-call': {\n // Upstream parses functionCall.args as google.protobuf.Struct and\n // rejects a raw string with 400. Guarantee an object: parse the string\n // form, fall back to {} when it is truncated/malformed (the failure\n // mode for long multi-turn histories).\n let args: unknown = {}\n if (typeof block.arguments === 'object' && block.arguments !== null && !Array.isArray(block.arguments)) {\n args = block.arguments\n } else if (typeof block.arguments === 'string') {\n try {\n const parsed: unknown = JSON.parse(block.arguments)\n if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {\n args = parsed\n }\n } catch {\n // truncated/malformed JSON -> empty object\n }\n }\n // Antigravity rejects functionCall parts without a thoughtSignature\n // (400). Replay the signature captured for this tool call id on the\n // previous turn; the sentinel is the established bypass when nothing is\n // cached (both reference implementations default to it).\n const signature = getThoughtSignature(block.id) ?? THOUGHT_SIGNATURE_SENTINEL\n return [{\n thoughtSignature: signature,\n functionCall: { id: block.id, name: block.name, args },\n }]\n }\n case 'tool-result': {\n const name = toolNames.get(block.toolCallId) ?? block.toolCallId\n const text = block.content\n .filter((b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text')\n .map((b) => b.text)\n .join('\\n')\n return [{\n functionResponse: {\n name,\n response: { result: text, is_error: block.isError === true },\n },\n }]\n }\n default:\n return [] // unknown block types (merge-extensible) are skipped\n }\n}\n\nfunction messageToContent(message: Message, toolNames: Map<string, string>): AgyContent | null {\n const parts = message.content.flatMap((block) => blockToParts(block, toolNames))\n if (parts.length === 0) return null\n const role = message.role === 'assistant' ? 'model' : 'user'\n return { role, parts }\n}\n\nfunction toolsToDeclarations(tools: ToolSchema[] | undefined): AgyRequestBody['request']['tools'] {\n if (!tools || tools.length === 0) return undefined\n return [{\n functionDeclarations: tools.map((tool) => ({\n name: tool.name,\n description: tool.description,\n parameters: sanitizeToolSchema(tool.parameters),\n })),\n }]\n}\n\n/** Build the wrapped Antigravity request body for one call. */\nexport function toAgyRequestBody(\n options: GenerateOptions,\n context: { projectId?: string; sessionId?: string },\n): AgyRequestBody {\n const toolNames = buildToolNameIndex(options.messages)\n let contents = options.messages\n .map((message) => messageToContent(message, toolNames))\n .filter((c): c is AgyContent => c !== null)\n if (isClaudeModel(options.model)) {\n contents = stripTrailingModelTurn(contents)\n }\n\n const tools = toolsToDeclarations(options.tools)\n const generationConfig: NonNullable<AgyRequestBody['request']['generationConfig']> = {}\n if (options.temperature !== undefined) generationConfig.temperature = options.temperature\n if (options.maxTokens !== undefined) generationConfig.maxOutputTokens = options.maxTokens\n if (options.stop !== undefined && options.stop.length > 0) generationConfig.stopSequences = options.stop\n\n return {\n project: context.projectId || undefined,\n requestId: generateAntigravityRequestId(),\n model: options.model,\n userAgent: 'antigravity',\n requestType: 'agent',\n request: {\n contents,\n ...(options.system ? { systemInstruction: { parts: [{ text: options.system }] } } : {}),\n ...(tools ? { tools } : {}),\n ...(tools ? { toolConfig: { functionCallingConfig: { mode: 'VALIDATED' } } } : {}),\n ...(Object.keys(generationConfig).length > 0 ? { generationConfig } : {}),\n ...(context.sessionId ? { sessionId: context.sessionId } : {}),\n },\n }\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
- import { m as resolveAntigravityVersionBounded, n as generateFingerprint, r as recordFingerprintVersion } from "../session-tawi2_uh.mjs";
2
- import { t as createAgyRuntime } from "../plugin-common-I56kmGjr.mjs";
3
- import { t as exchangeAntigravity } from "../exchange-MhFUiBAm.mjs";
4
- import { n as upsertImportedAccount, r as authorizeAntigravity, t as parseImportSource } from "../import-DnKzgr-0.mjs";
1
+ import { m as resolveAntigravityVersionBounded, n as generateFingerprint, r as recordFingerprintVersion } from "../session-DFFF5_Yg.mjs";
2
+ import { t as createAgyRuntime } from "../plugin-common-YSDKp1DG.mjs";
3
+ import { t as exchangeAntigravity } from "../exchange-B1UGNtrv.mjs";
4
+ import { n as upsertImportedAccount, r as authorizeAntigravity, t as importManySources } from "../import-DjCR0KEj.mjs";
5
5
  //#region src/web/i18n.ts
6
6
  const I18N_DICT = {
7
7
  en: {
@@ -38,11 +38,14 @@ const I18N_DICT = {
38
38
  testing: "Testing...",
39
39
  copied: "Copied to clipboard!",
40
40
  importTitle: "Import Credentials",
41
- importDesc: "Paste an agy auth.json token document, or a credential blob (from dsh-agy login --blob):",
42
- importPlaceholder: "{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} or dsh-agy-cred-v1....",
41
+ importDesc: "Paste an agy auth.json token document, or a credential blob (from dsh-agy login --blob). Paste multiple blobs, one per line, for a batch import:",
42
+ importPlaceholder: "{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} or dsh-agy-cred-v1.... (one blob per line for batch)",
43
43
  importJson: "Import JSON",
44
44
  importBlobBtn: "Import Blob",
45
45
  importSuccess: "Successfully imported account: ",
46
+ exportAll: "Export All",
47
+ importBatchResult: "Imported {imported}, replaced {replaced}",
48
+ copyAllMsg: "All credential blobs exported. Copied to clipboard!",
46
49
  fingerprintTitle: "Device Fingerprint",
47
50
  fingerprintDevice: "Device ID",
48
51
  fingerprintUserAgent: "User Agent",
@@ -97,11 +100,14 @@ const I18N_DICT = {
97
100
  testing: "测试中...",
98
101
  copied: "已复制到剪贴板!",
99
102
  importTitle: "导入凭据",
100
- importDesc: "粘贴 agy auth.json 文件内容,或输入凭据 blob(来自 dsh-agy login --blob):",
101
- importPlaceholder: "{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} 或 dsh-agy-cred-v1....",
103
+ importDesc: "粘贴 agy auth.json 文件内容,或输入凭据 blob(来自 dsh-agy login --blob)。多个 blob 每行一个即可批量导入:",
104
+ importPlaceholder: "{\"token\":{\"access_token\":\"...\",\"refresh_token\":\"...\"}} 或 dsh-agy-cred-v1....(批量:每行一个 blob)",
102
105
  importJson: "导入 JSON",
103
106
  importBlobBtn: "导入 Blob",
104
107
  importSuccess: "成功导入账号:",
108
+ exportAll: "导出全部",
109
+ importBatchResult: "导入 {imported} 个,替换 {replaced} 个",
110
+ copyAllMsg: "全部凭据 Blob 已导出并复制到剪贴板!",
105
111
  fingerprintTitle: "设备指纹信息",
106
112
  fingerprintDevice: "设备 ID (DeviceId)",
107
113
  fingerprintUserAgent: "用户代理 (User-Agent)",
@@ -669,9 +675,9 @@ function renderDashboardHtml() {
669
675
  <span id="txt-import-title">Import Credentials</span>
670
676
  </div>
671
677
  <p style="font-size:12.5px;color:var(--text-secondary);margin-bottom:8px;" id="txt-import-desc">
672
- Paste an agy auth.json token document, or a credential blob (from dsh-agy login --blob):
678
+ Paste an agy auth.json token document, or a credential blob (from dsh-agy login --blob). Paste multiple blobs, one per line, for a batch import:
673
679
  </p>
674
- <textarea id="input-import" class="import-textarea" placeholder='{"token":{"access_token":"...","refresh_token":"..."}} or dsh-agy-cred-v1....'></textarea>
680
+ <textarea id="input-import" class="import-textarea" placeholder='{"token":{"access_token":"...","refresh_token":"..."}} or dsh-agy-cred-v1.... (one blob per line for batch)'></textarea>
675
681
  <div style="display:flex;gap:8px;">
676
682
  <button id="btn-import-json" class="btn btn-secondary">
677
683
  <span id="txt-import-json">Import JSON</span>
@@ -679,6 +685,10 @@ function renderDashboardHtml() {
679
685
  <button id="btn-import-blob" class="btn btn-secondary">
680
686
  <span id="txt-import-blob">Import Blob</span>
681
687
  </button>
688
+ <span style="flex:1"></span>
689
+ <button id="btn-export-all" class="btn btn-secondary">
690
+ <span id="txt-export-all">Export All</span>
691
+ </button>
682
692
  </div>
683
693
  </section>
684
694
  </div>
@@ -782,6 +792,7 @@ function renderDashboardHtml() {
782
792
  $('txt-import-desc').textContent = t('importDesc');
783
793
  $('txt-import-json').textContent = t('importJson');
784
794
  $('txt-import-blob').textContent = t('importBlobBtn');
795
+ $('txt-export-all').textContent = t('exportAll');
785
796
  $('input-import').placeholder = t('importPlaceholder');
786
797
  }
787
798
 
@@ -1073,25 +1084,34 @@ function renderDashboardHtml() {
1073
1084
  } catch (e) { toast(String(e), 'error'); }
1074
1085
  };
1075
1086
 
1076
- $('btn-import-json').onclick = async () => {
1077
- const source = $('input-import').value.trim();
1078
- if (!source) return;
1087
+ const importLines = () => $('input-import').value.split('\n')
1088
+ .map((s) => s.trim())
1089
+ .filter(Boolean);
1090
+
1091
+ const doImport = async (kind) => {
1092
+ const sources = importLines();
1093
+ if (sources.length === 0) return;
1079
1094
  try {
1080
- const r = await api('/import', { method: 'POST', body: JSON.stringify({ kind: 'json', source }) });
1081
- toast(t('importSuccess') + (r.email || '(no email)'), 'success');
1095
+ const r = await api('/import', { method: 'POST', body: JSON.stringify({ kind, sources }) });
1096
+ if (r.errors && r.errors.length > 0) toast(r.errors[0], 'error');
1097
+ toast(t('importBatchResult').replace('{imported}', r.imported).replace('{replaced}', r.replaced), 'success');
1082
1098
  $('input-import').value = '';
1083
1099
  render();
1084
1100
  } catch (e) { toast(String(e), 'error'); }
1085
1101
  };
1086
1102
 
1087
- $('btn-import-blob').onclick = async () => {
1088
- const source = $('input-import').value.trim();
1089
- if (!source) return;
1103
+ $('btn-import-json').onclick = () => doImport('json');
1104
+ $('btn-import-blob').onclick = () => doImport('blob');
1105
+
1106
+ const btnExportAll = $('btn-export-all');
1107
+ if (btnExportAll) btnExportAll.onclick = async () => {
1090
1108
  try {
1091
- const r = await api('/import', { method: 'POST', body: JSON.stringify({ kind: 'blob', source }) });
1092
- toast(t('importSuccess') + (r.email || '(no email)'), 'success');
1093
- $('input-import').value = '';
1094
- render();
1109
+ const r = await api('/export-all', { method: 'POST' });
1110
+ if (!r.blobs || r.blobs.length === 0) { toast('No accounts to export', 'info'); return; }
1111
+ const text = r.blobs.map((b) => b.blob).join('\n');
1112
+ copyToClipboard(text, t('copyAllMsg'));
1113
+ $('diagnostics-panel').open = true;
1114
+ $('diagnostics-output').textContent = text;
1095
1115
  } catch (e) { toast(String(e), 'error'); }
1096
1116
  };
1097
1117
 
@@ -1298,7 +1318,7 @@ function createAgyWebRoutes(options) {
1298
1318
  const session = await sessions.getSession().catch(() => void 0);
1299
1319
  if (!session || session.index !== entry.index) continue;
1300
1320
  try {
1301
- const { fetchAvailableModels } = await import("../models-Cu1g7AXV.mjs").then((n) => n.n);
1321
+ const { fetchAvailableModels } = await import("../models-DagpZ75_.mjs").then((n) => n.n);
1302
1322
  const discovered = await fetchAvailableModels(session.auth.access, session.account.projectId);
1303
1323
  const models = Object.entries(discovered.models ?? {});
1304
1324
  if (models.length > 0) {
@@ -1381,12 +1401,28 @@ function createAgyWebRoutes(options) {
1381
1401
  try {
1382
1402
  const body = await readJson(req);
1383
1403
  const kind = body.kind === "blob" ? "blob" : "json";
1384
- const enriched = await parseImportSource(body.source, kind);
1385
- const { account } = await upsertImportedAccount(store, enriched, { overwriteExisting: true });
1386
- sendJson(res, 200, {
1387
- email: account.email ?? null,
1388
- projectId: enriched.projectId
1389
- });
1404
+ const sources = Array.isArray(body.sources) ? body.sources : body.source !== void 0 ? [body.source] : [];
1405
+ if (sources.length === 0) throw new Error("nothing to import");
1406
+ sendJson(res, 200, await importManySources(sources.map((source) => ({
1407
+ source,
1408
+ kind
1409
+ })), store, { overwriteExisting: true }));
1410
+ } catch (error) {
1411
+ sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
1412
+ }
1413
+ };
1414
+ const handleExportAll = async (_req, res) => {
1415
+ try {
1416
+ const storage = await store.load();
1417
+ const blobs = [];
1418
+ for (let index = 0; index < storage.accounts.length; index++) {
1419
+ const result = await sessions.exportBlob(index);
1420
+ if (result.blob) blobs.push({
1421
+ index,
1422
+ blob: result.blob
1423
+ });
1424
+ }
1425
+ sendJson(res, 200, { blobs });
1390
1426
  } catch (error) {
1391
1427
  sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
1392
1428
  }
@@ -1438,7 +1474,7 @@ function createAgyWebRoutes(options) {
1438
1474
  return;
1439
1475
  }
1440
1476
  try {
1441
- const { listAgyModels } = await import("../models-Cu1g7AXV.mjs").then((n) => n.n);
1477
+ const { listAgyModels } = await import("../models-DagpZ75_.mjs").then((n) => n.n);
1442
1478
  const models = await listAgyModels(session.auth.access, session.account.projectId);
1443
1479
  sendJson(res, 200, {
1444
1480
  account: session.account.email ?? null,
@@ -1544,6 +1580,11 @@ function createAgyWebRoutes(options) {
1544
1580
  path: "/agy/api/import",
1545
1581
  handler: handleImport
1546
1582
  },
1583
+ {
1584
+ kind: "exact",
1585
+ path: "/agy/api/export-all",
1586
+ handler: handleExportAll
1587
+ },
1547
1588
  {
1548
1589
  kind: "exact",
1549
1590
  path: "/agy/api/verify",