freddie 0.0.41 → 0.0.42
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.
- package/AGENTS.md +85 -11
- package/CHANGELOG.md +16 -0
- package/README.md +2 -2
- package/bin/freddie.js +12 -109
- package/package.json +11 -2
- package/src/acp/server.js +3 -3
- package/src/acp/session.js +8 -8
- package/src/acp/tools.js +5 -4
- package/src/agent/account_usage.js +5 -5
- package/src/agent/credential_sources.js +2 -2
- package/src/agent/curator.js +5 -5
- package/src/agent/machine.js +3 -2
- package/src/agent/manual_compression_feedback.js +5 -5
- package/src/agent/shell_hooks.js +2 -2
- package/src/auth.js +2 -2
- package/src/batch.js +2 -2
- package/src/cli/backup.js +3 -3
- package/src/cli/doctor.js +3 -3
- package/src/cli/dump.js +4 -2
- package/src/cli/env_loader.js +2 -2
- package/src/cli/gateway_cli.js +3 -4
- package/src/cli/hooks.js +2 -2
- package/src/cli/logs.js +4 -4
- package/src/cli/mcp_config.js +2 -2
- package/src/cli/plugins_cmd.js +3 -3
- package/src/cli/status.js +1 -1
- package/src/cli/tools_config.js +2 -2
- package/src/cli/uninstall.js +2 -2
- package/src/config.js +2 -2
- package/src/db.js +3 -3
- package/src/gateway/platforms.js +21 -0
- package/src/home.js +2 -2
- package/src/host/contract.js +39 -0
- package/src/host/host.js +159 -0
- package/src/host/index.js +27 -0
- package/src/index.js +2 -1
- package/src/mcp/server.js +5 -4
- package/src/observability/log.js +2 -2
- package/src/plugins/disk_cleanup/index.js +2 -2
- package/src/plugins/manager.js +13 -63
- package/src/plugins/memory/provider.js +26 -26
- package/src/plugins/observability/index.js +3 -3
- package/src/skills/index.js +2 -2
- package/src/skin/engine.js +2 -2
- package/src/toolsets.js +13 -15
- package/src/web/index.html +1 -1
- package/src/web/server.js +8 -94
- package/src/gateway/platforms/api_server.js +0 -21
- package/src/gateway/platforms/bluebubbles.js +0 -32
- package/src/gateway/platforms/dingtalk.js +0 -32
- package/src/gateway/platforms/discord.js +0 -24
- package/src/gateway/platforms/email.js +0 -51
- package/src/gateway/platforms/feishu.js +0 -32
- package/src/gateway/platforms/feishu_comment.js +0 -12
- package/src/gateway/platforms/feishu_comment_rules.js +0 -11
- package/src/gateway/platforms/homeassistant.js +0 -32
- package/src/gateway/platforms/matrix.js +0 -40
- package/src/gateway/platforms/mattermost.js +0 -29
- package/src/gateway/platforms/qqbot.js +0 -32
- package/src/gateway/platforms/signal.js +0 -33
- package/src/gateway/platforms/slack.js +0 -34
- package/src/gateway/platforms/sms.js +0 -34
- package/src/gateway/platforms/telegram.js +0 -38
- package/src/gateway/platforms/telegram_network.js +0 -17
- package/src/gateway/platforms/webhook.js +0 -19
- package/src/gateway/platforms/wecom.js +0 -32
- package/src/gateway/platforms/wecom_callback.js +0 -15
- package/src/gateway/platforms/wecom_crypto.js +0 -16
- package/src/gateway/platforms/weixin.js +0 -32
- package/src/gateway/platforms/whatsapp.js +0 -40
- package/src/gateway/platforms/yuanbao.js +0 -9
- package/src/gateway/platforms/yuanbao_media.js +0 -5
- package/src/gateway/platforms/yuanbao_proto.js +0 -9
- package/src/gateway/platforms/yuanbao_sticker.js +0 -6
- package/src/plugins/memory/_index.js +0 -8
- package/src/plugins/memory/byterover.js +0 -25
- package/src/plugins/memory/hindsight.js +0 -25
- package/src/plugins/memory/holographic.js +0 -31
- package/src/plugins/memory/honcho.js +0 -25
- package/src/plugins/memory/mem0.js +0 -25
- package/src/plugins/memory/openviking.js +0 -25
- package/src/plugins/memory/retaindb.js +0 -25
- package/src/plugins/memory/supermemory.js +0 -25
- package/src/tools/ansi_strip.js +0 -8
- package/src/tools/approval.js +0 -15
- package/src/tools/bash.js +0 -35
- package/src/tools/binary_extensions.js +0 -22
- package/src/tools/browser.js +0 -48
- package/src/tools/budget_config.js +0 -13
- package/src/tools/checkpoint.js +0 -29
- package/src/tools/clarify.js +0 -15
- package/src/tools/code_execution.js +0 -27
- package/src/tools/credential_files.js +0 -16
- package/src/tools/cronjob.js +0 -16
- package/src/tools/debug_helpers.js +0 -9
- package/src/tools/delegate.js +0 -28
- package/src/tools/discord_tool.js +0 -13
- package/src/tools/edit.js +0 -31
- package/src/tools/env_passthrough.js +0 -15
- package/src/tools/feishu_doc.js +0 -15
- package/src/tools/feishu_drive.js +0 -14
- package/src/tools/file_operations.js +0 -17
- package/src/tools/file_state.js +0 -16
- package/src/tools/file_tools.js +0 -23
- package/src/tools/fuzzy_match.js +0 -8
- package/src/tools/grep.js +0 -51
- package/src/tools/homeassistant_tool.js +0 -15
- package/src/tools/image_gen.js +0 -33
- package/src/tools/interrupt.js +0 -18
- package/src/tools/managed_tool_gateway.js +0 -11
- package/src/tools/mcp_oauth.js +0 -21
- package/src/tools/mcp_oauth_manager.js +0 -20
- package/src/tools/mcp_tool.js +0 -36
- package/src/tools/memory.js +0 -66
- package/src/tools/mixture_of_agents.js +0 -14
- package/src/tools/neutts_synth.js +0 -13
- package/src/tools/openrouter_client.js +0 -13
- package/src/tools/osv_check.js +0 -11
- package/src/tools/patch_parser.js +0 -42
- package/src/tools/path_security.js +0 -16
- package/src/tools/process_registry.js +0 -17
- package/src/tools/read.js +0 -26
- package/src/tools/registry.js +0 -54
- package/src/tools/rl_training.js +0 -13
- package/src/tools/schema_sanitizer.js +0 -18
- package/src/tools/send_message.js +0 -32
- package/src/tools/session_search.js +0 -23
- package/src/tools/skill_manager.js +0 -17
- package/src/tools/skill_usage.js +0 -20
- package/src/tools/skills_guard.js +0 -17
- package/src/tools/skills_hub.js +0 -31
- package/src/tools/skills_index.js +0 -14
- package/src/tools/skills_sync.js +0 -19
- package/src/tools/skills_tool.js +0 -11
- package/src/tools/slash_confirm.js +0 -16
- package/src/tools/terminal.js +0 -29
- package/src/tools/tirith_security.js +0 -25
- package/src/tools/todo.js +0 -54
- package/src/tools/tool_backend_helpers.js +0 -26
- package/src/tools/tool_output_limits.js +0 -15
- package/src/tools/tool_result_storage.js +0 -20
- package/src/tools/transcription.js +0 -19
- package/src/tools/tts.js +0 -19
- package/src/tools/url_safety.js +0 -15
- package/src/tools/vision.js +0 -18
- package/src/tools/voice_mode.js +0 -10
- package/src/tools/web_search.js +0 -37
- package/src/tools/web_tools.js +0 -18
- package/src/tools/website_policy.js +0 -14
- package/src/tools/write.js +0 -25
- package/src/tools/xai_http.js +0 -13
- 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 #
|
|
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
|
-
|
|
53
|
-
import { registry } from './tools/registry.js'
|
|
82
|
+
Tools are now plugins. Create `plugins/<name>/plugin.js` + `plugins/<name>/handler.js`:
|
|
54
83
|
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 `
|
|
86
|
-
- Always `
|
|
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 #
|
|
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 {
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
.
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.0.42",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
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 {
|
|
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
|
|
63
|
-
return { tools:
|
|
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)
|
package/src/acp/session.js
CHANGED
|
@@ -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 {
|
|
1
|
+
import { bootHost } from '../host/index.js'
|
|
2
2
|
import { Events } from './events.js'
|
|
3
3
|
export async function listToolsForAcp() {
|
|
4
|
-
await
|
|
5
|
-
return
|
|
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
|
|
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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
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(
|
|
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, '') }
|
package/src/agent/curator.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { db } from '../
|
|
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) {
|
|
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 } }
|
package/src/agent/machine.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createMachine, createActor, assign, fromPromise } from 'xstate'
|
|
2
|
-
import {
|
|
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
|
|
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 '../
|
|
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() }
|
package/src/agent/shell_hooks.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
4
|
-
function hookFile() { return path.join(
|
|
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 {
|
|
3
|
+
import { getFreddieHome } from './home.js'
|
|
4
4
|
|
|
5
5
|
class FileAuthStore {
|
|
6
|
-
constructor() { this.dir = path.join(
|
|
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 {
|
|
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(
|
|
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 {
|
|
3
|
+
import { getFreddieHome } from '../home.js'
|
|
4
4
|
export async function createBackup({ outFile } = {}) {
|
|
5
|
-
const home =
|
|
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(
|
|
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 {
|
|
4
|
+
import { getFreddieHome } from '../home.js'
|
|
5
5
|
const CHECKS = [
|
|
6
|
-
{ name: 'freddie-home', run: () => fs.existsSync(
|
|
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(
|
|
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) } } })
|