freddie 0.0.48 → 0.0.50

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 (230) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +2 -1
  3. package/plugins/ansi_strip/handler.js +7 -0
  4. package/plugins/ansi_strip/plugin.js +2 -0
  5. package/plugins/approval/handler.js +13 -0
  6. package/plugins/approval/plugin.js +2 -0
  7. package/plugins/bash/handler.js +33 -0
  8. package/plugins/bash/plugin.js +2 -0
  9. package/plugins/binary_extensions/handler.js +20 -0
  10. package/plugins/binary_extensions/plugin.js +2 -0
  11. package/plugins/browser/handler.js +46 -0
  12. package/plugins/browser/plugin.js +2 -0
  13. package/plugins/budget_config/handler.js +12 -0
  14. package/plugins/budget_config/plugin.js +2 -0
  15. package/plugins/checkpoint/handler.js +27 -0
  16. package/plugins/checkpoint/plugin.js +2 -0
  17. package/plugins/clarify/handler.js +13 -0
  18. package/plugins/clarify/plugin.js +2 -0
  19. package/plugins/code_execution/handler.js +25 -0
  20. package/plugins/code_execution/plugin.js +2 -0
  21. package/plugins/core-agent-machine/plugin.js +8 -0
  22. package/plugins/core-cli/plugin.js +83 -0
  23. package/plugins/core-commands/plugin.js +7 -0
  24. package/plugins/core-compressor/plugin.js +15 -0
  25. package/plugins/core-context-engine/plugin.js +7 -0
  26. package/plugins/core-cron/plugin.js +7 -0
  27. package/plugins/core-skills/plugin.js +7 -0
  28. package/plugins/credential_files/handler.js +14 -0
  29. package/plugins/credential_files/plugin.js +2 -0
  30. package/plugins/cronjob/handler.js +14 -0
  31. package/plugins/cronjob/plugin.js +2 -0
  32. package/plugins/debug_helpers/handler.js +8 -0
  33. package/plugins/debug_helpers/plugin.js +2 -0
  34. package/plugins/delegate/handler.js +27 -0
  35. package/plugins/delegate/plugin.js +2 -0
  36. package/plugins/discord_tool/handler.js +12 -0
  37. package/plugins/discord_tool/plugin.js +2 -0
  38. package/plugins/edit/handler.js +29 -0
  39. package/plugins/edit/plugin.js +2 -0
  40. package/plugins/env_passthrough/handler.js +14 -0
  41. package/plugins/env_passthrough/plugin.js +2 -0
  42. package/plugins/feishu_doc/handler.js +14 -0
  43. package/plugins/feishu_doc/plugin.js +2 -0
  44. package/plugins/feishu_drive/handler.js +13 -0
  45. package/plugins/feishu_drive/plugin.js +2 -0
  46. package/plugins/file_operations/handler.js +15 -0
  47. package/plugins/file_operations/plugin.js +2 -0
  48. package/plugins/file_state/handler.js +14 -0
  49. package/plugins/file_state/plugin.js +2 -0
  50. package/plugins/file_tools/handler.js +21 -0
  51. package/plugins/file_tools/plugin.js +2 -0
  52. package/plugins/fuzzy_match/handler.js +7 -0
  53. package/plugins/fuzzy_match/plugin.js +2 -0
  54. package/plugins/gm-cc/plugin.js +28 -0
  55. package/plugins/grep/handler.js +49 -0
  56. package/plugins/grep/plugin.js +2 -0
  57. package/plugins/gui-agents/plugin.js +26 -0
  58. package/plugins/gui-batch/plugin.js +11 -0
  59. package/plugins/gui-chat/plugin.js +22 -0
  60. package/plugins/gui-config/plugin.js +12 -0
  61. package/plugins/gui-cron/plugin.js +13 -0
  62. package/plugins/gui-debug/plugin.js +24 -0
  63. package/plugins/gui-env/plugin.js +7 -0
  64. package/plugins/gui-gateway/plugin.js +9 -0
  65. package/plugins/gui-profiles-commands-health/plugin.js +11 -0
  66. package/plugins/gui-sessions/plugin.js +9 -0
  67. package/plugins/gui-skills/plugin.js +8 -0
  68. package/plugins/gui-tools/plugin.js +7 -0
  69. package/plugins/homeassistant_tool/handler.js +14 -0
  70. package/plugins/homeassistant_tool/plugin.js +2 -0
  71. package/plugins/image_gen/handler.js +31 -0
  72. package/plugins/image_gen/plugin.js +2 -0
  73. package/plugins/interrupt/handler.js +16 -0
  74. package/plugins/interrupt/plugin.js +2 -0
  75. package/plugins/managed_tool_gateway/handler.js +9 -0
  76. package/plugins/managed_tool_gateway/plugin.js +2 -0
  77. package/plugins/mcp_oauth/handler.js +20 -0
  78. package/plugins/mcp_oauth/plugin.js +2 -0
  79. package/plugins/mcp_oauth_manager/handler.js +18 -0
  80. package/plugins/mcp_oauth_manager/plugin.js +2 -0
  81. package/plugins/mcp_tool/handler.js +34 -0
  82. package/plugins/mcp_tool/plugin.js +2 -0
  83. package/plugins/memory/handler.js +66 -0
  84. package/plugins/memory/plugin.js +2 -0
  85. package/plugins/memory-byterover/handler.js +25 -0
  86. package/plugins/memory-byterover/plugin.js +2 -0
  87. package/plugins/memory-hindsight/handler.js +25 -0
  88. package/plugins/memory-hindsight/plugin.js +2 -0
  89. package/plugins/memory-holographic/handler.js +31 -0
  90. package/plugins/memory-holographic/plugin.js +2 -0
  91. package/plugins/memory-honcho/handler.js +25 -0
  92. package/plugins/memory-honcho/plugin.js +2 -0
  93. package/plugins/memory-mem0/handler.js +25 -0
  94. package/plugins/memory-mem0/plugin.js +2 -0
  95. package/plugins/memory-openviking/handler.js +25 -0
  96. package/plugins/memory-openviking/plugin.js +2 -0
  97. package/plugins/memory-retaindb/handler.js +25 -0
  98. package/plugins/memory-retaindb/plugin.js +2 -0
  99. package/plugins/memory-supermemory/handler.js +25 -0
  100. package/plugins/memory-supermemory/plugin.js +2 -0
  101. package/plugins/mixture_of_agents/handler.js +13 -0
  102. package/plugins/mixture_of_agents/plugin.js +2 -0
  103. package/plugins/neutts_synth/handler.js +12 -0
  104. package/plugins/neutts_synth/plugin.js +2 -0
  105. package/plugins/openrouter_client/handler.js +12 -0
  106. package/plugins/openrouter_client/plugin.js +2 -0
  107. package/plugins/osv_check/handler.js +10 -0
  108. package/plugins/osv_check/plugin.js +2 -0
  109. package/plugins/patch_parser/handler.js +40 -0
  110. package/plugins/patch_parser/plugin.js +2 -0
  111. package/plugins/path_security/handler.js +14 -0
  112. package/plugins/path_security/plugin.js +2 -0
  113. package/plugins/platform-api_server/handler.js +21 -0
  114. package/plugins/platform-api_server/plugin.js +2 -0
  115. package/plugins/platform-bluebubbles/handler.js +32 -0
  116. package/plugins/platform-bluebubbles/plugin.js +2 -0
  117. package/plugins/platform-dingtalk/handler.js +32 -0
  118. package/plugins/platform-dingtalk/plugin.js +2 -0
  119. package/plugins/platform-discord/handler.js +24 -0
  120. package/plugins/platform-discord/plugin.js +2 -0
  121. package/plugins/platform-email/handler.js +51 -0
  122. package/plugins/platform-email/plugin.js +2 -0
  123. package/plugins/platform-feishu/handler.js +32 -0
  124. package/plugins/platform-feishu/plugin.js +2 -0
  125. package/plugins/platform-feishu_comment/handler.js +12 -0
  126. package/plugins/platform-feishu_comment/plugin.js +2 -0
  127. package/plugins/platform-feishu_comment_rules/handler.js +11 -0
  128. package/plugins/platform-feishu_comment_rules/plugin.js +2 -0
  129. package/plugins/platform-homeassistant/handler.js +32 -0
  130. package/plugins/platform-homeassistant/plugin.js +2 -0
  131. package/plugins/platform-matrix/handler.js +40 -0
  132. package/plugins/platform-matrix/plugin.js +2 -0
  133. package/plugins/platform-mattermost/handler.js +29 -0
  134. package/plugins/platform-mattermost/plugin.js +2 -0
  135. package/plugins/platform-qqbot/handler.js +32 -0
  136. package/plugins/platform-qqbot/plugin.js +2 -0
  137. package/plugins/platform-signal/handler.js +33 -0
  138. package/plugins/platform-signal/plugin.js +2 -0
  139. package/plugins/platform-slack/handler.js +34 -0
  140. package/plugins/platform-slack/plugin.js +2 -0
  141. package/plugins/platform-sms/handler.js +34 -0
  142. package/plugins/platform-sms/plugin.js +2 -0
  143. package/plugins/platform-telegram/handler.js +38 -0
  144. package/plugins/platform-telegram/plugin.js +2 -0
  145. package/plugins/platform-telegram_network/handler.js +17 -0
  146. package/plugins/platform-telegram_network/plugin.js +2 -0
  147. package/plugins/platform-webhook/handler.js +19 -0
  148. package/plugins/platform-webhook/plugin.js +2 -0
  149. package/plugins/platform-wecom/handler.js +32 -0
  150. package/plugins/platform-wecom/plugin.js +2 -0
  151. package/plugins/platform-wecom_callback/handler.js +15 -0
  152. package/plugins/platform-wecom_callback/plugin.js +2 -0
  153. package/plugins/platform-wecom_crypto/handler.js +16 -0
  154. package/plugins/platform-wecom_crypto/plugin.js +2 -0
  155. package/plugins/platform-weixin/handler.js +32 -0
  156. package/plugins/platform-weixin/plugin.js +2 -0
  157. package/plugins/platform-whatsapp/handler.js +40 -0
  158. package/plugins/platform-whatsapp/plugin.js +2 -0
  159. package/plugins/platform-yuanbao/handler.js +9 -0
  160. package/plugins/platform-yuanbao/plugin.js +2 -0
  161. package/plugins/platform-yuanbao_media/handler.js +5 -0
  162. package/plugins/platform-yuanbao_media/plugin.js +2 -0
  163. package/plugins/platform-yuanbao_proto/handler.js +9 -0
  164. package/plugins/platform-yuanbao_proto/plugin.js +2 -0
  165. package/plugins/platform-yuanbao_sticker/handler.js +6 -0
  166. package/plugins/platform-yuanbao_sticker/plugin.js +2 -0
  167. package/plugins/process_registry/handler.js +15 -0
  168. package/plugins/process_registry/plugin.js +2 -0
  169. package/plugins/read/handler.js +24 -0
  170. package/plugins/read/plugin.js +2 -0
  171. package/plugins/rl_training/handler.js +12 -0
  172. package/plugins/rl_training/plugin.js +2 -0
  173. package/plugins/schema_sanitizer/handler.js +17 -0
  174. package/plugins/schema_sanitizer/plugin.js +2 -0
  175. package/plugins/send_message/handler.js +30 -0
  176. package/plugins/send_message/plugin.js +2 -0
  177. package/plugins/session_search/handler.js +21 -0
  178. package/plugins/session_search/plugin.js +2 -0
  179. package/plugins/skill_manager/handler.js +16 -0
  180. package/plugins/skill_manager/plugin.js +2 -0
  181. package/plugins/skill_usage/handler.js +18 -0
  182. package/plugins/skill_usage/plugin.js +2 -0
  183. package/plugins/skills_guard/handler.js +16 -0
  184. package/plugins/skills_guard/plugin.js +2 -0
  185. package/plugins/skills_hub/handler.js +29 -0
  186. package/plugins/skills_hub/plugin.js +2 -0
  187. package/plugins/skills_index/handler.js +12 -0
  188. package/plugins/skills_index/plugin.js +2 -0
  189. package/plugins/skills_sync/handler.js +17 -0
  190. package/plugins/skills_sync/plugin.js +2 -0
  191. package/plugins/skills_tool/handler.js +9 -0
  192. package/plugins/skills_tool/plugin.js +2 -0
  193. package/plugins/slash_confirm/handler.js +14 -0
  194. package/plugins/slash_confirm/plugin.js +2 -0
  195. package/plugins/terminal/handler.js +27 -0
  196. package/plugins/terminal/plugin.js +2 -0
  197. package/plugins/tirith_security/handler.js +23 -0
  198. package/plugins/tirith_security/plugin.js +2 -0
  199. package/plugins/todo/handler.js +52 -0
  200. package/plugins/todo/plugin.js +2 -0
  201. package/plugins/tool_backend_helpers/handler.js +24 -0
  202. package/plugins/tool_backend_helpers/plugin.js +2 -0
  203. package/plugins/tool_output_limits/handler.js +14 -0
  204. package/plugins/tool_output_limits/plugin.js +2 -0
  205. package/plugins/tool_result_storage/handler.js +18 -0
  206. package/plugins/tool_result_storage/plugin.js +2 -0
  207. package/plugins/transcription/handler.js +18 -0
  208. package/plugins/transcription/plugin.js +2 -0
  209. package/plugins/tts/handler.js +18 -0
  210. package/plugins/tts/plugin.js +2 -0
  211. package/plugins/url_safety/handler.js +14 -0
  212. package/plugins/url_safety/plugin.js +2 -0
  213. package/plugins/vision/handler.js +17 -0
  214. package/plugins/vision/plugin.js +2 -0
  215. package/plugins/voice_mode/handler.js +9 -0
  216. package/plugins/voice_mode/plugin.js +2 -0
  217. package/plugins/web_search/handler.js +35 -0
  218. package/plugins/web_search/plugin.js +2 -0
  219. package/plugins/web_tools/handler.js +17 -0
  220. package/plugins/web_tools/plugin.js +2 -0
  221. package/plugins/website_policy/handler.js +13 -0
  222. package/plugins/website_policy/plugin.js +2 -0
  223. package/plugins/write/handler.js +23 -0
  224. package/plugins/write/plugin.js +2 -0
  225. package/plugins/xai_http/handler.js +12 -0
  226. package/plugins/xai_http/plugin.js +2 -0
  227. package/plugins/yuanbao_tools/handler.js +12 -0
  228. package/plugins/yuanbao_tools/plugin.js +2 -0
  229. package/src/agent/llm_resolver.js +2 -1
  230. package/src/agent/pi-bridge.js +3 -1
@@ -0,0 +1,24 @@
1
+ import fs from 'node:fs'
2
+ export const _tool = ({
3
+ name: 'read',
4
+ toolset: 'core',
5
+ schema: {
6
+ name: 'read',
7
+ description: 'Read a file from disk. Returns lines with line numbers.',
8
+ parameters: {
9
+ type: 'object',
10
+ properties: {
11
+ path: { type: 'string' },
12
+ offset: { type: 'number', default: 0 },
13
+ limit: { type: 'number', default: 2000 },
14
+ },
15
+ required: ['path'],
16
+ },
17
+ },
18
+ handler: async ({ path: p, offset = 0, limit = 2000 }) => {
19
+ if (!fs.existsSync(p)) return { error: `not found: ${p}` }
20
+ const lines = fs.readFileSync(p, 'utf8').split('\n')
21
+ const slice = lines.slice(offset, offset + limit)
22
+ return { path: p, total: lines.length, content: slice.map((l, i) => `${(offset + i + 1).toString().padStart(6)}\t${l}`).join('\n') }
23
+ },
24
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-read', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,12 @@
1
+ export const _tool = ({
2
+ name: 'rl_training',
3
+ toolset: 'core',
4
+ schema: { name: 'rl_training', description: 'Kick off an RL rollout (Atropos integration).', parameters: { type: 'object', properties: { task: { type: 'string' }, model: { type: 'string' } }, required: ['task'] } },
5
+ requiresEnv: ['ATROPOS_URL'],
6
+ checkFn: () => Boolean(process.env.ATROPOS_URL),
7
+ handler: async ({ task, model }) => {
8
+ if (!process.env.ATROPOS_URL) return { error: 'ATROPOS_URL required' }
9
+ 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 }) })
10
+ return await r.json()
11
+ },
12
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-rl_training', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,17 @@
1
+ const DANGEROUS_PARAM_NAMES = new Set(['eval', 'exec', '__proto__', 'constructor'])
2
+ export function sanitizeSchema(schema) {
3
+ if (!schema || typeof schema !== 'object') return schema
4
+ const out = JSON.parse(JSON.stringify(schema))
5
+ if (out.parameters?.properties) {
6
+ for (const k of Object.keys(out.parameters.properties)) {
7
+ if (DANGEROUS_PARAM_NAMES.has(k)) delete out.parameters.properties[k]
8
+ }
9
+ }
10
+ return out
11
+ }
12
+ export const _tool = ({
13
+ name: 'schema_sanitizer',
14
+ toolset: 'core',
15
+ schema: { name: 'schema_sanitizer', description: 'Strip dangerous fields (eval, exec, __proto__) from a tool schema.', parameters: { type: 'object', properties: { schema: {} }, required: ['schema'] } },
16
+ handler: async ({ schema }) => ({ sanitized: sanitizeSchema(schema) }),
17
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-schema_sanitizer', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,30 @@
1
+ const PLATFORM_MODULES = {
2
+ telegram: '../gateway/platforms/telegram.js',
3
+ discord: '../gateway/platforms/discord.js',
4
+ slack: '../gateway/platforms/slack.js',
5
+ whatsapp: '../gateway/platforms/whatsapp.js',
6
+ email: '../gateway/platforms/email.js',
7
+ sms: '../gateway/platforms/sms.js',
8
+ matrix: '../gateway/platforms/matrix.js',
9
+ signal: '../gateway/platforms/signal.js',
10
+ mattermost: '../gateway/platforms/mattermost.js',
11
+ }
12
+
13
+ export const _tool = ({
14
+ name: 'send_message',
15
+ toolset: 'core',
16
+ 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'] } },
17
+ handler: async ({ platform, to, text }) => {
18
+ const mod = PLATFORM_MODULES[platform]
19
+ if (!mod) return { error: 'unknown platform: ' + platform }
20
+ const m = await import(mod)
21
+ const cls = Object.values(m)[0]
22
+ const inst = new cls({})
23
+ try { await inst.start() } catch (e) { return { error: String(e.message || e) } }
24
+ try {
25
+ const out = await inst.send({ to, text })
26
+ await inst.stop?.()
27
+ return { ok: true, response: out }
28
+ } catch (e) { await inst.stop?.(); return { error: String(e.message || e) } }
29
+ },
30
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-send_message', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,21 @@
1
+ import { search, listSessions, getMessages } from '../../src/sessions.js'
2
+ export const _tool0 = ({
3
+ name: 'session_search',
4
+ toolset: 'core',
5
+ 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'] } },
6
+ handler: async ({ query, limit = 20, session_id = null }) => {
7
+ if (session_id) {
8
+ const msgs = await getMessages(session_id)
9
+ const q = String(query).toLowerCase()
10
+ return { items: msgs.filter(m => String(m.content || '').toLowerCase().includes(q)).slice(0, limit) }
11
+ }
12
+ return { items: await search(query, limit) }
13
+ },
14
+ })
15
+
16
+ export const _tool1 = ({
17
+ name: 'session_list',
18
+ toolset: 'core',
19
+ schema: { name: 'session_list', description: 'List recent sessions.', parameters: { type: 'object', properties: { limit: { type: 'number', default: 20 } } } },
20
+ handler: async ({ limit = 20 }) => ({ sessions: await listSessions(limit) }),
21
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool0, _tool1 } from './handler.js'
2
+ export default { name: 'tool-session_search', surfaces: 'pi', register({ pi }) { for (const t of [_tool0, _tool1]) pi.tools.register(t) } }
@@ -0,0 +1,16 @@
1
+ import { listSkills, findSkill, skillAsUserMessage } from '../../src/skills/index.js'
2
+
3
+ const ACTIONS = {
4
+ list: () => ({ skills: listSkills().map(s => ({ name: s.name, description: s.description, file: s.file })) }),
5
+ get: ({ name }) => { const s = findSkill(name); return s ? { skill: s } : { error: 'not found: ' + name } },
6
+ invoke: ({ name, args = '' }) => {
7
+ const m = skillAsUserMessage(name, args)
8
+ return m ? { message: m } : { error: 'not found: ' + name }
9
+ },
10
+ }
11
+ export const _tool = ({
12
+ name: 'skill_manager',
13
+ toolset: 'core',
14
+ 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'] } },
15
+ handler: async (a) => { const fn = ACTIONS[a.action]; return fn ? fn(a) : { error: 'unknown action' } },
16
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-skill_manager', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,18 @@
1
+ import { db } from '../../src/db.js'
2
+ async function init() {
3
+ const d = await db()
4
+ await 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)`)
5
+ return d
6
+ }
7
+ export const _tool = ({
8
+ name: 'skill_usage',
9
+ toolset: 'core',
10
+ 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'] } },
11
+ handler: async ({ action, name, session_id = null, limit = 20 }) => {
12
+ const d = await init()
13
+ if (action === 'record') { await d.prepare(`INSERT INTO skill_usage (name, ts, session_id) VALUES (?, ?, ?)`).run(name, Date.now(), session_id); return { recorded: true } }
14
+ if (action === 'top') return { top: await d.prepare(`SELECT name, COUNT(*) AS uses FROM skill_usage GROUP BY name ORDER BY uses DESC LIMIT ?`).all(limit) }
15
+ if (action === 'recent') return { recent: await d.prepare(`SELECT name, ts, session_id FROM skill_usage ORDER BY id DESC LIMIT ?`).all(limit) }
16
+ return { error: 'unknown action' }
17
+ },
18
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-skill_usage', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,16 @@
1
+ import { detectSecrets } from '../../src/agent/redact.js'
2
+
3
+ const DANGEROUS = [/rm\s+-rf\s+\//, /:\(\)\s*\{\s*:\|:&\s*\};:/, /chmod\s+-R\s+777/]
4
+
5
+ export const _tool = ({
6
+ name: 'skills_guard',
7
+ toolset: 'core',
8
+ 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'] } },
9
+ handler: async ({ body }) => {
10
+ const issues = []
11
+ for (const re of DANGEROUS) if (re.test(body)) issues.push({ kind: 'dangerous-cmd', pattern: re.source })
12
+ const secrets = detectSecrets(body)
13
+ if (secrets.length) issues.push({ kind: 'secret', count: secrets.length })
14
+ return { safe: issues.length === 0, issues }
15
+ },
16
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-skills_guard', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,29 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { getFreddieHome } from '../../src/home.js'
4
+ const HUB_INDEX_URL = 'https://raw.githubusercontent.com/AnEntrypoint/freddie-skills/main/index.json'
5
+
6
+ const ACTIONS = {
7
+ catalog: async () => {
8
+ try { const r = await fetch(HUB_INDEX_URL); if (!r.ok) return { items: [], error: 'fetch ' + r.status }; return { items: await r.json() } }
9
+ catch (e) { return { items: [], error: String(e.message || e) } }
10
+ },
11
+ install: async ({ name, body }) => {
12
+ if (!name || !body) return { error: 'name + body required' }
13
+ const dir = path.join(getFreddieHome(), 'skills', name)
14
+ fs.mkdirSync(dir, { recursive: true })
15
+ fs.writeFileSync(path.join(dir, 'SKILL.md'), body, 'utf8')
16
+ return { installed: dir }
17
+ },
18
+ uninstall: async ({ name }) => {
19
+ const dir = path.join(getFreddieHome(), 'skills', name)
20
+ if (fs.existsSync(dir)) { fs.rmSync(dir, { recursive: true, force: true }); return { uninstalled: name } }
21
+ return { error: 'not found' }
22
+ },
23
+ }
24
+ export const _tool = ({
25
+ name: 'skills_hub',
26
+ toolset: 'core',
27
+ 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'] } },
28
+ handler: async (a) => { const fn = ACTIONS[a.action]; return fn ? await fn(a) : { error: 'unknown action' } },
29
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-skills_hub', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,12 @@
1
+ import { listSkills } from '../../src/skills/index.js'
2
+ export const _tool = ({
3
+ name: 'skills_index',
4
+ toolset: 'core',
5
+ 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' } } } },
6
+ handler: async ({ query }) => {
7
+ const all = listSkills().map(s => ({ name: s.name, description: s.description, hint: (s.body || '').split('\n').find(l => l.trim()) || '' }))
8
+ if (!query) return { items: all }
9
+ const q = String(query).toLowerCase()
10
+ return { items: all.filter(s => (s.name + ' ' + s.description + ' ' + s.hint).toLowerCase().includes(q)) }
11
+ },
12
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-skills_index', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,17 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { getFreddieHome } from '../../src/home.js'
4
+ export const _tool = ({
5
+ name: 'skills_sync',
6
+ toolset: 'core',
7
+ 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'] } },
8
+ handler: async ({ repo }) => {
9
+ const dir = path.join(getFreddieHome(), 'skills')
10
+ fs.mkdirSync(dir, { recursive: true })
11
+ const { spawnSync } = await import('node:child_process')
12
+ const exists = fs.existsSync(path.join(dir, '.git'))
13
+ const cmd = exists ? ['git', '-C', dir, 'pull'] : ['git', 'clone', repo, dir]
14
+ const r = spawnSync(cmd[0], cmd.slice(1), { encoding: 'utf8' })
15
+ return { exitCode: r.status, stdout: r.stdout, stderr: r.stderr }
16
+ },
17
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-skills_sync', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,9 @@
1
+ import { listSkills, findSkill, skillAsUserMessage } from '../../src/skills/index.js'
2
+ export const _tool = ({
3
+ name: 'skill',
4
+ toolset: 'core',
5
+ 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'] } },
6
+ handler: async ({ name, args = '' }) => {
7
+ const m = skillAsUserMessage(name, args); return m ? { message: m } : { error: 'skill not found: ' + name, available: listSkills().map(s => s.name) }
8
+ },
9
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-skills_tool', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,14 @@
1
+ import { resolveCommand, getCommand } from '../../src/commands/registry.js'
2
+ const DESTRUCTIVE = new Set(['reset', 'clear', 'delete'])
3
+
4
+ export const _tool = ({
5
+ name: 'slash_confirm',
6
+ toolset: 'core',
7
+ 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'] } },
8
+ handler: async ({ input }) => {
9
+ const name = resolveCommand(input)
10
+ if (!name) return { recognised: false, input }
11
+ const def = getCommand(name)
12
+ return { recognised: true, name, requiresConfirm: DESTRUCTIVE.has(name), description: def?.description }
13
+ },
14
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-slash_confirm', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,27 @@
1
+ import { spawn } from 'node:child_process'
2
+ const _sessions = new Map()
3
+
4
+ export const _tool = ({
5
+ name: 'terminal',
6
+ toolset: 'core',
7
+ 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'] } },
8
+ handler: async ({ action, id, input, cwd }) => {
9
+ if (action === 'open') {
10
+ const sid = 'term-' + Date.now()
11
+ const sh = process.platform === 'win32' ? 'cmd' : 'sh'
12
+ const child = spawn(sh, [], { cwd: cwd || process.cwd(), env: process.env })
13
+ const buf = { stdout: '', stderr: '' }
14
+ child.stdout?.on('data', d => buf.stdout += d.toString())
15
+ child.stderr?.on('data', d => buf.stderr += d.toString())
16
+ _sessions.set(sid, { child, buf })
17
+ return { id: sid, opened: true }
18
+ }
19
+ const s = _sessions.get(id)
20
+ if (!s) return { error: 'unknown terminal id: ' + id }
21
+ if (action === 'send') { s.child.stdin?.write(input + '\n'); return { sent: true } }
22
+ if (action === 'read') { const out = { ...s.buf }; s.buf.stdout = ''; s.buf.stderr = ''; return out }
23
+ if (action === 'close') { try { s.child.kill('SIGTERM') } catch {} _sessions.delete(id); return { closed: id } }
24
+ if (action === 'list') return { sessions: [..._sessions.keys()] }
25
+ return { error: 'unknown action' }
26
+ },
27
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-terminal', 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
+ import { getFreddieHome } from '../../src/home.js'
4
+ function policyPath() { return path.join(getFreddieHome(), 'policy.json') }
5
+ function loadPolicy() { try { return JSON.parse(fs.readFileSync(policyPath(), 'utf8')) } catch { return { tools: {}, hosts: { allow: [], deny: [] } } } }
6
+
7
+ export const _tool = ({
8
+ name: 'tirith_security',
9
+ toolset: 'core',
10
+ 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'] } },
11
+ handler: async ({ kind, target }) => {
12
+ const p = loadPolicy()
13
+ if (kind === 'tool') {
14
+ const t = p.tools?.[target]
15
+ if (t === 'allow' || t === 'deny') return { decision: t }
16
+ }
17
+ if (kind === 'host') {
18
+ if (p.hosts?.deny?.some(d => target.includes(d))) return { decision: 'deny' }
19
+ if (p.hosts?.allow?.some(d => target.includes(d))) return { decision: 'allow' }
20
+ }
21
+ return { decision: 'ask' }
22
+ },
23
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-tirith_security', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,52 @@
1
+ import { db } from '../../src/db.js'
2
+ async function init() {
3
+ const d = await db()
4
+ 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)`)
5
+ return d
6
+ }
7
+
8
+ const ACTIONS = {
9
+ add: async ({ session_id = null, content }) => {
10
+ if (!content) return { error: 'content required' }
11
+ const d = await init(); const now = Date.now()
12
+ const info = await d.prepare(`INSERT INTO todos (session_id, content, status, created, updated) VALUES (?, ?, 'pending', ?, ?)`).run(session_id, content, now, now)
13
+ return { id: Number(info.lastInsertRowid), content, status: 'pending' }
14
+ },
15
+ list: async ({ session_id = null }) => {
16
+ const d = await init()
17
+ const rows = session_id ? await d.prepare(`SELECT * FROM todos WHERE session_id = ? ORDER BY id DESC`).all(session_id) : await d.prepare(`SELECT * FROM todos ORDER BY id DESC`).all()
18
+ return { todos: rows }
19
+ },
20
+ update: async ({ id, status }) => {
21
+ if (!id) return { error: 'id required' }
22
+ await (await init()).prepare(`UPDATE todos SET status = ?, updated = ? WHERE id = ?`).run(status, Date.now(), id)
23
+ return { id, status }
24
+ },
25
+ complete: async ({ id }) => ACTIONS.update({ id, status: 'completed' }),
26
+ delete: async ({ id }) => { await (await init()).prepare(`DELETE FROM todos WHERE id = ?`).run(id); return { id, deleted: true } },
27
+ }
28
+
29
+ export const _tool = ({
30
+ name: 'todo',
31
+ toolset: 'core',
32
+ schema: {
33
+ name: 'todo',
34
+ description: 'Manage per-session todos. Actions: add, list, update, complete, delete.',
35
+ parameters: {
36
+ type: 'object',
37
+ properties: {
38
+ action: { type: 'string', enum: Object.keys(ACTIONS) },
39
+ content: { type: 'string' },
40
+ id: { type: 'number' },
41
+ status: { type: 'string' },
42
+ session_id: { type: 'string' },
43
+ },
44
+ required: ['action'],
45
+ },
46
+ },
47
+ handler: async (args) => {
48
+ const fn = ACTIONS[args.action]
49
+ if (!fn) return { error: 'unknown action: ' + args.action }
50
+ return fn(args)
51
+ },
52
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-todo', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,24 @@
1
+ export function shapeArgs(schema, args) {
2
+ if (!schema?.properties) return args
3
+ const out = {}
4
+ for (const [k, def] of Object.entries(schema.properties)) {
5
+ if (k in args) out[k] = args[k]
6
+ else if ('default' in def) out[k] = def.default
7
+ }
8
+ return out
9
+ }
10
+ export function describeTools(filter = null) {
11
+ let list = registry.list()
12
+ if (filter) list = list.filter(t => t.toolset === filter)
13
+ return list.map(t => ({ name: t.name, description: t.schema.description, toolset: t.toolset }))
14
+ }
15
+ export const _tool = ({
16
+ name: 'tool_backend_helpers',
17
+ toolset: 'core',
18
+ 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'] } },
19
+ handler: async ({ action, filter, schema, args }) => {
20
+ if (action === 'describe') return { tools: describeTools(filter) }
21
+ if (action === 'shape') return { args: shapeArgs(schema, args || {}) }
22
+ return { error: 'unknown action' }
23
+ },
24
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-tool_backend_helpers', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -0,0 +1,14 @@
1
+ import { getConfigValue } from '../../src/config.js'
2
+
3
+ export function truncate(s, max = null) {
4
+ const limit = max ?? getConfigValue('tool.output_limit', 100_000)
5
+ const t = String(s)
6
+ if (t.length <= limit) return t
7
+ return t.slice(0, limit) + `\n…[truncated ${t.length - limit} chars]`
8
+ }
9
+ export const _tool = ({
10
+ name: 'tool_output_limits',
11
+ toolset: 'core',
12
+ 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'] } },
13
+ handler: async ({ text, max }) => ({ text: truncate(text, max) }),
14
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-tool_output_limits', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -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) } }