freddie 0.0.47 → 0.0.49

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 (232) hide show
  1. package/AGENTS.md +12 -0
  2. package/CHANGELOG.md +26 -2
  3. package/package.json +3 -2
  4. package/plugins/ansi_strip/handler.js +7 -0
  5. package/plugins/ansi_strip/plugin.js +2 -0
  6. package/plugins/approval/handler.js +13 -0
  7. package/plugins/approval/plugin.js +2 -0
  8. package/plugins/bash/handler.js +33 -0
  9. package/plugins/bash/plugin.js +2 -0
  10. package/plugins/binary_extensions/handler.js +20 -0
  11. package/plugins/binary_extensions/plugin.js +2 -0
  12. package/plugins/browser/handler.js +46 -0
  13. package/plugins/browser/plugin.js +2 -0
  14. package/plugins/budget_config/handler.js +12 -0
  15. package/plugins/budget_config/plugin.js +2 -0
  16. package/plugins/checkpoint/handler.js +27 -0
  17. package/plugins/checkpoint/plugin.js +2 -0
  18. package/plugins/clarify/handler.js +13 -0
  19. package/plugins/clarify/plugin.js +2 -0
  20. package/plugins/code_execution/handler.js +25 -0
  21. package/plugins/code_execution/plugin.js +2 -0
  22. package/plugins/core-agent-machine/plugin.js +8 -0
  23. package/plugins/core-cli/plugin.js +83 -0
  24. package/plugins/core-commands/plugin.js +7 -0
  25. package/plugins/core-compressor/plugin.js +15 -0
  26. package/plugins/core-context-engine/plugin.js +7 -0
  27. package/plugins/core-cron/plugin.js +7 -0
  28. package/plugins/core-skills/plugin.js +7 -0
  29. package/plugins/credential_files/handler.js +14 -0
  30. package/plugins/credential_files/plugin.js +2 -0
  31. package/plugins/cronjob/handler.js +14 -0
  32. package/plugins/cronjob/plugin.js +2 -0
  33. package/plugins/debug_helpers/handler.js +8 -0
  34. package/plugins/debug_helpers/plugin.js +2 -0
  35. package/plugins/delegate/handler.js +27 -0
  36. package/plugins/delegate/plugin.js +2 -0
  37. package/plugins/discord_tool/handler.js +12 -0
  38. package/plugins/discord_tool/plugin.js +2 -0
  39. package/plugins/edit/handler.js +29 -0
  40. package/plugins/edit/plugin.js +2 -0
  41. package/plugins/env_passthrough/handler.js +14 -0
  42. package/plugins/env_passthrough/plugin.js +2 -0
  43. package/plugins/feishu_doc/handler.js +14 -0
  44. package/plugins/feishu_doc/plugin.js +2 -0
  45. package/plugins/feishu_drive/handler.js +13 -0
  46. package/plugins/feishu_drive/plugin.js +2 -0
  47. package/plugins/file_operations/handler.js +15 -0
  48. package/plugins/file_operations/plugin.js +2 -0
  49. package/plugins/file_state/handler.js +14 -0
  50. package/plugins/file_state/plugin.js +2 -0
  51. package/plugins/file_tools/handler.js +21 -0
  52. package/plugins/file_tools/plugin.js +2 -0
  53. package/plugins/fuzzy_match/handler.js +7 -0
  54. package/plugins/fuzzy_match/plugin.js +2 -0
  55. package/plugins/gm-cc/plugin.js +28 -0
  56. package/plugins/grep/handler.js +49 -0
  57. package/plugins/grep/plugin.js +2 -0
  58. package/plugins/gui-agents/plugin.js +26 -0
  59. package/plugins/gui-batch/plugin.js +11 -0
  60. package/plugins/gui-chat/plugin.js +21 -0
  61. package/plugins/gui-config/plugin.js +12 -0
  62. package/plugins/gui-cron/plugin.js +13 -0
  63. package/plugins/gui-debug/plugin.js +24 -0
  64. package/plugins/gui-env/plugin.js +7 -0
  65. package/plugins/gui-gateway/plugin.js +9 -0
  66. package/plugins/gui-profiles-commands-health/plugin.js +11 -0
  67. package/plugins/gui-sessions/plugin.js +9 -0
  68. package/plugins/gui-skills/plugin.js +8 -0
  69. package/plugins/gui-tools/plugin.js +7 -0
  70. package/plugins/homeassistant_tool/handler.js +14 -0
  71. package/plugins/homeassistant_tool/plugin.js +2 -0
  72. package/plugins/image_gen/handler.js +31 -0
  73. package/plugins/image_gen/plugin.js +2 -0
  74. package/plugins/interrupt/handler.js +16 -0
  75. package/plugins/interrupt/plugin.js +2 -0
  76. package/plugins/managed_tool_gateway/handler.js +9 -0
  77. package/plugins/managed_tool_gateway/plugin.js +2 -0
  78. package/plugins/mcp_oauth/handler.js +20 -0
  79. package/plugins/mcp_oauth/plugin.js +2 -0
  80. package/plugins/mcp_oauth_manager/handler.js +18 -0
  81. package/plugins/mcp_oauth_manager/plugin.js +2 -0
  82. package/plugins/mcp_tool/handler.js +34 -0
  83. package/plugins/mcp_tool/plugin.js +2 -0
  84. package/plugins/memory/handler.js +66 -0
  85. package/plugins/memory/plugin.js +2 -0
  86. package/plugins/memory-byterover/handler.js +25 -0
  87. package/plugins/memory-byterover/plugin.js +2 -0
  88. package/plugins/memory-hindsight/handler.js +25 -0
  89. package/plugins/memory-hindsight/plugin.js +2 -0
  90. package/plugins/memory-holographic/handler.js +31 -0
  91. package/plugins/memory-holographic/plugin.js +2 -0
  92. package/plugins/memory-honcho/handler.js +25 -0
  93. package/plugins/memory-honcho/plugin.js +2 -0
  94. package/plugins/memory-mem0/handler.js +25 -0
  95. package/plugins/memory-mem0/plugin.js +2 -0
  96. package/plugins/memory-openviking/handler.js +25 -0
  97. package/plugins/memory-openviking/plugin.js +2 -0
  98. package/plugins/memory-retaindb/handler.js +25 -0
  99. package/plugins/memory-retaindb/plugin.js +2 -0
  100. package/plugins/memory-supermemory/handler.js +25 -0
  101. package/plugins/memory-supermemory/plugin.js +2 -0
  102. package/plugins/mixture_of_agents/handler.js +13 -0
  103. package/plugins/mixture_of_agents/plugin.js +2 -0
  104. package/plugins/neutts_synth/handler.js +12 -0
  105. package/plugins/neutts_synth/plugin.js +2 -0
  106. package/plugins/openrouter_client/handler.js +12 -0
  107. package/plugins/openrouter_client/plugin.js +2 -0
  108. package/plugins/osv_check/handler.js +10 -0
  109. package/plugins/osv_check/plugin.js +2 -0
  110. package/plugins/patch_parser/handler.js +40 -0
  111. package/plugins/patch_parser/plugin.js +2 -0
  112. package/plugins/path_security/handler.js +14 -0
  113. package/plugins/path_security/plugin.js +2 -0
  114. package/plugins/platform-api_server/handler.js +21 -0
  115. package/plugins/platform-api_server/plugin.js +2 -0
  116. package/plugins/platform-bluebubbles/handler.js +32 -0
  117. package/plugins/platform-bluebubbles/plugin.js +2 -0
  118. package/plugins/platform-dingtalk/handler.js +32 -0
  119. package/plugins/platform-dingtalk/plugin.js +2 -0
  120. package/plugins/platform-discord/handler.js +24 -0
  121. package/plugins/platform-discord/plugin.js +2 -0
  122. package/plugins/platform-email/handler.js +51 -0
  123. package/plugins/platform-email/plugin.js +2 -0
  124. package/plugins/platform-feishu/handler.js +32 -0
  125. package/plugins/platform-feishu/plugin.js +2 -0
  126. package/plugins/platform-feishu_comment/handler.js +12 -0
  127. package/plugins/platform-feishu_comment/plugin.js +2 -0
  128. package/plugins/platform-feishu_comment_rules/handler.js +11 -0
  129. package/plugins/platform-feishu_comment_rules/plugin.js +2 -0
  130. package/plugins/platform-homeassistant/handler.js +32 -0
  131. package/plugins/platform-homeassistant/plugin.js +2 -0
  132. package/plugins/platform-matrix/handler.js +40 -0
  133. package/plugins/platform-matrix/plugin.js +2 -0
  134. package/plugins/platform-mattermost/handler.js +29 -0
  135. package/plugins/platform-mattermost/plugin.js +2 -0
  136. package/plugins/platform-qqbot/handler.js +32 -0
  137. package/plugins/platform-qqbot/plugin.js +2 -0
  138. package/plugins/platform-signal/handler.js +33 -0
  139. package/plugins/platform-signal/plugin.js +2 -0
  140. package/plugins/platform-slack/handler.js +34 -0
  141. package/plugins/platform-slack/plugin.js +2 -0
  142. package/plugins/platform-sms/handler.js +34 -0
  143. package/plugins/platform-sms/plugin.js +2 -0
  144. package/plugins/platform-telegram/handler.js +38 -0
  145. package/plugins/platform-telegram/plugin.js +2 -0
  146. package/plugins/platform-telegram_network/handler.js +17 -0
  147. package/plugins/platform-telegram_network/plugin.js +2 -0
  148. package/plugins/platform-webhook/handler.js +19 -0
  149. package/plugins/platform-webhook/plugin.js +2 -0
  150. package/plugins/platform-wecom/handler.js +32 -0
  151. package/plugins/platform-wecom/plugin.js +2 -0
  152. package/plugins/platform-wecom_callback/handler.js +15 -0
  153. package/plugins/platform-wecom_callback/plugin.js +2 -0
  154. package/plugins/platform-wecom_crypto/handler.js +16 -0
  155. package/plugins/platform-wecom_crypto/plugin.js +2 -0
  156. package/plugins/platform-weixin/handler.js +32 -0
  157. package/plugins/platform-weixin/plugin.js +2 -0
  158. package/plugins/platform-whatsapp/handler.js +40 -0
  159. package/plugins/platform-whatsapp/plugin.js +2 -0
  160. package/plugins/platform-yuanbao/handler.js +9 -0
  161. package/plugins/platform-yuanbao/plugin.js +2 -0
  162. package/plugins/platform-yuanbao_media/handler.js +5 -0
  163. package/plugins/platform-yuanbao_media/plugin.js +2 -0
  164. package/plugins/platform-yuanbao_proto/handler.js +9 -0
  165. package/plugins/platform-yuanbao_proto/plugin.js +2 -0
  166. package/plugins/platform-yuanbao_sticker/handler.js +6 -0
  167. package/plugins/platform-yuanbao_sticker/plugin.js +2 -0
  168. package/plugins/process_registry/handler.js +15 -0
  169. package/plugins/process_registry/plugin.js +2 -0
  170. package/plugins/read/handler.js +24 -0
  171. package/plugins/read/plugin.js +2 -0
  172. package/plugins/rl_training/handler.js +12 -0
  173. package/plugins/rl_training/plugin.js +2 -0
  174. package/plugins/schema_sanitizer/handler.js +17 -0
  175. package/plugins/schema_sanitizer/plugin.js +2 -0
  176. package/plugins/send_message/handler.js +30 -0
  177. package/plugins/send_message/plugin.js +2 -0
  178. package/plugins/session_search/handler.js +21 -0
  179. package/plugins/session_search/plugin.js +2 -0
  180. package/plugins/skill_manager/handler.js +16 -0
  181. package/plugins/skill_manager/plugin.js +2 -0
  182. package/plugins/skill_usage/handler.js +18 -0
  183. package/plugins/skill_usage/plugin.js +2 -0
  184. package/plugins/skills_guard/handler.js +16 -0
  185. package/plugins/skills_guard/plugin.js +2 -0
  186. package/plugins/skills_hub/handler.js +29 -0
  187. package/plugins/skills_hub/plugin.js +2 -0
  188. package/plugins/skills_index/handler.js +12 -0
  189. package/plugins/skills_index/plugin.js +2 -0
  190. package/plugins/skills_sync/handler.js +17 -0
  191. package/plugins/skills_sync/plugin.js +2 -0
  192. package/plugins/skills_tool/handler.js +9 -0
  193. package/plugins/skills_tool/plugin.js +2 -0
  194. package/plugins/slash_confirm/handler.js +14 -0
  195. package/plugins/slash_confirm/plugin.js +2 -0
  196. package/plugins/terminal/handler.js +27 -0
  197. package/plugins/terminal/plugin.js +2 -0
  198. package/plugins/tirith_security/handler.js +23 -0
  199. package/plugins/tirith_security/plugin.js +2 -0
  200. package/plugins/todo/handler.js +52 -0
  201. package/plugins/todo/plugin.js +2 -0
  202. package/plugins/tool_backend_helpers/handler.js +24 -0
  203. package/plugins/tool_backend_helpers/plugin.js +2 -0
  204. package/plugins/tool_output_limits/handler.js +14 -0
  205. package/plugins/tool_output_limits/plugin.js +2 -0
  206. package/plugins/tool_result_storage/handler.js +18 -0
  207. package/plugins/tool_result_storage/plugin.js +2 -0
  208. package/plugins/transcription/handler.js +18 -0
  209. package/plugins/transcription/plugin.js +2 -0
  210. package/plugins/tts/handler.js +18 -0
  211. package/plugins/tts/plugin.js +2 -0
  212. package/plugins/url_safety/handler.js +14 -0
  213. package/plugins/url_safety/plugin.js +2 -0
  214. package/plugins/vision/handler.js +17 -0
  215. package/plugins/vision/plugin.js +2 -0
  216. package/plugins/voice_mode/handler.js +9 -0
  217. package/plugins/voice_mode/plugin.js +2 -0
  218. package/plugins/web_search/handler.js +35 -0
  219. package/plugins/web_search/plugin.js +2 -0
  220. package/plugins/web_tools/handler.js +17 -0
  221. package/plugins/web_tools/plugin.js +2 -0
  222. package/plugins/website_policy/handler.js +13 -0
  223. package/plugins/website_policy/plugin.js +2 -0
  224. package/plugins/write/handler.js +23 -0
  225. package/plugins/write/plugin.js +2 -0
  226. package/plugins/xai_http/handler.js +12 -0
  227. package/plugins/xai_http/plugin.js +2 -0
  228. package/plugins/yuanbao_tools/handler.js +12 -0
  229. package/plugins/yuanbao_tools/plugin.js +2 -0
  230. package/src/sessions.js +2 -1
  231. package/src/web/app.js +17 -0
  232. package/src/web/index.html +7 -3
@@ -0,0 +1,18 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import crypto from 'node:crypto'
4
+ import { getFreddieHome } from '../../src/home.js'
5
+ function dir() { const d = path.join(getFreddieHome(), 'tool-results'); fs.mkdirSync(d, { recursive: true }); return d }
6
+
7
+ export const _tool = ({
8
+ name: 'tool_result_storage',
9
+ toolset: 'core',
10
+ 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'] } },
11
+ handler: async ({ action, content, token }) => {
12
+ 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 } }
13
+ if (action === 'fetch') { const f = path.join(dir(), token + '.txt'); return fs.existsSync(f) ? { content: fs.readFileSync(f, 'utf8') } : { error: 'not found' } }
14
+ if (action === 'list') return { tokens: fs.readdirSync(dir()).filter(f => f.endsWith('.txt')).map(f => f.replace(/\.txt$/, '')) }
15
+ if (action === 'delete') { const f = path.join(dir(), token + '.txt'); if (fs.existsSync(f)) fs.unlinkSync(f); return { deleted: token } }
16
+ return { error: 'unknown action' }
17
+ },
18
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-tool_result_storage', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,18 @@
1
+ import fs from 'node:fs'
2
+ export const _tool = ({
3
+ name: 'transcription',
4
+ toolset: 'creative',
5
+ 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'] } },
6
+ requiresEnv: ['OPENAI_API_KEY'],
7
+ checkFn: () => Boolean(process.env.OPENAI_API_KEY),
8
+ handler: async ({ file_path, model = 'whisper-1' }) => {
9
+ if (!process.env.OPENAI_API_KEY) return { error: 'OPENAI_API_KEY required' }
10
+ if (!fs.existsSync(file_path)) return { error: 'file not found: ' + file_path }
11
+ const blob = new Blob([fs.readFileSync(file_path)])
12
+ const fd = new FormData()
13
+ fd.append('file', blob, file_path.split(/[\\/]/).pop())
14
+ fd.append('model', model)
15
+ const r = await fetch('https://api.openai.com/v1/audio/transcriptions', { method: 'POST', headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY}` }, body: fd })
16
+ return await r.json()
17
+ },
18
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-transcription', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,18 @@
1
+ export const _tool = ({
2
+ name: 'tts',
3
+ toolset: 'creative',
4
+ 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'] } },
5
+ requiresEnv: ['OPENAI_API_KEY or ELEVENLABS_API_KEY'],
6
+ checkFn: () => Boolean(process.env.OPENAI_API_KEY || process.env.ELEVENLABS_API_KEY),
7
+ handler: async ({ text, provider = 'openai', voice = 'alloy' }) => {
8
+ if (provider === 'elevenlabs') {
9
+ if (!process.env.ELEVENLABS_API_KEY) return { error: 'ELEVENLABS_API_KEY required' }
10
+ const v = voice || '21m00Tcm4TlvDq8ikWAM'
11
+ 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 }) })
12
+ return { status: r.status, contentType: r.headers.get('content-type'), bytes: (await r.arrayBuffer()).byteLength }
13
+ }
14
+ if (!process.env.OPENAI_API_KEY) return { error: 'OPENAI_API_KEY required' }
15
+ 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 }) })
16
+ return { status: r.status, bytes: (await r.arrayBuffer()).byteLength }
17
+ },
18
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-tts', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,14 @@
1
+ const SUSPICIOUS = ['phish', 'malware', '.onion']
2
+ const PRIVATE_RANGES = [/^10\./, /^192\.168\./, /^172\.(1[6-9]|2\d|3[01])\./, /^127\./, /^0\./, /^169\.254\./]
3
+ export const _tool = ({
4
+ name: 'url_safety',
5
+ toolset: 'core',
6
+ schema: { name: 'url_safety', description: 'Heuristic URL safety check (private IPs, known-bad TLDs, scheme).', parameters: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] } },
7
+ handler: async ({ url }) => {
8
+ let u; try { u = new URL(url) } catch { return { safe: false, reason: 'invalid URL' } }
9
+ if (!['http:', 'https:'].includes(u.protocol)) return { safe: false, reason: 'unsupported scheme: ' + u.protocol }
10
+ if (PRIVATE_RANGES.some(re => re.test(u.hostname))) return { safe: false, reason: 'private IP host' }
11
+ for (const s of SUSPICIOUS) if (u.hostname.includes(s)) return { safe: false, reason: 'suspicious token: ' + s }
12
+ return { safe: true, host: u.hostname }
13
+ },
14
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-url_safety', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,17 @@
1
+ export const _tool = ({
2
+ name: 'vision',
3
+ toolset: 'creative',
4
+ 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'] } },
5
+ requiresEnv: ['OPENAI_API_KEY or ANTHROPIC_API_KEY'],
6
+ checkFn: () => Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY),
7
+ handler: async ({ image_url, prompt = 'Describe this image.', provider = 'openai' }) => {
8
+ if (provider === 'anthropic') {
9
+ if (!process.env.ANTHROPIC_API_KEY) return { error: 'ANTHROPIC_API_KEY required' }
10
+ 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 }] }] }) })
11
+ return await r.json()
12
+ }
13
+ if (!process.env.OPENAI_API_KEY) return { error: 'OPENAI_API_KEY required' }
14
+ 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 } }] }] }) })
15
+ return await r.json()
16
+ },
17
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-vision', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,9 @@
1
+ export const _tool = ({
2
+ name: 'voice_mode',
3
+ toolset: 'creative',
4
+ 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' } } } },
5
+ handler: async ({ enabled }, ctx = {}) => {
6
+ if (typeof ctx.setVoiceMode === 'function') return await ctx.setVoiceMode(Boolean(enabled))
7
+ return { enabled: Boolean(enabled), note: 'voice mode toggled in-process; bind ctx.setVoiceMode to wire to a real audio loop' }
8
+ },
9
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-voice_mode', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,35 @@
1
+ export const _tool = ({
2
+ name: 'web_search',
3
+ toolset: 'browse',
4
+ schema: {
5
+ name: 'web_search',
6
+ description: 'Search the web (DuckDuckGo HTML or SerpAPI). Returns title/url/snippet list.',
7
+ parameters: {
8
+ type: 'object',
9
+ properties: {
10
+ query: { type: 'string' },
11
+ limit: { type: 'number', default: 5 },
12
+ },
13
+ required: ['query'],
14
+ },
15
+ },
16
+ checkFn: () => true,
17
+ requiresEnv: ['SERPAPI_KEY (optional, falls back to DDG)'],
18
+ handler: async ({ query, limit = 5 }) => {
19
+ if (process.env.SERPAPI_KEY) {
20
+ const url = `https://serpapi.com/search.json?q=${encodeURIComponent(query)}&api_key=${process.env.SERPAPI_KEY}`
21
+ const data = await fetch(url).then(r => r.json())
22
+ const results = (data.organic_results || []).slice(0, limit).map(r => ({ title: r.title, url: r.link, snippet: r.snippet }))
23
+ return { results }
24
+ }
25
+ const fetchFn = globalThis.__freddieFetch || fetch
26
+ const html = await fetchFn(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`).then(r => r.text())
27
+ const results = []
28
+ const re = /<a class="result__a"[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>[\s\S]*?<a class="result__snippet"[^>]*>([^<]+)<\/a>/g
29
+ let m
30
+ while ((m = re.exec(html)) && results.length < limit) {
31
+ results.push({ url: m[1], title: m[2].replace(/&amp;/g, '&'), snippet: m[3].replace(/<\/?b>/g, '') })
32
+ }
33
+ return { results }
34
+ },
35
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-web_search', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,17 @@
1
+ export const _tool0 = ({
2
+ name: 'web_fetch',
3
+ toolset: 'browse',
4
+ 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'] } },
5
+ handler: async ({ url, method = 'GET', headers = {}, body, parse = 'text' }) => {
6
+ const r = await fetch(url, { method, headers, body })
7
+ const ct = r.headers.get('content-type')
8
+ const out = parse === 'json' ? await r.json().catch(() => null) : await r.text()
9
+ return { status: r.status, contentType: ct, body: out }
10
+ },
11
+ })
12
+ export const _tool1 = ({
13
+ name: 'web_extract',
14
+ toolset: 'browse',
15
+ schema: { name: 'web_extract', description: 'Strip tags from HTML to plain text.', parameters: { type: 'object', properties: { html: { type: 'string' } }, required: ['html'] } },
16
+ handler: async ({ html }) => ({ text: String(html).replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim() }),
17
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool0, _tool1 } from './handler.js'
2
+ export default { name: 'tool-web_tools', surfaces: 'pi', register({ pi }) { for (const t of [_tool0, _tool1]) pi.tools.register(t) } }
@@ -0,0 +1,13 @@
1
+ import { getConfigValue } from '../../src/config.js'
2
+ export const _tool = ({
3
+ name: 'website_policy',
4
+ toolset: 'core',
5
+ 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'] } },
6
+ handler: async ({ url }) => {
7
+ const policy = getConfigValue('website_policy', { allow: [], deny: [], ratelimit_ms: 1000 }) || {}
8
+ const u = new URL(url)
9
+ if (policy.deny?.some(d => u.hostname.includes(d))) return { decision: 'deny' }
10
+ if (policy.allow?.length && !policy.allow.some(a => u.hostname.includes(a))) return { decision: 'deny', reason: 'not in allow list' }
11
+ return { decision: 'allow', ratelimit_ms: policy.ratelimit_ms || 1000 }
12
+ },
13
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-website_policy', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,23 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ export const _tool = ({
4
+ name: 'write',
5
+ toolset: 'core',
6
+ schema: {
7
+ name: 'write',
8
+ description: 'Write content to a file (overwrites).',
9
+ parameters: {
10
+ type: 'object',
11
+ properties: {
12
+ path: { type: 'string' },
13
+ content: { type: 'string' },
14
+ },
15
+ required: ['path', 'content'],
16
+ },
17
+ },
18
+ handler: async ({ path: p, content }) => {
19
+ fs.mkdirSync(path.dirname(p), { recursive: true })
20
+ fs.writeFileSync(p, content, 'utf8')
21
+ return { path: p, bytes: Buffer.byteLength(content, 'utf8') }
22
+ },
23
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-write', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,12 @@
1
+ export const _tool = ({
2
+ name: 'xai_grok',
3
+ toolset: 'core',
4
+ 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'] } },
5
+ requiresEnv: ['XAI_API_KEY'],
6
+ checkFn: () => Boolean(process.env.XAI_API_KEY),
7
+ handler: async ({ prompt, model = 'grok-3' }) => {
8
+ if (!process.env.XAI_API_KEY) return { error: 'XAI_API_KEY required' }
9
+ 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 }] }) })
10
+ return await r.json()
11
+ },
12
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-xai_http', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,12 @@
1
+ export const _tool = ({
2
+ name: 'yuanbao_tools',
3
+ toolset: 'core',
4
+ 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'] } },
5
+ requiresEnv: ['YUANBAO_API_KEY'],
6
+ checkFn: () => Boolean(process.env.YUANBAO_API_KEY),
7
+ handler: async ({ prompt, model = 'hunyuan-pro' }) => {
8
+ if (!process.env.YUANBAO_API_KEY) return { error: 'YUANBAO_API_KEY required' }
9
+ 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 }] }) })
10
+ return await r.json()
11
+ },
12
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-yuanbao_tools', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
package/src/sessions.js CHANGED
@@ -69,6 +69,7 @@ export async function listSessions(limit = 50) {
69
69
 
70
70
  export async function search(query, limit = 20) {
71
71
  const d = await db()
72
+ const likePattern = `%${query}%`
72
73
  // Try FTS5 if available (libsql, but not busybase since triggers can't be created)
73
74
  try {
74
75
  const ftsResult = await d.prepare(`SELECT m.id, m.session_id, m.content FROM messages_fts f JOIN messages m ON m.id = f.rowid WHERE messages_fts MATCH ? ORDER BY rank LIMIT ?`).all(query, limit)
@@ -77,7 +78,7 @@ export async function search(query, limit = 20) {
77
78
  // FTS5 not available, fall through to LIKE
78
79
  }
79
80
  // Fallback to LIKE search
80
- return await d.prepare(`SELECT id, session_id, content FROM messages WHERE content LIKE ? ORDER BY ts DESC LIMIT ?`).all(`%${query}%`, limit)
81
+ return await d.prepare(`SELECT id, session_id, content FROM messages WHERE content LIKE ? ORDER BY ts DESC LIMIT ?`).all(likePattern, limit)
81
82
  }
82
83
 
83
84
  export function closeDb() { return closeDbImpl() }
package/src/web/app.js CHANGED
@@ -6,6 +6,7 @@ await installStyles()
6
6
 
7
7
  if (!window.__debug) { try { window.__debug = {} } catch { Object.defineProperty(window, '__debug', { value: {}, writable: true, configurable: true }) } }
8
8
  window.__debug.dashboard = () => ({ booted: true, ts: Date.now(), framework: 'anentrypoint-design+webjsx', route: location.hash || '#/sessions' })
9
+ window.__debug.agents = () => ({ registered: true, active: AppState.agents?.active || null, count: AppState.agents?.count || 0 })
9
10
 
10
11
  const j = async (u, opts) => { try { const r = await fetch(u, opts); if (!r.ok) throw new Error(r.status + ' ' + r.statusText); return await r.json() } catch (e) { return { __error: String(e) } } }
11
12
 
@@ -13,6 +14,7 @@ const ROUTES = [
13
14
  { path: '#/home', label: 'Home', glyph: '⌂' },
14
15
  { path: '#/chat', label: 'Chat', glyph: '⌨' },
15
16
  { path: '#/sessions', label: 'Sessions', glyph: '✉' },
17
+ { path: '#/agents', label: 'Agents', glyph: '◈' },
16
18
  { path: '#/analytics', label: 'Analytics', glyph: '◉' },
17
19
  { path: '#/models', label: 'Models', glyph: '◎' },
18
20
  { path: '#/logs', label: 'Logs', glyph: '☰' },
@@ -36,6 +38,7 @@ const AppState = {
36
38
  sessionsFilter: '',
37
39
  chat: { messages: [], draft: '', streaming: false },
38
40
  batch: { results: null, running: false },
41
+ agents: { count: 0, active: null },
39
42
  }
40
43
  function applyTheme() { document.documentElement.setAttribute('data-theme', AppState.theme) }
41
44
  applyTheme()
@@ -141,6 +144,20 @@ const PAGES = {
141
144
  ]
142
145
  },
143
146
 
147
+ '#/agents': async () => {
148
+ const agents = await j('/api/agents')
149
+ const count = agents.__error ? 0 : (agents.count || 0)
150
+ const active = agents.active || null
151
+ return [
152
+ kpi([[count || 0, 'Total agents'], [active ? 1 : 0, 'Active']]),
153
+ Panel({ title: 'Agent overview', children: Receipt({ rows: [
154
+ ['Total agent turns', String(agents.turns || 0)],
155
+ ['Active agent', active || '(none)'],
156
+ ['Last activity', agents.last_activity ? new Date(agents.last_activity).toLocaleString() : '—'],
157
+ ]}) }),
158
+ ]
159
+ },
160
+
144
161
  '#/analytics': async () => {
145
162
  const [sessions, tools, debug] = await Promise.all([j('/api/sessions'), j('/api/tools'), j('/api/debug')])
146
163
  const all = Array.isArray(sessions) ? sessions : []
@@ -146,10 +146,12 @@ html[data-theme="dark"] .ds-247420 {
146
146
  .ds-247420 .kpi-card .num { font-size: 1.75rem; color: var(--panel-text); font-weight: 400; font-family: 'JetBrains Mono', monospace; }
147
147
  .ds-247420 .kpi-card .lbl { font-size: 0.75rem; color: var(--panel-text-2); font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; margin-top: 2px; }
148
148
 
149
- .ds-247420 .row-form { display: flex; gap: 8px; flex-wrap: wrap; align-items: stretch; margin-bottom: 12px; }
150
- .ds-247420 .row-form input { flex: 1; min-width: 140px; }
149
+ .ds-247420 .row-form { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 12px; }
150
+ .ds-247420 .row-form input { flex: 1; min-width: 140px; padding: 10px 12px !important; }
151
151
 
152
- .ds-247420 .chat .chat-thread { max-height: calc(100vh - 320px); overflow-y: auto; }
152
+ .ds-247420 .chat { display: flex; flex-direction: column; height: 100%; }
153
+ .ds-247420 .chat .chat-thread { flex: 1; max-height: calc(100vh - 320px); overflow-y: auto; }
154
+ .ds-247420 .chat .chat-composer { padding-top: 12px; border-top: 1px solid var(--panel-2); }
153
155
 
154
156
  .ds-247420 .chip { border-radius: 6px; padding: 4px 10px; font-size: 12px; font-weight: 600; display: inline-block; }
155
157
  .ds-247420 .chip.ok { background: var(--green); color: white; }
@@ -158,6 +160,8 @@ html[data-theme="dark"] .ds-247420 {
158
160
  .ds-247420 .empty { color: var(--panel-text-3); padding: 12px 0; }
159
161
  .ds-247420 a { color: var(--link); text-decoration: none; }
160
162
  .ds-247420 a:hover { text-decoration: underline; }
163
+
164
+ #app { padding: 16px 20px; }
161
165
  </style>
162
166
  </head>
163
167
  <body>