sanook-cli 0.5.1 → 0.5.5

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 (217) hide show
  1. package/.env.example +161 -3
  2. package/CHANGELOG.md +148 -10
  3. package/README.md +255 -26
  4. package/README.th.md +95 -7
  5. package/dist/approval.js +13 -0
  6. package/dist/bin.js +3552 -155
  7. package/dist/brain-consolidate.js +335 -0
  8. package/dist/brain-context.js +262 -0
  9. package/dist/brain-doctor.js +318 -0
  10. package/dist/brain-eval.js +186 -0
  11. package/dist/brain-final.js +377 -0
  12. package/dist/brain-metrics.js +277 -0
  13. package/dist/brain-new.js +402 -0
  14. package/dist/brain-pack.js +210 -0
  15. package/dist/brain-repair.js +280 -0
  16. package/dist/brain-review.js +382 -0
  17. package/dist/brain.js +15 -1
  18. package/dist/brand.js +1 -1
  19. package/dist/cli-args.js +190 -0
  20. package/dist/cli-option-values.js +16 -0
  21. package/dist/clipboard.js +65 -0
  22. package/dist/commands.js +266 -27
  23. package/dist/compaction.js +96 -11
  24. package/dist/config.js +149 -33
  25. package/dist/context-compression.js +191 -0
  26. package/dist/context-pack.js +145 -0
  27. package/dist/cost.js +49 -15
  28. package/dist/dashboard/api-helpers.js +87 -0
  29. package/dist/dashboard/server.js +179 -0
  30. package/dist/dashboard/static/app.js +277 -0
  31. package/dist/dashboard/static/index.html +39 -0
  32. package/dist/dashboard/static/styles.css +85 -0
  33. package/dist/diff.js +10 -2
  34. package/dist/first-run.js +21 -0
  35. package/dist/gateway/auth.js +49 -9
  36. package/dist/gateway/bluebubbles.js +205 -0
  37. package/dist/gateway/config.js +929 -0
  38. package/dist/gateway/deliver.js +399 -0
  39. package/dist/gateway/discord.js +124 -0
  40. package/dist/gateway/doctor.js +456 -0
  41. package/dist/gateway/email.js +501 -0
  42. package/dist/gateway/googlechat.js +207 -0
  43. package/dist/gateway/homeassistant.js +256 -0
  44. package/dist/gateway/ledger.js +38 -1
  45. package/dist/gateway/line.js +171 -0
  46. package/dist/gateway/lock.js +3 -1
  47. package/dist/gateway/matrix.js +366 -0
  48. package/dist/gateway/mattermost.js +322 -0
  49. package/dist/gateway/ntfy.js +218 -0
  50. package/dist/gateway/schedule.js +31 -4
  51. package/dist/gateway/serve.js +267 -7
  52. package/dist/gateway/server.js +253 -19
  53. package/dist/gateway/service.js +224 -0
  54. package/dist/gateway/session.js +362 -0
  55. package/dist/gateway/signal.js +351 -0
  56. package/dist/gateway/slack.js +124 -0
  57. package/dist/gateway/sms.js +169 -0
  58. package/dist/gateway/targets.js +576 -0
  59. package/dist/gateway/teams.js +106 -0
  60. package/dist/gateway/telegram.js +38 -15
  61. package/dist/gateway/webhooks.js +220 -0
  62. package/dist/gateway/whatsapp.js +230 -0
  63. package/dist/hooks.js +13 -2
  64. package/dist/hotkeys.js +21 -0
  65. package/dist/i18n/en.js +98 -0
  66. package/dist/i18n/index.js +19 -0
  67. package/dist/i18n/th.js +98 -0
  68. package/dist/i18n/types.js +1 -0
  69. package/dist/insights-args.js +55 -0
  70. package/dist/insights.js +86 -0
  71. package/dist/knowledge.js +55 -29
  72. package/dist/loop.js +157 -29
  73. package/dist/lsp/index.js +23 -5
  74. package/dist/mcp-hub.js +33 -0
  75. package/dist/mcp-registry.js +494 -0
  76. package/dist/mcp-risk.js +71 -0
  77. package/dist/mcp-server.js +1 -1
  78. package/dist/mcp.js +120 -10
  79. package/dist/memory-log.js +90 -0
  80. package/dist/memory-store.js +37 -1
  81. package/dist/memory.js +148 -37
  82. package/dist/model-picker.js +58 -0
  83. package/dist/orchestrate.js +51 -19
  84. package/dist/personality.js +58 -0
  85. package/dist/plan-handoff.js +17 -0
  86. package/dist/polyglot.js +162 -0
  87. package/dist/process-runner.js +96 -0
  88. package/dist/project-init.js +91 -0
  89. package/dist/project-registry.js +143 -0
  90. package/dist/project-scaffold.js +124 -0
  91. package/dist/prompt-size.js +155 -0
  92. package/dist/providers/codex-login.js +138 -0
  93. package/dist/providers/codex.js +89 -43
  94. package/dist/providers/keys.js +22 -1
  95. package/dist/providers/models.js +2 -2
  96. package/dist/providers/registry.js +14 -47
  97. package/dist/search/chunk.js +7 -8
  98. package/dist/search/cli.js +83 -0
  99. package/dist/search/embed-store.js +3 -0
  100. package/dist/search/embedding-config.js +22 -0
  101. package/dist/search/engine.js +2 -13
  102. package/dist/search/indexer.js +44 -1
  103. package/dist/search/store.js +23 -1
  104. package/dist/session-distill.js +84 -0
  105. package/dist/session.js +92 -16
  106. package/dist/skill-install.js +53 -13
  107. package/dist/skills.js +33 -0
  108. package/dist/slash-completion.js +155 -0
  109. package/dist/support-dump.js +206 -0
  110. package/dist/tool-catalog.js +59 -0
  111. package/dist/tools/edit.js +45 -15
  112. package/dist/tools/git.js +10 -5
  113. package/dist/tools/homeassistant.js +106 -0
  114. package/dist/tools/index.js +10 -0
  115. package/dist/tools/list.js +19 -6
  116. package/dist/tools/permission.js +992 -12
  117. package/dist/tools/polyglot.js +126 -0
  118. package/dist/tools/read.js +16 -4
  119. package/dist/tools/sandbox.js +38 -13
  120. package/dist/tools/schedule.js +19 -3
  121. package/dist/tools/search.js +226 -15
  122. package/dist/tools/task.js +40 -9
  123. package/dist/tools/timeout.js +23 -3
  124. package/dist/tools/web-fetch-tool.js +33 -0
  125. package/dist/trust.js +11 -1
  126. package/dist/turn-retrieval.js +83 -0
  127. package/dist/ui/app.js +878 -32
  128. package/dist/ui/banner.js +78 -4
  129. package/dist/ui/history.js +37 -5
  130. package/dist/ui/markdown.js +122 -0
  131. package/dist/ui/mentions.js +3 -2
  132. package/dist/ui/overlay.js +496 -0
  133. package/dist/ui/queue.js +23 -0
  134. package/dist/ui/render.js +20 -1
  135. package/dist/ui/session-panel.js +115 -0
  136. package/dist/ui/setup-providers.js +40 -0
  137. package/dist/ui/setup.js +172 -46
  138. package/dist/ui/status.js +142 -0
  139. package/dist/ui/thinking-panel.js +36 -0
  140. package/dist/ui/tool-trail.js +97 -0
  141. package/dist/ui/transcript.js +26 -0
  142. package/dist/ui/useBusyElapsed.js +19 -0
  143. package/dist/ui/useEditor.js +144 -5
  144. package/dist/ui/useGitBranch.js +57 -0
  145. package/dist/update.js +56 -17
  146. package/dist/web-fetch.js +637 -0
  147. package/dist/web-surface.js +190 -0
  148. package/dist/worktree.js +175 -4
  149. package/package.json +5 -5
  150. package/second-brain/AGENTS.md +6 -4
  151. package/second-brain/CLAUDE.md +7 -1
  152. package/second-brain/Evals/_Index.md +10 -2
  153. package/second-brain/Evals/quality-ledger.md +9 -1
  154. package/second-brain/Evals/second-brain-benchmarks.md +62 -0
  155. package/second-brain/GEMINI.md +5 -4
  156. package/second-brain/Home.md +1 -1
  157. package/second-brain/Projects/_Index.md +19 -4
  158. package/second-brain/Projects/sanook-cli/_Index.md +30 -0
  159. package/second-brain/Projects/sanook-cli/context.md +35 -0
  160. package/second-brain/Projects/sanook-cli/current-state.md +32 -0
  161. package/second-brain/Projects/sanook-cli/overview.md +41 -0
  162. package/second-brain/Projects/sanook-cli/repo.md +34 -0
  163. package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +197 -0
  164. package/second-brain/README.md +1 -1
  165. package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
  166. package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
  167. package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
  168. package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -0
  169. package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
  170. package/second-brain/Research/2026-06-19-hermes-python-architecture-for-sanook.md +49 -0
  171. package/second-brain/Research/2026-06-19-terminal-ui-brand-research.md +52 -0
  172. package/second-brain/Research/_Index.md +8 -1
  173. package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
  174. package/second-brain/Reviews/_Index.md +1 -1
  175. package/second-brain/Runbooks/_Index.md +6 -1
  176. package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
  177. package/second-brain/SANOOK.md +45 -0
  178. package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
  179. package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
  180. package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
  181. package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
  182. package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
  183. package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
  184. package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
  185. package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
  186. package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
  187. package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
  188. package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
  189. package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
  190. package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
  191. package/second-brain/Sessions/_Index.md +15 -1
  192. package/second-brain/Shared/AI-Context-Index.md +22 -0
  193. package/second-brain/Shared/Context-Packs/_Index.md +9 -1
  194. package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
  195. package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
  196. package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
  197. package/second-brain/Shared/Operating-State/current-state.md +14 -4
  198. package/second-brain/Shared/Scripts/_Index.md +3 -1
  199. package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
  200. package/second-brain/Shared/Tech-Standards/_Index.md +6 -1
  201. package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
  202. package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
  203. package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
  204. package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -0
  205. package/second-brain/Shared/User-Memory/_Index.md +4 -1
  206. package/second-brain/Shared/User-Memory/response-examples.md +98 -0
  207. package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
  208. package/second-brain/Templates/_Index.md +9 -0
  209. package/second-brain/Templates/final-lite.md +111 -0
  210. package/second-brain/Templates/final.md +231 -0
  211. package/second-brain/Templates/project-workspace/_Index.md +31 -0
  212. package/second-brain/Templates/project-workspace/context.md +28 -0
  213. package/second-brain/Templates/project-workspace/current-state.md +29 -0
  214. package/second-brain/Templates/project-workspace/overview.md +39 -0
  215. package/second-brain/Templates/project-workspace/repo.md +33 -0
  216. package/second-brain/Vault Structure Map.md +2 -1
  217. package/skills/structured-output-llm/SKILL.md +1 -1
@@ -0,0 +1,929 @@
1
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { appHomePath } from '../brand.js';
4
+ import { parseAllowedChats } from './telegram.js';
5
+ const CONFIG_PATH = appHomePath('gateway', 'config.json');
6
+ export function gatewayConfigPath() {
7
+ return CONFIG_PATH;
8
+ }
9
+ export async function readGatewayConfig() {
10
+ try {
11
+ const parsed = JSON.parse(await readFile(CONFIG_PATH, 'utf8'));
12
+ if (!parsed || typeof parsed !== 'object')
13
+ return {};
14
+ const raw = parsed;
15
+ const telegram = raw.telegram;
16
+ const discord = raw.discord;
17
+ const slack = raw.slack;
18
+ const mattermost = raw.mattermost;
19
+ const homeassistant = raw.homeassistant;
20
+ const email = raw.email;
21
+ const line = raw.line;
22
+ const sms = raw.sms;
23
+ const ntfy = raw.ntfy;
24
+ const signal = raw.signal;
25
+ const whatsapp = raw.whatsapp;
26
+ const matrix = raw.matrix;
27
+ const googleChat = raw.googleChat;
28
+ const bluebubbles = raw.bluebubbles;
29
+ const teams = raw.teams;
30
+ const webhooks = raw.webhooks;
31
+ return {
32
+ telegram: telegram
33
+ ? {
34
+ enabled: telegram.enabled !== false,
35
+ botToken: typeof telegram.botToken === 'string' ? telegram.botToken : undefined,
36
+ allowedChatIds: Array.isArray(telegram.allowedChatIds)
37
+ ? telegram.allowedChatIds.filter((n) => Number.isInteger(n))
38
+ : undefined,
39
+ allowWrite: telegram.allowWrite === true,
40
+ }
41
+ : undefined,
42
+ discord: discord
43
+ ? {
44
+ enabled: discord.enabled !== false,
45
+ botToken: typeof discord.botToken === 'string' ? discord.botToken : undefined,
46
+ defaultChannelId: typeof discord.defaultChannelId === 'string' ? discord.defaultChannelId : undefined,
47
+ allowedChannelIds: Array.isArray(discord.allowedChannelIds)
48
+ ? discord.allowedChannelIds.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
49
+ : undefined,
50
+ allowWrite: discord.allowWrite === true,
51
+ }
52
+ : undefined,
53
+ slack: slack
54
+ ? {
55
+ enabled: slack.enabled !== false,
56
+ botToken: typeof slack.botToken === 'string' ? slack.botToken : undefined,
57
+ appToken: typeof slack.appToken === 'string' ? slack.appToken : undefined,
58
+ defaultChannelId: typeof slack.defaultChannelId === 'string' ? slack.defaultChannelId : undefined,
59
+ allowedChannelIds: Array.isArray(slack.allowedChannelIds)
60
+ ? slack.allowedChannelIds.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
61
+ : undefined,
62
+ allowWrite: slack.allowWrite === true,
63
+ }
64
+ : undefined,
65
+ mattermost: mattermost
66
+ ? {
67
+ enabled: mattermost.enabled !== false,
68
+ serverUrl: typeof mattermost.serverUrl === 'string' ? mattermost.serverUrl.trim() : undefined,
69
+ token: typeof mattermost.token === 'string' ? mattermost.token : undefined,
70
+ homeChannel: typeof mattermost.homeChannel === 'string' ? mattermost.homeChannel.trim() : undefined,
71
+ homeChannelName: typeof mattermost.homeChannelName === 'string' ? mattermost.homeChannelName : undefined,
72
+ allowedUsers: Array.isArray(mattermost.allowedUsers)
73
+ ? mattermost.allowedUsers.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
74
+ : undefined,
75
+ allowedChannels: Array.isArray(mattermost.allowedChannels)
76
+ ? mattermost.allowedChannels.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
77
+ : undefined,
78
+ freeResponseChannels: Array.isArray(mattermost.freeResponseChannels)
79
+ ? mattermost.freeResponseChannels.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
80
+ : undefined,
81
+ allowAllUsers: mattermost.allowAllUsers === true,
82
+ requireMention: mattermost.requireMention !== false,
83
+ groupSessionsPerUser: mattermost.groupSessionsPerUser !== false,
84
+ replyMode: mattermost.replyMode === 'thread' ? 'thread' : 'off',
85
+ }
86
+ : undefined,
87
+ homeassistant: homeassistant
88
+ ? {
89
+ enabled: homeassistant.enabled !== false,
90
+ url: typeof homeassistant.url === 'string' ? homeassistant.url.trim() : undefined,
91
+ token: typeof homeassistant.token === 'string' ? homeassistant.token : undefined,
92
+ homeChannel: typeof homeassistant.homeChannel === 'string' ? homeassistant.homeChannel.trim() : undefined,
93
+ homeChannelName: typeof homeassistant.homeChannelName === 'string' ? homeassistant.homeChannelName : undefined,
94
+ watchDomains: Array.isArray(homeassistant.watchDomains)
95
+ ? homeassistant.watchDomains.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
96
+ : undefined,
97
+ watchEntities: Array.isArray(homeassistant.watchEntities)
98
+ ? homeassistant.watchEntities.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
99
+ : undefined,
100
+ ignoreEntities: Array.isArray(homeassistant.ignoreEntities)
101
+ ? homeassistant.ignoreEntities.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
102
+ : undefined,
103
+ watchAll: homeassistant.watchAll === true,
104
+ cooldownSeconds: Number.isInteger(homeassistant.cooldownSeconds) ? homeassistant.cooldownSeconds : undefined,
105
+ }
106
+ : undefined,
107
+ email: email
108
+ ? {
109
+ enabled: email.enabled !== false,
110
+ address: typeof email.address === 'string' ? email.address : undefined,
111
+ password: typeof email.password === 'string' ? email.password : undefined,
112
+ imapHost: typeof email.imapHost === 'string' ? email.imapHost : undefined,
113
+ imapPort: Number.isInteger(email.imapPort) ? email.imapPort : undefined,
114
+ smtpHost: typeof email.smtpHost === 'string' ? email.smtpHost : undefined,
115
+ smtpPort: Number.isInteger(email.smtpPort) ? email.smtpPort : undefined,
116
+ homeAddress: typeof email.homeAddress === 'string' ? email.homeAddress : undefined,
117
+ allowedUsers: Array.isArray(email.allowedUsers)
118
+ ? email.allowedUsers.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim().toLowerCase())
119
+ : undefined,
120
+ allowAllUsers: email.allowAllUsers === true,
121
+ pollIntervalSeconds: Number.isInteger(email.pollIntervalSeconds) ? email.pollIntervalSeconds : undefined,
122
+ }
123
+ : undefined,
124
+ line: line
125
+ ? {
126
+ enabled: line.enabled !== false,
127
+ channelAccessToken: typeof line.channelAccessToken === 'string' ? line.channelAccessToken : undefined,
128
+ channelSecret: typeof line.channelSecret === 'string' ? line.channelSecret : undefined,
129
+ homeChannel: typeof line.homeChannel === 'string' ? line.homeChannel : undefined,
130
+ allowedUsers: Array.isArray(line.allowedUsers)
131
+ ? line.allowedUsers.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
132
+ : undefined,
133
+ allowedGroups: Array.isArray(line.allowedGroups)
134
+ ? line.allowedGroups.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
135
+ : undefined,
136
+ allowedRooms: Array.isArray(line.allowedRooms)
137
+ ? line.allowedRooms.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
138
+ : undefined,
139
+ allowAllUsers: line.allowAllUsers === true,
140
+ publicUrl: typeof line.publicUrl === 'string' ? line.publicUrl : undefined,
141
+ }
142
+ : undefined,
143
+ sms: sms
144
+ ? {
145
+ enabled: sms.enabled !== false,
146
+ accountSid: typeof sms.accountSid === 'string' ? sms.accountSid : undefined,
147
+ authToken: typeof sms.authToken === 'string' ? sms.authToken : undefined,
148
+ phoneNumber: typeof sms.phoneNumber === 'string' ? sms.phoneNumber : undefined,
149
+ homeChannel: typeof sms.homeChannel === 'string' ? sms.homeChannel : undefined,
150
+ homeChannelName: typeof sms.homeChannelName === 'string' ? sms.homeChannelName : undefined,
151
+ allowedUsers: Array.isArray(sms.allowedUsers)
152
+ ? sms.allowedUsers.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
153
+ : undefined,
154
+ allowAllUsers: sms.allowAllUsers === true,
155
+ webhookUrl: typeof sms.webhookUrl === 'string' ? sms.webhookUrl : undefined,
156
+ insecureNoSignature: sms.insecureNoSignature === true,
157
+ }
158
+ : undefined,
159
+ ntfy: ntfy
160
+ ? {
161
+ enabled: ntfy.enabled !== false,
162
+ serverUrl: typeof ntfy.serverUrl === 'string' ? ntfy.serverUrl : undefined,
163
+ topic: typeof ntfy.topic === 'string' ? ntfy.topic : undefined,
164
+ publishTopic: typeof ntfy.publishTopic === 'string' ? ntfy.publishTopic : undefined,
165
+ token: typeof ntfy.token === 'string' ? ntfy.token : undefined,
166
+ homeChannel: typeof ntfy.homeChannel === 'string' ? ntfy.homeChannel : undefined,
167
+ homeChannelName: typeof ntfy.homeChannelName === 'string' ? ntfy.homeChannelName : undefined,
168
+ allowedUsers: Array.isArray(ntfy.allowedUsers)
169
+ ? ntfy.allowedUsers.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
170
+ : undefined,
171
+ allowAllUsers: ntfy.allowAllUsers === true,
172
+ markdown: ntfy.markdown === true,
173
+ }
174
+ : undefined,
175
+ signal: signal
176
+ ? {
177
+ enabled: signal.enabled !== false,
178
+ httpUrl: typeof signal.httpUrl === 'string' ? signal.httpUrl : undefined,
179
+ account: typeof signal.account === 'string' ? signal.account.trim() : undefined,
180
+ homeChannel: typeof signal.homeChannel === 'string' ? signal.homeChannel.trim() : undefined,
181
+ homeChannelName: typeof signal.homeChannelName === 'string' ? signal.homeChannelName : undefined,
182
+ allowedUsers: Array.isArray(signal.allowedUsers)
183
+ ? signal.allowedUsers.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
184
+ : undefined,
185
+ groupAllowedUsers: Array.isArray(signal.groupAllowedUsers)
186
+ ? signal.groupAllowedUsers.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
187
+ : undefined,
188
+ allowAllUsers: signal.allowAllUsers === true,
189
+ requireMention: signal.requireMention === true,
190
+ }
191
+ : undefined,
192
+ whatsapp: whatsapp
193
+ ? {
194
+ enabled: whatsapp.enabled !== false,
195
+ phoneNumberId: typeof whatsapp.phoneNumberId === 'string' ? whatsapp.phoneNumberId.trim() : undefined,
196
+ accessToken: typeof whatsapp.accessToken === 'string' ? whatsapp.accessToken : undefined,
197
+ appSecret: typeof whatsapp.appSecret === 'string' ? whatsapp.appSecret : undefined,
198
+ verifyToken: typeof whatsapp.verifyToken === 'string' ? whatsapp.verifyToken : undefined,
199
+ homeChannel: typeof whatsapp.homeChannel === 'string' ? whatsapp.homeChannel.trim() : undefined,
200
+ homeChannelName: typeof whatsapp.homeChannelName === 'string' ? whatsapp.homeChannelName : undefined,
201
+ allowedUsers: Array.isArray(whatsapp.allowedUsers)
202
+ ? whatsapp.allowedUsers.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
203
+ : undefined,
204
+ allowAllUsers: whatsapp.allowAllUsers === true,
205
+ publicUrl: typeof whatsapp.publicUrl === 'string' ? whatsapp.publicUrl : undefined,
206
+ apiVersion: typeof whatsapp.apiVersion === 'string' ? whatsapp.apiVersion.trim() : undefined,
207
+ }
208
+ : undefined,
209
+ matrix: matrix
210
+ ? {
211
+ enabled: matrix.enabled !== false,
212
+ homeserver: typeof matrix.homeserver === 'string' ? matrix.homeserver.trim() : undefined,
213
+ accessToken: typeof matrix.accessToken === 'string' ? matrix.accessToken : undefined,
214
+ userId: typeof matrix.userId === 'string' ? matrix.userId.trim() : undefined,
215
+ password: typeof matrix.password === 'string' ? matrix.password : undefined,
216
+ homeRoom: typeof matrix.homeRoom === 'string' ? matrix.homeRoom.trim() : undefined,
217
+ homeRoomName: typeof matrix.homeRoomName === 'string' ? matrix.homeRoomName : undefined,
218
+ allowedUsers: Array.isArray(matrix.allowedUsers)
219
+ ? matrix.allowedUsers.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
220
+ : undefined,
221
+ allowedRooms: Array.isArray(matrix.allowedRooms)
222
+ ? matrix.allowedRooms.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
223
+ : undefined,
224
+ freeResponseRooms: Array.isArray(matrix.freeResponseRooms)
225
+ ? matrix.freeResponseRooms.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
226
+ : undefined,
227
+ allowAllUsers: matrix.allowAllUsers === true,
228
+ requireMention: matrix.requireMention !== false,
229
+ groupSessionsPerUser: matrix.groupSessionsPerUser !== false,
230
+ autoJoin: matrix.autoJoin !== false,
231
+ pollTimeoutMs: Number.isInteger(matrix.pollTimeoutMs) ? matrix.pollTimeoutMs : undefined,
232
+ }
233
+ : undefined,
234
+ googleChat: googleChat
235
+ ? {
236
+ enabled: googleChat.enabled !== false,
237
+ projectId: typeof googleChat.projectId === 'string' ? googleChat.projectId.trim() : undefined,
238
+ subscriptionName: typeof googleChat.subscriptionName === 'string' ? googleChat.subscriptionName.trim() : undefined,
239
+ serviceAccountJson: typeof googleChat.serviceAccountJson === 'string' ? googleChat.serviceAccountJson.trim() : undefined,
240
+ apiBaseUrl: typeof googleChat.apiBaseUrl === 'string' ? googleChat.apiBaseUrl.trim() : undefined,
241
+ incomingWebhookUrl: typeof googleChat.incomingWebhookUrl === 'string' ? googleChat.incomingWebhookUrl.trim() : undefined,
242
+ homeChannel: typeof googleChat.homeChannel === 'string' ? googleChat.homeChannel.trim() : undefined,
243
+ homeChannelName: typeof googleChat.homeChannelName === 'string' ? googleChat.homeChannelName : undefined,
244
+ allowedUsers: Array.isArray(googleChat.allowedUsers)
245
+ ? googleChat.allowedUsers.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
246
+ : undefined,
247
+ allowedSpaces: Array.isArray(googleChat.allowedSpaces)
248
+ ? googleChat.allowedSpaces.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
249
+ : undefined,
250
+ freeResponseSpaces: Array.isArray(googleChat.freeResponseSpaces)
251
+ ? googleChat.freeResponseSpaces.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
252
+ : undefined,
253
+ allowAllUsers: googleChat.allowAllUsers === true,
254
+ allowAllSpaces: googleChat.allowAllSpaces === true,
255
+ maxMessages: Number.isInteger(googleChat.maxMessages) ? googleChat.maxMessages : undefined,
256
+ maxBytes: Number.isInteger(googleChat.maxBytes) ? googleChat.maxBytes : undefined,
257
+ }
258
+ : undefined,
259
+ bluebubbles: bluebubbles
260
+ ? {
261
+ enabled: bluebubbles.enabled !== false,
262
+ serverUrl: typeof bluebubbles.serverUrl === 'string' ? bluebubbles.serverUrl.trim() : undefined,
263
+ password: typeof bluebubbles.password === 'string' ? bluebubbles.password : undefined,
264
+ webhookHost: typeof bluebubbles.webhookHost === 'string' ? bluebubbles.webhookHost.trim() : undefined,
265
+ webhookPort: Number.isInteger(bluebubbles.webhookPort) ? bluebubbles.webhookPort : undefined,
266
+ webhookPath: typeof bluebubbles.webhookPath === 'string' ? bluebubbles.webhookPath.trim() : undefined,
267
+ homeChannel: typeof bluebubbles.homeChannel === 'string' ? bluebubbles.homeChannel.trim() : undefined,
268
+ homeChannelName: typeof bluebubbles.homeChannelName === 'string' ? bluebubbles.homeChannelName : undefined,
269
+ allowedUsers: Array.isArray(bluebubbles.allowedUsers)
270
+ ? bluebubbles.allowedUsers.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
271
+ : undefined,
272
+ allowAllUsers: bluebubbles.allowAllUsers === true,
273
+ requireMention: bluebubbles.requireMention === true,
274
+ mentionPatterns: Array.isArray(bluebubbles.mentionPatterns)
275
+ ? bluebubbles.mentionPatterns.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
276
+ : undefined,
277
+ sendReadReceipts: bluebubbles.sendReadReceipts !== false,
278
+ }
279
+ : undefined,
280
+ teams: teams
281
+ ? {
282
+ enabled: teams.enabled !== false,
283
+ deliveryMode: teams.deliveryMode === 'graph' ? 'graph' : 'incoming_webhook',
284
+ incomingWebhookUrl: typeof teams.incomingWebhookUrl === 'string' ? teams.incomingWebhookUrl.trim() : undefined,
285
+ graphAccessToken: typeof teams.graphAccessToken === 'string' ? teams.graphAccessToken : undefined,
286
+ teamId: typeof teams.teamId === 'string' ? teams.teamId.trim() : undefined,
287
+ channelId: typeof teams.channelId === 'string' ? teams.channelId.trim() : undefined,
288
+ chatId: typeof teams.chatId === 'string' ? teams.chatId.trim() : undefined,
289
+ homeChannel: typeof teams.homeChannel === 'string' ? teams.homeChannel.trim() : undefined,
290
+ homeChannelName: typeof teams.homeChannelName === 'string' ? teams.homeChannelName : undefined,
291
+ clientId: typeof teams.clientId === 'string' ? teams.clientId.trim() : undefined,
292
+ clientSecret: typeof teams.clientSecret === 'string' ? teams.clientSecret : undefined,
293
+ tenantId: typeof teams.tenantId === 'string' ? teams.tenantId.trim() : undefined,
294
+ allowedUsers: Array.isArray(teams.allowedUsers)
295
+ ? teams.allowedUsers.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim())
296
+ : undefined,
297
+ allowAllUsers: teams.allowAllUsers === true,
298
+ port: Number.isInteger(teams.port) ? teams.port : undefined,
299
+ }
300
+ : undefined,
301
+ webhooks: webhooks
302
+ ? {
303
+ enabled: webhooks.enabled !== false,
304
+ secret: typeof webhooks.secret === 'string' ? webhooks.secret : undefined,
305
+ publicUrl: typeof webhooks.publicUrl === 'string' ? webhooks.publicUrl : undefined,
306
+ rateLimitPerMinute: Number.isInteger(webhooks.rateLimitPerMinute) ? webhooks.rateLimitPerMinute : undefined,
307
+ routes: webhooks.routes && typeof webhooks.routes === 'object'
308
+ ? Object.fromEntries(Object.entries(webhooks.routes)
309
+ .filter(([name, route]) => typeof name === 'string' && Boolean(route) && typeof route === 'object')
310
+ .map(([name, route]) => {
311
+ const r = route;
312
+ return [
313
+ name.trim(),
314
+ {
315
+ events: Array.isArray(r.events)
316
+ ? r.events.filter((event) => typeof event === 'string' && event.trim()).map((event) => event.trim())
317
+ : undefined,
318
+ secret: typeof r.secret === 'string' ? r.secret : undefined,
319
+ prompt: typeof r.prompt === 'string' ? r.prompt : undefined,
320
+ deliver: typeof r.deliver === 'string' ? r.deliver : undefined,
321
+ deliverOnly: r.deliverOnly === true,
322
+ description: typeof r.description === 'string' ? r.description : undefined,
323
+ rateLimitPerMinute: Number.isInteger(r.rateLimitPerMinute) ? r.rateLimitPerMinute : undefined,
324
+ },
325
+ ];
326
+ })
327
+ .filter(([name]) => Boolean(name)))
328
+ : undefined,
329
+ }
330
+ : undefined,
331
+ };
332
+ }
333
+ catch {
334
+ return {};
335
+ }
336
+ }
337
+ export async function writeGatewayConfig(config) {
338
+ await mkdir(dirname(CONFIG_PATH), { recursive: true });
339
+ await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
340
+ await chmod(CONFIG_PATH, 0o600).catch(() => { });
341
+ }
342
+ export async function patchGatewayConfig(patch) {
343
+ const current = await readGatewayConfig();
344
+ const next = {
345
+ ...current,
346
+ ...patch,
347
+ telegram: patch.telegram ? { ...current.telegram, ...patch.telegram } : current.telegram,
348
+ discord: patch.discord ? { ...current.discord, ...patch.discord } : current.discord,
349
+ slack: patch.slack ? { ...current.slack, ...patch.slack } : current.slack,
350
+ mattermost: patch.mattermost ? { ...current.mattermost, ...patch.mattermost } : current.mattermost,
351
+ homeassistant: patch.homeassistant ? { ...current.homeassistant, ...patch.homeassistant } : current.homeassistant,
352
+ email: patch.email ? { ...current.email, ...patch.email } : current.email,
353
+ line: patch.line ? { ...current.line, ...patch.line } : current.line,
354
+ sms: patch.sms ? { ...current.sms, ...patch.sms } : current.sms,
355
+ ntfy: patch.ntfy ? { ...current.ntfy, ...patch.ntfy } : current.ntfy,
356
+ signal: patch.signal ? { ...current.signal, ...patch.signal } : current.signal,
357
+ whatsapp: patch.whatsapp ? { ...current.whatsapp, ...patch.whatsapp } : current.whatsapp,
358
+ matrix: patch.matrix ? { ...current.matrix, ...patch.matrix } : current.matrix,
359
+ googleChat: patch.googleChat ? { ...current.googleChat, ...patch.googleChat } : current.googleChat,
360
+ bluebubbles: patch.bluebubbles ? { ...current.bluebubbles, ...patch.bluebubbles } : current.bluebubbles,
361
+ teams: patch.teams ? { ...current.teams, ...patch.teams } : current.teams,
362
+ webhooks: patch.webhooks
363
+ ? {
364
+ ...current.webhooks,
365
+ ...patch.webhooks,
366
+ routes: patch.webhooks.routes ? { ...current.webhooks?.routes, ...patch.webhooks.routes } : current.webhooks?.routes,
367
+ }
368
+ : current.webhooks,
369
+ };
370
+ await writeGatewayConfig(next);
371
+ return next;
372
+ }
373
+ function parseStringList(raw) {
374
+ if (!raw)
375
+ return [];
376
+ return raw
377
+ .split(',')
378
+ .map((s) => s.trim())
379
+ .filter(Boolean);
380
+ }
381
+ function parseFlexibleStringList(raw) {
382
+ const text = raw?.trim();
383
+ if (!text)
384
+ return [];
385
+ if (text.startsWith('[')) {
386
+ try {
387
+ const parsed = JSON.parse(text);
388
+ if (Array.isArray(parsed))
389
+ return parsed.map((item) => String(item).trim()).filter(Boolean);
390
+ }
391
+ catch {
392
+ // Fall back to CSV/newline parsing below.
393
+ }
394
+ }
395
+ return text
396
+ .split(/\r?\n|,/)
397
+ .map((s) => s.trim())
398
+ .filter(Boolean);
399
+ }
400
+ function optionalTrim(raw) {
401
+ const trimmed = raw?.trim();
402
+ return trimmed || undefined;
403
+ }
404
+ export function resolveTelegramConfig(config, env = process.env) {
405
+ const envToken = env.TELEGRAM_BOT_TOKEN;
406
+ const cfg = config.telegram;
407
+ const token = envToken || cfg?.botToken;
408
+ const allowedChatIds = env.TELEGRAM_ALLOWED_CHATS
409
+ ? parseAllowedChats(env.TELEGRAM_ALLOWED_CHATS)
410
+ : (cfg?.allowedChatIds ?? []);
411
+ return {
412
+ token,
413
+ allowedChatIds,
414
+ allowWrite: env.TELEGRAM_ALLOW_WRITE === '1' || cfg?.allowWrite === true,
415
+ enabled: cfg?.enabled !== false,
416
+ source: envToken ? 'env' : token ? 'config' : 'none',
417
+ };
418
+ }
419
+ export function resolveDiscordConfig(config, env = process.env) {
420
+ const envToken = env.DISCORD_BOT_TOKEN;
421
+ const cfg = config.discord;
422
+ const token = envToken || cfg?.botToken;
423
+ return {
424
+ token,
425
+ defaultChannelId: env.DISCORD_DEFAULT_CHANNEL || cfg?.defaultChannelId,
426
+ allowedChannelIds: env.DISCORD_ALLOWED_CHANNELS ? parseStringList(env.DISCORD_ALLOWED_CHANNELS) : (cfg?.allowedChannelIds ?? []),
427
+ allowWrite: env.DISCORD_ALLOW_WRITE === '1' || cfg?.allowWrite === true,
428
+ enabled: cfg?.enabled !== false,
429
+ source: envToken ? 'env' : token ? 'config' : 'none',
430
+ };
431
+ }
432
+ export function resolveSlackConfig(config, env = process.env) {
433
+ const envBotToken = env.SLACK_BOT_TOKEN;
434
+ const cfg = config.slack;
435
+ const botToken = envBotToken || cfg?.botToken;
436
+ return {
437
+ botToken,
438
+ appToken: env.SLACK_APP_TOKEN || cfg?.appToken,
439
+ defaultChannelId: env.SLACK_DEFAULT_CHANNEL || cfg?.defaultChannelId,
440
+ allowedChannelIds: env.SLACK_ALLOWED_CHANNELS ? parseStringList(env.SLACK_ALLOWED_CHANNELS) : (cfg?.allowedChannelIds ?? []),
441
+ allowWrite: env.SLACK_ALLOW_WRITE === '1' || cfg?.allowWrite === true,
442
+ enabled: cfg?.enabled !== false,
443
+ source: envBotToken ? 'env' : botToken ? 'config' : 'none',
444
+ };
445
+ }
446
+ export function resolveMattermostConfig(config, env = process.env) {
447
+ const cfg = config.mattermost;
448
+ const envServerUrl = optionalTrim(env.MATTERMOST_URL) || optionalTrim(env.MATTERMOST_SERVER_URL);
449
+ const envToken = optionalTrim(env.MATTERMOST_TOKEN);
450
+ const serverUrl = (envServerUrl || optionalTrim(cfg?.serverUrl))?.replace(/\/+$/, '');
451
+ const token = envToken || optionalTrim(cfg?.token);
452
+ const requireMentionEnv = env.MATTERMOST_REQUIRE_MENTION;
453
+ const groupSessionsEnv = env.MATTERMOST_GROUP_SESSIONS_PER_USER;
454
+ const replyModeRaw = optionalTrim(env.MATTERMOST_REPLY_MODE) || optionalTrim(cfg?.replyMode);
455
+ const replyMode = replyModeRaw === 'thread' ? 'thread' : 'off';
456
+ return {
457
+ serverUrl,
458
+ token,
459
+ homeChannel: optionalTrim(env.MATTERMOST_HOME_CHANNEL) || optionalTrim(cfg?.homeChannel),
460
+ homeChannelName: optionalTrim(env.MATTERMOST_HOME_CHANNEL_NAME) || optionalTrim(cfg?.homeChannelName),
461
+ allowedUsers: env.MATTERMOST_ALLOWED_USERS ? parseStringList(env.MATTERMOST_ALLOWED_USERS) : (cfg?.allowedUsers ?? []),
462
+ allowedChannels: env.MATTERMOST_ALLOWED_CHANNELS ? parseStringList(env.MATTERMOST_ALLOWED_CHANNELS) : (cfg?.allowedChannels ?? []),
463
+ freeResponseChannels: env.MATTERMOST_FREE_RESPONSE_CHANNELS
464
+ ? parseStringList(env.MATTERMOST_FREE_RESPONSE_CHANNELS)
465
+ : (cfg?.freeResponseChannels ?? []),
466
+ allowAllUsers: env.MATTERMOST_ALLOW_ALL_USERS === '1' || env.MATTERMOST_ALLOW_ALL_USERS === 'true' || cfg?.allowAllUsers === true,
467
+ requireMention: requireMentionEnv == null ? cfg?.requireMention !== false : requireMentionEnv === '1' || requireMentionEnv === 'true',
468
+ groupSessionsPerUser: groupSessionsEnv == null ? cfg?.groupSessionsPerUser !== false : groupSessionsEnv === '1' || groupSessionsEnv === 'true',
469
+ replyMode,
470
+ enabled: cfg?.enabled !== false,
471
+ source: envServerUrl || envToken ? 'env' : serverUrl || token ? 'config' : 'none',
472
+ };
473
+ }
474
+ export function resolveHomeAssistantConfig(config, env = process.env) {
475
+ const cfg = config.homeassistant;
476
+ const envToken = optionalTrim(env.HASS_TOKEN);
477
+ const envUrl = optionalTrim(env.HASS_URL);
478
+ const url = (envUrl || optionalTrim(cfg?.url) || 'http://homeassistant.local:8123').replace(/\/+$/, '');
479
+ const watchAllEnv = env.HASS_WATCH_ALL;
480
+ return {
481
+ url,
482
+ token: envToken || optionalTrim(cfg?.token),
483
+ homeChannel: optionalTrim(env.HASS_HOME_CHANNEL) || optionalTrim(cfg?.homeChannel),
484
+ homeChannelName: optionalTrim(env.HASS_HOME_CHANNEL_NAME) || optionalTrim(cfg?.homeChannelName),
485
+ watchDomains: env.HASS_WATCH_DOMAINS ? parseStringList(env.HASS_WATCH_DOMAINS) : (cfg?.watchDomains ?? []),
486
+ watchEntities: env.HASS_WATCH_ENTITIES ? parseStringList(env.HASS_WATCH_ENTITIES) : (cfg?.watchEntities ?? []),
487
+ ignoreEntities: env.HASS_IGNORE_ENTITIES ? parseStringList(env.HASS_IGNORE_ENTITIES) : (cfg?.ignoreEntities ?? []),
488
+ watchAll: watchAllEnv == null ? cfg?.watchAll === true : watchAllEnv === '1' || watchAllEnv === 'true',
489
+ cooldownSeconds: parsePositiveInt(env.HASS_COOLDOWN_SECONDS, cfg?.cooldownSeconds ?? 30),
490
+ enabled: cfg?.enabled !== false,
491
+ source: envToken || envUrl ? 'env' : cfg?.token || cfg?.url ? 'config' : 'none',
492
+ };
493
+ }
494
+ function parsePositiveInt(raw, fallback) {
495
+ if (!raw)
496
+ return fallback;
497
+ const n = Number(raw);
498
+ return Number.isInteger(n) && n > 0 ? n : fallback;
499
+ }
500
+ export function resolveEmailConfig(config, env = process.env) {
501
+ const cfg = config.email;
502
+ const envAddress = env.EMAIL_ADDRESS;
503
+ const address = envAddress || cfg?.address;
504
+ const password = env.EMAIL_PASSWORD || cfg?.password;
505
+ const imapHost = env.EMAIL_IMAP_HOST || cfg?.imapHost;
506
+ const smtpHost = env.EMAIL_SMTP_HOST || cfg?.smtpHost;
507
+ return {
508
+ address,
509
+ password,
510
+ imapHost,
511
+ imapPort: parsePositiveInt(env.EMAIL_IMAP_PORT, cfg?.imapPort ?? 993),
512
+ smtpHost,
513
+ smtpPort: parsePositiveInt(env.EMAIL_SMTP_PORT, cfg?.smtpPort ?? 587),
514
+ homeAddress: env.EMAIL_HOME_ADDRESS || cfg?.homeAddress,
515
+ allowedUsers: env.EMAIL_ALLOWED_USERS ? parseStringList(env.EMAIL_ALLOWED_USERS).map((s) => s.toLowerCase()) : (cfg?.allowedUsers ?? []),
516
+ allowAllUsers: env.EMAIL_ALLOW_ALL_USERS === '1' || env.EMAIL_ALLOW_ALL_USERS === 'true' || cfg?.allowAllUsers === true,
517
+ pollIntervalSeconds: parsePositiveInt(env.EMAIL_POLL_INTERVAL, cfg?.pollIntervalSeconds ?? 15),
518
+ enabled: cfg?.enabled !== false,
519
+ source: envAddress ? 'env' : address ? 'config' : 'none',
520
+ };
521
+ }
522
+ export function resolveLineConfig(config, env = process.env) {
523
+ const cfg = config.line;
524
+ const envToken = env.LINE_CHANNEL_ACCESS_TOKEN;
525
+ const channelAccessToken = envToken || cfg?.channelAccessToken;
526
+ return {
527
+ channelAccessToken,
528
+ channelSecret: env.LINE_CHANNEL_SECRET || cfg?.channelSecret,
529
+ homeChannel: env.LINE_HOME_CHANNEL || cfg?.homeChannel,
530
+ allowedUsers: env.LINE_ALLOWED_USERS ? parseStringList(env.LINE_ALLOWED_USERS) : (cfg?.allowedUsers ?? []),
531
+ allowedGroups: env.LINE_ALLOWED_GROUPS ? parseStringList(env.LINE_ALLOWED_GROUPS) : (cfg?.allowedGroups ?? []),
532
+ allowedRooms: env.LINE_ALLOWED_ROOMS ? parseStringList(env.LINE_ALLOWED_ROOMS) : (cfg?.allowedRooms ?? []),
533
+ allowAllUsers: env.LINE_ALLOW_ALL_USERS === '1' || env.LINE_ALLOW_ALL_USERS === 'true' || cfg?.allowAllUsers === true,
534
+ publicUrl: env.LINE_PUBLIC_URL || cfg?.publicUrl,
535
+ enabled: cfg?.enabled !== false,
536
+ source: envToken ? 'env' : channelAccessToken ? 'config' : 'none',
537
+ };
538
+ }
539
+ export function resolveSmsConfig(config, env = process.env) {
540
+ const cfg = config.sms;
541
+ const envAccountSid = env.TWILIO_ACCOUNT_SID;
542
+ const envAuthToken = env.TWILIO_AUTH_TOKEN;
543
+ const envPhoneNumber = env.TWILIO_PHONE_NUMBER;
544
+ const accountSid = envAccountSid || cfg?.accountSid;
545
+ const authToken = envAuthToken || cfg?.authToken;
546
+ const phoneNumber = envPhoneNumber || cfg?.phoneNumber;
547
+ return {
548
+ accountSid,
549
+ authToken,
550
+ phoneNumber,
551
+ homeChannel: env.SMS_HOME_CHANNEL || cfg?.homeChannel,
552
+ homeChannelName: env.SMS_HOME_CHANNEL_NAME || cfg?.homeChannelName,
553
+ allowedUsers: env.SMS_ALLOWED_USERS ? parseStringList(env.SMS_ALLOWED_USERS) : (cfg?.allowedUsers ?? []),
554
+ allowAllUsers: env.SMS_ALLOW_ALL_USERS === '1' || env.SMS_ALLOW_ALL_USERS === 'true' || cfg?.allowAllUsers === true,
555
+ webhookUrl: env.SMS_WEBHOOK_URL || cfg?.webhookUrl,
556
+ insecureNoSignature: env.SMS_INSECURE_NO_SIGNATURE === '1' || env.SMS_INSECURE_NO_SIGNATURE === 'true' || cfg?.insecureNoSignature === true,
557
+ enabled: cfg?.enabled !== false,
558
+ source: envAccountSid || envAuthToken || envPhoneNumber ? 'env' : accountSid || authToken || phoneNumber ? 'config' : 'none',
559
+ };
560
+ }
561
+ export function resolveNtfyConfig(config, env = process.env) {
562
+ const cfg = config.ntfy;
563
+ const envServerUrl = optionalTrim(env.NTFY_SERVER_URL);
564
+ const envTopic = optionalTrim(env.NTFY_TOPIC);
565
+ const envToken = optionalTrim(env.NTFY_TOKEN);
566
+ const topic = envTopic || optionalTrim(cfg?.topic);
567
+ const token = envToken || optionalTrim(cfg?.token);
568
+ const serverUrl = (envServerUrl || optionalTrim(cfg?.serverUrl) || 'https://ntfy.sh').replace(/\/+$/, '');
569
+ return {
570
+ serverUrl,
571
+ topic,
572
+ publishTopic: optionalTrim(env.NTFY_PUBLISH_TOPIC) || optionalTrim(cfg?.publishTopic),
573
+ token,
574
+ homeChannel: optionalTrim(env.NTFY_HOME_CHANNEL) || optionalTrim(cfg?.homeChannel),
575
+ homeChannelName: optionalTrim(env.NTFY_HOME_CHANNEL_NAME) || optionalTrim(cfg?.homeChannelName),
576
+ allowedUsers: env.NTFY_ALLOWED_USERS ? parseStringList(env.NTFY_ALLOWED_USERS) : (cfg?.allowedUsers ?? []),
577
+ allowAllUsers: env.NTFY_ALLOW_ALL_USERS === '1' || env.NTFY_ALLOW_ALL_USERS === 'true' || cfg?.allowAllUsers === true,
578
+ markdown: env.NTFY_MARKDOWN === '1' || env.NTFY_MARKDOWN === 'true' || cfg?.markdown === true,
579
+ enabled: cfg?.enabled !== false,
580
+ source: envTopic || envToken || envServerUrl ? 'env' : topic || token || optionalTrim(cfg?.serverUrl) ? 'config' : 'none',
581
+ };
582
+ }
583
+ export function resolveSignalConfig(config, env = process.env) {
584
+ const cfg = config.signal;
585
+ const envHttpUrl = env.SIGNAL_HTTP_URL;
586
+ const envAccount = env.SIGNAL_ACCOUNT;
587
+ const account = envAccount || cfg?.account;
588
+ const httpUrl = (envHttpUrl || cfg?.httpUrl || 'http://127.0.0.1:8080').replace(/\/+$/, '');
589
+ return {
590
+ httpUrl,
591
+ account,
592
+ homeChannel: env.SIGNAL_HOME_CHANNEL || cfg?.homeChannel,
593
+ homeChannelName: env.SIGNAL_HOME_CHANNEL_NAME || cfg?.homeChannelName,
594
+ allowedUsers: env.SIGNAL_ALLOWED_USERS ? parseStringList(env.SIGNAL_ALLOWED_USERS) : (cfg?.allowedUsers ?? []),
595
+ groupAllowedUsers: env.SIGNAL_GROUP_ALLOWED_USERS ? parseStringList(env.SIGNAL_GROUP_ALLOWED_USERS) : (cfg?.groupAllowedUsers ?? []),
596
+ allowAllUsers: env.SIGNAL_ALLOW_ALL_USERS === '1' || env.SIGNAL_ALLOW_ALL_USERS === 'true' || cfg?.allowAllUsers === true,
597
+ requireMention: env.SIGNAL_REQUIRE_MENTION === '1' || env.SIGNAL_REQUIRE_MENTION === 'true' || cfg?.requireMention === true,
598
+ enabled: cfg?.enabled !== false,
599
+ source: envHttpUrl || envAccount ? 'env' : account || cfg?.httpUrl ? 'config' : 'none',
600
+ };
601
+ }
602
+ export function resolveWhatsAppConfig(config, env = process.env) {
603
+ const cfg = config.whatsapp;
604
+ const envPhoneNumberId = optionalTrim(env.WHATSAPP_CLOUD_PHONE_NUMBER_ID);
605
+ const envAccessToken = optionalTrim(env.WHATSAPP_CLOUD_ACCESS_TOKEN);
606
+ const envAppSecret = optionalTrim(env.WHATSAPP_CLOUD_APP_SECRET);
607
+ const phoneNumberId = envPhoneNumberId || optionalTrim(cfg?.phoneNumberId);
608
+ const accessToken = envAccessToken || optionalTrim(cfg?.accessToken);
609
+ const appSecret = envAppSecret || optionalTrim(cfg?.appSecret);
610
+ return {
611
+ phoneNumberId,
612
+ accessToken,
613
+ appSecret,
614
+ verifyToken: optionalTrim(env.WHATSAPP_CLOUD_VERIFY_TOKEN) || optionalTrim(cfg?.verifyToken),
615
+ homeChannel: optionalTrim(env.WHATSAPP_CLOUD_HOME_CHANNEL) || optionalTrim(cfg?.homeChannel),
616
+ homeChannelName: optionalTrim(env.WHATSAPP_CLOUD_HOME_CHANNEL_NAME) || optionalTrim(cfg?.homeChannelName),
617
+ allowedUsers: env.WHATSAPP_CLOUD_ALLOWED_USERS ? parseStringList(env.WHATSAPP_CLOUD_ALLOWED_USERS) : (cfg?.allowedUsers ?? []),
618
+ allowAllUsers: env.WHATSAPP_CLOUD_ALLOW_ALL_USERS === '1' || env.WHATSAPP_CLOUD_ALLOW_ALL_USERS === 'true' || cfg?.allowAllUsers === true,
619
+ publicUrl: optionalTrim(env.WHATSAPP_CLOUD_PUBLIC_URL) || optionalTrim(cfg?.publicUrl),
620
+ apiVersion: optionalTrim(env.WHATSAPP_CLOUD_API_VERSION) || optionalTrim(cfg?.apiVersion) || 'v20.0',
621
+ enabled: cfg?.enabled !== false,
622
+ source: envPhoneNumberId || envAccessToken || envAppSecret ? 'env' : phoneNumberId || accessToken || appSecret ? 'config' : 'none',
623
+ };
624
+ }
625
+ export function resolveMatrixConfig(config, env = process.env) {
626
+ const cfg = config.matrix;
627
+ const envHomeserver = optionalTrim(env.MATRIX_HOMESERVER);
628
+ const envAccessToken = optionalTrim(env.MATRIX_ACCESS_TOKEN);
629
+ const envUserId = optionalTrim(env.MATRIX_USER_ID);
630
+ const envPassword = optionalTrim(env.MATRIX_PASSWORD);
631
+ const homeserver = (envHomeserver || optionalTrim(cfg?.homeserver))?.replace(/\/+$/, '');
632
+ const accessToken = envAccessToken || optionalTrim(cfg?.accessToken);
633
+ const userId = envUserId || optionalTrim(cfg?.userId);
634
+ const password = envPassword || optionalTrim(cfg?.password);
635
+ const requireMentionEnv = env.MATRIX_REQUIRE_MENTION;
636
+ const groupSessionsEnv = env.MATRIX_GROUP_SESSIONS_PER_USER;
637
+ const autoJoinEnv = env.MATRIX_AUTO_JOIN;
638
+ return {
639
+ homeserver,
640
+ accessToken,
641
+ userId,
642
+ password,
643
+ homeRoom: optionalTrim(env.MATRIX_HOME_ROOM) || optionalTrim(cfg?.homeRoom),
644
+ homeRoomName: optionalTrim(env.MATRIX_HOME_ROOM_NAME) || optionalTrim(cfg?.homeRoomName),
645
+ allowedUsers: env.MATRIX_ALLOWED_USERS ? parseStringList(env.MATRIX_ALLOWED_USERS) : (cfg?.allowedUsers ?? []),
646
+ allowedRooms: env.MATRIX_ALLOWED_ROOMS ? parseStringList(env.MATRIX_ALLOWED_ROOMS) : (cfg?.allowedRooms ?? []),
647
+ freeResponseRooms: env.MATRIX_FREE_RESPONSE_ROOMS ? parseStringList(env.MATRIX_FREE_RESPONSE_ROOMS) : (cfg?.freeResponseRooms ?? []),
648
+ allowAllUsers: env.MATRIX_ALLOW_ALL_USERS === '1' || env.MATRIX_ALLOW_ALL_USERS === 'true' || cfg?.allowAllUsers === true,
649
+ requireMention: requireMentionEnv == null ? cfg?.requireMention !== false : requireMentionEnv === '1' || requireMentionEnv === 'true',
650
+ groupSessionsPerUser: groupSessionsEnv == null ? cfg?.groupSessionsPerUser !== false : groupSessionsEnv === '1' || groupSessionsEnv === 'true',
651
+ autoJoin: autoJoinEnv == null ? cfg?.autoJoin !== false : autoJoinEnv === '1' || autoJoinEnv === 'true',
652
+ pollTimeoutMs: parsePositiveInt(env.MATRIX_POLL_TIMEOUT_MS, cfg?.pollTimeoutMs ?? 30_000),
653
+ enabled: cfg?.enabled !== false,
654
+ source: envHomeserver || envAccessToken || envUserId || envPassword
655
+ ? 'env'
656
+ : homeserver || accessToken || userId || password
657
+ ? 'config'
658
+ : 'none',
659
+ };
660
+ }
661
+ export function resolveGoogleChatConfig(config, env = process.env) {
662
+ const cfg = config.googleChat;
663
+ const envProjectId = optionalTrim(env.GOOGLE_CHAT_PROJECT_ID) || optionalTrim(env.GOOGLE_CLOUD_PROJECT);
664
+ const envSubscriptionName = optionalTrim(env.GOOGLE_CHAT_SUBSCRIPTION_NAME);
665
+ const envServiceAccountJson = optionalTrim(env.GOOGLE_CHAT_SERVICE_ACCOUNT_JSON) || optionalTrim(env.GOOGLE_APPLICATION_CREDENTIALS);
666
+ const envApiBaseUrl = optionalTrim(env.GOOGLE_CHAT_API_BASE_URL);
667
+ const envIncomingWebhookUrl = optionalTrim(env.GOOGLE_CHAT_INCOMING_WEBHOOK_URL) || optionalTrim(env.GOOGLE_CHAT_WEBHOOK_URL);
668
+ return {
669
+ projectId: envProjectId || optionalTrim(cfg?.projectId),
670
+ subscriptionName: envSubscriptionName || optionalTrim(cfg?.subscriptionName),
671
+ serviceAccountJson: envServiceAccountJson || optionalTrim(cfg?.serviceAccountJson),
672
+ apiBaseUrl: (envApiBaseUrl || optionalTrim(cfg?.apiBaseUrl) || 'https://chat.googleapis.com').replace(/\/+$/, ''),
673
+ incomingWebhookUrl: envIncomingWebhookUrl || optionalTrim(cfg?.incomingWebhookUrl),
674
+ homeChannel: optionalTrim(env.GOOGLE_CHAT_HOME_CHANNEL) || optionalTrim(cfg?.homeChannel),
675
+ homeChannelName: optionalTrim(env.GOOGLE_CHAT_HOME_CHANNEL_NAME) || optionalTrim(cfg?.homeChannelName),
676
+ allowedUsers: env.GOOGLE_CHAT_ALLOWED_USERS ? parseStringList(env.GOOGLE_CHAT_ALLOWED_USERS) : (cfg?.allowedUsers ?? []),
677
+ allowedSpaces: env.GOOGLE_CHAT_ALLOWED_SPACES ? parseStringList(env.GOOGLE_CHAT_ALLOWED_SPACES) : (cfg?.allowedSpaces ?? []),
678
+ freeResponseSpaces: env.GOOGLE_CHAT_FREE_RESPONSE_SPACES
679
+ ? parseStringList(env.GOOGLE_CHAT_FREE_RESPONSE_SPACES)
680
+ : (cfg?.freeResponseSpaces ?? []),
681
+ allowAllUsers: env.GOOGLE_CHAT_ALLOW_ALL_USERS === '1' || env.GOOGLE_CHAT_ALLOW_ALL_USERS === 'true' || cfg?.allowAllUsers === true,
682
+ allowAllSpaces: env.GOOGLE_CHAT_ALLOW_ALL_SPACES === '1' || env.GOOGLE_CHAT_ALLOW_ALL_SPACES === 'true' || cfg?.allowAllSpaces === true,
683
+ maxMessages: parsePositiveInt(env.GOOGLE_CHAT_MAX_MESSAGES, cfg?.maxMessages ?? 1),
684
+ maxBytes: parsePositiveInt(env.GOOGLE_CHAT_MAX_BYTES, cfg?.maxBytes ?? 16_777_216),
685
+ enabled: cfg?.enabled !== false,
686
+ source: envProjectId || envSubscriptionName || envServiceAccountJson || envApiBaseUrl || envIncomingWebhookUrl
687
+ ? 'env'
688
+ : cfg?.projectId || cfg?.subscriptionName || cfg?.serviceAccountJson || cfg?.apiBaseUrl || cfg?.incomingWebhookUrl
689
+ ? 'config'
690
+ : 'none',
691
+ };
692
+ }
693
+ export function resolveBlueBubblesConfig(config, env = process.env) {
694
+ const cfg = config.bluebubbles;
695
+ const envServerUrl = optionalTrim(env.BLUEBUBBLES_SERVER_URL);
696
+ const envPassword = optionalTrim(env.BLUEBUBBLES_PASSWORD);
697
+ const envWebhookHost = optionalTrim(env.BLUEBUBBLES_WEBHOOK_HOST);
698
+ const envWebhookPath = optionalTrim(env.BLUEBUBBLES_WEBHOOK_PATH);
699
+ const envRequireMention = env.BLUEBUBBLES_REQUIRE_MENTION;
700
+ const envSendReadReceipts = env.BLUEBUBBLES_SEND_READ_RECEIPTS;
701
+ return {
702
+ serverUrl: envServerUrl || optionalTrim(cfg?.serverUrl),
703
+ password: envPassword || optionalTrim(cfg?.password),
704
+ webhookHost: envWebhookHost || optionalTrim(cfg?.webhookHost) || '127.0.0.1',
705
+ webhookPort: parsePositiveInt(env.BLUEBUBBLES_WEBHOOK_PORT, cfg?.webhookPort ?? 8645),
706
+ webhookPath: envWebhookPath || optionalTrim(cfg?.webhookPath) || '/bluebubbles-webhook',
707
+ homeChannel: optionalTrim(env.BLUEBUBBLES_HOME_CHANNEL) || optionalTrim(cfg?.homeChannel),
708
+ homeChannelName: optionalTrim(env.BLUEBUBBLES_HOME_CHANNEL_NAME) || optionalTrim(cfg?.homeChannelName),
709
+ allowedUsers: env.BLUEBUBBLES_ALLOWED_USERS ? parseStringList(env.BLUEBUBBLES_ALLOWED_USERS) : (cfg?.allowedUsers ?? []),
710
+ allowAllUsers: env.BLUEBUBBLES_ALLOW_ALL_USERS === '1' || env.BLUEBUBBLES_ALLOW_ALL_USERS === 'true' || cfg?.allowAllUsers === true,
711
+ requireMention: envRequireMention == null ? cfg?.requireMention === true : envRequireMention === '1' || envRequireMention === 'true',
712
+ mentionPatterns: env.BLUEBUBBLES_MENTION_PATTERNS
713
+ ? parseFlexibleStringList(env.BLUEBUBBLES_MENTION_PATTERNS)
714
+ : (cfg?.mentionPatterns ?? []),
715
+ sendReadReceipts: envSendReadReceipts == null
716
+ ? cfg?.sendReadReceipts !== false
717
+ : envSendReadReceipts === '1' || envSendReadReceipts === 'true',
718
+ enabled: cfg?.enabled !== false,
719
+ source: envServerUrl || envPassword ? 'env' : cfg?.serverUrl || cfg?.password ? 'config' : 'none',
720
+ };
721
+ }
722
+ export function resolveTeamsConfig(config, env = process.env) {
723
+ const cfg = config.teams;
724
+ const envIncomingWebhookUrl = optionalTrim(env.TEAMS_INCOMING_WEBHOOK_URL);
725
+ const envGraphAccessToken = optionalTrim(env.TEAMS_GRAPH_ACCESS_TOKEN);
726
+ const envClientId = optionalTrim(env.TEAMS_CLIENT_ID);
727
+ const envClientSecret = optionalTrim(env.TEAMS_CLIENT_SECRET);
728
+ const envTenantId = optionalTrim(env.TEAMS_TENANT_ID);
729
+ const deliveryModeRaw = optionalTrim(env.TEAMS_DELIVERY_MODE) || optionalTrim(cfg?.deliveryMode);
730
+ const deliveryMode = deliveryModeRaw === 'graph' ? 'graph' : 'incoming_webhook';
731
+ const incomingWebhookUrl = envIncomingWebhookUrl || optionalTrim(cfg?.incomingWebhookUrl);
732
+ const graphAccessToken = envGraphAccessToken || optionalTrim(cfg?.graphAccessToken);
733
+ const teamId = optionalTrim(env.TEAMS_TEAM_ID) || optionalTrim(cfg?.teamId);
734
+ const channelId = optionalTrim(env.TEAMS_CHANNEL_ID) || optionalTrim(cfg?.channelId);
735
+ const chatId = optionalTrim(env.TEAMS_CHAT_ID) || optionalTrim(cfg?.chatId);
736
+ const clientId = envClientId || optionalTrim(cfg?.clientId);
737
+ const clientSecret = envClientSecret || optionalTrim(cfg?.clientSecret);
738
+ const tenantId = envTenantId || optionalTrim(cfg?.tenantId);
739
+ return {
740
+ deliveryMode,
741
+ incomingWebhookUrl,
742
+ graphAccessToken,
743
+ teamId,
744
+ channelId,
745
+ chatId,
746
+ homeChannel: optionalTrim(env.TEAMS_HOME_CHANNEL) || optionalTrim(cfg?.homeChannel),
747
+ homeChannelName: optionalTrim(env.TEAMS_HOME_CHANNEL_NAME) || optionalTrim(cfg?.homeChannelName),
748
+ clientId,
749
+ clientSecret,
750
+ tenantId,
751
+ allowedUsers: env.TEAMS_ALLOWED_USERS ? parseStringList(env.TEAMS_ALLOWED_USERS) : (cfg?.allowedUsers ?? []),
752
+ allowAllUsers: env.TEAMS_ALLOW_ALL_USERS === '1' || env.TEAMS_ALLOW_ALL_USERS === 'true' || cfg?.allowAllUsers === true,
753
+ port: parsePositiveInt(env.TEAMS_PORT, cfg?.port ?? 3978),
754
+ enabled: cfg?.enabled !== false,
755
+ source: envIncomingWebhookUrl || envGraphAccessToken || envClientId || envClientSecret || envTenantId
756
+ ? 'env'
757
+ : incomingWebhookUrl || graphAccessToken || clientId || clientSecret || tenantId
758
+ ? 'config'
759
+ : 'none',
760
+ };
761
+ }
762
+ export function resolveWebhookConfig(config, env = process.env) {
763
+ const cfg = config.webhooks;
764
+ const envSecret = env.WEBHOOK_SECRET;
765
+ const secret = envSecret || cfg?.secret;
766
+ const routes = Object.fromEntries(Object.entries(cfg?.routes ?? {}).map(([name, route]) => [
767
+ name,
768
+ {
769
+ name,
770
+ events: route.events ?? [],
771
+ secret: route.secret,
772
+ prompt: route.prompt,
773
+ deliver: route.deliver?.trim() || 'log',
774
+ deliverOnly: route.deliverOnly === true,
775
+ description: route.description,
776
+ rateLimitPerMinute: route.rateLimitPerMinute,
777
+ },
778
+ ]));
779
+ const envEnabled = env.WEBHOOK_ENABLED;
780
+ return {
781
+ enabled: envEnabled === '1' || envEnabled === 'true' || cfg?.enabled === true,
782
+ secret,
783
+ publicUrl: env.WEBHOOK_PUBLIC_URL || cfg?.publicUrl,
784
+ routes,
785
+ rateLimitPerMinute: parsePositiveInt(env.WEBHOOK_RATE_LIMIT_PER_MINUTE, cfg?.rateLimitPerMinute ?? 30),
786
+ source: envSecret || envEnabled ? 'env' : cfg ? 'config' : 'none',
787
+ };
788
+ }
789
+ export function redactGatewayConfig(config) {
790
+ return {
791
+ ...config,
792
+ telegram: config.telegram
793
+ ? {
794
+ ...config.telegram,
795
+ botToken: config.telegram.botToken ? '<secret:TELEGRAM_BOT_TOKEN>' : undefined,
796
+ }
797
+ : undefined,
798
+ discord: config.discord
799
+ ? {
800
+ ...config.discord,
801
+ botToken: config.discord.botToken ? '<secret:DISCORD_BOT_TOKEN>' : undefined,
802
+ }
803
+ : undefined,
804
+ slack: config.slack
805
+ ? {
806
+ ...config.slack,
807
+ botToken: config.slack.botToken ? '<secret:SLACK_BOT_TOKEN>' : undefined,
808
+ appToken: config.slack.appToken ? '<secret:SLACK_APP_TOKEN>' : undefined,
809
+ }
810
+ : undefined,
811
+ mattermost: config.mattermost
812
+ ? {
813
+ ...config.mattermost,
814
+ token: config.mattermost.token ? '<secret:MATTERMOST_TOKEN>' : undefined,
815
+ }
816
+ : undefined,
817
+ homeassistant: config.homeassistant
818
+ ? {
819
+ ...config.homeassistant,
820
+ token: config.homeassistant.token ? '<secret:HASS_TOKEN>' : undefined,
821
+ }
822
+ : undefined,
823
+ email: config.email
824
+ ? {
825
+ ...config.email,
826
+ password: config.email.password ? '<secret:EMAIL_PASSWORD>' : undefined,
827
+ }
828
+ : undefined,
829
+ line: config.line
830
+ ? {
831
+ ...config.line,
832
+ channelAccessToken: config.line.channelAccessToken ? '<secret:LINE_CHANNEL_ACCESS_TOKEN>' : undefined,
833
+ channelSecret: config.line.channelSecret ? '<secret:LINE_CHANNEL_SECRET>' : undefined,
834
+ }
835
+ : undefined,
836
+ sms: config.sms
837
+ ? {
838
+ ...config.sms,
839
+ authToken: config.sms.authToken ? '<secret:TWILIO_AUTH_TOKEN>' : undefined,
840
+ }
841
+ : undefined,
842
+ ntfy: config.ntfy
843
+ ? {
844
+ ...config.ntfy,
845
+ token: config.ntfy.token ? '<secret:NTFY_TOKEN>' : undefined,
846
+ }
847
+ : undefined,
848
+ signal: config.signal
849
+ ? {
850
+ ...config.signal,
851
+ account: redactSignalIdentifier(config.signal.account),
852
+ homeChannel: redactSignalIdentifier(config.signal.homeChannel),
853
+ allowedUsers: config.signal.allowedUsers?.map(redactSignalIdentifier).filter((id) => Boolean(id)),
854
+ groupAllowedUsers: config.signal.groupAllowedUsers?.map(redactSignalIdentifier).filter((id) => Boolean(id)),
855
+ }
856
+ : undefined,
857
+ whatsapp: config.whatsapp
858
+ ? {
859
+ ...config.whatsapp,
860
+ accessToken: config.whatsapp.accessToken ? '<secret:WHATSAPP_CLOUD_ACCESS_TOKEN>' : undefined,
861
+ appSecret: config.whatsapp.appSecret ? '<secret:WHATSAPP_CLOUD_APP_SECRET>' : undefined,
862
+ verifyToken: config.whatsapp.verifyToken ? '<secret:WHATSAPP_CLOUD_VERIFY_TOKEN>' : undefined,
863
+ homeChannel: redactPhoneLike(config.whatsapp.homeChannel),
864
+ allowedUsers: config.whatsapp.allowedUsers?.map(redactPhoneLike).filter((id) => Boolean(id)),
865
+ }
866
+ : undefined,
867
+ matrix: config.matrix
868
+ ? {
869
+ ...config.matrix,
870
+ accessToken: config.matrix.accessToken ? '<secret:MATRIX_ACCESS_TOKEN>' : undefined,
871
+ password: config.matrix.password ? '<secret:MATRIX_PASSWORD>' : undefined,
872
+ }
873
+ : undefined,
874
+ googleChat: config.googleChat
875
+ ? {
876
+ ...config.googleChat,
877
+ serviceAccountJson: config.googleChat.serviceAccountJson ? '<secret:GOOGLE_CHAT_SERVICE_ACCOUNT_JSON>' : undefined,
878
+ incomingWebhookUrl: config.googleChat.incomingWebhookUrl ? '<secret:GOOGLE_CHAT_INCOMING_WEBHOOK_URL>' : undefined,
879
+ }
880
+ : undefined,
881
+ bluebubbles: config.bluebubbles
882
+ ? {
883
+ ...config.bluebubbles,
884
+ password: config.bluebubbles.password ? '<secret:BLUEBUBBLES_PASSWORD>' : undefined,
885
+ }
886
+ : undefined,
887
+ teams: config.teams
888
+ ? {
889
+ ...config.teams,
890
+ incomingWebhookUrl: config.teams.incomingWebhookUrl ? '<secret:TEAMS_INCOMING_WEBHOOK_URL>' : undefined,
891
+ graphAccessToken: config.teams.graphAccessToken ? '<secret:TEAMS_GRAPH_ACCESS_TOKEN>' : undefined,
892
+ clientSecret: config.teams.clientSecret ? '<secret:TEAMS_CLIENT_SECRET>' : undefined,
893
+ }
894
+ : undefined,
895
+ webhooks: config.webhooks
896
+ ? {
897
+ ...config.webhooks,
898
+ secret: config.webhooks.secret ? '<secret:WEBHOOK_SECRET>' : undefined,
899
+ routes: config.webhooks.routes
900
+ ? Object.fromEntries(Object.entries(config.webhooks.routes).map(([name, route]) => [
901
+ name,
902
+ { ...route, secret: route.secret ? '<secret:WEBHOOK_ROUTE_SECRET>' : undefined },
903
+ ]))
904
+ : undefined,
905
+ }
906
+ : undefined,
907
+ };
908
+ }
909
+ function redactPhoneLike(value) {
910
+ const trimmed = value?.trim();
911
+ if (!trimmed)
912
+ return undefined;
913
+ const visible = trimmed.replace(/[\s()+.-]+/g, '');
914
+ if (visible.length <= 6)
915
+ return '<redacted>';
916
+ return `${visible.slice(0, 4)}…${visible.slice(-4)}`;
917
+ }
918
+ function redactSignalIdentifier(value) {
919
+ const trimmed = value?.trim();
920
+ if (!trimmed)
921
+ return undefined;
922
+ if (trimmed === '*')
923
+ return '*';
924
+ const [prefix, body] = trimmed.startsWith('group:') ? ['group:', trimmed.slice('group:'.length)] : ['', trimmed];
925
+ const visible = body.replace(/\s+/g, '');
926
+ if (visible.length <= 6)
927
+ return `${prefix}<redacted>`;
928
+ return `${prefix}${visible.slice(0, 4)}…${visible.slice(-4)}`;
929
+ }