freddie 0.0.41

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 (307) hide show
  1. package/AGENTS.md +180 -0
  2. package/CHANGELOG.md +32 -0
  3. package/README.md +130 -0
  4. package/bin/freddie.js +116 -0
  5. package/package.json +59 -0
  6. package/skills/creative/README.md +3 -0
  7. package/skills/creative/architecture-diagram/SKILL.md +52 -0
  8. package/skills/creative/ascii-video/SKILL.md +60 -0
  9. package/skills/creative/concept-diagrams/SKILL.md +65 -0
  10. package/skills/data/README.md +3 -0
  11. package/skills/data/etl-pipelines/SKILL.md +60 -0
  12. package/skills/data/sql-explainer/SKILL.md +60 -0
  13. package/skills/ops/README.md +3 -0
  14. package/skills/ops/incident-response/SKILL.md +74 -0
  15. package/skills/ops/log-triage/SKILL.md +79 -0
  16. package/skills/planning/README.md +3 -0
  17. package/skills/planning/okr-drafter/SKILL.md +60 -0
  18. package/skills/planning/weekly-review/SKILL.md +64 -0
  19. package/skills/software-development/README.md +3 -0
  20. package/skills/software-development/code-review/SKILL.md +70 -0
  21. package/skills/software-development/rfc-writer/SKILL.md +68 -0
  22. package/skills/software-development/systematic-debugging/SKILL.md +80 -0
  23. package/src/acp/auth.js +21 -0
  24. package/src/acp/entry.js +2 -0
  25. package/src/acp/events.js +10 -0
  26. package/src/acp/main.js +8 -0
  27. package/src/acp/permissions.js +29 -0
  28. package/src/acp/server.js +84 -0
  29. package/src/acp/session.js +26 -0
  30. package/src/acp/tools.js +17 -0
  31. package/src/agent/account_usage.js +19 -0
  32. package/src/agent/acptoapi-bridge.js +80 -0
  33. package/src/agent/anthropic_adapter.js +10 -0
  34. package/src/agent/auxiliary_client.js +20 -0
  35. package/src/agent/bedrock_adapter.js +11 -0
  36. package/src/agent/codex_responses_adapter.js +10 -0
  37. package/src/agent/compress/compressor.js +55 -0
  38. package/src/agent/compress/fallback.js +14 -0
  39. package/src/agent/compress/index.js +6 -0
  40. package/src/agent/compress/policy.js +47 -0
  41. package/src/agent/compress/prompt.js +46 -0
  42. package/src/agent/compress/prune.js +16 -0
  43. package/src/agent/compress/tokens.js +31 -0
  44. package/src/agent/context_references.js +40 -0
  45. package/src/agent/copilot_acp_client.js +6 -0
  46. package/src/agent/credential_pool.js +30 -0
  47. package/src/agent/credential_sources.js +18 -0
  48. package/src/agent/curator.js +5 -0
  49. package/src/agent/display.js +23 -0
  50. package/src/agent/error_classifier.js +15 -0
  51. package/src/agent/file_safety.js +9 -0
  52. package/src/agent/gemini_cloudcode_adapter.js +9 -0
  53. package/src/agent/gemini_native_adapter.js +11 -0
  54. package/src/agent/gemini_schema.js +19 -0
  55. package/src/agent/google_code_assist.js +8 -0
  56. package/src/agent/google_oauth.js +21 -0
  57. package/src/agent/image_gen_provider.js +8 -0
  58. package/src/agent/image_gen_registry.js +6 -0
  59. package/src/agent/image_routing.js +13 -0
  60. package/src/agent/insights.js +9 -0
  61. package/src/agent/llm_resolver.js +21 -0
  62. package/src/agent/lmstudio_reasoning.js +13 -0
  63. package/src/agent/machine.js +102 -0
  64. package/src/agent/manual_compression_feedback.js +5 -0
  65. package/src/agent/memory_manager.js +14 -0
  66. package/src/agent/memory_provider.js +1 -0
  67. package/src/agent/model_metadata.js +28 -0
  68. package/src/agent/models_dev.js +13 -0
  69. package/src/agent/moonshot_schema.js +11 -0
  70. package/src/agent/oauth_endpoints.js +79 -0
  71. package/src/agent/onboarding.js +16 -0
  72. package/src/agent/pi-bridge.js +37 -0
  73. package/src/agent/prompt_builder.js +12 -0
  74. package/src/agent/prompt_caching.js +24 -0
  75. package/src/agent/rate_limit_tracker.js +12 -0
  76. package/src/agent/redact.js +25 -0
  77. package/src/agent/retry_utils.js +17 -0
  78. package/src/agent/shell_hooks.js +16 -0
  79. package/src/agent/skill_commands.js +16 -0
  80. package/src/agent/skill_preprocessing.js +12 -0
  81. package/src/agent/skill_utils.js +14 -0
  82. package/src/agent/subdirectory_hints.js +17 -0
  83. package/src/agent/title_generator.js +13 -0
  84. package/src/agent/trajectory.js +9 -0
  85. package/src/agent/usage_pricing.js +16 -0
  86. package/src/auth.js +84 -0
  87. package/src/batch.js +32 -0
  88. package/src/cli/auth_commands.js +17 -0
  89. package/src/cli/azure_detect.js +9 -0
  90. package/src/cli/backup.js +17 -0
  91. package/src/cli/banner.js +13 -0
  92. package/src/cli/browser_connect.js +11 -0
  93. package/src/cli/callbacks.js +5 -0
  94. package/src/cli/claw.js +8 -0
  95. package/src/cli/cli_output.js +19 -0
  96. package/src/cli/clipboard.js +24 -0
  97. package/src/cli/codex_models.js +8 -0
  98. package/src/cli/colors.js +13 -0
  99. package/src/cli/completer.js +98 -0
  100. package/src/cli/completion.js +21 -0
  101. package/src/cli/copilot_auth.js +9 -0
  102. package/src/cli/curator_cli.js +5 -0
  103. package/src/cli/curses.js +15 -0
  104. package/src/cli/debug.js +6 -0
  105. package/src/cli/default_soul.js +20 -0
  106. package/src/cli/dingtalk_auth.js +12 -0
  107. package/src/cli/doctor.js +15 -0
  108. package/src/cli/dump.js +11 -0
  109. package/src/cli/env_loader.js +25 -0
  110. package/src/cli/fallback_cmd.js +9 -0
  111. package/src/cli/gateway_cli.js +17 -0
  112. package/src/cli/hooks.js +9 -0
  113. package/src/cli/interactive.js +61 -0
  114. package/src/cli/logs.js +32 -0
  115. package/src/cli/main.js +7 -0
  116. package/src/cli/mcp_config.js +9 -0
  117. package/src/cli/memory_setup.js +12 -0
  118. package/src/cli/model_catalog.js +23 -0
  119. package/src/cli/model_normalize.js +12 -0
  120. package/src/cli/model_switch.js +11 -0
  121. package/src/cli/models.js +13 -0
  122. package/src/cli/nous_subscription.js +12 -0
  123. package/src/cli/oneshot.js +6 -0
  124. package/src/cli/pairing.js +21 -0
  125. package/src/cli/platforms.js +14 -0
  126. package/src/cli/plugins.js +4 -0
  127. package/src/cli/plugins_cmd.js +21 -0
  128. package/src/cli/profiles_cli.js +6 -0
  129. package/src/cli/providers.js +18 -0
  130. package/src/cli/pty_bridge.js +16 -0
  131. package/src/cli/relaunch.js +7 -0
  132. package/src/cli/runtime_provider.js +9 -0
  133. package/src/cli/setup.js +131 -0
  134. package/src/cli/skills_config.js +6 -0
  135. package/src/cli/skills_hub.js +8 -0
  136. package/src/cli/slack_cli.js +17 -0
  137. package/src/cli/status.js +10 -0
  138. package/src/cli/timeouts.js +5 -0
  139. package/src/cli/tips.js +14 -0
  140. package/src/cli/tools_config.js +15 -0
  141. package/src/cli/uninstall.js +8 -0
  142. package/src/cli/vercel_auth.js +13 -0
  143. package/src/cli/voice.js +6 -0
  144. package/src/cli/web_server.js +13 -0
  145. package/src/cli/webhook.js +12 -0
  146. package/src/commands/profile.js +72 -0
  147. package/src/commands/registry.js +94 -0
  148. package/src/config.js +125 -0
  149. package/src/context/engine.js +42 -0
  150. package/src/cron/cron-parse.js +27 -0
  151. package/src/cron/scheduler.js +63 -0
  152. package/src/db.js +178 -0
  153. package/src/gateway/base.js +13 -0
  154. package/src/gateway/builtin_hooks/boot.js +5 -0
  155. package/src/gateway/builtin_hooks/broadcast.js +3 -0
  156. package/src/gateway/builtin_hooks/deny.js +6 -0
  157. package/src/gateway/builtin_hooks/index.js +17 -0
  158. package/src/gateway/builtin_hooks/presence.js +4 -0
  159. package/src/gateway/builtin_hooks/routing.js +7 -0
  160. package/src/gateway/helpers.js +27 -0
  161. package/src/gateway/platforms/api_server.js +21 -0
  162. package/src/gateway/platforms/bluebubbles.js +32 -0
  163. package/src/gateway/platforms/dingtalk.js +32 -0
  164. package/src/gateway/platforms/discord.js +24 -0
  165. package/src/gateway/platforms/email.js +51 -0
  166. package/src/gateway/platforms/feishu.js +32 -0
  167. package/src/gateway/platforms/feishu_comment.js +12 -0
  168. package/src/gateway/platforms/feishu_comment_rules.js +11 -0
  169. package/src/gateway/platforms/homeassistant.js +32 -0
  170. package/src/gateway/platforms/matrix.js +40 -0
  171. package/src/gateway/platforms/mattermost.js +29 -0
  172. package/src/gateway/platforms/qqbot.js +32 -0
  173. package/src/gateway/platforms/signal.js +33 -0
  174. package/src/gateway/platforms/slack.js +34 -0
  175. package/src/gateway/platforms/sms.js +34 -0
  176. package/src/gateway/platforms/telegram.js +38 -0
  177. package/src/gateway/platforms/telegram_network.js +17 -0
  178. package/src/gateway/platforms/webhook.js +19 -0
  179. package/src/gateway/platforms/wecom.js +32 -0
  180. package/src/gateway/platforms/wecom_callback.js +15 -0
  181. package/src/gateway/platforms/wecom_crypto.js +16 -0
  182. package/src/gateway/platforms/weixin.js +32 -0
  183. package/src/gateway/platforms/whatsapp.js +40 -0
  184. package/src/gateway/platforms/yuanbao.js +9 -0
  185. package/src/gateway/platforms/yuanbao_media.js +5 -0
  186. package/src/gateway/platforms/yuanbao_proto.js +9 -0
  187. package/src/gateway/platforms/yuanbao_sticker.js +6 -0
  188. package/src/gateway/run.js +42 -0
  189. package/src/gateway/service.js +143 -0
  190. package/src/home.js +44 -0
  191. package/src/index.js +47 -0
  192. package/src/mcp/server.js +49 -0
  193. package/src/observability/debug.js +31 -0
  194. package/src/observability/log.js +38 -0
  195. package/src/plugins/achievements/index.js +9 -0
  196. package/src/plugins/cockpit/index.js +8 -0
  197. package/src/plugins/context_engine/index.js +13 -0
  198. package/src/plugins/disk_cleanup/index.js +22 -0
  199. package/src/plugins/google_meet/index.js +19 -0
  200. package/src/plugins/image_gen/index.js +5 -0
  201. package/src/plugins/manager.js +66 -0
  202. package/src/plugins/memory/_index.js +8 -0
  203. package/src/plugins/memory/byterover.js +25 -0
  204. package/src/plugins/memory/hindsight.js +25 -0
  205. package/src/plugins/memory/holographic.js +31 -0
  206. package/src/plugins/memory/honcho.js +25 -0
  207. package/src/plugins/memory/mem0.js +25 -0
  208. package/src/plugins/memory/openviking.js +25 -0
  209. package/src/plugins/memory/provider.js +35 -0
  210. package/src/plugins/memory/retaindb.js +25 -0
  211. package/src/plugins/memory/supermemory.js +25 -0
  212. package/src/plugins/observability/index.js +18 -0
  213. package/src/plugins/platforms/index.js +20 -0
  214. package/src/plugins/spotify/index.js +22 -0
  215. package/src/rl/atropos.js +22 -0
  216. package/src/rl/cli.js +18 -0
  217. package/src/sessions.js +84 -0
  218. package/src/skills/index.js +49 -0
  219. package/src/skin/engine.js +81 -0
  220. package/src/swe/runner.js +26 -0
  221. package/src/time.js +25 -0
  222. package/src/tools/ansi_strip.js +8 -0
  223. package/src/tools/approval.js +15 -0
  224. package/src/tools/bash.js +35 -0
  225. package/src/tools/binary_extensions.js +22 -0
  226. package/src/tools/browser.js +48 -0
  227. package/src/tools/budget_config.js +13 -0
  228. package/src/tools/checkpoint.js +29 -0
  229. package/src/tools/clarify.js +15 -0
  230. package/src/tools/code_execution.js +27 -0
  231. package/src/tools/credential_files.js +16 -0
  232. package/src/tools/cronjob.js +16 -0
  233. package/src/tools/debug_helpers.js +9 -0
  234. package/src/tools/delegate.js +28 -0
  235. package/src/tools/discord_tool.js +13 -0
  236. package/src/tools/edit.js +31 -0
  237. package/src/tools/env_passthrough.js +15 -0
  238. package/src/tools/environments/base.js +26 -0
  239. package/src/tools/environments/daytona.js +48 -0
  240. package/src/tools/environments/docker.js +14 -0
  241. package/src/tools/environments/file_sync.js +60 -0
  242. package/src/tools/environments/index.js +36 -0
  243. package/src/tools/environments/local.js +31 -0
  244. package/src/tools/environments/modal.js +33 -0
  245. package/src/tools/environments/singularity.js +38 -0
  246. package/src/tools/environments/ssh.js +14 -0
  247. package/src/tools/environments/vercel_sandbox.js +47 -0
  248. package/src/tools/feishu_doc.js +15 -0
  249. package/src/tools/feishu_drive.js +14 -0
  250. package/src/tools/file_operations.js +17 -0
  251. package/src/tools/file_state.js +16 -0
  252. package/src/tools/file_tools.js +23 -0
  253. package/src/tools/fuzzy_match.js +8 -0
  254. package/src/tools/grep.js +51 -0
  255. package/src/tools/homeassistant_tool.js +15 -0
  256. package/src/tools/image_gen.js +33 -0
  257. package/src/tools/interrupt.js +18 -0
  258. package/src/tools/managed_tool_gateway.js +11 -0
  259. package/src/tools/mcp_oauth.js +21 -0
  260. package/src/tools/mcp_oauth_manager.js +20 -0
  261. package/src/tools/mcp_tool.js +36 -0
  262. package/src/tools/memory.js +66 -0
  263. package/src/tools/mixture_of_agents.js +14 -0
  264. package/src/tools/neutts_synth.js +13 -0
  265. package/src/tools/openrouter_client.js +13 -0
  266. package/src/tools/osv_check.js +11 -0
  267. package/src/tools/patch_parser.js +42 -0
  268. package/src/tools/path_security.js +16 -0
  269. package/src/tools/process_registry.js +17 -0
  270. package/src/tools/read.js +26 -0
  271. package/src/tools/registry.js +54 -0
  272. package/src/tools/rl_training.js +13 -0
  273. package/src/tools/schema_sanitizer.js +18 -0
  274. package/src/tools/send_message.js +32 -0
  275. package/src/tools/session_search.js +23 -0
  276. package/src/tools/skill_manager.js +17 -0
  277. package/src/tools/skill_usage.js +20 -0
  278. package/src/tools/skills_guard.js +17 -0
  279. package/src/tools/skills_hub.js +31 -0
  280. package/src/tools/skills_index.js +14 -0
  281. package/src/tools/skills_sync.js +19 -0
  282. package/src/tools/skills_tool.js +11 -0
  283. package/src/tools/slash_confirm.js +16 -0
  284. package/src/tools/terminal.js +29 -0
  285. package/src/tools/tirith_security.js +25 -0
  286. package/src/tools/todo.js +54 -0
  287. package/src/tools/tool_backend_helpers.js +26 -0
  288. package/src/tools/tool_output_limits.js +15 -0
  289. package/src/tools/tool_result_storage.js +20 -0
  290. package/src/tools/transcription.js +19 -0
  291. package/src/tools/tts.js +19 -0
  292. package/src/tools/url_safety.js +15 -0
  293. package/src/tools/vision.js +18 -0
  294. package/src/tools/voice_mode.js +10 -0
  295. package/src/tools/web_search.js +37 -0
  296. package/src/tools/web_tools.js +18 -0
  297. package/src/tools/website_policy.js +14 -0
  298. package/src/tools/write.js +25 -0
  299. package/src/tools/xai_http.js +13 -0
  300. package/src/tools/yuanbao_tools.js +13 -0
  301. package/src/toolset_distributions.js +18 -0
  302. package/src/toolsets.js +26 -0
  303. package/src/tui/index.js +26 -0
  304. package/src/utils.js +54 -0
  305. package/src/web/app.js +547 -0
  306. package/src/web/index.html +167 -0
  307. package/src/web/server.js +109 -0
package/src/index.js ADDED
@@ -0,0 +1,47 @@
1
+ export * from './home.js'
2
+ export * from './config.js'
3
+ export * from './sessions.js'
4
+ export * from './toolsets.js'
5
+ export { registry, discoverBuiltinTools } from './tools/registry.js'
6
+ export { runTurn, createAgentMachine } from './agent/machine.js'
7
+ export { Gateway, bootMdHook } from './gateway/run.js'
8
+ export { AcpServer } from './acp/server.js'
9
+ export { COMMAND_REGISTRY, resolveCommand, gatewayHelpLines, telegramBotCommands, slackSubcommandMap, GATEWAY_KNOWN_COMMANDS, COMMANDS_BY_CATEGORY } from './commands/registry.js'
10
+ export { getActiveSkin, setActiveSkin, listBuiltinSkins, loadSkin } from './skin/engine.js'
11
+ export { pluginManager, PluginManager } from './plugins/manager.js'
12
+ export { listSkills, findSkill, skillAsUserMessage } from './skills/index.js'
13
+ export { log, logger } from './observability/log.js'
14
+ export { registerDebug, snapshot, snapshotAll, listDebug, attachDebugRoutes } from './observability/debug.js'
15
+ export { createJob, listJobs, cancelJob, deleteJob, tick as cronTick, startScheduler, stopScheduler } from './cron/scheduler.js'
16
+ export { parseCron, matches as cronMatches } from './cron/cron-parse.js'
17
+ export { runBatch } from './batch.js'
18
+ export { createDashboard } from './web/server.js'
19
+ export { createEnvironment, defaultEnvironment, LocalEnvironment, DockerEnvironment, SshEnvironment } from './tools/environments/index.js'
20
+ export { getAuthStore } from './auth.js'
21
+ export { buildContext, blocksToSystemMessage, ContextPlugins } from './context/engine.js'
22
+ export { callLLM as piCallLLM } from './agent/pi-bridge.js'
23
+ export { interactive } from './cli/interactive.js'
24
+ export { listMemoryProviders, createMemoryProvider, registerMemoryProvider } from './plugins/memory/provider.js'
25
+ export { compress, shouldCompress, computeCompressionPlan, SUMMARY_PREFIX, estimateMessagesTokens, pruneOldToolResults, markFailure as compressMarkFailure, shouldRetry as compressShouldRetry, clearFailure as compressClearFailure } from './agent/compress/index.js'
26
+ export * as time from './time.js'
27
+ export * as utils from './utils.js'
28
+ export * as accountUsage from './agent/account_usage.js'
29
+ export * as credentialPool from './agent/credential_pool.js'
30
+ export * as displayAgent from './agent/display.js'
31
+ export * as redact from './agent/redact.js'
32
+ export * as modelMetadata from './agent/model_metadata.js'
33
+ export * as imageRouting from './agent/image_routing.js'
34
+ export * as skillCommands from './agent/skill_commands.js'
35
+ export * as contextRefs from './agent/context_references.js'
36
+ export * as trajectory from './agent/trajectory.js'
37
+ export * as toolsetDistributions from './toolset_distributions.js'
38
+ export { McpServer } from './mcp/server.js'
39
+ export { runMiniSwe, loadTask as loadSweTask } from './swe/runner.js'
40
+ export { rlSubcommand } from './rl/cli.js'
41
+ export { AtroposClient } from './rl/atropos.js'
42
+ export { setupWizard } from './cli/setup.js'
43
+ export { launchCurses } from './cli/curses.js'
44
+ export { loadToolsConfig, saveToolsConfig, setToolOverride } from './cli/tools_config.js'
45
+ export * as cliWebServer from './cli/web_server.js'
46
+ export { launchTui } from './tui/index.js'
47
+ export { registerBuiltinHooks } from './gateway/builtin_hooks/index.js'
@@ -0,0 +1,49 @@
1
+ import readline from 'node:readline'
2
+ import { registry, discoverBuiltinTools } from '../tools/registry.js'
3
+ import { listSkills } from '../skills/index.js'
4
+ import { logger } from '../observability/log.js'
5
+
6
+ const log = logger('mcp')
7
+
8
+ const PROTOCOL_VERSION = '2024-11-05'
9
+
10
+ export class McpServer {
11
+ constructor({ stdin = process.stdin, stdout = process.stdout } = {}) { this.in = stdin; this.out = stdout }
12
+ start() {
13
+ const rl = readline.createInterface({ input: this.in, crlfDelay: Infinity })
14
+ rl.on('line', (l) => this.handle(l).catch(e => this._err(null, String(e))))
15
+ this.rl = rl
16
+ }
17
+ stop() { this.rl?.close() }
18
+ _send(o) { this.out.write(JSON.stringify(o) + '\n') }
19
+ _ok(id, result) { this._send({ jsonrpc: '2.0', id, result }) }
20
+ _err(id, message) { this._send({ jsonrpc: '2.0', id, error: { code: -32603, message } }) }
21
+ async handle(line) {
22
+ if (!line.trim()) return
23
+ let req; try { req = JSON.parse(line) } catch { return this._err(null, 'invalid json') }
24
+ log.info('rpc', { method: req.method, id: req.id })
25
+ const fn = METHODS[req.method]
26
+ if (!fn) return this._err(req.id, 'unknown method: ' + req.method)
27
+ try { this._ok(req.id, await fn(req.params || {})) } catch (e) { this._err(req.id, String(e?.message || e)) }
28
+ }
29
+ }
30
+ const METHODS = {
31
+ initialize: () => ({
32
+ protocolVersion: PROTOCOL_VERSION,
33
+ capabilities: { tools: { listChanged: false }, prompts: { listChanged: false }, resources: { listChanged: false } },
34
+ serverInfo: { name: 'freddie-mcp', version: '0.4.0' },
35
+ }),
36
+ 'tools/list': async () => {
37
+ await discoverBuiltinTools()
38
+ return { tools: registry.list().map(t => ({ name: t.name, description: t.schema.description, inputSchema: t.schema.parameters || {} })) }
39
+ },
40
+ 'tools/call': async ({ name, arguments: args = {} }) => {
41
+ const out = await registry.dispatch(name, args)
42
+ return { content: [{ type: 'text', text: typeof out === 'string' ? out : JSON.stringify(out) }] }
43
+ },
44
+ 'prompts/list': async () => {
45
+ const skills = listSkills()
46
+ return { prompts: skills.map(s => ({ name: s.name, description: s.description })) }
47
+ },
48
+ 'resources/list': async () => ({ resources: [] }),
49
+ }
@@ -0,0 +1,31 @@
1
+ const _registry = new Map()
2
+
3
+ export function registerDebug(name, fn) {
4
+ _registry.set(name, fn)
5
+ }
6
+
7
+ export function listDebug() {
8
+ return [...registry().keys()]
9
+ }
10
+
11
+ export function snapshot(name) {
12
+ const fn = _registry.get(name)
13
+ if (!fn) return { error: `unknown subsystem: ${name}` }
14
+ try { return fn() } catch (e) { return { error: String(e) } }
15
+ }
16
+
17
+ export function snapshotAll() {
18
+ const out = {}
19
+ for (const [k, fn] of _registry.entries()) {
20
+ try { out[k] = fn() } catch (e) { out[k] = { error: String(e) } }
21
+ }
22
+ return out
23
+ }
24
+
25
+ export function attachDebugRoutes(app) {
26
+ app.get('/debug', (_, res) => res.json([..._registry.keys()]))
27
+ app.get('/debug/:name', (req, res) => res.json(snapshot(req.params.name)))
28
+ app.get('/debug-all', (_, res) => res.json(snapshotAll()))
29
+ }
30
+
31
+ function registry() { return _registry }
@@ -0,0 +1,38 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { getFophHome } from '../home.js'
4
+
5
+ const SEVERITIES = { debug: 10, info: 20, warning: 30, error: 40 }
6
+
7
+ let _streams = new Map()
8
+
9
+ function streamFor(name) {
10
+ if (_streams.has(name)) return _streams.get(name)
11
+ const dir = path.join(getFophHome(), 'logs')
12
+ fs.mkdirSync(dir, { recursive: true })
13
+ const s = fs.createWriteStream(path.join(dir, `${name}.log`), { flags: 'a' })
14
+ _streams.set(name, s)
15
+ return s
16
+ }
17
+
18
+ export function log({ subsystem = 'app', severity = 'info', msg = '', ...rest }) {
19
+ const ts = new Date().toISOString()
20
+ const rec = { ts, subsystem, severity, msg, ...rest }
21
+ const line = JSON.stringify(rec) + '\n'
22
+ streamFor(subsystem).write(line)
23
+ if (SEVERITIES[severity] >= 30) streamFor('errors').write(line)
24
+ }
25
+
26
+ export function logger(subsystem) {
27
+ return {
28
+ debug: (msg, e = {}) => log({ subsystem, severity: 'debug', msg, ...e }),
29
+ info: (msg, e = {}) => log({ subsystem, severity: 'info', msg, ...e }),
30
+ warn: (msg, e = {}) => log({ subsystem, severity: 'warning', msg, ...e }),
31
+ error: (msg, e = {}) => log({ subsystem, severity: 'error', msg, ...e }),
32
+ }
33
+ }
34
+
35
+ export function closeAll() {
36
+ for (const s of _streams.values()) s.end()
37
+ _streams.clear()
38
+ }
@@ -0,0 +1,9 @@
1
+ import { db } from '../../db.js'
2
+
3
+ async function init() { const d = await db(); await d.exec(`CREATE TABLE IF NOT EXISTS achievements (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, ts INTEGER NOT NULL, payload TEXT)`); return d }
4
+ export async function award(name, payload = null) { await (await init()).prepare('INSERT INTO achievements (name, ts, payload) VALUES (?, ?, ?)').run(name, Date.now(), payload ? JSON.stringify(payload) : null) }
5
+ export async function listAchievements() { return await (await init()).prepare('SELECT * FROM achievements ORDER BY id DESC LIMIT 100').all() }
6
+ export const plugin = {
7
+ name: 'achievements',
8
+ register: (ctx) => { ctx.registerHook('onSessionStart', async (p) => { await award('session-start', { id: p.session?.id }); return p }) },
9
+ }
@@ -0,0 +1,8 @@
1
+ export const plugin = {
2
+ name: 'cockpit',
3
+ register: (ctx) => {
4
+ if (typeof ctx.attachRoute !== 'function') return
5
+ ctx.attachRoute('/api/cockpit/widgets', (_, res) => res.json([{ id: 'sessions', title: 'Sessions', kind: 'list' }, { id: 'tools', title: 'Tools', kind: 'count' }, { id: 'errors', title: 'Recent errors', kind: 'log' }]))
6
+ ctx.attachRoute('/api/cockpit/health', (_, res) => res.json({ ok: true, ts: Date.now() }))
7
+ },
8
+ }
@@ -0,0 +1,13 @@
1
+ import { ContextPlugins, buildContext, blocksToSystemMessage } from '../../context/engine.js'
2
+ export const plugin = {
3
+ name: 'context-engine',
4
+ register: (ctx) => {
5
+ ctx.registerHook('preLlmCall', async (p) => {
6
+ if (!p?.message) return p
7
+ const blocks = await buildContext({ message: p.message, plugins: ['file', 'skills'] })
8
+ const sys = blocksToSystemMessage(blocks)
9
+ return sys ? { ...p, prepend: [sys, ...(p.prepend || [])] } : p
10
+ })
11
+ },
12
+ }
13
+ export { ContextPlugins, buildContext, blocksToSystemMessage }
@@ -0,0 +1,22 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { getFophHome } from '../../home.js'
4
+
5
+ const MAX_AGE_DAYS = { logs: 30, batches: 14, 'tool-results': 7, checkpoints: 90 }
6
+ export function cleanup({ now = Date.now() } = {}) {
7
+ const removed = []
8
+ for (const [sub, days] of Object.entries(MAX_AGE_DAYS)) {
9
+ const dir = path.join(getFophHome(), sub)
10
+ if (!fs.existsSync(dir)) continue
11
+ const cutoff = now - days * 86400_000
12
+ for (const f of fs.readdirSync(dir)) {
13
+ const p = path.join(dir, f)
14
+ try { if (fs.statSync(p).mtimeMs < cutoff) { fs.rmSync(p, { recursive: true, force: true }); removed.push(p) } } catch {}
15
+ }
16
+ }
17
+ return { removed: removed.length }
18
+ }
19
+ export const plugin = {
20
+ name: 'disk-cleanup',
21
+ register: (ctx) => { ctx.registerHook('onSessionEnd', async (p) => { cleanup(); return p }) },
22
+ }
@@ -0,0 +1,19 @@
1
+ export const plugin = {
2
+ name: 'google-meet',
3
+ register: (ctx) => {
4
+ ctx.registerTool({
5
+ name: 'google_meet',
6
+ toolset: 'core',
7
+ schema: { name: 'google_meet', description: 'Schedule / list Google Meet calls.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'schedule'] }, summary: { type: 'string' }, start: { type: 'string' }, end: { type: 'string' } }, required: ['action'] } },
8
+ requiresEnv: ['GOOGLE_OAUTH_TOKEN'],
9
+ checkFn: () => Boolean(process.env.GOOGLE_OAUTH_TOKEN),
10
+ handler: async ({ action, summary, start, end }) => {
11
+ if (!process.env.GOOGLE_OAUTH_TOKEN) return { error: 'GOOGLE_OAUTH_TOKEN required' }
12
+ const auth = { authorization: `Bearer ${process.env.GOOGLE_OAUTH_TOKEN}` }
13
+ if (action === 'list') return await (await fetch('https://www.googleapis.com/calendar/v3/calendars/primary/events?conferenceDataVersion=1', { headers: auth })).json()
14
+ if (action === 'schedule') return await (await fetch('https://www.googleapis.com/calendar/v3/calendars/primary/events?conferenceDataVersion=1', { method: 'POST', headers: { ...auth, 'content-type': 'application/json' }, body: JSON.stringify({ summary, start: { dateTime: start }, end: { dateTime: end }, conferenceData: { createRequest: { requestId: String(Date.now()) } } }) })).json()
15
+ return { error: 'unknown action' }
16
+ },
17
+ })
18
+ },
19
+ }
@@ -0,0 +1,5 @@
1
+ import '../../tools/image_gen.js'
2
+ export const plugin = {
3
+ name: 'image-gen',
4
+ register: () => {},
5
+ }
@@ -0,0 +1,66 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { getFophHome } from '../home.js'
4
+ import { registry as toolRegistry } from '../tools/registry.js'
5
+ import { logger } from '../observability/log.js'
6
+
7
+ const log = logger('plugins')
8
+
9
+ const HOOK_NAMES = ['preToolCall', 'postToolCall', 'preLlmCall', 'postLlmCall', 'onSessionStart', 'onSessionEnd']
10
+
11
+ export class PluginManager {
12
+ constructor() {
13
+ this.plugins = []
14
+ this.hooks = Object.fromEntries(HOOK_NAMES.map(n => [n, []]))
15
+ this.cliCommands = []
16
+ }
17
+
18
+ async discoverPlugins(extraDirs = []) {
19
+ const dirs = [path.join(getFophHome(), 'plugins'), path.join(process.cwd(), '.freddie', 'plugins'), ...extraDirs]
20
+ for (const d of dirs) {
21
+ if (!fs.existsSync(d)) continue
22
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
23
+ if (!entry.isDirectory()) continue
24
+ const pkg = path.join(d, entry.name, 'package.json')
25
+ if (!fs.existsSync(pkg)) continue
26
+ try { await this._loadPlugin(path.join(d, entry.name)) } catch (e) { log.error('plugin load failed', { name: entry.name, err: String(e) }) }
27
+ }
28
+ }
29
+ return this.plugins.length
30
+ }
31
+
32
+ async _loadPlugin(dir) {
33
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
34
+ const main = path.join(dir, pkg.main || 'index.js')
35
+ const mod = await import(`file://${main.replace(/\\/g, '/')}`)
36
+ const ctx = this._ctx()
37
+ if (typeof mod.register === 'function') await mod.register(ctx)
38
+ this.plugins.push({ name: pkg.name, dir, main })
39
+ log.info('plugin loaded', { name: pkg.name })
40
+ }
41
+
42
+ register(plugin) {
43
+ const ctx = this._ctx()
44
+ plugin.register?.(ctx)
45
+ this.plugins.push({ name: plugin.name || 'inline', ...plugin })
46
+ }
47
+
48
+ _ctx() {
49
+ const self = this
50
+ return {
51
+ registerHook: (name, fn) => { if (!HOOK_NAMES.includes(name)) throw new Error(`unknown hook: ${name}`); self.hooks[name].push(fn) },
52
+ registerTool: (spec) => toolRegistry.register(spec),
53
+ registerCliCommand: (def) => self.cliCommands.push(def),
54
+ }
55
+ }
56
+
57
+ async invoke(hook, payload) {
58
+ let cur = payload
59
+ for (const fn of this.hooks[hook] || []) {
60
+ try { cur = (await fn(cur)) || cur } catch (e) { log.error('hook failed', { hook, err: String(e) }) }
61
+ }
62
+ return cur
63
+ }
64
+ }
65
+
66
+ export const pluginManager = new PluginManager()
@@ -0,0 +1,8 @@
1
+ export { HonchoMemory } from './honcho.js'
2
+ export { Mem0Memory } from './mem0.js'
3
+ export { SupermemoryMemory } from './supermemory.js'
4
+ export { ByteroverMemory } from './byterover.js'
5
+ export { HindsightMemory } from './hindsight.js'
6
+ export { HolographicMemory } from './holographic.js'
7
+ export { OpenvikingMemory } from './openviking.js'
8
+ export { RetaindbMemory } from './retaindb.js'
@@ -0,0 +1,25 @@
1
+ export class ByteroverMemory {
2
+ constructor(opts = {}) {
3
+ this.name = 'byterover'
4
+ this.apiKey = opts.apiKey || process.env.BYTEROVER_API_KEY
5
+ this.base = opts.base || "https://api.byterover.com"
6
+ this.userId = opts.userId || 'default'
7
+ }
8
+ getRequiredEnv() { return ["BYTEROVER_API_KEY"] }
9
+ _headers() {
10
+ if (!this.apiKey) throw new Error('ByteroverMemory: BYTEROVER_API_KEY required')
11
+ return { authorization: `Bearer ${this.apiKey}`, 'content-type': 'application/json' }
12
+ }
13
+ async syncTurn(messages) {
14
+ const res = await fetch(`${this.base}/memories`, { method: 'POST', headers: this._headers(), body: JSON.stringify({ user_id: this.userId, messages }) })
15
+ return { status: res.status, ok: res.ok }
16
+ }
17
+ async prefetch(query) {
18
+ const url = `${this.base}/memories/search?query=${encodeURIComponent(query || '')}&user_id=${encodeURIComponent(this.userId)}`
19
+ const res = await fetch(url, { headers: this._headers() })
20
+ if (!res.ok) return { items: [], status: res.status }
21
+ return { items: await res.json() }
22
+ }
23
+ async shutdown() {}
24
+ async postSetup() {}
25
+ }
@@ -0,0 +1,25 @@
1
+ export class HindsightMemory {
2
+ constructor(opts = {}) {
3
+ this.name = 'hindsight'
4
+ this.apiKey = opts.apiKey || process.env.HINDSIGHT_API_KEY
5
+ this.base = opts.base || "https://api.hindsightai.com"
6
+ this.userId = opts.userId || 'default'
7
+ }
8
+ getRequiredEnv() { return ["HINDSIGHT_API_KEY"] }
9
+ _headers() {
10
+ if (!this.apiKey) throw new Error('HindsightMemory: HINDSIGHT_API_KEY required')
11
+ return { authorization: `Bearer ${this.apiKey}`, 'content-type': 'application/json' }
12
+ }
13
+ async syncTurn(messages) {
14
+ const res = await fetch(`${this.base}/memories`, { method: 'POST', headers: this._headers(), body: JSON.stringify({ user_id: this.userId, messages }) })
15
+ return { status: res.status, ok: res.ok }
16
+ }
17
+ async prefetch(query) {
18
+ const url = `${this.base}/memories/search?query=${encodeURIComponent(query || '')}&user_id=${encodeURIComponent(this.userId)}`
19
+ const res = await fetch(url, { headers: this._headers() })
20
+ if (!res.ok) return { items: [], status: res.status }
21
+ return { items: await res.json() }
22
+ }
23
+ async shutdown() {}
24
+ async postSetup() {}
25
+ }
@@ -0,0 +1,31 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { getFophHome } from '../../home.js'
4
+
5
+ export class HolographicMemory {
6
+ constructor(opts = {}) {
7
+ this.name = 'holographic'
8
+ this.dir = opts.dir || path.join(getFophHome(), 'memory', 'holographic')
9
+ fs.mkdirSync(this.dir, { recursive: true })
10
+ }
11
+ getRequiredEnv() { return [] }
12
+ async syncTurn(messages) {
13
+ const file = path.join(this.dir, Date.now() + '.json')
14
+ fs.writeFileSync(file, JSON.stringify({ ts: Date.now(), messages }), 'utf8')
15
+ return { stored: file }
16
+ }
17
+ async prefetch(query) {
18
+ const files = fs.readdirSync(this.dir).filter(f => f.endsWith('.json')).sort().slice(-20)
19
+ const items = []
20
+ for (const f of files) {
21
+ try {
22
+ const data = JSON.parse(fs.readFileSync(path.join(this.dir, f), 'utf8'))
23
+ const last = data.messages?.slice(-2).map(m => m.content).join(' ') || ''
24
+ if (!query || last.toLowerCase().includes(String(query).toLowerCase())) items.push({ file: f, summary: last.slice(0, 200) })
25
+ } catch {}
26
+ }
27
+ return { items }
28
+ }
29
+ async shutdown() {}
30
+ async postSetup() {}
31
+ }
@@ -0,0 +1,25 @@
1
+ export class HonchoMemory {
2
+ constructor(opts = {}) {
3
+ this.name = 'honcho'
4
+ this.apiKey = opts.apiKey || process.env.HONCHO_API_KEY
5
+ this.base = opts.base || "https://api.honcho.dev"
6
+ this.userId = opts.userId || 'default'
7
+ }
8
+ getRequiredEnv() { return ["HONCHO_API_KEY"] }
9
+ _headers() {
10
+ if (!this.apiKey) throw new Error('HonchoMemory: HONCHO_API_KEY required')
11
+ return { authorization: `Bearer ${this.apiKey}`, 'content-type': 'application/json' }
12
+ }
13
+ async syncTurn(messages) {
14
+ const res = await fetch(`${this.base}/memories`, { method: 'POST', headers: this._headers(), body: JSON.stringify({ user_id: this.userId, messages }) })
15
+ return { status: res.status, ok: res.ok }
16
+ }
17
+ async prefetch(query) {
18
+ const url = `${this.base}/memories/search?query=${encodeURIComponent(query || '')}&user_id=${encodeURIComponent(this.userId)}`
19
+ const res = await fetch(url, { headers: this._headers() })
20
+ if (!res.ok) return { items: [], status: res.status }
21
+ return { items: await res.json() }
22
+ }
23
+ async shutdown() {}
24
+ async postSetup() {}
25
+ }
@@ -0,0 +1,25 @@
1
+ export class Mem0Memory {
2
+ constructor(opts = {}) {
3
+ this.name = 'mem0'
4
+ this.apiKey = opts.apiKey || process.env.MEM0_API_KEY
5
+ this.base = opts.base || "https://api.mem0.ai/v1"
6
+ this.userId = opts.userId || 'default'
7
+ }
8
+ getRequiredEnv() { return ["MEM0_API_KEY"] }
9
+ _headers() {
10
+ if (!this.apiKey) throw new Error('Mem0Memory: MEM0_API_KEY required')
11
+ return { authorization: `Bearer ${this.apiKey}`, 'content-type': 'application/json' }
12
+ }
13
+ async syncTurn(messages) {
14
+ const res = await fetch(`${this.base}/memories`, { method: 'POST', headers: this._headers(), body: JSON.stringify({ user_id: this.userId, messages }) })
15
+ return { status: res.status, ok: res.ok }
16
+ }
17
+ async prefetch(query) {
18
+ const url = `${this.base}/memories/search?query=${encodeURIComponent(query || '')}&user_id=${encodeURIComponent(this.userId)}`
19
+ const res = await fetch(url, { headers: this._headers() })
20
+ if (!res.ok) return { items: [], status: res.status }
21
+ return { items: await res.json() }
22
+ }
23
+ async shutdown() {}
24
+ async postSetup() {}
25
+ }
@@ -0,0 +1,25 @@
1
+ export class OpenvikingMemory {
2
+ constructor(opts = {}) {
3
+ this.name = 'openviking'
4
+ this.apiKey = opts.apiKey || process.env.OPENVIKING_API_KEY
5
+ this.base = opts.base || "https://api.openviking.com"
6
+ this.userId = opts.userId || 'default'
7
+ }
8
+ getRequiredEnv() { return ["OPENVIKING_API_KEY"] }
9
+ _headers() {
10
+ if (!this.apiKey) throw new Error('OpenvikingMemory: OPENVIKING_API_KEY required')
11
+ return { authorization: `Bearer ${this.apiKey}`, 'content-type': 'application/json' }
12
+ }
13
+ async syncTurn(messages) {
14
+ const res = await fetch(`${this.base}/memories`, { method: 'POST', headers: this._headers(), body: JSON.stringify({ user_id: this.userId, messages }) })
15
+ return { status: res.status, ok: res.ok }
16
+ }
17
+ async prefetch(query) {
18
+ const url = `${this.base}/memories/search?query=${encodeURIComponent(query || '')}&user_id=${encodeURIComponent(this.userId)}`
19
+ const res = await fetch(url, { headers: this._headers() })
20
+ if (!res.ok) return { items: [], status: res.status }
21
+ return { items: await res.json() }
22
+ }
23
+ async shutdown() {}
24
+ async postSetup() {}
25
+ }
@@ -0,0 +1,35 @@
1
+ import { HonchoMemory, Mem0Memory, SupermemoryMemory, ByteroverMemory, HindsightMemory, HolographicMemory, OpenvikingMemory, RetaindbMemory } from './_index.js'
2
+
3
+ const _registered = new Map()
4
+
5
+ export class MemoryProvider {
6
+ constructor(opts = {}) { this.opts = opts; this.name = 'base' }
7
+ async syncTurn(_messages) { throw new Error('syncTurn not implemented') }
8
+ async prefetch(_query) { throw new Error('prefetch not implemented') }
9
+ async shutdown() {}
10
+ async postSetup(_home, _config) {}
11
+ getRequiredEnv() { return [] }
12
+ }
13
+
14
+ export function registerMemoryProvider(name, factory) { _registered.set(name, factory) }
15
+ export function listMemoryProviders() { return [..._registered.keys()] }
16
+ export function createMemoryProvider(name, opts) {
17
+ const factory = _registered.get(name)
18
+ if (!factory) throw new Error(`unknown memory provider: ${name}. Available: ${[..._registered.keys()].join(',') || 'none'}`)
19
+ return factory(opts)
20
+ }
21
+
22
+ const PROVIDERS = {
23
+ honcho: HonchoMemory,
24
+ mem0: Mem0Memory,
25
+ supermemory: SupermemoryMemory,
26
+ byterover: ByteroverMemory,
27
+ hindsight: HindsightMemory,
28
+ holographic: HolographicMemory,
29
+ openviking: OpenvikingMemory,
30
+ retaindb: RetaindbMemory,
31
+ }
32
+
33
+ for (const [name, Cls] of Object.entries(PROVIDERS)) {
34
+ registerMemoryProvider(name, (opts) => new Cls(opts))
35
+ }
@@ -0,0 +1,25 @@
1
+ export class RetaindbMemory {
2
+ constructor(opts = {}) {
3
+ this.name = 'retaindb'
4
+ this.apiKey = opts.apiKey || process.env.RETAINDB_API_KEY
5
+ this.base = opts.base || "https://api.retaindb.com"
6
+ this.userId = opts.userId || 'default'
7
+ }
8
+ getRequiredEnv() { return ["RETAINDB_API_KEY"] }
9
+ _headers() {
10
+ if (!this.apiKey) throw new Error('RetaindbMemory: RETAINDB_API_KEY required')
11
+ return { authorization: `Bearer ${this.apiKey}`, 'content-type': 'application/json' }
12
+ }
13
+ async syncTurn(messages) {
14
+ const res = await fetch(`${this.base}/memories`, { method: 'POST', headers: this._headers(), body: JSON.stringify({ user_id: this.userId, messages }) })
15
+ return { status: res.status, ok: res.ok }
16
+ }
17
+ async prefetch(query) {
18
+ const url = `${this.base}/memories/search?query=${encodeURIComponent(query || '')}&user_id=${encodeURIComponent(this.userId)}`
19
+ const res = await fetch(url, { headers: this._headers() })
20
+ if (!res.ok) return { items: [], status: res.status }
21
+ return { items: await res.json() }
22
+ }
23
+ async shutdown() {}
24
+ async postSetup() {}
25
+ }
@@ -0,0 +1,25 @@
1
+ export class SupermemoryMemory {
2
+ constructor(opts = {}) {
3
+ this.name = 'supermemory'
4
+ this.apiKey = opts.apiKey || process.env.SUPERMEMORY_API_KEY
5
+ this.base = opts.base || "https://api.supermemory.ai/v3"
6
+ this.userId = opts.userId || 'default'
7
+ }
8
+ getRequiredEnv() { return ["SUPERMEMORY_API_KEY"] }
9
+ _headers() {
10
+ if (!this.apiKey) throw new Error('SupermemoryMemory: SUPERMEMORY_API_KEY required')
11
+ return { authorization: `Bearer ${this.apiKey}`, 'content-type': 'application/json' }
12
+ }
13
+ async syncTurn(messages) {
14
+ const res = await fetch(`${this.base}/memories`, { method: 'POST', headers: this._headers(), body: JSON.stringify({ user_id: this.userId, messages }) })
15
+ return { status: res.status, ok: res.ok }
16
+ }
17
+ async prefetch(query) {
18
+ const url = `${this.base}/memories/search?query=${encodeURIComponent(query || '')}&user_id=${encodeURIComponent(this.userId)}`
19
+ const res = await fetch(url, { headers: this._headers() })
20
+ if (!res.ok) return { items: [], status: res.status }
21
+ return { items: await res.json() }
22
+ }
23
+ async shutdown() {}
24
+ async postSetup() {}
25
+ }
@@ -0,0 +1,18 @@
1
+ import { listDebug, snapshotAll } from '../../observability/debug.js'
2
+
3
+ const _counters = new Map()
4
+ export function inc(name, delta = 1) { _counters.set(name, (_counters.get(name) || 0) + delta) }
5
+ export function counters() { return Object.fromEntries(_counters) }
6
+ export function metricsText() {
7
+ const lines = ['# HELP foph_counter generic counter', '# TYPE foph_counter counter']
8
+ for (const [name, value] of _counters) lines.push(`foph_counter{name="${name}"} ${value}`)
9
+ for (const sub of listDebug()) lines.push(`foph_subsystem{name="${sub}"} 1`)
10
+ return lines.join('\n') + '\n'
11
+ }
12
+ export const plugin = {
13
+ name: 'observability',
14
+ register: (ctx) => {
15
+ ctx.registerHook('preToolCall', async (p) => { inc('tool_calls_total'); inc(`tool_call:${p?.name || 'unknown'}`); return p })
16
+ if (typeof ctx.attachRoute === 'function') ctx.attachRoute('/metrics', (_, res) => { res.set('content-type', 'text/plain'); res.send(metricsText()) })
17
+ },
18
+ }
@@ -0,0 +1,20 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
6
+
7
+ export async function loadAllGatewayPlatforms() {
8
+ const dir = path.resolve(__dirname, '../../gateway/platforms')
9
+ const out = []
10
+ for (const f of fs.readdirSync(dir).filter(f => f.endsWith('.js'))) {
11
+ const mod = await import('file://' + path.join(dir, f).replace(/\\/g, '/'))
12
+ const cls = Object.values(mod).find(v => typeof v === 'function' && /Adapter$/.test(v.name))
13
+ if (cls) out.push({ name: f.replace(/\.js$/, ''), cls })
14
+ }
15
+ return out
16
+ }
17
+ export const plugin = {
18
+ name: 'platforms',
19
+ register: () => {},
20
+ }
@@ -0,0 +1,22 @@
1
+ export const plugin = {
2
+ name: 'spotify',
3
+ register: (ctx) => {
4
+ ctx.registerTool({
5
+ name: 'spotify',
6
+ toolset: 'core',
7
+ schema: { name: 'spotify', description: 'Spotify playback control (Web API).', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['play', 'pause', 'next', 'previous', 'search', 'queue'] }, query: { type: 'string' }, uri: { type: 'string' } }, required: ['action'] } },
8
+ requiresEnv: ['SPOTIFY_ACCESS_TOKEN'],
9
+ checkFn: () => Boolean(process.env.SPOTIFY_ACCESS_TOKEN),
10
+ handler: async ({ action, query, uri }) => {
11
+ if (!process.env.SPOTIFY_ACCESS_TOKEN) return { error: 'SPOTIFY_ACCESS_TOKEN required' }
12
+ const auth = { authorization: `Bearer ${process.env.SPOTIFY_ACCESS_TOKEN}` }
13
+ const base = 'https://api.spotify.com/v1'
14
+ const map = { play: ['PUT', '/me/player/play'], pause: ['PUT', '/me/player/pause'], next: ['POST', '/me/player/next'], previous: ['POST', '/me/player/previous'] }
15
+ if (map[action]) { const r = await fetch(base + map[action][1], { method: map[action][0], headers: auth, body: uri ? JSON.stringify({ uris: [uri] }) : null }); return { status: r.status } }
16
+ if (action === 'search') return await (await fetch(`${base}/search?q=${encodeURIComponent(query)}&type=track&limit=10`, { headers: auth })).json()
17
+ if (action === 'queue') return await (await fetch(`${base}/me/player/queue?uri=${encodeURIComponent(uri)}`, { method: 'POST', headers: auth })).json()
18
+ return { error: 'unknown action' }
19
+ },
20
+ })
21
+ },
22
+ }