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,22 @@
1
+ export class AtroposClient {
2
+ constructor(opts = {}) {
3
+ this.url = opts.url || process.env.ATROPOS_URL
4
+ this.token = opts.token || process.env.ATROPOS_TOKEN
5
+ this.platform = 'atropos'
6
+ }
7
+ getRequiredEnv() { return ['ATROPOS_URL', 'ATROPOS_TOKEN'] }
8
+ async start() {
9
+ if (!this.url) throw new Error('AtroposClient: ATROPOS_URL required')
10
+ const r = await fetch(`${this.url}/health`, { headers: this._headers() })
11
+ if (!r.ok) throw new Error('atropos health failed: ' + r.status)
12
+ }
13
+ _headers() { return this.token ? { authorization: `Bearer ${this.token}` } : {} }
14
+ async listRollouts() {
15
+ const r = await fetch(`${this.url}/rollouts`, { headers: this._headers() })
16
+ if (!r.ok) return []
17
+ return await r.json()
18
+ }
19
+ async submitTrajectory(traj) {
20
+ return fetch(`${this.url}/trajectories`, { method: 'POST', headers: { ...this._headers(), 'content-type': 'application/json' }, body: JSON.stringify(traj) }).then(r => r.json())
21
+ }
22
+ }
package/src/rl/cli.js ADDED
@@ -0,0 +1,18 @@
1
+ import { logger } from '../observability/log.js'
2
+
3
+ const log = logger('rl')
4
+
5
+ export function listRollouts() { return [] }
6
+ export function replayTrajectory(_id) { throw new Error('rl.replay: atropos integration out of scope. Set ATROPOS_URL and provide an adapter.') }
7
+ export function score(_traj) { throw new Error('rl.score: atropos integration out of scope.') }
8
+
9
+ export function rlSubcommand({ args = [] } = {}) {
10
+ const [action] = args
11
+ if (!action || action === 'list') {
12
+ log.info('rl list')
13
+ console.log('No rollouts available. Tinker/Atropos integration is documented as a residual; configure ATROPOS_URL and provide an adapter to enable.')
14
+ return 0
15
+ }
16
+ console.log('rl: action "' + action + '" requires Atropos. See ARCHITECTURE residual.')
17
+ return 1
18
+ }
@@ -0,0 +1,84 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { db as getDb, closeDb as closeDbImpl, resetForTests as resetForTestsImpl } from './db.js'
3
+
4
+ let _initialized = false
5
+
6
+ async function initDb() {
7
+ const d = await getDb()
8
+ if (_initialized) return d
9
+ _initialized = true
10
+
11
+ await d.exec(`
12
+ CREATE TABLE IF NOT EXISTS sessions (
13
+ id TEXT PRIMARY KEY,
14
+ platform TEXT, user_id TEXT, chat_id TEXT, thread_id TEXT,
15
+ title TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, model TEXT
16
+ );
17
+ CREATE TABLE IF NOT EXISTS messages (
18
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
19
+ session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT,
20
+ tool_calls TEXT, tool_call_id TEXT, ts INTEGER NOT NULL,
21
+ FOREIGN KEY(session_id) REFERENCES sessions(id)
22
+ );
23
+ CREATE INDEX IF NOT EXISTS idx_msg_session ON messages(session_id, ts);
24
+ `)
25
+
26
+ // libsql supports FTS5 natively; create FTS virtual table
27
+ try {
28
+ await d.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(content, session_id UNINDEXED, content='messages', content_rowid='id')`)
29
+ await d.prepare(`CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN INSERT INTO messages_fts(rowid, content, session_id) VALUES (new.id, new.content, new.session_id); END`).run()
30
+ } catch (e) {
31
+ console.log('[sessions.js] FTS5 creation failed:', e.message);
32
+ }
33
+
34
+ return d
35
+ }
36
+
37
+ async function db() {
38
+ return await initDb()
39
+ }
40
+
41
+ export async function createSession({ platform = 'cli', userId = null, chatId = null, threadId = null, title = null, model = null } = {}) {
42
+ const d = await db()
43
+ const id = randomUUID()
44
+ const now = Date.now()
45
+ await d.prepare(`INSERT INTO sessions (id, platform, user_id, chat_id, thread_id, title, created_at, updated_at, model) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
46
+ .run(id, platform, userId, chatId, threadId, title, now, now, model)
47
+ return id
48
+ }
49
+
50
+ export async function appendMessage(sessionId, { role, content = '', toolCalls = null, toolCallId = null }) {
51
+ const d = await db()
52
+ const now = Date.now()
53
+ const info = await d.prepare(`INSERT INTO messages (session_id, role, content, tool_calls, tool_call_id, ts) VALUES (?, ?, ?, ?, ?, ?)`)
54
+ .run(sessionId, role, content, toolCalls ? JSON.stringify(toolCalls) : null, toolCallId, now)
55
+ await d.prepare(`UPDATE sessions SET updated_at = ? WHERE id = ?`).run(now, sessionId)
56
+ return info.lastInsertRowid
57
+ }
58
+
59
+ export async function getMessages(sessionId) {
60
+ const d = await db()
61
+ const rows = await d.prepare(`SELECT id, role, content, tool_calls, tool_call_id, ts FROM messages WHERE session_id = ? ORDER BY ts ASC, id ASC`).all(sessionId)
62
+ return rows.map(r => ({ ...r, tool_calls: r.tool_calls ? JSON.parse(r.tool_calls) : null }))
63
+ }
64
+
65
+ export async function listSessions(limit = 50) {
66
+ const d = await db()
67
+ return await d.prepare(`SELECT id, platform, title, created_at, updated_at, model FROM sessions ORDER BY updated_at DESC LIMIT ?`).all(limit)
68
+ }
69
+
70
+ export async function search(query, limit = 20) {
71
+ const d = await db()
72
+ // Try FTS5 if available (libsql, but not busybase since triggers can't be created)
73
+ try {
74
+ const ftsResult = await d.prepare(`SELECT m.id, m.session_id, m.content FROM messages_fts f JOIN messages m ON m.id = f.rowid WHERE messages_fts MATCH ? ORDER BY rank LIMIT ?`).all(query, limit)
75
+ if (ftsResult && ftsResult.length > 0) return ftsResult
76
+ } catch (e) {
77
+ // FTS5 not available, fall through to LIKE
78
+ }
79
+ // Fallback to LIKE search
80
+ return await d.prepare(`SELECT id, session_id, content FROM messages WHERE content LIKE ? ORDER BY ts DESC LIMIT ?`).all(`%${query}%`, limit)
81
+ }
82
+
83
+ export function closeDb() { return closeDbImpl() }
84
+ export function resetForTests() { return resetForTestsImpl() }
@@ -0,0 +1,49 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import yaml from 'js-yaml'
4
+ import os from 'node:os'
5
+ import { getFophHome } from '../home.js'
6
+
7
+ const FRONTMATTER = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/
8
+
9
+ export function listSkills(extraDirs = []) {
10
+ const dirs = [path.join(getFophHome(), 'skills'), path.join(process.cwd(), 'skills'), ...extraDirs]
11
+ const out = []
12
+ for (const d of dirs) {
13
+ if (!fs.existsSync(d)) continue
14
+ walk(d, out)
15
+ }
16
+ return out.filter(s => platformOk(s))
17
+ }
18
+
19
+ function walk(d, out) {
20
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
21
+ const full = path.join(d, entry.name)
22
+ if (entry.isDirectory()) { walk(full, out); continue }
23
+ if (entry.name === 'SKILL.md') out.push(loadSkill(full))
24
+ }
25
+ }
26
+
27
+ export function loadSkill(file) {
28
+ const raw = fs.readFileSync(file, 'utf8')
29
+ const m = FRONTMATTER.exec(raw)
30
+ if (!m) return { file, name: path.basename(path.dirname(file)), description: '', body: raw, frontmatter: {} }
31
+ const fm = yaml.load(m[1]) || {}
32
+ return { file, name: fm.name || path.basename(path.dirname(file)), description: fm.description || '', frontmatter: fm, body: m[2], platforms: fm.platforms }
33
+ }
34
+
35
+ function platformOk(skill) {
36
+ const plats = skill.platforms || skill.frontmatter?.platforms
37
+ if (!Array.isArray(plats) || plats.length === 0) return true
38
+ const platform = os.platform() === 'darwin' ? 'macos' : os.platform()
39
+ return plats.includes(platform)
40
+ }
41
+
42
+ export function findSkill(name) { return listSkills().find(s => s.name === name) || null }
43
+
44
+ export function skillAsUserMessage(name, args = '') {
45
+ const s = findSkill(name)
46
+ if (!s) return null
47
+ const prefix = args ? `Arguments: ${args}\n\n` : ''
48
+ return { role: 'user', content: `[skill:${name}]\n${prefix}${s.body}` }
49
+ }
@@ -0,0 +1,81 @@
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
+ import { getConfigValue, saveConfigValue } from '../config.js'
6
+
7
+ const _BUILTIN_SKINS = {
8
+ default: {
9
+ name: 'default', description: 'Classic gold/kawaii',
10
+ colors: { banner_border: '#FFD700', banner_title: '#FFD700', banner_accent: '#FFA500', banner_dim: '#666666', banner_text: '#FFFFFF', response_border: '#FFD700' },
11
+ spinner: { waiting_faces: ['(◡‿◡)', '(◕‿◕)'], thinking_faces: ['(¬‿¬)', '(◔‿◔)'], thinking_verbs: ['thinking', 'pondering'], wings: [['', '']] },
12
+ branding: { agent_name: 'Freddie', welcome: 'Welcome to Freddie.', response_label: ' ✨ Freddie ', prompt_symbol: '> ' },
13
+ tool_prefix: '┊', tool_emojis: {},
14
+ },
15
+ ares: {
16
+ name: 'ares', description: 'Crimson/bronze war-god',
17
+ colors: { banner_border: '#C0392B', banner_title: '#E67E22', banner_accent: '#922B21', banner_dim: '#444444', banner_text: '#ECE0D1', response_border: '#922B21' },
18
+ spinner: { waiting_faces: ['⚔️ ', '🛡️ '], thinking_faces: ['🔥', '💢'], thinking_verbs: ['marshalling', 'forging'], wings: [['⟨⚔', '⚔⟩']] },
19
+ branding: { agent_name: 'Ares', welcome: 'Forge with Ares.', response_label: ' 🔥 Ares ', prompt_symbol: '⚔ ' },
20
+ tool_prefix: '▏',
21
+ },
22
+ mono: {
23
+ name: 'mono', description: 'Grayscale monochrome',
24
+ colors: { banner_border: '#999999', banner_title: '#FFFFFF', banner_accent: '#CCCCCC', banner_dim: '#555555', banner_text: '#EEEEEE', response_border: '#999999' },
25
+ spinner: { waiting_faces: ['…'], thinking_faces: ['·'], thinking_verbs: ['thinking'], wings: [['', '']] },
26
+ branding: { agent_name: 'Freddie', welcome: 'Mono.', response_label: ' Freddie ', prompt_symbol: '> ' },
27
+ tool_prefix: '|',
28
+ },
29
+ slate: {
30
+ name: 'slate', description: 'Cool blue developer',
31
+ colors: { banner_border: '#3498DB', banner_title: '#5DADE2', banner_accent: '#2874A6', banner_dim: '#566573', banner_text: '#D6EAF8', response_border: '#3498DB' },
32
+ spinner: { waiting_faces: ['◆'], thinking_faces: ['◇'], thinking_verbs: ['compiling'], wings: [['', '']] },
33
+ branding: { agent_name: 'Freddie', welcome: 'Slate ready.', response_label: ' ◆ Freddie ', prompt_symbol: '◆ ' },
34
+ tool_prefix: '┊',
35
+ },
36
+ }
37
+
38
+ let _active = null
39
+
40
+ export function loadSkin(name) {
41
+ const userPath = path.join(getFophHome(), 'skins', `${name}.yaml`)
42
+ if (fs.existsSync(userPath)) {
43
+ const fromYaml = yaml.load(fs.readFileSync(userPath, 'utf8')) || {}
44
+ return mergeWithDefault(fromYaml)
45
+ }
46
+ if (_BUILTIN_SKINS[name]) return mergeWithDefault(_BUILTIN_SKINS[name])
47
+ return _BUILTIN_SKINS.default
48
+ }
49
+
50
+ function mergeWithDefault(skin) {
51
+ const base = JSON.parse(JSON.stringify(_BUILTIN_SKINS.default))
52
+ return deepMerge(base, skin)
53
+ }
54
+
55
+ function deepMerge(t, s) {
56
+ for (const k of Object.keys(s)) {
57
+ if (s[k] && typeof s[k] === 'object' && !Array.isArray(s[k]) && t[k] && typeof t[k] === 'object' && !Array.isArray(t[k])) deepMerge(t[k], s[k])
58
+ else t[k] = s[k]
59
+ }
60
+ return t
61
+ }
62
+
63
+ export function initSkinFromConfig() {
64
+ const name = getConfigValue('display.skin', 'default')
65
+ _active = loadSkin(name)
66
+ return _active
67
+ }
68
+
69
+ export function getActiveSkin() {
70
+ if (!_active) initSkinFromConfig()
71
+ return _active
72
+ }
73
+
74
+ export function setActiveSkin(name) {
75
+ _active = loadSkin(name)
76
+ saveConfigValue('display.skin', name)
77
+ return _active
78
+ }
79
+
80
+ export function listBuiltinSkins() { return Object.keys(_BUILTIN_SKINS) }
81
+ export function resetForTests() { _active = null }
@@ -0,0 +1,26 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { runTurn } from '../agent/machine.js'
4
+ import { logger } from '../observability/log.js'
5
+
6
+ const log = logger('swe')
7
+
8
+ export async function runMiniSwe({ task, model = null, callLLM = null, env = {}, timeoutMs = 600_000 } = {}) {
9
+ if (!task || !task.prompt) throw new Error('task.prompt required')
10
+ log.info('mini-swe start', { task: task.id || 'anon' })
11
+ const out = await runTurn({ prompt: task.prompt, model, callLLM, timeoutMs })
12
+ const passed = !out.error && (task.expect ? checkExpect(out.result, task.expect) : true)
13
+ log.info('mini-swe done', { passed })
14
+ return { passed, result: out.result, error: out.error, iterations: out.iterations, messages: out.messages }
15
+ }
16
+ function checkExpect(result, expect) {
17
+ if (typeof expect === 'string') return String(result || '').includes(expect)
18
+ if (expect instanceof RegExp) return expect.test(String(result || ''))
19
+ if (typeof expect === 'function') return Boolean(expect(result))
20
+ return true
21
+ }
22
+ export function loadTask(file) {
23
+ const txt = fs.readFileSync(file, 'utf8')
24
+ if (file.endsWith('.json')) return JSON.parse(txt)
25
+ return { id: path.basename(file, path.extname(file)), prompt: txt }
26
+ }
package/src/time.js ADDED
@@ -0,0 +1,25 @@
1
+ export function now() { return Date.now() }
2
+ export function fmtIso(ms = Date.now()) { return new Date(ms).toISOString() }
3
+ const UNITS = { ms: 1, s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 }
4
+ export function parseDuration(s) {
5
+ if (typeof s === 'number') return s
6
+ const m = String(s).trim().match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)$/i)
7
+ if (!m) throw new Error('bad duration: ' + s)
8
+ return Math.round(Number(m[1]) * UNITS[m[2].toLowerCase()])
9
+ }
10
+ export function humanizeDuration(ms) {
11
+ const abs = Math.abs(ms); const p = []
12
+ const d = Math.floor(abs / UNITS.d); if (d) p.push(d + 'd')
13
+ const h = Math.floor((abs % UNITS.d) / UNITS.h); if (h) p.push(h + 'h')
14
+ const m = Math.floor((abs % UNITS.h) / UNITS.m); if (m) p.push(m + 'm')
15
+ const s = Math.floor((abs % UNITS.m) / 1000); if (s) p.push(s + 's')
16
+ if (!p.length) p.push((abs % 1000) + 'ms')
17
+ return p.slice(0, 2).join(' ')
18
+ }
19
+ export function tsRelative(ms, ref = Date.now()) {
20
+ const diff = ref - ms
21
+ if (diff < 60_000) return 'just now'
22
+ if (diff < 3_600_000) return Math.floor(diff / 60_000) + 'm ago'
23
+ if (diff < 86_400_000) return Math.floor(diff / 3_600_000) + 'h ago'
24
+ return Math.floor(diff / 86_400_000) + 'd ago'
25
+ }
@@ -0,0 +1,8 @@
1
+ import { ansiStrip } from '../utils.js'
2
+ import { registry } from './registry.js'
3
+ registry.register({
4
+ name: 'ansi_strip',
5
+ toolset: 'core',
6
+ schema: { name: 'ansi_strip', description: 'Strip ANSI escape sequences.', parameters: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] } },
7
+ handler: async ({ text }) => ({ text: ansiStrip(text) }),
8
+ })
@@ -0,0 +1,15 @@
1
+ import { registry } from './registry.js'
2
+
3
+ registry.register({
4
+ name: 'approval',
5
+ toolset: 'core',
6
+ schema: {
7
+ name: 'approval',
8
+ description: 'Request approval for a destructive action. Returns the user decision (allow|deny). In non-interactive contexts, defaults to deny unless config.acp.always_allow includes this action.',
9
+ parameters: { type: 'object', properties: { action: { type: 'string' }, reason: { type: 'string' } }, required: ['action'] },
10
+ },
11
+ handler: async ({ action, reason = '' }, ctx = {}) => {
12
+ if (typeof ctx.askApproval === 'function') return await ctx.askApproval({ action, reason })
13
+ return { decision: 'deny', reason: 'no interactive approval channel; supply ctx.askApproval' }
14
+ },
15
+ })
@@ -0,0 +1,35 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { registry } from './registry.js'
3
+
4
+ registry.register({
5
+ name: 'bash',
6
+ toolset: 'core',
7
+ schema: {
8
+ name: 'bash',
9
+ description: 'Run a shell command. Returns stdout/stderr/exitCode.',
10
+ parameters: {
11
+ type: 'object',
12
+ properties: {
13
+ command: { type: 'string', description: 'Shell command to execute' },
14
+ cwd: { type: 'string', description: 'Working directory' },
15
+ timeout_ms: { type: 'number', description: 'Hard timeout in ms', default: 60000 },
16
+ background: { type: 'boolean', default: false },
17
+ },
18
+ required: ['command'],
19
+ },
20
+ },
21
+ handler: async (args) => {
22
+ const { command, cwd = process.cwd(), timeout_ms = 60000 } = args
23
+ return await new Promise((resolve) => {
24
+ const sh = process.platform === 'win32' ? 'cmd' : 'sh'
25
+ const flag = process.platform === 'win32' ? '/c' : '-c'
26
+ const child = spawn(sh, [flag, command], { cwd, env: process.env })
27
+ let stdout = '', stderr = ''
28
+ const t = setTimeout(() => { try { child.kill('SIGKILL') } catch {} resolve({ exitCode: -1, stdout, stderr: stderr + '\n[timeout]', timedOut: true }) }, timeout_ms)
29
+ child.stdout?.on('data', d => stdout += d.toString())
30
+ child.stderr?.on('data', d => stderr += d.toString())
31
+ child.on('close', code => { clearTimeout(t); resolve({ exitCode: code, stdout, stderr }) })
32
+ child.on('error', e => { clearTimeout(t); resolve({ exitCode: -1, stdout, stderr: stderr + '\n' + e.message }) })
33
+ })
34
+ },
35
+ })
@@ -0,0 +1,22 @@
1
+ import { registry } from './registry.js'
2
+
3
+ export const BINARY_EXTENSIONS = new Set([
4
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico', '.tiff',
5
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
6
+ '.zip', '.tar', '.gz', '.bz2', '.xz', '.7z', '.rar',
7
+ '.exe', '.dll', '.so', '.dylib', '.bin',
8
+ '.mp3', '.mp4', '.mov', '.avi', '.flac', '.ogg',
9
+ '.ttf', '.otf', '.woff', '.woff2',
10
+ '.wasm', '.class', '.jar',
11
+ ])
12
+ export function isBinary(filename) {
13
+ const lower = String(filename).toLowerCase()
14
+ for (const ext of BINARY_EXTENSIONS) if (lower.endsWith(ext)) return true
15
+ return false
16
+ }
17
+ registry.register({
18
+ name: 'binary_extensions',
19
+ toolset: 'core',
20
+ schema: { name: 'binary_extensions', description: 'Check whether a filename has a known binary extension.', parameters: { type: 'object', properties: { filename: { type: 'string' } }, required: ['filename'] } },
21
+ handler: async ({ filename }) => ({ binary: isBinary(filename), known: BINARY_EXTENSIONS.size }),
22
+ })
@@ -0,0 +1,48 @@
1
+ import { registry } from './registry.js'
2
+
3
+ let _puppeteerAvail = null
4
+ async function probe() {
5
+ if (_puppeteerAvail !== null) return _puppeteerAvail
6
+ try { await import('puppeteer-core'); _puppeteerAvail = true } catch { _puppeteerAvail = false }
7
+ return _puppeteerAvail
8
+ }
9
+
10
+ registry.register({
11
+ name: 'browser',
12
+ toolset: 'browse',
13
+ schema: {
14
+ name: 'browser',
15
+ description: 'Browser automation: navigate, click, type, evaluate, screenshot. Requires puppeteer-core.',
16
+ parameters: {
17
+ type: 'object',
18
+ properties: {
19
+ action: { type: 'string', enum: ['navigate', 'click', 'type', 'evaluate', 'screenshot', 'text'] },
20
+ url: { type: 'string' },
21
+ selector: { type: 'string' },
22
+ text: { type: 'string' },
23
+ script: { type: 'string' },
24
+ path: { type: 'string' },
25
+ },
26
+ required: ['action'],
27
+ },
28
+ },
29
+ checkFn: () => true,
30
+ requiresEnv: ['puppeteer-core'],
31
+ handler: async (args) => {
32
+ const ok = await probe()
33
+ if (!ok) return { error: 'puppeteer-core not installed. Run: npm install puppeteer-core' }
34
+ const puppeteer = (await import('puppeteer-core')).default
35
+ const browser = await puppeteer.launch({ headless: 'new' })
36
+ try {
37
+ const page = await browser.newPage()
38
+ const a = args.action
39
+ if (a === 'navigate') { await page.goto(args.url, { waitUntil: 'domcontentloaded' }); return { url: page.url(), title: await page.title() } }
40
+ if (a === 'click') { await page.goto(args.url, { waitUntil: 'domcontentloaded' }); await page.click(args.selector); return { ok: true } }
41
+ if (a === 'type') { await page.goto(args.url, { waitUntil: 'domcontentloaded' }); await page.type(args.selector, args.text); return { ok: true } }
42
+ if (a === 'evaluate') { await page.goto(args.url, { waitUntil: 'domcontentloaded' }); return { result: await page.evaluate(args.script) } }
43
+ if (a === 'text') { await page.goto(args.url, { waitUntil: 'domcontentloaded' }); return { text: await page.evaluate(() => document.body.innerText) } }
44
+ if (a === 'screenshot') { await page.goto(args.url, { waitUntil: 'domcontentloaded' }); await page.screenshot({ path: args.path }); return { saved: args.path } }
45
+ return { error: 'unknown action: ' + a }
46
+ } finally { await browser.close() }
47
+ },
48
+ })
@@ -0,0 +1,13 @@
1
+ import { registry } from './registry.js'
2
+ import { getConfigValue, saveConfigValue } from '../config.js'
3
+
4
+ registry.register({
5
+ name: 'budget_config',
6
+ toolset: 'core',
7
+ schema: { name: 'budget_config', description: 'Read/write per-session token budget limits.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['get', 'set'] }, max_tokens: { type: 'number' }, max_cost_usd: { type: 'number' } }, required: ['action'] } },
8
+ handler: async ({ action, max_tokens, max_cost_usd }) => {
9
+ if (action === 'get') return { max_tokens: getConfigValue('budget.max_tokens'), max_cost_usd: getConfigValue('budget.max_cost_usd') }
10
+ if (action === 'set') { if (typeof max_tokens === 'number') saveConfigValue('budget.max_tokens', max_tokens); if (typeof max_cost_usd === 'number') saveConfigValue('budget.max_cost_usd', max_cost_usd); return { saved: true } }
11
+ return { error: 'unknown action' }
12
+ },
13
+ })
@@ -0,0 +1,29 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { getFophHome } from '../home.js'
4
+ import { registry } from './registry.js'
5
+
6
+ function dir() { const d = path.join(getFophHome(), 'checkpoints'); fs.mkdirSync(d, { recursive: true }); return d }
7
+
8
+ const ACTIONS = {
9
+ save: ({ name, data }) => {
10
+ if (!name) return { error: 'name required' }
11
+ const p = path.join(dir(), name + '.json')
12
+ fs.writeFileSync(p, JSON.stringify(data || {}, null, 2), 'utf8')
13
+ return { saved: p }
14
+ },
15
+ load: ({ name }) => {
16
+ const p = path.join(dir(), name + '.json')
17
+ if (!fs.existsSync(p)) return { error: 'not found' }
18
+ return { data: JSON.parse(fs.readFileSync(p, 'utf8')) }
19
+ },
20
+ list: () => ({ items: fs.readdirSync(dir()).filter(f => f.endsWith('.json')).map(f => f.replace(/\.json$/, '')) }),
21
+ delete: ({ name }) => { const p = path.join(dir(), name + '.json'); if (fs.existsSync(p)) fs.unlinkSync(p); return { deleted: name } },
22
+ }
23
+
24
+ registry.register({
25
+ name: 'checkpoint',
26
+ toolset: 'core',
27
+ schema: { name: 'checkpoint', description: 'Save/load/list/delete named JSON checkpoints under ~/.freddie/checkpoints.', parameters: { type: 'object', properties: { action: { type: 'string', enum: Object.keys(ACTIONS) }, name: { type: 'string' }, data: {} }, required: ['action'] } },
28
+ handler: async (args) => { const fn = ACTIONS[args.action]; return fn ? fn(args) : { error: 'unknown action' } },
29
+ })
@@ -0,0 +1,15 @@
1
+ import { registry } from './registry.js'
2
+
3
+ registry.register({
4
+ name: 'clarify',
5
+ toolset: 'core',
6
+ schema: {
7
+ name: 'clarify',
8
+ description: 'Pose a clarifying question to the user before proceeding. Returns the user response. In non-interactive contexts, returns { error } so the agent must proceed with stated assumption.',
9
+ parameters: { type: 'object', properties: { question: { type: 'string' }, options: { type: 'array', items: { type: 'string' } } }, required: ['question'] },
10
+ },
11
+ handler: async ({ question, options = [] }, ctx = {}) => {
12
+ if (typeof ctx.askUser === 'function') return await ctx.askUser({ question, options })
13
+ return { error: 'no interactive channel; assume defaults', question, options }
14
+ },
15
+ })
@@ -0,0 +1,27 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { registry } from './registry.js'
3
+
4
+ const RUNNERS = {
5
+ python: ['python', '-c'], python3: ['python3', '-c'],
6
+ node: ['node', '-e'], deno: ['deno', 'eval'],
7
+ ruby: ['ruby', '-e'], bash: ['bash', '-c'],
8
+ }
9
+
10
+ registry.register({
11
+ name: 'code_execution',
12
+ toolset: 'core',
13
+ schema: { name: 'code_execution', description: 'Execute a code snippet in a chosen runner (python, node, deno, ruby, bash). Returns stdout/stderr/exitCode.', parameters: { type: 'object', properties: { code: { type: 'string' }, runner: { type: 'string', enum: Object.keys(RUNNERS), default: 'python' }, timeout_ms: { type: 'number', default: 30000 } }, required: ['code'] } },
14
+ handler: async ({ code, runner = 'python', timeout_ms = 30000 }) => {
15
+ const cmd = RUNNERS[runner]
16
+ if (!cmd) return { error: 'unknown runner: ' + runner }
17
+ return await new Promise(resolve => {
18
+ const child = spawn(cmd[0], [cmd[1], code], { env: process.env })
19
+ let stdout = '', stderr = ''
20
+ const t = setTimeout(() => { try { child.kill('SIGKILL') } catch {} resolve({ exitCode: -1, stdout, stderr: stderr + '\n[timeout]' }) }, timeout_ms)
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
+ })
@@ -0,0 +1,16 @@
1
+ import { getAuthStore } from '../auth.js'
2
+ import { registry } from './registry.js'
3
+
4
+ registry.register({
5
+ name: 'credential_files',
6
+ toolset: 'core',
7
+ schema: { name: 'credential_files', description: 'Get/set credentials in ~/.freddie/auth/.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['get', 'set', 'list', 'delete'] }, name: { type: 'string' }, value: {} }, required: ['action'] } },
8
+ handler: async ({ action, name, value }) => {
9
+ const s = getAuthStore()
10
+ if (action === 'get') return { credential: await s.getCredential(name) }
11
+ if (action === 'set') return await s.setCredential(name, value)
12
+ if (action === 'list') return { credentials: await s.listCredentials() }
13
+ if (action === 'delete') return await s.deleteCredential(name)
14
+ return { error: 'unknown action' }
15
+ },
16
+ })
@@ -0,0 +1,16 @@
1
+ import { createJob, listJobs, cancelJob, deleteJob } from '../cron/scheduler.js'
2
+ import { registry } from './registry.js'
3
+
4
+ const ACTIONS = {
5
+ add: ({ cron, prompt, model = null }) => ({ id: createJob({ cron, prompt, model }) }),
6
+ list: () => ({ jobs: listJobs() }),
7
+ cancel: ({ id }) => { cancelJob(id); return { id, cancelled: true } },
8
+ delete: ({ id }) => { deleteJob(id); return { id, deleted: true } },
9
+ }
10
+
11
+ registry.register({
12
+ name: 'cronjob',
13
+ toolset: 'core',
14
+ schema: { name: 'cronjob', description: 'Manage agent cron jobs (add, list, cancel, delete).', parameters: { type: 'object', properties: { action: { type: 'string', enum: Object.keys(ACTIONS) }, cron: { type: 'string' }, prompt: { type: 'string' }, model: { type: 'string' }, id: { type: 'number' } }, required: ['action'] } },
15
+ handler: async (args) => { const fn = ACTIONS[args.action]; if (!fn) return { error: 'unknown action' }; try { return fn(args) } catch (e) { return { error: String(e.message || e) } } },
16
+ })
@@ -0,0 +1,9 @@
1
+ import { registry } from './registry.js'
2
+ import { snapshotAll, listDebug } from '../observability/debug.js'
3
+
4
+ registry.register({
5
+ name: 'debug_helpers',
6
+ toolset: 'core',
7
+ schema: { name: 'debug_helpers', description: 'Inspect any registered /debug subsystem.', parameters: { type: 'object', properties: { name: { type: 'string' } } } },
8
+ handler: async ({ name }) => name ? snapshotAll()[name] || { error: 'unknown subsystem: ' + name } : { subsystems: listDebug() },
9
+ })
@@ -0,0 +1,28 @@
1
+ import { registry } from './registry.js'
2
+ import { runTurn } from '../agent/machine.js'
3
+
4
+ const MAX_DEPTH = 3
5
+
6
+ registry.register({
7
+ name: 'delegate',
8
+ toolset: 'core',
9
+ schema: {
10
+ name: 'delegate',
11
+ description: 'Spawn a sub-agent to handle a focused task. Returns the sub-agent final result.',
12
+ parameters: {
13
+ type: 'object',
14
+ properties: {
15
+ task: { type: 'string' },
16
+ model: { type: 'string' },
17
+ max_iterations: { type: 'number', default: 30 },
18
+ },
19
+ required: ['task'],
20
+ },
21
+ },
22
+ handler: async ({ task, model, max_iterations = 30 }, ctx = {}) => {
23
+ const depth = (ctx.depth || 0) + 1
24
+ if (depth > MAX_DEPTH) return { error: `delegate recursion depth exceeded (${MAX_DEPTH})` }
25
+ const out = await runTurn({ prompt: task, model, callLLM: ctx.callLLM, maxIterations: max_iterations, timeoutMs: 60000 })
26
+ return { result: out.result, error: out.error, iterations: out.iterations, depth }
27
+ },
28
+ })
@@ -0,0 +1,13 @@
1
+ import { registry } from './registry.js'
2
+ registry.register({
3
+ name: 'discord_tool',
4
+ toolset: 'core',
5
+ schema: { name: 'discord_tool', description: 'Send a message to a Discord channel via REST.', parameters: { type: 'object', properties: { channel_id: { type: 'string' }, content: { type: 'string' } }, required: ['channel_id', 'content'] } },
6
+ requiresEnv: ['DISCORD_BOT_TOKEN'],
7
+ checkFn: () => Boolean(process.env.DISCORD_BOT_TOKEN),
8
+ handler: async ({ channel_id, content }) => {
9
+ if (!process.env.DISCORD_BOT_TOKEN) return { error: 'DISCORD_BOT_TOKEN required' }
10
+ const r = await fetch(`https://discord.com/api/v10/channels/${channel_id}/messages`, { method: 'POST', headers: { authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`, 'content-type': 'application/json' }, body: JSON.stringify({ content }) })
11
+ return await r.json()
12
+ },
13
+ })