mixdog 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/package.json +10 -3
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/background-task-meta-smoke.mjs +1 -1
  6. package/scripts/bench-run.mjs +262 -0
  7. package/scripts/compact-smoke.mjs +12 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  9. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  10. package/scripts/internal-comms-bench.mjs +727 -0
  11. package/scripts/internal-comms-smoke.mjs +75 -0
  12. package/scripts/lead-workflow-smoke.mjs +4 -4
  13. package/scripts/live-worker-smoke.mjs +9 -9
  14. package/scripts/output-style-bench.mjs +285 -0
  15. package/scripts/output-style-smoke.mjs +13 -10
  16. package/scripts/patch-replay.mjs +90 -0
  17. package/scripts/provider-stream-stall-test.mjs +276 -0
  18. package/scripts/provider-toolcall-test.mjs +599 -1
  19. package/scripts/routing-corpus.mjs +281 -0
  20. package/scripts/session-bench.mjs +1526 -0
  21. package/scripts/session-diag.mjs +595 -0
  22. package/scripts/session-ingest-smoke.mjs +2 -2
  23. package/scripts/task-bench.mjs +207 -0
  24. package/scripts/tool-failures.mjs +6 -6
  25. package/scripts/tool-smoke.mjs +306 -66
  26. package/scripts/toolcall-args-test.mjs +81 -0
  27. package/src/agents/debugger/AGENT.md +4 -4
  28. package/src/agents/heavy-worker/AGENT.md +4 -2
  29. package/src/agents/reviewer/AGENT.md +4 -4
  30. package/src/agents/worker/AGENT.md +4 -2
  31. package/src/app.mjs +10 -6
  32. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  33. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  34. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  35. package/src/headless-role.mjs +14 -14
  36. package/src/help.mjs +1 -0
  37. package/src/lib/mixdog-debug.cjs +0 -22
  38. package/src/lib/plugin-paths.cjs +1 -7
  39. package/src/lib/rules-builder.cjs +34 -56
  40. package/src/mixdog-session-runtime.mjs +710 -319
  41. package/src/output-styles/default.md +12 -7
  42. package/src/output-styles/minimal.md +25 -0
  43. package/src/output-styles/oneline.md +21 -0
  44. package/src/output-styles/simple.md +10 -9
  45. package/src/repl.mjs +12 -4
  46. package/src/rules/agent/00-common.md +7 -5
  47. package/src/rules/agent/30-explorer.md +7 -8
  48. package/src/rules/lead/01-general.md +3 -1
  49. package/src/rules/lead/lead-tool.md +7 -0
  50. package/src/rules/shared/01-tool.md +17 -12
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  52. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  54. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  55. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  56. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  57. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  59. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  60. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  61. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  62. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  65. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
  66. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
  67. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
  75. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  76. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  77. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  78. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
  79. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  80. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  81. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  82. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  83. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  84. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  85. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  86. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  87. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  92. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  99. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  104. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  105. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  106. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  107. package/src/runtime/channels/backends/discord.mjs +99 -9
  108. package/src/runtime/channels/backends/telegram.mjs +501 -0
  109. package/src/runtime/channels/index.mjs +441 -1254
  110. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  111. package/src/runtime/channels/lib/config.mjs +54 -3
  112. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  113. package/src/runtime/channels/lib/executor.mjs +0 -3
  114. package/src/runtime/channels/lib/format.mjs +4 -2
  115. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  116. package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
  117. package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
  118. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  119. package/src/runtime/channels/lib/session-discovery.mjs +0 -4
  120. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  121. package/src/runtime/channels/lib/tool-format.mjs +1 -2
  122. package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
  123. package/src/runtime/channels/lib/webhook.mjs +59 -31
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  126. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  127. package/src/runtime/memory/index.mjs +187 -43
  128. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  129. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  130. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  131. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  132. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  133. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  134. package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
  135. package/src/runtime/memory/lib/memory.mjs +101 -4
  136. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  137. package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
  138. package/src/runtime/memory/lib/session-ingest.mjs +116 -7
  139. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  140. package/src/runtime/memory/tool-defs.mjs +6 -3
  141. package/src/runtime/search/index.mjs +2 -7
  142. package/src/runtime/search/lib/config.mjs +0 -4
  143. package/src/runtime/search/lib/state.mjs +1 -15
  144. package/src/runtime/search/lib/web-tools.mjs +0 -1
  145. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  146. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  147. package/src/runtime/shared/child-spawn-gate.mjs +0 -6
  148. package/src/runtime/shared/config.mjs +9 -0
  149. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  150. package/src/runtime/shared/schedules-store.mjs +21 -19
  151. package/src/runtime/shared/tool-surface.mjs +98 -13
  152. package/src/runtime/shared/transcript-writer.mjs +129 -0
  153. package/src/runtime/shared/update-checker.mjs +214 -0
  154. package/src/standalone/agent-tool.mjs +255 -109
  155. package/src/standalone/channel-admin.mjs +133 -40
  156. package/src/standalone/channel-worker.mjs +8 -291
  157. package/src/standalone/explore-tool.mjs +2 -2
  158. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  159. package/src/standalone/provider-admin.mjs +11 -0
  160. package/src/standalone/seeds.mjs +1 -11
  161. package/src/standalone/usage-dashboard.mjs +1 -1
  162. package/src/tui/App.jsx +2137 -750
  163. package/src/tui/components/ConfirmBar.jsx +47 -0
  164. package/src/tui/components/ContextPanel.jsx +5 -3
  165. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  166. package/src/tui/components/Markdown.jsx +22 -98
  167. package/src/tui/components/Message.jsx +14 -35
  168. package/src/tui/components/Picker.jsx +87 -12
  169. package/src/tui/components/PromptInput.jsx +146 -9
  170. package/src/tui/components/QueuedCommands.jsx +1 -1
  171. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  172. package/src/tui/components/Spinner.jsx +7 -7
  173. package/src/tui/components/StatusLine.jsx +40 -21
  174. package/src/tui/components/TextEntryPanel.jsx +51 -7
  175. package/src/tui/components/ToolExecution.jsx +177 -100
  176. package/src/tui/components/TurnDone.jsx +4 -4
  177. package/src/tui/components/UsagePanel.jsx +1 -1
  178. package/src/tui/components/tool-output-format.mjs +312 -40
  179. package/src/tui/components/tool-output-format.test.mjs +180 -1
  180. package/src/tui/display-width.mjs +69 -0
  181. package/src/tui/display-width.test.mjs +35 -0
  182. package/src/tui/dist/index.mjs +7324 -2393
  183. package/src/tui/engine.mjs +287 -126
  184. package/src/tui/index.jsx +117 -7
  185. package/src/tui/keyboard-protocol.mjs +42 -0
  186. package/src/tui/lib/voice-recorder.mjs +453 -0
  187. package/src/tui/markdown/format-token.mjs +354 -142
  188. package/src/tui/markdown/format-token.test.mjs +155 -17
  189. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  190. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  191. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  192. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  193. package/src/tui/markdown/table-layout.mjs +9 -9
  194. package/src/tui/paste-attachments.mjs +0 -11
  195. package/src/tui/prompt-history-store.mjs +129 -0
  196. package/src/tui/prompt-history-store.test.mjs +52 -0
  197. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  198. package/src/tui/theme.mjs +41 -647
  199. package/src/tui/themes/base.mjs +86 -0
  200. package/src/tui/themes/basic.mjs +85 -0
  201. package/src/tui/themes/catppuccin.mjs +72 -0
  202. package/src/tui/themes/dracula.mjs +70 -0
  203. package/src/tui/themes/everforest.mjs +71 -0
  204. package/src/tui/themes/gruvbox.mjs +71 -0
  205. package/src/tui/themes/index.mjs +71 -0
  206. package/src/tui/themes/indigo.mjs +78 -0
  207. package/src/tui/themes/kanagawa.mjs +80 -0
  208. package/src/tui/themes/light.mjs +81 -0
  209. package/src/tui/themes/nord.mjs +72 -0
  210. package/src/tui/themes/onedark.mjs +16 -0
  211. package/src/tui/themes/rosepine.mjs +70 -0
  212. package/src/tui/themes/teal.mjs +81 -0
  213. package/src/tui/themes/tokyonight.mjs +79 -0
  214. package/src/tui/themes/utils.mjs +106 -0
  215. package/src/tui/themes/warm.mjs +79 -0
  216. package/src/tui/transcript-tool-failures.mjs +13 -2
  217. package/src/ui/markdown.mjs +1 -1
  218. package/src/ui/model-display.mjs +2 -2
  219. package/src/ui/statusline.mjs +26 -27
  220. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
  221. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  222. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  223. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  224. package/src/workflows/default/WORKFLOW.md +39 -12
  225. package/src/workflows/sequential/WORKFLOW.md +46 -0
  226. package/src/workflows/solo/WORKFLOW.md +7 -0
  227. package/vendor/ink/build/display-width.js +62 -0
  228. package/vendor/ink/build/ink.js +154 -20
  229. package/vendor/ink/build/measure-text.js +4 -1
  230. package/vendor/ink/build/output.js +115 -9
  231. package/vendor/ink/build/render-node-to-output.js +4 -1
  232. package/vendor/ink/build/render.js +4 -0
  233. package/src/hooks/lib/permission-rules.cjs +0 -170
  234. package/src/hooks/lib/settings-loader.cjs +0 -112
  235. package/src/lib/hook-pipe-path.cjs +0 -10
  236. package/src/output-styles/extreme-simple.md +0 -20
  237. package/src/rules/lead/04-workflow.md +0 -51
  238. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  239. package/src/workflows/default/workflow.json +0 -13
  240. package/src/workflows/solo/workflow.json +0 -7
@@ -14,6 +14,7 @@ import {
14
14
  deleteSecret,
15
15
  diagnoseDiscordTokenValue,
16
16
  getDiscordToken,
17
+ getTelegramToken,
17
18
  getWebhookAuthtoken,
18
19
  hasStoredSecret,
19
20
  readSection,
@@ -21,6 +22,7 @@ import {
21
22
  updateSection,
22
23
  } from '../runtime/shared/config.mjs';
23
24
  import { resolvePluginData } from '../runtime/shared/plugin-paths.mjs';
25
+ import { readMarkdownDocument } from '../runtime/shared/markdown-frontmatter.mjs';
24
26
 
25
27
  const NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
26
28
  const DEFAULT_CHANNELS = Object.freeze({
@@ -48,10 +50,6 @@ function webhooksDir() {
48
50
  return join(dataDir(), 'webhooks');
49
51
  }
50
52
 
51
- function readJson(path, fallback = {}) {
52
- try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return fallback; }
53
- }
54
-
55
53
  function readText(path, fallback = '') {
56
54
  try { return readFileSync(path, 'utf8'); } catch { return fallback; }
57
55
  }
@@ -63,8 +61,22 @@ function writeTextAtomic(path, text) {
63
61
  renameSync(tmp, path);
64
62
  }
65
63
 
66
- function writeJsonAtomic(path, data) {
67
- writeTextAtomic(path, JSON.stringify(data, null, 2) + '\n');
64
+ // Serialize a single-file frontmatter markdown document:
65
+ // ---
66
+ // key: value
67
+ // ---
68
+ //
69
+ // <body>
70
+ // Values are stringified as-is (enabled:false -> "false"); the reader casts
71
+ // types back. Mirrors the WORKFLOW.md / SKILL.md single-file convention.
72
+ function serializeFrontmatterDoc(meta = {}, body = '') {
73
+ const lines = ['---'];
74
+ for (const [key, value] of Object.entries(meta)) {
75
+ if (value === undefined || value === null) continue;
76
+ lines.push(`${key}: ${value}`);
77
+ }
78
+ lines.push('---', '');
79
+ return `${lines.join('\n')}\n${String(body || '').trim()}\n`;
68
80
  }
69
81
 
70
82
  function assertName(name, kind = 'name') {
@@ -88,6 +100,22 @@ function normalizeChannelsConfig(raw = {}) {
88
100
  };
89
101
  }
90
102
 
103
+ function channelIdForBackend(entry = {}, backend = 'discord') {
104
+ if (backend === 'telegram') {
105
+ return String(entry?.telegramChatId || (entry?.discordChannelId ? '' : entry?.channelId) || '');
106
+ }
107
+ return String(entry?.discordChannelId || (entry?.telegramChatId ? '' : entry?.channelId) || '');
108
+ }
109
+
110
+ function seedBackendChannelIds(entry = {}, backend = 'discord') {
111
+ const next = { ...(entry || {}) };
112
+ if (next.channelId) {
113
+ if (backend === 'telegram' && !next.telegramChatId && !next.discordChannelId) next.telegramChatId = next.channelId;
114
+ if (backend !== 'telegram' && !next.discordChannelId && !next.telegramChatId) next.discordChannelId = next.channelId;
115
+ }
116
+ return next;
117
+ }
118
+
91
119
  function updateChannelsSection(build) {
92
120
  let next;
93
121
  updateSection('channels', (current) => {
@@ -110,11 +138,14 @@ function listEntryDirs(dir) {
110
138
 
111
139
  export function listChannels(config = {}) {
112
140
  const cfg = normalizeChannelsConfig(config);
141
+ const backend = cfg.backend === 'telegram' ? 'telegram' : 'discord';
113
142
  return Object.entries(cfg.channelsConfig || {})
114
143
  .sort(([a], [b]) => a.localeCompare(b))
115
144
  .map(([name, value]) => ({
116
145
  name,
117
- channelId: String(value?.channelId || ''),
146
+ channelId: channelIdForBackend(value, backend),
147
+ discordChannelId: String(value?.discordChannelId || ''),
148
+ telegramChatId: String(value?.telegramChatId || ''),
118
149
  mode: value?.mode || 'interactive',
119
150
  main: cfg.mainChannel === name,
120
151
  }));
@@ -132,6 +163,18 @@ export function forgetDiscordToken() {
132
163
  return { ok: true };
133
164
  }
134
165
 
166
+ export function saveTelegramToken(token) {
167
+ const value = String(token || '').trim();
168
+ if (!value) throw new Error('Telegram bot token is required');
169
+ saveSecret(SECRET_ACCOUNTS.telegramToken, value);
170
+ return { ok: true, configured: Boolean(getTelegramToken()) };
171
+ }
172
+
173
+ export function forgetTelegramToken() {
174
+ deleteSecret(SECRET_ACCOUNTS.telegramToken);
175
+ return { ok: true };
176
+ }
177
+
135
178
  export function saveWebhookAuthtoken(token) {
136
179
  const value = String(token || '').trim();
137
180
  if (!value) throw new Error('Webhook/ngrok authtoken is required');
@@ -144,19 +187,31 @@ export function forgetWebhookAuthtoken() {
144
187
  return { ok: true };
145
188
  }
146
189
 
147
- export function saveChannel({ name, channelId, mode = 'interactive', main = false } = {}) {
190
+ export function saveChannel({ name, channelId, mode = 'interactive', main = false, backend = null } = {}) {
148
191
  const label = assertName(name, 'channel name');
149
192
  const id = String(channelId || '').trim();
150
193
  if (!id) throw new Error('channelId is required');
151
194
  const nextMode = mode === 'broadcast' ? 'broadcast' : 'interactive';
152
- return updateChannelsSection((cfg) => ({
153
- ...cfg,
154
- mainChannel: main || !cfg.mainChannel ? label : cfg.mainChannel,
155
- channelsConfig: {
156
- ...(cfg.channelsConfig || {}),
157
- [label]: { ...(cfg.channelsConfig?.[label] || {}), channelId: id, mode: nextMode },
158
- },
159
- }));
195
+ const targetBackend = backend === 'telegram' || backend === 'discord' ? backend : null;
196
+ return updateChannelsSection((cfg) => {
197
+ const activeBackend = cfg.backend === 'telegram' ? 'telegram' : 'discord';
198
+ const current = seedBackendChannelIds(cfg.channelsConfig?.[label] || {}, activeBackend);
199
+ const nextEntry = { ...current, mode: nextMode };
200
+ if (targetBackend === 'telegram') nextEntry.telegramChatId = id;
201
+ else if (targetBackend === 'discord') nextEntry.discordChannelId = id;
202
+ else nextEntry.channelId = id;
203
+ nextEntry.channelId = targetBackend
204
+ ? (channelIdForBackend(nextEntry, activeBackend) || (targetBackend === activeBackend ? id : ''))
205
+ : id;
206
+ return {
207
+ ...cfg,
208
+ mainChannel: main || !cfg.mainChannel ? label : cfg.mainChannel,
209
+ channelsConfig: {
210
+ ...(cfg.channelsConfig || {}),
211
+ [label]: nextEntry,
212
+ },
213
+ };
214
+ });
160
215
  }
161
216
 
162
217
  export function deleteChannel(name) {
@@ -183,6 +238,25 @@ export function setWebhookConfig(patch = {}) {
183
238
  }));
184
239
  }
185
240
 
241
+ export function setBackend(name) {
242
+ const value = String(name || '').trim();
243
+ if (value !== 'discord' && value !== 'telegram') {
244
+ throw new Error('backend must be discord or telegram');
245
+ }
246
+ updateChannelsSection((cfg) => {
247
+ const activeBackend = cfg.backend === 'telegram' ? 'telegram' : 'discord';
248
+ const channelsConfig = Object.fromEntries(Object.entries(cfg.channelsConfig || {}).map(([key, entry]) => [
249
+ key,
250
+ (() => {
251
+ const seeded = seedBackendChannelIds(entry, activeBackend);
252
+ return { ...seeded, channelId: channelIdForBackend(seeded, value) };
253
+ })(),
254
+ ]));
255
+ return { ...cfg, channelsConfig, backend: value };
256
+ });
257
+ return { ok: true, backend: value };
258
+ }
259
+
186
260
  function normalizeCron(time) {
187
261
  const value = String(time || '').trim();
188
262
  const parts = value.split(/\s+/).filter(Boolean);
@@ -195,12 +269,15 @@ function normalizeCron(time) {
195
269
  export function listSchedules() {
196
270
  return listEntryDirs(schedulesDir()).map((name) => {
197
271
  const dir = join(schedulesDir(), name);
198
- const config = readJson(join(dir, 'config.json'), {});
199
- const instructions = readText(join(dir, 'instructions.md'), '');
272
+ const { frontmatter, body } = readMarkdownDocument(readText(join(dir, 'SCHEDULE.md'), ''));
273
+ const config = { ...frontmatter };
274
+ if (Object.prototype.hasOwnProperty.call(config, 'enabled')) {
275
+ config.enabled = config.enabled !== 'false' && config.enabled !== false;
276
+ }
200
277
  return {
201
278
  name,
202
279
  ...config,
203
- instructions,
280
+ instructions: body,
204
281
  route: config.channel ? `channel:${config.channel}` : 'session',
205
282
  };
206
283
  });
@@ -231,8 +308,7 @@ export function saveSchedule({
231
308
  if (channel) config.channel = String(channel).trim();
232
309
  if (model) config.model = String(model).trim();
233
310
  if (enabled === false) config.enabled = false;
234
- writeJsonAtomic(join(dir, 'config.json'), config);
235
- writeTextAtomic(join(dir, 'instructions.md'), body + '\n');
311
+ writeTextAtomic(join(dir, 'SCHEDULE.md'), serializeFrontmatterDoc(config, body));
236
312
  return { name: id, ...config, instructions: body };
237
313
  }
238
314
 
@@ -245,24 +321,31 @@ export function deleteSchedule(name) {
245
321
  export function setScheduleEnabled(name, enabled) {
246
322
  const id = assertName(name, 'schedule name');
247
323
  const dir = join(schedulesDir(), id);
248
- const configPath = join(dir, 'config.json');
249
- if (!existsSync(configPath)) throw new Error(`schedule "${id}" does not exist`);
250
- const config = readJson(configPath, {});
251
- writeJsonAtomic(configPath, { ...config, enabled: enabled !== false });
324
+ const mdPath = join(dir, 'SCHEDULE.md');
325
+ if (!existsSync(mdPath)) throw new Error(`schedule "${id}" does not exist`);
326
+ const { frontmatter, body } = readMarkdownDocument(readText(mdPath, ''));
327
+ writeTextAtomic(mdPath, serializeFrontmatterDoc({ ...frontmatter, enabled: enabled !== false }, body));
252
328
  return { name: id, enabled: enabled !== false };
253
329
  }
254
330
 
255
331
  export function listWebhooks() {
256
332
  return listEntryDirs(webhooksDir()).map((name) => {
257
333
  const dir = join(webhooksDir(), name);
258
- const config = readJson(join(dir, 'config.json'), {});
259
- const instructions = readText(join(dir, 'instructions.md'), '');
334
+ const { frontmatter, body } = readMarkdownDocument(readText(join(dir, 'WEBHOOK.md'), ''));
335
+ const config = { ...frontmatter };
336
+ if (Object.prototype.hasOwnProperty.call(config, 'enabled')) {
337
+ config.enabled = config.enabled !== 'false' && config.enabled !== false;
338
+ }
339
+ // Secret lives in a side file (<name>/secret), never in frontmatter, so
340
+ // an arbitrary user-supplied secret (quotes/colon/newline) round-trips
341
+ // losslessly and setWebhookEnabled cannot corrupt it.
342
+ const hasSecret = Boolean(readText(join(dir, 'secret'), '').trim());
260
343
  return {
261
344
  name,
262
345
  ...config,
263
- secretSet: Boolean(config.secret),
346
+ secretSet: hasSecret,
264
347
  secret: undefined,
265
- instructions,
348
+ instructions: body,
266
349
  route: config.channel ? `channel:${config.channel}` : 'session',
267
350
  };
268
351
  });
@@ -289,16 +372,17 @@ export function saveWebhook({
289
372
  const dir = join(webhooksDir(), id);
290
373
  if (existsSync(dir) && overwrite !== true) throw new Error(`webhook "${id}" already exists`);
291
374
  mkdirSync(dir, { recursive: true });
292
- const config = {
293
- secret: String(secret || randomBytes(24).toString('hex')).trim(),
294
- parser: nextParser,
295
- };
375
+ const secretValue = String(secret || randomBytes(24).toString('hex')).trim();
376
+ const config = { parser: nextParser };
296
377
  if (channel) config.channel = String(channel).trim();
297
378
  if (model) config.model = String(model).trim();
298
379
  if (enabled === false) config.enabled = false;
299
- writeJsonAtomic(join(dir, 'config.json'), config);
300
- writeTextAtomic(join(dir, 'instructions.md'), body + '\n');
301
- return { name: id, ...config, instructions: body };
380
+ // Secret to a side file (plaintext, same exposure level as the former
381
+ // config.json), kept OUT of the frontmatter to avoid unquote round-trip
382
+ // corruption. deleteWebhook rmSync's the whole dir, so this is removed too.
383
+ writeTextAtomic(join(dir, 'secret'), secretValue + '\n');
384
+ writeTextAtomic(join(dir, 'WEBHOOK.md'), serializeFrontmatterDoc(config, body));
385
+ return { name: id, ...config, secret: secretValue, instructions: body };
302
386
  }
303
387
 
304
388
  export function deleteWebhook(name) {
@@ -310,10 +394,10 @@ export function deleteWebhook(name) {
310
394
  export function setWebhookEnabled(name, enabled) {
311
395
  const id = assertName(name, 'webhook name');
312
396
  const dir = join(webhooksDir(), id);
313
- const configPath = join(dir, 'config.json');
314
- if (!existsSync(configPath)) throw new Error(`webhook "${id}" does not exist`);
315
- const config = readJson(configPath, {});
316
- writeJsonAtomic(configPath, { ...config, enabled: enabled !== false });
397
+ const mdPath = join(dir, 'WEBHOOK.md');
398
+ if (!existsSync(mdPath)) throw new Error(`webhook "${id}" does not exist`);
399
+ const { frontmatter, body } = readMarkdownDocument(readText(mdPath, ''));
400
+ writeTextAtomic(mdPath, serializeFrontmatterDoc({ ...frontmatter, enabled: enabled !== false }, body));
317
401
  return { name: id, enabled: enabled !== false };
318
402
  }
319
403
 
@@ -322,7 +406,9 @@ export function channelSetup(config = null) {
322
406
  const discordToken = getDiscordToken();
323
407
  const discordProblem = diagnoseDiscordTokenValue(discordToken, cfg);
324
408
  const webhookAuth = getWebhookAuthtoken();
409
+ const telegramToken = getTelegramToken();
325
410
  return {
411
+ backend: cfg.backend || 'discord',
326
412
  discord: {
327
413
  backend: 'discord',
328
414
  authenticated: Boolean(discordToken && !discordProblem),
@@ -330,6 +416,13 @@ export function channelSetup(config = null) {
330
416
  status: discordToken ? (discordProblem ? 'Invalid' : 'Set') : 'Off',
331
417
  problem: discordProblem || null,
332
418
  },
419
+ telegram: {
420
+ backend: 'telegram',
421
+ authenticated: Boolean(telegramToken),
422
+ stored: hasStoredSecret(SECRET_ACCOUNTS.telegramToken),
423
+ status: telegramToken ? 'Set' : 'Off',
424
+ problem: null,
425
+ },
333
426
  webhook: {
334
427
  ...(cfg.webhook || {}),
335
428
  authenticated: Boolean(webhookAuth),