negotium 0.1.7 → 0.1.8

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 (256) hide show
  1. package/README.md +2 -2
  2. package/dist/chunk-0ay6cjdy.js +1331 -0
  3. package/dist/chunk-0ay6cjdy.js.map +16 -0
  4. package/dist/chunk-1k7t7bnx.js +97 -0
  5. package/dist/chunk-1k7t7bnx.js.map +10 -0
  6. package/dist/chunk-5ahkew9c.js +806 -0
  7. package/dist/chunk-5ahkew9c.js.map +17 -0
  8. package/dist/chunk-6cpn7ht2.js +287 -0
  9. package/dist/chunk-6cpn7ht2.js.map +13 -0
  10. package/dist/chunk-6kctn20e.js +3529 -0
  11. package/dist/chunk-6kctn20e.js.map +20 -0
  12. package/dist/chunk-7xqj7aex.js +77 -0
  13. package/dist/chunk-7xqj7aex.js.map +11 -0
  14. package/dist/chunk-9p0ww3tk.js +41 -0
  15. package/dist/chunk-9p0ww3tk.js.map +10 -0
  16. package/dist/chunk-a92a7nrk.js +252 -0
  17. package/dist/chunk-a92a7nrk.js.map +10 -0
  18. package/dist/chunk-c500xe9z.js +33 -0
  19. package/dist/chunk-c500xe9z.js.map +10 -0
  20. package/dist/chunk-cds1qxsf.js +1644 -0
  21. package/dist/chunk-cds1qxsf.js.map +17 -0
  22. package/dist/chunk-e4407g8x.js +274 -0
  23. package/dist/chunk-e4407g8x.js.map +10 -0
  24. package/dist/chunk-e5pbggsh.js +363 -0
  25. package/dist/chunk-e5pbggsh.js.map +11 -0
  26. package/dist/chunk-edb07sjp.js +567 -0
  27. package/dist/chunk-edb07sjp.js.map +14 -0
  28. package/dist/chunk-exd0v74a.js +3012 -0
  29. package/dist/chunk-exd0v74a.js.map +32 -0
  30. package/dist/chunk-fgrme9tr.js +4398 -0
  31. package/dist/chunk-fgrme9tr.js.map +47 -0
  32. package/dist/chunk-fka9eqe1.js +581 -0
  33. package/dist/chunk-fka9eqe1.js.map +11 -0
  34. package/dist/chunk-g82jygez.js +108 -0
  35. package/dist/chunk-g82jygez.js.map +10 -0
  36. package/dist/chunk-ghs2j1fr.js +312 -0
  37. package/dist/chunk-ghs2j1fr.js.map +10 -0
  38. package/dist/chunk-gmy11efb.js +125 -0
  39. package/dist/chunk-gmy11efb.js.map +10 -0
  40. package/dist/chunk-gxma4c70.js +110 -0
  41. package/dist/chunk-gxma4c70.js.map +10 -0
  42. package/dist/chunk-hkdqhm34.js +61 -0
  43. package/dist/chunk-hkdqhm34.js.map +10 -0
  44. package/dist/chunk-hpa0cpzh.js +134 -0
  45. package/dist/chunk-hpa0cpzh.js.map +10 -0
  46. package/dist/chunk-hwy3n31w.js +21 -0
  47. package/dist/chunk-hwy3n31w.js.map +9 -0
  48. package/dist/chunk-j2behpy7.js +214 -0
  49. package/dist/chunk-j2behpy7.js.map +10 -0
  50. package/dist/chunk-jc0at0tc.js +102 -0
  51. package/dist/chunk-jc0at0tc.js.map +10 -0
  52. package/dist/chunk-jkqq5ef6.js +243 -0
  53. package/dist/chunk-jkqq5ef6.js.map +11 -0
  54. package/dist/chunk-jzkw2f01.js +51 -0
  55. package/dist/chunk-jzkw2f01.js.map +10 -0
  56. package/dist/chunk-jzz91jwh.js +70 -0
  57. package/dist/chunk-jzz91jwh.js.map +10 -0
  58. package/dist/chunk-k9z47mab.js +247 -0
  59. package/dist/chunk-k9z47mab.js.map +12 -0
  60. package/dist/chunk-n17yxpa8.js +23 -0
  61. package/dist/chunk-n17yxpa8.js.map +10 -0
  62. package/dist/chunk-naa09ags.js +2143 -0
  63. package/dist/chunk-naa09ags.js.map +15 -0
  64. package/dist/chunk-ns3ke88n.js +142 -0
  65. package/dist/chunk-ns3ke88n.js.map +10 -0
  66. package/dist/chunk-p3cr2ejg.js +1626 -0
  67. package/dist/chunk-p3cr2ejg.js.map +15 -0
  68. package/dist/chunk-q5bcpf23.js +2925 -0
  69. package/dist/chunk-q5bcpf23.js.map +20 -0
  70. package/dist/chunk-rafc55bs.js +424 -0
  71. package/dist/chunk-rafc55bs.js.map +11 -0
  72. package/dist/chunk-rbrxjvbj.js +2830 -0
  73. package/dist/chunk-rbrxjvbj.js.map +23 -0
  74. package/dist/chunk-sa874j5c.js +9 -0
  75. package/dist/chunk-sa874j5c.js.map +10 -0
  76. package/dist/chunk-sgsz2fpq.js +289 -0
  77. package/dist/chunk-sgsz2fpq.js.map +10 -0
  78. package/dist/chunk-sm6fh5c5.js +569 -0
  79. package/dist/chunk-sm6fh5c5.js.map +14 -0
  80. package/dist/chunk-t9zcg8f1.js +499 -0
  81. package/dist/chunk-t9zcg8f1.js.map +13 -0
  82. package/dist/chunk-vp2db42d.js +121 -0
  83. package/dist/chunk-vp2db42d.js.map +10 -0
  84. package/dist/chunk-w13jjf4a.js +46 -0
  85. package/dist/chunk-w13jjf4a.js.map +10 -0
  86. package/dist/chunk-x1arr7ze.js +63 -0
  87. package/dist/chunk-x1arr7ze.js.map +10 -0
  88. package/dist/chunk-xfq68rch.js +583 -0
  89. package/dist/chunk-xfq68rch.js.map +11 -0
  90. package/dist/main.js +132 -20
  91. package/dist/main.js.map +4 -4
  92. package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
  93. package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
  94. package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
  95. package/dist/runtime/src/agents/archiver.ts +396 -0
  96. package/dist/runtime/src/agents/auth-check.ts +64 -0
  97. package/dist/runtime/src/agents/claude-provider.ts +737 -0
  98. package/dist/runtime/src/agents/claude-registry.ts +115 -0
  99. package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
  100. package/dist/runtime/src/agents/codex-provider.ts +785 -0
  101. package/dist/runtime/src/agents/codex-registry.ts +115 -0
  102. package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
  103. package/dist/runtime/src/agents/contracts.ts +69 -0
  104. package/dist/runtime/src/agents/deep-map.ts +13 -0
  105. package/dist/runtime/src/agents/execution-host.ts +91 -0
  106. package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
  107. package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
  108. package/dist/runtime/src/agents/fork.ts +65 -0
  109. package/dist/runtime/src/agents/hosted-agent.ts +48 -0
  110. package/dist/runtime/src/agents/idle-archiver.ts +128 -0
  111. package/dist/runtime/src/agents/index.ts +205 -0
  112. package/dist/runtime/src/agents/maestro-provider.ts +206 -0
  113. package/dist/runtime/src/agents/maestro-registry.ts +31 -0
  114. package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
  115. package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
  116. package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
  117. package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
  118. package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
  119. package/dist/runtime/src/agents/model-catalog.ts +151 -0
  120. package/dist/runtime/src/agents/registry.ts +24 -0
  121. package/dist/runtime/src/agents/rollout/claude.ts +331 -0
  122. package/dist/runtime/src/agents/rollout/codex.ts +569 -0
  123. package/dist/runtime/src/agents/rollout/shared.ts +205 -0
  124. package/dist/runtime/src/agents/self-config-core.ts +415 -0
  125. package/dist/runtime/src/agents/task-events.ts +44 -0
  126. package/dist/runtime/src/agents/tool-format.ts +352 -0
  127. package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
  128. package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
  129. package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
  130. package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
  131. package/dist/runtime/src/application/submit-user-message.ts +47 -0
  132. package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
  133. package/dist/runtime/src/application/switch-topic-model.ts +79 -0
  134. package/dist/runtime/src/application/topic-service.ts +139 -0
  135. package/dist/runtime/src/application/vault-command.ts +170 -0
  136. package/dist/runtime/src/bus.ts +319 -0
  137. package/dist/runtime/src/index.ts +333 -0
  138. package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
  139. package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
  140. package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
  141. package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
  142. package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
  143. package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
  144. package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
  145. package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
  146. package/dist/runtime/src/mcp/runtime-spec.ts +134 -0
  147. package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
  148. package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
  149. package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
  150. package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
  151. package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
  152. package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
  153. package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
  154. package/dist/runtime/src/mcp/system-health-server.ts +130 -0
  155. package/dist/runtime/src/mcp/task-server.ts +173 -0
  156. package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
  157. package/dist/runtime/src/mcp/vault-http.ts +219 -0
  158. package/dist/runtime/src/mcp/vault-run.ts +153 -0
  159. package/dist/runtime/src/mcp/vault-server.ts +104 -0
  160. package/dist/runtime/src/mcp/wiki-server.ts +755 -0
  161. package/dist/runtime/src/media/file-events.ts +33 -0
  162. package/dist/runtime/src/media/text-extractor.ts +312 -0
  163. package/dist/runtime/src/media/video.ts +92 -0
  164. package/dist/runtime/src/outbox/file-ops.ts +210 -0
  165. package/dist/runtime/src/outbox/index.ts +8 -0
  166. package/dist/runtime/src/outbox/utils.ts +51 -0
  167. package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
  168. package/dist/runtime/src/platform/config.ts +371 -0
  169. package/dist/runtime/src/platform/constants.ts +18 -0
  170. package/dist/runtime/src/platform/debug-users.ts +14 -0
  171. package/dist/runtime/src/platform/delay.ts +1 -0
  172. package/dist/runtime/src/platform/error.ts +9 -0
  173. package/dist/runtime/src/platform/file-utils.ts +16 -0
  174. package/dist/runtime/src/platform/jsonl.ts +202 -0
  175. package/dist/runtime/src/platform/lifecycle.ts +174 -0
  176. package/dist/runtime/src/platform/logger.ts +31 -0
  177. package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
  178. package/dist/runtime/src/platform/mcp-config.ts +865 -0
  179. package/dist/runtime/src/platform/modules.ts +145 -0
  180. package/dist/runtime/src/platform/node-plugins.ts +46 -0
  181. package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
  182. package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
  183. package/dist/runtime/src/prompts/builders.ts +357 -0
  184. package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
  185. package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
  186. package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
  187. package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
  188. package/dist/runtime/src/query/active-rooms.ts +532 -0
  189. package/dist/runtime/src/query/control.ts +313 -0
  190. package/dist/runtime/src/query/index.ts +24 -0
  191. package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
  192. package/dist/runtime/src/query/session-inbox-path.ts +42 -0
  193. package/dist/runtime/src/query/state.ts +94 -0
  194. package/dist/runtime/src/query/types.ts +83 -0
  195. package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
  196. package/dist/runtime/src/runtime/attachments.ts +151 -0
  197. package/dist/runtime/src/runtime/background-sessions.ts +83 -0
  198. package/dist/runtime/src/runtime/channel-context.ts +107 -0
  199. package/dist/runtime/src/runtime/errors.ts +69 -0
  200. package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
  201. package/dist/runtime/src/runtime/file-hooks.ts +81 -0
  202. package/dist/runtime/src/runtime/footer.ts +23 -0
  203. package/dist/runtime/src/runtime/inbox.ts +946 -0
  204. package/dist/runtime/src/runtime/tasks.ts +56 -0
  205. package/dist/runtime/src/runtime/topic-config.ts +29 -0
  206. package/dist/runtime/src/runtime/turn-runner.ts +2286 -0
  207. package/dist/runtime/src/runtime/usage-alert.ts +84 -0
  208. package/dist/runtime/src/runtime/visual-html.ts +109 -0
  209. package/dist/runtime/src/runtime/visual-store.ts +430 -0
  210. package/dist/runtime/src/runtime/visuals.ts +171 -0
  211. package/dist/runtime/src/security/sanitize.ts +27 -0
  212. package/dist/runtime/src/security/sensitive-path.ts +34 -0
  213. package/dist/runtime/src/storage/activity-log.ts +152 -0
  214. package/dist/runtime/src/storage/api-messages.ts +617 -0
  215. package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
  216. package/dist/runtime/src/storage/api-topic-config.ts +192 -0
  217. package/dist/runtime/src/storage/api-topics.ts +844 -0
  218. package/dist/runtime/src/storage/app-settings.ts +44 -0
  219. package/dist/runtime/src/storage/browser-profiles.ts +133 -0
  220. package/dist/runtime/src/storage/conversations.ts +268 -0
  221. package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
  222. package/dist/runtime/src/storage/forum/index.ts +4 -0
  223. package/dist/runtime/src/storage/forum/repository.ts +283 -0
  224. package/dist/runtime/src/storage/forum/schema.ts +300 -0
  225. package/dist/runtime/src/storage/forum-db.ts +23 -0
  226. package/dist/runtime/src/storage/runtime-events.ts +130 -0
  227. package/dist/runtime/src/storage/runtime-leases.ts +189 -0
  228. package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
  229. package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
  230. package/dist/runtime/src/storage/runtime-turn-requests.ts +300 -0
  231. package/dist/runtime/src/storage/self-schedules.ts +298 -0
  232. package/dist/runtime/src/storage/session-asks.ts +347 -0
  233. package/dist/runtime/src/storage/sqlite.ts +129 -0
  234. package/dist/runtime/src/storage/tasks.ts +179 -0
  235. package/dist/runtime/src/storage/token-stats.ts +144 -0
  236. package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
  237. package/dist/runtime/src/storage/topic-archive.ts +74 -0
  238. package/dist/runtime/src/storage/topic-settings.ts +67 -0
  239. package/dist/runtime/src/storage/topic-transcript.ts +98 -0
  240. package/dist/runtime/src/storage/vault-crypto.ts +65 -0
  241. package/dist/runtime/src/storage/vault.ts +225 -0
  242. package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
  243. package/dist/runtime/src/storage/wiki.ts +85 -0
  244. package/dist/runtime/src/topics/create.ts +123 -0
  245. package/dist/runtime/src/topics/derive.ts +398 -0
  246. package/dist/runtime/src/topics/lifecycle.ts +311 -0
  247. package/dist/runtime/src/topics/links.ts +7 -0
  248. package/dist/runtime/src/topics/personal-general.ts +65 -0
  249. package/dist/runtime/src/topics/session.ts +381 -0
  250. package/dist/runtime/src/types/api.ts +497 -0
  251. package/dist/runtime/src/types.ts +255 -0
  252. package/dist/runtime/src/version.ts +1 -0
  253. package/dist/runtime/tsconfig.json +13 -0
  254. package/package.json +16 -3
  255. package/dist/main.d.ts +0 -3
  256. package/dist/main.d.ts.map +0 -1
@@ -0,0 +1,225 @@
1
+ import { chmodSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { DATA_DIR } from "#platform/config";
4
+ import { Database } from "#storage/sqlite";
5
+ import { decryptVaultValue, encryptVaultValue } from "#storage/vault-crypto";
6
+
7
+ const VAULT_DB_PATH = join(DATA_DIR, "vault.db");
8
+ mkdirSync(DATA_DIR, { recursive: true });
9
+
10
+ const vaultDb = new Database(VAULT_DB_PATH, { create: true });
11
+ // Defense in depth: the database and its sidecars are owner-only, while each
12
+ // value is also encrypted independently by vault-crypto.
13
+ chmodSync(VAULT_DB_PATH, 0o600);
14
+ vaultDb.exec("PRAGMA journal_mode = WAL");
15
+ vaultDb.exec("PRAGMA busy_timeout = 5000");
16
+ vaultDb.exec(`
17
+ CREATE TABLE IF NOT EXISTS vault (
18
+ user_id TEXT NOT NULL,
19
+ key TEXT NOT NULL,
20
+ value TEXT NOT NULL,
21
+ description TEXT NOT NULL DEFAULT '',
22
+ PRIMARY KEY (user_id, key)
23
+ )
24
+ `);
25
+
26
+ // Migrate legacy schema where user_id was INTEGER (Telegram numeric ids only).
27
+ // The Otium API authenticates with string user ids ("otium-user-…"), so user_id
28
+ // is now TEXT for every channel. Rebuild + CAST existing rows so old Telegram
29
+ // secrets survive; idempotent (no-op once user_id is already TEXT).
30
+ {
31
+ const cols = vaultDb.prepare("PRAGMA table_info(vault)").all() as {
32
+ name: string;
33
+ type: string;
34
+ }[];
35
+ const uid = cols.find((c) => c.name === "user_id");
36
+ if (uid && uid.type.toUpperCase() === "INTEGER") {
37
+ vaultDb.exec("BEGIN");
38
+ vaultDb.exec(`
39
+ CREATE TABLE vault_migrated (
40
+ user_id TEXT NOT NULL,
41
+ key TEXT NOT NULL,
42
+ value TEXT NOT NULL,
43
+ description TEXT NOT NULL DEFAULT '',
44
+ PRIMARY KEY (user_id, key)
45
+ )
46
+ `);
47
+ vaultDb.exec(
48
+ "INSERT INTO vault_migrated SELECT CAST(user_id AS TEXT), key, value, description FROM vault",
49
+ );
50
+ vaultDb.exec("DROP TABLE vault");
51
+ vaultDb.exec("ALTER TABLE vault_migrated RENAME TO vault");
52
+ vaultDb.exec("COMMIT");
53
+ }
54
+ }
55
+
56
+ export interface VaultEntry {
57
+ key: string;
58
+ description: string;
59
+ }
60
+
61
+ export interface VaultEntryWithValue extends VaultEntry {
62
+ value: string;
63
+ }
64
+
65
+ export const VAULT_KEY_PATTERN = /^[A-Z][A-Z0-9_]{0,127}$/;
66
+ export const VAULT_VALUE_MIN_BYTES = 4;
67
+ export const VAULT_VALUE_MAX_BYTES = 64 * 1024;
68
+ export const VAULT_DESCRIPTION_MAX_LENGTH = 500;
69
+
70
+ export function normalizeVaultKey(key: string): string {
71
+ return key.trim().toUpperCase();
72
+ }
73
+
74
+ export function validateVaultKey(key: string): boolean {
75
+ return VAULT_KEY_PATTERN.test(normalizeVaultKey(key));
76
+ }
77
+
78
+ function decryptRow(userId: string, key: string, storedValue: string): string {
79
+ const decoded = decryptVaultValue(userId, key, storedValue);
80
+ if (decoded.legacyPlaintext) {
81
+ vaultDb
82
+ .prepare("UPDATE vault SET value = ? WHERE user_id = ? AND key = ? AND value = ?")
83
+ .run(encryptVaultValue(userId, key, decoded.value), userId, key, storedValue);
84
+ }
85
+ return decoded.value;
86
+ }
87
+
88
+ export function vaultListWithValues(userId: string): VaultEntryWithValue[] {
89
+ const rows = vaultDb
90
+ .prepare("SELECT key, description, value FROM vault WHERE user_id = ? ORDER BY key")
91
+ .all(userId) as VaultEntryWithValue[];
92
+ return rows.map((row) => ({
93
+ key: row.key,
94
+ description: row.description,
95
+ value: decryptRow(userId, row.key, row.value),
96
+ }));
97
+ }
98
+
99
+ export function vaultSet(userId: string, key: string, value: string, description = ""): void {
100
+ const normalizedKey = normalizeVaultKey(key);
101
+ vaultDb
102
+ .prepare(
103
+ `INSERT INTO vault (user_id, key, value, description) VALUES (?, ?, ?, ?)
104
+ ON CONFLICT (user_id, key) DO UPDATE
105
+ SET value = excluded.value, description = excluded.description`,
106
+ )
107
+ .run(userId, normalizedKey, encryptVaultValue(userId, normalizedKey, value), description);
108
+ }
109
+
110
+ export function vaultHasKey(userId: string, key: string): boolean {
111
+ const row = vaultDb
112
+ .prepare("SELECT 1 FROM vault WHERE user_id = ? AND key = ?")
113
+ .get(userId, normalizeVaultKey(key));
114
+ return row != null;
115
+ }
116
+
117
+ export function vaultDel(userId: string, key: string): boolean {
118
+ const result = vaultDb
119
+ .prepare("DELETE FROM vault WHERE user_id = ? AND key = ?")
120
+ .run(userId, normalizeVaultKey(key)) as { changes: number };
121
+ return result.changes > 0;
122
+ }
123
+
124
+ export function vaultDeleteAllForUser(userId: string): number {
125
+ const result = vaultDb.prepare("DELETE FROM vault WHERE user_id = ?").run(userId) as {
126
+ changes: number;
127
+ };
128
+ return result.changes;
129
+ }
130
+
131
+ export function vaultList(userId: string): VaultEntry[] {
132
+ return vaultDb
133
+ .prepare("SELECT key, description FROM vault WHERE user_id = ? ORDER BY key")
134
+ .all(userId) as VaultEntry[];
135
+ }
136
+
137
+ export function vaultGetValue(userId: string, key: string): string | undefined {
138
+ const normalizedKey = normalizeVaultKey(key);
139
+ const row = vaultDb
140
+ .prepare("SELECT key, value FROM vault WHERE user_id = ? AND key = ?")
141
+ .get(userId, normalizedKey) as { key: string; value: string } | undefined;
142
+ return row ? decryptRow(userId, row.key, row.value) : undefined;
143
+ }
144
+
145
+ export interface VaultSubstitutionResult {
146
+ text: string;
147
+ usedKeys: string[];
148
+ }
149
+
150
+ /** Replace {{KEY}} placeholders and report which credentials were consumed. */
151
+ export function vaultSubstituteDetailed(userId: string, text: string): VaultSubstitutionResult {
152
+ const entries = new Map(vaultListWithValues(userId).map((entry) => [entry.key, entry.value]));
153
+ const usedKeys = new Set<string>();
154
+ const substituted = text.replace(/\{\{([^}]+)\}\}/g, (match, rawKey: string) => {
155
+ const key = normalizeVaultKey(rawKey);
156
+ const value = entries.get(key);
157
+ if (value === undefined) return match;
158
+ usedKeys.add(key);
159
+ return value;
160
+ });
161
+ return { text: substituted, usedKeys: [...usedKeys] };
162
+ }
163
+
164
+ /** Detect real {{KEY}} references without decrypting any Vault values. */
165
+ export function valueReferencesVaultKey(userId: string, value: unknown): boolean {
166
+ const keys = new Set(vaultList(userId).map((entry) => entry.key));
167
+ const visit = (candidate: unknown): boolean => {
168
+ if (typeof candidate === "string") {
169
+ for (const match of candidate.matchAll(/\{\{([^}]+)\}\}/g)) {
170
+ if (keys.has(normalizeVaultKey(match[1] ?? ""))) return true;
171
+ }
172
+ return false;
173
+ }
174
+ if (Array.isArray(candidate)) return candidate.some(visit);
175
+ if (candidate && typeof candidate === "object") {
176
+ return Object.values(candidate as Record<string, unknown>).some(visit);
177
+ }
178
+ return false;
179
+ };
180
+ return visit(value);
181
+ }
182
+
183
+ function encodedSecretForms(value: string): string[] {
184
+ const forms = new Set<string>([
185
+ value,
186
+ encodeURIComponent(value),
187
+ Buffer.from(value, "utf8").toString("base64"),
188
+ Buffer.from(value, "utf8").toString("base64url"),
189
+ Buffer.from(value, "utf8").toString("hex"),
190
+ ]);
191
+ forms.delete("");
192
+ return [...forms].sort((a, b) => b.length - a.length);
193
+ }
194
+
195
+ /** Scrub raw and common encoded forms before tool output reaches a model. */
196
+ export function redactVaultSecrets(userId: string, text: string): string {
197
+ const candidates = vaultListWithValues(userId)
198
+ .flatMap((entry) => encodedSecretForms(entry.value).map((form) => ({ form, key: entry.key })))
199
+ .sort((a, b) => b.form.length - a.form.length || a.key.localeCompare(b.key));
200
+ if (candidates.length === 0) return text;
201
+ const candidatesByFirstCharacter = new Map<string, typeof candidates>();
202
+ for (const candidate of candidates) {
203
+ const first = candidate.form[0];
204
+ if (!first) continue;
205
+ const bucket = candidatesByFirstCharacter.get(first) ?? [];
206
+ bucket.push(candidate);
207
+ candidatesByFirstCharacter.set(first, bucket);
208
+ }
209
+
210
+ let redacted = "";
211
+ let offset = 0;
212
+ while (offset < text.length) {
213
+ const match = candidatesByFirstCharacter
214
+ .get(text[offset] ?? "")
215
+ ?.find((candidate) => text.startsWith(candidate.form, offset));
216
+ if (!match) {
217
+ redacted += text[offset];
218
+ offset += 1;
219
+ continue;
220
+ }
221
+ redacted += `[REDACTED:${match.key}]`;
222
+ offset += match.form.length;
223
+ }
224
+ return redacted;
225
+ }
@@ -0,0 +1,33 @@
1
+ const WIKI_SUMMARY_DATE_PREFIX = /^\d{4}-\d{2}-\d{2}-/;
2
+
3
+ export function wikiSummarySlug(value: string): string {
4
+ return value.replaceAll(/[^a-zA-Z0-9가-힣_-]+/g, "-").slice(0, 120) || "_";
5
+ }
6
+
7
+ export function isEphemeralWikiTopicId(topicId: string | undefined): boolean {
8
+ return topicId?.startsWith("__") ?? false;
9
+ }
10
+
11
+ export function wikiSummaryStorageSlug(rawTopic: string, topicId?: string): string {
12
+ return topicId && !isEphemeralWikiTopicId(topicId)
13
+ ? wikiSummarySlug(topicId)
14
+ : wikiSummarySlug(rawTopic);
15
+ }
16
+
17
+ export function wikiBriefStorageKey(rawTopic: string, topicId?: string): string {
18
+ return topicId && !isEphemeralWikiTopicId(topicId) ? topicId : rawTopic;
19
+ }
20
+
21
+ export function wikiSummaryFilename(date: string, rawTopic: string, topicId?: string): string {
22
+ return `${date}-${wikiSummaryStorageSlug(rawTopic, topicId)}.md`;
23
+ }
24
+
25
+ export function isTopicSummaryFile(
26
+ filename: string,
27
+ topicId: string,
28
+ legacyTopicTitle?: string,
29
+ ): boolean {
30
+ if (!WIKI_SUMMARY_DATE_PREFIX.test(filename)) return false;
31
+ if (filename.endsWith(`-${wikiSummarySlug(topicId)}.md`)) return true;
32
+ return legacyTopicTitle ? filename.endsWith(`-${wikiSummarySlug(legacyTopicTitle)}.md`) : false;
33
+ }
@@ -0,0 +1,85 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { SHARED_WIKI_DIR, WORKSPACE_DIR } from "#platform/config";
4
+ import { sanitizeTopicName } from "#security/sanitize";
5
+
6
+ export function getWikiDir(_userId: number, workspaceDir = WORKSPACE_DIR): string {
7
+ return join(workspaceDir, "wiki");
8
+ }
9
+
10
+ /**
11
+ * Shared wiki root. Otium keeps one workspace wiki for every topic and member;
12
+ * filesystem wiki state is not partitioned by user.
13
+ */
14
+ export function getSharedWikiDir(workspaceDir = WORKSPACE_DIR): string {
15
+ return workspaceDir === WORKSPACE_DIR ? SHARED_WIKI_DIR : join(workspaceDir, "wiki");
16
+ }
17
+
18
+ /** Find the most recent wiki/summaries/ file for a given topic. */
19
+ function findLatestSummaryFile(wikiDir: string, safeTopic: string): string | null {
20
+ const summariesDir = join(wikiDir, "summaries");
21
+ if (!existsSync(summariesDir)) return null;
22
+ const files = readdirSync(summariesDir)
23
+ .filter(
24
+ (f) =>
25
+ f.endsWith(".md") &&
26
+ f.match(new RegExp(`^\\d{4}-\\d{2}-\\d{2}-${safeTopic}(\\.md|-\\d+\\.md)$`)) &&
27
+ !f.endsWith("-sent-files.md"),
28
+ )
29
+ .sort()
30
+ .reverse();
31
+ return files.length > 0 ? join(summariesDir, files[0]) : null;
32
+ }
33
+
34
+ /** Return wiki/topic/<name>.md brief path and latest summary file for system prompt injection.
35
+ * Falls back to forkOrigin's brief when the topic's own brief doesn't exist yet.
36
+ * Returns empty memoryFiles if neither brief exists. */
37
+ export function getTopicMemoryFilePaths(
38
+ _userId: number,
39
+ topicName: string,
40
+ forkOrigin?: string,
41
+ workspaceDir = WORKSPACE_DIR,
42
+ ): {
43
+ memoryDir: string;
44
+ memoryFiles: string[];
45
+ latestSummaryFile?: string;
46
+ hasArchive?: boolean;
47
+ } {
48
+ const wikiDir = getSharedWikiDir(workspaceDir);
49
+
50
+ const resolveBrief = (name: string) => {
51
+ const safe = sanitizeTopicName(name, true);
52
+ const brief = join(wikiDir, "topic", `${safe}.md`);
53
+ const latestSummary = findLatestSummaryFile(wikiDir, safe);
54
+ return { brief, latestSummary };
55
+ };
56
+
57
+ const archiveDir = join(wikiDir, "archive");
58
+ const archiveExistsFor = (name: string) => {
59
+ const safe = sanitizeTopicName(name);
60
+ return (
61
+ existsSync(archiveDir) &&
62
+ readdirSync(archiveDir).some((f) => f.endsWith(".jsonl") && f.startsWith(`${safe}_`))
63
+ );
64
+ };
65
+
66
+ const sourceTopic = forkOrigin ?? topicName;
67
+ const hasArchive =
68
+ archiveExistsFor(sourceTopic) || (forkOrigin ? archiveExistsFor(topicName) : false);
69
+ const target = resolveBrief(sourceTopic);
70
+ if (existsSync(target.brief)) {
71
+ return {
72
+ memoryDir: dirname(target.brief),
73
+ memoryFiles: [basename(target.brief)],
74
+ ...(target.latestSummary ? { latestSummaryFile: target.latestSummary } : {}),
75
+ ...(hasArchive ? { hasArchive: true } : {}),
76
+ };
77
+ }
78
+
79
+ return {
80
+ memoryDir: join(wikiDir, "topic"),
81
+ memoryFiles: [],
82
+ ...(target.latestSummary ? { latestSummaryFile: target.latestSummary } : {}),
83
+ ...(hasArchive ? { hasArchive: true } : {}),
84
+ };
85
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Programmatic topic registration — the host-agnostic core of otium's
3
+ * `POST /topics` route. Every surface (negotium MCP `register_topic`, CLI,
4
+ * channel adapters) creates topics through this one function so validation
5
+ * and defaults never fork.
6
+ */
7
+
8
+ import { randomUUID } from "node:crypto";
9
+ import { mkdirSync } from "node:fs";
10
+ import { resolveModelForAgent } from "#agents/model-catalog";
11
+ import { getRegistry } from "#agents/registry";
12
+ import { WsHub } from "#bus";
13
+ import { FALLBACK_AGENT, resolveTopicWorkspaceDir } from "#platform/config";
14
+ import { RESERVED_TOPIC_NAMES } from "#platform/constants";
15
+ import { logger } from "#platform/logger";
16
+ import {
17
+ findTopicTitleConflict,
18
+ normalizeTopicKind,
19
+ normalizeTopicState,
20
+ upsertTopic,
21
+ } from "#storage/api-topics";
22
+ import { type AgentKind, type EffortLevel, isAgentKind } from "#types";
23
+ import type { TopicAccessMode, TopicDto } from "#types/api";
24
+
25
+ // Agent rooms default to the node-level FALLBACK_AGENT (env), not a hardcoded
26
+ // backend — a node without DeepSeek auth can still default to claude/codex.
27
+ const DEFAULT_AGENT_ROOM_AGENT: AgentKind = FALLBACK_AGENT;
28
+
29
+ export class TopicValidationError extends Error {
30
+ constructor(message: string) {
31
+ super(message);
32
+ this.name = "TopicValidationError";
33
+ }
34
+ }
35
+
36
+ export interface RegisterTopicOptions {
37
+ title: string;
38
+ userId: string;
39
+ /** "agent" (AI room, default) or "channel" (human room, AI optional). */
40
+ kind?: TopicDto["kind"];
41
+ /** AI backend for the room. Defaults to maestro for agent rooms. */
42
+ agent?: AgentKind | "none";
43
+ model?: string;
44
+ effort?: EffortLevel;
45
+ description?: string;
46
+ /** Local topics default to private; Otium publication must be explicit. */
47
+ accessMode?: TopicAccessMode;
48
+ }
49
+
50
+ /**
51
+ * Create a topic owned by `userId`. Throws {@link TopicValidationError} on
52
+ * invalid input (reserved/conflicting title, bad agent/model combination).
53
+ */
54
+ export function registerTopic(opts: RegisterTopicOptions): TopicDto {
55
+ const title = opts.title?.trim();
56
+ if (!title) throw new TopicValidationError("title is required");
57
+ if (RESERVED_TOPIC_NAMES.has(title.toLowerCase())) {
58
+ throw new TopicValidationError(`"${title}" is a reserved name`);
59
+ }
60
+
61
+ const requestedKind = normalizeTopicKind(opts.kind) ?? "agent";
62
+ if (requestedKind === "manager") {
63
+ throw new TopicValidationError("Manager rooms are system-managed");
64
+ }
65
+ const conflict = findTopicTitleConflict(title, requestedKind);
66
+ if (conflict) {
67
+ throw new TopicValidationError(`A topic named "${title}" already exists`);
68
+ }
69
+
70
+ const rawAgent = opts.agent;
71
+ if (requestedKind === "agent" && rawAgent === "none") {
72
+ throw new TopicValidationError("Agent rooms must have an AI agent");
73
+ }
74
+ if (rawAgent && rawAgent !== "none" && !isAgentKind(rawAgent)) {
75
+ throw new TopicValidationError(`Unknown agent '${rawAgent}'`);
76
+ }
77
+ const requestedAgent: AgentKind | undefined =
78
+ rawAgent === "none" || (requestedKind === "channel" && rawAgent === undefined)
79
+ ? undefined
80
+ : ((rawAgent as AgentKind | undefined) ?? DEFAULT_AGENT_ROOM_AGENT);
81
+
82
+ const { kind, aiMode, agent } = normalizeTopicState({
83
+ kind: requestedKind,
84
+ agent: requestedAgent,
85
+ });
86
+
87
+ // Derive per-mode model/effort defaults from the chosen agent's registry.
88
+ const registry = getRegistry(agent ?? "maestro");
89
+ if (agent && opts.model && !registry.validateModel(opts.model)) {
90
+ throw new TopicValidationError(`model '${opts.model}' is not valid for agent '${agent}'`);
91
+ }
92
+ if (agent && opts.effort && !registry.validateEffort(opts.effort)) {
93
+ throw new TopicValidationError(`effort '${opts.effort}' is not valid for agent '${agent}'`);
94
+ }
95
+ const defaultModel = resolveModelForAgent(agent ?? "maestro", opts.model, registry);
96
+ const defaultEffort = opts.effort ?? registry.defaultEffort;
97
+
98
+ const now = new Date().toISOString();
99
+ const topic: TopicDto = {
100
+ id: randomUUID(),
101
+ title,
102
+ kind,
103
+ description: opts.description,
104
+ agent,
105
+ defaultModel,
106
+ defaultEffort: defaultEffort ?? "medium",
107
+ aiMode,
108
+ participants: [{ userId: opts.userId, role: "owner" }],
109
+ accessMode: opts.accessMode ?? "private",
110
+ createdAt: now,
111
+ lastMessageAt: now,
112
+ };
113
+
114
+ upsertTopic(topic);
115
+ try {
116
+ mkdirSync(resolveTopicWorkspaceDir(topic.id), { recursive: true });
117
+ } catch (err) {
118
+ logger.warn({ err, topicId: topic.id }, "registerTopic: workspace dir create failed");
119
+ }
120
+ WsHub.get().broadcastTopicCreated(topic);
121
+ logger.info({ topicId: topic.id, title, kind, agent }, "topic registered");
122
+ return topic;
123
+ }