discoclaw 0.1.0

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 (393) hide show
  1. package/.context/README.md +42 -0
  2. package/.context/architecture.md +58 -0
  3. package/.context/bot-setup.md +24 -0
  4. package/.context/dev.md +230 -0
  5. package/.context/discord.md +144 -0
  6. package/.context/memory.md +257 -0
  7. package/.context/ops.md +59 -0
  8. package/.context/pa-safety.md +47 -0
  9. package/.context/pa.md +118 -0
  10. package/.context/project.md +43 -0
  11. package/.context/runtime.md +253 -0
  12. package/.context/tasks.md +71 -0
  13. package/.context/tools.md +75 -0
  14. package/.env.example +88 -0
  15. package/.env.example.full +378 -0
  16. package/LICENSE +21 -0
  17. package/README.md +220 -0
  18. package/dist/beads/auto-tag.js +2 -0
  19. package/dist/beads/auto-tag.test.js +62 -0
  20. package/dist/beads/bd-cli.js +9 -0
  21. package/dist/beads/bd-cli.test.js +495 -0
  22. package/dist/beads/bead-hooks-cli.js +149 -0
  23. package/dist/beads/bead-sync-cli.js +5 -0
  24. package/dist/beads/bead-sync-cli.test.js +72 -0
  25. package/dist/beads/bead-sync-coordinator.js +4 -0
  26. package/dist/beads/bead-sync-coordinator.test.js +239 -0
  27. package/dist/beads/bead-sync-watcher.js +2 -0
  28. package/dist/beads/bead-sync-watcher.test.js +96 -0
  29. package/dist/beads/bead-sync.js +7 -0
  30. package/dist/beads/bead-sync.test.js +876 -0
  31. package/dist/beads/bead-thread-cache.js +8 -0
  32. package/dist/beads/bead-thread-cache.test.js +91 -0
  33. package/dist/beads/discord-sync.js +18 -0
  34. package/dist/beads/discord-sync.test.js +782 -0
  35. package/dist/beads/find-bead-by-thread.test.js +36 -0
  36. package/dist/beads/forum-guard.js +2 -0
  37. package/dist/beads/forum-guard.test.js +204 -0
  38. package/dist/beads/initialize.js +3 -0
  39. package/dist/beads/initialize.test.js +304 -0
  40. package/dist/beads/types.js +10 -0
  41. package/dist/cli/daemon-installer.js +225 -0
  42. package/dist/cli/daemon-installer.test.js +289 -0
  43. package/dist/cli/index.js +42 -0
  44. package/dist/cli/init-wizard.js +374 -0
  45. package/dist/cli/init-wizard.test.js +191 -0
  46. package/dist/config.js +385 -0
  47. package/dist/config.test.js +589 -0
  48. package/dist/cron/auto-tag.js +100 -0
  49. package/dist/cron/auto-tag.test.js +91 -0
  50. package/dist/cron/cadence.js +74 -0
  51. package/dist/cron/cadence.test.js +53 -0
  52. package/dist/cron/cron-sync-coordinator.js +66 -0
  53. package/dist/cron/cron-sync-coordinator.test.js +118 -0
  54. package/dist/cron/cron-sync.js +165 -0
  55. package/dist/cron/cron-sync.test.js +228 -0
  56. package/dist/cron/cron-tag-map-watcher.js +128 -0
  57. package/dist/cron/cron-tag-map-watcher.test.js +155 -0
  58. package/dist/cron/default-timezone.js +23 -0
  59. package/dist/cron/default-timezone.test.js +30 -0
  60. package/dist/cron/discord-sync.js +205 -0
  61. package/dist/cron/discord-sync.test.js +353 -0
  62. package/dist/cron/executor.js +303 -0
  63. package/dist/cron/executor.test.js +614 -0
  64. package/dist/cron/forum-sync.js +347 -0
  65. package/dist/cron/forum-sync.test.js +539 -0
  66. package/dist/cron/job-lock.js +164 -0
  67. package/dist/cron/job-lock.test.js +178 -0
  68. package/dist/cron/parser.js +68 -0
  69. package/dist/cron/parser.test.js +115 -0
  70. package/dist/cron/run-control.js +24 -0
  71. package/dist/cron/run-control.test.js +27 -0
  72. package/dist/cron/run-stats.js +265 -0
  73. package/dist/cron/run-stats.test.js +160 -0
  74. package/dist/cron/scheduler.js +97 -0
  75. package/dist/cron/scheduler.test.js +112 -0
  76. package/dist/cron/tag-map.js +47 -0
  77. package/dist/cron/tag-map.test.js +64 -0
  78. package/dist/cron/types.js +1 -0
  79. package/dist/discoclaw-plan-format.test.js +137 -0
  80. package/dist/discoclaw-recipe-format.test.js +137 -0
  81. package/dist/discord/abort-registry.js +70 -0
  82. package/dist/discord/action-categories.js +36 -0
  83. package/dist/discord/action-types.js +1 -0
  84. package/dist/discord/action-utils.js +58 -0
  85. package/dist/discord/action-utils.test.js +58 -0
  86. package/dist/discord/actions-beads.js +1 -0
  87. package/dist/discord/actions-beads.test.js +372 -0
  88. package/dist/discord/actions-bot-profile.js +107 -0
  89. package/dist/discord/actions-bot-profile.test.js +138 -0
  90. package/dist/discord/actions-channels.js +427 -0
  91. package/dist/discord/actions-channels.test.js +697 -0
  92. package/dist/discord/actions-config.js +173 -0
  93. package/dist/discord/actions-config.test.js +322 -0
  94. package/dist/discord/actions-crons.js +586 -0
  95. package/dist/discord/actions-crons.test.js +499 -0
  96. package/dist/discord/actions-defer.js +60 -0
  97. package/dist/discord/actions-defer.test.js +134 -0
  98. package/dist/discord/actions-forge.js +134 -0
  99. package/dist/discord/actions-forge.test.js +206 -0
  100. package/dist/discord/actions-guild.js +301 -0
  101. package/dist/discord/actions-guild.test.js +386 -0
  102. package/dist/discord/actions-memory.js +106 -0
  103. package/dist/discord/actions-memory.test.js +248 -0
  104. package/dist/discord/actions-messaging.js +401 -0
  105. package/dist/discord/actions-messaging.test.js +738 -0
  106. package/dist/discord/actions-moderation.js +65 -0
  107. package/dist/discord/actions-moderation.test.js +88 -0
  108. package/dist/discord/actions-plan.js +445 -0
  109. package/dist/discord/actions-plan.test.js +610 -0
  110. package/dist/discord/actions-poll.js +38 -0
  111. package/dist/discord/actions-poll.test.js +93 -0
  112. package/dist/discord/actions-tasks.js +3 -0
  113. package/dist/discord/actions-tasks.test.js +418 -0
  114. package/dist/discord/actions.js +600 -0
  115. package/dist/discord/actions.test.js +522 -0
  116. package/dist/discord/allowed-mentions.js +3 -0
  117. package/dist/discord/allowed-mentions.test.js +17 -0
  118. package/dist/discord/allowlist.js +29 -0
  119. package/dist/discord/allowlist.test.js +24 -0
  120. package/dist/discord/audit-handler.js +191 -0
  121. package/dist/discord/audit-handler.test.js +361 -0
  122. package/dist/discord/bot.js +141 -0
  123. package/dist/discord/channel-context.js +181 -0
  124. package/dist/discord/defer-scheduler.js +45 -0
  125. package/dist/discord/destructive-confirmation.js +128 -0
  126. package/dist/discord/destructive-confirmation.test.js +49 -0
  127. package/dist/discord/discord-plan-auto-implement.test.js +18 -0
  128. package/dist/discord/durable-memory.js +145 -0
  129. package/dist/discord/durable-memory.test.js +281 -0
  130. package/dist/discord/durable-write-queue.js +4 -0
  131. package/dist/discord/file-download.js +308 -0
  132. package/dist/discord/file-download.test.js +303 -0
  133. package/dist/discord/forge-audit-verdict.js +140 -0
  134. package/dist/discord/forge-auto-implement.js +80 -0
  135. package/dist/discord/forge-auto-implement.test.js +110 -0
  136. package/dist/discord/forge-commands.js +698 -0
  137. package/dist/discord/forge-commands.test.js +1606 -0
  138. package/dist/discord/forge-plan-registry.js +68 -0
  139. package/dist/discord/forge-plan-registry.test.js +127 -0
  140. package/dist/discord/forum-count-sync.js +130 -0
  141. package/dist/discord/forum-count-sync.test.js +200 -0
  142. package/dist/discord/health-command.js +98 -0
  143. package/dist/discord/health-command.test.js +195 -0
  144. package/dist/discord/help-command.js +22 -0
  145. package/dist/discord/help-command.test.js +49 -0
  146. package/dist/discord/image-download.js +201 -0
  147. package/dist/discord/image-download.test.js +499 -0
  148. package/dist/discord/inflight-replies.js +228 -0
  149. package/dist/discord/inflight-replies.test.js +295 -0
  150. package/dist/discord/json-extract.js +110 -0
  151. package/dist/discord/keyed-queue.js +22 -0
  152. package/dist/discord/memory-commands.js +85 -0
  153. package/dist/discord/memory-commands.test.js +159 -0
  154. package/dist/discord/memory-timing.integration.test.js +159 -0
  155. package/dist/discord/message-coordinator.js +2347 -0
  156. package/dist/discord/message-coordinator.onboarding.test.js +183 -0
  157. package/dist/discord/message-coordinator.plan-run.test.js +264 -0
  158. package/dist/discord/message-history.js +53 -0
  159. package/dist/discord/message-history.test.js +95 -0
  160. package/dist/discord/models-command.js +59 -0
  161. package/dist/discord/models-command.test.js +150 -0
  162. package/dist/discord/nickname.test.js +76 -0
  163. package/dist/discord/onboarding-completion.js +55 -0
  164. package/dist/discord/onboarding-completion.test.js +176 -0
  165. package/dist/discord/output-common.js +178 -0
  166. package/dist/discord/output-common.test.js +198 -0
  167. package/dist/discord/output-utils.js +156 -0
  168. package/dist/discord/parse-identity-name.test.js +129 -0
  169. package/dist/discord/plan-commands.js +612 -0
  170. package/dist/discord/plan-commands.test.js +1622 -0
  171. package/dist/discord/plan-manager.js +1491 -0
  172. package/dist/discord/plan-manager.test.js +2380 -0
  173. package/dist/discord/plan-parser.js +110 -0
  174. package/dist/discord/plan-parser.test.js +63 -0
  175. package/dist/discord/plan-run-phase-start.js +20 -0
  176. package/dist/discord/plan-run-phase-start.test.js +29 -0
  177. package/dist/discord/platform-message.js +45 -0
  178. package/dist/discord/platform-message.test.js +110 -0
  179. package/dist/discord/prompt-common.js +240 -0
  180. package/dist/discord/prompt-common.test.js +423 -0
  181. package/dist/discord/reaction-handler.js +691 -0
  182. package/dist/discord/reaction-handler.test.js +1574 -0
  183. package/dist/discord/reaction-prompts.js +118 -0
  184. package/dist/discord/reaction-prompts.test.js +253 -0
  185. package/dist/discord/reply-reference.js +66 -0
  186. package/dist/discord/reply-reference.test.js +125 -0
  187. package/dist/discord/restart-command.js +143 -0
  188. package/dist/discord/restart-command.test.js +196 -0
  189. package/dist/discord/runtime-utils.js +43 -0
  190. package/dist/discord/runtime-utils.test.js +112 -0
  191. package/dist/discord/session-key.js +7 -0
  192. package/dist/discord/session-key.test.js +13 -0
  193. package/dist/discord/shortterm-memory.js +166 -0
  194. package/dist/discord/shortterm-memory.test.js +345 -0
  195. package/dist/discord/shutdown-context.js +122 -0
  196. package/dist/discord/shutdown-context.test.js +279 -0
  197. package/dist/discord/startup-profile.test.js +214 -0
  198. package/dist/discord/status-channel.js +190 -0
  199. package/dist/discord/status-channel.test.js +282 -0
  200. package/dist/discord/status-command.js +206 -0
  201. package/dist/discord/status-command.test.js +341 -0
  202. package/dist/discord/streaming-progress.js +107 -0
  203. package/dist/discord/streaming-progress.test.js +93 -0
  204. package/dist/discord/summarizer.js +89 -0
  205. package/dist/discord/summarizer.test.js +245 -0
  206. package/dist/discord/system-bootstrap.js +396 -0
  207. package/dist/discord/system-bootstrap.test.js +724 -0
  208. package/dist/discord/thread-context.js +169 -0
  209. package/dist/discord/thread-context.test.js +386 -0
  210. package/dist/discord/tool-aware-queue.js +116 -0
  211. package/dist/discord/tool-aware-queue.test.js +180 -0
  212. package/dist/discord/update-command.js +127 -0
  213. package/dist/discord/update-command.test.js +275 -0
  214. package/dist/discord/user-errors.js +40 -0
  215. package/dist/discord/user-errors.test.js +31 -0
  216. package/dist/discord/user-turn-to-durable.js +111 -0
  217. package/dist/discord/user-turn-to-durable.test.js +273 -0
  218. package/dist/discord-followup.test.js +677 -0
  219. package/dist/discord.channel-context.test.js +95 -0
  220. package/dist/discord.fail-closed.test.js +199 -0
  221. package/dist/discord.health-command.integration.test.js +140 -0
  222. package/dist/discord.js +190 -0
  223. package/dist/discord.prompt-context.test.js +1431 -0
  224. package/dist/discord.render.test.js +621 -0
  225. package/dist/discord.status-wiring.test.js +187 -0
  226. package/dist/engine/claudeCli.js +137 -0
  227. package/dist/engine/types.js +1 -0
  228. package/dist/group-queue.js +25 -0
  229. package/dist/health/credential-check.js +175 -0
  230. package/dist/health/credential-check.test.js +401 -0
  231. package/dist/health/startup-healing.js +139 -0
  232. package/dist/health/startup-healing.test.js +298 -0
  233. package/dist/identity.js +36 -0
  234. package/dist/index.js +1378 -0
  235. package/dist/logging/logger-like.js +1 -0
  236. package/dist/observability/memory-sampler.js +51 -0
  237. package/dist/observability/memory-sampler.test.js +93 -0
  238. package/dist/observability/metrics.js +88 -0
  239. package/dist/observability/metrics.test.js +42 -0
  240. package/dist/onboarding/onboarding-flow.js +246 -0
  241. package/dist/onboarding/onboarding-flow.test.js +238 -0
  242. package/dist/onboarding/onboarding-writer.js +102 -0
  243. package/dist/onboarding/onboarding-writer.test.js +143 -0
  244. package/dist/pidlock.js +187 -0
  245. package/dist/pidlock.test.js +128 -0
  246. package/dist/pipeline/engine.js +206 -0
  247. package/dist/pipeline/engine.test.js +771 -0
  248. package/dist/root-policy.js +21 -0
  249. package/dist/root-policy.test.js +55 -0
  250. package/dist/runtime/claude-code-cli.js +35 -0
  251. package/dist/runtime/claude-code-cli.test.js +1199 -0
  252. package/dist/runtime/cli-adapter.js +584 -0
  253. package/dist/runtime/cli-output-parsers.js +108 -0
  254. package/dist/runtime/cli-shared.js +96 -0
  255. package/dist/runtime/cli-shared.test.js +104 -0
  256. package/dist/runtime/cli-strategy.js +6 -0
  257. package/dist/runtime/codex-cli.js +16 -0
  258. package/dist/runtime/codex-cli.test.js +862 -0
  259. package/dist/runtime/concurrency-limit.js +80 -0
  260. package/dist/runtime/concurrency-limit.test.js +137 -0
  261. package/dist/runtime/gemini-cli.js +16 -0
  262. package/dist/runtime/gemini-cli.test.js +413 -0
  263. package/dist/runtime/long-running-process.js +415 -0
  264. package/dist/runtime/long-running-process.test.js +318 -0
  265. package/dist/runtime/model-smoke-helpers.js +160 -0
  266. package/dist/runtime/model-smoke.test.js +194 -0
  267. package/dist/runtime/model-tiers.js +33 -0
  268. package/dist/runtime/model-tiers.test.js +65 -0
  269. package/dist/runtime/openai-auth.js +151 -0
  270. package/dist/runtime/openai-auth.test.js +361 -0
  271. package/dist/runtime/openai-compat.js +178 -0
  272. package/dist/runtime/openai-compat.test.js +449 -0
  273. package/dist/runtime/process-pool.js +93 -0
  274. package/dist/runtime/process-pool.test.js +148 -0
  275. package/dist/runtime/registry.js +15 -0
  276. package/dist/runtime/registry.test.js +47 -0
  277. package/dist/runtime/session-scanner.js +186 -0
  278. package/dist/runtime/session-scanner.test.js +257 -0
  279. package/dist/runtime/strategies/claude-strategy.js +193 -0
  280. package/dist/runtime/strategies/codex-strategy.js +161 -0
  281. package/dist/runtime/strategies/gemini-strategy.js +64 -0
  282. package/dist/runtime/strategies/template-strategy.js +85 -0
  283. package/dist/runtime/tool-capabilities.js +27 -0
  284. package/dist/runtime/tool-capabilities.test.js +24 -0
  285. package/dist/runtime/tool-labels.js +48 -0
  286. package/dist/runtime/types.js +2 -0
  287. package/dist/sessionManager.js +47 -0
  288. package/dist/sessions.js +18 -0
  289. package/dist/tasks/architecture-contract.js +33 -0
  290. package/dist/tasks/architecture-contract.test.js +90 -0
  291. package/dist/tasks/auto-tag.js +50 -0
  292. package/dist/tasks/auto-tag.test.js +64 -0
  293. package/dist/tasks/bd-cli.js +164 -0
  294. package/dist/tasks/bd-cli.test.js +359 -0
  295. package/dist/tasks/bead-sync.js +1 -0
  296. package/dist/tasks/context-summary.js +27 -0
  297. package/dist/tasks/discord-sync.js +3 -0
  298. package/dist/tasks/discord-sync.test.js +685 -0
  299. package/dist/tasks/discord-types.js +4 -0
  300. package/dist/tasks/find-task-by-thread.test.js +36 -0
  301. package/dist/tasks/forum-guard.js +81 -0
  302. package/dist/tasks/forum-guard.test.js +192 -0
  303. package/dist/tasks/initialize.js +77 -0
  304. package/dist/tasks/initialize.test.js +263 -0
  305. package/dist/tasks/logger-types.js +1 -0
  306. package/dist/tasks/metrics-types.js +3 -0
  307. package/dist/tasks/migrate.js +33 -0
  308. package/dist/tasks/migrate.test.js +156 -0
  309. package/dist/tasks/path-defaults.js +67 -0
  310. package/dist/tasks/path-defaults.test.js +73 -0
  311. package/dist/tasks/runtime-types.js +1 -0
  312. package/dist/tasks/service.js +33 -0
  313. package/dist/tasks/service.test.js +51 -0
  314. package/dist/tasks/store.js +238 -0
  315. package/dist/tasks/store.test.js +417 -0
  316. package/dist/tasks/sync-context.js +1 -0
  317. package/dist/tasks/sync-contract.js +24 -0
  318. package/dist/tasks/sync-contract.test.js +25 -0
  319. package/dist/tasks/sync-coordinator-metrics.js +41 -0
  320. package/dist/tasks/sync-coordinator-retries.js +71 -0
  321. package/dist/tasks/sync-coordinator.js +96 -0
  322. package/dist/tasks/sync-coordinator.test.js +501 -0
  323. package/dist/tasks/sync-types.js +1 -0
  324. package/dist/tasks/sync-watcher.js +27 -0
  325. package/dist/tasks/sync-watcher.test.js +92 -0
  326. package/dist/tasks/tag-map.js +36 -0
  327. package/dist/tasks/tag-map.test.js +54 -0
  328. package/dist/tasks/task-action-contract.js +16 -0
  329. package/dist/tasks/task-action-contract.test.js +16 -0
  330. package/dist/tasks/task-action-executor.js +18 -0
  331. package/dist/tasks/task-action-executor.test.js +420 -0
  332. package/dist/tasks/task-action-mutation-helpers.js +17 -0
  333. package/dist/tasks/task-action-mutations.js +151 -0
  334. package/dist/tasks/task-action-prompt.js +62 -0
  335. package/dist/tasks/task-action-read-ops.js +73 -0
  336. package/dist/tasks/task-action-runner-types.js +1 -0
  337. package/dist/tasks/task-action-thread-sync.js +82 -0
  338. package/dist/tasks/task-actions.js +3 -0
  339. package/dist/tasks/task-cli.js +227 -0
  340. package/dist/tasks/task-context.js +1 -0
  341. package/dist/tasks/task-lifecycle.js +46 -0
  342. package/dist/tasks/task-lifecycle.test.js +35 -0
  343. package/dist/tasks/task-sync-apply-plan.js +95 -0
  344. package/dist/tasks/task-sync-apply-types.js +12 -0
  345. package/dist/tasks/task-sync-apply.js +319 -0
  346. package/dist/tasks/task-sync-cli.js +89 -0
  347. package/dist/tasks/task-sync-cli.test.js +70 -0
  348. package/dist/tasks/task-sync-engine.js +88 -0
  349. package/dist/tasks/task-sync-engine.test.js +934 -0
  350. package/dist/tasks/task-sync-phase-apply.js +171 -0
  351. package/dist/tasks/task-sync-pipeline.js +2 -0
  352. package/dist/tasks/task-sync-pipeline.test.js +265 -0
  353. package/dist/tasks/task-sync-reconcile-plan.js +182 -0
  354. package/dist/tasks/task-sync-reconcile.js +144 -0
  355. package/dist/tasks/task-sync.js +56 -0
  356. package/dist/tasks/task-sync.test.js +86 -0
  357. package/dist/tasks/thread-cache.js +42 -0
  358. package/dist/tasks/thread-cache.test.js +89 -0
  359. package/dist/tasks/thread-contracts.test.js +711 -0
  360. package/dist/tasks/thread-forum-ops.js +68 -0
  361. package/dist/tasks/thread-helpers.js +86 -0
  362. package/dist/tasks/thread-helpers.test.js +33 -0
  363. package/dist/tasks/thread-lifecycle-ops.js +144 -0
  364. package/dist/tasks/thread-ops-shared.js +21 -0
  365. package/dist/tasks/thread-ops.js +2 -0
  366. package/dist/tasks/types.js +20 -0
  367. package/dist/tasks/types.test.js +60 -0
  368. package/dist/test-setup.js +11 -0
  369. package/dist/test-setup.test.js +42 -0
  370. package/dist/transport/types.js +1 -0
  371. package/dist/validate.js +41 -0
  372. package/dist/validate.test.js +94 -0
  373. package/dist/version.js +15 -0
  374. package/dist/version.test.js +31 -0
  375. package/dist/webhook/server.js +199 -0
  376. package/dist/webhook/server.test.js +460 -0
  377. package/dist/workspace-bootstrap.js +135 -0
  378. package/dist/workspace-bootstrap.test.js +514 -0
  379. package/dist/workspace-permissions.js +134 -0
  380. package/dist/workspace-permissions.test.js +181 -0
  381. package/package.json +74 -0
  382. package/scripts/cron/cron-tag-map.json +9 -0
  383. package/scripts/tasks/tag-map.json +10 -0
  384. package/systemd/discoclaw.service +19 -0
  385. package/templates/recipes/integration.discoclaw-recipe.md +171 -0
  386. package/templates/workspace/AGENTS.md +217 -0
  387. package/templates/workspace/BOOTSTRAP.md +1 -0
  388. package/templates/workspace/HEARTBEAT.md +10 -0
  389. package/templates/workspace/IDENTITY.md +16 -0
  390. package/templates/workspace/MEMORY.md +24 -0
  391. package/templates/workspace/SOUL.md +52 -0
  392. package/templates/workspace/TOOLS.md +304 -0
  393. package/templates/workspace/USER.md +37 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ export class MemorySampler {
2
+ rssHwm = 0;
3
+ heapUsedHwm = 0;
4
+ count = 0;
5
+ source;
6
+ constructor(source = process.memoryUsage) {
7
+ this.source = source;
8
+ }
9
+ /** Take a sample and update high-water marks. Returns current stats. */
10
+ sample() {
11
+ const usage = this.source();
12
+ this.count++;
13
+ if (usage.rss > this.rssHwm)
14
+ this.rssHwm = usage.rss;
15
+ if (usage.heapUsed > this.heapUsedHwm)
16
+ this.heapUsedHwm = usage.heapUsed;
17
+ return this.current(usage);
18
+ }
19
+ /** Read current stats without updating counters or HWMs. */
20
+ peek() {
21
+ return this.current(this.source());
22
+ }
23
+ /** Reset high-water marks and sample count. */
24
+ reset() {
25
+ this.rssHwm = 0;
26
+ this.heapUsedHwm = 0;
27
+ this.count = 0;
28
+ }
29
+ current(usage) {
30
+ return {
31
+ rssBytes: usage.rss,
32
+ heapUsedBytes: usage.heapUsed,
33
+ heapTotalBytes: usage.heapTotal,
34
+ externalBytes: usage.external,
35
+ rssHwmBytes: Math.max(this.rssHwm, usage.rss),
36
+ heapUsedHwmBytes: Math.max(this.heapUsedHwm, usage.heapUsed),
37
+ sampleCount: this.count,
38
+ };
39
+ }
40
+ }
41
+ /** Format bytes as a human-readable MiB string, e.g. "42.1 MiB" */
42
+ export function formatMiB(bytes) {
43
+ return `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
44
+ }
45
+ /** Render a one-line memory summary suitable for `!health verbose`. */
46
+ export function renderMemoryLine(stats) {
47
+ return (`Memory: rss=${formatMiB(stats.rssBytes)} heapUsed=${formatMiB(stats.heapUsedBytes)}` +
48
+ ` heapTotal=${formatMiB(stats.heapTotalBytes)}` +
49
+ ` hwm(rss=${formatMiB(stats.rssHwmBytes)} heap=${formatMiB(stats.heapUsedHwmBytes)})` +
50
+ ` samples=${stats.sampleCount}`);
51
+ }
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { MemorySampler, formatMiB, renderMemoryLine } from './memory-sampler.js';
3
+ function makeSource(values) {
4
+ let idx = 0;
5
+ return () => {
6
+ const v = values[Math.min(idx++, values.length - 1)];
7
+ return { rss: 0, heapUsed: 0, heapTotal: 0, external: 0, arrayBuffers: 0, ...v };
8
+ };
9
+ }
10
+ describe('MemorySampler', () => {
11
+ it('returns current values from a single sample', () => {
12
+ const sampler = new MemorySampler(makeSource([{ rss: 100, heapUsed: 50, heapTotal: 80, external: 5 }]));
13
+ const stats = sampler.sample();
14
+ expect(stats.rssBytes).toBe(100);
15
+ expect(stats.heapUsedBytes).toBe(50);
16
+ expect(stats.heapTotalBytes).toBe(80);
17
+ expect(stats.externalBytes).toBe(5);
18
+ expect(stats.sampleCount).toBe(1);
19
+ });
20
+ it('tracks high-water marks across multiple samples', () => {
21
+ const sampler = new MemorySampler(makeSource([
22
+ { rss: 100, heapUsed: 50 },
23
+ { rss: 200, heapUsed: 30 },
24
+ { rss: 150, heapUsed: 80 },
25
+ ]));
26
+ sampler.sample();
27
+ sampler.sample();
28
+ const stats = sampler.sample();
29
+ expect(stats.rssHwmBytes).toBe(200);
30
+ expect(stats.heapUsedHwmBytes).toBe(80);
31
+ expect(stats.sampleCount).toBe(3);
32
+ });
33
+ it('hwm equals current value when current exceeds prior hwm', () => {
34
+ const sampler = new MemorySampler(makeSource([
35
+ { rss: 100, heapUsed: 50 },
36
+ { rss: 300, heapUsed: 200 },
37
+ ]));
38
+ sampler.sample();
39
+ const stats = sampler.sample();
40
+ expect(stats.rssHwmBytes).toBe(300);
41
+ expect(stats.heapUsedHwmBytes).toBe(200);
42
+ });
43
+ it('peek does not increment sample count or update hwm', () => {
44
+ const sampler = new MemorySampler(makeSource([
45
+ { rss: 100, heapUsed: 50 },
46
+ { rss: 999, heapUsed: 999 },
47
+ ]));
48
+ sampler.sample();
49
+ const peeked = sampler.peek();
50
+ expect(peeked.sampleCount).toBe(1);
51
+ // HWM should reflect sample (100/50), not the peek value (999/999)
52
+ expect(peeked.rssHwmBytes).toBe(999);
53
+ expect(peeked.heapUsedHwmBytes).toBe(999);
54
+ // But internal hwm state remains at 100/50 — verify via another sample
55
+ const sampler2 = new MemorySampler(makeSource([{ rss: 100, heapUsed: 50 }]));
56
+ sampler2.sample();
57
+ const after = sampler2.peek();
58
+ expect(after.sampleCount).toBe(1);
59
+ });
60
+ it('reset clears hwm and count', () => {
61
+ const sampler = new MemorySampler(makeSource([
62
+ { rss: 500, heapUsed: 300 },
63
+ { rss: 10, heapUsed: 5 },
64
+ ]));
65
+ sampler.sample();
66
+ sampler.reset();
67
+ const stats = sampler.sample();
68
+ expect(stats.rssHwmBytes).toBe(10);
69
+ expect(stats.heapUsedHwmBytes).toBe(5);
70
+ expect(stats.sampleCount).toBe(1);
71
+ });
72
+ });
73
+ describe('formatMiB', () => {
74
+ it('formats bytes to one decimal MiB', () => {
75
+ expect(formatMiB(1024 * 1024)).toBe('1.0 MiB');
76
+ expect(formatMiB(1.5 * 1024 * 1024)).toBe('1.5 MiB');
77
+ expect(formatMiB(0)).toBe('0.0 MiB');
78
+ });
79
+ });
80
+ describe('renderMemoryLine', () => {
81
+ it('produces a correctly formatted line', () => {
82
+ const line = renderMemoryLine({
83
+ rssBytes: 50 * 1024 * 1024,
84
+ heapUsedBytes: 30 * 1024 * 1024,
85
+ heapTotalBytes: 40 * 1024 * 1024,
86
+ externalBytes: 2 * 1024 * 1024,
87
+ rssHwmBytes: 60 * 1024 * 1024,
88
+ heapUsedHwmBytes: 35 * 1024 * 1024,
89
+ sampleCount: 12,
90
+ });
91
+ expect(line).toBe('Memory: rss=50.0 MiB heapUsed=30.0 MiB heapTotal=40.0 MiB hwm(rss=60.0 MiB heap=35.0 MiB) samples=12');
92
+ });
93
+ });
@@ -0,0 +1,88 @@
1
+ function percentile(values, p) {
2
+ if (values.length === 0)
3
+ return 0;
4
+ const sorted = [...values].sort((a, b) => a - b);
5
+ const idx = Math.min(sorted.length - 1, Math.max(0, Math.floor((sorted.length - 1) * p)));
6
+ return Math.round(sorted[idx] * 100) / 100;
7
+ }
8
+ function classifyError(message) {
9
+ const msg = String(message ?? '').toLowerCase();
10
+ if (!msg)
11
+ return 'unknown';
12
+ if (msg.includes('timed out'))
13
+ return 'timeout';
14
+ if (msg.includes('missing permissions') || msg.includes('missing access'))
15
+ return 'discord_permissions';
16
+ if (msg.includes('unauthorized') || msg.includes('auth'))
17
+ return 'auth';
18
+ if (msg.includes('stream stall'))
19
+ return 'stream_stall';
20
+ return 'other';
21
+ }
22
+ export class MetricsRegistry {
23
+ startedAtMs = Date.now();
24
+ counters = new Map();
25
+ latencies = {
26
+ message: [],
27
+ reaction: [],
28
+ cron: [],
29
+ };
30
+ maxLatencySamples = 400;
31
+ memorySampler;
32
+ setMemorySampler(sampler) {
33
+ this.memorySampler = sampler;
34
+ }
35
+ increment(name, value = 1) {
36
+ const next = (this.counters.get(name) ?? 0) + value;
37
+ this.counters.set(name, next);
38
+ }
39
+ recordInvokeStart(flow) {
40
+ this.increment(`invoke.${flow}.started`);
41
+ }
42
+ recordInvokeResult(flow, ms, ok, errorMessage) {
43
+ this.increment(`invoke.${flow}.${ok ? 'succeeded' : 'failed'}`);
44
+ this.pushLatency(flow, ms);
45
+ if (!ok) {
46
+ this.increment(`invoke.${flow}.error_class.${classifyError(errorMessage)}`);
47
+ }
48
+ }
49
+ recordActionResult(ok) {
50
+ this.increment(`actions.${ok ? 'succeeded' : 'failed'}`);
51
+ }
52
+ snapshot() {
53
+ const counters = {};
54
+ for (const [k, v] of this.counters.entries())
55
+ counters[k] = v;
56
+ const snap = {
57
+ startedAt: this.startedAtMs,
58
+ counters,
59
+ latencies: {
60
+ message: this.latencySummary('message'),
61
+ reaction: this.latencySummary('reaction'),
62
+ cron: this.latencySummary('cron'),
63
+ },
64
+ };
65
+ if (this.memorySampler) {
66
+ snap.memory = this.memorySampler.peek();
67
+ }
68
+ return snap;
69
+ }
70
+ pushLatency(flow, ms) {
71
+ const arr = this.latencies[flow];
72
+ arr.push(Math.max(0, ms));
73
+ if (arr.length > this.maxLatencySamples) {
74
+ arr.shift();
75
+ }
76
+ }
77
+ latencySummary(flow) {
78
+ const values = this.latencies[flow];
79
+ const maxMs = values.length > 0 ? Math.max(...values) : 0;
80
+ return {
81
+ count: values.length,
82
+ p50Ms: percentile(values, 0.5),
83
+ p95Ms: percentile(values, 0.95),
84
+ maxMs: Math.round(maxMs * 100) / 100,
85
+ };
86
+ }
87
+ }
88
+ export const globalMetrics = new MetricsRegistry();
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { MetricsRegistry } from './metrics.js';
3
+ describe('MetricsRegistry', () => {
4
+ it('tracks invoke counters and latency summaries', () => {
5
+ const m = new MetricsRegistry();
6
+ m.recordInvokeStart('message');
7
+ m.recordInvokeResult('message', 100, true);
8
+ m.recordInvokeResult('message', 200, false, 'timed out');
9
+ const snap = m.snapshot();
10
+ expect(snap.counters['invoke.message.started']).toBe(1);
11
+ expect(snap.counters['invoke.message.succeeded']).toBe(1);
12
+ expect(snap.counters['invoke.message.failed']).toBe(1);
13
+ expect(snap.counters['invoke.message.error_class.timeout']).toBe(1);
14
+ expect(snap.latencies.message.count).toBe(2);
15
+ expect(snap.latencies.message.maxMs).toBe(200);
16
+ expect(snap.latencies.reaction.count).toBe(0);
17
+ });
18
+ it('classifies stream stall errors as stream_stall', () => {
19
+ const m = new MetricsRegistry();
20
+ m.recordInvokeResult('message', 120000, false, 'stream stall: no output for 120000ms');
21
+ const snap = m.snapshot();
22
+ expect(snap.counters['invoke.message.error_class.stream_stall']).toBe(1);
23
+ });
24
+ it('includes memory stats when a memory sampler is configured', () => {
25
+ const m = new MetricsRegistry();
26
+ m.setMemorySampler({
27
+ peek: () => ({
28
+ rssBytes: 1,
29
+ heapUsedBytes: 2,
30
+ heapTotalBytes: 3,
31
+ externalBytes: 4,
32
+ rssHwmBytes: 5,
33
+ heapUsedHwmBytes: 6,
34
+ sampleCount: 7,
35
+ }),
36
+ });
37
+ const snap = m.snapshot();
38
+ expect(snap.memory).toBeDefined();
39
+ expect(snap.memory?.rssBytes).toBe(1);
40
+ expect(snap.memory?.sampleCount).toBe(7);
41
+ });
42
+ });
@@ -0,0 +1,246 @@
1
+ /**
2
+ * OnboardingFlow — pure conversation state machine for interactive onboarding.
3
+ *
4
+ * No file I/O. Tracks current step, collected values, and returns reply text.
5
+ * The caller (discord.ts) handles sending messages and invoking the writer.
6
+ */
7
+ const MAX_INPUT_LENGTH = 200;
8
+ const PLACEHOLDER_RE = /\{\{[^}]+\}\}/;
9
+ /** Common timezone abbreviations mapped to IANA names. */
10
+ const TIMEZONE_ABBR = {
11
+ PST: 'America/Los_Angeles',
12
+ PDT: 'America/Los_Angeles',
13
+ MST: 'America/Denver',
14
+ MDT: 'America/Denver',
15
+ CST: 'America/Chicago',
16
+ CDT: 'America/Chicago',
17
+ EST: 'America/New_York',
18
+ EDT: 'America/New_York',
19
+ GMT: 'Etc/GMT',
20
+ UTC: 'UTC',
21
+ CET: 'Europe/Paris',
22
+ CEST: 'Europe/Paris',
23
+ BST: 'Europe/London',
24
+ IST: 'Asia/Kolkata',
25
+ JST: 'Asia/Tokyo',
26
+ AEST: 'Australia/Sydney',
27
+ AEDT: 'Australia/Sydney',
28
+ NZST: 'Pacific/Auckland',
29
+ NZDT: 'Pacific/Auckland',
30
+ };
31
+ function parseTimezone(input) {
32
+ const trimmed = input.trim();
33
+ const abbr = TIMEZONE_ABBR[trimmed.toUpperCase()];
34
+ if (abbr)
35
+ return abbr;
36
+ try {
37
+ Intl.DateTimeFormat(undefined, { timeZone: trimmed });
38
+ return trimmed;
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ function parseCheckin(input) {
45
+ const t = input.trim().toLowerCase();
46
+ if (['yes', 'y', 'yeah', 'yep', 'sure', 'ok', '1', 'true'].includes(t))
47
+ return true;
48
+ if (['no', 'n', 'nope', 'nah', '0', 'false'].includes(t))
49
+ return false;
50
+ return null;
51
+ }
52
+ const FIELD_DEFS = [
53
+ { label: 'Your name', key: 'userName' },
54
+ { label: 'Timezone', key: 'timezone' },
55
+ { label: 'Morning check-in', key: 'morningCheckin' },
56
+ ];
57
+ const CONFIRM_YES = new Set(['yes', 'y', 'ok', 'confirm', 'looks good']);
58
+ export class OnboardingFlow {
59
+ step = 'NAME';
60
+ values = {};
61
+ writeError = '';
62
+ /** When true, completing any field returns to CONFIRM instead of advancing. */
63
+ editing = false;
64
+ /** Updated on start() and handleInput(). Used for timeout checking. */
65
+ lastActivityTimestamp = 0;
66
+ /** Tracks whether the redirect notice has been sent this session. */
67
+ hasRedirected = false;
68
+ /** Where the conversation is happening. */
69
+ channelMode = 'dm';
70
+ /** Guild channel ID when channelMode is 'guild'. */
71
+ channelId;
72
+ start(displayName) {
73
+ this.lastActivityTimestamp = Date.now();
74
+ return {
75
+ done: false,
76
+ reply: `Hey ${displayName}! Quick setup — just 3 questions.\n\n` +
77
+ `**What's your name?**`,
78
+ };
79
+ }
80
+ handleInput(text) {
81
+ this.lastActivityTimestamp = Date.now();
82
+ const input = text.trim();
83
+ if (this.step === 'WRITING') {
84
+ return { done: false, reply: 'Hang on, still writing your files...' };
85
+ }
86
+ if (this.step === 'DONE') {
87
+ return { done: true, reply: '' };
88
+ }
89
+ if (this.step === 'WRITE_ERROR') {
90
+ return this.handleWriteError(input);
91
+ }
92
+ if (this.step === 'CONFIRM') {
93
+ return this.handleConfirm(input);
94
+ }
95
+ // All question steps require input
96
+ if (!input) {
97
+ return { done: false, reply: "I need something here — what would you like?" };
98
+ }
99
+ if (input.length > MAX_INPUT_LENGTH) {
100
+ return { done: false, reply: `That's a bit long — can you keep it under ${MAX_INPUT_LENGTH} characters?` };
101
+ }
102
+ if (PLACEHOLDER_RE.test(input)) {
103
+ return { done: false, reply: "That looks like a template placeholder — give me something real!" };
104
+ }
105
+ switch (this.step) {
106
+ case 'NAME':
107
+ this.values.userName = input;
108
+ if (this.editing)
109
+ return this.finishEdit();
110
+ this.step = 'TIMEZONE';
111
+ return {
112
+ done: false,
113
+ reply: `Nice to meet you, ${input}!\n\n` +
114
+ `**What timezone are you in?**\n` +
115
+ `Use an IANA name like \`America/New_York\`, or an abbreviation like \`EST\`, \`PST\`, \`CET\`.`,
116
+ };
117
+ case 'TIMEZONE': {
118
+ const tz = parseTimezone(input);
119
+ if (!tz) {
120
+ return {
121
+ done: false,
122
+ reply: "I didn't recognize that timezone. Try an IANA name like `America/New_York`, " +
123
+ "or an abbreviation like `EST`, `PST`, `CET`.",
124
+ };
125
+ }
126
+ this.values.timezone = tz;
127
+ if (this.editing)
128
+ return this.finishEdit();
129
+ this.step = 'CHECKIN';
130
+ return {
131
+ done: false,
132
+ reply: `**Would you like a morning check-in message each day?** (yes / no)`,
133
+ };
134
+ }
135
+ case 'CHECKIN': {
136
+ const checkin = parseCheckin(input);
137
+ if (checkin === null) {
138
+ return {
139
+ done: false,
140
+ reply: "Just yes or no — would you like a morning check-in message?",
141
+ };
142
+ }
143
+ this.values.morningCheckin = checkin;
144
+ if (this.editing)
145
+ return this.finishEdit();
146
+ return this.showConfirmation();
147
+ }
148
+ default:
149
+ return { done: false, reply: '' };
150
+ }
151
+ }
152
+ markWriteComplete() {
153
+ this.step = 'DONE';
154
+ }
155
+ markWriteFailed(error) {
156
+ this.step = 'WRITE_ERROR';
157
+ this.writeError = error;
158
+ }
159
+ getValues() {
160
+ return this.values;
161
+ }
162
+ getValuesWithDefaults(displayName, systemTimezone) {
163
+ return {
164
+ userName: this.values.userName ?? displayName,
165
+ timezone: this.values.timezone ?? systemTimezone,
166
+ morningCheckin: this.values.morningCheckin ?? false,
167
+ };
168
+ }
169
+ showConfirmation() {
170
+ this.step = 'CONFIRM';
171
+ const lines = FIELD_DEFS.map((f, i) => {
172
+ const val = this.values[f.key];
173
+ let display;
174
+ if (val === undefined || val === null) {
175
+ display = '(unanswered)';
176
+ }
177
+ else if (typeof val === 'boolean') {
178
+ display = val ? 'Yes' : 'No';
179
+ }
180
+ else {
181
+ display = String(val);
182
+ }
183
+ return `${i + 1}. **${f.label}:** ${display}`;
184
+ });
185
+ return {
186
+ done: false,
187
+ reply: `Here's what I've got:\n\n` +
188
+ lines.join('\n') +
189
+ `\n\nType **yes** to confirm, or pick a number to edit a field.`,
190
+ };
191
+ }
192
+ handleConfirm(input) {
193
+ if (!input) {
194
+ return { done: false, reply: `Type 'yes' to confirm, or pick a number (1-${FIELD_DEFS.length}) to edit a field.` };
195
+ }
196
+ if (CONFIRM_YES.has(input.toLowerCase())) {
197
+ this.step = 'WRITING';
198
+ return { done: false, reply: 'Writing your files...', writeResult: 'pending' };
199
+ }
200
+ const num = parseInt(input, 10);
201
+ if (!isNaN(num) && num >= 1 && num <= FIELD_DEFS.length) {
202
+ return this.reaskField(FIELD_DEFS[num - 1].key);
203
+ }
204
+ return { done: false, reply: `Type 'yes' to confirm, or pick a number (1-${FIELD_DEFS.length}) to edit a field.` };
205
+ }
206
+ handleWriteError(input) {
207
+ const t = input.toLowerCase();
208
+ if (t === 'yes' || t === 'retry') {
209
+ this.step = 'WRITING';
210
+ return { done: false, reply: 'Retrying...', writeResult: 'pending' };
211
+ }
212
+ const num = parseInt(input, 10);
213
+ if (!isNaN(num) && num >= 1 && num <= FIELD_DEFS.length) {
214
+ return this.reaskField(FIELD_DEFS[num - 1].key);
215
+ }
216
+ return {
217
+ done: false,
218
+ reply: `Something went wrong writing your files: ${this.writeError}\n` +
219
+ `Type **retry** to try again, pick a number to edit a field, or \`!cancel\` to give up.`,
220
+ };
221
+ }
222
+ finishEdit() {
223
+ this.editing = false;
224
+ return this.showConfirmation();
225
+ }
226
+ reaskField(key) {
227
+ this.editing = true;
228
+ switch (key) {
229
+ case 'userName':
230
+ this.step = 'NAME';
231
+ return { done: false, reply: "**What's your name?**" };
232
+ case 'timezone':
233
+ this.step = 'TIMEZONE';
234
+ return {
235
+ done: false,
236
+ reply: '**What timezone are you in?**\n' +
237
+ 'Use an IANA name like `America/New_York`, or an abbreviation like `EST`, `PST`, `CET`.',
238
+ };
239
+ case 'morningCheckin':
240
+ this.step = 'CHECKIN';
241
+ return { done: false, reply: '**Would you like a morning check-in message each day?** (yes / no)' };
242
+ default:
243
+ return this.showConfirmation();
244
+ }
245
+ }
246
+ }