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.
- package/CHANGELOG.md +6 -0
- package/package.json +2 -1
- package/plugins/ansi_strip/handler.js +7 -0
- package/plugins/ansi_strip/plugin.js +2 -0
- package/plugins/approval/handler.js +13 -0
- package/plugins/approval/plugin.js +2 -0
- package/plugins/bash/handler.js +33 -0
- package/plugins/bash/plugin.js +2 -0
- package/plugins/binary_extensions/handler.js +20 -0
- package/plugins/binary_extensions/plugin.js +2 -0
- package/plugins/browser/handler.js +46 -0
- package/plugins/browser/plugin.js +2 -0
- package/plugins/budget_config/handler.js +12 -0
- package/plugins/budget_config/plugin.js +2 -0
- package/plugins/checkpoint/handler.js +27 -0
- package/plugins/checkpoint/plugin.js +2 -0
- package/plugins/clarify/handler.js +13 -0
- package/plugins/clarify/plugin.js +2 -0
- package/plugins/code_execution/handler.js +25 -0
- package/plugins/code_execution/plugin.js +2 -0
- package/plugins/core-agent-machine/plugin.js +8 -0
- package/plugins/core-cli/plugin.js +83 -0
- package/plugins/core-commands/plugin.js +7 -0
- package/plugins/core-compressor/plugin.js +15 -0
- package/plugins/core-context-engine/plugin.js +7 -0
- package/plugins/core-cron/plugin.js +7 -0
- package/plugins/core-skills/plugin.js +7 -0
- package/plugins/credential_files/handler.js +14 -0
- package/plugins/credential_files/plugin.js +2 -0
- package/plugins/cronjob/handler.js +14 -0
- package/plugins/cronjob/plugin.js +2 -0
- package/plugins/debug_helpers/handler.js +8 -0
- package/plugins/debug_helpers/plugin.js +2 -0
- package/plugins/delegate/handler.js +27 -0
- package/plugins/delegate/plugin.js +2 -0
- package/plugins/discord_tool/handler.js +12 -0
- package/plugins/discord_tool/plugin.js +2 -0
- package/plugins/edit/handler.js +29 -0
- package/plugins/edit/plugin.js +2 -0
- package/plugins/env_passthrough/handler.js +14 -0
- package/plugins/env_passthrough/plugin.js +2 -0
- package/plugins/feishu_doc/handler.js +14 -0
- package/plugins/feishu_doc/plugin.js +2 -0
- package/plugins/feishu_drive/handler.js +13 -0
- package/plugins/feishu_drive/plugin.js +2 -0
- package/plugins/file_operations/handler.js +15 -0
- package/plugins/file_operations/plugin.js +2 -0
- package/plugins/file_state/handler.js +14 -0
- package/plugins/file_state/plugin.js +2 -0
- package/plugins/file_tools/handler.js +21 -0
- package/plugins/file_tools/plugin.js +2 -0
- package/plugins/fuzzy_match/handler.js +7 -0
- package/plugins/fuzzy_match/plugin.js +2 -0
- package/plugins/gm-cc/plugin.js +28 -0
- package/plugins/grep/handler.js +49 -0
- package/plugins/grep/plugin.js +2 -0
- package/plugins/gui-agents/plugin.js +26 -0
- package/plugins/gui-batch/plugin.js +11 -0
- package/plugins/gui-chat/plugin.js +22 -0
- package/plugins/gui-config/plugin.js +12 -0
- package/plugins/gui-cron/plugin.js +13 -0
- package/plugins/gui-debug/plugin.js +24 -0
- package/plugins/gui-env/plugin.js +7 -0
- package/plugins/gui-gateway/plugin.js +9 -0
- package/plugins/gui-profiles-commands-health/plugin.js +11 -0
- package/plugins/gui-sessions/plugin.js +9 -0
- package/plugins/gui-skills/plugin.js +8 -0
- package/plugins/gui-tools/plugin.js +7 -0
- package/plugins/homeassistant_tool/handler.js +14 -0
- package/plugins/homeassistant_tool/plugin.js +2 -0
- package/plugins/image_gen/handler.js +31 -0
- package/plugins/image_gen/plugin.js +2 -0
- package/plugins/interrupt/handler.js +16 -0
- package/plugins/interrupt/plugin.js +2 -0
- package/plugins/managed_tool_gateway/handler.js +9 -0
- package/plugins/managed_tool_gateway/plugin.js +2 -0
- package/plugins/mcp_oauth/handler.js +20 -0
- package/plugins/mcp_oauth/plugin.js +2 -0
- package/plugins/mcp_oauth_manager/handler.js +18 -0
- package/plugins/mcp_oauth_manager/plugin.js +2 -0
- package/plugins/mcp_tool/handler.js +34 -0
- package/plugins/mcp_tool/plugin.js +2 -0
- package/plugins/memory/handler.js +66 -0
- package/plugins/memory/plugin.js +2 -0
- package/plugins/memory-byterover/handler.js +25 -0
- package/plugins/memory-byterover/plugin.js +2 -0
- package/plugins/memory-hindsight/handler.js +25 -0
- package/plugins/memory-hindsight/plugin.js +2 -0
- package/plugins/memory-holographic/handler.js +31 -0
- package/plugins/memory-holographic/plugin.js +2 -0
- package/plugins/memory-honcho/handler.js +25 -0
- package/plugins/memory-honcho/plugin.js +2 -0
- package/plugins/memory-mem0/handler.js +25 -0
- package/plugins/memory-mem0/plugin.js +2 -0
- package/plugins/memory-openviking/handler.js +25 -0
- package/plugins/memory-openviking/plugin.js +2 -0
- package/plugins/memory-retaindb/handler.js +25 -0
- package/plugins/memory-retaindb/plugin.js +2 -0
- package/plugins/memory-supermemory/handler.js +25 -0
- package/plugins/memory-supermemory/plugin.js +2 -0
- package/plugins/mixture_of_agents/handler.js +13 -0
- package/plugins/mixture_of_agents/plugin.js +2 -0
- package/plugins/neutts_synth/handler.js +12 -0
- package/plugins/neutts_synth/plugin.js +2 -0
- package/plugins/openrouter_client/handler.js +12 -0
- package/plugins/openrouter_client/plugin.js +2 -0
- package/plugins/osv_check/handler.js +10 -0
- package/plugins/osv_check/plugin.js +2 -0
- package/plugins/patch_parser/handler.js +40 -0
- package/plugins/patch_parser/plugin.js +2 -0
- package/plugins/path_security/handler.js +14 -0
- package/plugins/path_security/plugin.js +2 -0
- package/plugins/platform-api_server/handler.js +21 -0
- package/plugins/platform-api_server/plugin.js +2 -0
- package/plugins/platform-bluebubbles/handler.js +32 -0
- package/plugins/platform-bluebubbles/plugin.js +2 -0
- package/plugins/platform-dingtalk/handler.js +32 -0
- package/plugins/platform-dingtalk/plugin.js +2 -0
- package/plugins/platform-discord/handler.js +24 -0
- package/plugins/platform-discord/plugin.js +2 -0
- package/plugins/platform-email/handler.js +51 -0
- package/plugins/platform-email/plugin.js +2 -0
- package/plugins/platform-feishu/handler.js +32 -0
- package/plugins/platform-feishu/plugin.js +2 -0
- package/plugins/platform-feishu_comment/handler.js +12 -0
- package/plugins/platform-feishu_comment/plugin.js +2 -0
- package/plugins/platform-feishu_comment_rules/handler.js +11 -0
- package/plugins/platform-feishu_comment_rules/plugin.js +2 -0
- package/plugins/platform-homeassistant/handler.js +32 -0
- package/plugins/platform-homeassistant/plugin.js +2 -0
- package/plugins/platform-matrix/handler.js +40 -0
- package/plugins/platform-matrix/plugin.js +2 -0
- package/plugins/platform-mattermost/handler.js +29 -0
- package/plugins/platform-mattermost/plugin.js +2 -0
- package/plugins/platform-qqbot/handler.js +32 -0
- package/plugins/platform-qqbot/plugin.js +2 -0
- package/plugins/platform-signal/handler.js +33 -0
- package/plugins/platform-signal/plugin.js +2 -0
- package/plugins/platform-slack/handler.js +34 -0
- package/plugins/platform-slack/plugin.js +2 -0
- package/plugins/platform-sms/handler.js +34 -0
- package/plugins/platform-sms/plugin.js +2 -0
- package/plugins/platform-telegram/handler.js +38 -0
- package/plugins/platform-telegram/plugin.js +2 -0
- package/plugins/platform-telegram_network/handler.js +17 -0
- package/plugins/platform-telegram_network/plugin.js +2 -0
- package/plugins/platform-webhook/handler.js +19 -0
- package/plugins/platform-webhook/plugin.js +2 -0
- package/plugins/platform-wecom/handler.js +32 -0
- package/plugins/platform-wecom/plugin.js +2 -0
- package/plugins/platform-wecom_callback/handler.js +15 -0
- package/plugins/platform-wecom_callback/plugin.js +2 -0
- package/plugins/platform-wecom_crypto/handler.js +16 -0
- package/plugins/platform-wecom_crypto/plugin.js +2 -0
- package/plugins/platform-weixin/handler.js +32 -0
- package/plugins/platform-weixin/plugin.js +2 -0
- package/plugins/platform-whatsapp/handler.js +40 -0
- package/plugins/platform-whatsapp/plugin.js +2 -0
- package/plugins/platform-yuanbao/handler.js +9 -0
- package/plugins/platform-yuanbao/plugin.js +2 -0
- package/plugins/platform-yuanbao_media/handler.js +5 -0
- package/plugins/platform-yuanbao_media/plugin.js +2 -0
- package/plugins/platform-yuanbao_proto/handler.js +9 -0
- package/plugins/platform-yuanbao_proto/plugin.js +2 -0
- package/plugins/platform-yuanbao_sticker/handler.js +6 -0
- package/plugins/platform-yuanbao_sticker/plugin.js +2 -0
- package/plugins/process_registry/handler.js +15 -0
- package/plugins/process_registry/plugin.js +2 -0
- package/plugins/read/handler.js +24 -0
- package/plugins/read/plugin.js +2 -0
- package/plugins/rl_training/handler.js +12 -0
- package/plugins/rl_training/plugin.js +2 -0
- package/plugins/schema_sanitizer/handler.js +17 -0
- package/plugins/schema_sanitizer/plugin.js +2 -0
- package/plugins/send_message/handler.js +30 -0
- package/plugins/send_message/plugin.js +2 -0
- package/plugins/session_search/handler.js +21 -0
- package/plugins/session_search/plugin.js +2 -0
- package/plugins/skill_manager/handler.js +16 -0
- package/plugins/skill_manager/plugin.js +2 -0
- package/plugins/skill_usage/handler.js +18 -0
- package/plugins/skill_usage/plugin.js +2 -0
- package/plugins/skills_guard/handler.js +16 -0
- package/plugins/skills_guard/plugin.js +2 -0
- package/plugins/skills_hub/handler.js +29 -0
- package/plugins/skills_hub/plugin.js +2 -0
- package/plugins/skills_index/handler.js +12 -0
- package/plugins/skills_index/plugin.js +2 -0
- package/plugins/skills_sync/handler.js +17 -0
- package/plugins/skills_sync/plugin.js +2 -0
- package/plugins/skills_tool/handler.js +9 -0
- package/plugins/skills_tool/plugin.js +2 -0
- package/plugins/slash_confirm/handler.js +14 -0
- package/plugins/slash_confirm/plugin.js +2 -0
- package/plugins/terminal/handler.js +27 -0
- package/plugins/terminal/plugin.js +2 -0
- package/plugins/tirith_security/handler.js +23 -0
- package/plugins/tirith_security/plugin.js +2 -0
- package/plugins/todo/handler.js +52 -0
- package/plugins/todo/plugin.js +2 -0
- package/plugins/tool_backend_helpers/handler.js +24 -0
- package/plugins/tool_backend_helpers/plugin.js +2 -0
- package/plugins/tool_output_limits/handler.js +14 -0
- package/plugins/tool_output_limits/plugin.js +2 -0
- package/plugins/tool_result_storage/handler.js +18 -0
- package/plugins/tool_result_storage/plugin.js +2 -0
- package/plugins/transcription/handler.js +18 -0
- package/plugins/transcription/plugin.js +2 -0
- package/plugins/tts/handler.js +18 -0
- package/plugins/tts/plugin.js +2 -0
- package/plugins/url_safety/handler.js +14 -0
- package/plugins/url_safety/plugin.js +2 -0
- package/plugins/vision/handler.js +17 -0
- package/plugins/vision/plugin.js +2 -0
- package/plugins/voice_mode/handler.js +9 -0
- package/plugins/voice_mode/plugin.js +2 -0
- package/plugins/web_search/handler.js +35 -0
- package/plugins/web_search/plugin.js +2 -0
- package/plugins/web_tools/handler.js +17 -0
- package/plugins/web_tools/plugin.js +2 -0
- package/plugins/website_policy/handler.js +13 -0
- package/plugins/website_policy/plugin.js +2 -0
- package/plugins/write/handler.js +23 -0
- package/plugins/write/plugin.js +2 -0
- package/plugins/xai_http/handler.js +12 -0
- package/plugins/xai_http/plugin.js +2 -0
- package/plugins/yuanbao_tools/handler.js +12 -0
- package/plugins/yuanbao_tools/plugin.js +2 -0
- package/src/agent/llm_resolver.js +2 -1
- 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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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
|
+
})
|