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.
- package/AGENTS.md +85 -11
- package/CHANGELOG.md +16 -0
- package/README.md +2 -2
- package/bin/freddie.js +12 -109
- package/package.json +11 -2
- package/src/acp/server.js +3 -3
- package/src/acp/session.js +8 -8
- package/src/acp/tools.js +5 -4
- package/src/agent/account_usage.js +5 -5
- package/src/agent/credential_sources.js +2 -2
- package/src/agent/curator.js +5 -5
- package/src/agent/machine.js +3 -2
- package/src/agent/manual_compression_feedback.js +5 -5
- package/src/agent/shell_hooks.js +2 -2
- package/src/auth.js +2 -2
- package/src/batch.js +2 -2
- package/src/cli/backup.js +3 -3
- package/src/cli/doctor.js +3 -3
- package/src/cli/dump.js +4 -2
- package/src/cli/env_loader.js +2 -2
- package/src/cli/gateway_cli.js +3 -4
- package/src/cli/hooks.js +2 -2
- package/src/cli/logs.js +4 -4
- package/src/cli/mcp_config.js +2 -2
- package/src/cli/plugins_cmd.js +3 -3
- package/src/cli/status.js +1 -1
- package/src/cli/tools_config.js +2 -2
- package/src/cli/uninstall.js +2 -2
- package/src/config.js +2 -2
- package/src/db.js +3 -3
- package/src/gateway/platforms.js +21 -0
- package/src/home.js +2 -2
- package/src/host/contract.js +39 -0
- package/src/host/host.js +159 -0
- package/src/host/index.js +27 -0
- package/src/index.js +2 -1
- package/src/mcp/server.js +5 -4
- package/src/observability/log.js +2 -2
- package/src/plugins/disk_cleanup/index.js +2 -2
- package/src/plugins/manager.js +13 -63
- package/src/plugins/memory/provider.js +26 -26
- package/src/plugins/observability/index.js +3 -3
- package/src/skills/index.js +2 -2
- package/src/skin/engine.js +2 -2
- package/src/toolsets.js +13 -15
- package/src/web/index.html +1 -1
- package/src/web/server.js +8 -94
- package/src/gateway/platforms/api_server.js +0 -21
- package/src/gateway/platforms/bluebubbles.js +0 -32
- package/src/gateway/platforms/dingtalk.js +0 -32
- package/src/gateway/platforms/discord.js +0 -24
- package/src/gateway/platforms/email.js +0 -51
- package/src/gateway/platforms/feishu.js +0 -32
- package/src/gateway/platforms/feishu_comment.js +0 -12
- package/src/gateway/platforms/feishu_comment_rules.js +0 -11
- package/src/gateway/platforms/homeassistant.js +0 -32
- package/src/gateway/platforms/matrix.js +0 -40
- package/src/gateway/platforms/mattermost.js +0 -29
- package/src/gateway/platforms/qqbot.js +0 -32
- package/src/gateway/platforms/signal.js +0 -33
- package/src/gateway/platforms/slack.js +0 -34
- package/src/gateway/platforms/sms.js +0 -34
- package/src/gateway/platforms/telegram.js +0 -38
- package/src/gateway/platforms/telegram_network.js +0 -17
- package/src/gateway/platforms/webhook.js +0 -19
- package/src/gateway/platforms/wecom.js +0 -32
- package/src/gateway/platforms/wecom_callback.js +0 -15
- package/src/gateway/platforms/wecom_crypto.js +0 -16
- package/src/gateway/platforms/weixin.js +0 -32
- package/src/gateway/platforms/whatsapp.js +0 -40
- package/src/gateway/platforms/yuanbao.js +0 -9
- package/src/gateway/platforms/yuanbao_media.js +0 -5
- package/src/gateway/platforms/yuanbao_proto.js +0 -9
- package/src/gateway/platforms/yuanbao_sticker.js +0 -6
- package/src/plugins/memory/_index.js +0 -8
- package/src/plugins/memory/byterover.js +0 -25
- package/src/plugins/memory/hindsight.js +0 -25
- package/src/plugins/memory/holographic.js +0 -31
- package/src/plugins/memory/honcho.js +0 -25
- package/src/plugins/memory/mem0.js +0 -25
- package/src/plugins/memory/openviking.js +0 -25
- package/src/plugins/memory/retaindb.js +0 -25
- package/src/plugins/memory/supermemory.js +0 -25
- package/src/tools/ansi_strip.js +0 -8
- package/src/tools/approval.js +0 -15
- package/src/tools/bash.js +0 -35
- package/src/tools/binary_extensions.js +0 -22
- package/src/tools/browser.js +0 -48
- package/src/tools/budget_config.js +0 -13
- package/src/tools/checkpoint.js +0 -29
- package/src/tools/clarify.js +0 -15
- package/src/tools/code_execution.js +0 -27
- package/src/tools/credential_files.js +0 -16
- package/src/tools/cronjob.js +0 -16
- package/src/tools/debug_helpers.js +0 -9
- package/src/tools/delegate.js +0 -28
- package/src/tools/discord_tool.js +0 -13
- package/src/tools/edit.js +0 -31
- package/src/tools/env_passthrough.js +0 -15
- package/src/tools/feishu_doc.js +0 -15
- package/src/tools/feishu_drive.js +0 -14
- package/src/tools/file_operations.js +0 -17
- package/src/tools/file_state.js +0 -16
- package/src/tools/file_tools.js +0 -23
- package/src/tools/fuzzy_match.js +0 -8
- package/src/tools/grep.js +0 -51
- package/src/tools/homeassistant_tool.js +0 -15
- package/src/tools/image_gen.js +0 -33
- package/src/tools/interrupt.js +0 -18
- package/src/tools/managed_tool_gateway.js +0 -11
- package/src/tools/mcp_oauth.js +0 -21
- package/src/tools/mcp_oauth_manager.js +0 -20
- package/src/tools/mcp_tool.js +0 -36
- package/src/tools/memory.js +0 -66
- package/src/tools/mixture_of_agents.js +0 -14
- package/src/tools/neutts_synth.js +0 -13
- package/src/tools/openrouter_client.js +0 -13
- package/src/tools/osv_check.js +0 -11
- package/src/tools/patch_parser.js +0 -42
- package/src/tools/path_security.js +0 -16
- package/src/tools/process_registry.js +0 -17
- package/src/tools/read.js +0 -26
- package/src/tools/registry.js +0 -54
- package/src/tools/rl_training.js +0 -13
- package/src/tools/schema_sanitizer.js +0 -18
- package/src/tools/send_message.js +0 -32
- package/src/tools/session_search.js +0 -23
- package/src/tools/skill_manager.js +0 -17
- package/src/tools/skill_usage.js +0 -20
- package/src/tools/skills_guard.js +0 -17
- package/src/tools/skills_hub.js +0 -31
- package/src/tools/skills_index.js +0 -14
- package/src/tools/skills_sync.js +0 -19
- package/src/tools/skills_tool.js +0 -11
- package/src/tools/slash_confirm.js +0 -16
- package/src/tools/terminal.js +0 -29
- package/src/tools/tirith_security.js +0 -25
- package/src/tools/todo.js +0 -54
- package/src/tools/tool_backend_helpers.js +0 -26
- package/src/tools/tool_output_limits.js +0 -15
- package/src/tools/tool_result_storage.js +0 -20
- package/src/tools/transcription.js +0 -19
- package/src/tools/tts.js +0 -19
- package/src/tools/url_safety.js +0 -15
- package/src/tools/vision.js +0 -18
- package/src/tools/voice_mode.js +0 -10
- package/src/tools/web_search.js +0 -37
- package/src/tools/web_tools.js +0 -18
- package/src/tools/website_policy.js +0 -14
- package/src/tools/write.js +0 -25
- package/src/tools/xai_http.js +0 -13
- package/src/tools/yuanbao_tools.js +0 -13
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { host, bootHost } from '../../host/index.js'
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const _custom = new Map()
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
|
8
|
-
for (const [name, value] of _counters) lines.push(`
|
|
9
|
-
for (const sub of listDebug()) lines.push(`
|
|
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 = {
|
package/src/skills/index.js
CHANGED
|
@@ -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 {
|
|
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(
|
|
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
|
package/src/skin/engine.js
CHANGED
|
@@ -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 {
|
|
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(
|
|
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 {
|
|
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
|
|
7
|
-
const
|
|
8
|
-
|
|
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
|
|
15
|
-
const
|
|
16
|
-
|
|
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
|
|
23
|
-
|
|
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
|
}
|
package/src/web/index.html
CHANGED
|
@@ -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>
|
|
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 {
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
}
|