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
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 { getFophHome } from '../home.js'
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 out = { ts: Date.now(), foph_home: getFophHome(), config: loadConfig(), sessions: listSessions(1000).map(s => ({ ...s, messages: getMessages(s.id) })) }
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
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import { getFophHome } from '../home.js'
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(getFophHome(), '.env'), path.join(process.cwd(), '.env')]
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
@@ -1,12 +1,11 @@
1
1
  import { Gateway } from '../gateway/run.js'
2
- import { WebhookAdapter } from '../gateway/platforms/webhook.js'
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 = new WebhookAdapter({ port })
9
- const api = new ApiServerAdapter({ port: 0 })
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 { getFophHome } from '../home.js'
4
- function file() { return path.join(getFophHome(), 'hooks.json') }
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 { getFophHome } from '../home.js'
3
+ import { getFreddieHome } from '../home.js'
4
4
  export function listLogFiles() {
5
- const dir = path.join(getFophHome(), 'logs')
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(getFophHome(), 'logs', subsystem + '.log')
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(getFophHome(), 'logs', subsystem + '.log')
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
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import { getFophHome } from '../home.js'
4
- function file() { return path.join(getFophHome(), 'mcp.json') }
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] }
@@ -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 { getFophHome } from '../home.js'
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(getFophHome(), 'plugins', name)
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(getFophHome(), 'plugins', name)
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
  }
@@ -1,8 +1,8 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import { getFophHome } from '../home.js'
3
+ import { getFreddieHome } from '../home.js'
4
4
 
5
- function file() { return path.join(getFophHome(), 'tools_config.json') }
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) {
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs'
2
- import { getFophHome } from '../home.js'
2
+ import { getFreddieHome } from '../home.js'
3
3
  export function uninstall({ keepData = true } = {}) {
4
- const home = getFophHome()
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 { getFophHome } from './home.js'
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(getFophHome(), 'config.yaml') }
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 { getFophHome } from './home.js'
4
+ import { getFreddieHome } from './home.js'
5
5
 
6
6
  let _db = null
7
7
  let _dbPromise = null
8
- const DB_PATH = () => path.join(getFophHome(), 'state', 'sessions.db')
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(getFophHome(), 'state')
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 getFophHome() {
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 displayFophHome() {
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
+ }
@@ -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 { registry, discoverBuiltinTools } from './tools/registry.js'
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 { registry, discoverBuiltinTools } from '../tools/registry.js'
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 discoverBuiltinTools()
38
- return { tools: registry.list().map(t => ({ name: t.name, description: t.schema.description, inputSchema: t.schema.parameters || {} })) }
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 out = await registry.dispatch(name, args)
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 () => {
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import { getFophHome } from '../home.js'
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(getFophHome(), 'logs')
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 { getFophHome } from '../../home.js'
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(getFophHome(), sub)
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)) {
@@ -1,66 +1,16 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import { getFophHome } from '../home.js'
4
- import { registry as toolRegistry } from '../tools/registry.js'
5
- import { logger } from '../observability/log.js'
6
-
7
- const log = logger('plugins')
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 pluginManager = new PluginManager()
15
+ export const PluginManager = PluginManagerShim
16
+ export const pluginManager = new PluginManagerShim()