openmausbot 0.1.89 → 0.1.90

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 (140) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-BwaMaZEm.js → abnfDiagram-O67JEVCF-DUyXd0kL.js} +1 -1
  2. package/dist/assets/{arc-Be-8GKxd.js → arc-q73qM-At.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-Dp_U2Kv1.js → architectureDiagram-NJMV4G6O-D33O3Wur.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-DQjUjYUk.js → blockDiagram-BEXU5L5S-DLsJeoXC.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-Bd3aE4Kc.js → c4Diagram-YGBWAQC7-DnaljZnu.js} +1 -1
  6. package/dist/assets/channel-BCuyHjFV.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-BijjTBeJ.js → chunk-2Q5K7J3B-Da0h2i79.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-CMGdwvzW.js → chunk-3FUC2YCW-CfzHi54f.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-0G09BfsZ.js → chunk-5VM5RSS4-LjE4d9Jf.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-FOp3vSHe.js → chunk-GTNCS2PH-DY-UGmvc.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-Dv-20VIn.js → chunk-GWA4HPMP-BqeGhyhP.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-CRgrdFcz.js → chunk-JWPE2WC7-BYa9EBAg.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-3hP4f7QB.js → chunk-NETBCI7D-vTrDtWyb.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-CG98IjlG.js → chunk-WEXAMYUT-ic5tXadM.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-BrSTC9xG.js → chunk-XXDRQBXY-DvRCY-c6.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-rpQQQDFX.js → classDiagram-v2-NBCMYWYE-DVbmgz_L.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-G6RSafHA.js → cose-bilkent-JH36ORCC-vdUs2n57.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-CXjv-IM2.js → cynefin-OW5HDTMX-BvLGlV3U.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-Dw07MR9f.js → cynefinDiagram-VND7K2PF-C4GcJQDt.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-BwPnlTg2.js → dagre-6A5THRUB-BMSaHpt_.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-Cm6xcMiz.js → diagram-22UHCM2B-D5b9UBhC.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-CDpogPbE.js → diagram-3UASUU5V-CIKLlB7Y.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-BV8z3Pnc.js → diagram-ATOU4E4O-BzXb-7k7.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-CKCQhOOC.js → diagram-CDSNMT55-HsTvJ7uT.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-FGvJg69C.js → diagram-MLGK6HIB-Db9ySNbQ.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-CwATvgnS.js → diagram-MPIPVDR6-D6BKbpqY.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-CH_c3SSC.js → ebnfDiagram-ZINNZB2B-Bq067L0t.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-3guZi71R.js → elk-276RUBZZ-DhaD7fNJ.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-BMHphR2n.js → erDiagram-OPXOYQCR-fwIhfz36.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-Vm5DkW5x.js → flowDiagram-KWPJA3E3-DygIyu0V.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-DO-1ct07.js → ganttDiagram-FUAMR5RP-DPYfSO96.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-nxeksEIN.js → gitGraphDiagram-X574FWY7-DhkHeCle.js} +1 -1
  33. package/dist/assets/index-CRvo-DS2.css +1 -0
  34. package/dist/assets/index-CWYGlA63.js +588 -0
  35. package/dist/assets/{index-Oxk9x45i.js → index-G1PHKGyh.js} +1 -1
  36. package/dist/assets/{infoDiagram-VRGFBTTK-Dkcvr0p7.js → infoDiagram-VRGFBTTK-DstBLxME.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BwVibkoO.js → ishikawaDiagram-OU5B5YK6-CrIsGgch.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-ByiBZqgG.js → journeyDiagram-ZHPQQLJL-CYypGDbB.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-qqjgXBF3.js → kanban-definition-PNTS6WVX-DGjPt8E_.js} +1 -1
  40. package/dist/assets/{linear-CtJnKe6A.js → linear-Dwc3Hrru.js} +1 -1
  41. package/dist/assets/{mermaid.core-BEiDklrY.js → mermaid.core-Dc4OePBL.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-DfIYHia9.js → mindmap-definition-NLK3R4M7-DV-vepj-.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-CYyRqa1a.js → pegDiagram-GJSIUBJH-lx5Kj-n5.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-hOToy_zH.js → pieDiagram-5QR66LMP-Bq7Vmm15.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DKAsNN9I.js → quadrantDiagram-O4NWA36T-DeqeqOBj.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-BSA-d8JU.js → railroadDiagram-XR7U4H2S-CXwRQzSC.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-CSmlvb-p.js → requirementDiagram-PLB6GJNP-8tAeoB88.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-CD5F6fnG.js → sankeyDiagram-IPEJSGJF-BG5zjGWU.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-JUq5sx6d.js → sequenceDiagram-PO4LG4MO-DKF3iLcw.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-BjIS-Stk.js → sizeCapture-INFHLROL-B-y1qUgT.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-DklkUxOg.js → stateDiagram-v2-GCMORJYK-MkzPgxAc.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-CsjIU6be.js → swimlanes-2SLR337P-DnbaOEwH.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-BkFqK7AB.js → timeline-definition-EJHVYXUP-Bb4xG_Bt.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-70gJB1EU.js → usecaseDiagram-POWQR4AR-JgkR9pFf.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-83Tm62dN.js → vennDiagram-UO4OBE2U-CIPIrAYe.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-tTmseoX9.js → wardleyDiagram-VNRHLVJA-BjCLuUvJ.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-R1LBA55a.js → xychartDiagram-PMCCYNJV-DKgiaYxT.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/cloud-home-start.js +14795 -0
  61. package/dist-server/container-mcp.js +52 -4
  62. package/dist-server/drivers/agents-proxy.js +20 -10
  63. package/dist-server/index.js +8985 -6461
  64. package/dist-server/openmausbot.js +327 -145
  65. package/dist-server/pair-cli.js +327 -145
  66. package/dist-server/permission-proxy.js +1 -1
  67. package/dist-server/server/admission.js +46 -0
  68. package/dist-server/server/{box-create-idempotency.js → boat-create-idempotency.js} +32 -31
  69. package/dist-server/server/{box-delete-journal.js → boat-delete-journal.js} +26 -25
  70. package/dist-server/server/{box.js → boat.js} +330 -328
  71. package/dist-server/server/bot-profile.js +7 -1
  72. package/dist-server/server/channel-queue.js +58 -18
  73. package/dist-server/server/checkpoints.js +98 -52
  74. package/dist-server/server/claim-idle.js +33 -0
  75. package/dist-server/server/cloud-backend.js +5 -5
  76. package/dist-server/server/cloud-home-start.js +103 -0
  77. package/dist-server/server/cloud-home.js +218 -0
  78. package/dist-server/server/cloud-overflow.js +167 -0
  79. package/dist-server/server/composio.js +5 -0
  80. package/dist-server/server/computer-observation.js +2 -2
  81. package/dist-server/server/config.js +115 -6
  82. package/dist-server/server/container-computer.js +13 -0
  83. package/dist-server/server/control-client.js +2 -2
  84. package/dist-server/server/delegations.js +11 -3
  85. package/dist-server/server/drivers/acp/core.js +5 -4
  86. package/dist-server/server/drivers/agents-call.js +5 -1
  87. package/dist-server/server/drivers/agents-catalog.js +9 -3
  88. package/dist-server/server/drivers/{boxagent.js → boatagent.js} +13 -12
  89. package/dist-server/server/drivers/builtIn.js +2 -2
  90. package/dist-server/server/drivers/{chat-box-tools.js → chat-boat-tools.js} +3 -3
  91. package/dist-server/server/drivers/chat-mcp-tools.js +2 -2
  92. package/dist-server/server/drivers/claude.js +177 -13
  93. package/dist-server/server/drivers/native.js +1 -1
  94. package/dist-server/server/drivers/openai-chat.js +15 -4
  95. package/dist-server/server/drivers/pi.js +3 -3
  96. package/dist-server/server/hosted-models.js +6 -1
  97. package/dist-server/server/index.js +1029 -479
  98. package/dist-server/server/local-vm-inventory.js +11 -1
  99. package/dist-server/server/local-vm-seat-pool.js +141 -0
  100. package/dist-server/server/managed-policy.js +4 -1
  101. package/dist-server/server/memory-capture.js +194 -0
  102. package/dist-server/server/memory-entries.js +76 -0
  103. package/dist-server/server/memory-organize.js +102 -0
  104. package/dist-server/server/memory-tidy.js +144 -0
  105. package/dist-server/server/memory-topics.js +169 -0
  106. package/dist-server/server/memory-upkeep.js +452 -0
  107. package/dist-server/server/message-db.js +49 -5
  108. package/dist-server/server/permission-proxy.js +3 -3
  109. package/dist-server/server/profile-learned.js +98 -0
  110. package/dist-server/server/profile-versions.js +45 -11
  111. package/dist-server/server/recall.js +172 -0
  112. package/dist-server/server/redact.js +1 -1
  113. package/dist-server/server/room-cwd.js +1 -1
  114. package/dist-server/server/room-handoffs.js +61 -6
  115. package/dist-server/server/routes/bot-memory.js +215 -0
  116. package/dist-server/server/routine-requests.js +27 -16
  117. package/dist-server/server/routines.js +10 -6
  118. package/dist-server/server/steer-queue.js +41 -10
  119. package/dist-server/server/store.js +25 -1
  120. package/dist-server/server/surface.js +1 -1
  121. package/dist-server/server/system-prompt.js +5 -5
  122. package/dist-server/server/team-computers.js +2 -2
  123. package/dist-server/server/tightening-requests.js +34 -4
  124. package/dist-server/server/tool-summary.js +2 -2
  125. package/dist-server/server/tts/elevenlabs.js +1 -1
  126. package/dist-server/server/turn-resources.js +87 -11
  127. package/dist-server/server/vps-computer.js +1 -1
  128. package/dist-server/server/workspace.js +57 -22
  129. package/dist-server/shared/ask-question.js +7 -5
  130. package/dist-server/shared/bot-avatar.js +19 -0
  131. package/dist-server/shared/credential-request.js +3 -3
  132. package/dist-server/shared/tool-surface.js +1 -1
  133. package/dist-server/src/lib/composer-attachments.js +9 -0
  134. package/dist-server/vps-container-mcp.js +52 -4
  135. package/dist-server/workspace-backup.worker.js +52 -4
  136. package/package.json +1 -1
  137. package/dist/assets/channel-DqY_gMLg.js +0 -1
  138. package/dist/assets/index-BIMdIbaw.css +0 -1
  139. package/dist/assets/index-BVPdbq2r.js +0 -583
  140. package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +0 -8
@@ -0,0 +1,144 @@
1
+ // The nightly tidy-up of MEMORY.md, for a bot with Memory upkeep on. Three
2
+ // steps, in order, each leaving a trace:
3
+ // 1. expired — a live entry past its `until` day moves to memory/archive.md
4
+ // 2. duplicate — of two entries with the same fact identity (signs,
5
+ // symbols and case kept), the newest stays and older copies go
6
+ // 3. contradicted — one strict-JSON model call over what is left names
7
+ // pairs that cannot both be true; the loser is struck through with a
8
+ // date, never deleted, and at most floor(live × 0.2) of them change in
9
+ // one pass — none at all below five entries.
10
+ // Hand-written lines without a date are never touched. Pure here: the
11
+ // caller reads, calls the model, writes and journals.
12
+ import { factIdentity, isExpired, parseMemoryEntries } from "./memory-entries.js";
13
+ export const CONTRADICTION_SHARE = 0.2;
14
+ export const TIDY_MARKER = "You are the TIDY step of a memory system";
15
+ const PROMPT_ENTRIES = 200;
16
+ /** The newest-wins duplicates among live, unexpired entries. */
17
+ function duplicatesOf(live) {
18
+ const byKey = new Map();
19
+ for (const entry of live) {
20
+ const key = factIdentity(entry.body);
21
+ if (!key)
22
+ continue;
23
+ byKey.set(key, [...(byKey.get(key) ?? []), entry]);
24
+ }
25
+ const older = [];
26
+ for (const group of byKey.values()) {
27
+ if (group.length < 2)
28
+ continue;
29
+ // newest date first; on the same day the later line wins
30
+ const sorted = [...group].sort((x, y) => (x.date === y.date ? y.line - x.line : x.date < y.date ? 1 : -1));
31
+ older.push(...sorted.slice(1));
32
+ }
33
+ return older;
34
+ }
35
+ /** What survives steps 1 and 2: the list the contradiction call is asked
36
+ * about, so two copies of a fact beside its correction never confuse it. */
37
+ export function contradictionCandidates(text, today) {
38
+ const live = parseMemoryEntries(text).filter((e) => !e.struck && !isExpired(e, today));
39
+ const dupes = new Set(duplicatesOf(live).map((e) => e.line));
40
+ return live.filter((e) => !dupes.has(e.line));
41
+ }
42
+ /** How many contradictions one pass may act on. */
43
+ export function contradictionBudget(liveEntries) {
44
+ return Math.floor(liveEntries * CONTRADICTION_SHARE);
45
+ }
46
+ export function planTidy(text, today, contradictions = []) {
47
+ const entries = parseMemoryEntries(text);
48
+ const expired = entries.filter((e) => isExpired(e, today));
49
+ const live = entries.filter((e) => !e.struck && !isExpired(e, today));
50
+ const duplicates = duplicatesOf(live);
51
+ const candidates = contradictionCandidates(text, today);
52
+ const budget = contradictionBudget(live.length);
53
+ const touched = new Set();
54
+ const superseded = [];
55
+ let deferred = 0;
56
+ for (const pair of contradictions) {
57
+ const a = candidates[pair.a];
58
+ const b = candidates[pair.b];
59
+ if (!a || !b || a.line === b.line)
60
+ continue;
61
+ const [winner, loser] = pair.keep === "a" ? [a, b] : [b, a];
62
+ if (touched.has(winner.line) || touched.has(loser.line))
63
+ continue;
64
+ if (superseded.length >= budget) {
65
+ deferred += 1;
66
+ continue;
67
+ }
68
+ superseded.push({ loser, winner, ...(pair.remainder ? { remainder: pair.remainder } : {}) });
69
+ touched.add(loser.line);
70
+ touched.add(winner.line);
71
+ }
72
+ return { expired, duplicates, superseded, deferred };
73
+ }
74
+ export function planChanges(plan) {
75
+ return plan.expired.length + plan.duplicates.length + plan.superseded.length;
76
+ }
77
+ /** MEMORY.md after the plan, and the lines the archive gains. */
78
+ export function applyTidy(text, plan, today) {
79
+ const lines = text.split("\n");
80
+ const drop = new Set([...plan.expired, ...plan.duplicates].map((e) => e.line));
81
+ for (const { loser, remainder } of plan.superseded) {
82
+ const struck = `${loser.prefix}~~${loser.body}~~ · superseded ${today}`;
83
+ // the still-true part of a mixed line lives on as its own dated entry
84
+ lines[loser.line] = remainder ? `${struck}\n- ${today} · from tidy-up · ${remainder}` : struck;
85
+ }
86
+ const archived = plan.expired.map((e) => `${e.raw} · expired ${today}`);
87
+ return { text: lines.filter((_, index) => !drop.has(index)).join("\n"), archived };
88
+ }
89
+ export function contradictionPrompt(candidates) {
90
+ const listed = candidates.slice(0, PROMPT_ENTRIES).map((e, index) => `[${index}] (${e.date}) ${e.body}`).join("\n");
91
+ return [
92
+ `${TIDY_MARKER}. You have no tools. Read only the numbered notebook lines below, each with the date it was written.`,
93
+ "",
94
+ listed || "(nothing)",
95
+ "",
96
+ "Find pairs of lines that cannot both be true now: a direct contradiction about the same thing (a changed number, a new city, a reversed preference) — not a refinement, not two different topics, not two facts that can both hold.",
97
+ "Numbers, signs and symbols matter: \"balance is -10\" and \"balance is 10\" contradict; \"uses C\" and \"uses C++\" may both be true.",
98
+ "For each pair say which to keep: the later line unless the earlier one is clearly the correction.",
99
+ "When the line that loses also states other facts that are still true, give them as remainder, one sentence, worded as in the line; otherwise omit remainder.",
100
+ "Answer with one JSON object and nothing else:",
101
+ '{"pairs": [{"a": <index>, "b": <index>, "keep": "a"|"b", "remainder": "optional"}]}',
102
+ 'An empty list {"pairs": []} is the normal answer. Never invent a contradiction.',
103
+ ].join("\n");
104
+ }
105
+ export function parseContradictions(text, count) {
106
+ const trimmed = text.trim();
107
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(trimmed);
108
+ const candidate = (fenced ? fenced[1] : trimmed).trim();
109
+ const start = candidate.indexOf("{");
110
+ const end = candidate.lastIndexOf("}");
111
+ if (start < 0 || end <= start)
112
+ return [];
113
+ let parsed;
114
+ try {
115
+ parsed = JSON.parse(candidate.slice(start, end + 1));
116
+ }
117
+ catch {
118
+ return [];
119
+ }
120
+ if (!Array.isArray(parsed.pairs))
121
+ return [];
122
+ const out = [];
123
+ const seen = new Set();
124
+ for (const pair of parsed.pairs) {
125
+ if (!pair || typeof pair !== "object")
126
+ continue;
127
+ const { a, b, keep, remainder } = pair;
128
+ if (!Number.isInteger(a) || !Number.isInteger(b) || a === b)
129
+ continue;
130
+ const ia = a;
131
+ const ib = b;
132
+ if (ia < 0 || ib < 0 || ia >= count || ib >= count)
133
+ continue;
134
+ if (keep !== "a" && keep !== "b")
135
+ continue;
136
+ const key = `${Math.min(ia, ib)}:${Math.max(ia, ib)}`;
137
+ if (seen.has(key))
138
+ continue;
139
+ seen.add(key);
140
+ const rest = typeof remainder === "string" ? remainder.replace(/\s+/g, " ").replace(/~~/g, "").trim().slice(0, 300) : "";
141
+ out.push({ a: ia, b: ib, keep, ...(rest ? { remainder: rest } : {}) });
142
+ }
143
+ return out;
144
+ }
@@ -0,0 +1,169 @@
1
+ // The topic index: one line per memory/<topic>.md, so a bot knows what its
2
+ // longer notes cover without a pointer in MEMORY.md. A topic may begin with
3
+ // YAML frontmatter (`title`, `description`, `aliases`); aliases are the other
4
+ // words someone might search with, which matters because every lookup here
5
+ // is word matching, not meaning. Only the head of each file is read.
6
+ import { closeSync, openSync, readSync } from "node:fs";
7
+ /** How much of a topic file is read to find its frontmatter. */
8
+ const HEAD_BYTES = 2_048;
9
+ export const TOPIC_INDEX_MAX_TOPICS = 40;
10
+ export const TOPIC_INDEX_MAX_CHARS = 2_000;
11
+ function unquote(value) {
12
+ const v = value.trim();
13
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")))
14
+ return v.slice(1, -1).trim();
15
+ return v;
16
+ }
17
+ /** The frontmatter fields the index uses. Anything else, or no frontmatter,
18
+ * reads as an empty header — a hand-written topic is still listed by name. */
19
+ export function parseTopicHeader(text) {
20
+ const header = { aliases: [] };
21
+ // People write a "# Title" first and the block under it (the panel's own
22
+ // new-topic text used to), so a heading and blank lines may come before the
23
+ // block; the heading is the title when the block names none.
24
+ const lead = /^(?:\s*\r?\n|#{1,6}[ \t]+[^\r\n]*\r?\n)*/.exec(text)?.[0] ?? "";
25
+ const heading = /^#{1,6}[ \t]+([^\r\n]+)/m.exec(lead)?.[1]?.trim();
26
+ const m = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text.slice(lead.length));
27
+ if (!m) {
28
+ if (heading)
29
+ header.title = heading.slice(0, 160);
30
+ return header;
31
+ }
32
+ const lines = m[1].split(/\r?\n/);
33
+ for (let i = 0; i < lines.length; i += 1) {
34
+ const field = /^(title|description|aliases|tags)\s*:\s*(.*)$/.exec(lines[i]);
35
+ if (!field)
36
+ continue;
37
+ const [, key, value] = field;
38
+ if (key === "title" || key === "description") {
39
+ if (value.trim())
40
+ header[key] = unquote(value).slice(0, 160);
41
+ continue;
42
+ }
43
+ // aliases (and tags, which people use the same way): [a, b] or a list below
44
+ const items = [];
45
+ if (value.trim().startsWith("[")) {
46
+ items.push(...value.trim().replace(/^\[|\]$/g, "").split(","));
47
+ }
48
+ else if (value.trim()) {
49
+ items.push(...value.split(","));
50
+ }
51
+ else {
52
+ while (i + 1 < lines.length && /^\s*-\s+/.test(lines[i + 1]))
53
+ items.push(lines[(i += 1)].replace(/^\s*-\s+/, ""));
54
+ }
55
+ for (const item of items.map(unquote).filter(Boolean)) {
56
+ if (!header.aliases.includes(item))
57
+ header.aliases.push(item.slice(0, 40));
58
+ }
59
+ }
60
+ header.aliases = header.aliases.slice(0, 12);
61
+ if (!header.title && heading)
62
+ header.title = heading.slice(0, 160);
63
+ return header;
64
+ }
65
+ /** The words a topic answers to: its file name, title, description and
66
+ * aliases, lower-cased, for matching a message against without a search. */
67
+ export function topicWords(name, header) {
68
+ const text = [name.replace(/\.md$/i, "").replace(/[-_.]+/g, " "), header.title ?? "", header.description ?? "", ...header.aliases].join(" ");
69
+ return [...new Set(text.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((word) => word.length >= 3))];
70
+ }
71
+ /** A topic's text without its header block, for a recalled passage. */
72
+ export function topicBody(text) {
73
+ const lead = /^(?:\s*\r?\n|#{1,6}[ \t]+[^\r\n]*\r?\n)*/.exec(text)?.[0] ?? "";
74
+ const rest = text.slice(lead.length).replace(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/, "");
75
+ return rest.trim();
76
+ }
77
+ export function readTopicHead(path) {
78
+ let fd;
79
+ try {
80
+ fd = openSync(path, "r");
81
+ const buffer = Buffer.alloc(HEAD_BYTES);
82
+ const read = readSync(fd, buffer, 0, HEAD_BYTES, 0);
83
+ return buffer.subarray(0, read).toString("utf8");
84
+ }
85
+ catch {
86
+ return "";
87
+ }
88
+ finally {
89
+ if (fd !== undefined)
90
+ closeSync(fd);
91
+ }
92
+ }
93
+ /** One line per topic, capped by count and length. The archive the tidy-up
94
+ * keeps is listed last and labelled, so it is not mistaken for current notes. */
95
+ export function renderTopicIndex(topics) {
96
+ if (!topics.length)
97
+ return "";
98
+ const ordered = [...topics].sort((a, b) => Number(a.name === "archive.md") - Number(b.name === "archive.md") || a.name.localeCompare(b.name));
99
+ const lines = [];
100
+ let length = 0;
101
+ let omitted = 0;
102
+ for (const { name, header } of ordered) {
103
+ if (lines.length >= TOPIC_INDEX_MAX_TOPICS) {
104
+ omitted += 1;
105
+ continue;
106
+ }
107
+ const about = name === "archive.md" && !header.title && !header.description
108
+ ? "expired and replaced notes, kept for the record"
109
+ : [header.title, header.description].filter(Boolean).join(" — ");
110
+ const also = header.aliases.length ? ` (also: ${header.aliases.join(", ")})` : "";
111
+ const line = `- memory/${name}${about ? ` — ${about}` : ""}${also}`.replace(/\s+/g, " ");
112
+ if (length + line.length + 1 > TOPIC_INDEX_MAX_CHARS) {
113
+ omitted += 1;
114
+ continue;
115
+ }
116
+ lines.push(line);
117
+ length += line.length + 1;
118
+ }
119
+ if (omitted)
120
+ lines.push(`- …and ${omitted} more in memory/`);
121
+ return lines.join("\n");
122
+ }
123
+ const LEAD = /^(?:\s*\r?\n|#{1,6}[ \t]+[^\r\n]*\r?\n)*/;
124
+ const HEADER_BLOCK = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
125
+ function aliasList(aliases) {
126
+ return `[${aliases.join(", ")}]`;
127
+ }
128
+ /** A topic file with new entry lines appended and new aliases merged into its
129
+ * header — created, header first, when it does not exist yet. The header is
130
+ * what the topic index and recall read, so a topic the bot files for itself
131
+ * is found by the other words it was given. */
132
+ export function mergeTopicText(existing, input) {
133
+ const lines = input.lines.join("\n");
134
+ if (existing === null || !existing.trim()) {
135
+ const aliases = [...new Set(input.aliases)].slice(0, 12);
136
+ return `---\ntitle: ${input.title}\n${aliases.length ? `aliases: ${aliasList(aliases)}\n` : ""}---\n\n${lines}\n`;
137
+ }
138
+ const lead = LEAD.exec(existing)?.[0] ?? "";
139
+ const rest = existing.slice(lead.length);
140
+ const block = HEADER_BLOCK.exec(rest);
141
+ let text;
142
+ if (!block) {
143
+ const aliases = [...new Set(input.aliases)].slice(0, 12);
144
+ text = `---\ntitle: ${parseTopicHeader(existing).title ?? input.title}\n${aliases.length ? `aliases: ${aliasList(aliases)}\n` : ""}---\n\n${existing}`;
145
+ }
146
+ else {
147
+ const known = parseTopicHeader(existing).aliases;
148
+ const lower = new Set(known.map((alias) => alias.toLowerCase()));
149
+ const added = input.aliases.filter((alias) => !lower.has(alias.toLowerCase()));
150
+ if (!added.length)
151
+ text = existing;
152
+ else {
153
+ const merged = [...known, ...added].slice(0, 12);
154
+ const body = block[1].split(/\r?\n/);
155
+ const at = body.findIndex((line) => /^aliases\s*:/.test(line));
156
+ if (at === -1)
157
+ body.push(`aliases: ${aliasList(merged)}`);
158
+ else {
159
+ // a multi-line list under aliases: is replaced by the one-line form
160
+ let end = at + 1;
161
+ while (end < body.length && /^\s*-\s+/.test(body[end]))
162
+ end += 1;
163
+ body.splice(at, end - at, `aliases: ${aliasList(merged)}`);
164
+ }
165
+ text = `${lead}---\n${body.join("\n")}\n---\n${rest.slice(block[0].length)}`;
166
+ }
167
+ }
168
+ return `${text}${text.endsWith("\n") ? "" : "\n"}${lines}\n`;
169
+ }