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,25 +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 policyPath() { return path.join(getFophHome(), 'policy.json') }
7
- function loadPolicy() { try { return JSON.parse(fs.readFileSync(policyPath(), 'utf8')) } catch { return { tools: {}, hosts: { allow: [], deny: [] } } } }
8
-
9
- registry.register({
10
- name: 'tirith_security',
11
- toolset: 'core',
12
- schema: { name: 'tirith_security', description: 'Evaluate a candidate action against ~/.freddie/policy.json. Returns allow|deny|ask.', parameters: { type: 'object', properties: { kind: { type: 'string' }, target: { type: 'string' } }, required: ['kind', 'target'] } },
13
- handler: async ({ kind, target }) => {
14
- const p = loadPolicy()
15
- if (kind === 'tool') {
16
- const t = p.tools?.[target]
17
- if (t === 'allow' || t === 'deny') return { decision: t }
18
- }
19
- if (kind === 'host') {
20
- if (p.hosts?.deny?.some(d => target.includes(d))) return { decision: 'deny' }
21
- if (p.hosts?.allow?.some(d => target.includes(d))) return { decision: 'allow' }
22
- }
23
- return { decision: 'ask' }
24
- },
25
- })
package/src/tools/todo.js DELETED
@@ -1,54 +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 todos (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, content TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created INTEGER NOT NULL, updated INTEGER NOT NULL)`)
7
- return d
8
- }
9
-
10
- const ACTIONS = {
11
- add: async ({ session_id = null, content }) => {
12
- if (!content) return { error: 'content required' }
13
- const d = await init(); const now = Date.now()
14
- const info = d.prepare(`INSERT INTO todos (session_id, content, status, created, updated) VALUES (?, ?, 'pending', ?, ?)`).run(session_id, content, now, now)
15
- return { id: info.lastInsertRowid, content, status: 'pending' }
16
- },
17
- list: async ({ session_id = null }) => {
18
- const d = await init()
19
- const rows = session_id ? d.prepare(`SELECT * FROM todos WHERE session_id = ? ORDER BY id DESC`).all(session_id) : d.prepare(`SELECT * FROM todos ORDER BY id DESC`).all()
20
- return { todos: rows }
21
- },
22
- update: async ({ id, status }) => {
23
- if (!id) return { error: 'id required' }
24
- (await init()).prepare(`UPDATE todos SET status = ?, updated = ? WHERE id = ?`).run(status, Date.now(), id)
25
- return { id, status }
26
- },
27
- complete: async ({ id }) => ACTIONS.update({ id, status: 'completed' }),
28
- delete: async ({ id }) => { (await init()).prepare(`DELETE FROM todos WHERE id = ?`).run(id); return { id, deleted: true } },
29
- }
30
-
31
- registry.register({
32
- name: 'todo',
33
- toolset: 'core',
34
- schema: {
35
- name: 'todo',
36
- description: 'Manage per-session todos. Actions: add, list, update, complete, delete.',
37
- parameters: {
38
- type: 'object',
39
- properties: {
40
- action: { type: 'string', enum: Object.keys(ACTIONS) },
41
- content: { type: 'string' },
42
- id: { type: 'number' },
43
- status: { type: 'string' },
44
- session_id: { type: 'string' },
45
- },
46
- required: ['action'],
47
- },
48
- },
49
- handler: async (args) => {
50
- const fn = ACTIONS[args.action]
51
- if (!fn) return { error: 'unknown action: ' + args.action }
52
- return fn(args)
53
- },
54
- })
@@ -1,26 +0,0 @@
1
- import { registry } from './registry.js'
2
-
3
- export function shapeArgs(schema, args) {
4
- if (!schema?.properties) return args
5
- const out = {}
6
- for (const [k, def] of Object.entries(schema.properties)) {
7
- if (k in args) out[k] = args[k]
8
- else if ('default' in def) out[k] = def.default
9
- }
10
- return out
11
- }
12
- export function describeTools(filter = null) {
13
- let list = registry.list()
14
- if (filter) list = list.filter(t => t.toolset === filter)
15
- return list.map(t => ({ name: t.name, description: t.schema.description, toolset: t.toolset }))
16
- }
17
- registry.register({
18
- name: 'tool_backend_helpers',
19
- toolset: 'core',
20
- schema: { name: 'tool_backend_helpers', description: 'Helper meta-tool: describeTools(filter), shapeArgs(schema, args).', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['describe', 'shape'] }, filter: { type: 'string' }, schema: {}, args: {} }, required: ['action'] } },
21
- handler: async ({ action, filter, schema, args }) => {
22
- if (action === 'describe') return { tools: describeTools(filter) }
23
- if (action === 'shape') return { args: shapeArgs(schema, args || {}) }
24
- return { error: 'unknown action' }
25
- },
26
- })
@@ -1,15 +0,0 @@
1
- import { registry } from './registry.js'
2
- import { getConfigValue } from '../config.js'
3
-
4
- export function truncate(s, max = null) {
5
- const limit = max ?? getConfigValue('tool.output_limit', 100_000)
6
- const t = String(s)
7
- if (t.length <= limit) return t
8
- return t.slice(0, limit) + `\n…[truncated ${t.length - limit} chars]`
9
- }
10
- registry.register({
11
- name: 'tool_output_limits',
12
- toolset: 'core',
13
- schema: { name: 'tool_output_limits', description: 'Apply the configured output truncation cap to a string.', parameters: { type: 'object', properties: { text: { type: 'string' }, max: { type: 'number' } }, required: ['text'] } },
14
- handler: async ({ text, max }) => ({ text: truncate(text, max) }),
15
- })
@@ -1,20 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import crypto from 'node:crypto'
4
- import { getFophHome } from '../home.js'
5
- import { registry } from './registry.js'
6
-
7
- function dir() { const d = path.join(getFophHome(), 'tool-results'); fs.mkdirSync(d, { recursive: true }); return d }
8
-
9
- registry.register({
10
- name: 'tool_result_storage',
11
- toolset: 'core',
12
- schema: { name: 'tool_result_storage', description: 'Persist a large tool result to disk; return reference token. Actions: store, fetch, list, delete.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['store', 'fetch', 'list', 'delete'] }, content: { type: 'string' }, token: { type: 'string' } }, required: ['action'] } },
13
- handler: async ({ action, content, token }) => {
14
- if (action === 'store') { const t = crypto.randomBytes(8).toString('hex'); fs.writeFileSync(path.join(dir(), t + '.txt'), content || '', 'utf8'); return { token: t, bytes: (content || '').length } }
15
- if (action === 'fetch') { const f = path.join(dir(), token + '.txt'); return fs.existsSync(f) ? { content: fs.readFileSync(f, 'utf8') } : { error: 'not found' } }
16
- if (action === 'list') return { tokens: fs.readdirSync(dir()).filter(f => f.endsWith('.txt')).map(f => f.replace(/\.txt$/, '')) }
17
- if (action === 'delete') { const f = path.join(dir(), token + '.txt'); if (fs.existsSync(f)) fs.unlinkSync(f); return { deleted: token } }
18
- return { error: 'unknown action' }
19
- },
20
- })
@@ -1,19 +0,0 @@
1
- import fs from 'node:fs'
2
- import { registry } from './registry.js'
3
- registry.register({
4
- name: 'transcription',
5
- toolset: 'creative',
6
- schema: { name: 'transcription', description: 'Transcribe audio with OpenAI Whisper.', parameters: { type: 'object', properties: { file_path: { type: 'string' }, model: { type: 'string', default: 'whisper-1' } }, required: ['file_path'] } },
7
- requiresEnv: ['OPENAI_API_KEY'],
8
- checkFn: () => Boolean(process.env.OPENAI_API_KEY),
9
- handler: async ({ file_path, model = 'whisper-1' }) => {
10
- if (!process.env.OPENAI_API_KEY) return { error: 'OPENAI_API_KEY required' }
11
- if (!fs.existsSync(file_path)) return { error: 'file not found: ' + file_path }
12
- const blob = new Blob([fs.readFileSync(file_path)])
13
- const fd = new FormData()
14
- fd.append('file', blob, file_path.split(/[\\/]/).pop())
15
- fd.append('model', model)
16
- const r = await fetch('https://api.openai.com/v1/audio/transcriptions', { method: 'POST', headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY}` }, body: fd })
17
- return await r.json()
18
- },
19
- })
package/src/tools/tts.js DELETED
@@ -1,19 +0,0 @@
1
- import { registry } from './registry.js'
2
- registry.register({
3
- name: 'tts',
4
- toolset: 'creative',
5
- schema: { name: 'tts', description: 'Synthesize speech (OpenAI tts-1 or ElevenLabs).', parameters: { type: 'object', properties: { text: { type: 'string' }, provider: { type: 'string', enum: ['openai', 'elevenlabs'], default: 'openai' }, voice: { type: 'string' } }, required: ['text'] } },
6
- requiresEnv: ['OPENAI_API_KEY or ELEVENLABS_API_KEY'],
7
- checkFn: () => Boolean(process.env.OPENAI_API_KEY || process.env.ELEVENLABS_API_KEY),
8
- handler: async ({ text, provider = 'openai', voice = 'alloy' }) => {
9
- if (provider === 'elevenlabs') {
10
- if (!process.env.ELEVENLABS_API_KEY) return { error: 'ELEVENLABS_API_KEY required' }
11
- const v = voice || '21m00Tcm4TlvDq8ikWAM'
12
- const r = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${v}`, { method: 'POST', headers: { 'xi-api-key': process.env.ELEVENLABS_API_KEY, 'content-type': 'application/json' }, body: JSON.stringify({ text }) })
13
- return { status: r.status, contentType: r.headers.get('content-type'), bytes: (await r.arrayBuffer()).byteLength }
14
- }
15
- if (!process.env.OPENAI_API_KEY) return { error: 'OPENAI_API_KEY required' }
16
- const r = await fetch('https://api.openai.com/v1/audio/speech', { method: 'POST', headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ model: 'tts-1', input: text, voice }) })
17
- return { status: r.status, bytes: (await r.arrayBuffer()).byteLength }
18
- },
19
- })
@@ -1,15 +0,0 @@
1
- import { registry } from './registry.js'
2
- const SUSPICIOUS = ['phish', 'malware', '.onion']
3
- const PRIVATE_RANGES = [/^10\./, /^192\.168\./, /^172\.(1[6-9]|2\d|3[01])\./, /^127\./, /^0\./, /^169\.254\./]
4
- registry.register({
5
- name: 'url_safety',
6
- toolset: 'core',
7
- schema: { name: 'url_safety', description: 'Heuristic URL safety check (private IPs, known-bad TLDs, scheme).', parameters: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] } },
8
- handler: async ({ url }) => {
9
- let u; try { u = new URL(url) } catch { return { safe: false, reason: 'invalid URL' } }
10
- if (!['http:', 'https:'].includes(u.protocol)) return { safe: false, reason: 'unsupported scheme: ' + u.protocol }
11
- if (PRIVATE_RANGES.some(re => re.test(u.hostname))) return { safe: false, reason: 'private IP host' }
12
- for (const s of SUSPICIOUS) if (u.hostname.includes(s)) return { safe: false, reason: 'suspicious token: ' + s }
13
- return { safe: true, host: u.hostname }
14
- },
15
- })
@@ -1,18 +0,0 @@
1
- import { registry } from './registry.js'
2
- registry.register({
3
- name: 'vision',
4
- toolset: 'creative',
5
- schema: { name: 'vision', description: 'Describe an image (URL or base64) using a vision-capable LLM.', parameters: { type: 'object', properties: { image_url: { type: 'string' }, prompt: { type: 'string', default: 'Describe this image.' }, provider: { type: 'string', enum: ['openai', 'anthropic'], default: 'openai' } }, required: ['image_url'] } },
6
- requiresEnv: ['OPENAI_API_KEY or ANTHROPIC_API_KEY'],
7
- checkFn: () => Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY),
8
- handler: async ({ image_url, prompt = 'Describe this image.', provider = 'openai' }) => {
9
- if (provider === 'anthropic') {
10
- if (!process.env.ANTHROPIC_API_KEY) return { error: 'ANTHROPIC_API_KEY required' }
11
- const r = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'x-api-key': process.env.ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' }, body: JSON.stringify({ model: 'claude-sonnet-4-6', max_tokens: 1024, messages: [{ role: 'user', content: [{ type: 'image', source: { type: 'url', url: image_url } }, { type: 'text', text: prompt }] }] }) })
12
- return await r.json()
13
- }
14
- if (!process.env.OPENAI_API_KEY) return { error: 'OPENAI_API_KEY required' }
15
- const r = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: [{ type: 'text', text: prompt }, { type: 'image_url', image_url: { url: image_url } }] }] }) })
16
- return await r.json()
17
- },
18
- })
@@ -1,10 +0,0 @@
1
- import { registry } from './registry.js'
2
- registry.register({
3
- name: 'voice_mode',
4
- toolset: 'creative',
5
- schema: { name: 'voice_mode', description: 'Toggle full-duplex voice (transcription in + tts out) for the active session. Returns the new state.', parameters: { type: 'object', properties: { enabled: { type: 'boolean' } } } },
6
- handler: async ({ enabled }, ctx = {}) => {
7
- if (typeof ctx.setVoiceMode === 'function') return await ctx.setVoiceMode(Boolean(enabled))
8
- return { enabled: Boolean(enabled), note: 'voice mode toggled in-process; bind ctx.setVoiceMode to wire to a real audio loop' }
9
- },
10
- })
@@ -1,37 +0,0 @@
1
- import { registry } from './registry.js'
2
-
3
- registry.register({
4
- name: 'web_search',
5
- toolset: 'browse',
6
- schema: {
7
- name: 'web_search',
8
- description: 'Search the web (DuckDuckGo HTML or SerpAPI). Returns title/url/snippet list.',
9
- parameters: {
10
- type: 'object',
11
- properties: {
12
- query: { type: 'string' },
13
- limit: { type: 'number', default: 5 },
14
- },
15
- required: ['query'],
16
- },
17
- },
18
- checkFn: () => true,
19
- requiresEnv: ['SERPAPI_KEY (optional, falls back to DDG)'],
20
- handler: async ({ query, limit = 5 }) => {
21
- if (process.env.SERPAPI_KEY) {
22
- const url = `https://serpapi.com/search.json?q=${encodeURIComponent(query)}&api_key=${process.env.SERPAPI_KEY}`
23
- const data = await fetch(url).then(r => r.json())
24
- const results = (data.organic_results || []).slice(0, limit).map(r => ({ title: r.title, url: r.link, snippet: r.snippet }))
25
- return { results }
26
- }
27
- const fetchFn = globalThis.__fophFetch || fetch
28
- const html = await fetchFn(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`).then(r => r.text())
29
- const results = []
30
- const re = /<a class="result__a"[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>[\s\S]*?<a class="result__snippet"[^>]*>([^<]+)<\/a>/g
31
- let m
32
- while ((m = re.exec(html)) && results.length < limit) {
33
- results.push({ url: m[1], title: m[2].replace(/&amp;/g, '&'), snippet: m[3].replace(/<\/?b>/g, '') })
34
- }
35
- return { results }
36
- },
37
- })
@@ -1,18 +0,0 @@
1
- import { registry } from './registry.js'
2
- registry.register({
3
- name: 'web_fetch',
4
- toolset: 'browse',
5
- schema: { name: 'web_fetch', description: 'Fetch a URL and return text/json/headers.', parameters: { type: 'object', properties: { url: { type: 'string' }, method: { type: 'string', default: 'GET' }, headers: {}, body: { type: 'string' }, parse: { type: 'string', enum: ['text', 'json'] } }, required: ['url'] } },
6
- handler: async ({ url, method = 'GET', headers = {}, body, parse = 'text' }) => {
7
- const r = await fetch(url, { method, headers, body })
8
- const ct = r.headers.get('content-type')
9
- const out = parse === 'json' ? await r.json().catch(() => null) : await r.text()
10
- return { status: r.status, contentType: ct, body: out }
11
- },
12
- })
13
- registry.register({
14
- name: 'web_extract',
15
- toolset: 'browse',
16
- schema: { name: 'web_extract', description: 'Strip tags from HTML to plain text.', parameters: { type: 'object', properties: { html: { type: 'string' } }, required: ['html'] } },
17
- handler: async ({ html }) => ({ text: String(html).replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim() }),
18
- })
@@ -1,14 +0,0 @@
1
- import { registry } from './registry.js'
2
- import { getConfigValue } from '../config.js'
3
- registry.register({
4
- name: 'website_policy',
5
- toolset: 'core',
6
- schema: { name: 'website_policy', description: 'Per-host fetch policy (rate limit, allow/deny). Reads config.website_policy.', parameters: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] } },
7
- handler: async ({ url }) => {
8
- const policy = getConfigValue('website_policy', { allow: [], deny: [], ratelimit_ms: 1000 }) || {}
9
- const u = new URL(url)
10
- if (policy.deny?.some(d => u.hostname.includes(d))) return { decision: 'deny' }
11
- if (policy.allow?.length && !policy.allow.some(a => u.hostname.includes(a))) return { decision: 'deny', reason: 'not in allow list' }
12
- return { decision: 'allow', ratelimit_ms: policy.ratelimit_ms || 1000 }
13
- },
14
- })
@@ -1,25 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import { registry } from './registry.js'
4
-
5
- registry.register({
6
- name: 'write',
7
- toolset: 'core',
8
- schema: {
9
- name: 'write',
10
- description: 'Write content to a file (overwrites).',
11
- parameters: {
12
- type: 'object',
13
- properties: {
14
- path: { type: 'string' },
15
- content: { type: 'string' },
16
- },
17
- required: ['path', 'content'],
18
- },
19
- },
20
- handler: async ({ path: p, content }) => {
21
- fs.mkdirSync(path.dirname(p), { recursive: true })
22
- fs.writeFileSync(p, content, 'utf8')
23
- return { path: p, bytes: Buffer.byteLength(content, 'utf8') }
24
- },
25
- })
@@ -1,13 +0,0 @@
1
- import { registry } from './registry.js'
2
- registry.register({
3
- name: 'xai_grok',
4
- toolset: 'core',
5
- schema: { name: 'xai_grok', description: 'Chat completion via xAI Grok.', parameters: { type: 'object', properties: { prompt: { type: 'string' }, model: { type: 'string', default: 'grok-3' } }, required: ['prompt'] } },
6
- requiresEnv: ['XAI_API_KEY'],
7
- checkFn: () => Boolean(process.env.XAI_API_KEY),
8
- handler: async ({ prompt, model = 'grok-3' }) => {
9
- if (!process.env.XAI_API_KEY) return { error: 'XAI_API_KEY required' }
10
- const r = await fetch('https://api.x.ai/v1/chat/completions', { method: 'POST', headers: { authorization: `Bearer ${process.env.XAI_API_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ model, messages: [{ role: 'user', content: prompt }] }) })
11
- return await r.json()
12
- },
13
- })
@@ -1,13 +0,0 @@
1
- import { registry } from './registry.js'
2
- registry.register({
3
- name: 'yuanbao_tools',
4
- toolset: 'core',
5
- schema: { name: 'yuanbao_tools', description: 'Tencent Yuanbao chat completion (Hunyuan).', parameters: { type: 'object', properties: { prompt: { type: 'string' }, model: { type: 'string', default: 'hunyuan-pro' } }, required: ['prompt'] } },
6
- requiresEnv: ['YUANBAO_API_KEY'],
7
- checkFn: () => Boolean(process.env.YUANBAO_API_KEY),
8
- handler: async ({ prompt, model = 'hunyuan-pro' }) => {
9
- if (!process.env.YUANBAO_API_KEY) return { error: 'YUANBAO_API_KEY required' }
10
- const r = await fetch('https://api.hunyuan.cloud.tencent.com/v1/chat/completions', { method: 'POST', headers: { authorization: `Bearer ${process.env.YUANBAO_API_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ model, messages: [{ role: 'user', content: prompt }] }) })
11
- return await r.json()
12
- },
13
- })