freddie 0.0.47 → 0.0.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/AGENTS.md +12 -0
  2. package/CHANGELOG.md +26 -2
  3. package/package.json +3 -2
  4. package/plugins/ansi_strip/handler.js +7 -0
  5. package/plugins/ansi_strip/plugin.js +2 -0
  6. package/plugins/approval/handler.js +13 -0
  7. package/plugins/approval/plugin.js +2 -0
  8. package/plugins/bash/handler.js +33 -0
  9. package/plugins/bash/plugin.js +2 -0
  10. package/plugins/binary_extensions/handler.js +20 -0
  11. package/plugins/binary_extensions/plugin.js +2 -0
  12. package/plugins/browser/handler.js +46 -0
  13. package/plugins/browser/plugin.js +2 -0
  14. package/plugins/budget_config/handler.js +12 -0
  15. package/plugins/budget_config/plugin.js +2 -0
  16. package/plugins/checkpoint/handler.js +27 -0
  17. package/plugins/checkpoint/plugin.js +2 -0
  18. package/plugins/clarify/handler.js +13 -0
  19. package/plugins/clarify/plugin.js +2 -0
  20. package/plugins/code_execution/handler.js +25 -0
  21. package/plugins/code_execution/plugin.js +2 -0
  22. package/plugins/core-agent-machine/plugin.js +8 -0
  23. package/plugins/core-cli/plugin.js +83 -0
  24. package/plugins/core-commands/plugin.js +7 -0
  25. package/plugins/core-compressor/plugin.js +15 -0
  26. package/plugins/core-context-engine/plugin.js +7 -0
  27. package/plugins/core-cron/plugin.js +7 -0
  28. package/plugins/core-skills/plugin.js +7 -0
  29. package/plugins/credential_files/handler.js +14 -0
  30. package/plugins/credential_files/plugin.js +2 -0
  31. package/plugins/cronjob/handler.js +14 -0
  32. package/plugins/cronjob/plugin.js +2 -0
  33. package/plugins/debug_helpers/handler.js +8 -0
  34. package/plugins/debug_helpers/plugin.js +2 -0
  35. package/plugins/delegate/handler.js +27 -0
  36. package/plugins/delegate/plugin.js +2 -0
  37. package/plugins/discord_tool/handler.js +12 -0
  38. package/plugins/discord_tool/plugin.js +2 -0
  39. package/plugins/edit/handler.js +29 -0
  40. package/plugins/edit/plugin.js +2 -0
  41. package/plugins/env_passthrough/handler.js +14 -0
  42. package/plugins/env_passthrough/plugin.js +2 -0
  43. package/plugins/feishu_doc/handler.js +14 -0
  44. package/plugins/feishu_doc/plugin.js +2 -0
  45. package/plugins/feishu_drive/handler.js +13 -0
  46. package/plugins/feishu_drive/plugin.js +2 -0
  47. package/plugins/file_operations/handler.js +15 -0
  48. package/plugins/file_operations/plugin.js +2 -0
  49. package/plugins/file_state/handler.js +14 -0
  50. package/plugins/file_state/plugin.js +2 -0
  51. package/plugins/file_tools/handler.js +21 -0
  52. package/plugins/file_tools/plugin.js +2 -0
  53. package/plugins/fuzzy_match/handler.js +7 -0
  54. package/plugins/fuzzy_match/plugin.js +2 -0
  55. package/plugins/gm-cc/plugin.js +28 -0
  56. package/plugins/grep/handler.js +49 -0
  57. package/plugins/grep/plugin.js +2 -0
  58. package/plugins/gui-agents/plugin.js +26 -0
  59. package/plugins/gui-batch/plugin.js +11 -0
  60. package/plugins/gui-chat/plugin.js +21 -0
  61. package/plugins/gui-config/plugin.js +12 -0
  62. package/plugins/gui-cron/plugin.js +13 -0
  63. package/plugins/gui-debug/plugin.js +24 -0
  64. package/plugins/gui-env/plugin.js +7 -0
  65. package/plugins/gui-gateway/plugin.js +9 -0
  66. package/plugins/gui-profiles-commands-health/plugin.js +11 -0
  67. package/plugins/gui-sessions/plugin.js +9 -0
  68. package/plugins/gui-skills/plugin.js +8 -0
  69. package/plugins/gui-tools/plugin.js +7 -0
  70. package/plugins/homeassistant_tool/handler.js +14 -0
  71. package/plugins/homeassistant_tool/plugin.js +2 -0
  72. package/plugins/image_gen/handler.js +31 -0
  73. package/plugins/image_gen/plugin.js +2 -0
  74. package/plugins/interrupt/handler.js +16 -0
  75. package/plugins/interrupt/plugin.js +2 -0
  76. package/plugins/managed_tool_gateway/handler.js +9 -0
  77. package/plugins/managed_tool_gateway/plugin.js +2 -0
  78. package/plugins/mcp_oauth/handler.js +20 -0
  79. package/plugins/mcp_oauth/plugin.js +2 -0
  80. package/plugins/mcp_oauth_manager/handler.js +18 -0
  81. package/plugins/mcp_oauth_manager/plugin.js +2 -0
  82. package/plugins/mcp_tool/handler.js +34 -0
  83. package/plugins/mcp_tool/plugin.js +2 -0
  84. package/plugins/memory/handler.js +66 -0
  85. package/plugins/memory/plugin.js +2 -0
  86. package/plugins/memory-byterover/handler.js +25 -0
  87. package/plugins/memory-byterover/plugin.js +2 -0
  88. package/plugins/memory-hindsight/handler.js +25 -0
  89. package/plugins/memory-hindsight/plugin.js +2 -0
  90. package/plugins/memory-holographic/handler.js +31 -0
  91. package/plugins/memory-holographic/plugin.js +2 -0
  92. package/plugins/memory-honcho/handler.js +25 -0
  93. package/plugins/memory-honcho/plugin.js +2 -0
  94. package/plugins/memory-mem0/handler.js +25 -0
  95. package/plugins/memory-mem0/plugin.js +2 -0
  96. package/plugins/memory-openviking/handler.js +25 -0
  97. package/plugins/memory-openviking/plugin.js +2 -0
  98. package/plugins/memory-retaindb/handler.js +25 -0
  99. package/plugins/memory-retaindb/plugin.js +2 -0
  100. package/plugins/memory-supermemory/handler.js +25 -0
  101. package/plugins/memory-supermemory/plugin.js +2 -0
  102. package/plugins/mixture_of_agents/handler.js +13 -0
  103. package/plugins/mixture_of_agents/plugin.js +2 -0
  104. package/plugins/neutts_synth/handler.js +12 -0
  105. package/plugins/neutts_synth/plugin.js +2 -0
  106. package/plugins/openrouter_client/handler.js +12 -0
  107. package/plugins/openrouter_client/plugin.js +2 -0
  108. package/plugins/osv_check/handler.js +10 -0
  109. package/plugins/osv_check/plugin.js +2 -0
  110. package/plugins/patch_parser/handler.js +40 -0
  111. package/plugins/patch_parser/plugin.js +2 -0
  112. package/plugins/path_security/handler.js +14 -0
  113. package/plugins/path_security/plugin.js +2 -0
  114. package/plugins/platform-api_server/handler.js +21 -0
  115. package/plugins/platform-api_server/plugin.js +2 -0
  116. package/plugins/platform-bluebubbles/handler.js +32 -0
  117. package/plugins/platform-bluebubbles/plugin.js +2 -0
  118. package/plugins/platform-dingtalk/handler.js +32 -0
  119. package/plugins/platform-dingtalk/plugin.js +2 -0
  120. package/plugins/platform-discord/handler.js +24 -0
  121. package/plugins/platform-discord/plugin.js +2 -0
  122. package/plugins/platform-email/handler.js +51 -0
  123. package/plugins/platform-email/plugin.js +2 -0
  124. package/plugins/platform-feishu/handler.js +32 -0
  125. package/plugins/platform-feishu/plugin.js +2 -0
  126. package/plugins/platform-feishu_comment/handler.js +12 -0
  127. package/plugins/platform-feishu_comment/plugin.js +2 -0
  128. package/plugins/platform-feishu_comment_rules/handler.js +11 -0
  129. package/plugins/platform-feishu_comment_rules/plugin.js +2 -0
  130. package/plugins/platform-homeassistant/handler.js +32 -0
  131. package/plugins/platform-homeassistant/plugin.js +2 -0
  132. package/plugins/platform-matrix/handler.js +40 -0
  133. package/plugins/platform-matrix/plugin.js +2 -0
  134. package/plugins/platform-mattermost/handler.js +29 -0
  135. package/plugins/platform-mattermost/plugin.js +2 -0
  136. package/plugins/platform-qqbot/handler.js +32 -0
  137. package/plugins/platform-qqbot/plugin.js +2 -0
  138. package/plugins/platform-signal/handler.js +33 -0
  139. package/plugins/platform-signal/plugin.js +2 -0
  140. package/plugins/platform-slack/handler.js +34 -0
  141. package/plugins/platform-slack/plugin.js +2 -0
  142. package/plugins/platform-sms/handler.js +34 -0
  143. package/plugins/platform-sms/plugin.js +2 -0
  144. package/plugins/platform-telegram/handler.js +38 -0
  145. package/plugins/platform-telegram/plugin.js +2 -0
  146. package/plugins/platform-telegram_network/handler.js +17 -0
  147. package/plugins/platform-telegram_network/plugin.js +2 -0
  148. package/plugins/platform-webhook/handler.js +19 -0
  149. package/plugins/platform-webhook/plugin.js +2 -0
  150. package/plugins/platform-wecom/handler.js +32 -0
  151. package/plugins/platform-wecom/plugin.js +2 -0
  152. package/plugins/platform-wecom_callback/handler.js +15 -0
  153. package/plugins/platform-wecom_callback/plugin.js +2 -0
  154. package/plugins/platform-wecom_crypto/handler.js +16 -0
  155. package/plugins/platform-wecom_crypto/plugin.js +2 -0
  156. package/plugins/platform-weixin/handler.js +32 -0
  157. package/plugins/platform-weixin/plugin.js +2 -0
  158. package/plugins/platform-whatsapp/handler.js +40 -0
  159. package/plugins/platform-whatsapp/plugin.js +2 -0
  160. package/plugins/platform-yuanbao/handler.js +9 -0
  161. package/plugins/platform-yuanbao/plugin.js +2 -0
  162. package/plugins/platform-yuanbao_media/handler.js +5 -0
  163. package/plugins/platform-yuanbao_media/plugin.js +2 -0
  164. package/plugins/platform-yuanbao_proto/handler.js +9 -0
  165. package/plugins/platform-yuanbao_proto/plugin.js +2 -0
  166. package/plugins/platform-yuanbao_sticker/handler.js +6 -0
  167. package/plugins/platform-yuanbao_sticker/plugin.js +2 -0
  168. package/plugins/process_registry/handler.js +15 -0
  169. package/plugins/process_registry/plugin.js +2 -0
  170. package/plugins/read/handler.js +24 -0
  171. package/plugins/read/plugin.js +2 -0
  172. package/plugins/rl_training/handler.js +12 -0
  173. package/plugins/rl_training/plugin.js +2 -0
  174. package/plugins/schema_sanitizer/handler.js +17 -0
  175. package/plugins/schema_sanitizer/plugin.js +2 -0
  176. package/plugins/send_message/handler.js +30 -0
  177. package/plugins/send_message/plugin.js +2 -0
  178. package/plugins/session_search/handler.js +21 -0
  179. package/plugins/session_search/plugin.js +2 -0
  180. package/plugins/skill_manager/handler.js +16 -0
  181. package/plugins/skill_manager/plugin.js +2 -0
  182. package/plugins/skill_usage/handler.js +18 -0
  183. package/plugins/skill_usage/plugin.js +2 -0
  184. package/plugins/skills_guard/handler.js +16 -0
  185. package/plugins/skills_guard/plugin.js +2 -0
  186. package/plugins/skills_hub/handler.js +29 -0
  187. package/plugins/skills_hub/plugin.js +2 -0
  188. package/plugins/skills_index/handler.js +12 -0
  189. package/plugins/skills_index/plugin.js +2 -0
  190. package/plugins/skills_sync/handler.js +17 -0
  191. package/plugins/skills_sync/plugin.js +2 -0
  192. package/plugins/skills_tool/handler.js +9 -0
  193. package/plugins/skills_tool/plugin.js +2 -0
  194. package/plugins/slash_confirm/handler.js +14 -0
  195. package/plugins/slash_confirm/plugin.js +2 -0
  196. package/plugins/terminal/handler.js +27 -0
  197. package/plugins/terminal/plugin.js +2 -0
  198. package/plugins/tirith_security/handler.js +23 -0
  199. package/plugins/tirith_security/plugin.js +2 -0
  200. package/plugins/todo/handler.js +52 -0
  201. package/plugins/todo/plugin.js +2 -0
  202. package/plugins/tool_backend_helpers/handler.js +24 -0
  203. package/plugins/tool_backend_helpers/plugin.js +2 -0
  204. package/plugins/tool_output_limits/handler.js +14 -0
  205. package/plugins/tool_output_limits/plugin.js +2 -0
  206. package/plugins/tool_result_storage/handler.js +18 -0
  207. package/plugins/tool_result_storage/plugin.js +2 -0
  208. package/plugins/transcription/handler.js +18 -0
  209. package/plugins/transcription/plugin.js +2 -0
  210. package/plugins/tts/handler.js +18 -0
  211. package/plugins/tts/plugin.js +2 -0
  212. package/plugins/url_safety/handler.js +14 -0
  213. package/plugins/url_safety/plugin.js +2 -0
  214. package/plugins/vision/handler.js +17 -0
  215. package/plugins/vision/plugin.js +2 -0
  216. package/plugins/voice_mode/handler.js +9 -0
  217. package/plugins/voice_mode/plugin.js +2 -0
  218. package/plugins/web_search/handler.js +35 -0
  219. package/plugins/web_search/plugin.js +2 -0
  220. package/plugins/web_tools/handler.js +17 -0
  221. package/plugins/web_tools/plugin.js +2 -0
  222. package/plugins/website_policy/handler.js +13 -0
  223. package/plugins/website_policy/plugin.js +2 -0
  224. package/plugins/write/handler.js +23 -0
  225. package/plugins/write/plugin.js +2 -0
  226. package/plugins/xai_http/handler.js +12 -0
  227. package/plugins/xai_http/plugin.js +2 -0
  228. package/plugins/yuanbao_tools/handler.js +12 -0
  229. package/plugins/yuanbao_tools/plugin.js +2 -0
  230. package/src/sessions.js +2 -1
  231. package/src/web/app.js +17 -0
  232. package/src/web/index.html +7 -3
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-edit', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -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,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-env_passthrough', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -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,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-feishu_doc', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -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,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-feishu_drive', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -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,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-file_operations', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -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,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-file_state', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -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,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-file_tools', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -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,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-fuzzy_match', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -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,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-grep', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -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,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-homeassistant_tool', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -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,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-image_gen', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -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,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-interrupt', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -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,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-managed_tool_gateway', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -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,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-mcp_oauth', 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 { 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,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-mcp_oauth_manager', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }
@@ -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
+ })
@@ -0,0 +1,2 @@
1
+ import { _tool } from './handler.js'
2
+ export default { name: 'tool-mcp_tool', surfaces: 'pi', register({ pi }) { pi.tools.register(_tool) } }