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
@@ -0,0 +1,131 @@
1
+ import readline from 'node:readline'
2
+ import { saveConfigValue, getConfigValue } from '../config.js'
3
+ import { getAuthStore } from '../auth.js'
4
+ import { listBuiltinSkins, setActiveSkin } from '../skin/engine.js'
5
+ import { listEnvironments } from '../tools/environments/index.js'
6
+
7
+ const PROVIDERS = ['anthropic', 'openai', 'groq', 'openrouter', 'xai', 'gemini', 'bedrock']
8
+ const ENV_BY_PROVIDER = { anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY', groq: 'GROQ_API_KEY', openrouter: 'OPENROUTER_API_KEY', xai: 'XAI_API_KEY', gemini: 'GEMINI_API_KEY', bedrock: 'AWS_ACCESS_KEY_ID' }
9
+ const TTS_PROVIDERS = ['none', 'elevenlabs', 'kittentts', 'neutts', 'espeak']
10
+ const GATEWAY_PLATFORMS = ['telegram', 'discord', 'slack', 'whatsapp', 'matrix', 'mattermost', 'signal', 'email', 'sms', 'webhook', 'feishu', 'dingtalk', 'wecom', 'weixin', 'qqbot', 'bluebubbles', 'homeassistant']
11
+
12
+ function makeAsker(input, output) {
13
+ const rl = readline.createInterface({ input, output })
14
+ const ask = (q) => new Promise(r => rl.question(q, a => r(a.trim())))
15
+ return { ask, close: () => rl.close() }
16
+ }
17
+
18
+ export async function setupModelProvider({ input = process.stdin, output = process.stdout } = {}) {
19
+ const { ask, close } = makeAsker(input, output)
20
+ output.write('providers: ' + PROVIDERS.join(', ') + '\n')
21
+ const provider = (await ask('provider [anthropic]: ')) || 'anthropic'
22
+ if (!PROVIDERS.includes(provider)) { close(); throw new Error('unknown provider: ' + provider) }
23
+ saveConfigValue('agent.provider', provider)
24
+ const key = await ask('api key (leave blank to skip): ')
25
+ if (key) await getAuthStore().setCredential(ENV_BY_PROVIDER[provider], key)
26
+ const model = await ask('default model [empty=provider default]: ')
27
+ if (model) saveConfigValue('agent.model', model)
28
+ close()
29
+ return { provider, model: model || null }
30
+ }
31
+
32
+ export async function setupTerminalBackend({ input = process.stdin, output = process.stdout } = {}) {
33
+ const { ask, close } = makeAsker(input, output)
34
+ const opts = listEnvironments()
35
+ output.write('terminal backends: ' + opts.join(', ') + '\n')
36
+ const choice = (await ask('backend [local]: ')) || 'local'
37
+ if (!opts.includes(choice)) { close(); throw new Error('unknown backend: ' + choice) }
38
+ saveConfigValue('terminal.environment', choice)
39
+ if (choice === 'docker') {
40
+ const img = (await ask('docker image [ubuntu:22.04]: ')) || 'ubuntu:22.04'
41
+ saveConfigValue('terminal.docker_image', img)
42
+ } else if (choice === 'ssh') {
43
+ const host = await ask('ssh host: ')
44
+ if (host) saveConfigValue('terminal.ssh_host', host)
45
+ } else if (choice === 'modal' || choice === 'managed_modal') {
46
+ const tok = await ask('MODAL_TOKEN_ID (blank to skip): ')
47
+ if (tok) await getAuthStore().setCredential('MODAL_TOKEN_ID', tok)
48
+ } else if (choice === 'daytona') {
49
+ const k = await ask('DAYTONA_API_KEY (blank to skip): ')
50
+ if (k) await getAuthStore().setCredential('DAYTONA_API_KEY', k)
51
+ } else if (choice === 'vercel_sandbox') {
52
+ const t = await ask('VERCEL_TOKEN (blank to skip): ')
53
+ if (t) await getAuthStore().setCredential('VERCEL_TOKEN', t)
54
+ }
55
+ close()
56
+ return { backend: choice }
57
+ }
58
+
59
+ export async function setupTts({ input = process.stdin, output = process.stdout } = {}) {
60
+ const { ask, close } = makeAsker(input, output)
61
+ output.write('tts providers: ' + TTS_PROVIDERS.join(', ') + '\n')
62
+ const choice = (await ask('tts [none]: ')) || 'none'
63
+ if (!TTS_PROVIDERS.includes(choice)) { close(); throw new Error('unknown tts: ' + choice) }
64
+ saveConfigValue('tts.provider', choice)
65
+ if (choice === 'elevenlabs') {
66
+ const k = await ask('ELEVENLABS_API_KEY (blank to skip): ')
67
+ if (k) await getAuthStore().setCredential('ELEVENLABS_API_KEY', k)
68
+ }
69
+ close()
70
+ return { tts: choice }
71
+ }
72
+
73
+ export async function setupGatewayPlatform(name, { input = process.stdin, output = process.stdout } = {}) {
74
+ if (!GATEWAY_PLATFORMS.includes(name)) throw new Error('unknown platform: ' + name)
75
+ const { ask, close } = makeAsker(input, output)
76
+ const result = { platform: name }
77
+ const tokenEnv = {
78
+ telegram: 'TELEGRAM_BOT_TOKEN', discord: 'DISCORD_BOT_TOKEN', slack: 'SLACK_BOT_TOKEN', signal: 'SIGNAL_API_TOKEN',
79
+ matrix: 'MATRIX_ACCESS_TOKEN', mattermost: 'MATTERMOST_TOKEN', feishu: 'FEISHU_APP_SECRET', dingtalk: 'DINGTALK_APP_SECRET',
80
+ wecom: 'WECOM_APP_SECRET', weixin: 'WEIXIN_APP_SECRET', qqbot: 'QQ_BOT_TOKEN', bluebubbles: 'BLUEBUBBLES_PASSWORD',
81
+ whatsapp: 'WHATSAPP_TOKEN', email: 'IMAP_PASSWORD', sms: 'TWILIO_AUTH_TOKEN', homeassistant: 'HASS_TOKEN',
82
+ }[name]
83
+ if (tokenEnv) {
84
+ const v = await ask(tokenEnv + ' (blank to skip): ')
85
+ if (v) { await getAuthStore().setCredential(tokenEnv, v); result.tokenSaved = true }
86
+ }
87
+ saveConfigValue('gateway.platforms.' + name + '.enabled', true)
88
+ close()
89
+ return result
90
+ }
91
+
92
+ export async function setupAgentSettings({ input = process.stdin, output = process.stdout } = {}) {
93
+ const { ask, close } = makeAsker(input, output)
94
+ const maxIter = parseInt((await ask('max iterations [90]: ')) || '90', 10)
95
+ saveConfigValue('agent.max_iterations', maxIter)
96
+ const compress = ((await ask('enable compression [y]: ')) || 'y').toLowerCase() === 'y'
97
+ saveConfigValue('agent.compression.enabled', compress)
98
+ close()
99
+ return { max_iterations: maxIter, compression: compress }
100
+ }
101
+
102
+ export async function setupSkin({ input = process.stdin, output = process.stdout } = {}) {
103
+ const { ask, close } = makeAsker(input, output)
104
+ output.write('skins: ' + listBuiltinSkins().join(', ') + '\n')
105
+ const skin = (await ask('skin [default]: ')) || 'default'
106
+ setActiveSkin(skin)
107
+ saveConfigValue('display.skin', skin)
108
+ close()
109
+ return { skin }
110
+ }
111
+
112
+ export async function setupWizard({ input = process.stdin, output = process.stdout } = {}) {
113
+ output.write('freddie setup wizard\n')
114
+ const provider = await setupModelProvider({ input, output })
115
+ const backend = await setupTerminalBackend({ input, output })
116
+ const tts = await setupTts({ input, output })
117
+ const agent = await setupAgentSettings({ input, output })
118
+ const skin = await setupSkin({ input, output })
119
+ return { ...provider, ...backend, ...tts, ...agent, ...skin }
120
+ }
121
+
122
+ export function getSetupStatus() {
123
+ return {
124
+ provider: getConfigValue('agent.provider'),
125
+ terminal: getConfigValue('terminal.environment', 'local'),
126
+ tts: getConfigValue('tts.provider', 'none'),
127
+ skin: getConfigValue('display.skin', 'default'),
128
+ }
129
+ }
130
+
131
+ export { PROVIDERS, TTS_PROVIDERS, GATEWAY_PLATFORMS }
@@ -0,0 +1,6 @@
1
+ import { getConfigValue, saveConfigValue } from '../config.js'
2
+ export function getSkillsConfig() { return getConfigValue('skills.config', {}) || {} }
3
+ export function setSkillConfig(name, opts) { const cfg = getSkillsConfig(); cfg[name] = { ...(cfg[name] || {}), ...opts }; saveConfigValue('skills.config', cfg); return cfg[name] }
4
+ export function disableSkill(name) { return setSkillConfig(name, { disabled: true }) }
5
+ export function enableSkill(name) { const cfg = getSkillsConfig(); delete cfg[name]?.disabled; saveConfigValue('skills.config', cfg); return cfg[name] }
6
+ export function isSkillEnabled(name) { return !getSkillsConfig()[name]?.disabled }
@@ -0,0 +1,8 @@
1
+ const HUB_INDEX = 'https://raw.githubusercontent.com/AnEntrypoint/freddie-skills/main/index.json'
2
+ export async function fetchHub() { try { const r = await fetch(HUB_INDEX); return r.ok ? await r.json() : { items: [], error: 'fetch ' + r.status } } catch (e) { return { items: [], error: String(e.message || e) } } }
3
+ export async function searchHub(query) {
4
+ const data = await fetchHub()
5
+ if (data.error) return data
6
+ const q = String(query || '').toLowerCase()
7
+ return { items: (data.items || data || []).filter(i => !q || (i.name + ' ' + (i.description || '')).toLowerCase().includes(q)) }
8
+ }
@@ -0,0 +1,17 @@
1
+ import { listAll as listProfiles } from './profiles_cli.js'
2
+ import { getConfigValue, saveConfigValue } from '../config.js'
3
+ export const SLACK_SUBCOMMANDS = ['login', 'channels', 'send', 'config']
4
+ export async function slackChannels() {
5
+ const token = process.env.SLACK_BOT_TOKEN
6
+ if (!token) return { error: 'SLACK_BOT_TOKEN required' }
7
+ const r = await fetch('https://slack.com/api/conversations.list', { headers: { authorization: 'Bearer ' + token } })
8
+ return await r.json()
9
+ }
10
+ export async function slackSend({ channel, text }) {
11
+ const token = process.env.SLACK_BOT_TOKEN
12
+ if (!token) return { error: 'SLACK_BOT_TOKEN required' }
13
+ const r = await fetch('https://slack.com/api/chat.postMessage', { method: 'POST', headers: { authorization: 'Bearer ' + token, 'content-type': 'application/json' }, body: JSON.stringify({ channel, text }) })
14
+ return await r.json()
15
+ }
16
+ export function slackConfig() { return { profile: listProfiles().active, gateway_enabled: getConfigValue('gateway.platforms.slack.enabled') } }
17
+ export function setSlackEnabled(enabled) { saveConfigValue('gateway.platforms.slack.enabled', Boolean(enabled)); return { enabled: Boolean(enabled) } }
@@ -0,0 +1,10 @@
1
+ import { activeRuntime } from './runtime_provider.js'
2
+ import { activeProfile, listAll as listProfiles } from './profiles_cli.js'
3
+ import { getActiveSkin } from '../skin/engine.js'
4
+ import { listSessions } from '../sessions.js'
5
+ import { totalLifetime } from '../agent/account_usage.js'
6
+ import { runDoctor } from './doctor.js'
7
+ export async function systemStatus() {
8
+ const rt = await activeRuntime()
9
+ return { runtime: rt, profile: activeProfile(), skin: getActiveSkin().name, sessions: listSessions(5).length, lifetimeUsage: totalLifetime(), doctor: runDoctor().filter(c => !c.ok) }
10
+ }
@@ -0,0 +1,5 @@
1
+ import { getConfigValue, saveConfigValue } from '../config.js'
2
+ export const DEFAULTS = { agent_turn_ms: 60_000, tool_call_ms: 30_000, llm_call_ms: 90_000, batch_item_ms: 60_000, gateway_inbound_ms: 5_000 }
3
+ export function getTimeout(key) { return getConfigValue('timeouts.' + key, DEFAULTS[key] ?? 30_000) }
4
+ export function setTimeout_(key, ms) { saveConfigValue('timeouts.' + key, Number(ms)); return { key, ms: Number(ms) } }
5
+ export function listTimeouts() { return Object.fromEntries(Object.keys(DEFAULTS).map(k => [k, getTimeout(k)])) }
@@ -0,0 +1,14 @@
1
+ export const TIPS = [
2
+ 'Use /skill <name> to inject a skill body as a user message — preserves prompt cache.',
3
+ 'Profiles isolate state: freddie profile create <name>; FREDDIE_PROFILE=<name> freddie ...',
4
+ 'freddie doctor checks env, deps, config — run when something feels off.',
5
+ 'freddie dump exports your config + sessions to JSON for backup.',
6
+ 'Set FREDDIE_DEBUG=1 to see verbose logs.',
7
+ 'freddie dashboard runs a webjsx UI on a local port.',
8
+ '/cron add "*/15 * * * *" "your prompt" schedules a recurring run.',
9
+ 'freddie batch <file.txt> runs many prompts in parallel.',
10
+ 'Skin not for you? freddie skin ares|mono|slate.',
11
+ 'Memory provider: freddie memory-setup.',
12
+ ]
13
+ export function randomTip() { return TIPS[Math.floor(Math.random() * TIPS.length)] }
14
+ export function listTips() { return TIPS }
@@ -0,0 +1,15 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { getFophHome } from '../home.js'
4
+
5
+ function file() { return path.join(getFophHome(), 'tools_config.json') }
6
+ export function loadToolsConfig() { try { return JSON.parse(fs.readFileSync(file(), 'utf8')) } catch { return {} } }
7
+ export function saveToolsConfig(cfg) { fs.writeFileSync(file(), JSON.stringify(cfg, null, 2), 'utf8') }
8
+ export function setToolOverride(toolName, override) {
9
+ const cfg = loadToolsConfig()
10
+ cfg[toolName] = { ...(cfg[toolName] || {}), ...override }
11
+ saveToolsConfig(cfg)
12
+ return cfg[toolName]
13
+ }
14
+ export function getToolOverride(toolName) { return loadToolsConfig()[toolName] || null }
15
+ export function clearToolOverride(toolName) { const cfg = loadToolsConfig(); delete cfg[toolName]; saveToolsConfig(cfg); return true }
@@ -0,0 +1,8 @@
1
+ import fs from 'node:fs'
2
+ import { getFophHome } from '../home.js'
3
+ export function uninstall({ keepData = true } = {}) {
4
+ const home = getFophHome()
5
+ const removed = []
6
+ if (!keepData && fs.existsSync(home)) { fs.rmSync(home, { recursive: true, force: true }); removed.push(home) }
7
+ return { removed, keepData, hint: 'npm uninstall -g freddie (or remove your local checkout)' }
8
+ }
@@ -0,0 +1,13 @@
1
+ import { getAuthStore } from '../auth.js'
2
+ const KEY = 'VERCEL_TOKEN'
3
+ export async function getVercelToken() {
4
+ if (process.env.VERCEL_TOKEN) return { source: 'env', value: process.env.VERCEL_TOKEN }
5
+ const s = await getAuthStore().getCredential(KEY)
6
+ return s?.value ? { source: 'auth-store', value: s.value } : { source: 'none', value: null }
7
+ }
8
+ export async function setVercelToken(t) { return await getAuthStore().setCredential(KEY, t) }
9
+ export async function listVercelProjects() {
10
+ const t = (await getVercelToken()).value
11
+ if (!t) return { error: 'VERCEL_TOKEN required' }
12
+ return await (await fetch('https://api.vercel.com/v9/projects', { headers: { authorization: 'Bearer ' + t } })).json()
13
+ }
@@ -0,0 +1,6 @@
1
+ import { getConfigValue, saveConfigValue } from '../config.js'
2
+ export function voiceState() { return Boolean(getConfigValue('voice.enabled')) }
3
+ export function enable() { saveConfigValue('voice.enabled', true); return { enabled: true } }
4
+ export function disable() { saveConfigValue('voice.enabled', false); return { enabled: false } }
5
+ export function setBackend(name) { saveConfigValue('voice.backend', name); return { backend: name } }
6
+ export function getBackend() { return getConfigValue('voice.backend', 'openai') }
@@ -0,0 +1,13 @@
1
+ import { createDashboard } from '../web/server.js'
2
+ import { getConfigValue } from '../config.js'
3
+
4
+ let _dashboard = null
5
+
6
+ export async function start({ port = null } = {}) {
7
+ if (_dashboard) return _dashboard
8
+ const p = port || getConfigValue('web.port', 0)
9
+ _dashboard = await createDashboard({ port: Number(p) })
10
+ return _dashboard
11
+ }
12
+ export async function stop() { if (_dashboard) { await _dashboard.stop(); _dashboard = null } }
13
+ export function status() { return _dashboard ? { running: true, url: _dashboard.url, port: _dashboard.port } : { running: false } }
@@ -0,0 +1,12 @@
1
+ import express from 'express'
2
+ let _server = null
3
+ const _routes = new Map()
4
+ export async function startWebhookListener({ port = 0, path = '/webhook' } = {}) {
5
+ if (_server) return _server
6
+ const app = express(); app.use(express.json())
7
+ app.post(path, (req, res) => { for (const fn of _routes.values()) try { fn(req.body) } catch {} res.json({ ok: true }) })
8
+ _server = await new Promise(r => { const s = app.listen(port, () => r(s)) })
9
+ return { port: _server.address().port, path }
10
+ }
11
+ export function onWebhook(name, fn) { _routes.set(name, fn); return () => _routes.delete(name) }
12
+ export async function stopWebhookListener() { if (_server) await new Promise(r => _server.close(() => r())); _server = null; _routes.clear() }
@@ -0,0 +1,72 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { getProfilesRoot, listProfiles, applyProfileOverride } from '../home.js'
4
+
5
+ export function createProfile(name) {
6
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) throw new Error('profile name must match [a-zA-Z0-9_-]+')
7
+ const p = path.join(getProfilesRoot(), name)
8
+ if (fs.existsSync(p)) throw new Error(`profile exists: ${name}`)
9
+ fs.mkdirSync(p, { recursive: true })
10
+ return p
11
+ }
12
+
13
+ export function deleteProfile(name) {
14
+ const p = path.join(getProfilesRoot(), name)
15
+ if (!fs.existsSync(p)) throw new Error(`profile not found: ${name}`)
16
+ fs.rmSync(p, { recursive: true, force: true })
17
+ return name
18
+ }
19
+
20
+ export function switchProfile(name) {
21
+ if (name && name !== 'default') {
22
+ const p = path.join(getProfilesRoot(), name)
23
+ if (!fs.existsSync(p)) throw new Error(`profile not found: ${name}`)
24
+ }
25
+ applyProfileOverride(name)
26
+ return name || 'default'
27
+ }
28
+
29
+ export function listAllProfiles() { return ['default', ...listProfiles()] }
30
+
31
+ export function renameProfile(from, to) {
32
+ if (!/^[a-zA-Z0-9_-]+$/.test(to)) throw new Error('profile name must match [a-zA-Z0-9_-]+')
33
+ const src = path.join(getProfilesRoot(), from)
34
+ const dst = path.join(getProfilesRoot(), to)
35
+ if (!fs.existsSync(src)) throw new Error('profile not found: ' + from)
36
+ if (fs.existsSync(dst)) throw new Error('profile exists: ' + to)
37
+ fs.renameSync(src, dst)
38
+ return to
39
+ }
40
+
41
+ const IGNORE = new Set(['logs', 'sessions.db', 'sessions.db-wal', 'sessions.db-shm', '.lock'])
42
+
43
+ function copyFiltered(src, dst) {
44
+ fs.mkdirSync(dst, { recursive: true })
45
+ for (const ent of fs.readdirSync(src, { withFileTypes: true })) {
46
+ if (IGNORE.has(ent.name)) continue
47
+ const a = path.join(src, ent.name), b = path.join(dst, ent.name)
48
+ if (ent.isDirectory()) copyFiltered(a, b)
49
+ else if (ent.isFile()) fs.copyFileSync(a, b)
50
+ }
51
+ }
52
+
53
+ export function exportProfile(name, outDir) {
54
+ const src = path.join(getProfilesRoot(), name)
55
+ if (!fs.existsSync(src)) throw new Error('profile not found: ' + name)
56
+ const out = path.join(outDir, name + '-profile')
57
+ copyFiltered(src, out)
58
+ fs.writeFileSync(path.join(out, 'freddie-profile.json'), JSON.stringify({ name, exported: new Date().toISOString(), version: 1 }, null, 2))
59
+ return out
60
+ }
61
+
62
+ export function importProfile(srcDir, name) {
63
+ const meta = path.join(srcDir, 'freddie-profile.json')
64
+ if (!fs.existsSync(meta)) throw new Error('not a freddie profile export: missing freddie-profile.json')
65
+ const target = name || JSON.parse(fs.readFileSync(meta, 'utf8')).name
66
+ if (!/^[a-zA-Z0-9_-]+$/.test(target)) throw new Error('invalid profile name: ' + target)
67
+ const dst = path.join(getProfilesRoot(), target)
68
+ if (fs.existsSync(dst)) throw new Error('profile exists: ' + target)
69
+ copyFiltered(srcDir, dst)
70
+ fs.unlinkSync(path.join(dst, 'freddie-profile.json'))
71
+ return target
72
+ }
@@ -0,0 +1,94 @@
1
+ export const COMMAND_REGISTRY = [
2
+ cmd('help', 'Show available commands', 'Info', { aliases: ['h', '?'] }),
3
+ cmd('quit', 'Exit freddie', 'Exit', { aliases: ['exit', 'q'] }),
4
+ cmd('clear', 'Clear conversation history', 'Session', { aliases: ['cls'] }),
5
+ cmd('resume', 'Resume a previous session', 'Session', { args_hint: '[session-id]' }),
6
+ cmd('sessions', 'List recent sessions', 'Session'),
7
+ cmd('search', 'Search messages full-text', 'Session', { args_hint: '<query>' }),
8
+ cmd('background', 'Run command in background', 'Session', { aliases: ['bg'], args_hint: '<prompt>' }),
9
+ cmd('model', 'Set or show current model', 'Configuration', { args_hint: '[model]' }),
10
+ cmd('provider', 'Set or show current provider', 'Configuration', { args_hint: '[provider]' }),
11
+ cmd('toolsets', 'List or toggle toolsets', 'Tools & Skills'),
12
+ cmd('tools', 'List available tools', 'Tools & Skills'),
13
+ cmd('skills', 'Skills hub: install, list, run', 'Tools & Skills'),
14
+ cmd('skill', 'Run a skill by name', 'Tools & Skills', { args_hint: '<name>' }),
15
+ cmd('memory', 'Memory provider commands', 'Configuration'),
16
+ cmd('skin', 'Switch CLI skin', 'Configuration', { args_hint: '[name]' }),
17
+ cmd('profile', 'Manage profiles', 'Configuration', { args_hint: '<list|create|switch|delete>' }),
18
+ cmd('logs', 'Tail logs', 'Info'),
19
+ cmd('config', 'Show/set config values', 'Configuration', { args_hint: '[key] [value]' }),
20
+ cmd('status', 'Agent + gateway status', 'Info'),
21
+ cmd('copy', 'Copy last response', 'Session', { cli_only: true }),
22
+ cmd('paste', 'Paste from clipboard', 'Session', { cli_only: true }),
23
+ cmd('reset', 'Reset session state', 'Session'),
24
+ ]
25
+
26
+ function cmd(name, description, category, opts = {}) {
27
+ return {
28
+ name, description, category,
29
+ aliases: opts.aliases || [],
30
+ args_hint: opts.args_hint || '',
31
+ cli_only: !!opts.cli_only,
32
+ gateway_only: !!opts.gateway_only,
33
+ gateway_config_gate: opts.gateway_config_gate || null,
34
+ }
35
+ }
36
+
37
+ const _alias = new Map()
38
+ for (const c of COMMAND_REGISTRY) {
39
+ _alias.set(c.name, c.name)
40
+ for (const a of c.aliases) _alias.set(a, c.name)
41
+ }
42
+
43
+ export function resolveCommand(input) {
44
+ if (!input) return null
45
+ const stripped = input.replace(/^\//, '').split(/\s+/)[0]
46
+ return _alias.get(stripped) || null
47
+ }
48
+
49
+ export function getCommand(name) {
50
+ return COMMAND_REGISTRY.find(c => c.name === name) || null
51
+ }
52
+
53
+ export const GATEWAY_KNOWN_COMMANDS = new Set(
54
+ COMMAND_REGISTRY.filter(c => !c.cli_only || c.gateway_config_gate).map(c => c.name)
55
+ )
56
+
57
+ export function gatewayHelpLines() {
58
+ return COMMAND_REGISTRY.filter(c => !c.cli_only || c.gateway_config_gate)
59
+ .map(c => `/${c.name}${c.args_hint ? ' ' + c.args_hint : ''} — ${c.description}`)
60
+ }
61
+
62
+ export function telegramBotCommands() {
63
+ return COMMAND_REGISTRY.filter(c => !c.cli_only).map(c => ({ command: c.name, description: c.description }))
64
+ }
65
+
66
+ export function slackSubcommandMap() {
67
+ const out = {}
68
+ for (const c of COMMAND_REGISTRY) if (!c.cli_only) out[c.name] = c.description
69
+ return out
70
+ }
71
+
72
+ export function slackAppManifest({ appName = 'freddie', botUserName = 'freddie' } = {}) {
73
+ return {
74
+ display_information: { name: appName, description: 'Freddie agent', background_color: '#1a1a1a' },
75
+ features: {
76
+ bot_user: { display_name: botUserName, always_online: true },
77
+ slash_commands: [{ command: '/' + appName, description: 'Talk to ' + appName, usage_hint: '<message or /command>', should_escape: false }],
78
+ },
79
+ oauth_config: { scopes: { bot: ['app_mentions:read', 'chat:write', 'commands', 'im:history', 'im:read', 'im:write', 'users:read'] } },
80
+ settings: { event_subscriptions: { bot_events: ['app_mention', 'message.im'] }, interactivity: { is_enabled: true }, org_deploy_enabled: false, socket_mode_enabled: true },
81
+ }
82
+ }
83
+
84
+ export function discordSkillCommands() {
85
+ return COMMAND_REGISTRY.filter(c => !c.cli_only && c.category !== 'Exit').map(c => ({ name: c.name, description: c.description.slice(0, 100) || c.name }))
86
+ }
87
+
88
+ export const COMMANDS = Object.fromEntries(
89
+ COMMAND_REGISTRY.flatMap(c => [[c.name, c], ...c.aliases.map(a => [a, c])])
90
+ )
91
+
92
+ export const COMMANDS_BY_CATEGORY = COMMAND_REGISTRY.reduce((acc, c) => {
93
+ (acc[c.category] = acc[c.category] || []).push(c); return acc
94
+ }, {})
package/src/config.js ADDED
@@ -0,0 +1,125 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import yaml from 'js-yaml'
4
+ import { getFophHome } from './home.js'
5
+
6
+ export const DEFAULT_CONFIG = {
7
+ _config_version: 1,
8
+ display: { skin: 'default', tool_progress_command: false, background_process_notifications: 'all' },
9
+ agent: { provider: 'anthropic', model: '', max_iterations: 90, fallback_model: null, save_trajectories: false },
10
+ memory: { provider: null },
11
+ skills: { config: {} },
12
+ terminal: { cwd: null },
13
+ gateway: { timeout: 60, platforms: {} },
14
+ plugins: { enabled: [] },
15
+ toolsets: { enabled: ['core'], disabled: [] },
16
+ }
17
+
18
+ const MIGRATIONS = {
19
+ 1: cfg => cfg,
20
+ }
21
+
22
+ export function configPath() { return path.join(getFophHome(), 'config.yaml') }
23
+
24
+ export function loadConfig() {
25
+ const p = configPath()
26
+ if (!fs.existsSync(p)) return clone(DEFAULT_CONFIG)
27
+ const raw = yaml.load(fs.readFileSync(p, 'utf8')) || {}
28
+ const merged = deepMerge(clone(DEFAULT_CONFIG), raw)
29
+ return migrate(merged)
30
+ }
31
+
32
+ export function saveConfig(cfg) {
33
+ fs.mkdirSync(path.dirname(configPath()), { recursive: true })
34
+ fs.writeFileSync(configPath(), yaml.dump(cfg, { lineWidth: 100 }), 'utf8')
35
+ }
36
+
37
+ export function saveConfigValue(dotpath, value) {
38
+ const cfg = loadConfig()
39
+ setDot(cfg, dotpath, value)
40
+ saveConfig(cfg)
41
+ return cfg
42
+ }
43
+
44
+ export function getConfigValue(dotpath, fallback = undefined) {
45
+ const cfg = loadConfig()
46
+ return getDot(cfg, dotpath, fallback)
47
+ }
48
+
49
+ function setDot(obj, dotpath, value) {
50
+ const keys = dotpath.split('.')
51
+ let cur = obj
52
+ for (let i = 0; i < keys.length - 1; i++) {
53
+ if (typeof cur[keys[i]] !== 'object' || cur[keys[i]] === null) cur[keys[i]] = {}
54
+ cur = cur[keys[i]]
55
+ }
56
+ cur[keys[keys.length - 1]] = value
57
+ }
58
+
59
+ function getDot(obj, dotpath, fallback) {
60
+ return dotpath.split('.').reduce((c, k) => (c && k in c) ? c[k] : undefined, obj) ?? fallback
61
+ }
62
+
63
+ function deepMerge(target, src) {
64
+ if (!src || typeof src !== 'object') return target
65
+ for (const k of Object.keys(src)) {
66
+ if (src[k] && typeof src[k] === 'object' && !Array.isArray(src[k]) && target[k] && typeof target[k] === 'object' && !Array.isArray(target[k])) {
67
+ deepMerge(target[k], src[k])
68
+ } else {
69
+ target[k] = src[k]
70
+ }
71
+ }
72
+ return target
73
+ }
74
+
75
+ function migrate(cfg) {
76
+ const cur = cfg._config_version || 0
77
+ const target = DEFAULT_CONFIG._config_version
78
+ let work = cfg
79
+ for (let v = cur + 1; v <= target; v++) if (MIGRATIONS[v]) work = MIGRATIONS[v](work)
80
+ work._config_version = target
81
+ return work
82
+ }
83
+
84
+ function clone(o) { return JSON.parse(JSON.stringify(o)) }
85
+
86
+ export function validateConfigStructure(cfg) {
87
+ const issues = []
88
+ if (!cfg || typeof cfg !== 'object') return [{ path: '', message: 'config must be an object' }]
89
+ for (const [k, v] of Object.entries(DEFAULT_CONFIG)) {
90
+ if (!(k in cfg)) issues.push({ path: k, severity: 'info', message: 'missing key (will use default)' })
91
+ else if (typeof v === 'object' && !Array.isArray(v) && (typeof cfg[k] !== 'object' || Array.isArray(cfg[k]))) {
92
+ issues.push({ path: k, severity: 'warn', message: 'expected object, got ' + (Array.isArray(cfg[k]) ? 'array' : typeof cfg[k]) })
93
+ }
94
+ }
95
+ if (cfg.agent && typeof cfg.agent.max_iterations !== 'undefined' && (typeof cfg.agent.max_iterations !== 'number' || cfg.agent.max_iterations < 1)) {
96
+ issues.push({ path: 'agent.max_iterations', severity: 'error', message: 'must be a positive integer' })
97
+ }
98
+ if (cfg.toolsets && cfg.toolsets.enabled && !Array.isArray(cfg.toolsets.enabled)) {
99
+ issues.push({ path: 'toolsets.enabled', severity: 'error', message: 'must be an array' })
100
+ }
101
+ return issues
102
+ }
103
+
104
+ export function expandEnvVars(value) {
105
+ if (typeof value === 'string') return value.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, (_, name) => process.env[name] || '')
106
+ if (Array.isArray(value)) return value.map(expandEnvVars)
107
+ if (value && typeof value === 'object') { const out = {}; for (const [k, v] of Object.entries(value)) out[k] = expandEnvVars(v); return out }
108
+ return value
109
+ }
110
+
111
+ export function readRawConfig() {
112
+ const p = configPath()
113
+ return fs.existsSync(p) ? (yaml.load(fs.readFileSync(p, 'utf8')) || {}) : {}
114
+ }
115
+
116
+ export function checkConfigVersion() {
117
+ const raw = readRawConfig()
118
+ return { current: raw._config_version || 0, target: DEFAULT_CONFIG._config_version, needsMigration: (raw._config_version || 0) < DEFAULT_CONFIG._config_version }
119
+ }
120
+
121
+ export function getMissingConfigFields(cfg = loadConfig()) {
122
+ const missing = []
123
+ if (!cfg.agent?.provider) missing.push('agent.provider')
124
+ return missing
125
+ }
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { listSkills } from '../skills/index.js'
4
+
5
+ export const ContextPlugins = {
6
+ file: async ({ cwd = process.cwd() } = {}) => {
7
+ const candidates = ['.freddie-context', 'CLAUDE.md', 'AGENTS.md']
8
+ const blocks = []
9
+ for (const c of candidates) {
10
+ const p = path.join(cwd, c)
11
+ if (fs.existsSync(p)) blocks.push({ name: 'file:' + c, body: fs.readFileSync(p, 'utf8') })
12
+ }
13
+ return blocks
14
+ },
15
+ skills: async () => {
16
+ return listSkills().map(s => ({ name: 'skill:' + s.name, body: s.description }))
17
+ },
18
+ memory: async ({ provider } = {}) => {
19
+ if (!provider) return []
20
+ try {
21
+ const out = await provider.prefetch('')
22
+ return (out.items || []).slice(0, 5).map((it, i) => ({ name: 'memory:' + i, body: typeof it === 'string' ? it : JSON.stringify(it) }))
23
+ } catch { return [] }
24
+ },
25
+ }
26
+
27
+ export async function buildContext({ session = null, message = '', plugins = ['file'], options = {} } = {}) {
28
+ const blocks = []
29
+ for (const name of plugins) {
30
+ const p = ContextPlugins[name]
31
+ if (!p) continue
32
+ const got = await p({ session, message, ...options })
33
+ for (const b of got) blocks.push(b)
34
+ }
35
+ return blocks
36
+ }
37
+
38
+ export function blocksToSystemMessage(blocks) {
39
+ if (!blocks.length) return null
40
+ const body = blocks.map(b => `[${b.name}]\n${b.body}`).join('\n\n')
41
+ return { role: 'system', content: body }
42
+ }
@@ -0,0 +1,27 @@
1
+ const RANGES = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]]
2
+
3
+ export function parseCron(expr) {
4
+ const parts = expr.trim().split(/\s+/)
5
+ if (parts.length !== 5) throw new Error(`cron must have 5 fields: ${expr}`)
6
+ return parts.map((p, i) => parseField(p, RANGES[i][0], RANGES[i][1]))
7
+ }
8
+
9
+ function parseField(p, lo, hi) {
10
+ const out = new Set()
11
+ for (const piece of p.split(',')) {
12
+ let step = 1, range = piece
13
+ if (piece.includes('/')) { const [r, s] = piece.split('/'); range = r; step = Number(s) }
14
+ let from = lo, to = hi
15
+ if (range !== '*') {
16
+ if (range.includes('-')) { const [a, b] = range.split('-'); from = Number(a); to = Number(b) }
17
+ else { from = to = Number(range) }
18
+ }
19
+ for (let v = from; v <= to; v += step) out.add(v)
20
+ }
21
+ return out
22
+ }
23
+
24
+ export function matches(parsed, date) {
25
+ const fields = [date.getMinutes(), date.getHours(), date.getDate(), date.getMonth() + 1, date.getDay()]
26
+ return parsed.every((set, i) => set.has(fields[i]))
27
+ }