freddie 0.0.41

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 (307) hide show
  1. package/AGENTS.md +180 -0
  2. package/CHANGELOG.md +32 -0
  3. package/README.md +130 -0
  4. package/bin/freddie.js +116 -0
  5. package/package.json +59 -0
  6. package/skills/creative/README.md +3 -0
  7. package/skills/creative/architecture-diagram/SKILL.md +52 -0
  8. package/skills/creative/ascii-video/SKILL.md +60 -0
  9. package/skills/creative/concept-diagrams/SKILL.md +65 -0
  10. package/skills/data/README.md +3 -0
  11. package/skills/data/etl-pipelines/SKILL.md +60 -0
  12. package/skills/data/sql-explainer/SKILL.md +60 -0
  13. package/skills/ops/README.md +3 -0
  14. package/skills/ops/incident-response/SKILL.md +74 -0
  15. package/skills/ops/log-triage/SKILL.md +79 -0
  16. package/skills/planning/README.md +3 -0
  17. package/skills/planning/okr-drafter/SKILL.md +60 -0
  18. package/skills/planning/weekly-review/SKILL.md +64 -0
  19. package/skills/software-development/README.md +3 -0
  20. package/skills/software-development/code-review/SKILL.md +70 -0
  21. package/skills/software-development/rfc-writer/SKILL.md +68 -0
  22. package/skills/software-development/systematic-debugging/SKILL.md +80 -0
  23. package/src/acp/auth.js +21 -0
  24. package/src/acp/entry.js +2 -0
  25. package/src/acp/events.js +10 -0
  26. package/src/acp/main.js +8 -0
  27. package/src/acp/permissions.js +29 -0
  28. package/src/acp/server.js +84 -0
  29. package/src/acp/session.js +26 -0
  30. package/src/acp/tools.js +17 -0
  31. package/src/agent/account_usage.js +19 -0
  32. package/src/agent/acptoapi-bridge.js +80 -0
  33. package/src/agent/anthropic_adapter.js +10 -0
  34. package/src/agent/auxiliary_client.js +20 -0
  35. package/src/agent/bedrock_adapter.js +11 -0
  36. package/src/agent/codex_responses_adapter.js +10 -0
  37. package/src/agent/compress/compressor.js +55 -0
  38. package/src/agent/compress/fallback.js +14 -0
  39. package/src/agent/compress/index.js +6 -0
  40. package/src/agent/compress/policy.js +47 -0
  41. package/src/agent/compress/prompt.js +46 -0
  42. package/src/agent/compress/prune.js +16 -0
  43. package/src/agent/compress/tokens.js +31 -0
  44. package/src/agent/context_references.js +40 -0
  45. package/src/agent/copilot_acp_client.js +6 -0
  46. package/src/agent/credential_pool.js +30 -0
  47. package/src/agent/credential_sources.js +18 -0
  48. package/src/agent/curator.js +5 -0
  49. package/src/agent/display.js +23 -0
  50. package/src/agent/error_classifier.js +15 -0
  51. package/src/agent/file_safety.js +9 -0
  52. package/src/agent/gemini_cloudcode_adapter.js +9 -0
  53. package/src/agent/gemini_native_adapter.js +11 -0
  54. package/src/agent/gemini_schema.js +19 -0
  55. package/src/agent/google_code_assist.js +8 -0
  56. package/src/agent/google_oauth.js +21 -0
  57. package/src/agent/image_gen_provider.js +8 -0
  58. package/src/agent/image_gen_registry.js +6 -0
  59. package/src/agent/image_routing.js +13 -0
  60. package/src/agent/insights.js +9 -0
  61. package/src/agent/llm_resolver.js +21 -0
  62. package/src/agent/lmstudio_reasoning.js +13 -0
  63. package/src/agent/machine.js +102 -0
  64. package/src/agent/manual_compression_feedback.js +5 -0
  65. package/src/agent/memory_manager.js +14 -0
  66. package/src/agent/memory_provider.js +1 -0
  67. package/src/agent/model_metadata.js +28 -0
  68. package/src/agent/models_dev.js +13 -0
  69. package/src/agent/moonshot_schema.js +11 -0
  70. package/src/agent/oauth_endpoints.js +79 -0
  71. package/src/agent/onboarding.js +16 -0
  72. package/src/agent/pi-bridge.js +37 -0
  73. package/src/agent/prompt_builder.js +12 -0
  74. package/src/agent/prompt_caching.js +24 -0
  75. package/src/agent/rate_limit_tracker.js +12 -0
  76. package/src/agent/redact.js +25 -0
  77. package/src/agent/retry_utils.js +17 -0
  78. package/src/agent/shell_hooks.js +16 -0
  79. package/src/agent/skill_commands.js +16 -0
  80. package/src/agent/skill_preprocessing.js +12 -0
  81. package/src/agent/skill_utils.js +14 -0
  82. package/src/agent/subdirectory_hints.js +17 -0
  83. package/src/agent/title_generator.js +13 -0
  84. package/src/agent/trajectory.js +9 -0
  85. package/src/agent/usage_pricing.js +16 -0
  86. package/src/auth.js +84 -0
  87. package/src/batch.js +32 -0
  88. package/src/cli/auth_commands.js +17 -0
  89. package/src/cli/azure_detect.js +9 -0
  90. package/src/cli/backup.js +17 -0
  91. package/src/cli/banner.js +13 -0
  92. package/src/cli/browser_connect.js +11 -0
  93. package/src/cli/callbacks.js +5 -0
  94. package/src/cli/claw.js +8 -0
  95. package/src/cli/cli_output.js +19 -0
  96. package/src/cli/clipboard.js +24 -0
  97. package/src/cli/codex_models.js +8 -0
  98. package/src/cli/colors.js +13 -0
  99. package/src/cli/completer.js +98 -0
  100. package/src/cli/completion.js +21 -0
  101. package/src/cli/copilot_auth.js +9 -0
  102. package/src/cli/curator_cli.js +5 -0
  103. package/src/cli/curses.js +15 -0
  104. package/src/cli/debug.js +6 -0
  105. package/src/cli/default_soul.js +20 -0
  106. package/src/cli/dingtalk_auth.js +12 -0
  107. package/src/cli/doctor.js +15 -0
  108. package/src/cli/dump.js +11 -0
  109. package/src/cli/env_loader.js +25 -0
  110. package/src/cli/fallback_cmd.js +9 -0
  111. package/src/cli/gateway_cli.js +17 -0
  112. package/src/cli/hooks.js +9 -0
  113. package/src/cli/interactive.js +61 -0
  114. package/src/cli/logs.js +32 -0
  115. package/src/cli/main.js +7 -0
  116. package/src/cli/mcp_config.js +9 -0
  117. package/src/cli/memory_setup.js +12 -0
  118. package/src/cli/model_catalog.js +23 -0
  119. package/src/cli/model_normalize.js +12 -0
  120. package/src/cli/model_switch.js +11 -0
  121. package/src/cli/models.js +13 -0
  122. package/src/cli/nous_subscription.js +12 -0
  123. package/src/cli/oneshot.js +6 -0
  124. package/src/cli/pairing.js +21 -0
  125. package/src/cli/platforms.js +14 -0
  126. package/src/cli/plugins.js +4 -0
  127. package/src/cli/plugins_cmd.js +21 -0
  128. package/src/cli/profiles_cli.js +6 -0
  129. package/src/cli/providers.js +18 -0
  130. package/src/cli/pty_bridge.js +16 -0
  131. package/src/cli/relaunch.js +7 -0
  132. package/src/cli/runtime_provider.js +9 -0
  133. package/src/cli/setup.js +131 -0
  134. package/src/cli/skills_config.js +6 -0
  135. package/src/cli/skills_hub.js +8 -0
  136. package/src/cli/slack_cli.js +17 -0
  137. package/src/cli/status.js +10 -0
  138. package/src/cli/timeouts.js +5 -0
  139. package/src/cli/tips.js +14 -0
  140. package/src/cli/tools_config.js +15 -0
  141. package/src/cli/uninstall.js +8 -0
  142. package/src/cli/vercel_auth.js +13 -0
  143. package/src/cli/voice.js +6 -0
  144. package/src/cli/web_server.js +13 -0
  145. package/src/cli/webhook.js +12 -0
  146. package/src/commands/profile.js +72 -0
  147. package/src/commands/registry.js +94 -0
  148. package/src/config.js +125 -0
  149. package/src/context/engine.js +42 -0
  150. package/src/cron/cron-parse.js +27 -0
  151. package/src/cron/scheduler.js +63 -0
  152. package/src/db.js +178 -0
  153. package/src/gateway/base.js +13 -0
  154. package/src/gateway/builtin_hooks/boot.js +5 -0
  155. package/src/gateway/builtin_hooks/broadcast.js +3 -0
  156. package/src/gateway/builtin_hooks/deny.js +6 -0
  157. package/src/gateway/builtin_hooks/index.js +17 -0
  158. package/src/gateway/builtin_hooks/presence.js +4 -0
  159. package/src/gateway/builtin_hooks/routing.js +7 -0
  160. package/src/gateway/helpers.js +27 -0
  161. package/src/gateway/platforms/api_server.js +21 -0
  162. package/src/gateway/platforms/bluebubbles.js +32 -0
  163. package/src/gateway/platforms/dingtalk.js +32 -0
  164. package/src/gateway/platforms/discord.js +24 -0
  165. package/src/gateway/platforms/email.js +51 -0
  166. package/src/gateway/platforms/feishu.js +32 -0
  167. package/src/gateway/platforms/feishu_comment.js +12 -0
  168. package/src/gateway/platforms/feishu_comment_rules.js +11 -0
  169. package/src/gateway/platforms/homeassistant.js +32 -0
  170. package/src/gateway/platforms/matrix.js +40 -0
  171. package/src/gateway/platforms/mattermost.js +29 -0
  172. package/src/gateway/platforms/qqbot.js +32 -0
  173. package/src/gateway/platforms/signal.js +33 -0
  174. package/src/gateway/platforms/slack.js +34 -0
  175. package/src/gateway/platforms/sms.js +34 -0
  176. package/src/gateway/platforms/telegram.js +38 -0
  177. package/src/gateway/platforms/telegram_network.js +17 -0
  178. package/src/gateway/platforms/webhook.js +19 -0
  179. package/src/gateway/platforms/wecom.js +32 -0
  180. package/src/gateway/platforms/wecom_callback.js +15 -0
  181. package/src/gateway/platforms/wecom_crypto.js +16 -0
  182. package/src/gateway/platforms/weixin.js +32 -0
  183. package/src/gateway/platforms/whatsapp.js +40 -0
  184. package/src/gateway/platforms/yuanbao.js +9 -0
  185. package/src/gateway/platforms/yuanbao_media.js +5 -0
  186. package/src/gateway/platforms/yuanbao_proto.js +9 -0
  187. package/src/gateway/platforms/yuanbao_sticker.js +6 -0
  188. package/src/gateway/run.js +42 -0
  189. package/src/gateway/service.js +143 -0
  190. package/src/home.js +44 -0
  191. package/src/index.js +47 -0
  192. package/src/mcp/server.js +49 -0
  193. package/src/observability/debug.js +31 -0
  194. package/src/observability/log.js +38 -0
  195. package/src/plugins/achievements/index.js +9 -0
  196. package/src/plugins/cockpit/index.js +8 -0
  197. package/src/plugins/context_engine/index.js +13 -0
  198. package/src/plugins/disk_cleanup/index.js +22 -0
  199. package/src/plugins/google_meet/index.js +19 -0
  200. package/src/plugins/image_gen/index.js +5 -0
  201. package/src/plugins/manager.js +66 -0
  202. package/src/plugins/memory/_index.js +8 -0
  203. package/src/plugins/memory/byterover.js +25 -0
  204. package/src/plugins/memory/hindsight.js +25 -0
  205. package/src/plugins/memory/holographic.js +31 -0
  206. package/src/plugins/memory/honcho.js +25 -0
  207. package/src/plugins/memory/mem0.js +25 -0
  208. package/src/plugins/memory/openviking.js +25 -0
  209. package/src/plugins/memory/provider.js +35 -0
  210. package/src/plugins/memory/retaindb.js +25 -0
  211. package/src/plugins/memory/supermemory.js +25 -0
  212. package/src/plugins/observability/index.js +18 -0
  213. package/src/plugins/platforms/index.js +20 -0
  214. package/src/plugins/spotify/index.js +22 -0
  215. package/src/rl/atropos.js +22 -0
  216. package/src/rl/cli.js +18 -0
  217. package/src/sessions.js +84 -0
  218. package/src/skills/index.js +49 -0
  219. package/src/skin/engine.js +81 -0
  220. package/src/swe/runner.js +26 -0
  221. package/src/time.js +25 -0
  222. package/src/tools/ansi_strip.js +8 -0
  223. package/src/tools/approval.js +15 -0
  224. package/src/tools/bash.js +35 -0
  225. package/src/tools/binary_extensions.js +22 -0
  226. package/src/tools/browser.js +48 -0
  227. package/src/tools/budget_config.js +13 -0
  228. package/src/tools/checkpoint.js +29 -0
  229. package/src/tools/clarify.js +15 -0
  230. package/src/tools/code_execution.js +27 -0
  231. package/src/tools/credential_files.js +16 -0
  232. package/src/tools/cronjob.js +16 -0
  233. package/src/tools/debug_helpers.js +9 -0
  234. package/src/tools/delegate.js +28 -0
  235. package/src/tools/discord_tool.js +13 -0
  236. package/src/tools/edit.js +31 -0
  237. package/src/tools/env_passthrough.js +15 -0
  238. package/src/tools/environments/base.js +26 -0
  239. package/src/tools/environments/daytona.js +48 -0
  240. package/src/tools/environments/docker.js +14 -0
  241. package/src/tools/environments/file_sync.js +60 -0
  242. package/src/tools/environments/index.js +36 -0
  243. package/src/tools/environments/local.js +31 -0
  244. package/src/tools/environments/modal.js +33 -0
  245. package/src/tools/environments/singularity.js +38 -0
  246. package/src/tools/environments/ssh.js +14 -0
  247. package/src/tools/environments/vercel_sandbox.js +47 -0
  248. package/src/tools/feishu_doc.js +15 -0
  249. package/src/tools/feishu_drive.js +14 -0
  250. package/src/tools/file_operations.js +17 -0
  251. package/src/tools/file_state.js +16 -0
  252. package/src/tools/file_tools.js +23 -0
  253. package/src/tools/fuzzy_match.js +8 -0
  254. package/src/tools/grep.js +51 -0
  255. package/src/tools/homeassistant_tool.js +15 -0
  256. package/src/tools/image_gen.js +33 -0
  257. package/src/tools/interrupt.js +18 -0
  258. package/src/tools/managed_tool_gateway.js +11 -0
  259. package/src/tools/mcp_oauth.js +21 -0
  260. package/src/tools/mcp_oauth_manager.js +20 -0
  261. package/src/tools/mcp_tool.js +36 -0
  262. package/src/tools/memory.js +66 -0
  263. package/src/tools/mixture_of_agents.js +14 -0
  264. package/src/tools/neutts_synth.js +13 -0
  265. package/src/tools/openrouter_client.js +13 -0
  266. package/src/tools/osv_check.js +11 -0
  267. package/src/tools/patch_parser.js +42 -0
  268. package/src/tools/path_security.js +16 -0
  269. package/src/tools/process_registry.js +17 -0
  270. package/src/tools/read.js +26 -0
  271. package/src/tools/registry.js +54 -0
  272. package/src/tools/rl_training.js +13 -0
  273. package/src/tools/schema_sanitizer.js +18 -0
  274. package/src/tools/send_message.js +32 -0
  275. package/src/tools/session_search.js +23 -0
  276. package/src/tools/skill_manager.js +17 -0
  277. package/src/tools/skill_usage.js +20 -0
  278. package/src/tools/skills_guard.js +17 -0
  279. package/src/tools/skills_hub.js +31 -0
  280. package/src/tools/skills_index.js +14 -0
  281. package/src/tools/skills_sync.js +19 -0
  282. package/src/tools/skills_tool.js +11 -0
  283. package/src/tools/slash_confirm.js +16 -0
  284. package/src/tools/terminal.js +29 -0
  285. package/src/tools/tirith_security.js +25 -0
  286. package/src/tools/todo.js +54 -0
  287. package/src/tools/tool_backend_helpers.js +26 -0
  288. package/src/tools/tool_output_limits.js +15 -0
  289. package/src/tools/tool_result_storage.js +20 -0
  290. package/src/tools/transcription.js +19 -0
  291. package/src/tools/tts.js +19 -0
  292. package/src/tools/url_safety.js +15 -0
  293. package/src/tools/vision.js +18 -0
  294. package/src/tools/voice_mode.js +10 -0
  295. package/src/tools/web_search.js +37 -0
  296. package/src/tools/web_tools.js +18 -0
  297. package/src/tools/website_policy.js +14 -0
  298. package/src/tools/write.js +25 -0
  299. package/src/tools/xai_http.js +13 -0
  300. package/src/tools/yuanbao_tools.js +13 -0
  301. package/src/toolset_distributions.js +18 -0
  302. package/src/toolsets.js +26 -0
  303. package/src/tui/index.js +26 -0
  304. package/src/utils.js +54 -0
  305. package/src/web/app.js +547 -0
  306. package/src/web/index.html +167 -0
  307. package/src/web/server.js +109 -0
@@ -0,0 +1,63 @@
1
+ import { db } from '../db.js'
2
+ import { parseCron, matches } from './cron-parse.js'
3
+ import { runTurn } from '../agent/machine.js'
4
+ import { logger } from '../observability/log.js'
5
+
6
+ const log = logger('cron')
7
+
8
+ let _interval = null
9
+
10
+ async function init() {
11
+ const d = await db()
12
+ await d.exec(`CREATE TABLE IF NOT EXISTS cron_jobs (id INTEGER PRIMARY KEY AUTOINCREMENT, cron TEXT NOT NULL, prompt TEXT NOT NULL, model TEXT, last_run INTEGER, created INTEGER NOT NULL, enabled INTEGER NOT NULL DEFAULT 1)`)
13
+ return d
14
+ }
15
+
16
+ export async function createJob({ cron, prompt, model = null }) {
17
+ parseCron(cron)
18
+ const d = await init()
19
+ const info = await d.prepare(`INSERT INTO cron_jobs (cron, prompt, model, created, enabled) VALUES (?, ?, ?, ?, 1)`).run(cron, prompt, model, Date.now())
20
+ const id = Number(info.lastInsertRowid)
21
+ log.info('job created', { id, cron })
22
+ return id
23
+ }
24
+
25
+ export async function listJobs() {
26
+ return await (await init()).prepare(`SELECT * FROM cron_jobs ORDER BY id DESC`).all()
27
+ }
28
+
29
+ export async function cancelJob(id) {
30
+ await (await init()).prepare(`UPDATE cron_jobs SET enabled = 0 WHERE id = ?`).run(id)
31
+ }
32
+
33
+ export async function deleteJob(id) {
34
+ await (await init()).prepare(`DELETE FROM cron_jobs WHERE id = ?`).run(id)
35
+ }
36
+
37
+ export async function tick(now = new Date(), { callLLM = null } = {}) {
38
+ const d = await init()
39
+ const jobs = await d.prepare(`SELECT * FROM cron_jobs WHERE enabled = 1`).all()
40
+ const fired = []
41
+ for (const j of jobs) {
42
+ try {
43
+ const parsed = parseCron(j.cron)
44
+ if (!matches(parsed, now)) continue
45
+ const minuteKey = Math.floor(now.getTime() / 60000)
46
+ if (j.last_run && Math.floor(j.last_run / 60000) === minuteKey) continue
47
+ await d.prepare(`UPDATE cron_jobs SET last_run = ? WHERE id = ?`).run(now.getTime(), j.id)
48
+ fired.push(j)
49
+ runTurn({ prompt: j.prompt, callLLM }).catch(e => log.error('cron run failed', { id: j.id, err: String(e) }))
50
+ } catch (e) { log.error('cron tick failed', { id: j.id, err: String(e) }) }
51
+ }
52
+ return fired
53
+ }
54
+
55
+ export function startScheduler({ callLLM = null, intervalMs = 30000 } = {}) {
56
+ stopScheduler()
57
+ _interval = setInterval(() => { tick(new Date(), { callLLM }) }, intervalMs)
58
+ return _interval
59
+ }
60
+
61
+ export function stopScheduler() {
62
+ if (_interval) { clearInterval(_interval); _interval = null }
63
+ }
package/src/db.js ADDED
@@ -0,0 +1,178 @@
1
+ import path from 'node:path'
2
+ import fs from 'node:fs'
3
+ import { createClient } from '@libsql/client'
4
+ import { getFophHome } from './home.js'
5
+
6
+ let _db = null
7
+ let _dbPromise = null
8
+ const DB_PATH = () => path.join(getFophHome(), 'state', 'sessions.db')
9
+ const USE_MEMORY_DB = () => process.env.FREDDIE_TEST_DB === 'memory'
10
+
11
+ export async function db() {
12
+ if (_db) return _db
13
+ if (_dbPromise) return await _dbPromise
14
+
15
+ _dbPromise = (async () => {
16
+ let client
17
+ let dbPath = null
18
+
19
+ if (USE_MEMORY_DB()) {
20
+ // In-memory mode for tests: no file persistence
21
+ client = createClient({ url: 'file::memory:' })
22
+ } else {
23
+ const dir = path.join(getFophHome(), 'state')
24
+ fs.mkdirSync(dir, { recursive: true })
25
+ dbPath = DB_PATH()
26
+ client = createClient({ url: `file:${dbPath}` })
27
+ }
28
+
29
+ _db = new DbAdapter(client, dbPath)
30
+ _dbPromise = null
31
+ return _db
32
+ })()
33
+
34
+ return await _dbPromise
35
+ }
36
+
37
+ class DbAdapter {
38
+ constructor(client, dbPath) {
39
+ this.client = client
40
+ this.dbPath = dbPath
41
+ this._fts5_unavailable = false
42
+ }
43
+
44
+ prepare(sql) {
45
+ return new PreparedStatement(this.client, sql)
46
+ }
47
+
48
+ async exec(sql) {
49
+ try {
50
+ const statements = sql.split(';').filter(s => s.trim())
51
+ const results = []
52
+ for (const stmt of statements) {
53
+ if (stmt.trim()) {
54
+ const result = await this.client.execute({ sql: stmt.trim() })
55
+ results.push(result)
56
+ }
57
+ }
58
+ return results
59
+ } catch (e) {
60
+ throw e
61
+ }
62
+ }
63
+
64
+ async run(...args) {
65
+ const [sql, ...params] = args
66
+ const result = await this.client.execute({ sql, args: params })
67
+ return {
68
+ changes: result.rowsAffected,
69
+ lastInsertRowid: result.lastInsertRowid ? BigInt(result.lastInsertRowid) : 0n
70
+ }
71
+ }
72
+
73
+ transaction(fn) {
74
+ return async (...args) => {
75
+ try {
76
+ await this.client.execute('BEGIN TRANSACTION')
77
+ const result = await fn(...args)
78
+ await this.client.execute('COMMIT')
79
+ return result
80
+ } catch (e) {
81
+ try {
82
+ await this.client.execute('ROLLBACK')
83
+ } catch (_) {}
84
+ throw e
85
+ }
86
+ }
87
+ }
88
+
89
+ async close() {
90
+ if (this.client) {
91
+ await this.client.close()
92
+ this.client = null
93
+ }
94
+ }
95
+
96
+ async clearAll() {
97
+ try {
98
+ const result = await this.client.execute("SELECT name FROM sqlite_master WHERE type='table'")
99
+ if (result.rows && result.rows.length > 0) {
100
+ for (const [tableName] of result.rows) {
101
+ try {
102
+ await this.client.execute(`DROP TABLE IF EXISTS ${tableName}`)
103
+ } catch (e) {
104
+ // Ignore drop errors
105
+ }
106
+ }
107
+ }
108
+ } catch (e) {
109
+ // Ignore errors
110
+ }
111
+ }
112
+ }
113
+
114
+ class PreparedStatement {
115
+ constructor(client, sql) {
116
+ this.client = client
117
+ this.sql = sql
118
+ }
119
+
120
+ bind(params = []) {
121
+ this.params = params
122
+ return this
123
+ }
124
+
125
+ async run(...params) {
126
+ const p = Array.isArray(params[0]) ? params[0] : params
127
+ const result = await this.client.execute({ sql: this.sql, args: p })
128
+ return {
129
+ changes: result.rowsAffected,
130
+ lastInsertRowid: result.lastInsertRowid ? BigInt(result.lastInsertRowid) : 0n
131
+ }
132
+ }
133
+
134
+ async get(...params) {
135
+ const p = Array.isArray(params[0]) ? params[0] : params
136
+ const result = await this.client.execute({ sql: this.sql, args: p })
137
+ if (!result.rows || result.rows.length === 0) return null
138
+ const row = result.rows[0]
139
+ const obj = {}
140
+ result.columns.forEach((col, i) => {
141
+ obj[col] = row[i]
142
+ })
143
+ return obj
144
+ }
145
+
146
+ async all(...params) {
147
+ const p = Array.isArray(params[0]) ? params[0] : params
148
+ const result = await this.client.execute({ sql: this.sql, args: p })
149
+ if (!result.rows || result.rows.length === 0) return []
150
+ return result.rows.map(row => {
151
+ const obj = {}
152
+ result.columns.forEach((col, i) => {
153
+ obj[col] = row[i]
154
+ })
155
+ return obj
156
+ })
157
+ }
158
+ }
159
+
160
+ export async function closeDb() {
161
+ if (_db) {
162
+ await _db.close()
163
+ _db = null
164
+ }
165
+ _dbPromise = null
166
+ }
167
+
168
+ export async function resetForTests() {
169
+ // Clear all tables from current db (if open) to clean state
170
+ if (_db) {
171
+ await _db.clearAll()
172
+ }
173
+ await closeDb()
174
+
175
+ // Reset module state for fresh in-memory init
176
+ _db = null
177
+ _dbPromise = null
178
+ }
@@ -0,0 +1,13 @@
1
+ import { EventEmitter } from 'node:events'
2
+ export class BasePlatformAdapter extends EventEmitter {
3
+ constructor(opts = {}) { super(); this.opts = opts; this.platform = opts.platform || 'unknown'; this._running = false }
4
+ getRequiredEnv() { return [] }
5
+ isReady() {
6
+ const need = this.getRequiredEnv()
7
+ return need.every(e => process.env[e] || this.opts[e.toLowerCase()])
8
+ }
9
+ async start() { if (!this.isReady()) throw new Error(this.constructor.name + ': missing env: ' + this.getRequiredEnv().join(', ')); this._running = true }
10
+ async stop() { this._running = false }
11
+ async send(_reply) { throw new Error(this.constructor.name + '.send must be overridden') }
12
+ isRunning() { return this._running }
13
+ }
@@ -0,0 +1,5 @@
1
+ export const bootHook = async (msg) => {
2
+ if (typeof msg?.text !== 'string') return msg
3
+ if (msg.text.startsWith('/boot')) return { ...msg, text: msg.text.replace(/^\/boot\s*/, '').trim() || 'hello' }
4
+ return msg
5
+ }
@@ -0,0 +1,3 @@
1
+ const _subscribers = new Set()
2
+ export function addSubscriber(handler) { _subscribers.add(handler); return () => _subscribers.delete(handler) }
3
+ export const broadcastHook = async (reply) => { for (const s of _subscribers) try { s(reply) } catch {} return reply }
@@ -0,0 +1,6 @@
1
+ import { getConfigValue } from '../../config.js'
2
+ export const denyHook = async (msg) => {
3
+ const list = getConfigValue('gateway.deny_list', []) || []
4
+ if (msg?.from && list.includes(msg.from)) return { ...msg, denied: true, text: '' }
5
+ return msg
6
+ }
@@ -0,0 +1,17 @@
1
+ export { bootHook } from './boot.js'
2
+ export { presenceInboundHook, isOnline, presenceMap } from './presence.js'
3
+ export { broadcastHook, addSubscriber } from './broadcast.js'
4
+ export { routingHook } from './routing.js'
5
+ export { denyHook } from './deny.js'
6
+ import { bootHook } from './boot.js'
7
+ import { presenceInboundHook } from './presence.js'
8
+ import { broadcastHook } from './broadcast.js'
9
+ import { routingHook } from './routing.js'
10
+ import { denyHook } from './deny.js'
11
+ export function registerBuiltinHooks(gateway) {
12
+ gateway.addHook('inbound', presenceInboundHook)
13
+ gateway.addHook('inbound', denyHook)
14
+ gateway.addHook('inbound', bootHook)
15
+ gateway.addHook('inbound', routingHook)
16
+ gateway.addHook('outbound', broadcastHook)
17
+ }
@@ -0,0 +1,4 @@
1
+ const _presence = new Map()
2
+ export const presenceInboundHook = async (msg) => { if (msg?.from) _presence.set(msg.from, Date.now()); return msg }
3
+ export function isOnline(from, withinMs = 5 * 60_000) { const ts = _presence.get(from) || 0; return Date.now() - ts < withinMs }
4
+ export function presenceMap() { return Object.fromEntries(_presence) }
@@ -0,0 +1,7 @@
1
+ import { resolveCommand, getCommand } from '../../commands/registry.js'
2
+ export const routingHook = async (msg) => {
3
+ if (typeof msg?.text !== 'string' || !msg.text.startsWith('/')) return msg
4
+ const name = resolveCommand(msg.text)
5
+ if (!name) return msg
6
+ return { ...msg, slashCommand: name, slashArgs: msg.text.split(/\s+/).slice(1).join(' '), description: getCommand(name)?.description }
7
+ }
@@ -0,0 +1,27 @@
1
+ import crypto from 'node:crypto'
2
+ export function hmacSign(secret, body) { return crypto.createHmac('sha256', secret).update(body).digest('hex') }
3
+ export function hmacVerify(secret, body, signature) {
4
+ const expected = hmacSign(secret, body)
5
+ try { return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(String(signature))) } catch { return false }
6
+ }
7
+ export function aesEncrypt(key, plaintext) {
8
+ const iv = crypto.randomBytes(16)
9
+ const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key, 'hex').slice(0, 32), iv)
10
+ return iv.toString('hex') + ':' + cipher.update(plaintext, 'utf8', 'hex') + cipher.final('hex')
11
+ }
12
+ export function aesDecrypt(key, ciphertext) {
13
+ const [iv, data] = String(ciphertext).split(':')
14
+ const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key, 'hex').slice(0, 32), Buffer.from(iv, 'hex'))
15
+ return decipher.update(data, 'hex', 'utf8') + decipher.final('utf8')
16
+ }
17
+ export function parseSignedQuery(query, secret, sigParam = 'signature') {
18
+ const sig = query[sigParam]
19
+ if (!sig) return { valid: false, reason: 'no signature' }
20
+ const sorted = Object.keys(query).filter(k => k !== sigParam).sort().map(k => k + '=' + query[k]).join('&')
21
+ return { valid: hmacVerify(secret, sorted, sig), payload: query }
22
+ }
23
+ export function rateLimitWindow(map, key, windowMs = 1000) {
24
+ const last = map.get(key) || 0; const now = Date.now()
25
+ if (now - last < windowMs) return { allowed: false, retryIn: windowMs - (now - last) }
26
+ map.set(key, now); return { allowed: true }
27
+ }
@@ -0,0 +1,21 @@
1
+ import express from 'express'
2
+ import { EventEmitter } from 'node:events'
3
+
4
+ export class ApiServerAdapter extends EventEmitter {
5
+ constructor({ port = 0 } = {}) { super(); this.port = port; this._server = null; this._messages = [] }
6
+ async start() {
7
+ const app = express()
8
+ app.use(express.json())
9
+ app.post('/messages', (req, res) => {
10
+ const m = { from: req.body?.from || 'api', text: req.body?.text || '', raw: req.body }
11
+ this.emit('message', m)
12
+ res.json({ ok: true })
13
+ })
14
+ app.get('/messages', (_, res) => res.json(this._messages))
15
+ await new Promise(resolve => { this._server = app.listen(this.port, () => resolve()) })
16
+ this.port = this._server.address().port
17
+ }
18
+ async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
19
+ async send(reply) { this._messages.push(reply) }
20
+ drain() { const out = [...this._messages]; this._messages.length = 0; return out }
21
+ }
@@ -0,0 +1,32 @@
1
+ import express from 'express'
2
+ import { EventEmitter } from 'node:events'
3
+
4
+ export class BluebubblesAdapter extends EventEmitter {
5
+ constructor(opts = {}) {
6
+ super()
7
+ this.platform = 'bluebubbles'
8
+ this.token = opts.token || process.env.BLUEBUBBLES_PASSWORD
9
+ this.port = opts.port || 0
10
+ this.api = opts.api || "http://localhost:1234/api/v1/message/text"
11
+ this._server = null
12
+ }
13
+ getRequiredEnv() { return ["BLUEBUBBLES_PASSWORD"] }
14
+ async start() {
15
+ if (!this.token) throw new Error('BluebubblesAdapter: ' + this.getRequiredEnv().join(', ') + ' required')
16
+ const app = express()
17
+ app.use(express.json())
18
+ app.post('/webhook', (req, res) => {
19
+ const text = req.body?.text || req.body?.message?.text || req.body?.content || ''
20
+ const from = req.body?.from || req.body?.user_id || req.body?.sender_id || ''
21
+ this.emit('message', { from: String(from), text, raw: req.body })
22
+ res.json({ ok: true })
23
+ })
24
+ await new Promise(r => { this._server = app.listen(this.port, () => r()) })
25
+ this.port = this._server.address().port
26
+ }
27
+ async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
28
+ async send(reply) {
29
+ if (!this.token) throw new Error('BluebubblesAdapter: token required')
30
+ return fetch(this.api, { method: 'POST', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ to: reply.to, text: reply.text }) }).then(r => r.json())
31
+ }
32
+ }
@@ -0,0 +1,32 @@
1
+ import express from 'express'
2
+ import { EventEmitter } from 'node:events'
3
+
4
+ export class DingtalkAdapter extends EventEmitter {
5
+ constructor(opts = {}) {
6
+ super()
7
+ this.platform = 'dingtalk'
8
+ this.token = opts.token || process.env.DINGTALK_ACCESS_TOKEN
9
+ this.port = opts.port || 0
10
+ this.api = opts.api || "https://oapi.dingtalk.com/robot/send"
11
+ this._server = null
12
+ }
13
+ getRequiredEnv() { return ["DINGTALK_ACCESS_TOKEN"] }
14
+ async start() {
15
+ if (!this.token) throw new Error('DingtalkAdapter: ' + this.getRequiredEnv().join(', ') + ' required')
16
+ const app = express()
17
+ app.use(express.json())
18
+ app.post('/webhook', (req, res) => {
19
+ const text = req.body?.text || req.body?.message?.text || req.body?.content || ''
20
+ const from = req.body?.from || req.body?.user_id || req.body?.sender_id || ''
21
+ this.emit('message', { from: String(from), text, raw: req.body })
22
+ res.json({ ok: true })
23
+ })
24
+ await new Promise(r => { this._server = app.listen(this.port, () => r()) })
25
+ this.port = this._server.address().port
26
+ }
27
+ async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
28
+ async send(reply) {
29
+ if (!this.token) throw new Error('DingtalkAdapter: token required')
30
+ return fetch(this.api, { method: 'POST', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ to: reply.to, text: reply.text }) }).then(r => r.json())
31
+ }
32
+ }
@@ -0,0 +1,24 @@
1
+ import { EventEmitter } from 'node:events'
2
+
3
+ export class DiscordAdapter extends EventEmitter {
4
+ constructor(opts = {}) {
5
+ super()
6
+ this.platform = 'discord'
7
+ this.token = opts.token || process.env.DISCORD_BOT_TOKEN
8
+ this.api = opts.api || 'https://discord.com/api/v10'
9
+ this._ws = null
10
+ }
11
+ getRequiredEnv() { return ['DISCORD_BOT_TOKEN'] }
12
+ async start() {
13
+ if (!this.token) throw new Error('DiscordAdapter: DISCORD_BOT_TOKEN required')
14
+ const gw = await fetch(`${this.api}/gateway/bot`, { headers: { authorization: `Bot ${this.token}` } }).then(r => r.json())
15
+ if (!gw.url) throw new Error('DiscordAdapter: gateway lookup failed: ' + JSON.stringify(gw))
16
+ this.gatewayUrl = gw.url + '/?v=10&encoding=json'
17
+ }
18
+ async stop() { try { this._ws?.close?.() } catch {} }
19
+ async send(reply) {
20
+ if (!this.token) throw new Error('DiscordAdapter: token required')
21
+ const url = `${this.api}/channels/${reply.to}/messages`
22
+ return fetch(url, { method: 'POST', headers: { authorization: `Bot ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ content: reply.text }) }).then(r => r.json())
23
+ }
24
+ }
@@ -0,0 +1,51 @@
1
+ import { EventEmitter } from 'node:events'
2
+ import net from 'node:net'
3
+
4
+ export class EmailAdapter extends EventEmitter {
5
+ constructor(opts = {}) {
6
+ super()
7
+ this.platform = 'email'
8
+ this.smtpHost = opts.smtpHost || process.env.SMTP_HOST
9
+ this.smtpPort = opts.smtpPort || Number(process.env.SMTP_PORT || 587)
10
+ this.smtpUser = opts.smtpUser || process.env.SMTP_USER
11
+ this.smtpPass = opts.smtpPass || process.env.SMTP_PASS
12
+ this.imapHost = opts.imapHost || process.env.IMAP_HOST
13
+ this._running = false
14
+ }
15
+ getRequiredEnv() { return ['SMTP_HOST', 'SMTP_USER', 'SMTP_PASS'] }
16
+ async start() {
17
+ if (!this.smtpHost || !this.smtpUser || !this.smtpPass) throw new Error('EmailAdapter: SMTP_HOST/USER/PASS required')
18
+ this._running = true
19
+ }
20
+ async stop() { this._running = false }
21
+ async send(reply) {
22
+ return new Promise((resolve, reject) => {
23
+ const sock = net.createConnection(this.smtpPort, this.smtpHost)
24
+ const lines = []
25
+ const send = s => sock.write(s + '\r\n')
26
+ sock.on('data', d => {
27
+ const text = d.toString()
28
+ lines.push(text)
29
+ const code = parseInt(text.slice(0, 3), 10)
30
+ if (code >= 400) { sock.end(); return reject(new Error('SMTP error: ' + text)) }
31
+ })
32
+ sock.on('error', reject)
33
+ sock.on('connect', () => {
34
+ send('EHLO freddie')
35
+ send('AUTH LOGIN')
36
+ send(Buffer.from(this.smtpUser).toString('base64'))
37
+ send(Buffer.from(this.smtpPass).toString('base64'))
38
+ send('MAIL FROM:<' + this.smtpUser + '>')
39
+ send('RCPT TO:<' + reply.to + '>')
40
+ send('DATA')
41
+ send('Subject: ' + (reply.subject || 'freddie'))
42
+ send('To: ' + reply.to)
43
+ send('')
44
+ send(reply.text)
45
+ send('.')
46
+ send('QUIT')
47
+ setTimeout(() => { sock.end(); resolve({ ok: true, log: lines.join('') }) }, 500)
48
+ })
49
+ })
50
+ }
51
+ }
@@ -0,0 +1,32 @@
1
+ import express from 'express'
2
+ import { EventEmitter } from 'node:events'
3
+
4
+ export class FeishuAdapter extends EventEmitter {
5
+ constructor(opts = {}) {
6
+ super()
7
+ this.platform = 'feishu'
8
+ this.token = opts.token || process.env.FEISHU_APP_TOKEN
9
+ this.port = opts.port || 0
10
+ this.api = opts.api || "https://open.feishu.cn/open-apis/im/v1/messages"
11
+ this._server = null
12
+ }
13
+ getRequiredEnv() { return ["FEISHU_APP_TOKEN"] }
14
+ async start() {
15
+ if (!this.token) throw new Error('FeishuAdapter: ' + this.getRequiredEnv().join(', ') + ' required')
16
+ const app = express()
17
+ app.use(express.json())
18
+ app.post('/webhook', (req, res) => {
19
+ const text = req.body?.text || req.body?.message?.text || req.body?.content || ''
20
+ const from = req.body?.from || req.body?.user_id || req.body?.sender_id || ''
21
+ this.emit('message', { from: String(from), text, raw: req.body })
22
+ res.json({ ok: true })
23
+ })
24
+ await new Promise(r => { this._server = app.listen(this.port, () => r()) })
25
+ this.port = this._server.address().port
26
+ }
27
+ async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
28
+ async send(reply) {
29
+ if (!this.token) throw new Error('FeishuAdapter: token required')
30
+ return fetch(this.api, { method: 'POST', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ to: reply.to, text: reply.text }) }).then(r => r.json())
31
+ }
32
+ }
@@ -0,0 +1,12 @@
1
+ export async function listComments({ token, docToken }) {
2
+ const r = await fetch('https://open.feishu.cn/open-apis/comments/v1/files/' + docToken + '/comments', { headers: { authorization: 'Bearer ' + token } })
3
+ return await r.json()
4
+ }
5
+ export async function postComment({ token, docToken, content }) {
6
+ const r = await fetch('https://open.feishu.cn/open-apis/comments/v1/files/' + docToken + '/comments', { method: 'POST', headers: { authorization: 'Bearer ' + token, 'content-type': 'application/json' }, body: JSON.stringify({ comment: { content } }) })
7
+ return await r.json()
8
+ }
9
+ export async function resolveComment({ token, docToken, commentId }) {
10
+ const r = await fetch('https://open.feishu.cn/open-apis/comments/v1/files/' + docToken + '/comments/' + commentId + '/patch', { method: 'PATCH', headers: { authorization: 'Bearer ' + token, 'content-type': 'application/json' }, body: JSON.stringify({ is_solved: true }) })
11
+ return await r.json()
12
+ }
@@ -0,0 +1,11 @@
1
+ export const COMMENT_RULES = {
2
+ auto_reply_keywords: ['question', 'pls', 'help'],
3
+ skip_authors: [],
4
+ max_comment_age_hours: 168,
5
+ }
6
+ export function shouldAutoReply(comment, rules = COMMENT_RULES) {
7
+ const text = String(comment?.content || '').toLowerCase()
8
+ if (rules.skip_authors.includes(comment.author)) return false
9
+ if (rules.max_comment_age_hours && comment.created && (Date.now() - comment.created) > rules.max_comment_age_hours * 3600_000) return false
10
+ return rules.auto_reply_keywords.some(k => text.includes(k.toLowerCase()))
11
+ }
@@ -0,0 +1,32 @@
1
+ import express from 'express'
2
+ import { EventEmitter } from 'node:events'
3
+
4
+ export class HomeassistantAdapter extends EventEmitter {
5
+ constructor(opts = {}) {
6
+ super()
7
+ this.platform = 'homeassistant'
8
+ this.token = opts.token || process.env.HASS_TOKEN
9
+ this.port = opts.port || 0
10
+ this.api = opts.api || "http://homeassistant.local:8123/api/services/notify/notify"
11
+ this._server = null
12
+ }
13
+ getRequiredEnv() { return ["HASS_TOKEN"] }
14
+ async start() {
15
+ if (!this.token) throw new Error('HomeassistantAdapter: ' + this.getRequiredEnv().join(', ') + ' required')
16
+ const app = express()
17
+ app.use(express.json())
18
+ app.post('/webhook', (req, res) => {
19
+ const text = req.body?.text || req.body?.message?.text || req.body?.content || ''
20
+ const from = req.body?.from || req.body?.user_id || req.body?.sender_id || ''
21
+ this.emit('message', { from: String(from), text, raw: req.body })
22
+ res.json({ ok: true })
23
+ })
24
+ await new Promise(r => { this._server = app.listen(this.port, () => r()) })
25
+ this.port = this._server.address().port
26
+ }
27
+ async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
28
+ async send(reply) {
29
+ if (!this.token) throw new Error('HomeassistantAdapter: token required')
30
+ return fetch(this.api, { method: 'POST', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ to: reply.to, text: reply.text }) }).then(r => r.json())
31
+ }
32
+ }
@@ -0,0 +1,40 @@
1
+ import { EventEmitter } from 'node:events'
2
+
3
+ export class MatrixAdapter extends EventEmitter {
4
+ constructor(opts = {}) {
5
+ super()
6
+ this.platform = 'matrix'
7
+ this.homeserver = opts.homeserver || process.env.MATRIX_HOMESERVER
8
+ this.token = opts.token || process.env.MATRIX_ACCESS_TOKEN
9
+ this.since = null
10
+ this._running = false
11
+ }
12
+ getRequiredEnv() { return ['MATRIX_HOMESERVER', 'MATRIX_ACCESS_TOKEN'] }
13
+ async start() {
14
+ if (!this.homeserver || !this.token) throw new Error('MatrixAdapter: MATRIX_HOMESERVER + MATRIX_ACCESS_TOKEN required')
15
+ this._running = true
16
+ this._loop()
17
+ }
18
+ async stop() { this._running = false }
19
+ async _loop() {
20
+ while (this._running) {
21
+ try {
22
+ const url = `${this.homeserver}/_matrix/client/v3/sync?timeout=25000${this.since ? '&since=' + this.since : ''}`
23
+ const res = await fetch(url, { headers: { authorization: `Bearer ${this.token}` } })
24
+ const data = await res.json()
25
+ this.since = data.next_batch
26
+ const rooms = data.rooms?.join || {}
27
+ for (const [roomId, room] of Object.entries(rooms)) {
28
+ for (const ev of (room.timeline?.events || [])) {
29
+ if (ev.type === 'm.room.message') this.emit('message', { from: roomId, text: ev.content?.body || '', user: ev.sender, raw: ev })
30
+ }
31
+ }
32
+ } catch (e) { await new Promise(r => setTimeout(r, 2000)) }
33
+ }
34
+ }
35
+ async send(reply) {
36
+ const txnId = Date.now() + '-' + Math.random().toString(36).slice(2, 8)
37
+ const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(reply.to)}/send/m.room.message/${txnId}`
38
+ return fetch(url, { method: 'PUT', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ msgtype: 'm.text', body: reply.text }) }).then(r => r.json())
39
+ }
40
+ }