openmausbot 0.1.91 → 0.1.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/assets/DesktopViewer-Do6lBa4C.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
  3. package/dist/assets/{arc-DsivfZEh.js → arc-DIYc7Ymx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
  7. package/dist/assets/channel-Dtk5Mtic.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-v6bj852W.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
  34. package/dist/assets/index-Cdmjrl28.css +1 -0
  35. package/dist/assets/{index-DKNEM8Q6.js → index-DGBaEXrM.js} +1 -1
  36. package/dist/assets/index-DR7YPMkW.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
  41. package/dist/assets/{linear-yC_29R6e.js → linear-CmOWzX0P.js} +1 -1
  42. package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-CF1zwoQ-.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist/novnc-NOTICE.txt +118 -0
  62. package/dist-server/cloud-home-start.js +112 -14
  63. package/dist-server/container-mcp.js +195 -15
  64. package/dist-server/drivers/agents-proxy.js +5 -4
  65. package/dist-server/enterprise/server/index.js +5 -1
  66. package/dist-server/index.js +9300 -4155
  67. package/dist-server/local-computer-proxy.js +128 -7
  68. package/dist-server/mcp-gate.js +11 -5
  69. package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
  70. package/dist-server/openmausbot.js +3910 -1182
  71. package/dist-server/pair-cli.js +3910 -1182
  72. package/dist-server/server/boat.js +2 -2
  73. package/dist-server/server/browser-engine.js +10 -1
  74. package/dist-server/server/browser-live.js +46 -2
  75. package/dist-server/server/browser-runtime.js +74 -8
  76. package/dist-server/server/browser-tool-shape.js +6 -4
  77. package/dist-server/server/chief-of-staff.js +1 -1
  78. package/dist-server/server/cloud-home-start.js +118 -26
  79. package/dist-server/server/cloud-home.js +39 -3
  80. package/dist-server/server/cloud-lending.js +74 -10
  81. package/dist-server/server/cloud-owner.js +196 -0
  82. package/dist-server/server/cloud-secrets-boot.js +6 -0
  83. package/dist-server/server/cloud-secrets.js +65 -0
  84. package/dist-server/server/config.js +174 -14
  85. package/dist-server/server/container-computer.js +3 -2
  86. package/dist-server/server/desktop-viewer-targets.js +26 -0
  87. package/dist-server/server/drivers/acp/core.js +167 -21
  88. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  89. package/dist-server/server/drivers/agents-catalog.js +6 -6
  90. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  91. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  92. package/dist-server/server/drivers/antigravity.js +21 -0
  93. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  94. package/dist-server/server/drivers/claude.js +312 -34
  95. package/dist-server/server/drivers/codex.js +228 -18
  96. package/dist-server/server/drivers/grok.js +2 -2
  97. package/dist-server/server/drivers/mistral.js +4 -3
  98. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  99. package/dist-server/server/drivers/openai-chat.js +41 -3
  100. package/dist-server/server/drivers/openai-compat.js +73 -23
  101. package/dist-server/server/engine-install.js +46 -12
  102. package/dist-server/server/env-path.js +15 -3
  103. package/dist-server/server/harness/registry.js +2 -2
  104. package/dist-server/server/index.js +1338 -184
  105. package/dist-server/server/lending-memory.js +479 -0
  106. package/dist-server/server/local-vm-idle.js +24 -6
  107. package/dist-server/server/managed-desktop.js +3 -0
  108. package/dist-server/server/mcp-bridge.js +5 -3
  109. package/dist-server/server/mcp-http.js +6 -3
  110. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  111. package/dist-server/server/mcp-oauth-store.js +70 -0
  112. package/dist-server/server/mcp-oauth.js +550 -0
  113. package/dist-server/server/mcp-probe.js +14 -1
  114. package/dist-server/server/mcp-registry.js +43 -0
  115. package/dist-server/server/memory-upkeep.js +94 -84
  116. package/dist-server/server/model-catalog/catalog.js +374 -0
  117. package/dist-server/server/model-catalog/presets.js +176 -0
  118. package/dist-server/server/model-catalog/trim.js +219 -0
  119. package/dist-server/server/provider-key-check.js +4 -2
  120. package/dist-server/server/recent-work.js +1 -1
  121. package/dist-server/server/request-auth.js +23 -0
  122. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  123. package/dist-server/server/routes/bot-memory.js +31 -6
  124. package/dist-server/server/routes/desktop-viewer.js +194 -0
  125. package/dist-server/server/routine-requests.js +1 -1
  126. package/dist-server/server/routines.js +1 -1
  127. package/dist-server/server/sessions.js +56 -9
  128. package/dist-server/server/skills.js +25 -10
  129. package/dist-server/server/store.js +62 -122
  130. package/dist-server/server/thread-events.js +2 -0
  131. package/dist-server/server/thread-starters.js +33 -0
  132. package/dist-server/server/thread-title.js +46 -0
  133. package/dist-server/server/tts/fish.js +4 -5
  134. package/dist-server/server/tts/index.js +5 -3
  135. package/dist-server/server/vps-computer.js +79 -12
  136. package/dist-server/server/webhooks.js +18 -1
  137. package/dist-server/server/workspace-backup-policy.js +3 -0
  138. package/dist-server/server/workspace-backup.js +3 -0
  139. package/dist-server/server/workspace.js +65 -15
  140. package/dist-server/shared/workspace-backup-client.js +1 -0
  141. package/dist-server/src/lib/citations.js +177 -0
  142. package/dist-server/src/lib/composer-attachments.js +7 -1
  143. package/dist-server/vps-container-mcp.js +196 -15
  144. package/dist-server/workspace-backup.worker.js +73 -9
  145. package/enterprise/server/index.js +5 -1
  146. package/package.json +1 -1
  147. package/dist/assets/channel-DrfGMu7X.js +0 -1
  148. package/dist/assets/index-BlnCEcU6.js +0 -588
  149. package/dist/assets/index-DVxjOvID.css +0 -1
  150. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +0 -8
@@ -25,7 +25,7 @@ import { mergeTopicText } from "./memory-topics.js";
25
25
  import { applyMoves, MAX_MOVES, organizeCandidates, organizePrompt, parseMoves } from "./memory-organize.js";
26
26
  import { recordMemoryChange } from "./memory-journal.js";
27
27
  import { applyTidy, contradictionCandidates, contradictionPrompt, parseContradictions, planChanges, planTidy } from "./memory-tidy.js";
28
- import { ensureWorkspace, listMemoryTopics, memoryDate, memoryEntry, memoryTopicIndex, updateMemory, workspaceDir, writeMemoryFile, writeMemoryTopic } from "./workspace.js";
28
+ import { ensureWorkspace, listMemoryTopics, memoryDate, memoryEntry, memoryTopicIndex, readMemoryText, updateMemory, workspaceDir, writeMemoryFile, writeMemoryTopic } from "./workspace.js";
29
29
  export const CAPTURE_MAX_TURNS = 6;
30
30
  export const MODEL_TIMEOUT_MS = 60_000;
31
31
  export const TIDY_CHECK_MS = 10 * 60_000;
@@ -36,7 +36,7 @@ export const ARCHIVE_TOPIC = "archive.md";
36
36
  export const TOPIC_MAX_BYTES = 64 * 1024;
37
37
  /** On unless switched off: every bot keeps its memory in shape by default. */
38
38
  export function upkeepEnabled(bot) {
39
- return Boolean(bot) && bot.memoryUpkeep !== false;
39
+ return Boolean(bot) && bot.memoryUpkeep !== false && bot.memoryEnabled !== false;
40
40
  }
41
41
  /** How many "this entry is core" judgements are remembered per bot. */
42
42
  const MAX_CORE = 500;
@@ -63,7 +63,7 @@ function saveState(state) {
63
63
  }
64
64
  function readRaw(botId, relative) {
65
65
  try {
66
- return readFileSync(join(workspaceDir(botId), relative), "utf8");
66
+ return readMemoryText(join(workspaceDir(botId), relative));
67
67
  }
68
68
  catch {
69
69
  return null;
@@ -103,6 +103,7 @@ export function createMemoryUpkeep(deps) {
103
103
  void promise.finally(() => inflight.delete(promise)).catch(() => undefined);
104
104
  return promise;
105
105
  };
106
+ const writing = deps.writing ?? ((_botId, write) => write());
106
107
  async function capture(batch) {
107
108
  const bot = deps.bot(batch.botId);
108
109
  const at = now().getTime();
@@ -138,54 +139,57 @@ export function createMemoryUpkeep(deps) {
138
139
  // From here to the journal rows there is no await, so no write interleaves.
139
140
  if (!upkeepEnabled(deps.bot(bot.id)))
140
141
  return off;
141
- const before = readRaw(bot.id, "MEMORY.md");
142
- const parsed = parseCandidates(answer, today);
143
- const fresh = newCandidates(parsed, before ?? "");
144
- const source = `${deps.sourceLabel(bot.id, batch.threadId)} (noticed)`;
145
- let added = 0;
146
- let full = false;
147
- const byTopic = new Map();
148
- for (const candidate of fresh) {
149
- if (candidate.topic) {
150
- byTopic.set(candidate.topic, [...(byTopic.get(candidate.topic) ?? []), candidate]);
151
- continue;
152
- }
153
- const result = updateMemory(bot.id, { action: "append", text: candidate.text, ...(candidate.until ? { until: candidate.until } : {}) }, { source, now: now() });
154
- if (result.ok)
155
- added += 1;
156
- else if (result.code === "over-budget")
157
- full = true;
158
- }
159
- if (added)
160
- recordMemoryChange(bot.id, { path: "MEMORY.md", actor: "upkeep", via: "capture", threadId: batch.threadId, before, after: readRaw(bot.id, "MEMORY.md") });
161
- // Detail goes to a topic file the bot keeps for that subject, created
162
- // with a header (title and other words for it) the first time.
163
- const existing = new Map(listMemoryTopics(bot.id).map((topic) => [topic.name.toLowerCase(), topic.name]));
164
- let topics = 0;
165
- for (const [wanted, candidates] of byTopic) {
166
- const name = existing.get(wanted) ?? wanted;
167
- const path = `memory/${name}`;
168
- const current = readRaw(bot.id, path);
169
- if (current !== null && Buffer.byteLength(current, "utf8") > TOPIC_MAX_BYTES)
170
- continue;
171
- const seen = notebookIdentities(current ?? "");
172
- const lines = candidates
173
- .filter((candidate) => !seen.has(factIdentity(candidate.text)))
174
- .map((candidate) => memoryEntry(candidate.text, { source, now: now(), ...(candidate.until ? { until: candidate.until } : {}) }));
175
- if (!lines.length)
176
- continue;
177
- const title = name.replace(/\.md$/, "").replace(/-/g, " ");
178
- const aliases = [...new Set(candidates.flatMap((candidate) => candidate.topicAliases ?? []))];
179
- try {
180
- writeMemoryTopic(bot.id, name, mergeTopicText(current, { title, aliases, lines }));
142
+ const { parsed, added, topics, full } = writing(bot.id, () => {
143
+ const before = readRaw(bot.id, "MEMORY.md");
144
+ const parsed = parseCandidates(answer, today);
145
+ const fresh = newCandidates(parsed, before ?? "");
146
+ const source = `${deps.sourceLabel(bot.id, batch.threadId)} (noticed)`;
147
+ let added = 0;
148
+ let full = false;
149
+ const byTopic = new Map();
150
+ for (const candidate of fresh) {
151
+ if (candidate.topic) {
152
+ byTopic.set(candidate.topic, [...(byTopic.get(candidate.topic) ?? []), candidate]);
153
+ continue;
154
+ }
155
+ const result = updateMemory(bot.id, { action: "append", text: candidate.text, ...(candidate.until ? { until: candidate.until } : {}) }, { source, now: now() });
156
+ if (result.ok)
157
+ added += 1;
158
+ else if (result.code === "over-budget")
159
+ full = true;
181
160
  }
182
- catch {
183
- continue;
161
+ if (added)
162
+ recordMemoryChange(bot.id, { path: "MEMORY.md", actor: "upkeep", via: "capture", threadId: batch.threadId, before, after: readRaw(bot.id, "MEMORY.md") });
163
+ // Detail goes to a topic file the bot keeps for that subject, created
164
+ // with a header (title and other words for it) the first time.
165
+ const existing = new Map(listMemoryTopics(bot.id).map((topic) => [topic.name.toLowerCase(), topic.name]));
166
+ let topics = 0;
167
+ for (const [wanted, candidates] of byTopic) {
168
+ const name = existing.get(wanted) ?? wanted;
169
+ const path = `memory/${name}`;
170
+ const current = readRaw(bot.id, path);
171
+ if (current !== null && Buffer.byteLength(current, "utf8") > TOPIC_MAX_BYTES)
172
+ continue;
173
+ const seen = notebookIdentities(current ?? "");
174
+ const lines = candidates
175
+ .filter((candidate) => !seen.has(factIdentity(candidate.text)))
176
+ .map((candidate) => memoryEntry(candidate.text, { source, now: now(), ...(candidate.until ? { until: candidate.until } : {}) }));
177
+ if (!lines.length)
178
+ continue;
179
+ const title = name.replace(/\.md$/, "").replace(/-/g, " ");
180
+ const aliases = [...new Set(candidates.flatMap((candidate) => candidate.topicAliases ?? []))];
181
+ try {
182
+ writeMemoryTopic(bot.id, name, mergeTopicText(current, { title, aliases, lines }));
183
+ }
184
+ catch {
185
+ continue;
186
+ }
187
+ recordMemoryChange(bot.id, { path, actor: "upkeep", via: "capture", threadId: batch.threadId, before: current, after: readRaw(bot.id, path) });
188
+ existing.set(name.toLowerCase(), name);
189
+ topics += lines.length;
184
190
  }
185
- recordMemoryChange(bot.id, { path, actor: "upkeep", via: "capture", threadId: batch.threadId, before: current, after: readRaw(bot.id, path) });
186
- existing.set(name.toLowerCase(), name);
187
- topics += lines.length;
188
- }
191
+ return { parsed, added, topics, full };
192
+ });
189
193
  // Only the owner's own words reach About me: on a shared workspace
190
194
  // another person's facts are not the owner's profile. Every parsed fact
191
195
  // counts, not only new ones — the bot may already have noted it in its
@@ -245,37 +249,39 @@ export function createMemoryUpkeep(deps) {
245
249
  saveState(saved);
246
250
  if (!moves.length)
247
251
  return 0;
248
- const { byTopic } = applyMoves(before, moves);
249
- const written = new Set();
250
- const existing = new Map(listMemoryTopics(botId).map((topic) => [topic.name.toLowerCase(), topic.name]));
251
- let count = 0;
252
- for (const [wanted, group] of byTopic) {
253
- const name = existing.get(wanted) ?? wanted;
254
- const path = `memory/${name}`;
255
- const current = readRaw(botId, path);
256
- const seen = notebookIdentities(current ?? "");
257
- const lines = group.lines.filter((line) => !seen.has(lineFactIdentity(line)));
258
- if (lines.length) {
259
- try {
260
- writeMemoryTopic(botId, name, mergeTopicText(current, { title: name.replace(/\.md$/, "").replace(/-/g, " "), aliases: group.aliases, lines }));
252
+ return writing(botId, () => {
253
+ const { byTopic } = applyMoves(before, moves);
254
+ const written = new Set();
255
+ const existing = new Map(listMemoryTopics(botId).map((topic) => [topic.name.toLowerCase(), topic.name]));
256
+ let count = 0;
257
+ for (const [wanted, group] of byTopic) {
258
+ const name = existing.get(wanted) ?? wanted;
259
+ const path = `memory/${name}`;
260
+ const current = readRaw(botId, path);
261
+ const seen = notebookIdentities(current ?? "");
262
+ const lines = group.lines.filter((line) => !seen.has(lineFactIdentity(line)));
263
+ if (lines.length) {
264
+ try {
265
+ writeMemoryTopic(botId, name, mergeTopicText(current, { title: name.replace(/\.md$/, "").replace(/-/g, " "), aliases: group.aliases, lines }));
266
+ }
267
+ catch (error) {
268
+ log(`memory upkeep: could not write ${path}; kept its entries in MEMORY.md: ${error.message}`);
269
+ continue;
270
+ }
271
+ recordMemoryChange(botId, { path, actor: "upkeep", via: "organize", before: current, after: readRaw(botId, path) });
261
272
  }
262
- catch (error) {
263
- log(`memory upkeep: could not write ${path}; kept its entries in MEMORY.md: ${error.message}`);
264
- continue;
265
- }
266
- recordMemoryChange(botId, { path, actor: "upkeep", via: "organize", before: current, after: readRaw(botId, path) });
273
+ existing.set(name.toLowerCase(), name);
274
+ written.add(wanted);
275
+ count += group.lines.length;
267
276
  }
268
- existing.set(name.toLowerCase(), name);
269
- written.add(wanted);
270
- count += group.lines.length;
271
- }
272
- if (!written.size)
273
- return 0;
274
- const { text } = applyMoves(before, moves.filter((move) => written.has(move.topic)));
275
- writeMemoryFile(botId, text);
276
- recordMemoryChange(botId, { path: "MEMORY.md", actor: "upkeep", via: "organize", before, after: readRaw(botId, "MEMORY.md") });
277
- log(`memory upkeep: moved ${count} entr${count === 1 ? "y" : "ies"} from MEMORY.md into topic files for ${deps.bot(botId)?.name ?? botId}`);
278
- return count;
277
+ if (!written.size)
278
+ return 0;
279
+ const { text } = applyMoves(before, moves.filter((move) => written.has(move.topic)));
280
+ writeMemoryFile(botId, text);
281
+ recordMemoryChange(botId, { path: "MEMORY.md", actor: "upkeep", via: "organize", before, after: readRaw(botId, "MEMORY.md") });
282
+ log(`memory upkeep: moved ${count} entr${count === 1 ? "y" : "ies"} from MEMORY.md into topic files for ${deps.bot(botId)?.name ?? botId}`);
283
+ return count;
284
+ });
279
285
  }
280
286
  function appendArchive(botId, archived) {
281
287
  const archivePath = `memory/${ARCHIVE_TOPIC}`;
@@ -330,9 +336,11 @@ export function createMemoryUpkeep(deps) {
330
336
  // The archive is written first: a line is never out of MEMORY.md
331
337
  // before it is in the archive, and the newest journal row — the one a
332
338
  // person undoes — is MEMORY.md, whose undo brings the line back.
333
- if (archived.length)
334
- appendArchive(botId, archived);
335
- writeMemoryFile(botId, text);
339
+ writing(botId, () => {
340
+ if (archived.length)
341
+ appendArchive(botId, archived);
342
+ writeMemoryFile(botId, text);
343
+ });
336
344
  recordMemoryChange(botId, { path: "MEMORY.md", actor: "upkeep", via: "tidy", before, after: readRaw(botId, "MEMORY.md") });
337
345
  }
338
346
  // Topic files get the two steps that need no model: expired lines to the
@@ -350,9 +358,11 @@ export function createMemoryUpkeep(deps) {
350
358
  if (!planChanges(topicPlan))
351
359
  continue;
352
360
  const tidied = applyTidy(topicBefore, topicPlan, today);
353
- if (tidied.archived.length)
354
- appendArchive(botId, tidied.archived);
355
- writeMemoryTopic(botId, topic.name, tidied.text);
361
+ writing(botId, () => {
362
+ if (tidied.archived.length)
363
+ appendArchive(botId, tidied.archived);
364
+ writeMemoryTopic(botId, topic.name, tidied.text);
365
+ });
356
366
  recordMemoryChange(botId, { path, actor: "upkeep", via: "tidy", before: topicBefore, after: readRaw(botId, path) });
357
367
  topicExpired += topicPlan.expired.length;
358
368
  topicDuplicates += topicPlan.duplicates.length;
@@ -0,0 +1,374 @@
1
+ /*! Adapted from anomalyco/opencode@2fa3363c924c5c3e367b84a87ae478296a0ed59b
2
+ * packages/core/src/models-dev.ts:160-258, MIT; modified.
3
+ *
4
+ * MIT License
5
+ *
6
+ * Copyright (c) 2025 opencode
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the "Software"), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ */
26
+ // The model provider catalog: load it, keep it fresh, merge in the presets.
27
+ //
28
+ // Kept from OpenCode's models-dev.ts: a cache file refreshed from
29
+ // models.dev/api.json with a 10 s timeout, written through a temp file and a
30
+ // rename; a corrupt cache is deleted; a snapshot bundled with the app; a
31
+ // forced refresh skips the freshness check; failures are logged and never
32
+ // surface; one switch turns fetching off.
33
+ //
34
+ // Changed for OpenMausBot:
35
+ // - plain Node instead of Effect; one server process owns DATA_DIR, so a
36
+ // single in-flight promise stands in for OpenCode's cross-process flock;
37
+ // - data counts as fresh for 24 hours, and a refresh runs only when asked
38
+ // (Settings or the model picker opening), not on a timer;
39
+ // - If-None-Match with the stored ETag, a 20 MB cap, redirects refused;
40
+ // - every source is trimmed and brace-filtered (trim.ts) before use;
41
+ // - the source is the newest of: our cache, OpenCode's own cache
42
+ // (~/.cache/opencode/models.json, read only), the bundled snapshot. On a tie
43
+ // that order decides, so a newer snapshot beats an older cache;
44
+ // - switched off by OMB_DISABLE_MODEL_CATALOG_FETCH=1, and by the caller on
45
+ // managed or hosted installs.
46
+ //
47
+ // This list is never the last word on what a key can use: the provider's own
48
+ // /models, called with the key, is. The catalog supplies names, key variable
49
+ // names, base URLs and the capability flags used to pick recommended models.
50
+ import { existsSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
51
+ import { homedir } from "node:os";
52
+ import { dirname, join } from "node:path";
53
+ import { fileURLToPath } from "node:url";
54
+ import { writeFileAtomic } from "../atomic.js";
55
+ import { OPENAI_COMPATIBLE_NPM, PROVIDER_PRESETS } from "./presets.js";
56
+ import { MODEL_CATALOG_SCHEMA, catalogStats, packCatalog, trimModelsDevCatalog, unpackCatalog, } from "./trim.js";
57
+ export const MODELS_DEV_URL = "https://models.dev/api.json";
58
+ export const CATALOG_MAX_AGE_MS = 24 * 60 * 60 * 1000;
59
+ export const CATALOG_FETCH_TIMEOUT_MS = 10_000;
60
+ export const CATALOG_MAX_BYTES = 20 * 1024 * 1024;
61
+ export const CATALOG_DISABLE_ENV = "OMB_DISABLE_MODEL_CATALOG_FETCH";
62
+ export const SNAPSHOT_FILE = "models-dev.snapshot.json";
63
+ /** Our cache, under providers/ so backups and Move to Cloud leave it out. */
64
+ export function modelCatalogCachePath(dataDir) {
65
+ return join(dataDir, "providers", "model-providers", "models-dev.json");
66
+ }
67
+ /** Where OpenCode keeps models.dev (xdg-basedir's cache dir, on every OS). */
68
+ export function openCodeCatalogCachePath(env = process.env) {
69
+ return join(env.XDG_CACHE_HOME || join(homedir(), ".cache"), "opencode", "models.json");
70
+ }
71
+ /** The snapshot sits beside this module in a checkout, and under
72
+ * model-catalog/ beside the bundled server (scripts/bundle-server.mjs). */
73
+ function readBundledSnapshot() {
74
+ const here = dirname(fileURLToPath(import.meta.url));
75
+ for (const path of [join(here, SNAPSHOT_FILE), join(here, "model-catalog", SNAPSHOT_FILE)]) {
76
+ if (existsSync(path))
77
+ return readFileSync(path, "utf8");
78
+ }
79
+ return undefined;
80
+ }
81
+ // eslint-disable-next-line no-control-regex
82
+ const CONTROL = /[\u0000-\u001f\u007f]/u;
83
+ // An ETag is echoed back in a request header: keep only a plain, short one.
84
+ const safeEtag = (value) => typeof value === "string" && value.length <= 256 && !CONTROL.test(value) ? value : undefined;
85
+ const time = (value) => {
86
+ if (typeof value !== "string")
87
+ return undefined;
88
+ const parsed = Date.parse(value);
89
+ return Number.isNaN(parsed) ? undefined : parsed;
90
+ };
91
+ const message = (error) => error instanceof Error ? error.message : String(error);
92
+ export class ModelCatalogStore {
93
+ options;
94
+ cachePath;
95
+ memo;
96
+ inflight;
97
+ constructor(options) {
98
+ this.options = options;
99
+ this.cachePath = modelCatalogCachePath(options.dataDir);
100
+ }
101
+ get now() {
102
+ return (this.options.now ?? Date.now)();
103
+ }
104
+ log(text) {
105
+ (this.options.log ?? ((line) => console.warn(line)))(`[model-catalog] ${text}`);
106
+ }
107
+ get fetchDisabled() {
108
+ return Boolean(this.options.fetchDisabled) || (this.options.env ?? process.env)[CATALOG_DISABLE_ENV] === "1";
109
+ }
110
+ /** The newest usable catalog. Never throws; worst case an empty catalog. */
111
+ get() {
112
+ if (this.memo)
113
+ return this.memo;
114
+ const candidates = [this.ownCache(), this.openCodeCache(), this.snapshot()]
115
+ .filter((candidate) => candidate !== undefined);
116
+ // Stable sort: on equal times the listed order (cache, OpenCode, snapshot) wins.
117
+ candidates.sort((a, b) => b.updatedAt - a.updatedAt);
118
+ for (const candidate of candidates) {
119
+ let drops = 0;
120
+ const providers = trimModelsDevCatalog(candidate.providers(), { onDrop: () => { drops += 1; } });
121
+ if (!Object.keys(providers).length) {
122
+ this.log(`the ${candidate.origin} catalog held no usable providers; trying the next one`);
123
+ continue;
124
+ }
125
+ if (drops)
126
+ this.log(`left out ${drops} ${candidate.origin} catalog entries that were malformed or contained { or }`);
127
+ this.memo = { origin: candidate.origin, updatedAt: new Date(candidate.updatedAt).toISOString(), providers };
128
+ return this.memo;
129
+ }
130
+ this.memo = { origin: "empty", updatedAt: new Date(0).toISOString(), providers: trimModelsDevCatalog(undefined) };
131
+ return this.memo;
132
+ }
133
+ /** Refresh from models.dev unless the cache is under 24 hours old (or
134
+ * `force`). Concurrent calls share one request. Never throws. */
135
+ refresh(options = {}) {
136
+ if (this.fetchDisabled)
137
+ return Promise.resolve("disabled");
138
+ this.inflight ??= this.fetchAndStore(Boolean(options.force)).finally(() => {
139
+ this.inflight = undefined;
140
+ });
141
+ return this.inflight;
142
+ }
143
+ readDocument() {
144
+ if (!existsSync(this.cachePath))
145
+ return undefined;
146
+ try {
147
+ if (statSync(this.cachePath).size > (this.options.maxBytes ?? CATALOG_MAX_BYTES))
148
+ throw new Error("too large");
149
+ const doc = JSON.parse(readFileSync(this.cachePath, "utf8"));
150
+ const updatedAt = time(doc?.updatedAt);
151
+ if (doc?.schema !== MODEL_CATALOG_SCHEMA || updatedAt === undefined || !doc.providers)
152
+ throw new Error("unrecognised format");
153
+ return { doc, updatedAt };
154
+ }
155
+ catch (error) {
156
+ // As OpenCode does, drop a cache we cannot read; the next refresh rewrites it.
157
+ this.log(`removed an unreadable catalog cache (${message(error)})`);
158
+ try {
159
+ rmSync(this.cachePath, { force: true });
160
+ }
161
+ catch { /* best effort */ }
162
+ return undefined;
163
+ }
164
+ }
165
+ ownCache() {
166
+ const cached = this.readDocument();
167
+ return cached && { origin: "cache", updatedAt: cached.updatedAt, providers: () => unpackCatalog(cached.doc.providers) };
168
+ }
169
+ openCodeCache() {
170
+ const path = this.options.openCodeCachePath === undefined
171
+ ? openCodeCatalogCachePath(this.options.env ?? process.env)
172
+ : this.options.openCodeCachePath;
173
+ if (!path)
174
+ return undefined;
175
+ try {
176
+ const stat = statSync(path);
177
+ if (!stat.isFile() || stat.size > (this.options.maxBytes ?? CATALOG_MAX_BYTES))
178
+ return undefined;
179
+ // Read only if chosen: it is models.dev's full 5 MB catalog.
180
+ return { origin: "opencode", updatedAt: stat.mtimeMs, providers: () => {
181
+ try {
182
+ return JSON.parse(readFileSync(path, "utf8"));
183
+ }
184
+ catch {
185
+ return undefined;
186
+ }
187
+ } };
188
+ }
189
+ catch {
190
+ return undefined;
191
+ }
192
+ }
193
+ snapshot() {
194
+ try {
195
+ const text = (this.options.readSnapshot ?? readBundledSnapshot)();
196
+ if (text === undefined) {
197
+ this.log("the bundled catalog snapshot is missing");
198
+ return undefined;
199
+ }
200
+ const doc = JSON.parse(text);
201
+ const updatedAt = time(doc?.updatedAt);
202
+ if (doc?.schema !== MODEL_CATALOG_SCHEMA || updatedAt === undefined)
203
+ throw new Error("unrecognised format");
204
+ return { origin: "snapshot", updatedAt, providers: () => unpackCatalog(doc.providers) };
205
+ }
206
+ catch (error) {
207
+ this.log(`could not read the bundled catalog snapshot (${message(error)})`);
208
+ return undefined;
209
+ }
210
+ }
211
+ async fetchAndStore(force) {
212
+ const cached = this.readDocument();
213
+ const age = cached ? this.now - cached.updatedAt : Infinity;
214
+ // A cache stamped in the future (clock moved back) counts as stale.
215
+ if (!force && age >= 0 && age < CATALOG_MAX_AGE_MS)
216
+ return "fresh";
217
+ try {
218
+ const etag = cached ? safeEtag(cached.doc.source?.etag) : undefined;
219
+ const response = await this.download(etag);
220
+ if (response.status === 304) {
221
+ if (!cached)
222
+ throw new Error("models.dev answered 304 without a cached copy");
223
+ this.write({ ...cached.doc, updatedAt: new Date(this.now).toISOString() });
224
+ this.memo = undefined;
225
+ return "not-modified";
226
+ }
227
+ let raw;
228
+ try {
229
+ raw = JSON.parse(response.text);
230
+ }
231
+ catch {
232
+ throw new Error("models.dev returned invalid JSON");
233
+ }
234
+ let drops = 0;
235
+ const providers = trimModelsDevCatalog(raw, { onDrop: () => { drops += 1; } });
236
+ // Never replace a good cache with an empty one.
237
+ if (!Object.keys(providers).length)
238
+ throw new Error("models.dev returned no usable providers");
239
+ this.write({
240
+ schema: MODEL_CATALOG_SCHEMA,
241
+ updatedAt: new Date(this.now).toISOString(),
242
+ source: { name: "models.dev", url: MODELS_DEV_URL, license: "MIT", ...(response.etag ? { etag: response.etag } : {}) },
243
+ providers: packCatalog(providers),
244
+ });
245
+ this.memo = undefined;
246
+ const stats = catalogStats(providers);
247
+ this.log(`updated from models.dev: ${stats.providers} providers, ${stats.models} models` +
248
+ (drops ? `; left out ${drops} entries that were malformed or contained { or }` : ""));
249
+ return "updated";
250
+ }
251
+ catch (error) {
252
+ this.log(`could not refresh from models.dev (${message(error)}); keeping the current catalog`);
253
+ return "failed";
254
+ }
255
+ }
256
+ write(doc) {
257
+ mkdirSync(dirname(this.cachePath), { recursive: true });
258
+ writeFileAtomic(this.cachePath, JSON.stringify(doc));
259
+ }
260
+ async download(etag) {
261
+ const maxBytes = this.options.maxBytes ?? CATALOG_MAX_BYTES;
262
+ const controller = new AbortController();
263
+ const timer = setTimeout(() => controller.abort(), this.options.timeoutMs ?? CATALOG_FETCH_TIMEOUT_MS);
264
+ try {
265
+ const response = await (this.options.fetch ?? fetch)(MODELS_DEV_URL, {
266
+ headers: { accept: "application/json", "user-agent": "OpenMausBot", ...(etag ? { "if-none-match": etag } : {}) },
267
+ // Nothing secret is sent, but the catalog comes from models.dev only.
268
+ redirect: "error",
269
+ signal: controller.signal,
270
+ });
271
+ if (response.status === 304 && etag) {
272
+ void response.body?.cancel().catch(() => { });
273
+ return { status: 304 };
274
+ }
275
+ if (response.status !== 200) {
276
+ void response.body?.cancel().catch(() => { });
277
+ throw new Error(`models.dev returned HTTP ${response.status}`);
278
+ }
279
+ const declared = Number(response.headers.get("content-length"));
280
+ if (Number.isFinite(declared) && declared > maxBytes) {
281
+ void response.body?.cancel().catch(() => { });
282
+ throw new Error(`models.dev sent more than ${maxBytes} bytes`);
283
+ }
284
+ // The timer also covers reading the body.
285
+ const text = await readCapped(response, maxBytes);
286
+ return { status: 200, text, etag: safeEtag(response.headers.get("etag")) };
287
+ }
288
+ catch (error) {
289
+ if (controller.signal.aborted)
290
+ throw new Error("models.dev did not answer in time");
291
+ throw error;
292
+ }
293
+ finally {
294
+ clearTimeout(timer);
295
+ }
296
+ }
297
+ }
298
+ async function readCapped(response, maxBytes) {
299
+ if (!response.body)
300
+ return "";
301
+ const reader = response.body.getReader();
302
+ const decoder = new TextDecoder();
303
+ let total = 0;
304
+ let text = "";
305
+ for (;;) {
306
+ const { done, value } = await reader.read();
307
+ if (done)
308
+ break;
309
+ total += value.byteLength;
310
+ if (total > maxBytes) {
311
+ await reader.cancel().catch(() => { });
312
+ throw new Error(`models.dev sent more than ${maxBytes} bytes`);
313
+ }
314
+ text += decoder.decode(value, { stream: true });
315
+ }
316
+ return text + decoder.decode();
317
+ }
318
+ /** The SDK package for a preset's pinned `api`. models.dev's `npm` describes
319
+ * models.dev's `api` (or, when it lists none, the SDK's own default address,
320
+ * which the preset pins too), so it is kept only when that is the address the
321
+ * preset pins. Otherwise the pinned address is OpenAI-compatible by definition. */
322
+ function presetNpm(preset, provider) {
323
+ if (preset.npm)
324
+ return preset.npm;
325
+ if (!provider?.npm)
326
+ return undefined;
327
+ return provider.api === undefined || provider.api === preset.api ? provider.npm : OPENAI_COMPATIBLE_NPM;
328
+ }
329
+ /** The catalog merged with the presets: recommended rows first, in their
330
+ * order, then every other provider by name. Presets whose provider the
331
+ * catalog lacks (or dropped) are still listed. */
332
+ export function catalogEntries(catalog, presets = PROVIDER_PRESETS) {
333
+ const entries = [];
334
+ const covered = new Set();
335
+ for (const preset of presets) {
336
+ const provider = preset.catalogId ? catalog.providers[preset.catalogId] : undefined;
337
+ if (provider)
338
+ covered.add(provider.id);
339
+ const entry = {
340
+ id: preset.id,
341
+ label: preset.label,
342
+ env: provider?.env ?? (preset.env ? [preset.env] : []),
343
+ api: preset.api,
344
+ modelCount: provider ? Object.keys(provider.models).length : 0,
345
+ preset,
346
+ };
347
+ if (provider)
348
+ entry.catalogId = provider.id;
349
+ const npm = presetNpm(preset, provider);
350
+ if (npm)
351
+ entry.npm = npm;
352
+ const doc = preset.doc ?? provider?.doc;
353
+ if (doc)
354
+ entry.doc = doc;
355
+ if (preset.recommended !== undefined)
356
+ entry.recommended = preset.recommended;
357
+ entries.push(entry);
358
+ }
359
+ for (const provider of Object.values(catalog.providers)) {
360
+ if (covered.has(provider.id) || presets.some((preset) => preset.id === provider.id))
361
+ continue;
362
+ const entry = { id: provider.id, catalogId: provider.id, label: provider.name, env: provider.env, modelCount: Object.keys(provider.models).length };
363
+ if (provider.npm)
364
+ entry.npm = provider.npm;
365
+ if (provider.api)
366
+ entry.api = provider.api;
367
+ if (provider.doc)
368
+ entry.doc = provider.doc;
369
+ entries.push(entry);
370
+ }
371
+ return entries.sort((a, b) => (a.recommended ?? Infinity) - (b.recommended ?? Infinity) ||
372
+ a.label.localeCompare(b.label, "en", { sensitivity: "base" }) ||
373
+ a.id.localeCompare(b.id));
374
+ }