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.
- package/AGENTS.md +12 -0
- package/CHANGELOG.md +26 -2
- package/package.json +3 -2
- 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 +21 -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/sessions.js +2 -1
- package/src/web/app.js +17 -0
- package/src/web/index.html +7 -3
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getConfigValue } from '../../src/config.js'
|
|
2
|
+
|
|
3
|
+
export function buildBashEnv() {
|
|
4
|
+
const allow = getConfigValue('terminal.env_passthrough', ['HOME', 'USER', 'LANG', 'PATH', 'SHELL']) || []
|
|
5
|
+
const out = {}
|
|
6
|
+
for (const k of allow) if (process.env[k]) out[k] = process.env[k]
|
|
7
|
+
return out
|
|
8
|
+
}
|
|
9
|
+
export const _tool = ({
|
|
10
|
+
name: 'env_passthrough',
|
|
11
|
+
toolset: 'core',
|
|
12
|
+
schema: { name: 'env_passthrough', description: 'Compute the env-var subset that should be passed through to spawned shells.', parameters: { type: 'object', properties: {} } },
|
|
13
|
+
handler: async () => ({ env: buildBashEnv() }),
|
|
14
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const _tool = ({
|
|
2
|
+
name: 'feishu_doc',
|
|
3
|
+
toolset: 'core',
|
|
4
|
+
schema: { name: 'feishu_doc', description: 'Read or update a Feishu doc by token.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['get', 'patch'] }, doc_token: { type: 'string' }, content: {} }, required: ['action', 'doc_token'] } },
|
|
5
|
+
requiresEnv: ['FEISHU_APP_TOKEN'],
|
|
6
|
+
checkFn: () => Boolean(process.env.FEISHU_APP_TOKEN),
|
|
7
|
+
handler: async ({ action, doc_token, content }) => {
|
|
8
|
+
const auth = { authorization: `Bearer ${process.env.FEISHU_APP_TOKEN}` }
|
|
9
|
+
const base = 'https://open.feishu.cn/open-apis/docx/v1/documents/' + doc_token
|
|
10
|
+
if (action === 'get') return await (await fetch(base, { headers: auth })).json()
|
|
11
|
+
if (action === 'patch') return await (await fetch(base + '/blocks', { method: 'PATCH', headers: { ...auth, 'content-type': 'application/json' }, body: JSON.stringify(content) })).json()
|
|
12
|
+
return { error: 'unknown action' }
|
|
13
|
+
},
|
|
14
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const _tool = ({
|
|
2
|
+
name: 'feishu_drive',
|
|
3
|
+
toolset: 'core',
|
|
4
|
+
schema: { name: 'feishu_drive', description: 'List or download Feishu Drive files.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'download'] }, folder_token: { type: 'string' }, file_token: { type: 'string' } }, required: ['action'] } },
|
|
5
|
+
requiresEnv: ['FEISHU_APP_TOKEN'],
|
|
6
|
+
checkFn: () => Boolean(process.env.FEISHU_APP_TOKEN),
|
|
7
|
+
handler: async ({ action, folder_token, file_token }) => {
|
|
8
|
+
const auth = { authorization: `Bearer ${process.env.FEISHU_APP_TOKEN}` }
|
|
9
|
+
if (action === 'list') return await (await fetch('https://open.feishu.cn/open-apis/drive/v1/files?folder_token=' + (folder_token || ''), { headers: auth })).json()
|
|
10
|
+
if (action === 'download') return await (await fetch(`https://open.feishu.cn/open-apis/drive/v1/files/${file_token}/download`, { headers: auth })).json()
|
|
11
|
+
return { error: 'unknown action' }
|
|
12
|
+
},
|
|
13
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
const ACTIONS = {
|
|
4
|
+
move: ({ src, dest }) => { fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.renameSync(src, dest); return { moved: dest } },
|
|
5
|
+
copy: ({ src, dest }) => { fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.copyFileSync(src, dest); return { copied: dest } },
|
|
6
|
+
delete: ({ path: p }) => { if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true }); return { deleted: p } },
|
|
7
|
+
mkdir: ({ path: p }) => { fs.mkdirSync(p, { recursive: true }); return { created: p } },
|
|
8
|
+
stat: ({ path: p }) => { const s = fs.statSync(p); return { size: s.size, mtime: s.mtimeMs, isDir: s.isDirectory() } },
|
|
9
|
+
}
|
|
10
|
+
export const _tool = ({
|
|
11
|
+
name: 'file_operations',
|
|
12
|
+
toolset: 'core',
|
|
13
|
+
schema: { name: 'file_operations', description: 'move/copy/delete/mkdir/stat — atomic file ops.', parameters: { type: 'object', properties: { action: { type: 'string', enum: Object.keys(ACTIONS) }, src: { type: 'string' }, dest: { type: 'string' }, path: { type: 'string' } }, required: ['action'] } },
|
|
14
|
+
handler: async (a) => { const fn = ACTIONS[a.action]; try { return fn ? fn(a) : { error: 'unknown action' } } catch (e) { return { error: String(e.message || e) } } },
|
|
15
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { db } from '../../src/db.js'
|
|
2
|
+
async function init() { const d = await db(); await d.exec(`CREATE TABLE IF NOT EXISTS file_state (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, file_path TEXT NOT NULL, action TEXT NOT NULL, ts INTEGER NOT NULL)`); return d }
|
|
3
|
+
export const _tool = ({
|
|
4
|
+
name: 'file_state',
|
|
5
|
+
toolset: 'core',
|
|
6
|
+
schema: { name: 'file_state', description: 'Track files modified in this session (read|write|edit|delete) for diff-summary purposes.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['record', 'list', 'changed_in_session'] }, session_id: { type: 'string' }, file_path: { type: 'string' }, op: { type: 'string' } }, required: ['action'] } },
|
|
7
|
+
handler: async ({ action, session_id, file_path, op }) => {
|
|
8
|
+
const d = await init()
|
|
9
|
+
if (action === 'record') { await d.prepare('INSERT INTO file_state (session_id, file_path, action, ts) VALUES (?, ?, ?, ?)').run(session_id, file_path, op, Date.now()); return { recorded: true } }
|
|
10
|
+
if (action === 'list') return { items: await d.prepare('SELECT * FROM file_state WHERE session_id = ? ORDER BY id DESC').all(session_id) }
|
|
11
|
+
if (action === 'changed_in_session') return { files: [...new Set((await d.prepare('SELECT file_path FROM file_state WHERE session_id = ?').all(session_id)).map(r => r.file_path))] }
|
|
12
|
+
return { error: 'unknown action' }
|
|
13
|
+
},
|
|
14
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
function walk(dir, out, skip) {
|
|
4
|
+
let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
5
|
+
for (const e of entries) {
|
|
6
|
+
if (skip.has(e.name)) continue
|
|
7
|
+
const full = path.join(dir, e.name)
|
|
8
|
+
if (e.isDirectory()) walk(full, out, skip)
|
|
9
|
+
else out.push(full)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export const _tool = ({
|
|
13
|
+
name: 'file_tools',
|
|
14
|
+
toolset: 'core',
|
|
15
|
+
schema: { name: 'file_tools', description: 'list/glob files (recursive walk skipping node_modules, .git, dist).', parameters: { type: 'object', properties: { dir: { type: 'string', default: '.' }, ext: { type: 'string' }, limit: { type: 'number', default: 1000 } } } },
|
|
16
|
+
handler: async ({ dir = '.', ext, limit = 1000 }) => {
|
|
17
|
+
const out = []; walk(dir, out, new Set(['node_modules', '.git', 'dist', '.cache', 'build']))
|
|
18
|
+
const filtered = ext ? out.filter(f => f.endsWith(ext)) : out
|
|
19
|
+
return { files: filtered.slice(0, limit), total: filtered.length, truncated: filtered.length > limit }
|
|
20
|
+
},
|
|
21
|
+
})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { fuzzyMatch as helper } from '../../src/utils.js'
|
|
2
|
+
export const _tool = ({
|
|
3
|
+
name: 'fuzzy_match',
|
|
4
|
+
toolset: 'core',
|
|
5
|
+
schema: { name: 'fuzzy_match', description: 'Score a candidate string against a needle. Returns 0 for no match, higher for better match.', parameters: { type: 'object', properties: { needle: { type: 'string' }, haystack: { type: 'string' } }, required: ['needle', 'haystack'] } },
|
|
6
|
+
handler: async ({ needle, haystack }) => ({ score: helper(needle, haystack) }),
|
|
7
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
|
|
6
|
+
const _require = createRequire(import.meta.url);
|
|
7
|
+
const gmCcBase = path.dirname(_require.resolve('gm-cc/package.json'));
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
name: 'gm-cc',
|
|
11
|
+
surfaces: 'pi',
|
|
12
|
+
register({ pi }) {
|
|
13
|
+
const skillsDir = path.join(gmCcBase, 'skills');
|
|
14
|
+
if (!fs.existsSync(skillsDir)) return;
|
|
15
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
16
|
+
const skillMd = entry.isDirectory()
|
|
17
|
+
? path.join(skillsDir, entry.name, 'SKILL.md')
|
|
18
|
+
: entry.name.endsWith('.md') ? path.join(skillsDir, entry.name) : null;
|
|
19
|
+
if (!skillMd || !fs.existsSync(skillMd)) continue;
|
|
20
|
+
const raw = fs.readFileSync(skillMd, 'utf8');
|
|
21
|
+
const nameMatch = raw.match(/^name:\s*(.+)$/m);
|
|
22
|
+
const descMatch = raw.match(/^description:\s*(.+)$/m);
|
|
23
|
+
const name = nameMatch ? nameMatch[1].trim() : entry.name.replace(/\.md$/, '');
|
|
24
|
+
const description = descMatch ? descMatch[1].trim() : '';
|
|
25
|
+
pi.skills.register({ name: 'gm:' + name, description, content: raw, source: 'gm-cc' });
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
export const _tool = ({
|
|
4
|
+
name: 'grep',
|
|
5
|
+
toolset: 'core',
|
|
6
|
+
schema: {
|
|
7
|
+
name: 'grep',
|
|
8
|
+
description: 'Recursive regex search across files. Returns file:line:content matches.',
|
|
9
|
+
parameters: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
pattern: { type: 'string' },
|
|
13
|
+
path: { type: 'string', default: '.' },
|
|
14
|
+
glob: { type: 'string' },
|
|
15
|
+
head_limit: { type: 'number', default: 200 },
|
|
16
|
+
ignore_case: { type: 'boolean', default: false },
|
|
17
|
+
},
|
|
18
|
+
required: ['pattern'],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
handler: async ({ pattern, path: root = '.', head_limit = 200, ignore_case = false, glob }) => {
|
|
22
|
+
const re = new RegExp(pattern, ignore_case ? 'i' : '')
|
|
23
|
+
const out = []
|
|
24
|
+
const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', '.cache'])
|
|
25
|
+
const walk = (d) => {
|
|
26
|
+
if (out.length >= head_limit) return
|
|
27
|
+
let entries
|
|
28
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }) } catch { return }
|
|
29
|
+
for (const e of entries) {
|
|
30
|
+
if (out.length >= head_limit) return
|
|
31
|
+
const full = path.join(d, e.name)
|
|
32
|
+
if (e.isDirectory()) { if (!skipDirs.has(e.name)) walk(full); continue }
|
|
33
|
+
if (glob && !matchGlob(e.name, glob)) continue
|
|
34
|
+
let content
|
|
35
|
+
try { content = fs.readFileSync(full, 'utf8') } catch { continue }
|
|
36
|
+
content.split('\n').forEach((line, i) => {
|
|
37
|
+
if (out.length < head_limit && re.test(line)) out.push(`${full}:${i + 1}:${line.slice(0, 200)}`)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
walk(root)
|
|
42
|
+
return { matches: out, total: out.length, truncated: out.length >= head_limit }
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function matchGlob(name, glob) {
|
|
47
|
+
const re = new RegExp('^' + glob.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i')
|
|
48
|
+
return re.test(name)
|
|
49
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { bootHost } from '../../src/host/index.js'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'gui-agents', surfaces: 'gui',
|
|
5
|
+
register({ gui }) {
|
|
6
|
+
gui.route('GET', '/api/agents', async (req, res) => {
|
|
7
|
+
try {
|
|
8
|
+
const host = await bootHost()
|
|
9
|
+
const sessions = await (await import('../../src/sessions.js')).listSessions()
|
|
10
|
+
const activeSessions = sessions.filter(s => {
|
|
11
|
+
const updated = new Date(s.updated_at || 0)
|
|
12
|
+
const now = new Date()
|
|
13
|
+
return (now - updated) < 300000
|
|
14
|
+
})
|
|
15
|
+
res.json({
|
|
16
|
+
count: activeSessions.length,
|
|
17
|
+
active: activeSessions.length > 0 ? activeSessions[0].id : null,
|
|
18
|
+
turns: sessions.reduce((acc, s) => acc + (s.turn_count || 0), 0),
|
|
19
|
+
last_activity: activeSessions.length > 0 ? activeSessions[0].updated_at : null,
|
|
20
|
+
})
|
|
21
|
+
} catch (e) {
|
|
22
|
+
res.status(500).json({ error: String(e.message || e) })
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
},
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { runBatch } from '../../src/batch.js'
|
|
2
|
+
export default {
|
|
3
|
+
name: 'gui-batch', surfaces: 'gui',
|
|
4
|
+
register({ gui }) {
|
|
5
|
+
gui.route('POST', '/api/batch', async (req, res) => {
|
|
6
|
+
const { prompts = [], concurrency = 4, model = '' } = req.body || {}
|
|
7
|
+
if (!prompts.length) return res.status(400).json({ error: 'prompts required' })
|
|
8
|
+
try { res.json(await runBatch({ prompts, concurrency, model })) } catch (e) { res.status(500).json({ error: String(e.message || e) }) }
|
|
9
|
+
})
|
|
10
|
+
},
|
|
11
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { runTurn } from '../../src/agent/machine.js'
|
|
2
|
+
export default {
|
|
3
|
+
name: 'gui-chat', surfaces: 'gui',
|
|
4
|
+
register({ gui }) {
|
|
5
|
+
gui.route('POST', '/api/chat', async (req, res) => {
|
|
6
|
+
const { prompt, sessionId = null } = req.body || {}
|
|
7
|
+
if (!prompt) return res.status(400).json({ error: 'prompt required' })
|
|
8
|
+
res.setHeader('Content-Type', 'text/event-stream')
|
|
9
|
+
res.setHeader('Cache-Control', 'no-cache')
|
|
10
|
+
res.setHeader('Connection', 'keep-alive')
|
|
11
|
+
const send = (event, data) => res.write('event: ' + event + '\ndata: ' + JSON.stringify(data) + '\n\n')
|
|
12
|
+
send('start', { ts: Date.now(), sessionId })
|
|
13
|
+
try {
|
|
14
|
+
const out = await runTurn({ prompt, timeoutMs: 30000 })
|
|
15
|
+
for (const m of out.messages) send('message', m)
|
|
16
|
+
send('done', { result: out.result || '', iterations: out.iterations })
|
|
17
|
+
} catch (e) { send('error', { error: String(e.message || e) }) }
|
|
18
|
+
res.end()
|
|
19
|
+
})
|
|
20
|
+
},
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { loadConfig, saveConfigValue } from '../../src/config.js'
|
|
2
|
+
export default {
|
|
3
|
+
name: 'gui-config', surfaces: 'gui',
|
|
4
|
+
register({ gui }) {
|
|
5
|
+
gui.route('GET', '/api/config', (_, res) => res.json(loadConfig()))
|
|
6
|
+
gui.route('POST', '/api/config', (req, res) => {
|
|
7
|
+
const { key, value } = req.body || {}
|
|
8
|
+
if (!key) return res.status(400).json({ error: 'key required' })
|
|
9
|
+
try { res.json(saveConfigValue(key, value)) } catch (e) { res.status(400).json({ error: String(e.message || e) }) }
|
|
10
|
+
})
|
|
11
|
+
},
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { listJobs, createJob, deleteJob } from '../../src/cron/scheduler.js'
|
|
2
|
+
export default {
|
|
3
|
+
name: 'gui-cron', surfaces: 'gui',
|
|
4
|
+
register({ gui }) {
|
|
5
|
+
gui.route('GET', '/api/cron', async (_, res) => res.json(await listJobs()))
|
|
6
|
+
gui.route('POST', '/api/cron', async (req, res) => {
|
|
7
|
+
const { cron, prompt, model = null } = req.body || {}
|
|
8
|
+
if (!cron || !prompt) return res.status(400).json({ error: 'cron and prompt required' })
|
|
9
|
+
try { res.json({ id: await createJob({ cron, prompt, model }) }) } catch (e) { res.status(400).json({ error: String(e.message || e) }) }
|
|
10
|
+
})
|
|
11
|
+
gui.route('DELETE', '/api/cron/:id', async (req, res) => { await deleteJob(Number(req.params.id)); res.json({ ok: true }) })
|
|
12
|
+
},
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import { listDebug, snapshotAll, attachDebugRoutes } from '../../src/observability/debug.js'
|
|
4
|
+
import { getFreddieHome } from '../../src/home.js'
|
|
5
|
+
export default {
|
|
6
|
+
name: 'gui-debug', surfaces: 'gui',
|
|
7
|
+
register({ gui }) {
|
|
8
|
+
gui.route('GET', '/api/debug', (_, res) => res.json(listDebug()))
|
|
9
|
+
gui.route('GET', '/api/debug-all', (_, res) => res.json(snapshotAll()))
|
|
10
|
+
gui.route('GET', '/api/logs', (_, res) => {
|
|
11
|
+
const dir = path.join(getFreddieHome(), 'logs')
|
|
12
|
+
if (!fs.existsSync(dir)) return res.json([])
|
|
13
|
+
res.json(fs.readdirSync(dir).filter(f => f.endsWith('.log')).map(f => f.replace(/\.log$/, '')))
|
|
14
|
+
})
|
|
15
|
+
gui.route('GET', '/api/logs/:subsystem', (req, res) => {
|
|
16
|
+
const file = path.join(getFreddieHome(), 'logs', req.params.subsystem + '.log')
|
|
17
|
+
if (!fs.existsSync(file)) return res.json([])
|
|
18
|
+
const max = Number(req.query.max) || 200
|
|
19
|
+
const lines = fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).slice(-max)
|
|
20
|
+
res.json(lines.map(l => { try { return JSON.parse(l) } catch { return { raw: l } } }))
|
|
21
|
+
})
|
|
22
|
+
gui.api('debug', { attach: attachDebugRoutes })
|
|
23
|
+
},
|
|
24
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const ENV_KEYS = ['ANTHROPIC_API_KEY','OPENAI_API_KEY','GROQ_API_KEY','OPENROUTER_API_KEY','TELEGRAM_BOT_TOKEN','DISCORD_BOT_TOKEN','SLACK_BOT_TOKEN','SLACK_SIGNING_SECRET','WHATSAPP_API_TOKEN','SIGNAL_CLI_URL','MATRIX_HOMESERVER','MATTERMOST_URL','HONCHO_API_KEY','MEM0_API_KEY','SUPERMEMORY_API_KEY','BYTEROVER_API_KEY','HINDSIGHT_API_KEY','OPENVIKING_API_KEY','RETAINDB_API_KEY','SERPAPI_KEY','REPLICATE_API_TOKEN','SMTP_HOST','TWILIO_SID','HASS_TOKEN']
|
|
2
|
+
export default {
|
|
3
|
+
name: 'gui-env', surfaces: 'gui',
|
|
4
|
+
register({ gui }) {
|
|
5
|
+
gui.route('GET', '/api/env', (_, res) => res.json(ENV_KEYS.map(k => ({ key: k, set: !!process.env[k] }))))
|
|
6
|
+
},
|
|
7
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
name: 'gui-gateway', surfaces: 'gui',
|
|
3
|
+
register({ gui, host }) {
|
|
4
|
+
gui.route('GET', '/api/gateway', (_, res) => {
|
|
5
|
+
const platforms = host.pi.platforms.list().map(p => p.name)
|
|
6
|
+
res.json({ platforms: platforms.map(p => ({ name: p, enabled: false, note: 'start with freddie gateway --port <port>' })) })
|
|
7
|
+
})
|
|
8
|
+
},
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { listAllProfiles } from '../../src/commands/profile.js'
|
|
2
|
+
import { COMMAND_REGISTRY } from '../../src/commands/registry.js'
|
|
3
|
+
import { getFreddieHome } from '../../src/home.js'
|
|
4
|
+
export default {
|
|
5
|
+
name: 'gui-profiles-commands-health', surfaces: 'gui',
|
|
6
|
+
register({ gui }) {
|
|
7
|
+
gui.route('GET', '/api/profiles', (_, res) => res.json(listAllProfiles()))
|
|
8
|
+
gui.route('GET', '/api/commands', (_, res) => res.json(COMMAND_REGISTRY))
|
|
9
|
+
gui.route('GET', '/api/health', (_, res) => res.json({ ok: true, ts: Date.now(), freddie_home: getFreddieHome() }))
|
|
10
|
+
},
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { listSessions, search, getMessages } from '../../src/sessions.js'
|
|
2
|
+
export default {
|
|
3
|
+
name: 'gui-sessions', surfaces: 'gui',
|
|
4
|
+
register({ gui }) {
|
|
5
|
+
gui.route('GET', '/api/sessions', async (_, res) => res.json(await listSessions()))
|
|
6
|
+
gui.route('GET', '/api/sessions/:id/messages', async (req, res) => res.json(await getMessages(req.params.id)))
|
|
7
|
+
gui.route('GET', '/api/search', async (req, res) => res.json(await search(String(req.query.q || ''))))
|
|
8
|
+
},
|
|
9
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { listSkills } from '../../src/skills/index.js'
|
|
3
|
+
export default {
|
|
4
|
+
name: 'gui-skills', surfaces: 'gui',
|
|
5
|
+
register({ gui }) {
|
|
6
|
+
gui.route('GET', '/api/skills', (_, res) => res.json({ home: listSkills(), bundled: listSkills([path.resolve('skills')]) }))
|
|
7
|
+
},
|
|
8
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
name: 'gui-tools', surfaces: 'gui',
|
|
3
|
+
register({ gui, host }) {
|
|
4
|
+
gui.route('GET', '/api/tools', (_, res) => res.json(host.pi.tools.list().map(t => ({ name: t.name, toolset: t.toolset, schema: t.schema }))))
|
|
5
|
+
gui.route('GET', '/api/tools/detail', (_, res) => res.json(host.pi.tools.list().map(t => ({ name: t.name, toolset: t.toolset, description: t.schema?.description || '' }))))
|
|
6
|
+
},
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const _tool = ({
|
|
2
|
+
name: 'homeassistant_tool',
|
|
3
|
+
toolset: 'core',
|
|
4
|
+
schema: { name: 'homeassistant_tool', description: 'Read state or call a service on Home Assistant.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['state', 'service'] }, entity_id: { type: 'string' }, domain: { type: 'string' }, service: { type: 'string' }, data: {} }, required: ['action'] } },
|
|
5
|
+
requiresEnv: ['HASS_TOKEN', 'HASS_URL'],
|
|
6
|
+
checkFn: () => Boolean(process.env.HASS_TOKEN),
|
|
7
|
+
handler: async ({ action, entity_id, domain, service, data = {} }) => {
|
|
8
|
+
const url = process.env.HASS_URL || 'http://homeassistant.local:8123'
|
|
9
|
+
const auth = { authorization: `Bearer ${process.env.HASS_TOKEN}` }
|
|
10
|
+
if (action === 'state') return await (await fetch(`${url}/api/states/${entity_id}`, { headers: auth })).json()
|
|
11
|
+
if (action === 'service') return await (await fetch(`${url}/api/services/${domain}/${service}`, { method: 'POST', headers: { ...auth, 'content-type': 'application/json' }, body: JSON.stringify({ entity_id, ...data }) })).json()
|
|
12
|
+
return { error: 'unknown action' }
|
|
13
|
+
},
|
|
14
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const _tool = ({
|
|
2
|
+
name: 'image_gen',
|
|
3
|
+
toolset: 'creative',
|
|
4
|
+
schema: {
|
|
5
|
+
name: 'image_gen',
|
|
6
|
+
description: 'Generate an image from a prompt. Provider via config.image_gen.provider (openai|replicate).',
|
|
7
|
+
parameters: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
prompt: { type: 'string' },
|
|
11
|
+
provider: { type: 'string', enum: ['openai', 'replicate'] },
|
|
12
|
+
size: { type: 'string', default: '1024x1024' },
|
|
13
|
+
model: { type: 'string' },
|
|
14
|
+
},
|
|
15
|
+
required: ['prompt'],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
checkFn: () => Boolean(process.env.OPENAI_API_KEY || process.env.REPLICATE_API_TOKEN),
|
|
19
|
+
requiresEnv: ['OPENAI_API_KEY or REPLICATE_API_TOKEN'],
|
|
20
|
+
handler: async ({ prompt, provider, size = '1024x1024', model }) => {
|
|
21
|
+
const which = provider || (process.env.OPENAI_API_KEY ? 'openai' : 'replicate')
|
|
22
|
+
if (which === 'openai') {
|
|
23
|
+
if (!process.env.OPENAI_API_KEY) return { error: 'OPENAI_API_KEY required' }
|
|
24
|
+
const res = await fetch('https://api.openai.com/v1/images/generations', { method: 'POST', headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ model: model || 'gpt-image-1', prompt, size }) })
|
|
25
|
+
return await res.json()
|
|
26
|
+
}
|
|
27
|
+
if (!process.env.REPLICATE_API_TOKEN) return { error: 'REPLICATE_API_TOKEN required' }
|
|
28
|
+
const res = await fetch('https://api.replicate.com/v1/predictions', { method: 'POST', headers: { authorization: `Token ${process.env.REPLICATE_API_TOKEN}`, 'content-type': 'application/json' }, body: JSON.stringify({ version: model || 'black-forest-labs/flux-schnell', input: { prompt } }) })
|
|
29
|
+
return await res.json()
|
|
30
|
+
},
|
|
31
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const _flags = new Map()
|
|
2
|
+
export function setInterrupt(sessionId) { _flags.set(sessionId, true) }
|
|
3
|
+
export function isInterrupted(sessionId) { return _flags.get(sessionId) === true }
|
|
4
|
+
export function clearInterrupt(sessionId) { _flags.delete(sessionId) }
|
|
5
|
+
|
|
6
|
+
export const _tool = ({
|
|
7
|
+
name: 'interrupt',
|
|
8
|
+
toolset: 'core',
|
|
9
|
+
schema: { name: 'interrupt', description: 'Set/clear/check interrupt flag for a session — agent loop polls and exits early.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['set', 'clear', 'check'] }, session_id: { type: 'string' } }, required: ['action', 'session_id'] } },
|
|
10
|
+
handler: async ({ action, session_id }) => {
|
|
11
|
+
if (action === 'set') { setInterrupt(session_id); return { interrupted: true } }
|
|
12
|
+
if (action === 'clear') { clearInterrupt(session_id); return { cleared: true } }
|
|
13
|
+
if (action === 'check') return { interrupted: isInterrupted(session_id) }
|
|
14
|
+
return { error: 'unknown action' }
|
|
15
|
+
},
|
|
16
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const _tool = ({
|
|
2
|
+
name: 'managed_tool_gateway',
|
|
3
|
+
toolset: 'core',
|
|
4
|
+
schema: { name: 'managed_tool_gateway', description: 'Proxy: dispatch any registered tool by name with arguments. Used for tool-level audit and policy interception.', parameters: { type: 'object', properties: { name: { type: 'string' }, arguments: {} }, required: ['name'] } },
|
|
5
|
+
handler: async ({ name, arguments: args = {} }, ctx = {}) => {
|
|
6
|
+
if (typeof ctx.audit === 'function') ctx.audit({ name, args })
|
|
7
|
+
return { result: await registry.dispatch(name, args, ctx) }
|
|
8
|
+
},
|
|
9
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const _tool = ({
|
|
2
|
+
name: 'mcp_oauth',
|
|
3
|
+
toolset: 'core',
|
|
4
|
+
schema: { name: 'mcp_oauth', description: 'OAuth flow for an MCP server: build authorize URL, exchange code for token.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['authorize_url', 'exchange'] }, server_url: { type: 'string' }, client_id: { type: 'string' }, redirect_uri: { type: 'string' }, code: { type: 'string' }, code_verifier: { type: 'string' } }, required: ['action', 'server_url'] } },
|
|
5
|
+
handler: async ({ action, server_url, client_id, redirect_uri, code, code_verifier }) => {
|
|
6
|
+
if (action === 'authorize_url') {
|
|
7
|
+
const u = new URL(server_url + '/authorize')
|
|
8
|
+
if (client_id) u.searchParams.set('client_id', client_id)
|
|
9
|
+
if (redirect_uri) u.searchParams.set('redirect_uri', redirect_uri)
|
|
10
|
+
u.searchParams.set('response_type', 'code')
|
|
11
|
+
u.searchParams.set('code_challenge_method', 'S256')
|
|
12
|
+
return { url: u.toString() }
|
|
13
|
+
}
|
|
14
|
+
if (action === 'exchange') {
|
|
15
|
+
const r = await fetch(server_url + '/token', { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri, code_verifier, client_id }).toString() })
|
|
16
|
+
return await r.json()
|
|
17
|
+
}
|
|
18
|
+
return { error: 'unknown action' }
|
|
19
|
+
},
|
|
20
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { getFreddieHome } from '../../src/home.js'
|
|
4
|
+
function tokFile(server) { return path.join(getFreddieHome(), 'mcp-tokens', encodeURIComponent(server) + '.json') }
|
|
5
|
+
|
|
6
|
+
export const _tool = ({
|
|
7
|
+
name: 'mcp_oauth_manager',
|
|
8
|
+
toolset: 'core',
|
|
9
|
+
schema: { name: 'mcp_oauth_manager', description: 'Persist & retrieve MCP OAuth tokens. Actions: store, get, list, delete, refresh.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['store', 'get', 'list', 'delete'] }, server: { type: 'string' }, token: {} }, required: ['action'] } },
|
|
10
|
+
handler: async ({ action, server, token }) => {
|
|
11
|
+
const dir = path.join(getFreddieHome(), 'mcp-tokens'); fs.mkdirSync(dir, { recursive: true })
|
|
12
|
+
if (action === 'store') { fs.writeFileSync(tokFile(server), JSON.stringify({ server, token, ts: Date.now() }), { mode: 0o600 }); return { stored: server } }
|
|
13
|
+
if (action === 'get') { const f = tokFile(server); return fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, 'utf8')) : { error: 'not found' } }
|
|
14
|
+
if (action === 'list') return { servers: fs.readdirSync(dir).filter(f => f.endsWith('.json')).map(f => decodeURIComponent(f.replace(/\.json$/, ''))) }
|
|
15
|
+
if (action === 'delete') { const f = tokFile(server); if (fs.existsSync(f)) fs.unlinkSync(f); return { deleted: server } }
|
|
16
|
+
return { error: 'unknown action' }
|
|
17
|
+
},
|
|
18
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
const _clients = new Map()
|
|
3
|
+
|
|
4
|
+
export const _tool = ({
|
|
5
|
+
name: 'mcp_tool',
|
|
6
|
+
toolset: 'core',
|
|
7
|
+
schema: { name: 'mcp_tool', description: 'Connect to an external MCP server (stdio) and call its tools.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['connect', 'list', 'call', 'disconnect'] }, id: { type: 'string' }, command: { type: 'string' }, args: { type: 'array' }, name: { type: 'string' }, arguments: {} }, required: ['action'] } },
|
|
8
|
+
handler: async ({ action, id, command, args = [], name, arguments: callArgs = {} }) => {
|
|
9
|
+
if (action === 'connect') {
|
|
10
|
+
const cid = id || 'mcp-' + Date.now()
|
|
11
|
+
const child = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
12
|
+
_clients.set(cid, { child, nextId: 1, pending: new Map(), buf: '' })
|
|
13
|
+
const c = _clients.get(cid)
|
|
14
|
+
child.stdout.on('data', d => {
|
|
15
|
+
c.buf += d.toString()
|
|
16
|
+
const lines = c.buf.split('\n'); c.buf = lines.pop()
|
|
17
|
+
for (const l of lines) { try { const m = JSON.parse(l); const p = c.pending.get(m.id); if (p) { c.pending.delete(m.id); p.resolve(m) } } catch {} }
|
|
18
|
+
})
|
|
19
|
+
return { id: cid, connected: true }
|
|
20
|
+
}
|
|
21
|
+
const c = _clients.get(id)
|
|
22
|
+
if (!c) return { error: 'unknown id' }
|
|
23
|
+
const rpc = (method, params) => new Promise((resolve, reject) => {
|
|
24
|
+
const rid = c.nextId++
|
|
25
|
+
c.pending.set(rid, { resolve, reject })
|
|
26
|
+
c.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: rid, method, params }) + '\n')
|
|
27
|
+
setTimeout(() => { if (c.pending.has(rid)) { c.pending.delete(rid); reject(new Error('mcp timeout')) } }, 30000)
|
|
28
|
+
})
|
|
29
|
+
if (action === 'list') return await rpc('tools/list', {})
|
|
30
|
+
if (action === 'call') return await rpc('tools/call', { name, arguments: callArgs })
|
|
31
|
+
if (action === 'disconnect') { try { c.child.kill('SIGTERM') } catch {} _clients.delete(id); return { disconnected: id } }
|
|
32
|
+
return { error: 'unknown action' }
|
|
33
|
+
},
|
|
34
|
+
})
|