freddie 0.0.41 → 0.0.43

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 (153) hide show
  1. package/AGENTS.md +85 -11
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +2 -2
  4. package/bin/freddie.js +12 -109
  5. package/package.json +11 -2
  6. package/src/acp/server.js +3 -3
  7. package/src/acp/session.js +8 -8
  8. package/src/acp/tools.js +5 -4
  9. package/src/agent/account_usage.js +5 -5
  10. package/src/agent/acptoapi-bridge.js +3 -1
  11. package/src/agent/credential_sources.js +2 -2
  12. package/src/agent/curator.js +5 -5
  13. package/src/agent/machine.js +3 -2
  14. package/src/agent/manual_compression_feedback.js +5 -5
  15. package/src/agent/shell_hooks.js +2 -2
  16. package/src/auth.js +2 -2
  17. package/src/batch.js +2 -2
  18. package/src/cli/backup.js +3 -3
  19. package/src/cli/doctor.js +3 -3
  20. package/src/cli/dump.js +4 -2
  21. package/src/cli/env_loader.js +2 -2
  22. package/src/cli/gateway_cli.js +3 -4
  23. package/src/cli/hooks.js +2 -2
  24. package/src/cli/logs.js +4 -4
  25. package/src/cli/mcp_config.js +2 -2
  26. package/src/cli/plugins_cmd.js +3 -3
  27. package/src/cli/status.js +1 -1
  28. package/src/cli/tools_config.js +2 -2
  29. package/src/cli/uninstall.js +2 -2
  30. package/src/config.js +2 -2
  31. package/src/db.js +3 -3
  32. package/src/gateway/platforms.js +21 -0
  33. package/src/home.js +2 -2
  34. package/src/host/contract.js +39 -0
  35. package/src/host/host.js +159 -0
  36. package/src/host/index.js +27 -0
  37. package/src/index.js +2 -1
  38. package/src/mcp/server.js +5 -4
  39. package/src/observability/log.js +2 -2
  40. package/src/plugins/disk_cleanup/index.js +2 -2
  41. package/src/plugins/manager.js +13 -63
  42. package/src/plugins/memory/provider.js +26 -26
  43. package/src/plugins/observability/index.js +3 -3
  44. package/src/skills/index.js +2 -2
  45. package/src/skin/engine.js +2 -2
  46. package/src/toolsets.js +13 -15
  47. package/src/web/index.html +1 -1
  48. package/src/web/server.js +8 -94
  49. package/src/gateway/platforms/api_server.js +0 -21
  50. package/src/gateway/platforms/bluebubbles.js +0 -32
  51. package/src/gateway/platforms/dingtalk.js +0 -32
  52. package/src/gateway/platforms/discord.js +0 -24
  53. package/src/gateway/platforms/email.js +0 -51
  54. package/src/gateway/platforms/feishu.js +0 -32
  55. package/src/gateway/platforms/feishu_comment.js +0 -12
  56. package/src/gateway/platforms/feishu_comment_rules.js +0 -11
  57. package/src/gateway/platforms/homeassistant.js +0 -32
  58. package/src/gateway/platforms/matrix.js +0 -40
  59. package/src/gateway/platforms/mattermost.js +0 -29
  60. package/src/gateway/platforms/qqbot.js +0 -32
  61. package/src/gateway/platforms/signal.js +0 -33
  62. package/src/gateway/platforms/slack.js +0 -34
  63. package/src/gateway/platforms/sms.js +0 -34
  64. package/src/gateway/platforms/telegram.js +0 -38
  65. package/src/gateway/platforms/telegram_network.js +0 -17
  66. package/src/gateway/platforms/webhook.js +0 -19
  67. package/src/gateway/platforms/wecom.js +0 -32
  68. package/src/gateway/platforms/wecom_callback.js +0 -15
  69. package/src/gateway/platforms/wecom_crypto.js +0 -16
  70. package/src/gateway/platforms/weixin.js +0 -32
  71. package/src/gateway/platforms/whatsapp.js +0 -40
  72. package/src/gateway/platforms/yuanbao.js +0 -9
  73. package/src/gateway/platforms/yuanbao_media.js +0 -5
  74. package/src/gateway/platforms/yuanbao_proto.js +0 -9
  75. package/src/gateway/platforms/yuanbao_sticker.js +0 -6
  76. package/src/plugins/memory/_index.js +0 -8
  77. package/src/plugins/memory/byterover.js +0 -25
  78. package/src/plugins/memory/hindsight.js +0 -25
  79. package/src/plugins/memory/holographic.js +0 -31
  80. package/src/plugins/memory/honcho.js +0 -25
  81. package/src/plugins/memory/mem0.js +0 -25
  82. package/src/plugins/memory/openviking.js +0 -25
  83. package/src/plugins/memory/retaindb.js +0 -25
  84. package/src/plugins/memory/supermemory.js +0 -25
  85. package/src/tools/ansi_strip.js +0 -8
  86. package/src/tools/approval.js +0 -15
  87. package/src/tools/bash.js +0 -35
  88. package/src/tools/binary_extensions.js +0 -22
  89. package/src/tools/browser.js +0 -48
  90. package/src/tools/budget_config.js +0 -13
  91. package/src/tools/checkpoint.js +0 -29
  92. package/src/tools/clarify.js +0 -15
  93. package/src/tools/code_execution.js +0 -27
  94. package/src/tools/credential_files.js +0 -16
  95. package/src/tools/cronjob.js +0 -16
  96. package/src/tools/debug_helpers.js +0 -9
  97. package/src/tools/delegate.js +0 -28
  98. package/src/tools/discord_tool.js +0 -13
  99. package/src/tools/edit.js +0 -31
  100. package/src/tools/env_passthrough.js +0 -15
  101. package/src/tools/feishu_doc.js +0 -15
  102. package/src/tools/feishu_drive.js +0 -14
  103. package/src/tools/file_operations.js +0 -17
  104. package/src/tools/file_state.js +0 -16
  105. package/src/tools/file_tools.js +0 -23
  106. package/src/tools/fuzzy_match.js +0 -8
  107. package/src/tools/grep.js +0 -51
  108. package/src/tools/homeassistant_tool.js +0 -15
  109. package/src/tools/image_gen.js +0 -33
  110. package/src/tools/interrupt.js +0 -18
  111. package/src/tools/managed_tool_gateway.js +0 -11
  112. package/src/tools/mcp_oauth.js +0 -21
  113. package/src/tools/mcp_oauth_manager.js +0 -20
  114. package/src/tools/mcp_tool.js +0 -36
  115. package/src/tools/memory.js +0 -66
  116. package/src/tools/mixture_of_agents.js +0 -14
  117. package/src/tools/neutts_synth.js +0 -13
  118. package/src/tools/openrouter_client.js +0 -13
  119. package/src/tools/osv_check.js +0 -11
  120. package/src/tools/patch_parser.js +0 -42
  121. package/src/tools/path_security.js +0 -16
  122. package/src/tools/process_registry.js +0 -17
  123. package/src/tools/read.js +0 -26
  124. package/src/tools/registry.js +0 -54
  125. package/src/tools/rl_training.js +0 -13
  126. package/src/tools/schema_sanitizer.js +0 -18
  127. package/src/tools/send_message.js +0 -32
  128. package/src/tools/session_search.js +0 -23
  129. package/src/tools/skill_manager.js +0 -17
  130. package/src/tools/skill_usage.js +0 -20
  131. package/src/tools/skills_guard.js +0 -17
  132. package/src/tools/skills_hub.js +0 -31
  133. package/src/tools/skills_index.js +0 -14
  134. package/src/tools/skills_sync.js +0 -19
  135. package/src/tools/skills_tool.js +0 -11
  136. package/src/tools/slash_confirm.js +0 -16
  137. package/src/tools/terminal.js +0 -29
  138. package/src/tools/tirith_security.js +0 -25
  139. package/src/tools/todo.js +0 -54
  140. package/src/tools/tool_backend_helpers.js +0 -26
  141. package/src/tools/tool_output_limits.js +0 -15
  142. package/src/tools/tool_result_storage.js +0 -20
  143. package/src/tools/transcription.js +0 -19
  144. package/src/tools/tts.js +0 -19
  145. package/src/tools/url_safety.js +0 -15
  146. package/src/tools/vision.js +0 -18
  147. package/src/tools/voice_mode.js +0 -10
  148. package/src/tools/web_search.js +0 -37
  149. package/src/tools/web_tools.js +0 -18
  150. package/src/tools/website_policy.js +0 -14
  151. package/src/tools/write.js +0 -25
  152. package/src/tools/xai_http.js +0 -13
  153. package/src/tools/yuanbao_tools.js +0 -13
package/AGENTS.md CHANGED
@@ -12,10 +12,40 @@ Instructions for AI coding assistants working on Freddie.
12
12
  - `anentrypoint-design` v0.0.27 — webjsx + ripple-ui. Use for any web UI; do NOT add React. Source in C:/dev/anentrypoint-design; freddie links via `file:../anentrypoint-design`.
13
13
  - `xstate` v5 — every long-lived state machine (agent turns, gateway lifecycle, approvals).
14
14
 
15
+ ## Plugin architecture (2026-05-03, pre-v1, no compat shims)
16
+
17
+ The monolith was decomposed into a universal plugin contract. Every tool, platform, memory provider, GUI route, and core subsystem is a plugin under `plugins/<name>/`. The old paths (`src/tools/registry.js`, `src/tools/*.js`, `src/gateway/platforms/*.js`, `src/plugins/memory/*.js`) are GONE — do not reach for them.
18
+
19
+ Contract: `{ name, version?, surfaces: 'pi'|'gui'|'both', requires?: [...names], register(ctx) }` — defined in `src/host/contract.js` (39L).
20
+ - PI_VERBS: tool, env, command, cron, platform, memory, skill, context, agentExt, cli
21
+ - GUI_VERBS: route, page, nav, debug, api, asset
22
+ - HOOK_NAMES: preToolCall, postToolCall, preLlmCall, postLlmCall, onSessionStart, onSessionEnd, onTurnStart, onTurnEnd, onMessageInbound, onMessageOutbound
23
+ - Surface guard throws `plugin <name>: surface verb '<verb>' not allowed (declared surfaces=<name>)` at load
24
+ - `requires` cycles throw `plugin cycle: a -> b -> a` synchronously
25
+
26
+ Host: `src/host/host.js` (157L) — `createHost({surfaces, configStore, env})` + `discoverPlugins(roots)`. Singleton in `src/host/index.js`: `host()`, `bootHost(extraRoots)`, `resetHostForTests()`. Roots walked: `<repo>/plugins`, `~/.freddie/plugins/`, `<cwd>/.freddie/plugins/`.
27
+
28
+ `register(ctx)` receives `{ pi, gui, hooks, log, config, host, env }`:
29
+ - `log` — scoped JSONL with plugin name
30
+ - `config` — scoped under `plugins.<name>` (`get/set/all`)
31
+ - `host` — `{plugins(), get(name)}`
32
+
33
+ Migrated 120+ in-tree plugins: 70 tools, 27 platforms, 8 memory providers, 11 GUI dashboard plugins (`gui-sessions/tools/cron/skills/config/env/debug/chat/batch/gateway/profiles-commands-health`), 6 core plugins (`core-cli/skills/cron/commands/agent-machine/context-engine/compressor`). Tool plugins lay out as `plugins/<name>/{plugin,handler}.js` where handler exports `_tool` or `_tool0`/`_tool1` for multi-tool files; `plugin.js` calls `pi.tools.register(_tool)`.
34
+
35
+ Thin shims (still resolved through host, do not bypass):
36
+ - `src/plugins/manager.js` — over the host
37
+ - `src/web/server.js` (23L) — iterates `host.gui.routes.list()`
38
+ - `bin/freddie.js` (19L) — iterates `host.pi.cli.list()` and registers commander commands
39
+ - `src/gateway/platforms.js` — `makePlatform/getPlatformAdapter/listPlatformNames` (finds adapter by `*Adapter$` name match)
40
+ - `src/plugins/memory/provider.js` — host-router (`createMemoryProvider`, `listMemoryProviders`, `registerMemoryProvider`, `MemoryProvider`)
41
+ - All consumers (`acp/server.js`, `acp/tools.js`, `mcp/server.js`, `agent/machine.js`, `toolsets.js`, `cli/gateway_cli.js`) resolve via `bootHost()`
42
+
43
+ Witness 2026-05-03: test.js 12/12 green @ 195L (asserts `host.plugins().length>=100`, `platforms.list>=18`, `memory.list>=8`, surface guard throws, cycle throws). `node bin/freddie.js tools` shows 70. `help-all` 32 lines. 11 dashboard `/api/*` routes return 200.
44
+
15
45
  ## Layout
16
46
 
17
47
  ```
18
- src/home.js # getFophHome, applyProfileOverride
48
+ src/home.js # getFreddieHome, applyProfileOverride
19
49
  src/config.js # loadConfig, saveConfigValue, DEFAULT_CONFIG, _config_version migrations
20
50
  src/sessions.js # better-sqlite3 + FTS5
21
51
  src/auth.js # FileAuthStore for credentials
@@ -49,20 +79,29 @@ bin/freddie.js # commander CLI: tools, skills, profile, skin
49
79
 
50
80
  ## Adding a tool
51
81
 
52
- ```js
53
- import { registry } from './tools/registry.js'
82
+ Tools are now plugins. Create `plugins/<name>/plugin.js` + `plugins/<name>/handler.js`:
54
83
 
55
- registry.register({
84
+ ```js
85
+ // plugins/my-tool/handler.js
86
+ export const _tool = {
56
87
  name: 'my_tool',
57
88
  toolset: 'core',
58
89
  schema: { name: 'my_tool', description: '…', parameters: { type: 'object', properties: { x: { type: 'string' } }, required: ['x'] } },
59
90
  handler: async (args, ctx) => ({ ok: true, x: args.x }),
60
91
  checkFn: () => !!process.env.MY_KEY,
61
92
  requiresEnv: ['MY_KEY'],
62
- })
93
+ }
94
+
95
+ // plugins/my-tool/plugin.js
96
+ import { _tool } from './handler.js'
97
+ export default {
98
+ name: 'my-tool',
99
+ surfaces: 'pi',
100
+ register({ pi }) { pi.tools.register(_tool) },
101
+ }
63
102
  ```
64
103
 
65
- Drop the file in `src/tools/`. Auto-discovered by `discoverBuiltinTools()`.
104
+ Auto-discovered on `bootHost()`. For multi-tool files export `_tool0`, `_tool1`, ….
66
105
 
67
106
  ## Adding a slash command
68
107
 
@@ -76,14 +115,31 @@ Dispatch happens against the canonical name resolved via `resolveCommand()`. Gat
76
115
 
77
116
  ## Adding a gateway platform
78
117
 
79
- 1. Drop `src/gateway/platforms/<name>.js`
80
- 2. Extend `EventEmitter`, implement `start/stop/send`, emit `'message'`
81
- 3. `Gateway.register('name', adapter)` wires inbound to the agent
118
+ Platforms are plugins. Create `plugins/platform-<name>/{plugin,handler}.js`:
119
+
120
+ ```js
121
+ // handler.js — class name MUST end with `Adapter` for getPlatformAdapter() to resolve it
122
+ export class MynameAdapter extends EventEmitter {
123
+ async start() { /* … */ }
124
+ async stop() { /* … */ }
125
+ async send(msg) { /* … */ }
126
+ }
127
+
128
+ // plugin.js
129
+ import * as module from './handler.js'
130
+ export default {
131
+ name: 'platform-myname',
132
+ surfaces: 'pi',
133
+ register({ pi }) { pi.platforms.register({ name: 'myname', module }) },
134
+ }
135
+ ```
136
+
137
+ `makePlatform('myname', opts)` (from `src/gateway/platforms.js`) instantiates the adapter via `*Adapter$` name match.
82
138
 
83
139
  ## Profile-safe code
84
140
 
85
- - Always `getFophHome()` for state paths. Never `path.join(os.homedir(), '.freddie')`.
86
- - Always `displayFophHome()` for user-visible messages (returns `~/.freddie` or `~/.freddie/profiles/<name>`).
141
+ - Always `getFreddieHome()` for state paths. Never `path.join(os.homedir(), '.freddie')`.
142
+ - Always `displayFreddieHome()` for user-visible messages (returns `~/.freddie` or `~/.freddie/profiles/<name>`).
87
143
  - Profile operations are HOME-anchored: `getProfilesRoot()` returns `~/.freddie/profiles` regardless of active profile.
88
144
 
89
145
  ## Cache safety
@@ -100,6 +156,9 @@ One `test.js` at project root. ≤200 lines. Plain assertions, real data, real s
100
156
  - `pi-ai` reads provider keys via `findEnvKeys` / `getEnvApiKey`. Match its env var names (`ANTHROPIC_API_KEY`, etc.).
101
157
  - `floosie.ProcessorMachine` is an xstate machine. Compose, don't fork.
102
158
  - **Browser inline `<script type="module">` syntax errors** — When a pageerror reports "missing ) after argument list" with no file:line info, extract the script body to a separate `.js` file and run `node --check path/to/file.js`. Browsers swallow line numbers for inline modules; node's V8 parser prints exact line. Essential for debugging unbalanced parens in webjsx-style nested `h()` calls. (Confirmed 2026-04-30: freddie dashboard app.js, line 133.)
159
+ - **src/web/app.js 200-line policy violation** — File is 548 lines, violating gm hard cap (2.7× over). Only file in 283-file codebase over limit. Likely waived intentionally or is drift to fix. When touching app.js, prefer splitting into `{app,routes,components,state}.js` over expanding further. Do not add 50+ more lines without addressing the split.
160
+ - **libsql async debt class** — `src/sessions.js` (listSessions/search/getMessages/createSession/appendMessage) and `src/cron/scheduler.js` (listJobs/createJob/cancelJob/deleteJob) are async after the libsql migration. Sync callsites silently wrap each call in a Promise that rejects on iteration, surfacing as `TypeError: ... is not iterable` via `node bin/freddie.js sessions` or `freddie cron list`. Rule: every call into those modules must be awaited; tool ACTIONS inner functions async + handler awaits dispatched fn. Fixed 2026-05-03 across bin/freddie.js, src/web/server.js, src/cli/dump.js, src/cli/status.js, src/tools/session_search.js, src/tools/cronjob.js, src/acp/session.js. test.js can pass while CLI is broken — exercise the cli verb in test.js or smoke `node bin/freddie.js <verb>` after changes.
161
+ - **Bulk-rename: git grep is case-sensitive on literal patterns** — `git grep -lI <name>` only matches lowercase. For case-variant sweep during rename refactors, use `git grep -liI -e <lower> -e <Title> -e <UPPER>` (per-pattern `-i` requires `-e` form). Single-form check is a false-clean trap.
103
162
 
104
163
  ## Subsystem guide
105
164
 
@@ -157,10 +216,19 @@ All 21 named integration tests in `test.js` pass (exit 0). Subsystem coverage:
157
216
  - **LLM resolver priority** — (1) explicit `callLLM` arg, (2) pi-bridge if `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GROQ_API_KEY` / `OPENROUTER_API_KEY` env set, (3) acptoapi if `/v1/models` returns 200, (4) throw with actionable error. Configurable via `FREDDIE_LLM_URL` and `FREDDIE_LLM_MODEL` env vars.
158
217
  - **acptoapi Claude backend tool-call mismatch** — acptoapi's Claude backend returns Anthropic-style XML `<function_calls><invoke name="bash">…</invoke></function_calls>` in message content, not OpenAI `tool_calls` JSON array. This breaks the agent's tool-loop auto-fire. Workaround: use real Anthropic API key + pi-bridge for proper tool dispatch, or use structured `callLLM` stubs in tests.
159
218
 
219
+ ## Pre-rename validation snapshot (2026-05-03)
220
+
221
+ All 12 test.js named groups passing: home+config+skin, sessions+FTS5, tools+toolsets, agent-machine, gateway+platforms+hooks, acp-full, plugins+memory, profiles+observability+auth+env+context+cron+batch+slash+skills, utils+time+redact+model-meta+agent-helpers, mcp+swe+distributions+account+credpool, compressor+trajectory, env+pi+cli+tui+setup+website+helpers. CLI boots (`node bin/freddie.js --version` → 0.1.0), tools list 25+ across core/browse/creative, commander 14 commands. 284 source files, test.js 198/200 lines, pkg.version 0.0.39 but bin reports 0.1.0. Node_modules installed, lockfile present. Baseline established before rename; re-run test.js post-rename to isolate rename-induced failures.
222
+
160
223
  ## Learning audit
161
224
 
162
225
  - 2026-05-01: 5 items queried (pi-ai keys, profile paths, cache safety, floosie composition, browser errors); rs-learn store unavailable (exec:recall returned no results). 0 items migrated. New facts (anentrypoint-design build, dashboard live-rerender caveat, libuv spawn caveat) ingested directly into rs-learn; audit will retry in future sessions.
163
226
  - 2026-05-01 (session 2): 5 items queried (pi-ai env keys, profile safe paths, cache safety, floosie composition, browser syntax errors). rs-learn store still empty. 0 items migrated. Refined anentrypoint-design source/dist skew entry in AGENTS.md to include silent-failure pageerror diagnostic. New fact `reference/anentrypoint-design-dist-rebuild` ingested.
227
+ - 2026-05-03: Pre-rename validation snapshot recorded (all 12 test.js groups, CLI, tools, 284 files, version drift). Baseline stored to isolate post-rename regressions.
228
+ - 2026-05-03 (session 2): Ingested feedback/app-js-size-violation (src/web/app.js 548L violation) into AGENTS.md Substrate gotchas. rs-learn store unavailable (exec:memorize missing binary). 0 migration audit items queried. 1 new fact added.
229
+ - 2026-05-03 (session 3): Added Website theme + YAML caveats section (3 items: structured-YAML rendering, YAML colon-space trap, SSR innerHTML injection). rs-learn store still unavailable (exec:memorize → exit 127, command not found). 0 migration audit items queried. 3 new facts added to AGENTS.md only.
230
+ - 2026-05-03 (session 3): Ingested libsql-async-debt-class into AGENTS.md Substrate gotchas (sessions.js + cron/scheduler.js async callsites; silent TypeError class; test.js passes while CLI broken). rs-learn store still unavailable (exec:memorize/exec:recall not on PATH). 0 migration audit items queried. 1 new fact added.
231
+ - 2026-05-03 (session 4): Plugin-architecture decomposition recorded — added "Plugin architecture" section before Layout, rewrote "Adding a tool" + "Adding a gateway platform" for plugins/<name>/{plugin,handler}.js shape. Ingested 6 facts to rs-learn (project/freddie-plugin-architecture, reference/freddie-host-contract, reference/freddie-plugin-ctx, project/freddie-migrated-subsystems, reference/freddie-thin-shims, project/freddie-plugin-witness). Audit: 5 queries fired (pi-ai env keys, profile safe paths, libsql async debt, browser inline module errors, yaml colon space trap, plus self-test on freddie-plugin-architecture) — all returned "No recall results". rs-learn ingest path live but retrieval side empty for this session (likely needs learn-build propagation). 0 items migrated; AGENTS.md items retained.
164
232
 
165
233
  ## Dashboard web UI caveats
166
234
 
@@ -168,6 +236,12 @@ All 21 named integration tests in `test.js` pass (exit 0). Subsystem coverage:
168
236
  - **Live page rerender caveat** — AppState.body caching (page computed once at navigation, body saved) breaks for live routes like #/chat where AppState is mutated mid-flight (SSE pushes new messages). Fix: detect live routes in rerender(), recompute body: `if (AppState.hash === '#/chat') { Promise.resolve(PAGES['#/chat']()).then(b => { AppState.body = b; _mount() }); return }`. Any future live-streaming pages (cron output, traces) need the same treatment.
169
237
  - **libuv spawn caveat** — Spawning createDashboard() from exec:nodejs and keeping process alive triggers libuv UV_HANDLE_CLOSING crash on shutdown. Reliable alternative: boot via `node bin/freddie.js dashboard --port <port>`. Liveness checks: exec:browser → page.goto → window.__debug.dashboard() returns {booted, ts, framework, route}; window.__debug.chat() exposes {messages, streaming, draft}; window.__debug.sendChat(text) drives round-trips.
170
238
 
239
+ ## Website theme + YAML caveats (2026-05-03)
240
+
241
+ - **Structured-YAML rendering** — `website/theme.mjs` (164L) renders structured YAML via 247420 design vocabulary, not raw markdown. Consumes `page.hero` (heading/subheading/accent/body/badges/ctas), `page.sections[]` (rotating rail color green→purple→mascot→sun→flame→sky by section index, optional `lede` + per-item `benefit` italic), `page.examples[]` (railed link list with mono numeric ranks + ↗ glyph). Falls back to `page.body` markdown for prose. Style block inlined so rail/dot/chip/btn classes work without ds-247420 SDK CSS loading first. To get a specific rail color, reorder sections. Prefer enriching hero+sections+examples over expanding body markdown; copy existing YAML structure as template for new pages.
242
+ - **YAML colon-space trap** — In `website/content/pages/*.yaml`, any value containing `: ` outside backticks (e.g. `[linux, macos, windows]`, `requiresEnv: ['MY_KEY']` code snippets) MUST be double-quoted. The parser otherwise interprets the embedded colon as a mapping and the file fails to load. Hit twice: tools.yaml line 72, skills.yaml line 40. Fix is wrapping the whole value in `"..."`.
243
+ - **SSR innerHTML injection beats client dispatch** — anentrypoint-design v0.0.27 exposes Hero/HomeView/Panel/Row/Section/WorksList, but pre-mounted SSR injection via innerHTML is more reliable than dispatching components client-side at build — avoids depending on SDK loading before the static HTML paints. Emitted HTML carries rail/dot/chip/btn classes with inline styles to be self-sufficient.
244
+
171
245
  ## Residual complement (NOT ported this session)
172
246
 
173
247
  Genuinely out of session reach, with reasons:
package/CHANGELOG.md CHANGED
@@ -30,3 +30,19 @@ All notable changes to this project will be documented in this file.
30
30
  ## Previous Releases
31
31
 
32
32
  See git history for earlier versions.
33
+
34
+ ## v0.1.1 — Website expressiveness + async callsite repair
35
+
36
+ - website: theme.mjs now consumes structured YAML (hero/sections/examples) and renders via the 247420 design vocabulary — railed panels, badges, CTAs, mono-rank explore lists. Six content pages rewritten to express explicit when/why/how lines + per-row benefit framings.
37
+ - bug: repaired async callsite debt across bin/freddie.js, src/web/server.js, src/cli/dump.js, src/cli/status.js, src/tools/session_search.js, src/tools/cronjob.js, src/acp/session.js — every consumer of sessions.js and cron/scheduler.js now awaits. `freddie sessions`, `freddie cron list`, `freddie search` exit 0.
38
+ - tests: test.js still 12/12. tools registered: 70.
39
+
40
+ ## v0.2.0 — Plugin architecture foundation
41
+
42
+ - Universal plugin contract at src/host/contract.js: { name, surfaces: pi|gui|both, requires?, register(ctx) } with topo-sorted load, surface guards, dep cycle detection, hook registry.
43
+ - src/host/host.js implements createHost, surface registries (pi: tools/envs/commands/crons/platforms/memory/skills/contexts/agentExts; gui: routes/pages/nav/debug/api/asset), and discoverPlugins.
44
+ - src/host/index.js exposes singleton bootHost() that walks bundled plugins/ + ~/.freddie/plugins/ + .freddie/plugins/.
45
+ - 103 in-tree plugins shipped: 70 tools (plugins/<name>/), 27 platforms (plugins/platform-<name>/), 8 memory providers (plugins/memory-<name>/), plus the prior 6 stub plugins.
46
+ - src/tools/registry.js, src/tools/*.js, src/gateway/platforms/*.js all deleted; legacy src/plugins/manager.js becomes a thin shim over the new host.
47
+ - Consumers (bin/freddie.js, src/agent/machine.js, src/web/server.js, src/acp/server.js, src/acp/tools.js, src/mcp/server.js, src/cli/gateway_cli.js, src/toolsets.js) now resolve everything via bootHost().
48
+ - test.js asserts surface-guard throws, requires-cycle throws, plugin counts (>=100 plugins, >=18 platforms, >=8 memory). 12/12 groups green at 195 lines.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Freddie
2
-
2
+
3
3
  An open JS agent harness built on pi-mono, xstate, floosie, and anentrypoint-design. Features a full gateway, context compressor, multi-platform adapters, and a live dashboard — built with:
4
4
 
5
5
  - [`@mariozechner/pi-coding-agent`](https://www.npmjs.com/package/@mariozechner/pi-coding-agent) — agent + tools + interactive TUI substrate
@@ -81,7 +81,7 @@ Built-in: `bash`, `read`, `write`, `edit`, `grep`, `todo`, `memory`, `delegate`,
81
81
  freddie/
82
82
  ├── bin/freddie.js # commander CLI: tools, skills, profile, skin, sessions, search, gateway, acp, run, cron, batch, dashboard, help-all
83
83
  ├── src/
84
- │ ├── home.js # getFophHome + profiles
84
+ │ ├── home.js # getFreddieHome + profiles
85
85
  │ ├── config.js # YAML + migrations
86
86
  │ ├── sessions.js # SQLite + FTS5
87
87
  │ ├── auth.js # FileAuthStore (~/.freddie/auth/)
package/bin/freddie.js CHANGED
@@ -1,116 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander'
3
- import { registry, discoverBuiltinTools } from '../src/tools/registry.js'
4
- import { listAllProfiles, createProfile, deleteProfile, switchProfile } from '../src/commands/profile.js'
5
- import { listSkills } from '../src/skills/index.js'
6
- import { Gateway } from '../src/gateway/run.js'
7
- import { WebhookAdapter } from '../src/gateway/platforms/webhook.js'
8
- import { ApiServerAdapter } from '../src/gateway/platforms/api_server.js'
9
- import { AcpServer } from '../src/acp/server.js'
10
- import { COMMAND_REGISTRY, COMMANDS_BY_CATEGORY } from '../src/commands/registry.js'
11
- import { getActiveSkin, listBuiltinSkins, setActiveSkin } from '../src/skin/engine.js'
12
- import { listSessions, search } from '../src/sessions.js'
3
+ import { bootHost } from '../src/host/index.js'
13
4
 
14
5
  const program = new Command()
15
- program.name('freddie').version('0.1.0').description('Freddie — JS port of hermes-agent built on pi-mono')
16
-
17
- program.command('tools')
18
- .description('List/inspect tools')
19
- .argument('[action]', 'list | get', 'list')
20
- .argument('[name]')
21
- .action(async (action, name) => {
22
- await discoverBuiltinTools()
23
- if (action === 'get' && name) { console.log(JSON.stringify(registry.get(name)?.schema, null, 2)); return }
24
- for (const t of registry.list()) console.log(`${t.toolset.padEnd(10)} ${t.name}\t${t.schema.description.slice(0, 60)}`)
25
- })
26
-
27
- program.command('skills')
28
- .description('List skills')
29
- .argument('[action]', 'list', 'list')
30
- .action(() => {
31
- for (const s of listSkills()) console.log(`${s.name}\t${s.description.slice(0, 80)}`)
32
- })
33
-
34
- program.command('profile')
35
- .argument('[action]', 'list | create | switch | delete', 'list')
36
- .argument('[name]')
37
- .action((action, name) => {
38
- if (action === 'list') { for (const p of listAllProfiles()) console.log(p); return }
39
- if (action === 'create') { createProfile(name); console.log('created:', name); return }
40
- if (action === 'delete') { deleteProfile(name); console.log('deleted:', name); return }
41
- if (action === 'switch') { switchProfile(name); console.log('switched:', name || 'default'); return }
42
- })
43
-
44
- program.command('skin')
45
- .argument('[name]')
46
- .action((name) => {
47
- if (!name) { console.log('active:', getActiveSkin().name); console.log('available:', listBuiltinSkins().join(', ')); return }
48
- setActiveSkin(name); console.log('switched to:', name)
49
- })
50
-
51
- program.command('sessions').action(() => { for (const s of listSessions()) console.log(`${s.id}\t${s.platform}\t${new Date(s.updated_at).toISOString()}\t${s.title || ''}`) })
52
- program.command('search').argument('<query>').action((q) => { for (const r of search(q)) console.log(`${r.session_id}\t${(r.content || '').slice(0, 100)}`) })
53
-
54
- program.command('gateway')
55
- .option('--port <port>', 'webhook port', '0')
56
- .action(async (opts) => {
57
- const webhook = new WebhookAdapter({ port: Number(opts.port) })
58
- const api = new ApiServerAdapter({ port: 0 })
59
- const gw = new Gateway({ platforms: { webhook, api_server: api } })
60
- await gw.start()
61
- console.log('webhook port:', webhook.port, '\napi_server port:', api.port)
62
- process.on('SIGINT', async () => { await gw.stop(); process.exit(0) })
63
- })
64
-
65
- program.command('acp').action(() => { const s = new AcpServer(); s.start() })
66
-
67
- program.command('help-all').action(() => {
68
- for (const [cat, cmds] of Object.entries(COMMANDS_BY_CATEGORY)) {
69
- console.log(`\n# ${cat}`)
70
- for (const c of cmds) console.log(` /${c.name}${c.args_hint ? ' ' + c.args_hint : ''}\t${c.description}`)
6
+ program.name('freddie').version('0.1.0').description('Freddie — open JS agent harness, plugin-driven')
7
+
8
+ const host = await bootHost()
9
+ for (const def of host.pi.cli.list()) {
10
+ const cmd = program.command(def.name).description(def.description || '')
11
+ for (const a of def.args || []) {
12
+ const tag = a.required ? `<${a.name}>` : `[${a.name}]`
13
+ cmd.argument(tag, a.description || '', a.default)
71
14
  }
72
- })
73
-
74
- program.command('run').description('Run interactive REPL').action(async () => {
75
- const { interactive } = await import('../src/cli/interactive.js')
76
- let callLLM = null
77
- try { ({ callLLM } = await import('../src/agent/pi-bridge.js')) } catch {}
78
- await interactive({ callLLM })
79
- })
80
-
81
- program.command('cron')
82
- .argument('[action]', 'list | add | cancel | delete | tick', 'list')
83
- .argument('[arg1]')
84
- .argument('[arg2]')
85
- .action(async (action, a1, a2) => {
86
- const { listJobs, createJob, cancelJob, deleteJob, tick } = await import('../src/cron/scheduler.js')
87
- if (action === 'list') { for (const j of listJobs()) console.log(`${j.id}\t${j.cron}\t${j.enabled ? 'on ' : 'off'}\t${j.prompt.slice(0, 60)}`); return }
88
- if (action === 'add') { const id = createJob({ cron: a1, prompt: a2 }); console.log('created:', id); return }
89
- if (action === 'cancel') { cancelJob(Number(a1)); console.log('cancelled:', a1); return }
90
- if (action === 'delete') { deleteJob(Number(a1)); console.log('deleted:', a1); return }
91
- if (action === 'tick') { console.log('fired:', (await tick()).length); return }
92
- })
93
-
94
- program.command('batch')
95
- .argument('<file>', 'JSONL or TXT prompts file')
96
- .option('--concurrency <n>', '', '4')
97
- .option('--model <model>', '', '')
98
- .action(async (file, opts) => {
99
- const fs = await import('node:fs')
100
- const { runBatch } = await import('../src/batch.js')
101
- const raw = fs.readFileSync(file, 'utf8').trim().split('\n')
102
- const prompts = raw.map(l => { try { return JSON.parse(l).prompt || JSON.parse(l) } catch { return l } }).filter(Boolean)
103
- const out = await runBatch({ prompts, concurrency: Number(opts.concurrency), model: opts.model })
104
- console.log('batch:', out.id, '\nfile:', out.file, '\nresults:', out.results.length)
105
- })
106
-
107
- program.command('dashboard')
108
- .option('--port <port>', '', '0')
109
- .action(async (opts) => {
110
- const { createDashboard } = await import('../src/web/server.js')
111
- const d = await createDashboard({ port: Number(opts.port) })
112
- console.log('dashboard:', d.url)
113
- process.on('SIGINT', async () => { await d.stop(); process.exit(0) })
114
- })
15
+ for (const o of def.options || []) cmd.option(o.flag, o.description || '', o.default)
16
+ cmd.action(def.action)
17
+ }
115
18
 
116
19
  program.parseAsync(process.argv).catch(e => { console.error(e); process.exit(1) })
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "freddie",
3
- "version": "0.0.41",
3
+ "version": "0.0.43",
4
4
  "type": "module",
5
- "description": "Freddie — Free Realtime Dynamic Dialogue Information Engine. Open JS agent harness built on pi-mono, floosie, xstate, and anentrypoint-design",
5
+ "description": "Open JS agent harness built on pi-mono, floosie, xstate, and anentrypoint-design",
6
6
  "bin": {
7
7
  "freddie": "./bin/freddie.js"
8
8
  },
@@ -26,6 +26,15 @@
26
26
  "xstate": "^5.31.0",
27
27
  "zod": "^4.0.0"
28
28
  },
29
+ "optionalDependencies": {
30
+ "@libsql/darwin-arm64": "0.3.19",
31
+ "@libsql/darwin-x64": "0.3.19",
32
+ "@libsql/linux-arm64-gnu": "0.3.19",
33
+ "@libsql/linux-arm64-musl": "0.3.19",
34
+ "@libsql/linux-x64-gnu": "0.3.19",
35
+ "@libsql/linux-x64-musl": "0.3.19",
36
+ "@libsql/win32-x64-msvc": "0.3.19"
37
+ },
29
38
  "engines": {
30
39
  "node": ">=20.6.0"
31
40
  },
package/src/acp/server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import readline from 'node:readline'
2
2
  import { EventEmitter } from 'node:events'
3
- import { registry, discoverBuiltinTools } from '../tools/registry.js'
3
+ import { bootHost } from '../host/index.js'
4
4
  import { runTurn } from '../agent/machine.js'
5
5
  import { logger } from '../observability/log.js'
6
6
  import { Events } from './events.js'
@@ -59,8 +59,8 @@ const METHODS = {
59
59
  'session.list': (srv) => srv.sessions.list(),
60
60
  'session.end': (srv, { sessionId }) => { Events.sessionEnded((o) => srv.send(o), { sessionId }); return srv.sessions.end(sessionId) },
61
61
  'tool.list': async () => {
62
- await discoverBuiltinTools()
63
- return { tools: registry.list().map(t => ({ name: t.name, toolset: t.toolset, schema: t.schema })) }
62
+ const h = await bootHost()
63
+ return { tools: h.pi.tools.list().map(t => ({ name: t.name, toolset: t.toolset, schema: t.schema })) }
64
64
  },
65
65
  'permission.respond': (srv, { reqId, decision }) => {
66
66
  const pending = srv._pendingPerm.get(reqId)
@@ -2,25 +2,25 @@ import { createSession, appendMessage, getMessages, listSessions } from '../sess
2
2
 
3
3
  export class AcpSessionManager {
4
4
  constructor() { this.active = new Map() }
5
- new(opts = {}) {
6
- const id = createSession({ platform: 'acp', ...opts })
5
+ async new(opts = {}) {
6
+ const id = await createSession({ platform: 'acp', ...opts })
7
7
  this.active.set(id, { id, created: Date.now(), opts })
8
8
  return { sessionId: id }
9
9
  }
10
- resume(sessionId) {
11
- const messages = getMessages(sessionId)
10
+ async resume(sessionId) {
11
+ const messages = await getMessages(sessionId)
12
12
  if (!messages) return null
13
13
  this.active.set(sessionId, { id: sessionId, resumed: Date.now(), messages })
14
14
  return { sessionId, messages }
15
15
  }
16
- list() {
17
- return { sessions: listSessions(50).filter(s => s.platform === 'acp') }
16
+ async list() {
17
+ return { sessions: (await listSessions(50)).filter(s => s.platform === 'acp') }
18
18
  }
19
19
  end(sessionId) {
20
20
  this.active.delete(sessionId)
21
21
  return { ended: sessionId }
22
22
  }
23
- appendUser(sessionId, content) { appendMessage(sessionId, { role: 'user', content }) }
24
- appendAssistant(sessionId, content) { appendMessage(sessionId, { role: 'assistant', content }) }
23
+ async appendUser(sessionId, content) { await appendMessage(sessionId, { role: 'user', content }) }
24
+ async appendAssistant(sessionId, content) { await appendMessage(sessionId, { role: 'assistant', content }) }
25
25
  isActive(sessionId) { return this.active.has(sessionId) }
26
26
  }
package/src/acp/tools.js CHANGED
@@ -1,13 +1,14 @@
1
- import { registry, discoverBuiltinTools } from '../tools/registry.js'
1
+ import { bootHost } from '../host/index.js'
2
2
  import { Events } from './events.js'
3
3
  export async function listToolsForAcp() {
4
- await discoverBuiltinTools()
5
- return registry.list().map(t => ({ name: t.name, toolset: t.toolset, schema: t.schema, requiresEnv: t.requiresEnv || [] }))
4
+ const h = await bootHost()
5
+ return h.pi.tools.list().map(t => ({ name: t.name, toolset: t.toolset, schema: t.schema, requiresEnv: t.requiresEnv || [] }))
6
6
  }
7
7
  export async function dispatchWithEvents({ name, args, send, sessionId = null }) {
8
+ const h = await bootHost()
8
9
  Events.toolStart(send, { sessionId, name, args })
9
10
  try {
10
- const result = await registry.dispatch(name, args, { sessionId })
11
+ const result = await h.pi.dispatchTool(name, args, { sessionId })
11
12
  Events.toolComplete(send, { sessionId, name, result })
12
13
  return { ok: true, result }
13
14
  } catch (e) {
@@ -2,18 +2,18 @@ import { db } from '../db.js'
2
2
 
3
3
  async function init() {
4
4
  const d = await db()
5
- d.exec(`CREATE TABLE IF NOT EXISTS account_usage (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, model TEXT, prompt_tokens INTEGER, completion_tokens INTEGER, cost_usd REAL, ts INTEGER NOT NULL)`)
5
+ await d.exec(`CREATE TABLE IF NOT EXISTS account_usage (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, model TEXT, prompt_tokens INTEGER, completion_tokens INTEGER, cost_usd REAL, ts INTEGER NOT NULL)`)
6
6
  return d
7
7
  }
8
8
  export async function record({ sessionId = null, model, promptTokens = 0, completionTokens = 0, costUsd = 0 } = {}) {
9
- (await init()).prepare(`INSERT INTO account_usage (session_id, model, prompt_tokens, completion_tokens, cost_usd, ts) VALUES (?, ?, ?, ?, ?, ?)`).run(sessionId, model, promptTokens, completionTokens, costUsd, Date.now())
9
+ await (await init()).prepare(`INSERT INTO account_usage (session_id, model, prompt_tokens, completion_tokens, cost_usd, ts) VALUES (?, ?, ?, ?, ?, ?)`).run(sessionId, model, promptTokens, completionTokens, costUsd, Date.now())
10
10
  }
11
11
  export async function totalForSession(sessionId) {
12
- return (await init()).prepare(`SELECT SUM(prompt_tokens) AS prompt, SUM(completion_tokens) AS completion, SUM(cost_usd) AS cost FROM account_usage WHERE session_id = ?`).get(sessionId) || { prompt: 0, completion: 0, cost: 0 }
12
+ return await (await init()).prepare(`SELECT SUM(prompt_tokens) AS prompt, SUM(completion_tokens) AS completion, SUM(cost_usd) AS cost FROM account_usage WHERE session_id = ?`).get(sessionId) || { prompt: 0, completion: 0, cost: 0 }
13
13
  }
14
14
  export async function totalLifetime() {
15
- return (await init()).prepare(`SELECT SUM(prompt_tokens) AS prompt, SUM(completion_tokens) AS completion, SUM(cost_usd) AS cost FROM account_usage`).get() || { prompt: 0, completion: 0, cost: 0 }
15
+ return await (await init()).prepare(`SELECT SUM(prompt_tokens) AS prompt, SUM(completion_tokens) AS completion, SUM(cost_usd) AS cost FROM account_usage`).get() || { prompt: 0, completion: 0, cost: 0 }
16
16
  }
17
17
  export async function listRecent(limit = 50) {
18
- return (await init()).prepare(`SELECT * FROM account_usage ORDER BY id DESC LIMIT ?`).all(limit)
18
+ return await (await init()).prepare(`SELECT * FROM account_usage ORDER BY id DESC LIMIT ?`).all(limit)
19
19
  }
@@ -75,6 +75,8 @@ function tryParseJson(s) { try { return typeof s === 'string' ? JSON.parse(s) :
75
75
  export async function isReachable() {
76
76
  try {
77
77
  const res = await fetch(getAcptoapiUrl().replace(/\/$/, '') + '/models', { headers: { authorization: 'Bearer none' } })
78
- return res.ok
78
+ if (!res.ok) return false
79
+ const json = await res.json()
80
+ return Array.isArray(json.data) && json.data.length > 0
79
81
  } catch { return false }
80
82
  }
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import { getFophHome } from '../home.js'
3
+ import { getFreddieHome } from '../home.js'
4
4
  import { getAuthStore } from '../auth.js'
5
5
  const ENV_MAP = { anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY', groq: 'GROQ_API_KEY', xai: 'XAI_API_KEY', openrouter: 'OPENROUTER_API_KEY', mistral: 'MISTRAL_API_KEY', deepseek: 'DEEPSEEK_API_KEY' }
6
6
  export async function resolveKey(provider) {
@@ -8,7 +8,7 @@ export async function resolveKey(provider) {
8
8
  if (process.env[env]) return { source: 'env', value: process.env[env] }
9
9
  const stored = await getAuthStore().getCredential(env)
10
10
  if (stored?.value) return { source: 'auth-store', value: stored.value }
11
- const dotEnv = path.join(getFophHome(), '.env')
11
+ const dotEnv = path.join(getFreddieHome(), '.env')
12
12
  if (fs.existsSync(dotEnv)) {
13
13
  const m = fs.readFileSync(dotEnv, 'utf8').match(new RegExp('^' + env + '=(.+)$', 'm'))
14
14
  if (m) return { source: 'dotenv', value: m[1].replace(/^["']|["']$/g, '') }
@@ -1,5 +1,5 @@
1
- import { db } from '../sessions.js'
2
- function init() { const d = db(); d.exec(`CREATE TABLE IF NOT EXISTS curated (id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT, key TEXT, value TEXT, ts INTEGER NOT NULL)`); return d }
3
- export function add(kind, key, value) { init().prepare(`INSERT INTO curated (kind, key, value, ts) VALUES (?, ?, ?, ?)`).run(kind, key, JSON.stringify(value), Date.now()); return { added: true } }
4
- export function list(kind) { return init().prepare(`SELECT * FROM curated WHERE kind = ? ORDER BY id DESC`).all(kind).map(r => ({ ...r, value: JSON.parse(r.value) })) }
5
- export function clear(kind) { init().prepare(`DELETE FROM curated WHERE kind = ?`).run(kind); return { cleared: kind } }
1
+ import { db } from '../db.js'
2
+ async function init() { const d = await db(); await d.exec(`CREATE TABLE IF NOT EXISTS curated (id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT, key TEXT, value TEXT, ts INTEGER NOT NULL)`); return d }
3
+ export async function add(kind, key, value) { await (await init()).prepare(`INSERT INTO curated (kind, key, value, ts) VALUES (?, ?, ?, ?)`).run(kind, key, JSON.stringify(value), Date.now()); return { added: true } }
4
+ export async function list(kind) { const rows = await (await init()).prepare(`SELECT * FROM curated WHERE kind = ? ORDER BY id DESC`).all(kind); return rows.map(r => ({ ...r, value: JSON.parse(r.value) })) }
5
+ export async function clear(kind) { await (await init()).prepare(`DELETE FROM curated WHERE kind = ?`).run(kind); return { cleared: kind } }
@@ -1,5 +1,5 @@
1
1
  import { createMachine, createActor, assign, fromPromise } from 'xstate'
2
- import { registry } from '../tools/registry.js'
2
+ import { bootHost } from '../host/index.js'
3
3
  import { getEnabledToolSchemas } from '../toolsets.js'
4
4
  import { logger } from '../observability/log.js'
5
5
  import { resolveCallLLM } from './llm_resolver.js'
@@ -59,11 +59,12 @@ export function createAgentMachine({ provider, model, maxIterations = 90, callLL
59
59
  executing_tools: {
60
60
  invoke: {
61
61
  src: fromPromise(async ({ input }) => {
62
+ const h = await bootHost()
62
63
  const last = input.messages[input.messages.length - 1]
63
64
  const calls = last.tool_calls || []
64
65
  const results = []
65
66
  for (const call of calls) {
66
- const res = await registry.dispatch(call.name || call.function?.name, call.arguments || call.function?.arguments || {})
67
+ const res = await h.pi.dispatchTool(call.name || call.function?.name, call.arguments || call.function?.arguments || {})
67
68
  results.push({ tool_call_id: call.id || call.tool_call_id, content: res })
68
69
  }
69
70
  return results
@@ -1,5 +1,5 @@
1
- import { db } from '../sessions.js'
2
- function init() { const d = db(); d.exec(`CREATE TABLE IF NOT EXISTS compression_feedback (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, summary TEXT, rating INTEGER, notes TEXT, ts INTEGER NOT NULL)`); return d }
3
- export function record({ sessionId, summary, rating, notes = '' }) { init().prepare(`INSERT INTO compression_feedback (session_id, summary, rating, notes, ts) VALUES (?, ?, ?, ?, ?)`).run(sessionId, summary || '', rating, notes, Date.now()); return { recorded: true } }
4
- export function listForSession(sessionId) { return init().prepare(`SELECT * FROM compression_feedback WHERE session_id = ? ORDER BY id DESC`).all(sessionId) }
5
- export function aggregate() { return init().prepare(`SELECT AVG(rating) AS avg, COUNT(*) AS n FROM compression_feedback`).get() }
1
+ import { db } from '../db.js'
2
+ async function init() { const d = await db(); await d.exec(`CREATE TABLE IF NOT EXISTS compression_feedback (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, summary TEXT, rating INTEGER, notes TEXT, ts INTEGER NOT NULL)`); return d }
3
+ export async function record({ sessionId, summary, rating, notes = '' }) { await (await init()).prepare(`INSERT INTO compression_feedback (session_id, summary, rating, notes, ts) VALUES (?, ?, ?, ?, ?)`).run(sessionId, summary || '', rating, notes, Date.now()); return { recorded: true } }
4
+ export async function listForSession(sessionId) { return await (await init()).prepare(`SELECT * FROM compression_feedback WHERE session_id = ? ORDER BY id DESC`).all(sessionId) }
5
+ export async function aggregate() { return await (await init()).prepare(`SELECT AVG(rating) AS avg, COUNT(*) AS n FROM compression_feedback`).get() }
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import { getFophHome } from '../home.js'
4
- function hookFile() { return path.join(getFophHome(), 'shell-hooks.json') }
3
+ import { getFreddieHome } from '../home.js'
4
+ function hookFile() { return path.join(getFreddieHome(), 'shell-hooks.json') }
5
5
  export function loadHooks() { try { return JSON.parse(fs.readFileSync(hookFile(), 'utf8')) } catch { return { pre_run: [], post_run: [] } } }
6
6
  export function saveHooks(h) { fs.writeFileSync(hookFile(), JSON.stringify(h, null, 2), 'utf8') }
7
7
  export function addHook(stage, command) { const h = loadHooks(); (h[stage] = h[stage] || []).push(command); saveHooks(h); return h }
package/src/auth.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import { getFophHome } from './home.js'
3
+ import { getFreddieHome } from './home.js'
4
4
 
5
5
  class FileAuthStore {
6
- constructor() { this.dir = path.join(getFophHome(), 'auth'); fs.mkdirSync(this.dir, { recursive: true }) }
6
+ constructor() { this.dir = path.join(getFreddieHome(), 'auth'); fs.mkdirSync(this.dir, { recursive: true }) }
7
7
  _path(name) { return path.join(this.dir, name + '.json') }
8
8
  async setCredential(name, value) {
9
9
  fs.writeFileSync(this._path(name), JSON.stringify({ name, value, updated: Date.now() }), { encoding: 'utf8', mode: 0o600 })
package/src/batch.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { runTurn } from './agent/machine.js'
4
- import { getFophHome } from './home.js'
4
+ import { getFreddieHome } from './home.js'
5
5
  import { randomUUID } from 'node:crypto'
6
6
 
7
7
  export async function runBatch({ prompts = [], concurrency = 4, model, callLLM } = {}) {
8
8
  if (!Array.isArray(prompts) || prompts.length === 0) throw new Error('prompts required')
9
9
  const id = randomUUID()
10
- const dir = path.join(getFophHome(), 'batches')
10
+ const dir = path.join(getFreddieHome(), 'batches')
11
11
  fs.mkdirSync(dir, { recursive: true })
12
12
  const file = path.join(dir, id + '.jsonl')
13
13
  const stream = fs.createWriteStream(file, { flags: 'a' })
package/src/cli/backup.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import { getFophHome } from '../home.js'
3
+ import { getFreddieHome } from '../home.js'
4
4
  export async function createBackup({ outFile } = {}) {
5
- const home = getFophHome()
5
+ const home = getFreddieHome()
6
6
  const out = outFile || path.join(home, 'backups', 'freddie-' + new Date().toISOString().replace(/[:.]/g, '-') + '.tar.gz')
7
7
  fs.mkdirSync(path.dirname(out), { recursive: true })
8
8
  const { spawnSync } = await import('node:child_process')
@@ -11,7 +11,7 @@ export async function createBackup({ outFile } = {}) {
11
11
  return { ok: false, stderr: r.stderr, hint: 'tar may be missing on Windows; install GNU tar or use a different archiver.' }
12
12
  }
13
13
  export function listBackups() {
14
- const dir = path.join(getFophHome(), 'backups')
14
+ const dir = path.join(getFreddieHome(), 'backups')
15
15
  if (!fs.existsSync(dir)) return []
16
16
  return fs.readdirSync(dir).filter(f => f.endsWith('.tar.gz')).map(f => ({ name: f, file: path.join(dir, f), mtime: fs.statSync(path.join(dir, f)).mtimeMs }))
17
17
  }
package/src/cli/doctor.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { spawnSync } from 'node:child_process'
4
- import { getFophHome } from '../home.js'
4
+ import { getFreddieHome } from '../home.js'
5
5
  const CHECKS = [
6
- { name: 'freddie-home', run: () => fs.existsSync(getFophHome()) ? { ok: true } : { ok: false, fix: 'mkdir -p ' + getFophHome() } },
6
+ { name: 'freddie-home', run: () => fs.existsSync(getFreddieHome()) ? { ok: true } : { ok: false, fix: 'mkdir -p ' + getFreddieHome() } },
7
7
  { name: 'node-version', run: () => { const v = process.versions.node; const major = Number(v.split('.')[0]); return major >= 20 ? { ok: true, value: v } : { ok: false, fix: 'install node >=20', value: v } } },
8
8
  { name: 'better-sqlite3', run: () => { try { require.resolve('better-sqlite3'); return { ok: true } } catch { return { ok: false, fix: 'npm install' } } } },
9
9
  { name: 'gh-cli', run: () => { const r = spawnSync('gh', ['--version'], { encoding: 'utf8' }); return r.status === 0 ? { ok: true, value: r.stdout.split('\n')[0] } : { ok: false, fix: 'install gh CLI' } } },
10
10
  { name: 'git', run: () => { const r = spawnSync('git', ['--version'], { encoding: 'utf8' }); return r.status === 0 ? { ok: true, value: r.stdout.trim() } : { ok: false, fix: 'install git' } } },
11
- { name: 'config-file', run: () => { const p = path.join(getFophHome(), 'config.yaml'); return fs.existsSync(p) ? { ok: true } : { ok: false, fix: 'freddie setup' } } },
11
+ { name: 'config-file', run: () => { const p = path.join(getFreddieHome(), 'config.yaml'); return fs.existsSync(p) ? { ok: true } : { ok: false, fix: 'freddie setup' } } },
12
12
  ]
13
13
  export function runDoctor() {
14
14
  return CHECKS.map(c => { try { return { name: c.name, ...c.run() } } catch (e) { return { name: c.name, ok: false, error: String(e.message || e) } } })