freddie 0.0.41 → 0.0.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/AGENTS.md +85 -11
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +2 -2
  4. package/bin/freddie.js +12 -109
  5. package/package.json +11 -2
  6. package/src/acp/server.js +3 -3
  7. package/src/acp/session.js +8 -8
  8. package/src/acp/tools.js +5 -4
  9. package/src/agent/account_usage.js +5 -5
  10. package/src/agent/credential_sources.js +2 -2
  11. package/src/agent/curator.js +5 -5
  12. package/src/agent/machine.js +3 -2
  13. package/src/agent/manual_compression_feedback.js +5 -5
  14. package/src/agent/shell_hooks.js +2 -2
  15. package/src/auth.js +2 -2
  16. package/src/batch.js +2 -2
  17. package/src/cli/backup.js +3 -3
  18. package/src/cli/doctor.js +3 -3
  19. package/src/cli/dump.js +4 -2
  20. package/src/cli/env_loader.js +2 -2
  21. package/src/cli/gateway_cli.js +3 -4
  22. package/src/cli/hooks.js +2 -2
  23. package/src/cli/logs.js +4 -4
  24. package/src/cli/mcp_config.js +2 -2
  25. package/src/cli/plugins_cmd.js +3 -3
  26. package/src/cli/status.js +1 -1
  27. package/src/cli/tools_config.js +2 -2
  28. package/src/cli/uninstall.js +2 -2
  29. package/src/config.js +2 -2
  30. package/src/db.js +3 -3
  31. package/src/gateway/platforms.js +21 -0
  32. package/src/home.js +2 -2
  33. package/src/host/contract.js +39 -0
  34. package/src/host/host.js +159 -0
  35. package/src/host/index.js +27 -0
  36. package/src/index.js +2 -1
  37. package/src/mcp/server.js +5 -4
  38. package/src/observability/log.js +2 -2
  39. package/src/plugins/disk_cleanup/index.js +2 -2
  40. package/src/plugins/manager.js +13 -63
  41. package/src/plugins/memory/provider.js +26 -26
  42. package/src/plugins/observability/index.js +3 -3
  43. package/src/skills/index.js +2 -2
  44. package/src/skin/engine.js +2 -2
  45. package/src/toolsets.js +13 -15
  46. package/src/web/index.html +1 -1
  47. package/src/web/server.js +8 -94
  48. package/src/gateway/platforms/api_server.js +0 -21
  49. package/src/gateway/platforms/bluebubbles.js +0 -32
  50. package/src/gateway/platforms/dingtalk.js +0 -32
  51. package/src/gateway/platforms/discord.js +0 -24
  52. package/src/gateway/platforms/email.js +0 -51
  53. package/src/gateway/platforms/feishu.js +0 -32
  54. package/src/gateway/platforms/feishu_comment.js +0 -12
  55. package/src/gateway/platforms/feishu_comment_rules.js +0 -11
  56. package/src/gateway/platforms/homeassistant.js +0 -32
  57. package/src/gateway/platforms/matrix.js +0 -40
  58. package/src/gateway/platforms/mattermost.js +0 -29
  59. package/src/gateway/platforms/qqbot.js +0 -32
  60. package/src/gateway/platforms/signal.js +0 -33
  61. package/src/gateway/platforms/slack.js +0 -34
  62. package/src/gateway/platforms/sms.js +0 -34
  63. package/src/gateway/platforms/telegram.js +0 -38
  64. package/src/gateway/platforms/telegram_network.js +0 -17
  65. package/src/gateway/platforms/webhook.js +0 -19
  66. package/src/gateway/platforms/wecom.js +0 -32
  67. package/src/gateway/platforms/wecom_callback.js +0 -15
  68. package/src/gateway/platforms/wecom_crypto.js +0 -16
  69. package/src/gateway/platforms/weixin.js +0 -32
  70. package/src/gateway/platforms/whatsapp.js +0 -40
  71. package/src/gateway/platforms/yuanbao.js +0 -9
  72. package/src/gateway/platforms/yuanbao_media.js +0 -5
  73. package/src/gateway/platforms/yuanbao_proto.js +0 -9
  74. package/src/gateway/platforms/yuanbao_sticker.js +0 -6
  75. package/src/plugins/memory/_index.js +0 -8
  76. package/src/plugins/memory/byterover.js +0 -25
  77. package/src/plugins/memory/hindsight.js +0 -25
  78. package/src/plugins/memory/holographic.js +0 -31
  79. package/src/plugins/memory/honcho.js +0 -25
  80. package/src/plugins/memory/mem0.js +0 -25
  81. package/src/plugins/memory/openviking.js +0 -25
  82. package/src/plugins/memory/retaindb.js +0 -25
  83. package/src/plugins/memory/supermemory.js +0 -25
  84. package/src/tools/ansi_strip.js +0 -8
  85. package/src/tools/approval.js +0 -15
  86. package/src/tools/bash.js +0 -35
  87. package/src/tools/binary_extensions.js +0 -22
  88. package/src/tools/browser.js +0 -48
  89. package/src/tools/budget_config.js +0 -13
  90. package/src/tools/checkpoint.js +0 -29
  91. package/src/tools/clarify.js +0 -15
  92. package/src/tools/code_execution.js +0 -27
  93. package/src/tools/credential_files.js +0 -16
  94. package/src/tools/cronjob.js +0 -16
  95. package/src/tools/debug_helpers.js +0 -9
  96. package/src/tools/delegate.js +0 -28
  97. package/src/tools/discord_tool.js +0 -13
  98. package/src/tools/edit.js +0 -31
  99. package/src/tools/env_passthrough.js +0 -15
  100. package/src/tools/feishu_doc.js +0 -15
  101. package/src/tools/feishu_drive.js +0 -14
  102. package/src/tools/file_operations.js +0 -17
  103. package/src/tools/file_state.js +0 -16
  104. package/src/tools/file_tools.js +0 -23
  105. package/src/tools/fuzzy_match.js +0 -8
  106. package/src/tools/grep.js +0 -51
  107. package/src/tools/homeassistant_tool.js +0 -15
  108. package/src/tools/image_gen.js +0 -33
  109. package/src/tools/interrupt.js +0 -18
  110. package/src/tools/managed_tool_gateway.js +0 -11
  111. package/src/tools/mcp_oauth.js +0 -21
  112. package/src/tools/mcp_oauth_manager.js +0 -20
  113. package/src/tools/mcp_tool.js +0 -36
  114. package/src/tools/memory.js +0 -66
  115. package/src/tools/mixture_of_agents.js +0 -14
  116. package/src/tools/neutts_synth.js +0 -13
  117. package/src/tools/openrouter_client.js +0 -13
  118. package/src/tools/osv_check.js +0 -11
  119. package/src/tools/patch_parser.js +0 -42
  120. package/src/tools/path_security.js +0 -16
  121. package/src/tools/process_registry.js +0 -17
  122. package/src/tools/read.js +0 -26
  123. package/src/tools/registry.js +0 -54
  124. package/src/tools/rl_training.js +0 -13
  125. package/src/tools/schema_sanitizer.js +0 -18
  126. package/src/tools/send_message.js +0 -32
  127. package/src/tools/session_search.js +0 -23
  128. package/src/tools/skill_manager.js +0 -17
  129. package/src/tools/skill_usage.js +0 -20
  130. package/src/tools/skills_guard.js +0 -17
  131. package/src/tools/skills_hub.js +0 -31
  132. package/src/tools/skills_index.js +0 -14
  133. package/src/tools/skills_sync.js +0 -19
  134. package/src/tools/skills_tool.js +0 -11
  135. package/src/tools/slash_confirm.js +0 -16
  136. package/src/tools/terminal.js +0 -29
  137. package/src/tools/tirith_security.js +0 -25
  138. package/src/tools/todo.js +0 -54
  139. package/src/tools/tool_backend_helpers.js +0 -26
  140. package/src/tools/tool_output_limits.js +0 -15
  141. package/src/tools/tool_result_storage.js +0 -20
  142. package/src/tools/transcription.js +0 -19
  143. package/src/tools/tts.js +0 -19
  144. package/src/tools/url_safety.js +0 -15
  145. package/src/tools/vision.js +0 -18
  146. package/src/tools/voice_mode.js +0 -10
  147. package/src/tools/web_search.js +0 -37
  148. package/src/tools/web_tools.js +0 -18
  149. package/src/tools/website_policy.js +0 -14
  150. package/src/tools/write.js +0 -25
  151. package/src/tools/xai_http.js +0 -13
  152. package/src/tools/yuanbao_tools.js +0 -13
@@ -1,20 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import { getFophHome } from '../home.js'
4
- import { registry } from './registry.js'
5
-
6
- function tokFile(server) { return path.join(getFophHome(), 'mcp-tokens', encodeURIComponent(server) + '.json') }
7
-
8
- registry.register({
9
- name: 'mcp_oauth_manager',
10
- toolset: 'core',
11
- schema: { name: 'mcp_oauth_manager', description: 'Persist & retrieve MCP OAuth tokens. Actions: store, get, list, delete, refresh.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['store', 'get', 'list', 'delete'] }, server: { type: 'string' }, token: {} }, required: ['action'] } },
12
- handler: async ({ action, server, token }) => {
13
- const dir = path.join(getFophHome(), 'mcp-tokens'); fs.mkdirSync(dir, { recursive: true })
14
- if (action === 'store') { fs.writeFileSync(tokFile(server), JSON.stringify({ server, token, ts: Date.now() }), { mode: 0o600 }); return { stored: server } }
15
- if (action === 'get') { const f = tokFile(server); return fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, 'utf8')) : { error: 'not found' } }
16
- if (action === 'list') return { servers: fs.readdirSync(dir).filter(f => f.endsWith('.json')).map(f => decodeURIComponent(f.replace(/\.json$/, ''))) }
17
- if (action === 'delete') { const f = tokFile(server); if (fs.existsSync(f)) fs.unlinkSync(f); return { deleted: server } }
18
- return { error: 'unknown action' }
19
- },
20
- })
@@ -1,36 +0,0 @@
1
- import { spawn } from 'node:child_process'
2
- import { registry } from './registry.js'
3
-
4
- const _clients = new Map()
5
-
6
- registry.register({
7
- name: 'mcp_tool',
8
- toolset: 'core',
9
- schema: { name: 'mcp_tool', description: 'Connect to an external MCP server (stdio) and call its tools.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['connect', 'list', 'call', 'disconnect'] }, id: { type: 'string' }, command: { type: 'string' }, args: { type: 'array' }, name: { type: 'string' }, arguments: {} }, required: ['action'] } },
10
- handler: async ({ action, id, command, args = [], name, arguments: callArgs = {} }) => {
11
- if (action === 'connect') {
12
- const cid = id || 'mcp-' + Date.now()
13
- const child = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] })
14
- _clients.set(cid, { child, nextId: 1, pending: new Map(), buf: '' })
15
- const c = _clients.get(cid)
16
- child.stdout.on('data', d => {
17
- c.buf += d.toString()
18
- const lines = c.buf.split('\n'); c.buf = lines.pop()
19
- for (const l of lines) { try { const m = JSON.parse(l); const p = c.pending.get(m.id); if (p) { c.pending.delete(m.id); p.resolve(m) } } catch {} }
20
- })
21
- return { id: cid, connected: true }
22
- }
23
- const c = _clients.get(id)
24
- if (!c) return { error: 'unknown id' }
25
- const rpc = (method, params) => new Promise((resolve, reject) => {
26
- const rid = c.nextId++
27
- c.pending.set(rid, { resolve, reject })
28
- c.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: rid, method, params }) + '\n')
29
- setTimeout(() => { if (c.pending.has(rid)) { c.pending.delete(rid); reject(new Error('mcp timeout')) } }, 30000)
30
- })
31
- if (action === 'list') return await rpc('tools/list', {})
32
- if (action === 'call') return await rpc('tools/call', { name, arguments: callArgs })
33
- if (action === 'disconnect') { try { c.child.kill('SIGTERM') } catch {} _clients.delete(id); return { disconnected: id } }
34
- return { error: 'unknown action' }
35
- },
36
- })
@@ -1,66 +0,0 @@
1
- import { db } from '../db.js'
2
- import { registry } from './registry.js'
3
- import { getConfigValue } from '../config.js'
4
- import { createMemoryProvider } from '../plugins/memory/provider.js'
5
-
6
- async function init() {
7
- const d = await db()
8
- d.exec(`CREATE TABLE IF NOT EXISTS memory_local (id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, ts INTEGER NOT NULL)`)
9
- if (!d._fts5_unavailable) {
10
- try { d.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS memory_local_fts USING fts5(content, content='memory_local', content_rowid='id')`) } catch (e) { d._fts5_unavailable = true }
11
- try { d.exec(`CREATE TRIGGER IF NOT EXISTS memory_local_ai AFTER INSERT ON memory_local BEGIN INSERT INTO memory_local_fts(rowid, content) VALUES (new.id, new.content); END`) } catch (e) { d._fts5_unavailable = true }
12
- }
13
- return d
14
- }
15
-
16
- function provider() {
17
- const name = getConfigValue('memory.provider')
18
- if (!name) return null
19
- try { return createMemoryProvider(name, {}) } catch { return null }
20
- }
21
-
22
- const ACTIONS = {
23
- add: async ({ content }) => {
24
- if (!content) return { error: 'content required' }
25
- const p = provider()
26
- if (p) { await p.syncTurn([{ role: 'note', content }]); return { stored: 'remote' } }
27
- const d = await init()
28
- const info = d.prepare(`INSERT INTO memory_local (content, ts) VALUES (?, ?)`).run(content, Date.now())
29
- return { id: info.lastInsertRowid, stored: 'local' }
30
- },
31
- search: async ({ query, limit = 10 }) => {
32
- const p = provider()
33
- if (p) return await p.prefetch(query)
34
- const d = await init()
35
- if (d._fts5_unavailable) {
36
- const rows = d.prepare(`SELECT id, content, ts FROM memory_local WHERE content LIKE ? ORDER BY ts DESC LIMIT ?`).all('%' + query + '%', limit)
37
- return { items: rows }
38
- }
39
- try {
40
- const rows = d.prepare(`SELECT m.id, m.content, m.ts FROM memory_local_fts f JOIN memory_local m ON m.id = f.rowid WHERE memory_local_fts MATCH ? ORDER BY rank LIMIT ?`).all(query, limit)
41
- return { items: rows }
42
- } catch {
43
- const rows = d.prepare(`SELECT id, content, ts FROM memory_local WHERE content LIKE ? ORDER BY ts DESC LIMIT ?`).all('%' + query + '%', limit)
44
- return { items: rows }
45
- }
46
- },
47
- list: async () => {
48
- const d = await init()
49
- return { items: d.prepare(`SELECT id, content, ts FROM memory_local ORDER BY id DESC LIMIT 50`).all() }
50
- },
51
- }
52
-
53
- registry.register({
54
- name: 'memory',
55
- toolset: 'core',
56
- schema: {
57
- name: 'memory',
58
- description: 'Add/search/list memory. Routes to configured provider or local SQLite fallback.',
59
- parameters: { type: 'object', properties: { action: { type: 'string', enum: Object.keys(ACTIONS) }, content: { type: 'string' }, query: { type: 'string' }, limit: { type: 'number' } }, required: ['action'] },
60
- },
61
- handler: async (args) => {
62
- const fn = ACTIONS[args.action]
63
- if (!fn) return { error: 'unknown action: ' + args.action }
64
- return await fn(args)
65
- },
66
- })
@@ -1,14 +0,0 @@
1
- import { registry } from './registry.js'
2
- import { runTurn } from '../agent/machine.js'
3
-
4
- registry.register({
5
- name: 'mixture_of_agents',
6
- toolset: 'core',
7
- schema: { name: 'mixture_of_agents', description: 'Run the same prompt through N sub-agents (different models or seeds), then synthesize the results. Reduces variance.', parameters: { type: 'object', properties: { prompt: { type: 'string' }, models: { type: 'array', items: { type: 'string' } }, callLLM: {} }, required: ['prompt'] } },
8
- handler: async ({ prompt, models = ['default'] }, ctx = {}) => {
9
- const llm = ctx.callLLM || null
10
- const runs = await Promise.all(models.map(m => runTurn({ prompt, model: m, callLLM: llm, timeoutMs: 30000 }).catch(e => ({ error: String(e.message || e) }))))
11
- const synthesized = runs.map(r => r.result || r.error || '').join('\n---\n')
12
- return { runs: runs.length, synthesized }
13
- },
14
- })
@@ -1,13 +0,0 @@
1
- import { registry } from './registry.js'
2
- registry.register({
3
- name: 'neutts_synth',
4
- toolset: 'creative',
5
- schema: { name: 'neutts_synth', description: 'Local NeuTTS synth (alternate TTS backend).', parameters: { type: 'object', properties: { text: { type: 'string' }, voice: { type: 'string', default: 'default' } }, required: ['text'] } },
6
- requiresEnv: ['NEUTTS_URL'],
7
- checkFn: () => Boolean(process.env.NEUTTS_URL),
8
- handler: async ({ text, voice = 'default' }) => {
9
- if (!process.env.NEUTTS_URL) return { error: 'NEUTTS_URL required' }
10
- const r = await fetch(process.env.NEUTTS_URL + '/synthesize', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text, voice }) })
11
- return { status: r.status, bytes: (await r.arrayBuffer()).byteLength }
12
- },
13
- })
@@ -1,13 +0,0 @@
1
- import { registry } from './registry.js'
2
- registry.register({
3
- name: 'openrouter',
4
- toolset: 'core',
5
- schema: { name: 'openrouter', description: 'Chat completion via OpenRouter (any model).', parameters: { type: 'object', properties: { prompt: { type: 'string' }, model: { type: 'string', default: 'anthropic/claude-sonnet-4' } }, required: ['prompt'] } },
6
- requiresEnv: ['OPENROUTER_API_KEY'],
7
- checkFn: () => Boolean(process.env.OPENROUTER_API_KEY),
8
- handler: async ({ prompt, model = 'anthropic/claude-sonnet-4' }) => {
9
- if (!process.env.OPENROUTER_API_KEY) return { error: 'OPENROUTER_API_KEY required' }
10
- const r = await fetch('https://openrouter.ai/api/v1/chat/completions', { method: 'POST', headers: { authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ model, messages: [{ role: 'user', content: prompt }] }) })
11
- return await r.json()
12
- },
13
- })
@@ -1,11 +0,0 @@
1
- import { registry } from './registry.js'
2
- registry.register({
3
- name: 'osv_check',
4
- toolset: 'core',
5
- schema: { name: 'osv_check', description: 'Query osv.dev for known vulnerabilities. Pass either {package, version, ecosystem} or {commit_sha, repo}.', parameters: { type: 'object', properties: { package: { type: 'string' }, version: { type: 'string' }, ecosystem: { type: 'string' }, commit_sha: { type: 'string' } } } },
6
- handler: async (args) => {
7
- const body = args.commit_sha ? { commit: args.commit_sha } : { package: { name: args.package, ecosystem: args.ecosystem || 'npm' }, version: args.version }
8
- const r = await fetch('https://api.osv.dev/v1/query', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
9
- return await r.json()
10
- },
11
- })
@@ -1,42 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import { registry } from './registry.js'
4
-
5
- export function applyUnifiedDiff(diff, { cwd = process.cwd() } = {}) {
6
- const lines = diff.split('\n')
7
- const results = []
8
- let curFile = null, curHunks = [], curHunk = null
9
- const flush = () => {
10
- if (!curFile) return
11
- const file = path.join(cwd, curFile)
12
- if (!fs.existsSync(file)) { results.push({ file: curFile, error: 'not found' }); curFile = null; curHunks = []; return }
13
- let src = fs.readFileSync(file, 'utf8').split('\n')
14
- for (const h of curHunks) {
15
- const before = src.slice(0, h.start)
16
- const after = src.slice(h.start + h.removed)
17
- src = [...before, ...h.added, ...after]
18
- }
19
- fs.writeFileSync(file, src.join('\n'), 'utf8')
20
- results.push({ file: curFile, applied: curHunks.length })
21
- curFile = null; curHunks = []
22
- }
23
- for (const l of lines) {
24
- if (l.startsWith('--- ')) { flush(); curFile = l.slice(6).trim() }
25
- else if (l.startsWith('+++ ')) {}
26
- else if (l.startsWith('@@ ')) {
27
- const m = l.match(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/)
28
- if (m) { curHunk = { start: Number(m[1]) - 1, removed: Number(m[2]), added: [] }; curHunks.push(curHunk) }
29
- } else if (curHunk) {
30
- if (l.startsWith('+')) curHunk.added.push(l.slice(1))
31
- else if (l.startsWith(' ')) curHunk.added.push(l.slice(1))
32
- }
33
- }
34
- flush()
35
- return results
36
- }
37
- registry.register({
38
- name: 'patch_parser',
39
- toolset: 'core',
40
- schema: { name: 'patch_parser', description: 'Apply a unified diff to files in cwd. Returns per-file results.', parameters: { type: 'object', properties: { diff: { type: 'string' }, cwd: { type: 'string' } }, required: ['diff'] } },
41
- handler: async ({ diff, cwd }) => ({ results: applyUnifiedDiff(diff, { cwd }) }),
42
- })
@@ -1,16 +0,0 @@
1
- import path from 'node:path'
2
- import { registry } from './registry.js'
3
-
4
- const FORBIDDEN = ['/etc/passwd', '/etc/shadow', '/.ssh/', '/.aws/', 'C:\\Windows\\System32']
5
- export function isPathSafe(p, { cwd = process.cwd() } = {}) {
6
- const abs = path.resolve(cwd, p)
7
- for (const bad of FORBIDDEN) if (abs.includes(bad)) return { safe: false, reason: 'forbidden: ' + bad }
8
- if (abs.includes('..')) return { safe: false, reason: 'parent reference in resolved path' }
9
- return { safe: true, abs }
10
- }
11
- registry.register({
12
- name: 'path_security',
13
- toolset: 'core',
14
- schema: { name: 'path_security', description: 'Check whether a path is allowed (no /etc/passwd, no .ssh/, etc).', parameters: { type: 'object', properties: { path: { type: 'string' }, cwd: { type: 'string' } }, required: ['path'] } },
15
- handler: async ({ path: p, cwd }) => isPathSafe(p, { cwd }),
16
- })
@@ -1,17 +0,0 @@
1
- import { registry } from './registry.js'
2
-
3
- const _processes = new Map()
4
- export function registerProcess(id, child, meta = {}) { _processes.set(id, { id, pid: child.pid, started: Date.now(), ...meta }); child.on?.('exit', () => _processes.delete(id)) }
5
- export function listProcesses() { return [..._processes.values()] }
6
- export function killProcess(id) { const p = _processes.get(id); if (p) try { process.kill(p.pid, 'SIGTERM') } catch {} return p ? { killed: id } : { error: 'unknown id' } }
7
-
8
- registry.register({
9
- name: 'process_registry',
10
- toolset: 'core',
11
- schema: { name: 'process_registry', description: 'List/kill spawned background processes tracked by freddie.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'kill'] }, id: { type: 'string' } }, required: ['action'] } },
12
- handler: async ({ action, id }) => {
13
- if (action === 'list') return { processes: listProcesses() }
14
- if (action === 'kill') return killProcess(id)
15
- return { error: 'unknown action' }
16
- },
17
- })
package/src/tools/read.js DELETED
@@ -1,26 +0,0 @@
1
- import fs from 'node:fs'
2
- import { registry } from './registry.js'
3
-
4
- registry.register({
5
- name: 'read',
6
- toolset: 'core',
7
- schema: {
8
- name: 'read',
9
- description: 'Read a file from disk. Returns lines with line numbers.',
10
- parameters: {
11
- type: 'object',
12
- properties: {
13
- path: { type: 'string' },
14
- offset: { type: 'number', default: 0 },
15
- limit: { type: 'number', default: 2000 },
16
- },
17
- required: ['path'],
18
- },
19
- },
20
- handler: async ({ path: p, offset = 0, limit = 2000 }) => {
21
- if (!fs.existsSync(p)) return { error: `not found: ${p}` }
22
- const lines = fs.readFileSync(p, 'utf8').split('\n')
23
- const slice = lines.slice(offset, offset + limit)
24
- return { path: p, total: lines.length, content: slice.map((l, i) => `${(offset + i + 1).toString().padStart(6)}\t${l}`).join('\n') }
25
- },
26
- })
@@ -1,54 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
-
5
- const _tools = new Map()
6
- let _discovered = false
7
-
8
- export const registry = {
9
- register(spec) {
10
- if (!spec?.name) throw new Error('tool name required')
11
- _tools.set(spec.name, {
12
- name: spec.name,
13
- toolset: spec.toolset || 'core',
14
- schema: spec.schema || { name: spec.name, description: '', parameters: { type: 'object', properties: {} } },
15
- handler: spec.handler,
16
- checkFn: spec.checkFn || (() => true),
17
- requiresEnv: spec.requiresEnv || [],
18
- })
19
- },
20
- list() { return [..._tools.values()] },
21
- get(name) { return _tools.get(name) },
22
- available() { return [..._tools.values()].filter(t => safeCheck(t)) },
23
- schemas(tools) {
24
- const src = tools || this.available()
25
- return src.map(t => t.schema)
26
- },
27
- async dispatch(name, args = {}, ctx = {}) {
28
- const t = _tools.get(name)
29
- if (!t) return JSON.stringify({ error: `unknown tool: ${name}` })
30
- if (!safeCheck(t)) return JSON.stringify({ error: `tool unavailable: ${name}`, requires: t.requiresEnv })
31
- try {
32
- const result = await t.handler(args, ctx)
33
- return typeof result === 'string' ? result : JSON.stringify(result)
34
- } catch (e) {
35
- return JSON.stringify({ error: String(e?.message || e), tool: name })
36
- }
37
- },
38
- clearForTests() { _tools.clear(); _discovered = false },
39
- }
40
-
41
- function safeCheck(t) {
42
- try { return t.checkFn(t) !== false } catch { return false }
43
- }
44
-
45
- export async function discoverBuiltinTools() {
46
- if (_discovered) return
47
- _discovered = true
48
- const here = path.dirname(fileURLToPath(import.meta.url))
49
- const entries = fs.readdirSync(here).filter(f => f.endsWith('.js') && f !== 'registry.js' && f !== 'index.js')
50
- for (const f of entries) {
51
- const url = new URL(`./${f}`, import.meta.url).href
52
- try { await import(url) } catch (e) { console.error(`tool load failed ${f}: ${e.message}`) }
53
- }
54
- }
@@ -1,13 +0,0 @@
1
- import { registry } from './registry.js'
2
- registry.register({
3
- name: 'rl_training',
4
- toolset: 'core',
5
- schema: { name: 'rl_training', description: 'Kick off an RL rollout (Atropos integration).', parameters: { type: 'object', properties: { task: { type: 'string' }, model: { type: 'string' } }, required: ['task'] } },
6
- requiresEnv: ['ATROPOS_URL'],
7
- checkFn: () => Boolean(process.env.ATROPOS_URL),
8
- handler: async ({ task, model }) => {
9
- if (!process.env.ATROPOS_URL) return { error: 'ATROPOS_URL required' }
10
- const r = await fetch(process.env.ATROPOS_URL + '/rollouts', { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${process.env.ATROPOS_TOKEN || ''}` }, body: JSON.stringify({ task, model }) })
11
- return await r.json()
12
- },
13
- })
@@ -1,18 +0,0 @@
1
- import { registry } from './registry.js'
2
- const DANGEROUS_PARAM_NAMES = new Set(['eval', 'exec', '__proto__', 'constructor'])
3
- export function sanitizeSchema(schema) {
4
- if (!schema || typeof schema !== 'object') return schema
5
- const out = JSON.parse(JSON.stringify(schema))
6
- if (out.parameters?.properties) {
7
- for (const k of Object.keys(out.parameters.properties)) {
8
- if (DANGEROUS_PARAM_NAMES.has(k)) delete out.parameters.properties[k]
9
- }
10
- }
11
- return out
12
- }
13
- registry.register({
14
- name: 'schema_sanitizer',
15
- toolset: 'core',
16
- schema: { name: 'schema_sanitizer', description: 'Strip dangerous fields (eval, exec, __proto__) from a tool schema.', parameters: { type: 'object', properties: { schema: {} }, required: ['schema'] } },
17
- handler: async ({ schema }) => ({ sanitized: sanitizeSchema(schema) }),
18
- })
@@ -1,32 +0,0 @@
1
- import { registry } from './registry.js'
2
-
3
- const PLATFORM_MODULES = {
4
- telegram: '../gateway/platforms/telegram.js',
5
- discord: '../gateway/platforms/discord.js',
6
- slack: '../gateway/platforms/slack.js',
7
- whatsapp: '../gateway/platforms/whatsapp.js',
8
- email: '../gateway/platforms/email.js',
9
- sms: '../gateway/platforms/sms.js',
10
- matrix: '../gateway/platforms/matrix.js',
11
- signal: '../gateway/platforms/signal.js',
12
- mattermost: '../gateway/platforms/mattermost.js',
13
- }
14
-
15
- registry.register({
16
- name: 'send_message',
17
- toolset: 'core',
18
- schema: { name: 'send_message', description: 'Send a message to a recipient on the named platform. Uses the gateway adapter; requires the platform credentials.', parameters: { type: 'object', properties: { platform: { type: 'string', enum: Object.keys(PLATFORM_MODULES) }, to: { type: 'string' }, text: { type: 'string' } }, required: ['platform', 'to', 'text'] } },
19
- handler: async ({ platform, to, text }) => {
20
- const mod = PLATFORM_MODULES[platform]
21
- if (!mod) return { error: 'unknown platform: ' + platform }
22
- const m = await import(mod)
23
- const cls = Object.values(m)[0]
24
- const inst = new cls({})
25
- try { await inst.start() } catch (e) { return { error: String(e.message || e) } }
26
- try {
27
- const out = await inst.send({ to, text })
28
- await inst.stop?.()
29
- return { ok: true, response: out }
30
- } catch (e) { await inst.stop?.(); return { error: String(e.message || e) } }
31
- },
32
- })
@@ -1,23 +0,0 @@
1
- import { search, listSessions, getMessages } from '../sessions.js'
2
- import { registry } from './registry.js'
3
-
4
- registry.register({
5
- name: 'session_search',
6
- toolset: 'core',
7
- schema: { name: 'session_search', description: 'Full-text search across past session messages. Returns hits with session_id and content snippet.', parameters: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number', default: 20 }, session_id: { type: 'string' } }, required: ['query'] } },
8
- handler: async ({ query, limit = 20, session_id = null }) => {
9
- if (session_id) {
10
- const msgs = getMessages(session_id)
11
- const q = String(query).toLowerCase()
12
- return { items: msgs.filter(m => String(m.content || '').toLowerCase().includes(q)).slice(0, limit) }
13
- }
14
- return { items: search(query, limit) }
15
- },
16
- })
17
-
18
- registry.register({
19
- name: 'session_list',
20
- toolset: 'core',
21
- schema: { name: 'session_list', description: 'List recent sessions.', parameters: { type: 'object', properties: { limit: { type: 'number', default: 20 } } } },
22
- handler: async ({ limit = 20 }) => ({ sessions: listSessions(limit) }),
23
- })
@@ -1,17 +0,0 @@
1
- import { registry } from './registry.js'
2
- import { listSkills, findSkill, skillAsUserMessage } from '../skills/index.js'
3
-
4
- const ACTIONS = {
5
- list: () => ({ skills: listSkills().map(s => ({ name: s.name, description: s.description, file: s.file })) }),
6
- get: ({ name }) => { const s = findSkill(name); return s ? { skill: s } : { error: 'not found: ' + name } },
7
- invoke: ({ name, args = '' }) => {
8
- const m = skillAsUserMessage(name, args)
9
- return m ? { message: m } : { error: 'not found: ' + name }
10
- },
11
- }
12
- registry.register({
13
- name: 'skill_manager',
14
- toolset: 'core',
15
- schema: { name: 'skill_manager', description: 'List, fetch, or invoke a skill from ~/.freddie/skills/ or bundled skills/.', parameters: { type: 'object', properties: { action: { type: 'string', enum: Object.keys(ACTIONS) }, name: { type: 'string' }, args: { type: 'string' } }, required: ['action'] } },
16
- handler: async (a) => { const fn = ACTIONS[a.action]; return fn ? fn(a) : { error: 'unknown action' } },
17
- })
@@ -1,20 +0,0 @@
1
- import { db } from '../db.js'
2
- import { registry } from './registry.js'
3
-
4
- async function init() {
5
- const d = await db()
6
- d.exec(`CREATE TABLE IF NOT EXISTS skill_usage (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, ts INTEGER NOT NULL, session_id TEXT)`)
7
- return d
8
- }
9
- registry.register({
10
- name: 'skill_usage',
11
- toolset: 'core',
12
- schema: { name: 'skill_usage', description: 'Track / query skill invocation stats.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['record', 'top', 'recent'] }, name: { type: 'string' }, session_id: { type: 'string' }, limit: { type: 'number', default: 20 } }, required: ['action'] } },
13
- handler: async ({ action, name, session_id = null, limit = 20 }) => {
14
- const d = await init()
15
- if (action === 'record') { d.prepare(`INSERT INTO skill_usage (name, ts, session_id) VALUES (?, ?, ?)`).run(name, Date.now(), session_id); return { recorded: true } }
16
- if (action === 'top') return { top: d.prepare(`SELECT name, COUNT(*) AS uses FROM skill_usage GROUP BY name ORDER BY uses DESC LIMIT ?`).all(limit) }
17
- if (action === 'recent') return { recent: d.prepare(`SELECT name, ts, session_id FROM skill_usage ORDER BY id DESC LIMIT ?`).all(limit) }
18
- return { error: 'unknown action' }
19
- },
20
- })
@@ -1,17 +0,0 @@
1
- import { registry } from './registry.js'
2
- import { detectSecrets } from '../agent/redact.js'
3
-
4
- const DANGEROUS = [/rm\s+-rf\s+\//, /:\(\)\s*\{\s*:\|:&\s*\};:/, /chmod\s+-R\s+777/]
5
-
6
- registry.register({
7
- name: 'skills_guard',
8
- toolset: 'core',
9
- schema: { name: 'skills_guard', description: 'Inspect skill body for dangerous patterns (rm -rf /, fork bombs, secrets) before injection.', parameters: { type: 'object', properties: { body: { type: 'string' } }, required: ['body'] } },
10
- handler: async ({ body }) => {
11
- const issues = []
12
- for (const re of DANGEROUS) if (re.test(body)) issues.push({ kind: 'dangerous-cmd', pattern: re.source })
13
- const secrets = detectSecrets(body)
14
- if (secrets.length) issues.push({ kind: 'secret', count: secrets.length })
15
- return { safe: issues.length === 0, issues }
16
- },
17
- })
@@ -1,31 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import { getFophHome } from '../home.js'
4
- import { registry } from './registry.js'
5
-
6
- const HUB_INDEX_URL = 'https://raw.githubusercontent.com/AnEntrypoint/freddie-skills/main/index.json'
7
-
8
- const ACTIONS = {
9
- catalog: async () => {
10
- try { const r = await fetch(HUB_INDEX_URL); if (!r.ok) return { items: [], error: 'fetch ' + r.status }; return { items: await r.json() } }
11
- catch (e) { return { items: [], error: String(e.message || e) } }
12
- },
13
- install: async ({ name, body }) => {
14
- if (!name || !body) return { error: 'name + body required' }
15
- const dir = path.join(getFophHome(), 'skills', name)
16
- fs.mkdirSync(dir, { recursive: true })
17
- fs.writeFileSync(path.join(dir, 'SKILL.md'), body, 'utf8')
18
- return { installed: dir }
19
- },
20
- uninstall: async ({ name }) => {
21
- const dir = path.join(getFophHome(), 'skills', name)
22
- if (fs.existsSync(dir)) { fs.rmSync(dir, { recursive: true, force: true }); return { uninstalled: name } }
23
- return { error: 'not found' }
24
- },
25
- }
26
- registry.register({
27
- name: 'skills_hub',
28
- toolset: 'core',
29
- schema: { name: 'skills_hub', description: 'Browse and install community skills.', parameters: { type: 'object', properties: { action: { type: 'string', enum: Object.keys(ACTIONS) }, name: { type: 'string' }, body: { type: 'string' } }, required: ['action'] } },
30
- handler: async (a) => { const fn = ACTIONS[a.action]; return fn ? await fn(a) : { error: 'unknown action' } },
31
- })
@@ -1,14 +0,0 @@
1
- import { listSkills } from '../skills/index.js'
2
- import { registry } from './registry.js'
3
-
4
- registry.register({
5
- name: 'skills_index',
6
- toolset: 'core',
7
- schema: { name: 'skills_index', description: 'Build a search index of available skills (name + description + first-line of body) for the agent to query.', parameters: { type: 'object', properties: { query: { type: 'string' } } } },
8
- handler: async ({ query }) => {
9
- const all = listSkills().map(s => ({ name: s.name, description: s.description, hint: (s.body || '').split('\n').find(l => l.trim()) || '' }))
10
- if (!query) return { items: all }
11
- const q = String(query).toLowerCase()
12
- return { items: all.filter(s => (s.name + ' ' + s.description + ' ' + s.hint).toLowerCase().includes(q)) }
13
- },
14
- })
@@ -1,19 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import { getFophHome } from '../home.js'
4
- import { registry } from './registry.js'
5
-
6
- registry.register({
7
- name: 'skills_sync',
8
- toolset: 'core',
9
- schema: { name: 'skills_sync', description: 'Sync ~/.freddie/skills/ with a remote git repo (clone or pull).', parameters: { type: 'object', properties: { repo: { type: 'string' } }, required: ['repo'] } },
10
- handler: async ({ repo }) => {
11
- const dir = path.join(getFophHome(), 'skills')
12
- fs.mkdirSync(dir, { recursive: true })
13
- const { spawnSync } = await import('node:child_process')
14
- const exists = fs.existsSync(path.join(dir, '.git'))
15
- const cmd = exists ? ['git', '-C', dir, 'pull'] : ['git', 'clone', repo, dir]
16
- const r = spawnSync(cmd[0], cmd.slice(1), { encoding: 'utf8' })
17
- return { exitCode: r.status, stdout: r.stdout, stderr: r.stderr }
18
- },
19
- })
@@ -1,11 +0,0 @@
1
- import { listSkills, findSkill, skillAsUserMessage } from '../skills/index.js'
2
- import { registry } from './registry.js'
3
-
4
- registry.register({
5
- name: 'skill',
6
- toolset: 'core',
7
- schema: { name: 'skill', description: 'Run a skill by name. Returns the user-message representation that should be added to the conversation.', parameters: { type: 'object', properties: { name: { type: 'string' }, args: { type: 'string' } }, required: ['name'] } },
8
- handler: async ({ name, args = '' }) => {
9
- const m = skillAsUserMessage(name, args); return m ? { message: m } : { error: 'skill not found: ' + name, available: listSkills().map(s => s.name) }
10
- },
11
- })
@@ -1,16 +0,0 @@
1
- import { resolveCommand, getCommand } from '../commands/registry.js'
2
- import { registry } from './registry.js'
3
-
4
- const DESTRUCTIVE = new Set(['reset', 'clear', 'delete'])
5
-
6
- registry.register({
7
- name: 'slash_confirm',
8
- toolset: 'core',
9
- schema: { name: 'slash_confirm', description: 'Resolve a slash command and indicate whether it requires confirmation before running.', parameters: { type: 'object', properties: { input: { type: 'string' } }, required: ['input'] } },
10
- handler: async ({ input }) => {
11
- const name = resolveCommand(input)
12
- if (!name) return { recognised: false, input }
13
- const def = getCommand(name)
14
- return { recognised: true, name, requiresConfirm: DESTRUCTIVE.has(name), description: def?.description }
15
- },
16
- })
@@ -1,29 +0,0 @@
1
- import { spawn } from 'node:child_process'
2
- import { registry } from './registry.js'
3
-
4
- const _sessions = new Map()
5
-
6
- registry.register({
7
- name: 'terminal',
8
- toolset: 'core',
9
- schema: { name: 'terminal', description: 'Open a long-lived shell session, send input lines, capture output. Actions: open, send, read, close.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['open', 'send', 'read', 'close', 'list'] }, id: { type: 'string' }, input: { type: 'string' }, cwd: { type: 'string' } }, required: ['action'] } },
10
- handler: async ({ action, id, input, cwd }) => {
11
- if (action === 'open') {
12
- const sid = 'term-' + Date.now()
13
- const sh = process.platform === 'win32' ? 'cmd' : 'sh'
14
- const child = spawn(sh, [], { cwd: cwd || process.cwd(), env: process.env })
15
- const buf = { stdout: '', stderr: '' }
16
- child.stdout?.on('data', d => buf.stdout += d.toString())
17
- child.stderr?.on('data', d => buf.stderr += d.toString())
18
- _sessions.set(sid, { child, buf })
19
- return { id: sid, opened: true }
20
- }
21
- const s = _sessions.get(id)
22
- if (!s) return { error: 'unknown terminal id: ' + id }
23
- if (action === 'send') { s.child.stdin?.write(input + '\n'); return { sent: true } }
24
- if (action === 'read') { const out = { ...s.buf }; s.buf.stdout = ''; s.buf.stderr = ''; return out }
25
- if (action === 'close') { try { s.child.kill('SIGTERM') } catch {} _sessions.delete(id); return { closed: id } }
26
- if (action === 'list') return { sessions: [..._sessions.keys()] }
27
- return { error: 'unknown action' }
28
- },
29
- })