freddie 0.0.41 → 0.0.42

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 (152) hide show
  1. package/AGENTS.md +85 -11
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +2 -2
  4. package/bin/freddie.js +12 -109
  5. package/package.json +11 -2
  6. package/src/acp/server.js +3 -3
  7. package/src/acp/session.js +8 -8
  8. package/src/acp/tools.js +5 -4
  9. package/src/agent/account_usage.js +5 -5
  10. package/src/agent/credential_sources.js +2 -2
  11. package/src/agent/curator.js +5 -5
  12. package/src/agent/machine.js +3 -2
  13. package/src/agent/manual_compression_feedback.js +5 -5
  14. package/src/agent/shell_hooks.js +2 -2
  15. package/src/auth.js +2 -2
  16. package/src/batch.js +2 -2
  17. package/src/cli/backup.js +3 -3
  18. package/src/cli/doctor.js +3 -3
  19. package/src/cli/dump.js +4 -2
  20. package/src/cli/env_loader.js +2 -2
  21. package/src/cli/gateway_cli.js +3 -4
  22. package/src/cli/hooks.js +2 -2
  23. package/src/cli/logs.js +4 -4
  24. package/src/cli/mcp_config.js +2 -2
  25. package/src/cli/plugins_cmd.js +3 -3
  26. package/src/cli/status.js +1 -1
  27. package/src/cli/tools_config.js +2 -2
  28. package/src/cli/uninstall.js +2 -2
  29. package/src/config.js +2 -2
  30. package/src/db.js +3 -3
  31. package/src/gateway/platforms.js +21 -0
  32. package/src/home.js +2 -2
  33. package/src/host/contract.js +39 -0
  34. package/src/host/host.js +159 -0
  35. package/src/host/index.js +27 -0
  36. package/src/index.js +2 -1
  37. package/src/mcp/server.js +5 -4
  38. package/src/observability/log.js +2 -2
  39. package/src/plugins/disk_cleanup/index.js +2 -2
  40. package/src/plugins/manager.js +13 -63
  41. package/src/plugins/memory/provider.js +26 -26
  42. package/src/plugins/observability/index.js +3 -3
  43. package/src/skills/index.js +2 -2
  44. package/src/skin/engine.js +2 -2
  45. package/src/toolsets.js +13 -15
  46. package/src/web/index.html +1 -1
  47. package/src/web/server.js +8 -94
  48. package/src/gateway/platforms/api_server.js +0 -21
  49. package/src/gateway/platforms/bluebubbles.js +0 -32
  50. package/src/gateway/platforms/dingtalk.js +0 -32
  51. package/src/gateway/platforms/discord.js +0 -24
  52. package/src/gateway/platforms/email.js +0 -51
  53. package/src/gateway/platforms/feishu.js +0 -32
  54. package/src/gateway/platforms/feishu_comment.js +0 -12
  55. package/src/gateway/platforms/feishu_comment_rules.js +0 -11
  56. package/src/gateway/platforms/homeassistant.js +0 -32
  57. package/src/gateway/platforms/matrix.js +0 -40
  58. package/src/gateway/platforms/mattermost.js +0 -29
  59. package/src/gateway/platforms/qqbot.js +0 -32
  60. package/src/gateway/platforms/signal.js +0 -33
  61. package/src/gateway/platforms/slack.js +0 -34
  62. package/src/gateway/platforms/sms.js +0 -34
  63. package/src/gateway/platforms/telegram.js +0 -38
  64. package/src/gateway/platforms/telegram_network.js +0 -17
  65. package/src/gateway/platforms/webhook.js +0 -19
  66. package/src/gateway/platforms/wecom.js +0 -32
  67. package/src/gateway/platforms/wecom_callback.js +0 -15
  68. package/src/gateway/platforms/wecom_crypto.js +0 -16
  69. package/src/gateway/platforms/weixin.js +0 -32
  70. package/src/gateway/platforms/whatsapp.js +0 -40
  71. package/src/gateway/platforms/yuanbao.js +0 -9
  72. package/src/gateway/platforms/yuanbao_media.js +0 -5
  73. package/src/gateway/platforms/yuanbao_proto.js +0 -9
  74. package/src/gateway/platforms/yuanbao_sticker.js +0 -6
  75. package/src/plugins/memory/_index.js +0 -8
  76. package/src/plugins/memory/byterover.js +0 -25
  77. package/src/plugins/memory/hindsight.js +0 -25
  78. package/src/plugins/memory/holographic.js +0 -31
  79. package/src/plugins/memory/honcho.js +0 -25
  80. package/src/plugins/memory/mem0.js +0 -25
  81. package/src/plugins/memory/openviking.js +0 -25
  82. package/src/plugins/memory/retaindb.js +0 -25
  83. package/src/plugins/memory/supermemory.js +0 -25
  84. package/src/tools/ansi_strip.js +0 -8
  85. package/src/tools/approval.js +0 -15
  86. package/src/tools/bash.js +0 -35
  87. package/src/tools/binary_extensions.js +0 -22
  88. package/src/tools/browser.js +0 -48
  89. package/src/tools/budget_config.js +0 -13
  90. package/src/tools/checkpoint.js +0 -29
  91. package/src/tools/clarify.js +0 -15
  92. package/src/tools/code_execution.js +0 -27
  93. package/src/tools/credential_files.js +0 -16
  94. package/src/tools/cronjob.js +0 -16
  95. package/src/tools/debug_helpers.js +0 -9
  96. package/src/tools/delegate.js +0 -28
  97. package/src/tools/discord_tool.js +0 -13
  98. package/src/tools/edit.js +0 -31
  99. package/src/tools/env_passthrough.js +0 -15
  100. package/src/tools/feishu_doc.js +0 -15
  101. package/src/tools/feishu_drive.js +0 -14
  102. package/src/tools/file_operations.js +0 -17
  103. package/src/tools/file_state.js +0 -16
  104. package/src/tools/file_tools.js +0 -23
  105. package/src/tools/fuzzy_match.js +0 -8
  106. package/src/tools/grep.js +0 -51
  107. package/src/tools/homeassistant_tool.js +0 -15
  108. package/src/tools/image_gen.js +0 -33
  109. package/src/tools/interrupt.js +0 -18
  110. package/src/tools/managed_tool_gateway.js +0 -11
  111. package/src/tools/mcp_oauth.js +0 -21
  112. package/src/tools/mcp_oauth_manager.js +0 -20
  113. package/src/tools/mcp_tool.js +0 -36
  114. package/src/tools/memory.js +0 -66
  115. package/src/tools/mixture_of_agents.js +0 -14
  116. package/src/tools/neutts_synth.js +0 -13
  117. package/src/tools/openrouter_client.js +0 -13
  118. package/src/tools/osv_check.js +0 -11
  119. package/src/tools/patch_parser.js +0 -42
  120. package/src/tools/path_security.js +0 -16
  121. package/src/tools/process_registry.js +0 -17
  122. package/src/tools/read.js +0 -26
  123. package/src/tools/registry.js +0 -54
  124. package/src/tools/rl_training.js +0 -13
  125. package/src/tools/schema_sanitizer.js +0 -18
  126. package/src/tools/send_message.js +0 -32
  127. package/src/tools/session_search.js +0 -23
  128. package/src/tools/skill_manager.js +0 -17
  129. package/src/tools/skill_usage.js +0 -20
  130. package/src/tools/skills_guard.js +0 -17
  131. package/src/tools/skills_hub.js +0 -31
  132. package/src/tools/skills_index.js +0 -14
  133. package/src/tools/skills_sync.js +0 -19
  134. package/src/tools/skills_tool.js +0 -11
  135. package/src/tools/slash_confirm.js +0 -16
  136. package/src/tools/terminal.js +0 -29
  137. package/src/tools/tirith_security.js +0 -25
  138. package/src/tools/todo.js +0 -54
  139. package/src/tools/tool_backend_helpers.js +0 -26
  140. package/src/tools/tool_output_limits.js +0 -15
  141. package/src/tools/tool_result_storage.js +0 -20
  142. package/src/tools/transcription.js +0 -19
  143. package/src/tools/tts.js +0 -19
  144. package/src/tools/url_safety.js +0 -15
  145. package/src/tools/vision.js +0 -18
  146. package/src/tools/voice_mode.js +0 -10
  147. package/src/tools/web_search.js +0 -37
  148. package/src/tools/web_tools.js +0 -18
  149. package/src/tools/website_policy.js +0 -14
  150. package/src/tools/write.js +0 -25
  151. package/src/tools/xai_http.js +0 -13
  152. package/src/tools/yuanbao_tools.js +0 -13
@@ -1,35 +1,35 @@
1
- import { HonchoMemory, Mem0Memory, SupermemoryMemory, ByteroverMemory, HindsightMemory, HolographicMemory, OpenvikingMemory, RetaindbMemory } from './_index.js'
1
+ import { host, bootHost } from '../../host/index.js'
2
2
 
3
- const _registered = new Map()
3
+ const _custom = new Map()
4
4
 
5
- export class MemoryProvider {
6
- constructor(opts = {}) { this.opts = opts; this.name = 'base' }
7
- async syncTurn(_messages) { throw new Error('syncTurn not implemented') }
8
- async prefetch(_query) { throw new Error('prefetch not implemented') }
9
- async shutdown() {}
10
- async postSetup(_home, _config) {}
11
- getRequiredEnv() { return [] }
5
+ function ensureBoot() {
6
+ const h = host()
7
+ return h.pi && h.pi.memory.size() > 0 ? h : bootHost()
12
8
  }
13
9
 
14
- export function registerMemoryProvider(name, factory) { _registered.set(name, factory) }
15
- export function listMemoryProviders() { return [..._registered.keys()] }
16
- export function createMemoryProvider(name, opts) {
17
- const factory = _registered.get(name)
18
- if (!factory) throw new Error(`unknown memory provider: ${name}. Available: ${[..._registered.keys()].join(',') || 'none'}`)
19
- return factory(opts)
10
+ export function listMemoryProviders() {
11
+ const h = host()
12
+ const names = (h.pi && h.pi.memory) ? h.pi.memory.list().map(m => m.name) : []
13
+ return [...new Set([...names, ..._custom.keys()])]
20
14
  }
21
15
 
22
- const PROVIDERS = {
23
- honcho: HonchoMemory,
24
- mem0: Mem0Memory,
25
- supermemory: SupermemoryMemory,
26
- byterover: ByteroverMemory,
27
- hindsight: HindsightMemory,
28
- holographic: HolographicMemory,
29
- openviking: OpenvikingMemory,
30
- retaindb: RetaindbMemory,
16
+ export function createMemoryProvider(name, opts = {}) {
17
+ if (_custom.has(name)) return new (_custom.get(name))(opts)
18
+ const h = host()
19
+ const rec = h.pi?.memory.get(name)
20
+ if (!rec) throw new Error(`memory provider not found: ${name}. Boot host first or register a provider class.`)
21
+ const mod = rec.module || {}
22
+ const cls = Object.values(mod).find(v => typeof v === 'function' && /Memory$/.test(v.name)) || Object.values(mod).find(v => typeof v === 'function')
23
+ if (!cls) throw new Error(`memory provider ${name}: no class exported`)
24
+ return new cls(opts)
31
25
  }
32
26
 
33
- for (const [name, Cls] of Object.entries(PROVIDERS)) {
34
- registerMemoryProvider(name, (opts) => new Cls(opts))
27
+ export function registerMemoryProvider(name, cls) { _custom.set(name, cls) }
28
+
29
+ export class MemoryProvider {
30
+ constructor(opts = {}) { Object.assign(this, opts) }
31
+ async syncTurn() {}
32
+ async prefetch() { return { items: [] } }
35
33
  }
34
+
35
+ export async function ensureProvidersLoaded() { await ensureBoot() }
@@ -4,9 +4,9 @@ const _counters = new Map()
4
4
  export function inc(name, delta = 1) { _counters.set(name, (_counters.get(name) || 0) + delta) }
5
5
  export function counters() { return Object.fromEntries(_counters) }
6
6
  export function metricsText() {
7
- const lines = ['# HELP foph_counter generic counter', '# TYPE foph_counter counter']
8
- for (const [name, value] of _counters) lines.push(`foph_counter{name="${name}"} ${value}`)
9
- for (const sub of listDebug()) lines.push(`foph_subsystem{name="${sub}"} 1`)
7
+ const lines = ['# HELP freddie_counter generic counter', '# TYPE freddie_counter counter']
8
+ for (const [name, value] of _counters) lines.push(`freddie_counter{name="${name}"} ${value}`)
9
+ for (const sub of listDebug()) lines.push(`freddie_subsystem{name="${sub}"} 1`)
10
10
  return lines.join('\n') + '\n'
11
11
  }
12
12
  export const plugin = {
@@ -2,12 +2,12 @@ import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import yaml from 'js-yaml'
4
4
  import os from 'node:os'
5
- import { getFophHome } from '../home.js'
5
+ import { getFreddieHome } from '../home.js'
6
6
 
7
7
  const FRONTMATTER = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/
8
8
 
9
9
  export function listSkills(extraDirs = []) {
10
- const dirs = [path.join(getFophHome(), 'skills'), path.join(process.cwd(), 'skills'), ...extraDirs]
10
+ const dirs = [path.join(getFreddieHome(), 'skills'), path.join(process.cwd(), 'skills'), ...extraDirs]
11
11
  const out = []
12
12
  for (const d of dirs) {
13
13
  if (!fs.existsSync(d)) continue
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import yaml from 'js-yaml'
4
- import { getFophHome } from '../home.js'
4
+ import { getFreddieHome } from '../home.js'
5
5
  import { getConfigValue, saveConfigValue } from '../config.js'
6
6
 
7
7
  const _BUILTIN_SKINS = {
@@ -38,7 +38,7 @@ const _BUILTIN_SKINS = {
38
38
  let _active = null
39
39
 
40
40
  export function loadSkin(name) {
41
- const userPath = path.join(getFophHome(), 'skins', `${name}.yaml`)
41
+ const userPath = path.join(getFreddieHome(), 'skins', `${name}.yaml`)
42
42
  if (fs.existsSync(userPath)) {
43
43
  const fromYaml = yaml.load(fs.readFileSync(userPath, 'utf8')) || {}
44
44
  return mergeWithDefault(fromYaml)
package/src/toolsets.js CHANGED
@@ -1,26 +1,24 @@
1
- import { registry, discoverBuiltinTools } from './tools/registry.js'
1
+ import { bootHost } from './host/index.js'
2
2
 
3
3
  export const _FREDDIE_CORE_TOOLS = ['bash', 'read', 'write', 'edit', 'grep']
4
4
 
5
+ function available(host) {
6
+ return host.pi.tools.list().filter(t => !t.checkFn || t.checkFn(t) !== false)
7
+ }
8
+
5
9
  export async function getEnabledToolSchemas(enabled = ['core'], disabled = []) {
6
- await discoverBuiltinTools()
7
- const all = registry.available()
8
- const enabledSet = new Set(enabled)
9
- const disabledSet = new Set(disabled)
10
- return all.filter(t => enabledSet.has(t.toolset) && !disabledSet.has(t.name)).map(t => t.schema)
10
+ const h = await bootHost()
11
+ const enabledSet = new Set(enabled); const disabledSet = new Set(disabled)
12
+ return available(h).filter(t => enabledSet.has(t.toolset || 'core') && !disabledSet.has(t.name)).map(t => t.schema)
11
13
  }
12
14
 
13
15
  export async function getEnabledToolNames(enabled = ['core'], disabled = []) {
14
- await discoverBuiltinTools()
15
- const all = registry.available()
16
- const enabledSet = new Set(enabled)
17
- const disabledSet = new Set(disabled)
18
- return all.filter(t => enabledSet.has(t.toolset) && !disabledSet.has(t.name)).map(t => t.name)
16
+ const h = await bootHost()
17
+ const enabledSet = new Set(enabled); const disabledSet = new Set(disabled)
18
+ return available(h).filter(t => enabledSet.has(t.toolset || 'core') && !disabledSet.has(t.name)).map(t => t.name)
19
19
  }
20
20
 
21
21
  export async function getAvailableToolsets() {
22
- await discoverBuiltinTools()
23
- const ts = new Set()
24
- for (const t of registry.list()) ts.add(t.toolset)
25
- return [...ts]
22
+ const h = await bootHost()
23
+ return [...new Set(h.pi.tools.list().map(t => t.toolset || 'core'))]
26
24
  }
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Foph Dashboard</title>
6
+ <title>Freddie Dashboard</title>
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
8
8
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600&family=Archivo+Black&family=JetBrains+Mono:wght@400;500&display=swap">
9
9
  <script type="importmap">
package/src/web/server.js CHANGED
@@ -1,108 +1,22 @@
1
1
  import express from 'express'
2
2
  import path from 'node:path'
3
- import fs from 'node:fs'
4
3
  import { fileURLToPath } from 'node:url'
5
- import { listSessions, search, getMessages } from '../sessions.js'
6
- import { registry, discoverBuiltinTools } from '../tools/registry.js'
7
- import { listDebug, snapshot, snapshotAll, attachDebugRoutes } from '../observability/debug.js'
8
- import { loadConfig, saveConfigValue } from '../config.js'
9
- import { listJobs, createJob, deleteJob } from '../cron/scheduler.js'
10
- import { listSkills } from '../skills/index.js'
11
- import { listAllProfiles } from '../commands/profile.js'
12
- import { COMMAND_REGISTRY } from '../commands/registry.js'
13
- import { getFophHome } from '../home.js'
14
- import { runTurn } from '../agent/machine.js'
15
- import { runBatch } from '../batch.js'
4
+ import { bootHost } from '../host/index.js'
16
5
 
17
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
18
7
 
19
- const ENV_KEYS = [
20
- 'ANTHROPIC_API_KEY','OPENAI_API_KEY','GROQ_API_KEY','OPENROUTER_API_KEY',
21
- 'TELEGRAM_BOT_TOKEN','DISCORD_BOT_TOKEN','SLACK_BOT_TOKEN','SLACK_SIGNING_SECRET',
22
- 'WHATSAPP_API_TOKEN','SIGNAL_CLI_URL','MATRIX_HOMESERVER','MATTERMOST_URL',
23
- 'HONCHO_API_KEY','MEM0_API_KEY','SUPERMEMORY_API_KEY','BYTEROVER_API_KEY',
24
- 'HINDSIGHT_API_KEY','OPENVIKING_API_KEY','RETAINDB_API_KEY','SERPAPI_KEY',
25
- 'REPLICATE_API_TOKEN','SMTP_HOST','TWILIO_SID','HASS_TOKEN',
26
- ]
27
-
28
- function readLogs(subsystem, max = 200) {
29
- const file = path.join(getFophHome(), 'logs', `${subsystem}.log`)
30
- if (!fs.existsSync(file)) return []
31
- const lines = fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).slice(-max)
32
- return lines.map(l => { try { return JSON.parse(l) } catch { return { raw: l } } })
33
- }
34
-
35
8
  export async function createDashboard({ port = 0 } = {}) {
36
- await discoverBuiltinTools()
9
+ const host = await bootHost()
37
10
  const app = express()
38
11
  app.use(express.json())
39
12
  app.use(express.static(__dirname))
40
13
  app.use('/vendor/anentrypoint-design', express.static(path.join(__dirname, '..', '..', 'node_modules', 'anentrypoint-design', 'dist')))
41
- app.get('/api/sessions', (_, res) => res.json(listSessions()))
42
- app.get('/api/sessions/:id/messages', (req, res) => res.json(getMessages(req.params.id)))
43
- app.get('/api/search', (req, res) => res.json(search(String(req.query.q || ''))))
44
- app.get('/api/tools', (_, res) => res.json(registry.list().map(t => ({ name: t.name, toolset: t.toolset, schema: t.schema }))))
45
- app.get('/api/debug', (_, res) => res.json(listDebug()))
46
- app.get('/api/debug-all', (_, res) => res.json(snapshotAll()))
47
- app.get('/api/config', (_, res) => res.json(loadConfig()))
48
- app.get('/api/cron', (_, res) => res.json(listJobs()))
49
- app.get('/api/skills', (_, res) => {
50
- const home = listSkills()
51
- const bundled = listSkills([path.resolve('skills')])
52
- res.json({ home, bundled })
53
- })
54
- app.get('/api/profiles', (_, res) => res.json(listAllProfiles()))
55
- app.get('/api/commands', (_, res) => res.json(COMMAND_REGISTRY))
56
- app.get('/api/env', (_, res) => res.json(ENV_KEYS.map(k => ({ key: k, set: !!process.env[k] }))))
57
- app.get('/api/logs/:subsystem', (req, res) => res.json(readLogs(req.params.subsystem, Number(req.query.max) || 200)))
58
- app.get('/api/logs', (_, res) => {
59
- const dir = path.join(getFophHome(), 'logs')
60
- if (!fs.existsSync(dir)) return res.json([])
61
- res.json(fs.readdirSync(dir).filter(f => f.endsWith('.log')).map(f => f.replace(/\.log$/, '')))
62
- })
63
- app.get('/api/health', (_, res) => res.json({ ok: true, ts: Date.now(), foph_home: getFophHome() }))
64
- app.get('/api/tools/detail', (_, res) => res.json(registry.list().map(t => ({ name: t.name, toolset: t.toolset, description: t.schema?.description || '' }))))
65
- app.get('/api/gateway', (_, res) => {
66
- const platforms = ['webhook', 'api_server', 'telegram', 'discord', 'slack', 'whatsapp', 'signal', 'matrix',
67
- 'mattermost', 'email', 'sms', 'mqtt', 'feishu', 'line', 'viber', 'teams', 'wechat', 'rss']
68
- res.json({ platforms: platforms.map(p => ({ name: p, enabled: false, note: 'start with freddie gateway --port <port>' })) })
69
- })
70
- app.post('/api/batch', async (req, res) => {
71
- const { prompts = [], concurrency = 4, model = '' } = req.body || {}
72
- if (!prompts.length) return res.status(400).json({ error: 'prompts required' })
73
- try { res.json(await runBatch({ prompts, concurrency, model })) } catch (e) { res.status(500).json({ error: String(e.message || e) }) }
74
- })
75
-
76
- app.post('/api/cron', (req, res) => {
77
- const { cron, prompt, model = null } = req.body || {}
78
- if (!cron || !prompt) return res.status(400).json({ error: 'cron and prompt required' })
79
- try { res.json({ id: createJob({ cron, prompt, model }) }) } catch (e) { res.status(400).json({ error: String(e.message || e) }) }
80
- })
81
- app.delete('/api/cron/:id', (req, res) => { deleteJob(Number(req.params.id)); res.json({ ok: true }) })
82
-
83
- app.post('/api/config', (req, res) => {
84
- const { key, value } = req.body || {}
85
- if (!key) return res.status(400).json({ error: 'key required' })
86
- try { res.json(saveConfigValue(key, value)) } catch (e) { res.status(400).json({ error: String(e.message || e) }) }
87
- })
88
-
89
- app.post('/api/chat', async (req, res) => {
90
- const { prompt, sessionId = null } = req.body || {}
91
- if (!prompt) return res.status(400).json({ error: 'prompt required' })
92
- res.setHeader('Content-Type', 'text/event-stream')
93
- res.setHeader('Cache-Control', 'no-cache')
94
- res.setHeader('Connection', 'keep-alive')
95
- const send = (event, data) => res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
96
- send('start', { ts: Date.now(), sessionId })
97
- try {
98
- const out = await runTurn({ prompt, timeoutMs: 30000 })
99
- for (const m of out.messages) send('message', m)
100
- send('done', { result: out.result || '', iterations: out.iterations })
101
- } catch (e) { send('error', { error: String(e.message || e) }) }
102
- res.end()
103
- })
104
-
105
- attachDebugRoutes(app)
14
+ for (const r of host.gui.routes.list()) {
15
+ const verb = r.method.toLowerCase()
16
+ if (typeof app[verb] === 'function') app[verb](r.path, r.handler)
17
+ }
18
+ const debugApi = host.gui._state.apis.get('debug')
19
+ if (debugApi?.attach) debugApi.attach(app)
106
20
  const server = await new Promise(r => { const s = app.listen(port, () => r(s)) })
107
21
  const actualPort = server.address().port
108
22
  return { server, port: actualPort, url: `http://127.0.0.1:${actualPort}/`, stop: () => new Promise(r => server.close(() => r())) }
@@ -1,21 +0,0 @@
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
- }
@@ -1,32 +0,0 @@
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
- }
@@ -1,32 +0,0 @@
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
- }
@@ -1,24 +0,0 @@
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
- }
@@ -1,51 +0,0 @@
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
- }
@@ -1,32 +0,0 @@
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
- }
@@ -1,12 +0,0 @@
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
- }
@@ -1,11 +0,0 @@
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
- }
@@ -1,32 +0,0 @@
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
- }
@@ -1,40 +0,0 @@
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
- }
@@ -1,29 +0,0 @@
1
- import express from 'express'
2
- import { EventEmitter } from 'node:events'
3
-
4
- export class MattermostAdapter extends EventEmitter {
5
- constructor(opts = {}) {
6
- super()
7
- this.platform = 'mattermost'
8
- this.url = opts.url || process.env.MATTERMOST_URL
9
- this.token = opts.token || process.env.MATTERMOST_TOKEN
10
- this.port = opts.port || 0
11
- this._server = null
12
- }
13
- getRequiredEnv() { return ['MATTERMOST_URL', 'MATTERMOST_TOKEN'] }
14
- async start() {
15
- if (!this.url || !this.token) throw new Error('MattermostAdapter: MATTERMOST_URL + MATTERMOST_TOKEN required')
16
- const app = express()
17
- app.use(express.urlencoded({ extended: true }))
18
- app.post('/hook', (req, res) => {
19
- this.emit('message', { from: req.body.channel_id, text: req.body.text || '', user: req.body.user_id, raw: req.body })
20
- res.json({})
21
- })
22
- await new Promise(r => { this._server = app.listen(this.port, () => r()) })
23
- this.port = this._server.address().port
24
- }
25
- async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
26
- async send(reply) {
27
- return fetch(`${this.url}/api/v4/posts`, { method: 'POST', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ channel_id: reply.to, message: reply.text }) }).then(r => r.json())
28
- }
29
- }