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,31 @@
1
+ import fs from 'node:fs'
2
+ import { registry } from './registry.js'
3
+
4
+ registry.register({
5
+ name: 'edit',
6
+ toolset: 'core',
7
+ schema: {
8
+ name: 'edit',
9
+ description: 'Replace exact string in file. Fails if old_string occurs zero or multiple times unless replace_all.',
10
+ parameters: {
11
+ type: 'object',
12
+ properties: {
13
+ path: { type: 'string' },
14
+ old_string: { type: 'string' },
15
+ new_string: { type: 'string' },
16
+ replace_all: { type: 'boolean', default: false },
17
+ },
18
+ required: ['path', 'old_string', 'new_string'],
19
+ },
20
+ },
21
+ handler: async ({ path: p, old_string, new_string, replace_all = false }) => {
22
+ if (!fs.existsSync(p)) return { error: `not found: ${p}` }
23
+ const src = fs.readFileSync(p, 'utf8')
24
+ const occurrences = src.split(old_string).length - 1
25
+ if (occurrences === 0) return { error: 'old_string not found' }
26
+ if (occurrences > 1 && !replace_all) return { error: `old_string matches ${occurrences} times; pass replace_all=true` }
27
+ const out = replace_all ? src.split(old_string).join(new_string) : src.replace(old_string, new_string)
28
+ fs.writeFileSync(p, out, 'utf8')
29
+ return { path: p, replacements: replace_all ? occurrences : 1 }
30
+ },
31
+ })
@@ -0,0 +1,15 @@
1
+ import { registry } from './registry.js'
2
+ import { getConfigValue } from '../config.js'
3
+
4
+ export function buildBashEnv() {
5
+ const allow = getConfigValue('terminal.env_passthrough', ['HOME', 'USER', 'LANG', 'PATH', 'SHELL']) || []
6
+ const out = {}
7
+ for (const k of allow) if (process.env[k]) out[k] = process.env[k]
8
+ return out
9
+ }
10
+ registry.register({
11
+ name: 'env_passthrough',
12
+ toolset: 'core',
13
+ schema: { name: 'env_passthrough', description: 'Compute the env-var subset that should be passed through to spawned shells.', parameters: { type: 'object', properties: {} } },
14
+ handler: async () => ({ env: buildBashEnv() }),
15
+ })
@@ -0,0 +1,26 @@
1
+ export class BaseEnvironment {
2
+ constructor(opts = {}) { this.opts = opts; this.cwd = opts.cwd || '/workspace'; this.name = 'base' }
3
+ async run(_cmd, _o) { throw new Error(this.name + '.run() not implemented') }
4
+ async put(_l, _r) { throw new Error(this.name + '.put() not implemented') }
5
+ async get(_r, _l) { throw new Error(this.name + '.get() not implemented') }
6
+ async shutdown() {}
7
+ async ready() { return true }
8
+ }
9
+
10
+ export async function fetchJson(url, { method = 'GET', headers = {}, body, timeoutMs = 60000 } = {}) {
11
+ const ctrl = new AbortController()
12
+ const t = setTimeout(() => ctrl.abort(), timeoutMs)
13
+ try {
14
+ const res = await fetch(url, { method, headers: { 'content-type': 'application/json', ...headers }, body: body ? JSON.stringify(body) : undefined, signal: ctrl.signal })
15
+ const txt = await res.text()
16
+ let json = null
17
+ try { json = txt ? JSON.parse(txt) : null } catch { json = { raw: txt } }
18
+ return { ok: res.ok, status: res.status, json, text: txt }
19
+ } finally { clearTimeout(t) }
20
+ }
21
+
22
+ export function requireEnv(name) {
23
+ const v = process.env[name]
24
+ if (!v) throw new Error('missing env: ' + name)
25
+ return v
26
+ }
@@ -0,0 +1,48 @@
1
+ import { BaseEnvironment, fetchJson, requireEnv } from './base.js'
2
+
3
+ export class DaytonaEnvironment extends BaseEnvironment {
4
+ constructor(opts = {}) {
5
+ super(opts)
6
+ this.name = 'daytona'
7
+ this.apiUrl = opts.apiUrl || process.env.DAYTONA_API_URL || 'https://app.daytona.io/api'
8
+ this.apiKey = opts.apiKey || process.env.DAYTONA_API_KEY
9
+ this.target = opts.target || process.env.DAYTONA_TARGET || 'us'
10
+ this.workspaceId = opts.workspaceId || null
11
+ this.cwd = opts.cwd || '/workspace'
12
+ }
13
+ headers() { return { authorization: 'Bearer ' + (this.apiKey || requireEnv('DAYTONA_API_KEY')) } }
14
+ async ensureWorkspace() {
15
+ if (this.workspaceId) return this.workspaceId
16
+ const r = await fetchJson(this.apiUrl + '/workspace', { method: 'POST', headers: this.headers(), body: { target: this.target, image: this.opts.image || 'ubuntu:22.04' } })
17
+ if (!r.ok) throw new Error('daytona create: ' + r.status + ' ' + r.text)
18
+ this.workspaceId = r.json.id
19
+ return this.workspaceId
20
+ }
21
+ async run(cmd, { timeoutMs = 120000 } = {}) {
22
+ try {
23
+ const id = await this.ensureWorkspace()
24
+ const r = await fetchJson(this.apiUrl + '/workspace/' + id + '/exec', { method: 'POST', headers: this.headers(), body: { command: cmd, cwd: this.cwd }, timeoutMs })
25
+ return { exitCode: r.json?.exit_code ?? (r.ok ? 0 : -1), stdout: r.json?.stdout || '', stderr: r.json?.stderr || (!r.ok ? r.text : '') }
26
+ } catch (e) { return { exitCode: -1, stdout: '', stderr: e.message } }
27
+ }
28
+ async put(localPath, remotePath) {
29
+ const fs = await import('node:fs')
30
+ const id = await this.ensureWorkspace()
31
+ const buf = fs.readFileSync(localPath)
32
+ const r = await fetchJson(this.apiUrl + '/workspace/' + id + '/files', { method: 'POST', headers: this.headers(), body: { path: remotePath, content: buf.toString('base64'), encoding: 'base64' } })
33
+ return { copied: remotePath, ok: r.ok }
34
+ }
35
+ async get(remotePath, localPath) {
36
+ const fs = await import('node:fs')
37
+ const id = await this.ensureWorkspace()
38
+ const r = await fetchJson(this.apiUrl + '/workspace/' + id + '/files?path=' + encodeURIComponent(remotePath), { headers: this.headers() })
39
+ if (!r.ok) return { error: r.text }
40
+ fs.writeFileSync(localPath, Buffer.from(r.json?.content || '', 'base64'))
41
+ return { copied: localPath }
42
+ }
43
+ async shutdown() {
44
+ if (!this.workspaceId) return
45
+ await fetchJson(this.apiUrl + '/workspace/' + this.workspaceId, { method: 'DELETE', headers: this.headers() })
46
+ this.workspaceId = null
47
+ }
48
+ }
@@ -0,0 +1,14 @@
1
+ export class DockerEnvironment {
2
+ constructor(opts = {}) { this.image = opts.image || 'ubuntu:latest'; this.name = 'docker' }
3
+ async run(_cmd) { throw new Error('DockerEnvironment: install dockerode and replace this method') }
4
+ async put() { throw new Error('DockerEnvironment: install dockerode') }
5
+ async get() { throw new Error('DockerEnvironment: install dockerode') }
6
+ async shutdown() {}
7
+ }
8
+
9
+ let _dockerodeAvailable = null
10
+ export async function probeDockerode() {
11
+ if (_dockerodeAvailable !== null) return _dockerodeAvailable
12
+ try { await import('dockerode'); _dockerodeAvailable = true } catch { _dockerodeAvailable = false }
13
+ return _dockerodeAvailable
14
+ }
@@ -0,0 +1,60 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import crypto from 'node:crypto'
4
+
5
+ function sha256(buf) { return crypto.createHash('sha256').update(buf).digest('hex') }
6
+
7
+ function walk(root) {
8
+ const out = []
9
+ function rec(dir, rel) {
10
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
11
+ const abs = path.join(dir, ent.name)
12
+ const r = path.posix.join(rel, ent.name)
13
+ if (ent.isDirectory()) {
14
+ if (ent.name === '.git' || ent.name === 'node_modules') continue
15
+ rec(abs, r)
16
+ } else if (ent.isFile()) {
17
+ const buf = fs.readFileSync(abs)
18
+ out.push({ rel: r, abs, hash: sha256(buf), size: buf.length })
19
+ }
20
+ }
21
+ }
22
+ rec(root, '')
23
+ return out
24
+ }
25
+
26
+ export async function syncTo(env, localRoot, remoteRoot, { onProgress } = {}) {
27
+ const files = walk(localRoot)
28
+ let transferred = 0
29
+ for (const f of files) {
30
+ const target = path.posix.join(remoteRoot, f.rel)
31
+ const r = await env.put(f.abs, target)
32
+ transferred++
33
+ if (onProgress) onProgress({ rel: f.rel, transferred, total: files.length, status: r.error ? 'error' : 'ok', error: r.error })
34
+ }
35
+ return { files: files.length, transferred }
36
+ }
37
+
38
+ export async function syncFrom(env, remoteRoot, localRoot, manifest = []) {
39
+ fs.mkdirSync(localRoot, { recursive: true })
40
+ let transferred = 0
41
+ for (const rel of manifest) {
42
+ const remote = path.posix.join(remoteRoot, rel)
43
+ const local = path.join(localRoot, rel)
44
+ fs.mkdirSync(path.dirname(local), { recursive: true })
45
+ const r = await env.get(remote, local)
46
+ if (!r.error) transferred++
47
+ }
48
+ return { transferred, total: manifest.length }
49
+ }
50
+
51
+ export function diffManifest(localRoot, remoteManifest = []) {
52
+ const local = walk(localRoot)
53
+ const localByRel = new Map(local.map(f => [f.rel, f]))
54
+ const remoteByRel = new Map(remoteManifest.map(f => [f.rel, f]))
55
+ const toUpload = local.filter(f => remoteByRel.get(f.rel)?.hash !== f.hash).map(f => f.rel)
56
+ const toDelete = remoteManifest.filter(f => !localByRel.has(f.rel)).map(f => f.rel)
57
+ return { toUpload, toDelete }
58
+ }
59
+
60
+ export { walk, sha256 }
@@ -0,0 +1,36 @@
1
+ import { LocalEnvironment } from './local.js'
2
+ import { DockerEnvironment } from './docker.js'
3
+ import { SshEnvironment } from './ssh.js'
4
+ import { ModalEnvironment, ManagedModalEnvironment } from './modal.js'
5
+ import { DaytonaEnvironment } from './daytona.js'
6
+ import { SingularityEnvironment } from './singularity.js'
7
+ import { VercelSandboxEnvironment } from './vercel_sandbox.js'
8
+ import { BaseEnvironment } from './base.js'
9
+ import { syncTo, syncFrom, diffManifest } from './file_sync.js'
10
+ import { getConfigValue } from '../../config.js'
11
+
12
+ const FACTORIES = {
13
+ local: (opts) => new LocalEnvironment(opts),
14
+ docker: (opts) => new DockerEnvironment(opts),
15
+ ssh: (opts) => new SshEnvironment(opts),
16
+ modal: (opts) => new ModalEnvironment(opts),
17
+ managed_modal: (opts) => new ManagedModalEnvironment(opts),
18
+ daytona: (opts) => new DaytonaEnvironment(opts),
19
+ singularity: (opts) => new SingularityEnvironment(opts),
20
+ vercel_sandbox: (opts) => new VercelSandboxEnvironment(opts),
21
+ }
22
+
23
+ export function listEnvironments() { return Object.keys(FACTORIES) }
24
+
25
+ export function createEnvironment(name, opts = {}) {
26
+ const f = FACTORIES[name]
27
+ if (!f) throw new Error('unknown environment: ' + name + ' (available: ' + Object.keys(FACTORIES).join(', ') + ')')
28
+ return f(opts)
29
+ }
30
+
31
+ export function defaultEnvironment(opts = {}) {
32
+ const name = getConfigValue('terminal.environment', 'local')
33
+ return createEnvironment(name, opts)
34
+ }
35
+
36
+ export { LocalEnvironment, DockerEnvironment, SshEnvironment, ModalEnvironment, ManagedModalEnvironment, DaytonaEnvironment, SingularityEnvironment, VercelSandboxEnvironment, BaseEnvironment, syncTo, syncFrom, diffManifest }
@@ -0,0 +1,31 @@
1
+ import { spawn } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ export class LocalEnvironment {
6
+ constructor(opts = {}) { this.cwd = opts.cwd || process.cwd(); this.name = 'local' }
7
+ async run(cmd, { timeoutMs = 60000 } = {}) {
8
+ return new Promise(resolve => {
9
+ const sh = process.platform === 'win32' ? 'cmd' : 'sh'
10
+ const flag = process.platform === 'win32' ? '/c' : '-c'
11
+ const child = spawn(sh, [flag, cmd], { cwd: this.cwd, env: process.env })
12
+ let stdout = '', stderr = ''
13
+ const t = setTimeout(() => { try { child.kill('SIGKILL') } catch {} resolve({ exitCode: -1, stdout, stderr: stderr + '\n[timeout]' }) }, timeoutMs)
14
+ child.stdout?.on('data', d => stdout += d.toString())
15
+ child.stderr?.on('data', d => stderr += d.toString())
16
+ child.on('close', code => { clearTimeout(t); resolve({ exitCode: code, stdout, stderr }) })
17
+ child.on('error', e => { clearTimeout(t); resolve({ exitCode: -1, stdout, stderr: stderr + '\n' + e.message }) })
18
+ })
19
+ }
20
+ async put(localPath, remotePath) {
21
+ fs.mkdirSync(path.dirname(remotePath), { recursive: true })
22
+ fs.copyFileSync(localPath, remotePath)
23
+ return { copied: remotePath }
24
+ }
25
+ async get(remotePath, localPath) {
26
+ fs.mkdirSync(path.dirname(localPath), { recursive: true })
27
+ fs.copyFileSync(remotePath, localPath)
28
+ return { copied: localPath }
29
+ }
30
+ async shutdown() {}
31
+ }
@@ -0,0 +1,33 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { BaseEnvironment } from './base.js'
3
+
4
+ export class ModalEnvironment extends BaseEnvironment {
5
+ constructor(opts = {}) {
6
+ super(opts)
7
+ this.name = 'modal'
8
+ this.app = opts.app || 'freddie-sandbox'
9
+ this.image = opts.image || 'python:3.11'
10
+ this.cwd = opts.cwd || '/sandbox'
11
+ this.token = process.env.MODAL_TOKEN_ID
12
+ this.secret = process.env.MODAL_TOKEN_SECRET
13
+ }
14
+ async run(cmd, { timeoutMs = 120000 } = {}) {
15
+ if (!this.token) return { exitCode: -1, stdout: '', stderr: 'MODAL_TOKEN_ID required' }
16
+ return new Promise(resolve => {
17
+ const env = { ...process.env, MODAL_TOKEN_ID: this.token, MODAL_TOKEN_SECRET: this.secret }
18
+ const child = spawn('modal', ['run', '--detach=false', '-q', '-', cmd], { env, shell: process.platform === 'win32' })
19
+ let stdout = '', stderr = ''
20
+ const t = setTimeout(() => { try { child.kill('SIGKILL') } catch {} resolve({ exitCode: -1, stdout, stderr: stderr + '\n[timeout]' }) }, timeoutMs)
21
+ child.stdout?.on('data', d => stdout += d.toString())
22
+ child.stderr?.on('data', d => stderr += d.toString())
23
+ child.on('close', code => { clearTimeout(t); resolve({ exitCode: code, stdout, stderr }) })
24
+ child.on('error', e => { clearTimeout(t); resolve({ exitCode: -1, stdout, stderr: stderr + '\n' + e.message }) })
25
+ })
26
+ }
27
+ async put(localPath, remotePath) { return { error: 'modal put requires modal volume put: ' + localPath + ' -> ' + remotePath } }
28
+ async get(remotePath, localPath) { return { error: 'modal get requires modal volume get: ' + remotePath + ' -> ' + localPath } }
29
+ }
30
+
31
+ export class ManagedModalEnvironment extends ModalEnvironment {
32
+ constructor(opts = {}) { super({ ...opts, app: opts.app || 'freddie-managed' }); this.name = 'managed_modal' }
33
+ }
@@ -0,0 +1,38 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { BaseEnvironment } from './base.js'
3
+
4
+ export class SingularityEnvironment extends BaseEnvironment {
5
+ constructor(opts = {}) {
6
+ super(opts)
7
+ this.name = 'singularity'
8
+ this.image = opts.image || 'docker://ubuntu:22.04'
9
+ this.binary = opts.binary || process.env.SINGULARITY_BIN || 'singularity'
10
+ this.binds = opts.binds || []
11
+ this.cwd = opts.cwd || '/workspace'
12
+ }
13
+ _bindArgs() {
14
+ const args = []
15
+ for (const b of this.binds) args.push('--bind', b)
16
+ return args
17
+ }
18
+ async run(cmd, { timeoutMs = 120000 } = {}) {
19
+ return new Promise(resolve => {
20
+ const args = ['exec', '--pwd', this.cwd, ...this._bindArgs(), this.image, 'sh', '-c', cmd]
21
+ const child = spawn(this.binary, args, { env: process.env })
22
+ let stdout = '', stderr = ''
23
+ const t = setTimeout(() => { try { child.kill('SIGKILL') } catch {} resolve({ exitCode: -1, stdout, stderr: stderr + '\n[timeout]' }) }, timeoutMs)
24
+ child.stdout?.on('data', d => stdout += d.toString())
25
+ child.stderr?.on('data', d => stderr += d.toString())
26
+ child.on('close', code => { clearTimeout(t); resolve({ exitCode: code, stdout, stderr }) })
27
+ child.on('error', e => { clearTimeout(t); resolve({ exitCode: -1, stdout, stderr: stderr + '\n' + e.message }) })
28
+ })
29
+ }
30
+ async put(localPath, remotePath) {
31
+ const r = await this.run('mkdir -p "$(dirname \'' + remotePath + '\')" && cp "' + localPath + '" "' + remotePath + '"')
32
+ return r.exitCode === 0 ? { copied: remotePath } : { error: r.stderr }
33
+ }
34
+ async get(remotePath, localPath) {
35
+ const r = await this.run('cp "' + remotePath + '" "' + localPath + '"')
36
+ return r.exitCode === 0 ? { copied: localPath } : { error: r.stderr }
37
+ }
38
+ }
@@ -0,0 +1,14 @@
1
+ export class SshEnvironment {
2
+ constructor(opts = {}) { this.host = opts.host; this.user = opts.user; this.name = 'ssh' }
3
+ async run(_cmd) { throw new Error('SshEnvironment: install ssh2 and replace this method') }
4
+ async put() { throw new Error('SshEnvironment: install ssh2') }
5
+ async get() { throw new Error('SshEnvironment: install ssh2') }
6
+ async shutdown() {}
7
+ }
8
+
9
+ let _ssh2Available = null
10
+ export async function probeSsh2() {
11
+ if (_ssh2Available !== null) return _ssh2Available
12
+ try { await import('ssh2'); _ssh2Available = true } catch { _ssh2Available = false }
13
+ return _ssh2Available
14
+ }
@@ -0,0 +1,47 @@
1
+ import { BaseEnvironment, fetchJson, requireEnv } from './base.js'
2
+
3
+ export class VercelSandboxEnvironment extends BaseEnvironment {
4
+ constructor(opts = {}) {
5
+ super(opts)
6
+ this.name = 'vercel_sandbox'
7
+ this.apiUrl = opts.apiUrl || process.env.VERCEL_SANDBOX_URL || 'https://api.vercel.com/v1/sandbox'
8
+ this.token = opts.token || process.env.VERCEL_TOKEN
9
+ this.runtime = opts.runtime || 'node22'
10
+ this.sandboxId = null
11
+ this.cwd = opts.cwd || '/vercel/sandbox'
12
+ }
13
+ headers() { return { authorization: 'Bearer ' + (this.token || requireEnv('VERCEL_TOKEN')) } }
14
+ async ensureSandbox() {
15
+ if (this.sandboxId) return this.sandboxId
16
+ const r = await fetchJson(this.apiUrl, { method: 'POST', headers: this.headers(), body: { runtime: this.runtime, timeout: this.opts.timeoutSec || 600 } })
17
+ if (!r.ok) throw new Error('vercel sandbox create: ' + r.status + ' ' + r.text)
18
+ this.sandboxId = r.json.id
19
+ return this.sandboxId
20
+ }
21
+ async run(cmd, { timeoutMs = 120000 } = {}) {
22
+ try {
23
+ const id = await this.ensureSandbox()
24
+ const r = await fetchJson(this.apiUrl + '/' + id + '/exec', { method: 'POST', headers: this.headers(), body: { cmd: ['sh', '-c', cmd], cwd: this.cwd }, timeoutMs })
25
+ return { exitCode: r.json?.exitCode ?? (r.ok ? 0 : -1), stdout: r.json?.stdout || '', stderr: r.json?.stderr || (!r.ok ? r.text : '') }
26
+ } catch (e) { return { exitCode: -1, stdout: '', stderr: e.message } }
27
+ }
28
+ async put(localPath, remotePath) {
29
+ const fs = await import('node:fs')
30
+ const id = await this.ensureSandbox()
31
+ const r = await fetchJson(this.apiUrl + '/' + id + '/files', { method: 'PUT', headers: this.headers(), body: { path: remotePath, content: fs.readFileSync(localPath).toString('base64'), encoding: 'base64' } })
32
+ return r.ok ? { copied: remotePath } : { error: r.text }
33
+ }
34
+ async get(remotePath, localPath) {
35
+ const fs = await import('node:fs')
36
+ const id = await this.ensureSandbox()
37
+ const r = await fetchJson(this.apiUrl + '/' + id + '/files?path=' + encodeURIComponent(remotePath), { headers: this.headers() })
38
+ if (!r.ok) return { error: r.text }
39
+ fs.writeFileSync(localPath, Buffer.from(r.json?.content || '', 'base64'))
40
+ return { copied: localPath }
41
+ }
42
+ async shutdown() {
43
+ if (!this.sandboxId) return
44
+ await fetchJson(this.apiUrl + '/' + this.sandboxId, { method: 'DELETE', headers: this.headers() })
45
+ this.sandboxId = null
46
+ }
47
+ }
@@ -0,0 +1,15 @@
1
+ import { registry } from './registry.js'
2
+ registry.register({
3
+ name: 'feishu_doc',
4
+ toolset: 'core',
5
+ 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'] } },
6
+ requiresEnv: ['FEISHU_APP_TOKEN'],
7
+ checkFn: () => Boolean(process.env.FEISHU_APP_TOKEN),
8
+ handler: async ({ action, doc_token, content }) => {
9
+ const auth = { authorization: `Bearer ${process.env.FEISHU_APP_TOKEN}` }
10
+ const base = 'https://open.feishu.cn/open-apis/docx/v1/documents/' + doc_token
11
+ if (action === 'get') return await (await fetch(base, { headers: auth })).json()
12
+ if (action === 'patch') return await (await fetch(base + '/blocks', { method: 'PATCH', headers: { ...auth, 'content-type': 'application/json' }, body: JSON.stringify(content) })).json()
13
+ return { error: 'unknown action' }
14
+ },
15
+ })
@@ -0,0 +1,14 @@
1
+ import { registry } from './registry.js'
2
+ registry.register({
3
+ name: 'feishu_drive',
4
+ toolset: 'core',
5
+ 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'] } },
6
+ requiresEnv: ['FEISHU_APP_TOKEN'],
7
+ checkFn: () => Boolean(process.env.FEISHU_APP_TOKEN),
8
+ handler: async ({ action, folder_token, file_token }) => {
9
+ const auth = { authorization: `Bearer ${process.env.FEISHU_APP_TOKEN}` }
10
+ if (action === 'list') return await (await fetch('https://open.feishu.cn/open-apis/drive/v1/files?folder_token=' + (folder_token || ''), { headers: auth })).json()
11
+ if (action === 'download') return await (await fetch(`https://open.feishu.cn/open-apis/drive/v1/files/${file_token}/download`, { headers: auth })).json()
12
+ return { error: 'unknown action' }
13
+ },
14
+ })
@@ -0,0 +1,17 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { registry } from './registry.js'
4
+
5
+ const ACTIONS = {
6
+ move: ({ src, dest }) => { fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.renameSync(src, dest); return { moved: dest } },
7
+ copy: ({ src, dest }) => { fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.copyFileSync(src, dest); return { copied: dest } },
8
+ delete: ({ path: p }) => { if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true }); return { deleted: p } },
9
+ mkdir: ({ path: p }) => { fs.mkdirSync(p, { recursive: true }); return { created: p } },
10
+ stat: ({ path: p }) => { const s = fs.statSync(p); return { size: s.size, mtime: s.mtimeMs, isDir: s.isDirectory() } },
11
+ }
12
+ registry.register({
13
+ name: 'file_operations',
14
+ toolset: 'core',
15
+ 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'] } },
16
+ handler: async (a) => { const fn = ACTIONS[a.action]; try { return fn ? fn(a) : { error: 'unknown action' } } catch (e) { return { error: String(e.message || e) } } },
17
+ })
@@ -0,0 +1,16 @@
1
+ import { db } from '../db.js'
2
+ import { registry } from './registry.js'
3
+
4
+ async function init() { const d = await db(); 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 }
5
+ registry.register({
6
+ name: 'file_state',
7
+ toolset: 'core',
8
+ 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'] } },
9
+ handler: async ({ action, session_id, file_path, op }) => {
10
+ const d = await init()
11
+ if (action === 'record') { d.prepare('INSERT INTO file_state (session_id, file_path, action, ts) VALUES (?, ?, ?, ?)').run(session_id, file_path, op, Date.now()); return { recorded: true } }
12
+ if (action === 'list') return { items: d.prepare('SELECT * FROM file_state WHERE session_id = ? ORDER BY id DESC').all(session_id) }
13
+ if (action === 'changed_in_session') return { files: [...new Set(d.prepare('SELECT file_path FROM file_state WHERE session_id = ?').all(session_id).map(r => r.file_path))] }
14
+ return { error: 'unknown action' }
15
+ },
16
+ })
@@ -0,0 +1,23 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { registry } from './registry.js'
4
+
5
+ function walk(dir, out, skip) {
6
+ let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }) } catch { return }
7
+ for (const e of entries) {
8
+ if (skip.has(e.name)) continue
9
+ const full = path.join(dir, e.name)
10
+ if (e.isDirectory()) walk(full, out, skip)
11
+ else out.push(full)
12
+ }
13
+ }
14
+ registry.register({
15
+ name: 'file_tools',
16
+ toolset: 'core',
17
+ 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 } } } },
18
+ handler: async ({ dir = '.', ext, limit = 1000 }) => {
19
+ const out = []; walk(dir, out, new Set(['node_modules', '.git', 'dist', '.cache', 'build']))
20
+ const filtered = ext ? out.filter(f => f.endsWith(ext)) : out
21
+ return { files: filtered.slice(0, limit), total: filtered.length, truncated: filtered.length > limit }
22
+ },
23
+ })
@@ -0,0 +1,8 @@
1
+ import { fuzzyMatch as helper } from '../utils.js'
2
+ import { registry } from './registry.js'
3
+ registry.register({
4
+ name: 'fuzzy_match',
5
+ toolset: 'core',
6
+ 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'] } },
7
+ handler: async ({ needle, haystack }) => ({ score: helper(needle, haystack) }),
8
+ })
@@ -0,0 +1,51 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { registry } from './registry.js'
4
+
5
+ registry.register({
6
+ name: 'grep',
7
+ toolset: 'core',
8
+ schema: {
9
+ name: 'grep',
10
+ description: 'Recursive regex search across files. Returns file:line:content matches.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ pattern: { type: 'string' },
15
+ path: { type: 'string', default: '.' },
16
+ glob: { type: 'string' },
17
+ head_limit: { type: 'number', default: 200 },
18
+ ignore_case: { type: 'boolean', default: false },
19
+ },
20
+ required: ['pattern'],
21
+ },
22
+ },
23
+ handler: async ({ pattern, path: root = '.', head_limit = 200, ignore_case = false, glob }) => {
24
+ const re = new RegExp(pattern, ignore_case ? 'i' : '')
25
+ const out = []
26
+ const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', '.cache'])
27
+ const walk = (d) => {
28
+ if (out.length >= head_limit) return
29
+ let entries
30
+ try { entries = fs.readdirSync(d, { withFileTypes: true }) } catch { return }
31
+ for (const e of entries) {
32
+ if (out.length >= head_limit) return
33
+ const full = path.join(d, e.name)
34
+ if (e.isDirectory()) { if (!skipDirs.has(e.name)) walk(full); continue }
35
+ if (glob && !matchGlob(e.name, glob)) continue
36
+ let content
37
+ try { content = fs.readFileSync(full, 'utf8') } catch { continue }
38
+ content.split('\n').forEach((line, i) => {
39
+ if (out.length < head_limit && re.test(line)) out.push(`${full}:${i + 1}:${line.slice(0, 200)}`)
40
+ })
41
+ }
42
+ }
43
+ walk(root)
44
+ return { matches: out, total: out.length, truncated: out.length >= head_limit }
45
+ },
46
+ })
47
+
48
+ function matchGlob(name, glob) {
49
+ const re = new RegExp('^' + glob.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i')
50
+ return re.test(name)
51
+ }
@@ -0,0 +1,15 @@
1
+ import { registry } from './registry.js'
2
+ registry.register({
3
+ name: 'homeassistant_tool',
4
+ toolset: 'core',
5
+ 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'] } },
6
+ requiresEnv: ['HASS_TOKEN', 'HASS_URL'],
7
+ checkFn: () => Boolean(process.env.HASS_TOKEN),
8
+ handler: async ({ action, entity_id, domain, service, data = {} }) => {
9
+ const url = process.env.HASS_URL || 'http://homeassistant.local:8123'
10
+ const auth = { authorization: `Bearer ${process.env.HASS_TOKEN}` }
11
+ if (action === 'state') return await (await fetch(`${url}/api/states/${entity_id}`, { headers: auth })).json()
12
+ 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()
13
+ return { error: 'unknown action' }
14
+ },
15
+ })
@@ -0,0 +1,33 @@
1
+ import { registry } from './registry.js'
2
+
3
+ registry.register({
4
+ name: 'image_gen',
5
+ toolset: 'creative',
6
+ schema: {
7
+ name: 'image_gen',
8
+ description: 'Generate an image from a prompt. Provider via config.image_gen.provider (openai|replicate).',
9
+ parameters: {
10
+ type: 'object',
11
+ properties: {
12
+ prompt: { type: 'string' },
13
+ provider: { type: 'string', enum: ['openai', 'replicate'] },
14
+ size: { type: 'string', default: '1024x1024' },
15
+ model: { type: 'string' },
16
+ },
17
+ required: ['prompt'],
18
+ },
19
+ },
20
+ checkFn: () => Boolean(process.env.OPENAI_API_KEY || process.env.REPLICATE_API_TOKEN),
21
+ requiresEnv: ['OPENAI_API_KEY or REPLICATE_API_TOKEN'],
22
+ handler: async ({ prompt, provider, size = '1024x1024', model }) => {
23
+ const which = provider || (process.env.OPENAI_API_KEY ? 'openai' : 'replicate')
24
+ if (which === 'openai') {
25
+ if (!process.env.OPENAI_API_KEY) return { error: 'OPENAI_API_KEY required' }
26
+ 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 }) })
27
+ return await res.json()
28
+ }
29
+ if (!process.env.REPLICATE_API_TOKEN) return { error: 'REPLICATE_API_TOKEN required' }
30
+ 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 } }) })
31
+ return await res.json()
32
+ },
33
+ })
@@ -0,0 +1,18 @@
1
+ import { registry } from './registry.js'
2
+
3
+ const _flags = new Map()
4
+ export function setInterrupt(sessionId) { _flags.set(sessionId, true) }
5
+ export function isInterrupted(sessionId) { return _flags.get(sessionId) === true }
6
+ export function clearInterrupt(sessionId) { _flags.delete(sessionId) }
7
+
8
+ registry.register({
9
+ name: 'interrupt',
10
+ toolset: 'core',
11
+ 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'] } },
12
+ handler: async ({ action, session_id }) => {
13
+ if (action === 'set') { setInterrupt(session_id); return { interrupted: true } }
14
+ if (action === 'clear') { clearInterrupt(session_id); return { cleared: true } }
15
+ if (action === 'check') return { interrupted: isInterrupted(session_id) }
16
+ return { error: 'unknown action' }
17
+ },
18
+ })