freddie 0.0.121 → 0.0.122
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 +35 -10
- package/package.json +3 -2
- package/plugins/core-cli/plugin.js +132 -5
- package/plugins/gm-skill/plugin.js +16 -4
- package/plugins/gui-auth/plugin.js +45 -0
- package/plugins/gui-chat/plugin.js +21 -4
- package/plugins/gui-env/plugin.js +14 -1
- package/plugins/gui-sessions/plugin.js +7 -1
- package/plugins/memory/handler.js +23 -46
- package/plugins/platform-discord/handler.js +83 -1
- package/plugins/platform-whatsapp/handler.js +31 -5
- package/src/acp/server.js +1 -1
- package/src/acp/tools.js +5 -1
- package/src/agent/acptoapi-bridge.js +33 -9
- package/src/agent/llm_resolver.js +10 -1
- package/src/agent/machine.js +61 -12
- package/src/agent/tool_call_text.js +68 -0
- package/src/batch.js +4 -3
- package/src/browser/index.js +4 -0
- package/src/cli/cli_output.js +4 -4
- package/src/cli/interactive.js +58 -9
- package/src/cli/memory_setup.js +18 -4
- package/src/cli/relaunch.js +2 -2
- package/src/cli/stdin_secret.js +31 -0
- package/src/context/engine.js +7 -4
- package/src/gateway/run.js +18 -6
- package/src/index.js +1 -0
- package/src/learn/gm-learn.js +179 -0
- package/src/plugins/case/index.js +28 -0
- package/src/plugins/case/toolset.js +312 -0
- package/src/sessions.js +38 -1
- package/src/toolset_distributions.js +3 -0
- package/src/toolsets.js +1 -1
- package/src/web/app.js +55 -12
- package/src/web/index.html +30 -3
- package/src/web/server.js +51 -2
- package/src/web/state.js +74 -32
package/AGENTS.md
CHANGED
|
@@ -52,9 +52,7 @@ Matrix wired: shim passes `matrixSource: process.env.FREDDIE_MATRIX_URL || <repo
|
|
|
52
52
|
|
|
53
53
|
`agent.model_preference: []` in `~/.freddie/config.yaml` is an array of `{ provider, model? }` objects; `resolveCallLLM` tries each in order, skipping unavailable (sampler-gated) and marking failures with backoff.
|
|
54
54
|
|
|
55
|
-
`
|
|
56
|
-
|
|
57
|
-
`src/agent/llm_resolver.js::acpChat()` speaks the kilo ACP protocol: POST `/session` → GET `/event` (SSE) → POST `/session/<id>/message`. Streams `message.part.updated` events to assemble content; terminates on `session.idle`. **`/event` must be opened BEFORE `/message` POST or messages drop.** `ACP_BACKENDS`: kilo on `http://localhost:4780`, opencode on `http://localhost:4790`. kilo + opencode ACP backends return content only, no tool_calls — for multi-iteration tool-using loops, use OpenAI-compatible providers (mistral, openrouter, sambanova, groq).
|
|
55
|
+
ACP protocol detail (`acpChat`, kilo/opencode backends, `/event`-before-`/message`, max_tokens 4096 floor) — see rs-learn (recall "Freddie ACP protocol detail").
|
|
58
56
|
|
|
59
57
|
## Plugin architecture
|
|
60
58
|
|
|
@@ -81,6 +79,17 @@ Thin shims (resolved through host, do not bypass): `src/plugins/manager.js`, `sr
|
|
|
81
79
|
|
|
82
80
|
`plugins/gm-skill/plugin.js` registers ONE canonical skill named `gm-skill`. Resolution order: (1) `~/.claude/skills/gm-skill/SKILL.md`, (2) `node_modules/gm-cc/skills/gm-skill/SKILL.md`. All other `gm-*` platform variants (gm-cc, gm-codex, gm-cursor, gm-jetbrains, gm-kilo, gm-oc, gm-vscode, gm-zed, gm-gc, gm-copilot-cli) are DEPRECATED — do not register them. `src/host/host_helpers.js::loadCcFromNodeModules` carries `CC_EXCLUDE = new Set(['gm-cc'])` so the gm-cc npm package is not auto-discovered as a cc-plugin. test.js asserts exactly one gm-prefixed skill is registered, named `gm-skill`.
|
|
83
81
|
|
|
82
|
+
## Learning: gm rs-learn is THE memory mechanism
|
|
83
|
+
|
|
84
|
+
freddie learns through **gm rs-learn**, in-process, via `src/learn/gm-learn.js`. This is the single canonical learning store; the local-SQLite store is gone and the third-party providers (`plugins/memory-*/`) are legacy opt-in only.
|
|
85
|
+
|
|
86
|
+
- `src/learn/gm-learn.js` lazy-loads gm-plugkit's wasm via the ESM `createPlugkit()` export (`gm-plugkit/plugkit-wasm-wrapper.js`), caches one instance process-wide, and exposes `memorize`/`recall`/`autoRecall`/`prune`/`projectNamespace`. Every call degrades to a no-op (never throws into a turn) when gm/wasm is absent. First call cold-loads the wasm + BAAI/bge-small-en-v1.5 embed model, so it is lazy off the hot path. The wasm resolves `.gm/rs-learn.db` from process cwd; namespace is per active project (`projectNamespace()`).
|
|
87
|
+
- **The learning loop (workflow):** every turn (`src/agent/machine.js`) auto-recalls salient memories for the prompt on entry (injected as a "Relevant memories (gm rs-learn)" system part) and auto-learns a deduped `Q:..A:..` salient fact on substantive, non-error completion (`autoLearnTurn`, dedupe cos>=0.92, min len 40). `src/context/engine.js` `ContextPlugins.memory` does query-aware recall over the same store.
|
|
88
|
+
- **The `memory` tool** (`plugins/memory/handler.js`) is the explicit manual surface over the same store: `add`->memorize, `search`->recall (score-ranked), `list`->broad recall, `forget`->prune by explicit key.
|
|
89
|
+
- **gm-plugkit in-process API:** `createPlugkit()` is consumed by importing the wrapper file directly (`index.js` is CJS; the export lives on the ESM wrapper). The wrapper's CLI IIFE is guarded by `_isCliEntry` so importing it does not start the daemon.
|
|
90
|
+
- Legacy migration: `node scripts/migrate-memory-to-gm.mjs [namespace]` drains old `memory_local` rows into rs-learn. `src/cli/memory_setup.js` defaults `memory.provider='gm'` (no key/config); third-party providers stay behind explicit `configureProvider`.
|
|
91
|
+
- **Browser / gh-pages path:** `src/learn/gm-learn.js` is environment-aware. Where `node:module` is unavailable (e.g. thebird on gh-pages) it skips the `createPlugkit()` import and instead routes memorize-fire/recall/auto-recall/prune through a host bridge: `globalThis.__GM_DISPATCH__(verb, body) -> json|Promise<json>` (the host's already-loaded in-page plugkit.wasm) and `globalThis.__GM_NAMESPACE__` (string or fn -> active-workspace namespace). gm-learn probes both lazily each call (so a cold-loading wasm is picked up once ready) and degrades to no-op when the bridge is absent. This makes freddie LEARN in-browser with no node deps.
|
|
92
|
+
|
|
84
93
|
## Multi-project workspace
|
|
85
94
|
|
|
86
95
|
Freddie supports multiple isolated projects, each with its own home directory and plugin set. Registry at `~/.freddie/projects.json` stores `{ active, projects: [{name, path, created_at}] }`. Default project (`~/.freddie`) is protected from deletion.
|
|
@@ -102,7 +111,12 @@ All web UI for freddie + thebird lives in `anentrypoint-design`. Consumers must
|
|
|
102
111
|
- **thebird** consumes the same SDK. Bespoke windowing (`wm.js`, `launcher.js`, `shell.js`) and any context-menu / theme-toggle DOM should migrate into the SDK as reusable kits; do not extend them in thebird.
|
|
103
112
|
- Theme toggle: SDK owns the controller. Consumers import it; they do NOT reimplement localStorage + `prefers-color-scheme` listeners.
|
|
104
113
|
|
|
105
|
-
Build: `node scripts/build.mjs` in `C:/dev/anentrypoint-design` emits `dist/247420.js` + `dist/247420.css`. Rebuild after SDK edits or `component is not a function` kills mount silently. `server.js` serves SDK from `node_modules/anentrypoint-design/dist/`.
|
|
114
|
+
Build: `node scripts/build.mjs` in `C:/dev/anentrypoint-design` emits `dist/247420.js` + `dist/247420.css`. Rebuild after SDK edits or `component is not a function` kills mount silently. `server.js` serves SDK from `node_modules/anentrypoint-design/dist/`. To witness a local SDK edit, rebuild then `cp dist/247420.{js,css}` into freddie's `node_modules/anentrypoint-design/dist/` (server aliases `sdk.js`/`sdk.css` → `247420.*`). SPA routes are `#fd-<page>` (e.g. `#fd-env`), not `#/<page>` — navigate by clicking the nav link when browser-witnessing.
|
|
115
|
+
|
|
116
|
+
GUI key/path/conversation endpoints (freddie-owned `plugins/gui-*`, consumed by the SDK pages):
|
|
117
|
+
- **Keys** (`plugins/gui-auth`): `GET /api/auth` (per-provider env|stored|none + masked `fingerprint`, never the raw value), `POST /api/auth {provider,key}` (stores via auth store), `DELETE /api/auth/:provider`. The SDK `env` page (labelled "keys") renders a masked-input + save/remove per provider. `GET /api/env` (`plugins/gui-env`) now reports auth-store keys too, not just `process.env`.
|
|
118
|
+
- **Conversations** (`plugins/gui-sessions`): `GET /api/sessions/:id` (single), `DELETE /api/sessions/:id` (purges messages + FTS), alongside the existing list/messages/search.
|
|
119
|
+
- **Paths** (`plugins/gui-projects`): full CRUD already (`GET/POST/DELETE /api/projects`, `POST /api/projects/active`).
|
|
106
120
|
|
|
107
121
|
Theme attribute scoping: `class="ds-247420"` on `<html>`, `data-theme="dark|light"` on `<body>`. Putting both on the same node breaks the descendant selector and themes do not switch.
|
|
108
122
|
|
|
@@ -149,7 +163,8 @@ src/host/{contract,host,host_helpers,index}.js # plugin contract + discovery +
|
|
|
149
163
|
plugins/<name>/{plugin,handler}.js # ~150 plugins: tools, platforms, memory, gui, core
|
|
150
164
|
skills/ # bundled skill bundles (creative/, software-development/, ops/, data/, planning/)
|
|
151
165
|
website/ # flatspace docs site: flatspace.config.mjs + theme.mjs + content/pages/*.yaml
|
|
152
|
-
bin/freddie.js # commander CLI: tools, skills, profile, skin, sessions, search, gateway, acp, run, cron, batch, dashboard, help-all
|
|
166
|
+
bin/freddie.js # commander CLI: tools, skills, profile, skin, sessions, search, gateway, acp, run, cron, batch, dashboard, help-all + user-facing key/path/conversation verbs: auth, project, session, doctor, setup
|
|
167
|
+
src/cli/stdin_secret.js # readStdinSecret — masked/piped key entry (never argv) for `auth set`
|
|
153
168
|
```
|
|
154
169
|
|
|
155
170
|
## Adding a tool
|
|
@@ -211,6 +226,17 @@ export default {
|
|
|
211
226
|
|
|
212
227
|
`makePlatform('myname', opts)` in `src/gateway/platforms.js` instantiates the adapter via `*Adapter$` name match.
|
|
213
228
|
|
|
229
|
+
## User-facing CLI: keys, paths, conversations
|
|
230
|
+
|
|
231
|
+
The first-run user surface lives in `plugins/core-cli/plugin.js` (registered via `pi.cli`). Keep these terse and friendly — they are the zero-to-first-conversation path, so errors print one line, never a stack:
|
|
232
|
+
|
|
233
|
+
- **Keys** — `freddie auth list|set <provider>|rm <provider>|test [provider]|show`. `set` reads the key from stdin/masked-TTY via `src/cli/stdin_secret.js` (never argv — argv leaks to shell history/`ps`); stores through `src/auth.js` `getAuthStore()`. `list` shows env var + `[set]/[--]` + source `(env|stored|none)`. Unknown provider prints the valid list (`isKnownAuthProvider` guard), never a silent no-op. `test` reuses acptoapi `isAvailable` — does NOT reimplement provider HTTP.
|
|
234
|
+
- **Paths/workspaces** — `freddie project list|create <name> <path>|use <name>|rm <name>|current` over `src/projects.js`. `list` marks the active project `[*]` and shows its home path. `rm default` surfaces the projects.js guard as a friendly error. Mirrors the `gui-projects` HTTP CRUD.
|
|
235
|
+
- **Conversations** — `freddie session list|show <id>|rm <id>` over `src/sessions.js`. `session list` shows the auto-derived title (first user prompt). `freddie run --resume [id]` continues the most-recent (or matched) conversation: `src/cli/interactive.js` loads prior `getMessages` into `state.messages`. The REPL also has `/sessions`, `/resume <id>`, `/keys`, `/project` slash commands.
|
|
236
|
+
- **Onboarding** — `freddie doctor` (one-glance health: env checks via `src/cli/doctor.js` `runDoctor()` + provider keys + active project/home + saved-conversation count) and `freddie setup` (guided first-run via `src/cli/setup.js` `setupWizard`). Reuse these modules — do not reimplement.
|
|
237
|
+
|
|
238
|
+
`src/sessions.js` exposes `getSession(id)`, `deleteSession(id)` (purges messages + rebuilds the external-content `messages_fts` index), `setSessionTitle`, and auto-derives a title from the first user prompt in `appendMessage`. **All session calls are async (libsql) — every callsite must `await`** (a bare call silently rejects and the conversation is never persisted; this was the REPL history-loss bug).
|
|
239
|
+
|
|
214
240
|
## Profile-safe code
|
|
215
241
|
|
|
216
242
|
- Always `getFreddieHome()` for state paths. Never `path.join(os.homedir(), '.freddie')`.
|
|
@@ -276,8 +302,7 @@ On Windows, test.js must call `closeDb()` and log-stream `closeAll()` before exi
|
|
|
276
302
|
|
|
277
303
|
## Cross-project Rust gotchas
|
|
278
304
|
|
|
279
|
-
|
|
280
|
-
- **rs-exec timeout alias**: both `--timeout` (long-form) and `--timeout-ms` (plugin convention) are accepted on `Cmd::Exec` and `Cmd::Bash`.
|
|
305
|
+
rs-plugkit exec utility verbs + rs-exec timeout aliases — see rs-learn (recall "Freddie cross-project Rust gotchas").
|
|
281
306
|
|
|
282
307
|
## Plugsdk integration
|
|
283
308
|
|
|
@@ -287,9 +312,7 @@ On Windows, test.js must call `closeDb()` and log-stream `closeAll()` before exi
|
|
|
287
312
|
|
|
288
313
|
## opencode CLI shim
|
|
289
314
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
Start ACP daemon: `& 'C:\Users\user\AppData\Roaming\npm\opencode.cmd' serve --port 4790 --hostname 127.0.0.1`. Verify via `GET http://127.0.0.1:4790/` returning 200. `OPENCODE_SERVER_PASSWORD is not set` warning is harmless for localhost.
|
|
315
|
+
Windows: use the npm install (`opencode.cmd`), not the broken bun shim; ACP daemon on 4790 — see rs-learn (recall "Freddie opencode CLI shim Windows").
|
|
293
316
|
|
|
294
317
|
## scripts/sync-upstream.mjs
|
|
295
318
|
|
|
@@ -358,3 +381,5 @@ Sampler integration: agent-loop failures feed acptoapi's per-provider backoff (5
|
|
|
358
381
|
- `website/theme.mjs` 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. Prefer enriching hero+sections+examples over expanding body markdown.
|
|
359
382
|
- **YAML colon-space trap**: in `website/content/pages/*.yaml`, any value containing `: ` outside backticks (e.g. `[linux, macos, windows]`, `requiresEnv: ['MY_KEY']` snippets) MUST be double-quoted. The parser otherwise interprets the embedded colon as a mapping and the file fails to load.
|
|
360
383
|
- SSR innerHTML injection beats client dispatch for site pages — emit HTML with rail/dot/chip/btn classes + inline styles so it paints before the SDK loads.
|
|
384
|
+
|
|
385
|
+
@.gm/next-step.md
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freddie",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.122",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Open JS agent harness built on pi-mono, floosie, xstate, and anentrypoint-design",
|
|
6
6
|
"bin": {
|
|
@@ -28,11 +28,12 @@
|
|
|
28
28
|
"@mariozechner/pi-coding-agent": "^0.70.6",
|
|
29
29
|
"@mariozechner/pi-tui": "^0.70.6",
|
|
30
30
|
"acptoapi": "^1.0.115",
|
|
31
|
-
"anentrypoint-design": "
|
|
31
|
+
"anentrypoint-design": "latest",
|
|
32
32
|
"commander": "^14.0.0",
|
|
33
33
|
"express": "^5.0.0",
|
|
34
34
|
"flatspace": "^1.0.18",
|
|
35
35
|
"floosie": "^0.6.14",
|
|
36
|
+
"gm-plugkit": "^2.0.1535",
|
|
36
37
|
"gm-skill": "latest",
|
|
37
38
|
"js-yaml": "^4.1.0",
|
|
38
39
|
"libsql-plugkit-client": "^0.0.10",
|
|
@@ -5,7 +5,11 @@ import { makePlatform } from '../../src/gateway/platforms.js'
|
|
|
5
5
|
import { AcpServer } from '../../src/acp/server.js'
|
|
6
6
|
import { COMMANDS_BY_CATEGORY } from '../../src/commands/registry.js'
|
|
7
7
|
import { getActiveSkin, listBuiltinSkins, setActiveSkin } from '../../src/skin/engine.js'
|
|
8
|
-
import { listSessions, search } from '../../src/sessions.js'
|
|
8
|
+
import { listSessions, getSession, getMessages, deleteSession, search } from '../../src/sessions.js'
|
|
9
|
+
import { listAuthProviders, isKnownAuthProvider, envForProvider, hasUsableSecret, getAuthStore, clearProviderAuth, tokenFingerprint } from '../../src/auth.js'
|
|
10
|
+
import { listProjects, getActiveProject, createProject, deleteProject, setActiveProject } from '../../src/projects.js'
|
|
11
|
+
import { displayFreddieHome, getFreddieHome } from '../../src/home.js'
|
|
12
|
+
import { readStdinSecret } from '../../src/cli/stdin_secret.js'
|
|
9
13
|
|
|
10
14
|
export default {
|
|
11
15
|
name: 'core-cli', surfaces: 'pi',
|
|
@@ -50,11 +54,13 @@ export default {
|
|
|
50
54
|
for (const c of cmds) console.log(` /${c.name}${c.args_hint ? ' ' + c.args_hint : ''}\t${c.description}`)
|
|
51
55
|
}
|
|
52
56
|
} })
|
|
53
|
-
C({ name: 'run', description: 'Interactive REPL', action: async () => {
|
|
57
|
+
C({ name: 'run', description: 'Interactive REPL (--resume [id] continues a past conversation)', options: [{ flag: '--resume [id]', default: '' }], action: async (opts) => {
|
|
54
58
|
const { interactive } = await import('../../src/cli/interactive.js')
|
|
55
59
|
let callLLM = null
|
|
56
60
|
try { ({ callLLM } = await import('../../src/agent/pi-bridge.js')) } catch {}
|
|
57
|
-
|
|
61
|
+
// --resume with no value = continue the most recent session; --resume <id> = that one.
|
|
62
|
+
const resume = opts.resume === true ? true : (opts.resume || null)
|
|
63
|
+
await interactive({ callLLM, resume })
|
|
58
64
|
} })
|
|
59
65
|
C({ name: 'exec', description: 'Run a single prompt through the agent and exit', options: [{ flag: '--prompt <prompt>', required: true }, { flag: '--model <model>', default: '' }, { flag: '--provider <provider>', default: '' }, { flag: '--skill <skill>', default: '' }, { flag: '--cwd <cwd>', default: '' }, { flag: '--timeout <ms>', default: '60000' }, { flag: '--witness <path>', default: '' }], action: async (opts) => {
|
|
60
66
|
const { runTurn } = await import('../../src/agent/machine.js')
|
|
@@ -93,8 +99,8 @@ export default {
|
|
|
93
99
|
if (action === 'providers') { for (const p of listKnownProviders()) console.log(p); return }
|
|
94
100
|
const result = await discoverAndPersist({ provider })
|
|
95
101
|
for (const [p, r] of Object.entries(result)) {
|
|
96
|
-
if (r.error) console.log(`${p.padEnd(12)}
|
|
97
|
-
else console.log(`${p.padEnd(12)}
|
|
102
|
+
if (r.error) console.log(`${p.padEnd(12)} [fail] ${r.error}`)
|
|
103
|
+
else console.log(`${p.padEnd(12)} [ok] ${r.models.length} models - ${r.models.slice(0, 5).join(', ')}${r.models.length > 5 ? ', ...' : ''}`)
|
|
98
104
|
}
|
|
99
105
|
} })
|
|
100
106
|
C({ name: 'dashboard', description: 'Boot web dashboard', options: [{ flag: '--port <port>', default: '0' }, { flag: '--cwd <dir>', default: '' }], action: async (opts) => {
|
|
@@ -104,5 +110,126 @@ export default {
|
|
|
104
110
|
console.log('dashboard:', d.url)
|
|
105
111
|
process.on('SIGINT', async () => { await d.stop(); process.exit(0) })
|
|
106
112
|
} })
|
|
113
|
+
|
|
114
|
+
// --- Key management: `freddie auth list|set|rm|test|show` ---------------
|
|
115
|
+
C({ name: 'auth', description: 'Manage provider API keys (list|set <provider>|rm <provider>|test [provider]|show)', args: [{ name: 'action', default: 'list' }, { name: 'provider' }], action: async (action, provider) => {
|
|
116
|
+
const known = (p) => { if (!isKnownAuthProvider(p)) { console.error(`unknown provider: ${p}\nknown: ${listAuthProviders().join(', ')}`); process.exit(1) } }
|
|
117
|
+
if (action === 'list' || action === 'show') {
|
|
118
|
+
for (const p of listAuthProviders()) {
|
|
119
|
+
const env = envForProvider(p) || ''
|
|
120
|
+
const inEnv = !!(env && process.env[env])
|
|
121
|
+
const stored = !inEnv && !!(await getAuthStore().getCredential(env))
|
|
122
|
+
const src = inEnv ? 'env' : (stored ? 'stored' : 'none')
|
|
123
|
+
console.log(`${p.padEnd(12)} ${env.padEnd(22)} ${(await hasUsableSecret(p)) ? '[set]' : '[--]'} (${src})`)
|
|
124
|
+
}
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
if (action === 'set') {
|
|
128
|
+
known(provider)
|
|
129
|
+
const env = envForProvider(provider)
|
|
130
|
+
const key = await readStdinSecret(`${env} (key, hidden): `)
|
|
131
|
+
if (!key) { console.error('no key provided'); process.exit(1) }
|
|
132
|
+
await getAuthStore().setCredential(env, key)
|
|
133
|
+
console.log(`stored ${env} (${tokenFingerprint(key)})`)
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
if (action === 'rm') { known(provider); await clearProviderAuth(provider); console.log(`removed key for ${provider}`); return }
|
|
137
|
+
if (action === 'test') {
|
|
138
|
+
const sdk = await import('acptoapi').catch(() => null)
|
|
139
|
+
const targets = provider ? [provider] : listAuthProviders()
|
|
140
|
+
for (const p of targets) {
|
|
141
|
+
if (provider) known(p)
|
|
142
|
+
const has = await hasUsableSecret(p)
|
|
143
|
+
if (!has) { console.log(`${p.padEnd(12)} [--] no key`); continue }
|
|
144
|
+
let reachable = true
|
|
145
|
+
try { if (sdk?.isAvailable) reachable = sdk.isAvailable(p) } catch {}
|
|
146
|
+
console.log(`${p.padEnd(12)} ${reachable ? '[ok]' : '[backoff]'} key present`)
|
|
147
|
+
}
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
console.error('usage: freddie auth [list|set <provider>|rm <provider>|test [provider]|show]'); process.exit(1)
|
|
151
|
+
} })
|
|
152
|
+
|
|
153
|
+
// --- Path/workspace management: `freddie project list|create|use|rm|current` ---
|
|
154
|
+
C({ name: 'project', description: 'Manage workspace projects (list|create <name> <path>|use <name>|rm <name>|current)', args: [{ name: 'action', default: 'list' }, { name: 'name' }, { name: 'projectPath' }], action: async (action, name, projectPath) => {
|
|
155
|
+
if (action === 'list') {
|
|
156
|
+
const active = getActiveProject()
|
|
157
|
+
for (const p of listProjects()) console.log(`${p.name === active.name ? '[*]' : '[ ]'} ${p.name.padEnd(16)} ${p.path}\t${(p.created_at || '').slice(0, 10)}`)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
if (action === 'current') { const a = getActiveProject(); console.log(`${a.name}\t${a.path}`); return }
|
|
161
|
+
if (action === 'create') {
|
|
162
|
+
if (!name || !projectPath) { console.error('usage: freddie project create <name> <absolute-path>'); process.exit(1) }
|
|
163
|
+
try { const p = createProject({ name, projectPath }); console.log(`created project ${p.name} -> ${p.path}`) }
|
|
164
|
+
catch (e) { console.error('error:', e.message); process.exit(1) }
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
if (action === 'use') {
|
|
168
|
+
if (!name) { console.error('usage: freddie project use <name>'); process.exit(1) }
|
|
169
|
+
try { const p = setActiveProject(name); console.log(`active project: ${p.name} (${p.path})`) }
|
|
170
|
+
catch (e) { console.error('error:', e.message); process.exit(1) }
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
if (action === 'rm') {
|
|
174
|
+
if (!name) { console.error('usage: freddie project rm <name>'); process.exit(1) }
|
|
175
|
+
try { deleteProject(name); console.log(`removed project ${name}`) }
|
|
176
|
+
catch (e) { console.error('error:', e.message); process.exit(1) }
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
console.error('usage: freddie project [list|create <name> <path>|use <name>|rm <name>|current]'); process.exit(1)
|
|
180
|
+
} })
|
|
181
|
+
|
|
182
|
+
// --- Conversation management: `freddie session list|show|rm` -----------
|
|
183
|
+
C({ name: 'session', description: 'Manage conversations (list|show <id>|rm <id>)', args: [{ name: 'action', default: 'list' }, { name: 'id' }], action: async (action, id) => {
|
|
184
|
+
if (action === 'list') {
|
|
185
|
+
const rows = await listSessions()
|
|
186
|
+
if (!rows.length) { console.log('(no conversations yet — run `freddie run`)'); return }
|
|
187
|
+
for (const s of rows) console.log(`${s.id.slice(0, 8)}\t${new Date(s.updated_at).toISOString().slice(0, 16).replace('T', ' ')}\t${s.title || '(untitled)'}`)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
if (action === 'show') {
|
|
191
|
+
if (!id) { console.error('usage: freddie session show <id>'); process.exit(1) }
|
|
192
|
+
const rows = await listSessions(500)
|
|
193
|
+
const target = rows.find(s => s.id === id || s.id.startsWith(id))
|
|
194
|
+
if (!target) { console.error('no session matching:', id); process.exit(1) }
|
|
195
|
+
const s = await getSession(target.id)
|
|
196
|
+
console.log(`# ${s.title || '(untitled)'} [${s.id.slice(0, 8)}] ${s.model || ''} ${new Date(s.created_at).toISOString().slice(0, 16).replace('T', ' ')}`)
|
|
197
|
+
for (const m of await getMessages(target.id)) console.log(`\n${m.role}: ${m.content || (m.tool_calls ? '[tool call]' : '')}`)
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
if (action === 'rm') {
|
|
201
|
+
if (!id) { console.error('usage: freddie session rm <id>'); process.exit(1) }
|
|
202
|
+
const rows = await listSessions(500)
|
|
203
|
+
const target = rows.find(s => s.id === id || s.id.startsWith(id))
|
|
204
|
+
if (!target) { console.error('no session matching:', id); process.exit(1) }
|
|
205
|
+
await deleteSession(target.id); console.log(`removed session ${target.id.slice(0, 8)}`)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
console.error('usage: freddie session [list|show <id>|rm <id>]'); process.exit(1)
|
|
209
|
+
} })
|
|
210
|
+
|
|
211
|
+
// --- Onboarding: `freddie doctor` one-glance health --------------------
|
|
212
|
+
C({ name: 'doctor', description: 'Health check: keys, active project, conversations, environment', action: async () => {
|
|
213
|
+
const { runDoctor } = await import('../../src/cli/doctor.js')
|
|
214
|
+
console.log('# environment')
|
|
215
|
+
for (const c of runDoctor()) console.log(` ${c.ok ? '[ok]' : '[--]'} ${c.name.padEnd(16)} ${c.value || c.fix || ''}`)
|
|
216
|
+
console.log('\n# provider keys')
|
|
217
|
+
let anyKey = false
|
|
218
|
+
for (const p of listAuthProviders()) { const ok = await hasUsableSecret(p); if (ok) anyKey = true; if (ok) console.log(` [ok] ${p}`) }
|
|
219
|
+
if (!anyKey) console.log(' [--] no provider keys set — run `freddie auth set <provider>` or `freddie setup`')
|
|
220
|
+
const proj = getActiveProject()
|
|
221
|
+
console.log(`\n# workspace\n active project: ${proj.name}\n home: ${displayFreddieHome()} (${getFreddieHome()})`)
|
|
222
|
+
const sessions = await listSessions(500)
|
|
223
|
+
console.log(`\n# conversations\n ${sessions.length} saved (latest: ${sessions[0] ? (sessions[0].title || sessions[0].id.slice(0, 8)) : 'none'})`)
|
|
224
|
+
} })
|
|
225
|
+
|
|
226
|
+
// --- Onboarding: `freddie setup` guided first-run ----------------------
|
|
227
|
+
C({ name: 'setup', description: 'Guided first-run: pick provider, store a key, configure defaults', action: async () => {
|
|
228
|
+
const { setupWizard, getSetupStatus } = await import('../../src/cli/setup.js')
|
|
229
|
+
await setupWizard({})
|
|
230
|
+
const st = getSetupStatus()
|
|
231
|
+
console.log(`\nsetup complete — provider: ${st.provider}, skin: ${st.skin}`)
|
|
232
|
+
console.log('next: `freddie run` to start a conversation, or `freddie doctor` to verify')
|
|
233
|
+
} })
|
|
107
234
|
},
|
|
108
235
|
}
|
|
@@ -39,11 +39,23 @@ function parseFrontmatter(md) {
|
|
|
39
39
|
export default {
|
|
40
40
|
name: 'gm-skill',
|
|
41
41
|
surfaces: 'pi',
|
|
42
|
-
register({ pi }) {
|
|
42
|
+
register({ pi, log }) {
|
|
43
|
+
const warn = (msg, data) => { try { (log && log.warn) ? log.warn(msg, data) : console.warn(`[gm-skill] ${msg}`, data || '') } catch {} }
|
|
43
44
|
const skillPath = resolveSkillMd()
|
|
44
|
-
if (!skillPath)
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
if (!skillPath) {
|
|
46
|
+
warn('SKILL.md unresolvable; gm-skill not registered. Run `bun x gm-plugkit@latest spool` to provision it, or install the gm-skill package.', {
|
|
47
|
+
searched: ['~/.agents/skills/gm-skill/SKILL.md', '~/.claude/skills/gm-skill/SKILL.md', 'node_modules: gm-skill, gm-cc'],
|
|
48
|
+
})
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
let raw, fields, body
|
|
52
|
+
try {
|
|
53
|
+
raw = fs.readFileSync(skillPath, 'utf8')
|
|
54
|
+
;({ fields, body } = parseFrontmatter(raw))
|
|
55
|
+
} catch (e) {
|
|
56
|
+
warn('failed reading/parsing SKILL.md; gm-skill not registered', { file: skillPath, error: e && e.message })
|
|
57
|
+
return
|
|
58
|
+
}
|
|
47
59
|
pi.skills.register({
|
|
48
60
|
name: 'gm-skill',
|
|
49
61
|
description: fields.description || 'AI-native software engineering harness',
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { listAuthProviders, isKnownAuthProvider, envForProvider, hasUsableSecret, getAuthStore, clearProviderAuth, tokenFingerprint } from '../../src/auth.js'
|
|
2
|
+
|
|
3
|
+
// Dashboard key management. Mirrors the `freddie auth` CLI verbs so users can
|
|
4
|
+
// set/inspect/remove provider API keys from the web UI without env vars or a
|
|
5
|
+
// restart. GET never returns raw secret values — only presence, source, and a
|
|
6
|
+
// masked fingerprint.
|
|
7
|
+
async function providerState(p) {
|
|
8
|
+
const env = envForProvider(p) || ''
|
|
9
|
+
const inEnv = !!(env && process.env[env])
|
|
10
|
+
const stored = inEnv ? null : await getAuthStore().getCredential(env)
|
|
11
|
+
const value = inEnv ? process.env[env] : (stored?.value || '')
|
|
12
|
+
return {
|
|
13
|
+
provider: p,
|
|
14
|
+
env,
|
|
15
|
+
set: await hasUsableSecret(p),
|
|
16
|
+
source: inEnv ? 'env' : (stored ? 'stored' : 'none'),
|
|
17
|
+
fingerprint: value ? tokenFingerprint(value) : '',
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
name: 'gui-auth',
|
|
23
|
+
surfaces: 'gui',
|
|
24
|
+
register({ gui }) {
|
|
25
|
+
gui.route('GET', '/api/auth', async (_, res) => {
|
|
26
|
+
const rows = []
|
|
27
|
+
for (const p of listAuthProviders()) rows.push(await providerState(p))
|
|
28
|
+
res.json(rows)
|
|
29
|
+
})
|
|
30
|
+
gui.route('POST', '/api/auth', async (req, res) => {
|
|
31
|
+
const { provider, key } = req.body || {}
|
|
32
|
+
if (!isKnownAuthProvider(provider)) return res.status(400).json({ error: 'unknown provider', known: listAuthProviders() })
|
|
33
|
+
if (!key || typeof key !== 'string' || !key.trim()) return res.status(400).json({ error: 'key required' })
|
|
34
|
+
const env = envForProvider(provider)
|
|
35
|
+
await getAuthStore().setCredential(env, key.trim())
|
|
36
|
+
res.json(await providerState(provider))
|
|
37
|
+
})
|
|
38
|
+
gui.route('DELETE', '/api/auth/:provider', async (req, res) => {
|
|
39
|
+
const provider = req.params.provider
|
|
40
|
+
if (!isKnownAuthProvider(provider)) return res.status(400).json({ error: 'unknown provider', known: listAuthProviders() })
|
|
41
|
+
await clearProviderAuth(provider)
|
|
42
|
+
res.json(await providerState(provider))
|
|
43
|
+
})
|
|
44
|
+
},
|
|
45
|
+
}
|
|
@@ -6,16 +6,33 @@ export default {
|
|
|
6
6
|
gui.route('POST', '/api/chat', async (req, res) => {
|
|
7
7
|
const { prompt, sessionId: incomingSessionId = null, cwd, skill, provider, model } = req.body || {}
|
|
8
8
|
if (!prompt) return res.status(400).json({ error: 'prompt required' })
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
// Content negotiation: the SDK dashboard chat client does a plain
|
|
10
|
+
// fetch().json() and reads `.result`/`.messages` — it is NOT an
|
|
11
|
+
// EventSource consumer. Default to a single JSON response so that
|
|
12
|
+
// path works; stream SSE only when the caller explicitly asks via
|
|
13
|
+
// `Accept: text/event-stream` (curl / EventSource clients).
|
|
14
|
+
const wantsSse = String(req.headers.accept || '').includes('text/event-stream')
|
|
13
15
|
let sessionId = incomingSessionId
|
|
14
16
|
if (!sessionId) {
|
|
15
17
|
try {
|
|
16
18
|
sessionId = await createSession({ platform: 'web', title: prompt.slice(0, 80), cwd: cwd || null, skill: skill || null, model: model || null })
|
|
17
19
|
} catch (_) { sessionId = null }
|
|
18
20
|
}
|
|
21
|
+
|
|
22
|
+
if (!wantsSse) {
|
|
23
|
+
try {
|
|
24
|
+
const out = await runTurn({ prompt, timeoutMs: 120000, cwd, skill, provider, model })
|
|
25
|
+
if (out.error) return res.status(500).json({ error: out.error, sessionId })
|
|
26
|
+
return res.json({ result: out.result || '', messages: out.messages || [], iterations: out.iterations, sessionId })
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return res.status(500).json({ error: String(e.message || e), sessionId })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
res.setHeader('Content-Type', 'text/event-stream')
|
|
33
|
+
res.setHeader('Cache-Control', 'no-cache')
|
|
34
|
+
res.setHeader('Connection', 'keep-alive')
|
|
35
|
+
const send = (event, data) => res.write('event: ' + event + '\ndata: ' + JSON.stringify(data) + '\n\n')
|
|
19
36
|
send('start', { ts: Date.now(), sessionId })
|
|
20
37
|
try {
|
|
21
38
|
const out = await runTurn({ prompt, timeoutMs: 120000, cwd, skill, provider, model })
|
|
@@ -1,7 +1,20 @@
|
|
|
1
|
+
import { getAuthStore } from '../../src/auth.js'
|
|
1
2
|
const ENV_KEYS = ['ANTHROPIC_API_KEY','OPENAI_API_KEY','GROQ_API_KEY','OPENROUTER_API_KEY','TELEGRAM_BOT_TOKEN','DISCORD_BOT_TOKEN','SLACK_BOT_TOKEN','SLACK_SIGNING_SECRET','WHATSAPP_API_TOKEN','SIGNAL_CLI_URL','MATRIX_HOMESERVER','MATTERMOST_URL','HONCHO_API_KEY','MEM0_API_KEY','SUPERMEMORY_API_KEY','BYTEROVER_API_KEY','HINDSIGHT_API_KEY','OPENVIKING_API_KEY','RETAINDB_API_KEY','SERPAPI_KEY','REPLICATE_API_TOKEN','SMTP_HOST','TWILIO_SID','HASS_TOKEN']
|
|
2
3
|
export default {
|
|
3
4
|
name: 'gui-env', surfaces: 'gui',
|
|
4
5
|
register({ gui }) {
|
|
5
|
-
|
|
6
|
+
// A key set via `freddie auth set` (or the dashboard) lives in the auth
|
|
7
|
+
// store, not process.env. Report it as set with its source so the env
|
|
8
|
+
// page reflects the real key state, not just the shell environment.
|
|
9
|
+
gui.route('GET', '/api/env', async (_, res) => {
|
|
10
|
+
const store = getAuthStore()
|
|
11
|
+
const out = []
|
|
12
|
+
for (const k of ENV_KEYS) {
|
|
13
|
+
if (process.env[k]) { out.push({ key: k, set: true, source: 'env' }); continue }
|
|
14
|
+
const cred = await store.getCredential(k)
|
|
15
|
+
out.push({ key: k, set: !!cred?.value, source: cred?.value ? 'stored' : 'none' })
|
|
16
|
+
}
|
|
17
|
+
res.json(out)
|
|
18
|
+
})
|
|
6
19
|
},
|
|
7
20
|
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { listSessions, search, getMessages } from '../../src/sessions.js'
|
|
1
|
+
import { listSessions, search, getMessages, getSession, deleteSession } from '../../src/sessions.js'
|
|
2
2
|
export default {
|
|
3
3
|
name: 'gui-sessions', surfaces: 'gui',
|
|
4
4
|
register({ gui }) {
|
|
5
5
|
gui.route('GET', '/api/sessions', async (_, res) => res.json(await listSessions()))
|
|
6
|
+
gui.route('GET', '/api/sessions/:id', async (req, res) => {
|
|
7
|
+
const s = await getSession(req.params.id)
|
|
8
|
+
if (!s) return res.status(404).json({ error: 'session not found' })
|
|
9
|
+
res.json(s)
|
|
10
|
+
})
|
|
6
11
|
gui.route('GET', '/api/sessions/:id/messages', async (req, res) => res.json(await getMessages(req.params.id)))
|
|
12
|
+
gui.route('DELETE', '/api/sessions/:id', async (req, res) => res.json(await deleteSession(req.params.id)))
|
|
7
13
|
gui.route('GET', '/api/search', async (req, res) => res.json(await search(String(req.query.q || ''))))
|
|
8
14
|
},
|
|
9
15
|
}
|
|
@@ -1,52 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// memory tool — the explicit manual surface over freddie's primary learning store, gm rs-learn.
|
|
2
|
+
// add -> memorize (semantic embed), search -> recall (vector top-k), list -> broad recall,
|
|
3
|
+
// forget -> prune by key. All routed in-process through src/learn/gm-learn.js; no local SQLite.
|
|
4
|
+
import { memorize, recall, prune, projectNamespace } from '../../src/learn/gm-learn.js'
|
|
4
5
|
|
|
5
|
-
async function
|
|
6
|
-
const d = await db()
|
|
7
|
-
await d.exec(`CREATE TABLE IF NOT EXISTS memory_local (id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, ts INTEGER NOT NULL)`)
|
|
8
|
-
if (!d._fts5_unavailable) {
|
|
9
|
-
try { await d.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS memory_local_fts USING fts5(content, content='memory_local', content_rowid='id')`) } catch (e) { d._fts5_unavailable = true }
|
|
10
|
-
try { await d.exec(`CREATE TRIGGER IF NOT EXISTS memory_local_ai AFTER INSERT ON memory_local BEGIN INSERT INTO memory_local_fts(rowid, content) VALUES (new.id, new.content); END`) } catch (e) { d._fts5_unavailable = true }
|
|
11
|
-
}
|
|
12
|
-
return d
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function provider() {
|
|
16
|
-
const name = getConfigValue('memory.provider')
|
|
17
|
-
if (!name) return null
|
|
18
|
-
try { return createMemoryProvider(name, {}) } catch { return null }
|
|
19
|
-
}
|
|
6
|
+
async function ns(args) { return args.namespace || await projectNamespace() }
|
|
20
7
|
|
|
21
8
|
const ACTIONS = {
|
|
22
|
-
add: async (
|
|
23
|
-
if (!content) return { error: 'content required' }
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
9
|
+
add: async (args) => {
|
|
10
|
+
if (!args.content) return { error: 'content required' }
|
|
11
|
+
const key = await memorize(args.content, { namespace: await ns(args) })
|
|
12
|
+
return key ? { key, stored: 'rs-learn' } : { stored: 'noop', note: 'gm rs-learn unavailable' }
|
|
13
|
+
},
|
|
14
|
+
search: async (args) => {
|
|
15
|
+
const limit = args.limit || 10
|
|
16
|
+
const hits = await recall(args.query || '', { limit, namespace: await ns(args) })
|
|
17
|
+
return { items: hits.map(h => ({ content: h.text, score: h.score, key: h.key })) }
|
|
29
18
|
},
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const likePattern = `%${query}%`
|
|
35
|
-
if (d._fts5_unavailable) {
|
|
36
|
-
const rows = await d.prepare(`SELECT id, content, ts FROM memory_local WHERE content LIKE ? ORDER BY ts DESC LIMIT ?`).all(likePattern, limit)
|
|
37
|
-
return { items: rows }
|
|
38
|
-
}
|
|
39
|
-
try {
|
|
40
|
-
const rows = await d.prepare(`SELECT m.id, m.content, m.ts FROM memory_local_fts f JOIN memory_local m ON m.id = f.rowid WHERE memory_local_fts MATCH ? ORDER BY rank LIMIT ?`).all(query, limit)
|
|
41
|
-
return { items: rows }
|
|
42
|
-
} catch {
|
|
43
|
-
const rows = await d.prepare(`SELECT id, content, ts FROM memory_local WHERE content LIKE ? ORDER BY ts DESC LIMIT ?`).all(likePattern, limit)
|
|
44
|
-
return { items: rows }
|
|
45
|
-
}
|
|
19
|
+
list: async (args) => {
|
|
20
|
+
// No native list verb; surface the most relevant memories via a broad recall.
|
|
21
|
+
const hits = await recall(args.query || 'project notes facts decisions', { limit: 50, namespace: await ns(args) })
|
|
22
|
+
return { items: hits.map(h => ({ content: h.text, score: h.score, key: h.key })) }
|
|
46
23
|
},
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return
|
|
24
|
+
forget: async (args) => {
|
|
25
|
+
if (!args.key) return { error: 'key required to forget (prune is by explicit key, never blind similarity-delete)' }
|
|
26
|
+
return await prune(args.key)
|
|
50
27
|
},
|
|
51
28
|
}
|
|
52
29
|
|
|
@@ -55,8 +32,8 @@ export const _tool = ({
|
|
|
55
32
|
toolset: 'core',
|
|
56
33
|
schema: {
|
|
57
34
|
name: 'memory',
|
|
58
|
-
description: 'Add/search/list memory.
|
|
59
|
-
parameters: { type: 'object', properties: { action: { type: 'string', enum: Object.keys(ACTIONS) }, content: { type: 'string' }, query: { type: 'string' }, limit: { type: 'number' } }, required: ['action'] },
|
|
35
|
+
description: 'Add/search/list/forget long-term memory, backed by gm rs-learn (semantic vector recall). add embeds a fact; search returns score-ranked semantic hits; list surfaces relevant memories; forget prunes by key.',
|
|
36
|
+
parameters: { type: 'object', properties: { action: { type: 'string', enum: Object.keys(ACTIONS) }, content: { type: 'string' }, query: { type: 'string' }, key: { type: 'string' }, namespace: { type: 'string' }, limit: { type: 'number' } }, required: ['action'] },
|
|
60
37
|
},
|
|
61
38
|
handler: async (args) => {
|
|
62
39
|
const fn = ACTIONS[args.action]
|