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
package/src/cli/dump.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
3
|
+
import { getFreddieHome } from '../home.js'
|
|
4
4
|
import { listSessions, getMessages } from '../sessions.js'
|
|
5
5
|
import { loadConfig } from '../config.js'
|
|
6
6
|
export async function dumpAll(outFile = null) {
|
|
7
|
-
const
|
|
7
|
+
const sessions = await listSessions(1000)
|
|
8
|
+
const enriched = await Promise.all(sessions.map(async s => ({ ...s, messages: await getMessages(s.id) })))
|
|
9
|
+
const out = { ts: Date.now(), freddie_home: getFreddieHome(), config: loadConfig(), sessions: enriched }
|
|
8
10
|
const json = JSON.stringify(out, null, 2)
|
|
9
11
|
if (outFile) { fs.mkdirSync(path.dirname(outFile), { recursive: true }); fs.writeFileSync(outFile, json, 'utf8'); return { written: outFile, bytes: json.length } }
|
|
10
12
|
return out
|
package/src/cli/env_loader.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
3
|
+
import { getFreddieHome } from '../home.js'
|
|
4
4
|
const RE = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/
|
|
5
5
|
function parse(text) {
|
|
6
6
|
const out = {}
|
|
@@ -13,7 +13,7 @@ function parse(text) {
|
|
|
13
13
|
return out
|
|
14
14
|
}
|
|
15
15
|
export function loadEnvFile(file = null) {
|
|
16
|
-
const candidates = file ? [file] : [path.join(
|
|
16
|
+
const candidates = file ? [file] : [path.join(getFreddieHome(), '.env'), path.join(process.cwd(), '.env')]
|
|
17
17
|
const merged = {}
|
|
18
18
|
for (const f of candidates) if (fs.existsSync(f)) Object.assign(merged, parse(fs.readFileSync(f, 'utf8')))
|
|
19
19
|
return merged
|
package/src/cli/gateway_cli.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { Gateway } from '../gateway/run.js'
|
|
2
|
-
import {
|
|
3
|
-
import { ApiServerAdapter } from '../gateway/platforms/api_server.js'
|
|
2
|
+
import { makePlatform } from '../gateway/platforms.js'
|
|
4
3
|
import { registerBuiltinHooks } from '../gateway/builtin_hooks/index.js'
|
|
5
4
|
let _gateway = null
|
|
6
5
|
export async function startGateway({ port = 0, hooks = true } = {}) {
|
|
7
6
|
if (_gateway) return _gateway
|
|
8
|
-
const wh =
|
|
9
|
-
const api =
|
|
7
|
+
const wh = await makePlatform('webhook', { port })
|
|
8
|
+
const api = await makePlatform('api_server', { port: 0 })
|
|
10
9
|
const gw = new Gateway({ platforms: { webhook: wh, api_server: api } })
|
|
11
10
|
if (hooks) registerBuiltinHooks(gw)
|
|
12
11
|
await gw.start()
|
package/src/cli/hooks.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
4
|
-
function file() { return path.join(
|
|
3
|
+
import { getFreddieHome } from '../home.js'
|
|
4
|
+
function file() { return path.join(getFreddieHome(), 'hooks.json') }
|
|
5
5
|
export function loadHooks() { try { return JSON.parse(fs.readFileSync(file(), 'utf8')) } catch { return { pre_command: [], post_command: [], pre_tool: [], post_tool: [] } } }
|
|
6
6
|
export function saveHooks(h) { fs.writeFileSync(file(), JSON.stringify(h, null, 2), 'utf8') }
|
|
7
7
|
export function addHook(stage, command) { const h = loadHooks(); (h[stage] = h[stage] || []).push(command); saveHooks(h); return h }
|
package/src/cli/logs.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
3
|
+
import { getFreddieHome } from '../home.js'
|
|
4
4
|
export function listLogFiles() {
|
|
5
|
-
const dir = path.join(
|
|
5
|
+
const dir = path.join(getFreddieHome(), 'logs')
|
|
6
6
|
if (!fs.existsSync(dir)) return []
|
|
7
7
|
return fs.readdirSync(dir).filter(f => f.endsWith('.log')).map(f => f.replace(/\.log$/, ''))
|
|
8
8
|
}
|
|
9
9
|
export function tail(subsystem, { max = 100, level = null } = {}) {
|
|
10
|
-
const file = path.join(
|
|
10
|
+
const file = path.join(getFreddieHome(), 'logs', subsystem + '.log')
|
|
11
11
|
if (!fs.existsSync(file)) return []
|
|
12
12
|
const lines = fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean)
|
|
13
13
|
let parsed = lines.map(l => { try { return JSON.parse(l) } catch { return { raw: l } } })
|
|
@@ -15,7 +15,7 @@ export function tail(subsystem, { max = 100, level = null } = {}) {
|
|
|
15
15
|
return parsed.slice(-max)
|
|
16
16
|
}
|
|
17
17
|
export async function followLog(subsystem, onLine) {
|
|
18
|
-
const file = path.join(
|
|
18
|
+
const file = path.join(getFreddieHome(), 'logs', subsystem + '.log')
|
|
19
19
|
let pos = fs.existsSync(file) ? fs.statSync(file).size : 0
|
|
20
20
|
const watcher = fs.watch(path.dirname(file), (_, name) => {
|
|
21
21
|
if (name !== subsystem + '.log') return
|
package/src/cli/mcp_config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
4
|
-
function file() { return path.join(
|
|
3
|
+
import { getFreddieHome } from '../home.js'
|
|
4
|
+
function file() { return path.join(getFreddieHome(), 'mcp.json') }
|
|
5
5
|
export function loadMcpConfig() { try { return JSON.parse(fs.readFileSync(file(), 'utf8')) } catch { return { servers: {} } } }
|
|
6
6
|
export function saveMcpConfig(cfg) { fs.writeFileSync(file(), JSON.stringify(cfg, null, 2), 'utf8') }
|
|
7
7
|
export function addServer(name, { command, args = [], env = {} }) { const c = loadMcpConfig(); c.servers[name] = { command, args, env }; saveMcpConfig(c); return c.servers[name] }
|
package/src/cli/plugins_cmd.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { listPluginsInstalled, listHooks, listCliCommands } from './plugins.js'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
-
import {
|
|
4
|
+
import { getFreddieHome } from '../home.js'
|
|
5
5
|
export async function pluginsSubcommand(action = 'list', { name, body } = {}) {
|
|
6
6
|
if (action === 'list') return { plugins: await listPluginsInstalled(), hooks: listHooks(), cliCommands: listCliCommands().length }
|
|
7
7
|
if (action === 'install') {
|
|
8
8
|
if (!name || !body) return { error: 'name + body required' }
|
|
9
|
-
const dir = path.join(
|
|
9
|
+
const dir = path.join(getFreddieHome(), 'plugins', name)
|
|
10
10
|
fs.mkdirSync(dir, { recursive: true })
|
|
11
11
|
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name, main: 'index.js' }, null, 2))
|
|
12
12
|
fs.writeFileSync(path.join(dir, 'index.js'), body, 'utf8')
|
|
13
13
|
return { installed: dir }
|
|
14
14
|
}
|
|
15
15
|
if (action === 'uninstall') {
|
|
16
|
-
const dir = path.join(
|
|
16
|
+
const dir = path.join(getFreddieHome(), 'plugins', name)
|
|
17
17
|
if (fs.existsSync(dir)) { fs.rmSync(dir, { recursive: true, force: true }); return { uninstalled: name } }
|
|
18
18
|
return { error: 'not found' }
|
|
19
19
|
}
|
package/src/cli/status.js
CHANGED
|
@@ -6,5 +6,5 @@ import { totalLifetime } from '../agent/account_usage.js'
|
|
|
6
6
|
import { runDoctor } from './doctor.js'
|
|
7
7
|
export async function systemStatus() {
|
|
8
8
|
const rt = await activeRuntime()
|
|
9
|
-
return { runtime: rt, profile: activeProfile(), skin: getActiveSkin().name, sessions: listSessions(5).length, lifetimeUsage: totalLifetime(), doctor: runDoctor().filter(c => !c.ok) }
|
|
9
|
+
return { runtime: rt, profile: activeProfile(), skin: getActiveSkin().name, sessions: (await listSessions(5)).length, lifetimeUsage: await totalLifetime(), doctor: runDoctor().filter(c => !c.ok) }
|
|
10
10
|
}
|
package/src/cli/tools_config.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
3
|
+
import { getFreddieHome } from '../home.js'
|
|
4
4
|
|
|
5
|
-
function file() { return path.join(
|
|
5
|
+
function file() { return path.join(getFreddieHome(), 'tools_config.json') }
|
|
6
6
|
export function loadToolsConfig() { try { return JSON.parse(fs.readFileSync(file(), 'utf8')) } catch { return {} } }
|
|
7
7
|
export function saveToolsConfig(cfg) { fs.writeFileSync(file(), JSON.stringify(cfg, null, 2), 'utf8') }
|
|
8
8
|
export function setToolOverride(toolName, override) {
|
package/src/cli/uninstall.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
|
-
import {
|
|
2
|
+
import { getFreddieHome } from '../home.js'
|
|
3
3
|
export function uninstall({ keepData = true } = {}) {
|
|
4
|
-
const home =
|
|
4
|
+
const home = getFreddieHome()
|
|
5
5
|
const removed = []
|
|
6
6
|
if (!keepData && fs.existsSync(home)) { fs.rmSync(home, { recursive: true, force: true }); removed.push(home) }
|
|
7
7
|
return { removed, keepData, hint: 'npm uninstall -g freddie (or remove your local checkout)' }
|
package/src/config.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
|
|
|
6
6
|
export const DEFAULT_CONFIG = {
|
|
7
7
|
_config_version: 1,
|
|
@@ -19,7 +19,7 @@ const MIGRATIONS = {
|
|
|
19
19
|
1: cfg => cfg,
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export function configPath() { return path.join(
|
|
22
|
+
export function configPath() { return path.join(getFreddieHome(), 'config.yaml') }
|
|
23
23
|
|
|
24
24
|
export function loadConfig() {
|
|
25
25
|
const p = configPath()
|
package/src/db.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import { createClient } from '@libsql/client'
|
|
4
|
-
import {
|
|
4
|
+
import { getFreddieHome } from './home.js'
|
|
5
5
|
|
|
6
6
|
let _db = null
|
|
7
7
|
let _dbPromise = null
|
|
8
|
-
const DB_PATH = () => path.join(
|
|
8
|
+
const DB_PATH = () => path.join(getFreddieHome(), 'state', 'sessions.db')
|
|
9
9
|
const USE_MEMORY_DB = () => process.env.FREDDIE_TEST_DB === 'memory'
|
|
10
10
|
|
|
11
11
|
export async function db() {
|
|
@@ -20,7 +20,7 @@ export async function db() {
|
|
|
20
20
|
// In-memory mode for tests: no file persistence
|
|
21
21
|
client = createClient({ url: 'file::memory:' })
|
|
22
22
|
} else {
|
|
23
|
-
const dir = path.join(
|
|
23
|
+
const dir = path.join(getFreddieHome(), 'state')
|
|
24
24
|
fs.mkdirSync(dir, { recursive: true })
|
|
25
25
|
dbPath = DB_PATH()
|
|
26
26
|
client = createClient({ url: `file:${dbPath}` })
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { bootHost } from '../host/index.js'
|
|
2
|
+
|
|
3
|
+
export async function getPlatformAdapter(name) {
|
|
4
|
+
const h = await bootHost()
|
|
5
|
+
const p = h.pi.platforms.get(name)
|
|
6
|
+
if (!p) throw new Error(`platform not registered: ${name}`)
|
|
7
|
+
const mod = p.module || {}
|
|
8
|
+
const cls = Object.values(mod).find(v => typeof v === 'function' && /Adapter$/.test(v.name)) || Object.values(mod).find(v => typeof v === 'function')
|
|
9
|
+
if (!cls) throw new Error(`platform ${name}: no adapter class exported`)
|
|
10
|
+
return cls
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function makePlatform(name, opts = {}) {
|
|
14
|
+
const Cls = await getPlatformAdapter(name)
|
|
15
|
+
return new Cls(opts)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function listPlatformNames() {
|
|
19
|
+
const h = await bootHost()
|
|
20
|
+
return h.pi.platforms.list().map(p => p.name)
|
|
21
|
+
}
|
package/src/home.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'node:fs'
|
|
|
4
4
|
|
|
5
5
|
let _cached = null
|
|
6
6
|
|
|
7
|
-
export function
|
|
7
|
+
export function getFreddieHome() {
|
|
8
8
|
if (_cached) return _cached
|
|
9
9
|
const env = process.env.FREDDIE_HOME
|
|
10
10
|
if (env) { _cached = env; ensure(env); return env }
|
|
@@ -16,7 +16,7 @@ export function getFophHome() {
|
|
|
16
16
|
return home
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function
|
|
19
|
+
export function displayFreddieHome() {
|
|
20
20
|
const profile = process.env.FREDDIE_PROFILE
|
|
21
21
|
return profile ? `~/.freddie/profiles/${profile}` : '~/.freddie'
|
|
22
22
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const SURFACES = ['pi', 'gui', 'both']
|
|
2
|
+
|
|
3
|
+
export const PI_VERBS = ['tool', 'env', 'command', 'cron', 'platform', 'memory', 'skill', 'context', 'agentExt', 'cli']
|
|
4
|
+
export const GUI_VERBS = ['route', 'page', 'nav', 'debug', 'api', 'asset']
|
|
5
|
+
|
|
6
|
+
export const HOOK_NAMES = [
|
|
7
|
+
'preToolCall', 'postToolCall',
|
|
8
|
+
'preLlmCall', 'postLlmCall',
|
|
9
|
+
'onSessionStart', 'onSessionEnd',
|
|
10
|
+
'onTurnStart', 'onTurnEnd',
|
|
11
|
+
'onMessageInbound', 'onMessageOutbound',
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
export function validatePlugin(p) {
|
|
15
|
+
if (!p || typeof p !== 'object') throw new Error('plugin: object required')
|
|
16
|
+
if (!p.name || typeof p.name !== 'string') throw new Error('plugin.name: string required')
|
|
17
|
+
if (!SURFACES.includes(p.surfaces)) throw new Error(`plugin ${p.name}: surfaces must be one of ${SURFACES.join(',')}`)
|
|
18
|
+
if (typeof p.register !== 'function') throw new Error(`plugin ${p.name}: register(ctx) function required`)
|
|
19
|
+
if (p.requires && !Array.isArray(p.requires)) throw new Error(`plugin ${p.name}: requires must be array`)
|
|
20
|
+
return p
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function topoSort(plugins) {
|
|
24
|
+
const byName = new Map(plugins.map(p => [p.name, p]))
|
|
25
|
+
const seen = new Map()
|
|
26
|
+
const out = []
|
|
27
|
+
const visit = (name, stack) => {
|
|
28
|
+
if (seen.get(name) === 'done') return
|
|
29
|
+
if (seen.get(name) === 'visiting') throw new Error(`plugin cycle: ${[...stack, name].join(' -> ')}`)
|
|
30
|
+
const p = byName.get(name)
|
|
31
|
+
if (!p) throw new Error(`plugin missing: ${name} (required by ${stack[stack.length - 1] || 'root'})`)
|
|
32
|
+
seen.set(name, 'visiting')
|
|
33
|
+
for (const dep of p.requires || []) visit(dep, [...stack, name])
|
|
34
|
+
seen.set(name, 'done')
|
|
35
|
+
out.push(p)
|
|
36
|
+
}
|
|
37
|
+
for (const p of plugins) visit(p.name, [])
|
|
38
|
+
return out
|
|
39
|
+
}
|
package/src/host/host.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
import { validatePlugin, topoSort, HOOK_NAMES, PI_VERBS, GUI_VERBS } from './contract.js'
|
|
5
|
+
|
|
6
|
+
function makePiSurface() {
|
|
7
|
+
const tools = new Map()
|
|
8
|
+
const envs = new Map()
|
|
9
|
+
const commands = new Map()
|
|
10
|
+
const crons = new Map()
|
|
11
|
+
const platforms = new Map()
|
|
12
|
+
const memory = new Map()
|
|
13
|
+
const skills = new Map()
|
|
14
|
+
const contexts = new Map()
|
|
15
|
+
const agentExts = new Map()
|
|
16
|
+
const cli = new Map()
|
|
17
|
+
return {
|
|
18
|
+
_state: { tools, envs, commands, crons, platforms, memory, skills, contexts, agentExts, cli },
|
|
19
|
+
tools: regOf(tools, 'tool'),
|
|
20
|
+
envs: regOf(envs, 'env'),
|
|
21
|
+
commands: regOf(commands, 'command'),
|
|
22
|
+
crons: regOf(crons, 'cron'),
|
|
23
|
+
platforms: regOf(platforms, 'platform'),
|
|
24
|
+
memory: regOf(memory, 'memory'),
|
|
25
|
+
skills: regOf(skills, 'skill'),
|
|
26
|
+
contexts: regOf(contexts, 'context'),
|
|
27
|
+
agentExts: regOf(agentExts, 'agentExt'),
|
|
28
|
+
cli: regOf(cli, 'cli'),
|
|
29
|
+
async dispatchTool(name, args = {}, ctx = {}) {
|
|
30
|
+
const t = tools.get(name)
|
|
31
|
+
if (!t) return JSON.stringify({ error: `unknown tool: ${name}` })
|
|
32
|
+
if (t.checkFn && t.checkFn(t) === false) return JSON.stringify({ error: `tool unavailable: ${name}`, requires: t.requiresEnv || [] })
|
|
33
|
+
try { const r = await t.handler(args, ctx); return typeof r === 'string' ? r : JSON.stringify(r) }
|
|
34
|
+
catch (e) { return JSON.stringify({ error: String(e?.message || e), tool: name }) }
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function regOf(map, kind) {
|
|
40
|
+
return {
|
|
41
|
+
register(spec) { if (!spec?.name) throw new Error(`${kind}.name required`); map.set(spec.name, spec) },
|
|
42
|
+
get(name) { return map.get(name) },
|
|
43
|
+
list() { return [...map.values()] },
|
|
44
|
+
has(name) { return map.has(name) },
|
|
45
|
+
size() { return map.size },
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function makeGuiSurface() {
|
|
50
|
+
const routes = []
|
|
51
|
+
const pages = new Map()
|
|
52
|
+
const nav = []
|
|
53
|
+
const debugs = new Map()
|
|
54
|
+
const apis = new Map()
|
|
55
|
+
const assets = new Map()
|
|
56
|
+
return {
|
|
57
|
+
_state: { routes, pages, nav, debugs, apis, assets },
|
|
58
|
+
route(method, path, handler) { routes.push({ method: method.toUpperCase(), path, handler }) },
|
|
59
|
+
page(slug, def) { pages.set(slug, def) },
|
|
60
|
+
nav(item) { nav.push(item) },
|
|
61
|
+
debug(name, snapshotFn) { debugs.set(name, snapshotFn) },
|
|
62
|
+
api(group, def) { apis.set(group, def) },
|
|
63
|
+
asset(p, content) { assets.set(p, content) },
|
|
64
|
+
routes: { list: () => routes },
|
|
65
|
+
pages: { get: (s) => pages.get(s), list: () => [...pages.values()], has: (s) => pages.has(s) },
|
|
66
|
+
navItems: { list: () => nav },
|
|
67
|
+
debugs: { list: () => [...debugs.entries()].map(([name, fn]) => ({ name, snapshot: fn })), get: (n) => debugs.get(n) },
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function makeHooks() {
|
|
72
|
+
const reg = Object.fromEntries(HOOK_NAMES.map(n => [n, []]))
|
|
73
|
+
return {
|
|
74
|
+
on(name, fn) {
|
|
75
|
+
if (!HOOK_NAMES.includes(name)) throw new Error(`unknown hook: ${name}`)
|
|
76
|
+
reg[name].push(fn)
|
|
77
|
+
},
|
|
78
|
+
async invoke(name, payload) {
|
|
79
|
+
let cur = payload
|
|
80
|
+
for (const fn of reg[name] || []) { cur = (await fn(cur)) ?? cur }
|
|
81
|
+
return cur
|
|
82
|
+
},
|
|
83
|
+
names() { return HOOK_NAMES },
|
|
84
|
+
listeners(name) { return [...(reg[name] || [])] },
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function guard(surfaceObj, allowed, pluginName, verbs) {
|
|
89
|
+
if (allowed) return surfaceObj
|
|
90
|
+
return new Proxy({}, {
|
|
91
|
+
get(_, key) {
|
|
92
|
+
if (verbs.includes(String(key))) {
|
|
93
|
+
return () => { throw new Error(`plugin ${pluginName}: surface verb '${String(key)}' not allowed (declared surfaces=${pluginName})`) }
|
|
94
|
+
}
|
|
95
|
+
return surfaceObj[key]
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function scopedConfig(name, store) {
|
|
101
|
+
const key = `plugins.${name}`
|
|
102
|
+
return {
|
|
103
|
+
get(k, d) { return store.get(`${key}.${k}`, d) },
|
|
104
|
+
set(k, v) { return store.set(`${key}.${k}`, v) },
|
|
105
|
+
all() { return store.all(key) || {} },
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function nullStore() {
|
|
110
|
+
const m = new Map()
|
|
111
|
+
return { get: (k, d) => m.has(k) ? m.get(k) : d, set: (k, v) => m.set(k, v), all: (prefix) => Object.fromEntries([...m.entries()].filter(([k]) => k.startsWith(prefix))) }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function createHost({ surfaces = ['pi', 'gui'], configStore = nullStore(), env = process.env } = {}) {
|
|
115
|
+
const pi = makePiSurface()
|
|
116
|
+
const gui = makeGuiSurface()
|
|
117
|
+
const hooks = makeHooks()
|
|
118
|
+
const loaded = []
|
|
119
|
+
const host = {
|
|
120
|
+
pi: surfaces.includes('pi') ? pi : null,
|
|
121
|
+
gui: surfaces.includes('gui') ? gui : null,
|
|
122
|
+
hooks,
|
|
123
|
+
plugins: () => loaded.map(p => ({ name: p.name, version: p.version || null, surfaces: p.surfaces, requires: p.requires || [] })),
|
|
124
|
+
get: (name) => loaded.find(p => p.name === name) || null,
|
|
125
|
+
}
|
|
126
|
+
async function loadAll(plugins) {
|
|
127
|
+
const validated = plugins.map(validatePlugin)
|
|
128
|
+
const sorted = topoSort(validated)
|
|
129
|
+
for (const p of sorted) {
|
|
130
|
+
const want = p.surfaces
|
|
131
|
+
const ctxPi = (want === 'pi' || want === 'both') && surfaces.includes('pi') ? pi : guard(pi, false, p.name, PI_VERBS)
|
|
132
|
+
const ctxGui = (want === 'gui' || want === 'both') && surfaces.includes('gui') ? gui : guard(gui, false, p.name, GUI_VERBS)
|
|
133
|
+
const log = (level, msg, fields) => { const line = JSON.stringify({ ts: Date.now(), plugin: p.name, level, msg, ...(fields || {}) }); if (env.FREDDIE_LOG_STDOUT) console.log(line) }
|
|
134
|
+
const logger = { info: (m, f) => log('info', m, f), warn: (m, f) => log('warn', m, f), error: (m, f) => log('error', m, f) }
|
|
135
|
+
const ctx = { pi: ctxPi, gui: ctxGui, hooks, log: logger, config: scopedConfig(p.name, configStore), host, env }
|
|
136
|
+
await p.register(ctx)
|
|
137
|
+
loaded.push(p)
|
|
138
|
+
}
|
|
139
|
+
return loaded.length
|
|
140
|
+
}
|
|
141
|
+
host.load = loadAll
|
|
142
|
+
return host
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function discoverPlugins(roots) {
|
|
146
|
+
const found = []
|
|
147
|
+
for (const root of roots) {
|
|
148
|
+
if (!root || !fs.existsSync(root)) continue
|
|
149
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
150
|
+
if (!entry.isDirectory()) continue
|
|
151
|
+
const file = path.join(root, entry.name, 'plugin.js')
|
|
152
|
+
if (!fs.existsSync(file)) continue
|
|
153
|
+
const mod = await import(pathToFileURL(file).href)
|
|
154
|
+
const p = mod.default || mod.plugin
|
|
155
|
+
if (p) found.push(p)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return found
|
|
159
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { createHost, discoverPlugins } from './host.js'
|
|
4
|
+
import { getFreddieHome } from '../home.js'
|
|
5
|
+
|
|
6
|
+
let _host = null
|
|
7
|
+
let _loaded = false
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const REPO_PLUGINS = path.resolve(__dirname, '..', '..', 'plugins')
|
|
11
|
+
|
|
12
|
+
export function host() {
|
|
13
|
+
if (!_host) _host = createHost({ surfaces: ['pi', 'gui'] })
|
|
14
|
+
return _host
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function bootHost(extraRoots = []) {
|
|
18
|
+
const h = host()
|
|
19
|
+
if (_loaded) return h
|
|
20
|
+
_loaded = true
|
|
21
|
+
const roots = [REPO_PLUGINS, path.join(getFreddieHome(), 'plugins'), path.join(process.cwd(), '.freddie', 'plugins'), ...extraRoots]
|
|
22
|
+
const plugins = await discoverPlugins(roots)
|
|
23
|
+
await h.load(plugins)
|
|
24
|
+
return h
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resetHostForTests() { _host = null; _loaded = false }
|
package/src/index.js
CHANGED
|
@@ -2,7 +2,8 @@ export * from './home.js'
|
|
|
2
2
|
export * from './config.js'
|
|
3
3
|
export * from './sessions.js'
|
|
4
4
|
export * from './toolsets.js'
|
|
5
|
-
export {
|
|
5
|
+
export { host, bootHost, resetHostForTests } from './host/index.js'
|
|
6
|
+
export { createHost, discoverPlugins } from './host/host.js'
|
|
6
7
|
export { runTurn, createAgentMachine } from './agent/machine.js'
|
|
7
8
|
export { Gateway, bootMdHook } from './gateway/run.js'
|
|
8
9
|
export { AcpServer } from './acp/server.js'
|
package/src/mcp/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import readline from 'node:readline'
|
|
2
|
-
import {
|
|
2
|
+
import { bootHost } from '../host/index.js'
|
|
3
3
|
import { listSkills } from '../skills/index.js'
|
|
4
4
|
import { logger } from '../observability/log.js'
|
|
5
5
|
|
|
@@ -34,11 +34,12 @@ const METHODS = {
|
|
|
34
34
|
serverInfo: { name: 'freddie-mcp', version: '0.4.0' },
|
|
35
35
|
}),
|
|
36
36
|
'tools/list': async () => {
|
|
37
|
-
await
|
|
38
|
-
return { tools:
|
|
37
|
+
const h = await bootHost()
|
|
38
|
+
return { tools: h.pi.tools.list().map(t => ({ name: t.name, description: t.schema?.description, inputSchema: t.schema?.parameters || {} })) }
|
|
39
39
|
},
|
|
40
40
|
'tools/call': async ({ name, arguments: args = {} }) => {
|
|
41
|
-
const
|
|
41
|
+
const h = await bootHost()
|
|
42
|
+
const out = await h.pi.dispatchTool(name, args)
|
|
42
43
|
return { content: [{ type: 'text', text: typeof out === 'string' ? out : JSON.stringify(out) }] }
|
|
43
44
|
},
|
|
44
45
|
'prompts/list': async () => {
|
package/src/observability/log.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
3
|
+
import { getFreddieHome } from '../home.js'
|
|
4
4
|
|
|
5
5
|
const SEVERITIES = { debug: 10, info: 20, warning: 30, error: 40 }
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ let _streams = new Map()
|
|
|
8
8
|
|
|
9
9
|
function streamFor(name) {
|
|
10
10
|
if (_streams.has(name)) return _streams.get(name)
|
|
11
|
-
const dir = path.join(
|
|
11
|
+
const dir = path.join(getFreddieHome(), 'logs')
|
|
12
12
|
fs.mkdirSync(dir, { recursive: true })
|
|
13
13
|
const s = fs.createWriteStream(path.join(dir, `${name}.log`), { flags: 'a' })
|
|
14
14
|
_streams.set(name, s)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
3
|
+
import { getFreddieHome } from '../../home.js'
|
|
4
4
|
|
|
5
5
|
const MAX_AGE_DAYS = { logs: 30, batches: 14, 'tool-results': 7, checkpoints: 90 }
|
|
6
6
|
export function cleanup({ now = Date.now() } = {}) {
|
|
7
7
|
const removed = []
|
|
8
8
|
for (const [sub, days] of Object.entries(MAX_AGE_DAYS)) {
|
|
9
|
-
const dir = path.join(
|
|
9
|
+
const dir = path.join(getFreddieHome(), sub)
|
|
10
10
|
if (!fs.existsSync(dir)) continue
|
|
11
11
|
const cutoff = now - days * 86400_000
|
|
12
12
|
for (const f of fs.readdirSync(dir)) {
|
package/src/plugins/manager.js
CHANGED
|
@@ -1,66 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const HOOK_NAMES = ['preToolCall', 'postToolCall', 'preLlmCall', 'postLlmCall', 'onSessionStart', 'onSessionEnd']
|
|
10
|
-
|
|
11
|
-
export class PluginManager {
|
|
12
|
-
constructor() {
|
|
13
|
-
this.plugins = []
|
|
14
|
-
this.hooks = Object.fromEntries(HOOK_NAMES.map(n => [n, []]))
|
|
15
|
-
this.cliCommands = []
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async discoverPlugins(extraDirs = []) {
|
|
19
|
-
const dirs = [path.join(getFophHome(), 'plugins'), path.join(process.cwd(), '.freddie', 'plugins'), ...extraDirs]
|
|
20
|
-
for (const d of dirs) {
|
|
21
|
-
if (!fs.existsSync(d)) continue
|
|
22
|
-
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
23
|
-
if (!entry.isDirectory()) continue
|
|
24
|
-
const pkg = path.join(d, entry.name, 'package.json')
|
|
25
|
-
if (!fs.existsSync(pkg)) continue
|
|
26
|
-
try { await this._loadPlugin(path.join(d, entry.name)) } catch (e) { log.error('plugin load failed', { name: entry.name, err: String(e) }) }
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return this.plugins.length
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async _loadPlugin(dir) {
|
|
33
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
|
|
34
|
-
const main = path.join(dir, pkg.main || 'index.js')
|
|
35
|
-
const mod = await import(`file://${main.replace(/\\/g, '/')}`)
|
|
36
|
-
const ctx = this._ctx()
|
|
37
|
-
if (typeof mod.register === 'function') await mod.register(ctx)
|
|
38
|
-
this.plugins.push({ name: pkg.name, dir, main })
|
|
39
|
-
log.info('plugin loaded', { name: pkg.name })
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
register(plugin) {
|
|
43
|
-
const ctx = this._ctx()
|
|
44
|
-
plugin.register?.(ctx)
|
|
45
|
-
this.plugins.push({ name: plugin.name || 'inline', ...plugin })
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
_ctx() {
|
|
49
|
-
const self = this
|
|
50
|
-
return {
|
|
51
|
-
registerHook: (name, fn) => { if (!HOOK_NAMES.includes(name)) throw new Error(`unknown hook: ${name}`); self.hooks[name].push(fn) },
|
|
52
|
-
registerTool: (spec) => toolRegistry.register(spec),
|
|
53
|
-
registerCliCommand: (def) => self.cliCommands.push(def),
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async invoke(hook, payload) {
|
|
58
|
-
let cur = payload
|
|
59
|
-
for (const fn of this.hooks[hook] || []) {
|
|
60
|
-
try { cur = (await fn(cur)) || cur } catch (e) { log.error('hook failed', { hook, err: String(e) }) }
|
|
61
|
-
}
|
|
62
|
-
return cur
|
|
1
|
+
import { bootHost, host } from '../host/index.js'
|
|
2
|
+
|
|
3
|
+
class PluginManagerShim {
|
|
4
|
+
async discoverPlugins() { await bootHost(); return host().plugins().length }
|
|
5
|
+
get plugins() { return host().plugins() }
|
|
6
|
+
get hooks() {
|
|
7
|
+
const h = host().hooks
|
|
8
|
+
return Object.fromEntries(h.names().map(n => [n, h.listeners(n)]))
|
|
63
9
|
}
|
|
10
|
+
get cliCommands() { return host().pi?.commands.list() || [] }
|
|
11
|
+
register() { throw new Error('legacy register() removed; use plugins/<name>/plugin.js with the new contract') }
|
|
12
|
+
async invoke(name, payload) { return host().hooks.invoke(name, payload) }
|
|
64
13
|
}
|
|
65
14
|
|
|
66
|
-
export const
|
|
15
|
+
export const PluginManager = PluginManagerShim
|
|
16
|
+
export const pluginManager = new PluginManagerShim()
|