freddie 0.0.102 → 0.0.104

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 CHANGED
@@ -30,7 +30,7 @@ When you touch one of the four direct-fetch utility plugins above, the right fix
30
30
 
31
31
  Consume these top-level acptoapi exports directly (no re-export shim, no helper module): `chat`, `stream`, `chain`, `chatChain`, `streamChain`, `fallback`, `buildAutoChain`, `resolveModel`, `parseCommaList`, `splitPrefix`, `listAllModelsAndQueues`, `resolveQueue`, `listAllQueues`, `loadMatrix`, `matrixScore`, `clearMatrixCache`, `peekStatus`, `getStatus`, `isAvailable`, `markFailed`, `markOk`, `resetAvailability`, `startSampler`, `stopSampler`, `createSampler`, `probe`, `probeModels`, `getCachedModels`, `getRunHistory`, `PROVIDER_KEYS`, `PROVIDER_DEFAULTS`.
32
32
 
33
- Public surface reference: `node_modules/acptoapi/AGENTS.md` "Public API — unified chain SDK". Acceptable freddie-side adapters (cannot be deleted yet): `model-discovery.js` (claude-cli/ACP/ollama probing breadth acptoapi doesn't cover), `model-sampler.js` (13L re-export shim test.js imports from it), `model-matrix.js` (28L MATRIX_FILE path helper), `acptoapi-bridge.js` (HTTP daemon passthrough at FREDDIE_LLM_URL when reachable, for `claude/*` etc that need the OAuth-managed daemon).
33
+ Public surface reference: `node_modules/acptoapi/AGENTS.md` "Public API — unified chain SDK". Acceptable freddie-side adapters (cannot be deleted yet): `model-discovery.js` (claude-cli/ACP/ollama probing breadth acptoapi doesn't cover; `listKnownProviders` merges `agent.discovered_models` keys + acptoapi `PROVIDER_KEYS` + `[claude-cli,kilo,opencode,ollama]`), `model-matrix.js` (28L MATRIX_FILE path helper + `matrixUsable` predicate — freddie-side because the matrix file path is repo-local), `acptoapi-bridge.js` (HTTP daemon passthrough at FREDDIE_LLM_URL when reachable, for `claude/*` etc that need the OAuth-managed daemon). Removed 2026-05-17: `src/agent/model-sampler.js` (was 13L re-export shim — callers now import sampler funcs directly via `createRequire('acptoapi')`).
34
34
 
35
35
  Matrix wired: shim passes `matrixSource: process.env.FREDDIE_MATRIX_URL || <repo>/.gm/model-availability.json` only for comma-list or `queue/<name>` model strings (single-shot omits to avoid leaking chain opts into upstream HTTP body — bug fixed in acptoapi 1.0.62 buildParams/_stripChainOpts, but the conditional pass-through stays as defense-in-depth).
36
36
 
@@ -64,7 +64,7 @@ Thin shims (still resolved through host, do not bypass):
64
64
 
65
65
  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.
66
66
 
67
- **gm-cc plugin integration** (2026-05-04) — gm-cc npm package (v2.0.727) successfully integrated via `plugins/gm-cc/plugin.js`. Plugin auto-discovers 12 SKILL.md files from gm-cc package, extracts name/description from YAML frontmatter, registers via `pi.skills.register({name: 'gm:'+name, description, content, source:'gm-cc'})`. Skills: browser, code-search, create-lang-plugin, gm, gm-complete, gm-emit, gm-execute, governance, pages, planning, ssh, update-docs. All accessible via `gm:*` namespace in pi.skills registry.
67
+ **gm-skill plugin integration** — `plugins/gm-skill/plugin.js` registers ONE canonical skill named `gm-skill`. Resolution order: (1) `~/.claude/skills/gm-skill/SKILL.md` (the `bun x skills add AnEntrypoint/gm-skill -y -g` install), (2) `node_modules/gm-cc/skills/gm-skill/SKILL.md` (npm fallback). 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 (which would otherwise inject all 25 deprecated variants). test.js asserts exactly one gm-prefixed skill is registered, named `gm-skill`.
68
68
 
69
69
  ## Multi-project workspace system (2026-05-04)
70
70
 
@@ -269,7 +269,7 @@ All 21 named integration tests in `test.js` pass (exit 0). Subsystem coverage:
269
269
  - **acptoapi bridge** — Integrated at `src/agent/acptoapi-bridge.js` + `src/agent/llm_resolver.js` (commit 5f55f1e). Localhost API (default port 4800) converting OpenAI/Anthropic SDK calls to multiple backends: Kilo Code, opencode, Claude CLI, Anthropic API, Gemini, Ollama, Bedrock. Endpoint `/v1/chat/completions`, OpenAI-compatible, accepts `Bearer none` auth.
270
270
  - **acptoapi dep pattern** (2026-05-12) — `package.json` now pins `"acptoapi": "^1.0.55"` from the npm registry (CI auto-bumps on each acptoapi push; restore-package.cjs ROOT FIX prevents file: regressions). For local SDK iteration, swap to `file:../acptoapi` temporarily. CJS/ESM boundary bridged via `createRequire(import.meta.url)` in freddie ESM files that import acptoapi CJS exports.
271
271
  - **LLM resolver priority** (2026-05-10) — (1) explicit provider+key, (2) acptoapi if `/v1/models` returns 200, (3) `agent.model_preference` config array (ordered failover, sampler-gated), (4) `sdk.buildAutoChain()` env-key scan, (5) throw. `PROVIDER_KEYS` and `PROVIDER_DEFAULTS` imported from `acptoapi` — not maintained in freddie. `sdk.chat()` returns OpenAI `{choices:[{message}]}` format; `sdkChat()` adapter in llm_resolver converts to freddie's `{content, tool_calls, raw}`.
272
- - **Model sampler — re-export shim** (2026-05-10) — `src/agent/model-sampler.js` is a 13-line re-export shim over acptoapi sampler. Sampler logic (5-step backoff 30s→480s, createSampler factory, singleton) lives in `c:\dev\acptoapi\lib\sampler.js`. Exports: `isAvailable`, `markFailed`, `markOk`, `resetAvailability`, `getStatus`, `probe`, `startSampler`, `stopSampler`, `createSampler`.
272
+ - **Model sampler — direct acptoapi import** (2026-05-17) — `src/agent/model-sampler.js` was deleted as redundant. Sampler funcs (`isAvailable`, `markFailed`, `markOk`, `resetAvailability`, `getStatus`, `probe`, `startSampler`, `stopSampler`, `createSampler`) come straight from `acptoapi` via `createRequire`. Backoff logic (5-step 30s→480s, createSampler factory, singleton) lives in `c:\dev\acptoapi\lib\sampler.js`.
273
273
  - **model_preference config key** (2026-05-10) — `agent.model_preference: []` in `~/.freddie/config.yaml`. Array of `{ provider, model? }` objects; `resolveCallLLM` tries each in order, skipping unavailable (sampler-gated) and marking failures with backoff. Config v2 migration adds the key on upgrade from v1.
274
274
  - **acptoapi Claude backend verified** (2026-05-03) — Live agent loop working: start acptoapi server `node bin/agentapi.js --port 4800` (c:\dev\acptoapi), then set `FREDDIE_LLM_URL=http://localhost:4800/v1 + FREDDIE_LLM_MODEL=claude/haiku`. Model prefix `claude/` routes to Claude CLI subprocess. freddie test.js 12/12 green confirming integration production-ready. Dashboard `/api/chat` and `/api/batch` are POST-only; GET returns 404 (correct).
275
275
 
@@ -290,6 +290,8 @@ All 12 test.js named groups passing: home+config+skin, sessions+FTS5, tools+tool
290
290
  - 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.
291
291
  - 2026-05-04: Recorded freddie publish workflow root fix — `.github/workflows/restore-package.cjs` now pins anentrypoint-design via `npm view` + version (`^<latest>`) instead of file: dep; `publish.yml` runs `npm install --package-lock-only` to sync lockfile. Updated "Rebase regression trap" entry with detailed causation + recurrence tell. Added new "GitHub Actions deploy-pages@v5 duplicate artifact rejection" caveat (rerun --failed silently re-uploads; trigger fresh run instead). rs-learn store still unavailable. 0 items migrated; 2 facts added/refined in AGENTS.md.
292
292
  - 2026-05-10: 15-provider LLM resolver expansion. Added model-sampler.js (backoff), PROVIDER_KEYS/DEFAULTS expansion, model_preference config key, config v2 migration. Updated "LLM resolver priority" + "15-provider support" + "Model sampler" + "model_preference" entries in AGENTS.md. test.js 12/12 green at exactly 200 lines. rs-learn store still unavailable. 4 new facts added to AGENTS.md.
293
+ - 2026-05-17 (continuation): +2 memorizes for uncommitted working-tree state — freddie-llm-resolver-anthropic-fallback-removed (project) captures the ~95L deletion of @anthropic-ai/sdk fallback in src/agent/llm_resolver.js (aligns with 'acptoapi is THE SDK' policy); freddie-stray-test-files-policy-violation (feedback) flags untracked src/agent/acptoapi-provider.js + test-acptoapi.js as violating the 'one test.js at root' rule. Total session exfiltration: 20 KV keys.
294
+ - 2026-05-17: AGENTS.md exfiltration to learning store. 18 memorize dispatches landed (KV keys mem-1779041362557 through mem-1779041487836, 21KB total) covering: substrate stack, dynamic stack contract, acptoapi SDK surface, plugin architecture, multi-project workspace, 3 gotcha batches (browser/async, codeinsight FPs, exec/CI), dashboard caveats, website YAML, LLM resolver+sampler, trajectory v2 + validation witness, opencode+kilo ACP, gm-cc + sync-upstream, model availability matrix, plugsdk + Rust gotchas, testing/profile/cache policies, layout map. learn-status confirms wasm-via-KV mode. In-session recall still returns empty (consistent with documented retrieval-side gap; index propagation is cross-session). PRD 18/18 resolved; phases PLAN -> EXECUTE -> VERIFY -> COMPLETE.
293
295
  - 2026-05-10 (session 2): Deep integration audit between freddie and acptoapi SDK. Moved sampler logic into acptoapi lib/sampler.js (createSampler factory + singleton, 5-step backoff). Added lib/provider-maps.js (PROVIDER_KEYS + PROVIDER_DEFAULTS, 17 providers, derived from BRANDS+auto-chain). freddie: switched to file:../acptoapi dep, model-sampler.js replaced with 13-line re-export shim, llm_resolver.js simplified 131→95L (OPENAI_COMPAT removed, PROVIDER_KEYS/DEFAULTS from SDK, buildAutoChain() for auto-scan, sdkChat() adapter for OpenAI→freddie format). test.js 12/12 green. Both repos pushed. AGENTS.md updated with acptoapi dep pattern, re-export shim pattern, CJS/ESM bridge via createRequire.
294
296
 
295
297
  ## Dashboard web UI caveats
package/package.json CHANGED
@@ -1,33 +1,43 @@
1
1
  {
2
2
  "name": "freddie",
3
- "version": "0.0.102",
3
+ "version": "0.0.104",
4
4
  "type": "module",
5
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
  },
9
9
  "main": "./src/index.js",
10
+ "exports": {
11
+ ".": "./src/index.js",
12
+ "./browser": {
13
+ "browser": "./dist/browser/freddie.js",
14
+ "default": "./src/browser/index.js"
15
+ }
16
+ },
10
17
  "scripts": {
11
18
  "start": "node bin/freddie.js",
12
- "test": "node test.js"
19
+ "test": "node test.js",
20
+ "build:browser": "vite build --config vite.browser.config.js"
13
21
  },
14
22
  "dependencies": {
23
+ "@anentrypoint/libsql-plugkit-client": "github:AnEntrypoint/libsql-plugkit-client",
15
24
  "@libsql/client": "^0.5.0",
16
25
  "@mariozechner/pi-agent-core": "^0.70.6",
17
26
  "@mariozechner/pi-ai": "^0.70.6",
18
27
  "@mariozechner/pi-coding-agent": "^0.70.6",
19
28
  "@mariozechner/pi-tui": "^0.70.6",
29
+ "acptoapi": "^1.0.90",
30
+ "anentrypoint-design": "^0.0.108",
20
31
  "commander": "^14.0.0",
21
32
  "express": "^5.0.0",
22
33
  "flatspace": "^1.0.18",
23
34
  "floosie": "^0.6.14",
24
- "gm-cc": "^2.0.727",
35
+ "gm-cc": "^2.0.1081",
25
36
  "js-yaml": "^4.1.0",
26
- "plugsdk": "^1.0.16",
37
+ "libsql-plugkit-client": "^0.0.10",
38
+ "plugsdk": "^1.0.20",
27
39
  "xstate": "^5.31.0",
28
- "zod": "^4.0.0",
29
- "anentrypoint-design": "^0.0.95",
30
- "acptoapi": "latest"
40
+ "zod": "^4.0.0"
31
41
  },
32
42
  "optionalDependencies": {
33
43
  "@libsql/darwin-arm64": "0.3.19",
@@ -71,5 +81,8 @@
71
81
  "freddie",
72
82
  "pi-mono",
73
83
  "xstate"
74
- ]
84
+ ],
85
+ "devDependencies": {
86
+ "vite": "^8.0.13"
87
+ }
75
88
  }
@@ -0,0 +1,49 @@
1
+ import { createRequire } from 'module'
2
+ import path from 'path'
3
+ import fs from 'fs'
4
+
5
+ const _require = createRequire(import.meta.url)
6
+
7
+ function resolveSkillMd() {
8
+ const home = process.env.USERPROFILE || process.env.HOME
9
+ if (home) {
10
+ const userSkill = path.join(home, '.claude', 'skills', 'gm-skill', 'SKILL.md')
11
+ if (fs.existsSync(userSkill)) return userSkill
12
+ }
13
+ try {
14
+ const pkgPath = _require.resolve('gm-cc/package.json')
15
+ const candidate = path.join(path.dirname(pkgPath), 'skills', 'gm-skill', 'SKILL.md')
16
+ if (fs.existsSync(candidate)) return candidate
17
+ } catch {}
18
+ return null
19
+ }
20
+
21
+ function parseFrontmatter(md) {
22
+ const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/)
23
+ if (!m) return { fields: {}, body: md }
24
+ const fields = {}
25
+ for (const line of m[1].split(/\r?\n/)) {
26
+ const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/)
27
+ if (kv) fields[kv[1]] = kv[2]
28
+ }
29
+ return { fields, body: m[2] }
30
+ }
31
+
32
+ export default {
33
+ name: 'gm-skill',
34
+ surfaces: 'pi',
35
+ register({ pi }) {
36
+ const skillPath = resolveSkillMd()
37
+ if (!skillPath) return
38
+ const raw = fs.readFileSync(skillPath, 'utf8')
39
+ const { fields, body } = parseFrontmatter(raw)
40
+ pi.skills.register({
41
+ name: 'gm-skill',
42
+ description: fields.description || 'AI-native software engineering harness',
43
+ content: body,
44
+ source: 'gm-skill',
45
+ frontmatter: fields,
46
+ file: skillPath,
47
+ })
48
+ },
49
+ }
@@ -0,0 +1,77 @@
1
+ // freddie ↔ acptoapi chain bridge — surfaces acptoapi /v1/chains, /debug/probe-live
2
+ // through freddie's GUI API so the dashboard can manage named fallback chains
3
+ // without thebird talking directly to acptoapi.
4
+ import { getAcptoapiUrl } from '../../src/agent/acptoapi-bridge.js'
5
+
6
+ function base() { return getAcptoapiUrl().replace(/\/v1\/?$/, '') }
7
+ const HDR = () => ({ 'content-type': 'application/json', authorization: 'Bearer none' })
8
+
9
+ async function fwd(url, init) {
10
+ try {
11
+ const r = await fetch(url, init)
12
+ const ct = r.headers.get('content-type') || ''
13
+ if (ct.includes('json')) return { status: r.status, json: await r.json() }
14
+ return { status: r.status, text: await r.text() }
15
+ } catch (e) { return { status: 502, json: { error: { message: e.message, via: url } } } }
16
+ }
17
+
18
+ export default {
19
+ name: 'gui-acptoapi-chains', surfaces: 'gui',
20
+ register({ gui }) {
21
+ gui.route('GET', '/api/acptoapi/health', async (_, res) => {
22
+ const r = await fwd(base() + '/health', { headers: HDR() })
23
+ res.status(r.status === 502 ? 502 : 200).json(r.json || { text: r.text })
24
+ })
25
+
26
+ gui.route('GET', '/api/acptoapi/chains', async (_, res) => {
27
+ const r = await fwd(base() + '/v1/chains', { headers: HDR() })
28
+ res.status(r.status).json(r.json || {})
29
+ })
30
+
31
+ gui.route('POST', '/api/acptoapi/chains', async (req, res) => {
32
+ const { name, links } = req.body || {}
33
+ if (!name || !Array.isArray(links)) return res.status(400).json({ error: { message: 'name + links[] required' } })
34
+ const r = await fwd(base() + '/v1/chains', {
35
+ method: 'POST', headers: HDR(), body: JSON.stringify({ name, links }),
36
+ })
37
+ res.status(r.status).json(r.json || {})
38
+ })
39
+
40
+ gui.route('DELETE', '/api/acptoapi/chains/:name', async (req, res) => {
41
+ const r = await fwd(base() + '/v1/chains?name=' + encodeURIComponent(req.params.name), {
42
+ method: 'DELETE', headers: HDR(),
43
+ })
44
+ res.status(r.status).json(r.json || {})
45
+ })
46
+
47
+ gui.route('GET', '/api/acptoapi/probe', async (req, res) => {
48
+ const force = req.query.force === '1' ? '?force=1' : ''
49
+ const r = await fwd(base() + '/debug/probe-live' + force, { headers: HDR() })
50
+ res.status(r.status).json(r.json || {})
51
+ })
52
+
53
+ gui.route('GET', '/api/acptoapi/models', async (_, res) => {
54
+ const r = await fwd(base() + '/v1/models', { headers: HDR() })
55
+ res.status(r.status).json(r.json || {})
56
+ })
57
+
58
+ gui.route('GET', '/api/acptoapi/auto-chain', async (_, res) => {
59
+ const r = await fwd(base() + '/debug/auto-chain', { headers: HDR() })
60
+ res.status(r.status).json(r.json || {})
61
+ })
62
+
63
+ // surface acptoapi config knobs freddie exposes via env
64
+ gui.route('GET', '/api/acptoapi/config', (_, res) => {
65
+ res.json({
66
+ url: base(),
67
+ model: process.env.FREDDIE_LLM_MODEL || 'claude/haiku',
68
+ envHints: {
69
+ FREDDIE_LLM_URL: process.env.FREDDIE_LLM_URL || null,
70
+ FREDDIE_LLM_MODEL: process.env.FREDDIE_LLM_MODEL || null,
71
+ ACPTOAPI_LIVE_PROBE: process.env.ACPTOAPI_LIVE_PROBE || null,
72
+ ACPTOAPI_PROBE_CAP: process.env.ACPTOAPI_PROBE_CAP || null,
73
+ },
74
+ })
75
+ })
76
+ },
77
+ }
@@ -1,9 +1,12 @@
1
+ // All upstream connectivity lives in acptoapi. This handler is a thin wrapper.
2
+ import { getAcptoapiUrl } from '../../src/agent/acptoapi-bridge.js'
3
+
1
4
  export const _tool = ({
2
5
  name: 'image_gen',
3
6
  toolset: 'creative',
4
7
  schema: {
5
8
  name: 'image_gen',
6
- description: 'Generate an image from a prompt. Provider via config.image_gen.provider (openai|replicate).',
9
+ description: 'Generate an image from a prompt. Routes through acptoapi /v1/images/generations.',
7
10
  parameters: {
8
11
  type: 'object',
9
12
  properties: {
@@ -19,13 +22,15 @@ export const _tool = ({
19
22
  requiresEnv: ['OPENAI_API_KEY or REPLICATE_API_TOKEN'],
20
23
  handler: async ({ prompt, provider, size = '1024x1024', model }) => {
21
24
  const which = provider || (process.env.OPENAI_API_KEY ? 'openai' : 'replicate')
22
- if (which === 'openai') {
23
- if (!process.env.OPENAI_API_KEY) return { error: 'OPENAI_API_KEY required' }
24
- const res = await fetch('https://api.openai.com/v1/images/generations', { method: 'POST', headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ model: model || 'gpt-image-1', prompt, size }) })
25
- return await res.json()
26
- }
27
- if (!process.env.REPLICATE_API_TOKEN) return { error: 'REPLICATE_API_TOKEN required' }
28
- const res = await fetch('https://api.replicate.com/v1/predictions', { method: 'POST', headers: { authorization: `Token ${process.env.REPLICATE_API_TOKEN}`, 'content-type': 'application/json' }, body: JSON.stringify({ version: model || 'black-forest-labs/flux-schnell', input: { prompt } }) })
29
- return await res.json()
25
+ const base = getAcptoapiUrl().replace(/\/v1\/?$/, '')
26
+ const body = which === 'openai'
27
+ ? { model: model || 'gpt-image-1', prompt, size }
28
+ : { version: model || 'black-forest-labs/flux-schnell', input: { prompt } }
29
+ const r = await fetch(base + '/v1/images/generations', {
30
+ method: 'POST',
31
+ headers: { 'content-type': 'application/json', 'x-provider': which, authorization: 'Bearer none' },
32
+ body: JSON.stringify(body),
33
+ })
34
+ return await r.json()
30
35
  },
31
36
  })
@@ -1,18 +1,25 @@
1
+ // Upstream connectivity lives in acptoapi.
1
2
  import fs from 'node:fs'
3
+ import { getAcptoapiUrl } from '../../src/agent/acptoapi-bridge.js'
4
+
2
5
  export const _tool = ({
3
6
  name: 'transcription',
4
7
  toolset: 'creative',
5
- schema: { name: 'transcription', description: 'Transcribe audio with OpenAI Whisper.', parameters: { type: 'object', properties: { file_path: { type: 'string' }, model: { type: 'string', default: 'whisper-1' } }, required: ['file_path'] } },
8
+ schema: { name: 'transcription', description: 'Transcribe audio via acptoapi /v1/audio/transcriptions (OpenAI Whisper).', parameters: { type: 'object', properties: { file_path: { type: 'string' }, model: { type: 'string', default: 'whisper-1' } }, required: ['file_path'] } },
6
9
  requiresEnv: ['OPENAI_API_KEY'],
7
10
  checkFn: () => Boolean(process.env.OPENAI_API_KEY),
8
11
  handler: async ({ file_path, model = 'whisper-1' }) => {
9
- if (!process.env.OPENAI_API_KEY) return { error: 'OPENAI_API_KEY required' }
10
12
  if (!fs.existsSync(file_path)) return { error: 'file not found: ' + file_path }
13
+ const base = getAcptoapiUrl().replace(/\/v1\/?$/, '')
11
14
  const blob = new Blob([fs.readFileSync(file_path)])
12
15
  const fd = new FormData()
13
16
  fd.append('file', blob, file_path.split(/[\\/]/).pop())
14
17
  fd.append('model', model)
15
- const r = await fetch('https://api.openai.com/v1/audio/transcriptions', { method: 'POST', headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY}` }, body: fd })
18
+ const r = await fetch(base + '/v1/audio/transcriptions', {
19
+ method: 'POST',
20
+ headers: { authorization: 'Bearer none' },
21
+ body: fd,
22
+ })
16
23
  return await r.json()
17
24
  },
18
25
  })
@@ -1,18 +1,23 @@
1
+ // Upstream connectivity lives in acptoapi.
2
+ import { getAcptoapiUrl } from '../../src/agent/acptoapi-bridge.js'
3
+
1
4
  export const _tool = ({
2
5
  name: 'tts',
3
6
  toolset: 'creative',
4
- schema: { name: 'tts', description: 'Synthesize speech (OpenAI tts-1 or ElevenLabs).', parameters: { type: 'object', properties: { text: { type: 'string' }, provider: { type: 'string', enum: ['openai', 'elevenlabs'], default: 'openai' }, voice: { type: 'string' } }, required: ['text'] } },
7
+ schema: { name: 'tts', description: 'Synthesize speech (OpenAI tts-1 or ElevenLabs) via acptoapi.', parameters: { type: 'object', properties: { text: { type: 'string' }, provider: { type: 'string', enum: ['openai', 'elevenlabs'], default: 'openai' }, voice: { type: 'string' } }, required: ['text'] } },
5
8
  requiresEnv: ['OPENAI_API_KEY or ELEVENLABS_API_KEY'],
6
9
  checkFn: () => Boolean(process.env.OPENAI_API_KEY || process.env.ELEVENLABS_API_KEY),
7
10
  handler: async ({ text, provider = 'openai', voice = 'alloy' }) => {
8
- if (provider === 'elevenlabs') {
9
- if (!process.env.ELEVENLABS_API_KEY) return { error: 'ELEVENLABS_API_KEY required' }
10
- const v = voice || '21m00Tcm4TlvDq8ikWAM'
11
- const r = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${v}`, { method: 'POST', headers: { 'xi-api-key': process.env.ELEVENLABS_API_KEY, 'content-type': 'application/json' }, body: JSON.stringify({ text }) })
12
- return { status: r.status, contentType: r.headers.get('content-type'), bytes: (await r.arrayBuffer()).byteLength }
13
- }
14
- if (!process.env.OPENAI_API_KEY) return { error: 'OPENAI_API_KEY required' }
15
- const r = await fetch('https://api.openai.com/v1/audio/speech', { method: 'POST', headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ model: 'tts-1', input: text, voice }) })
16
- return { status: r.status, bytes: (await r.arrayBuffer()).byteLength }
11
+ const base = getAcptoapiUrl().replace(/\/v1\/?$/, '')
12
+ const body = provider === 'elevenlabs'
13
+ ? { text, voice: voice || '21m00Tcm4TlvDq8ikWAM', provider: 'elevenlabs' }
14
+ : { model: 'tts-1', input: text, voice }
15
+ const xProv = provider === 'elevenlabs' ? 'tts.elevenlabs' : 'speech.openai'
16
+ const r = await fetch(base + '/v1/audio/speech', {
17
+ method: 'POST',
18
+ headers: { 'content-type': 'application/json', 'x-provider': xProv, authorization: 'Bearer none' },
19
+ body: JSON.stringify(body),
20
+ })
21
+ return { status: r.status, contentType: r.headers.get('content-type'), bytes: (await r.arrayBuffer()).byteLength }
17
22
  },
18
23
  })
@@ -1,17 +1,26 @@
1
+ // All upstream connectivity lives in acptoapi. Vision is just a multimodal
2
+ // chat-completions call — go through the existing bridge.
3
+ import { callLLM, getAcptoapiUrl } from '../../src/agent/acptoapi-bridge.js'
4
+
1
5
  export const _tool = ({
2
6
  name: 'vision',
3
7
  toolset: 'creative',
4
- schema: { name: 'vision', description: 'Describe an image (URL or base64) using a vision-capable LLM.', parameters: { type: 'object', properties: { image_url: { type: 'string' }, prompt: { type: 'string', default: 'Describe this image.' }, provider: { type: 'string', enum: ['openai', 'anthropic'], default: 'openai' } }, required: ['image_url'] } },
5
- requiresEnv: ['OPENAI_API_KEY or ANTHROPIC_API_KEY'],
8
+ schema: { name: 'vision', description: 'Describe an image (URL or base64) via acptoapi chat-completions.', parameters: { type: 'object', properties: { image_url: { type: 'string' }, prompt: { type: 'string', default: 'Describe this image.' }, model: { type: 'string' } }, required: ['image_url'] } },
9
+ requiresEnv: ['OPENAI_API_KEY or ANTHROPIC_API_KEY (provided to acptoapi)'],
6
10
  checkFn: () => Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY),
7
- handler: async ({ image_url, prompt = 'Describe this image.', provider = 'openai' }) => {
8
- if (provider === 'anthropic') {
9
- if (!process.env.ANTHROPIC_API_KEY) return { error: 'ANTHROPIC_API_KEY required' }
10
- const r = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'x-api-key': process.env.ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' }, body: JSON.stringify({ model: 'claude-sonnet-4-6', max_tokens: 1024, messages: [{ role: 'user', content: [{ type: 'image', source: { type: 'url', url: image_url } }, { type: 'text', text: prompt }] }] }) })
11
- return await r.json()
11
+ handler: async ({ image_url, prompt = 'Describe this image.', model }) => {
12
+ const messages = [{
13
+ role: 'user',
14
+ content: [
15
+ { type: 'text', text: prompt },
16
+ { type: 'image_url', image_url: { url: image_url } },
17
+ ],
18
+ }]
19
+ try {
20
+ const r = await callLLM({ messages, model: model || 'openai/gpt-4o-mini' })
21
+ return { content: r.content, raw: r.raw }
22
+ } catch (e) {
23
+ return { error: e.message, via: getAcptoapiUrl() }
12
24
  }
13
- if (!process.env.OPENAI_API_KEY) return { error: 'OPENAI_API_KEY required' }
14
- const r = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: [{ type: 'text', text: prompt }, { type: 'image_url', image_url: { url: image_url } }] }] }) })
15
- return await r.json()
16
25
  },
17
26
  })
@@ -0,0 +1,24 @@
1
+
2
+ import path from 'path';
3
+ const F='C:/dev/freddie';
4
+ const R=p=>path.resolve(p);
5
+ const ALIAS={
6
+ [R(F+'/src/host/index.js')]: R(F+'/src/agent/__browser_shims/host.js'),
7
+ [R(F+'/src/toolsets.js')]: R(F+'/src/agent/__browser_shims/toolsets.js'),
8
+ [R(F+'/src/agent/llm_resolver.js')]: R(F+'/src/agent/__browser_shims/llm_resolver.js'),
9
+ [R(F+'/src/observability/log.js')]: R(F+'/src/agent/__browser_shims/log.js'),
10
+ [R(F+'/src/config.js')]: R(F+'/src/agent/__browser_shims/config.js'),
11
+ [R(F+'/src/home.js')]: R(F+'/src/agent/__browser_shims/home.js'),
12
+ };
13
+ export default {
14
+ name: 'freddie-browser-alias',
15
+ setup(build){
16
+ build.onResolve({filter: /.*/}, async args=>{
17
+ if(args.path.startsWith('node:') || (!args.path.startsWith('.') && !path.isAbsolute(args.path))) return null;
18
+ const resolved = path.resolve(args.resolveDir||'', args.path);
19
+ const candidates=[resolved, resolved+'.js', resolved+'/index.js'];
20
+ for(const c of candidates) if(ALIAS[c]) return { path: ALIAS[c] };
21
+ return null;
22
+ });
23
+ }
24
+ };
@@ -0,0 +1,18 @@
1
+
2
+ import * as esbuild from 'esbuild';
3
+ import alias from './alias-plugin.mjs';
4
+ await esbuild.build({
5
+ entryPoints: ['C:/dev/freddie/src/agent/__browser_shims/entry.js'],
6
+ bundle: true,
7
+ format: 'esm',
8
+ platform: 'browser',
9
+ conditions: ['browser','module','import'],
10
+ outfile: 'C:/dev/thebird/docs/freddie-runtime.js',
11
+ plugins: [alias],
12
+ legalComments: 'none',
13
+ minify: false,
14
+ sourcemap: false,
15
+ logLevel: 'info',
16
+ external: [],
17
+ });
18
+ console.log('bundle ok');
@@ -0,0 +1,4 @@
1
+ export function getConfigValue(k,d){ return d }
2
+ export function setConfigValue(){}
3
+ export function loadConfig(){ return {} }
4
+ export function saveConfig(){}
@@ -0,0 +1,2 @@
1
+ export { createAgentMachine } from '../machine.js'
2
+ export { createMachine, createActor, assign, fromPromise, waitFor } from 'xstate'
@@ -0,0 +1,3 @@
1
+ export function getFreddieHome(){ return '/freddie' }
2
+ export function getFreddieProfileDir(){ return '/freddie/profile' }
3
+ export function ensureFreddieHome(){}
@@ -0,0 +1,36 @@
1
+ export async function bootHost() {
2
+ const br = globalThis.__freddieRuntimeBridge
3
+ if (!br || !br.host) throw new Error('freddie-runtime: __freddieRuntimeBridge.host not set')
4
+ const host = br.host
5
+ return {
6
+ hooks: {
7
+ async invoke(name, payload) {
8
+ const map = { preToolCall: 'pre_tool_use', postToolCall: 'post_tool_use', onMessageInbound: 'user_prompt_submit', onMessageOutbound: null, onSessionStart: null, onSessionEnd: 'stop', onPreCompact: null, onPostCompact: null }
9
+ const key = map[name]
10
+ if (!key || !host.pi.hooks[key]) return null
11
+ let merged = null
12
+ for (const fn of host.pi.hooks[key]) {
13
+ try {
14
+ const r = await fn(payload || {})
15
+ if (!r || typeof r !== 'object') continue
16
+ merged = merged || {}
17
+ if (r.decision === 'block') return { behavior: 'block', reason: r.reason || 'denied' }
18
+ if (r.systemMessage) merged.systemMessage = (merged.systemMessage ? merged.systemMessage + '\n' : '') + r.systemMessage
19
+ if (r.additionalContext) merged.additionalContext = (merged.additionalContext ? merged.additionalContext + '\n' : '') + r.additionalContext
20
+ if (r.args) merged.args = r.args
21
+ } catch (e) { console.warn('[freddie-runtime] hook ' + key + ' threw:', e && e.message) }
22
+ }
23
+ return merged
24
+ },
25
+ },
26
+ pi: {
27
+ tools: host.pi.tools,
28
+ skills: host.pi.skills,
29
+ hooks: host.pi.hooks,
30
+ dispatchTool: async (name, args) => {
31
+ const out = await host.runTool(name, args || {})
32
+ return typeof out === 'string' ? out : JSON.stringify(out)
33
+ },
34
+ },
35
+ }
36
+ }
@@ -0,0 +1,8 @@
1
+ export function resolveCallLLM({ provider, model } = {}) {
2
+ return async (input) => {
3
+ const br = globalThis.__freddieRuntimeBridge
4
+ if (!br || !br.callLLM) throw new Error('freddie-runtime: __freddieRuntimeBridge.callLLM not set')
5
+ return await br.callLLM({ ...input, provider, model: model || input.model })
6
+ }
7
+ }
8
+ export const matrixUsable = () => false
@@ -0,0 +1,10 @@
1
+ export function log(rec){ try{ console.debug('[freddie]', rec) }catch{} }
2
+ export function logger(subsystem){
3
+ return {
4
+ debug: (msg, e={}) => console.debug('[freddie:'+subsystem+']', msg, e),
5
+ info: (msg, e={}) => console.log('[freddie:'+subsystem+']', msg, e),
6
+ warn: (msg, e={}) => console.warn('[freddie:'+subsystem+']', msg, e),
7
+ error: (msg, e={}) => console.error('[freddie:'+subsystem+']', msg, e),
8
+ }
9
+ }
10
+ export function closeAll(){}
@@ -0,0 +1,18 @@
1
+ export async function getEnabledToolSchemas(enabled = ['core'], disabled = []) {
2
+ const br = globalThis.__freddieRuntimeBridge
3
+ if (!br || !br.host) return []
4
+ const host = br.host
5
+ const disabledSet = new Set(disabled)
6
+ const out = []
7
+ for (const [name, t] of host.pi.tools.entries()) {
8
+ if (disabledSet.has(name)) continue
9
+ out.push({ type: 'function', function: { name, description: t.description || '', parameters: t.inputSchema || { type: 'object', properties: {} } } })
10
+ }
11
+ return out
12
+ }
13
+ export async function getEnabledToolNames(enabled = ['core'], disabled = []) {
14
+ const br = globalThis.__freddieRuntimeBridge; if (!br || !br.host) return []
15
+ return [...br.host.pi.tools.keys()]
16
+ }
17
+ export async function getAvailableToolsets() { return ['core'] }
18
+ export const _FREDDIE_CORE_TOOLS = ['read','write','edit','grep','list']
@@ -84,11 +84,21 @@ function adaptResponse(r) {
84
84
 
85
85
  function tryParseJson(s) { try { return typeof s === 'string' ? JSON.parse(s) : (s || {}) } catch { return {} } }
86
86
 
87
- export async function isReachable() {
87
+ export async function isReachable(timeoutMs = 2000) {
88
88
  try {
89
- const res = await fetch(getAcptoapiUrl().replace(/\/$/, '') + '/models', { headers: { authorization: 'Bearer none' } })
90
- if (!res.ok) return false
91
- const json = await res.json()
92
- return Array.isArray(json.data) && json.data.length > 0
89
+ const controller = new AbortController()
90
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
91
+ try {
92
+ const res = await fetch(getAcptoapiUrl().replace(/\/$/, '') + '/models', {
93
+ headers: { authorization: 'Bearer none' },
94
+ signal: controller.signal
95
+ })
96
+ clearTimeout(timeoutId)
97
+ if (!res.ok) return false
98
+ const json = await res.json()
99
+ return Array.isArray(json.data) && json.data.length > 0
100
+ } finally {
101
+ clearTimeout(timeoutId)
102
+ }
93
103
  } catch { return false }
94
104
  }
@@ -1,10 +1,15 @@
1
- import { resolveKey } from './credential_sources.js'
1
+ // Upstream connectivity lives in acptoapi via /v1/responses passthrough.
2
+ import { getAcptoapiUrl } from './acptoapi-bridge.js'
2
3
  import { isCodexModel } from '../cli/codex_models.js'
4
+
3
5
  export async function chat({ input, model = 'o3-mini', tools = [], reasoning_effort = 'medium' } = {}) {
4
- const k = await resolveKey('openai')
5
- if (!k.value) throw new Error('OPENAI_API_KEY required (source: ' + k.source + ')')
6
6
  if (!isCodexModel(model)) console.warn('[codex_responses] non-codex model: ' + model)
7
- const r = await fetch('https://api.openai.com/v1/responses', { method: 'POST', headers: { authorization: 'Bearer ' + k.value, 'content-type': 'application/json' }, body: JSON.stringify({ model, input, ...(tools.length ? { tools } : {}), reasoning: { effort: reasoning_effort } }) })
7
+ const base = getAcptoapiUrl().replace(/\/v1\/?$/, '')
8
+ const r = await fetch(base + '/v1/responses', {
9
+ method: 'POST',
10
+ headers: { authorization: 'Bearer none', 'content-type': 'application/json' },
11
+ body: JSON.stringify({ model, input, ...(tools.length ? { tools } : {}), reasoning: { effort: reasoning_effort } }),
12
+ })
8
13
  return await r.json()
9
14
  }
10
15
  export const provider = 'codex_responses'
@@ -1,11 +1,16 @@
1
- import { resolveKey } from './credential_sources.js'
1
+ // Gemini upstream connectivity lives in acptoapi /v1beta/models/<model>:generateContent.
2
+ import { getAcptoapiUrl } from './acptoapi-bridge.js'
2
3
  import { adaptToolForGemini, adaptMessagesForGemini } from './gemini_schema.js'
4
+
3
5
  export async function chat({ messages, model = 'gemini-2.5-flash', tools = [] } = {}) {
4
- const k = await resolveKey('google')
5
- if (!k.value) throw new Error('GOOGLE_API_KEY required')
6
- const url = 'https://generativelanguage.googleapis.com/v1beta/models/' + model + ':generateContent?key=' + k.value
6
+ const base = getAcptoapiUrl().replace(/\/v1\/?$/, '')
7
+ const url = `${base}/v1beta/models/${model}:generateContent`
7
8
  const body = { contents: adaptMessagesForGemini(messages), ...(tools.length ? { tools: [{ function_declarations: tools.map(adaptToolForGemini) }] } : {}) }
8
- const r = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
9
+ const r = await fetch(url, {
10
+ method: 'POST',
11
+ headers: { 'content-type': 'application/json', authorization: 'Bearer none' },
12
+ body: JSON.stringify(body),
13
+ })
9
14
  return await r.json()
10
15
  }
11
16
  export const provider = 'google'
@@ -1,8 +1,20 @@
1
- import { resolveKey } from './credential_sources.js'
2
- const PROVIDERS = {
3
- openai: async ({ prompt, size, model }) => { const k = (await resolveKey('openai')).value; const r = await fetch('https://api.openai.com/v1/images/generations', { method: 'POST', headers: { authorization: 'Bearer ' + k, 'content-type': 'application/json' }, body: JSON.stringify({ model: model || 'gpt-image-1', prompt, size }) }); return await r.json() },
4
- replicate: async ({ prompt, model }) => { const k = process.env.REPLICATE_API_TOKEN; const r = await fetch('https://api.replicate.com/v1/predictions', { method: 'POST', headers: { authorization: 'Token ' + k, 'content-type': 'application/json' }, body: JSON.stringify({ version: model || 'black-forest-labs/flux-schnell', input: { prompt } }) }); return await r.json() },
5
- stability: async ({ prompt, model }) => { const k = process.env.STABILITY_API_KEY; const r = await fetch('https://api.stability.ai/v2beta/stable-image/generate/' + (model || 'core'), { method: 'POST', headers: { authorization: 'Bearer ' + k, accept: 'application/json' }, body: (() => { const fd = new FormData(); fd.append('prompt', prompt); return fd })() }); return await r.json() },
1
+ // All upstream image-generation traffic routes through acptoapi.
2
+ import { getAcptoapiUrl } from './acptoapi-bridge.js'
3
+
4
+ const PROVIDERS = ['openai', 'replicate', 'stability']
5
+
6
+ export async function generate({ provider = 'openai', prompt, size, model } = {}) {
7
+ if (!PROVIDERS.includes(provider)) throw new Error('unknown image provider: ' + provider)
8
+ const base = getAcptoapiUrl().replace(/\/v1\/?$/, '')
9
+ const body = provider === 'replicate'
10
+ ? { version: model || 'black-forest-labs/flux-schnell', input: { prompt } }
11
+ : { model: model || 'gpt-image-1', prompt, size }
12
+ const r = await fetch(base + '/v1/images/generations', {
13
+ method: 'POST',
14
+ headers: { authorization: 'Bearer none', 'content-type': 'application/json', 'x-provider': provider },
15
+ body: JSON.stringify(body),
16
+ })
17
+ return await r.json()
6
18
  }
7
- export async function generate({ provider = 'openai', ...args } = {}) { const fn = PROVIDERS[provider]; if (!fn) throw new Error('unknown image provider: ' + provider); return await fn(args) }
8
- export function listProviders() { return Object.keys(PROVIDERS) }
19
+
20
+ export function listProviders() { return PROVIDERS.slice() }
@@ -6,6 +6,7 @@ export { matrixUsable } from './model-matrix.js'
6
6
 
7
7
  const _require = createRequire(import.meta.url)
8
8
  const sdk = _require('acptoapi')
9
+
9
10
  export const PROVIDER_KEYS = sdk.PROVIDER_KEYS
10
11
  export const DEFAULTS = sdk.PROVIDER_DEFAULTS
11
12
 
@@ -59,10 +60,15 @@ export function resolveCallLLM({ provider, model } = {}) {
59
60
  }
60
61
  try {
61
62
  const isSimple = typeof m === 'string' && !m.includes(',') && !/^queue\//.test(m)
62
- if (isSimple && await bridgeReachable()) return await bridgeCall({ ...input, model: m })
63
+
64
+ if (isSimple && await bridgeReachable()) {
65
+ return await bridgeCall({ ...input, model: m })
66
+ }
67
+
63
68
  const opts = { model: m, messages: toMsgs(input.messages), tools: toTools(input.tools), onFallback: input.onFallback, output: 'openai' }
64
69
  if (/^queue\//.test(m)) opts.queuesMap = getConfigValue('agent.model_queues', {}) || {}
65
70
  if (m.includes(',') || /^queue\//.test(m)) opts.matrixSource = process.env.FREDDIE_MATRIX_URL || MATRIX_FILE
71
+
66
72
  const r = await sdk.chat(opts)
67
73
  return adapt(r)
68
74
  } catch (e) {
@@ -74,15 +74,23 @@ export function createAgentMachine({ provider, model, maxIterations = 90, callLL
74
74
  const last = input.messages[input.messages.length - 1]
75
75
  const calls = last.tool_calls || []
76
76
  const results = []
77
+ const extras = []
77
78
  for (const call of calls) {
78
- const res = await h.pi.dispatchTool(call.name || call.function?.name, call.arguments || call.function?.arguments || {})
79
- results.push({ tool_call_id: call.id || call.tool_call_id, content: res })
79
+ const tname = call.name || call.function?.name
80
+ const targs = call.arguments || call.function?.arguments || {}
81
+ const tcid = call.id || call.tool_call_id
82
+ const pushExtras = r => { if (r?.systemMessage) extras.push({ role: 'system', content: '[hook] ' + r.systemMessage }); if (r?.additionalContext) extras.push({ role: 'system', content: r.additionalContext }) }
83
+ const pre = await h.hooks.invoke('preToolCall', { name: tname, args: targs }); pushExtras(pre)
84
+ if (pre?.behavior === 'block') { results.push({ tool_call_id: tcid, content: JSON.stringify({ error: 'tool call denied by plugsdk hook', tool: tname, reason: pre.reason || 'denied' }) }); continue }
85
+ const res = await h.pi.dispatchTool(tname, (pre && pre.args) || targs)
86
+ pushExtras(await h.hooks.invoke('postToolCall', { name: tname, args: targs, result: res }))
87
+ results.push({ tool_call_id: tcid, content: res })
80
88
  }
81
- return results
89
+ return { results, extras }
82
90
  }),
83
91
  input: ({ context }) => ({ messages: context.messages }),
84
92
  onDone: { target: 'prompting', actions: assign({
85
- messages: ({ context, event }) => [...context.messages, ...event.output.map(r => ({ role: 'tool', tool_call_id: r.tool_call_id, content: r.content }))],
93
+ messages: ({ context, event }) => [...context.messages, ...event.output.results.map(r => ({ role: 'tool', tool_call_id: r.tool_call_id, content: r.content })), ...event.output.extras],
86
94
  iterations: ({ context }) => context.iterations + 1,
87
95
  }) },
88
96
  onError: { target: 'done', actions: assign({ error: ({ event }) => String(event.error?.message || event.error) }) },
@@ -143,30 +151,45 @@ async function writeTrajectory(out, { prompt, provider, model, skill, cwd, event
143
151
  } catch (_) {}
144
152
  }
145
153
 
154
+ function mergeHookExtras(messages, r, tag) {
155
+ if (!r) return messages
156
+ const e = []
157
+ if (r.systemMessage) e.push({ role: 'system', content: '[hook:' + tag + '] ' + r.systemMessage })
158
+ if (r.additionalContext) e.push({ role: 'system', content: r.additionalContext })
159
+ return e.length ? [...messages, ...e] : messages
160
+ }
161
+
146
162
  export async function runTurn({ prompt, messages = [], model, provider, callLLM, enabledToolsets, disabledToolsets, maxIterations = 90, timeoutMs = 30000, cwd, skill, witnessPath } = {}) {
147
- const events = []
148
- const initMessages = [...messages]
149
- const systemParts = []
150
- if (cwd) systemParts.push(`Working directory: ${cwd}. Always pass cwd="${cwd}" to bash tool calls. When reading or writing files use paths relative to this directory or absolute paths under it.`)
151
- if (skill) {
152
- const h = await bootHost()
153
- const skillDef = h.pi.skills.get(skill)
154
- if (skillDef?.content) systemParts.push('Skill context:\n' + skillDef.content)
155
- }
156
- if (systemParts.length > 0) initMessages.unshift({ role: 'user', content: systemParts.join('\n\n') })
163
+ const events = []; const h = await bootHost()
164
+ await h.hooks.invoke('onSessionStart', { prompt, model, provider, skill, cwd })
165
+ let initMessages = [...messages]; const sysParts = []
166
+ if (cwd) sysParts.push(`Working directory: ${cwd}. Always pass cwd="${cwd}" to bash tool calls. When reading or writing files use paths relative to this directory or absolute paths under it.`)
167
+ if (skill) { const sd = h.pi.skills.get(skill); if (sd?.content) sysParts.push('Skill context:\n' + sd.content) }
168
+ if (sysParts.length) initMessages.unshift({ role: 'user', content: sysParts.join('\n\n') })
169
+ const inbound = await h.hooks.invoke('onMessageInbound', { content: prompt })
170
+ if (inbound?.behavior === 'block') { await h.hooks.invoke('onSessionEnd', { reason: 'prompt_blocked' }); return { messages: initMessages, result: null, error: 'prompt blocked by plugsdk hook: ' + (inbound.reason || 'denied'), iterations: 0 } }
171
+ initMessages = mergeHookExtras(initMessages, inbound, 'onMessageInbound')
157
172
  const machine = createAgentMachine({ model, provider, callLLM, enabledToolsets, disabledToolsets, maxIterations, events })
158
- const actor = createActor(machine, { input: { messages: initMessages } })
159
- actor.start()
160
- actor.send({ type: 'SUBMIT', prompt })
173
+ const actor = createActor(machine, { input: { messages: initMessages } }); actor.start(); actor.send({ type: 'SUBMIT', prompt })
161
174
  return await new Promise((resolve, reject) => {
162
175
  const t = setTimeout(() => { try { actor.stop() } catch {} reject(new Error('agent turn timeout')) }, timeoutMs)
163
- actor.subscribe(snap => {
164
- if (snap.status === 'done') {
165
- clearTimeout(t)
166
- const errorStack = snap.output?.error ? (events.find(e => e.type === 'llm_call' && !e.ok)?.stack || null) : null
167
- writeTrajectory(snap.output, { prompt, provider, model, skill, cwd, events, errorStack, witnessPath }).finally(() => resolve(snap.output))
168
- }
176
+ actor.subscribe(snap => { if (snap.status !== 'done') return; clearTimeout(t)
177
+ ;(async () => {
178
+ const out = snap.output
179
+ const outbound = await h.hooks.invoke('onMessageOutbound', { content: out?.result || '' })
180
+ if (outbound?.systemMessage || outbound?.additionalContext) out.messages = mergeHookExtras(out.messages || [], outbound, 'onMessageOutbound')
181
+ await h.hooks.invoke('onSessionEnd', { reason: out?.error ? 'error' : 'ok', iterations: out?.iterations })
182
+ const errorStack = out?.error ? (events.find(e => e.type === 'llm_call' && !e.ok)?.stack || null) : null
183
+ await writeTrajectory(out, { prompt, provider, model, skill, cwd, events, errorStack, witnessPath }); resolve(out)
184
+ })().catch(reject)
169
185
  })
170
186
  })
171
187
  }
172
188
 
189
+ export async function invokeCompactHooks({ trigger = 'auto', messages = [] } = {}) {
190
+ const h = await bootHost()
191
+ const pre = await h.hooks.invoke('onPreCompact', { trigger, messages })
192
+ if (pre?.behavior === 'block') return { skipped: true, reason: pre.reason || 'blocked' }
193
+ return { pre, post: async (summary) => h.hooks.invoke('onPostCompact', { trigger, messages, summary }) }
194
+ }
195
+
@@ -1,98 +1,52 @@
1
+ // Upstream model enumeration lives in acptoapi. This module is a thin shim
2
+ // over GET /v1/models so freddie has zero direct vendor connectivity.
1
3
  import { createRequire } from 'module'
2
- import { resolveKey } from './credential_sources.js'
4
+ import { getAcptoapiUrl } from './acptoapi-bridge.js'
3
5
  import { saveConfigValue, getConfigValue } from '../config.js'
4
6
  import { logger } from '../observability/log.js'
5
7
 
6
8
  const _require = createRequire(import.meta.url)
7
- const { createModelProber } = _require('acptoapi/lib/model-prober')
8
- const { BRANDS } = _require('acptoapi/lib/openai-brands')
9
+ const _sdk = _require('acptoapi')
9
10
  const log = logger('model-discovery')
10
11
 
11
- const EXTRA = {
12
- anthropic: { url: 'https://api.anthropic.com/v1/models', envName: 'ANTHROPIC_API_KEY', auth: k => ({ 'x-api-key': k, 'anthropic-version': '2023-06-01' }), pick: j => (j.data || []).map(m => m.id) },
13
- gemini: { url: 'https://generativelanguage.googleapis.com/v1beta/models', envName: 'GOOGLE_API_KEY', keyParam: 'key', auth: () => ({}), pick: j => (j.models || []).map(m => (m.name || '').replace(/^models\//, '')).filter(Boolean) },
14
- ollama: { url: 'http://localhost:11434/api/tags', envName: null, auth: () => ({}), pick: j => (j.models || []).map(m => m.name || m.model).filter(Boolean) },
15
- }
16
- const ACP_BACKENDS = {
17
- kilo: { url: 'http://localhost:4780/session', staticModels: ['x-ai/grok-code-fast-1:optimized:free'] },
18
- opencode: { url: 'http://localhost:4790/session', staticModels: ['minimax-m2.5-free'] },
19
- }
20
- const CLI_BACKENDS = {
21
- 'claude-cli': { models: ['claude-haiku-4-5', 'claude-sonnet-4-6', 'claude-opus-4-7', 'haiku', 'sonnet', 'opus'] },
22
- }
23
-
24
- const prober = createModelProber()
25
-
26
- async function probeBrand(provider) {
27
- const resolved = await resolveKey(provider).catch(() => ({ value: null }))
28
- const brand = BRANDS[provider]
29
- const envKey = brand?.envKey
30
- const key = resolved.value || (envKey ? process.env[envKey] : undefined)
31
- if (!key) return { provider, error: 'no_key' }
32
- try {
33
- const r = await prober.probe(provider, key)
34
- if (r.error) return { provider, error: r.error }
35
- return { provider, models: r.models || [], last_ok_at: r.ts }
36
- } catch (e) { return { provider, error: String(e.message || e) } }
37
- }
38
-
39
- async function probeExtra(provider) {
40
- const ep = EXTRA[provider]
41
- if (!ep) return { provider, error: 'no_extra_endpoint' }
42
- const resolved = await resolveKey(provider).catch(() => ({ value: null }))
43
- const key = resolved.value || (ep.envName ? process.env[ep.envName] : undefined)
44
- if (!key && provider !== 'ollama') return { provider, error: 'no_key' }
45
- try {
46
- const url = ep.keyParam ? `${ep.url}?${ep.keyParam}=${encodeURIComponent(key)}` : ep.url
47
- const headers = ep.auth(key)
48
- const res = await fetch(url, { method: 'GET', headers, signal: AbortSignal.timeout(8000) })
49
- if (!res.ok) { const t = await res.text(); return { provider, error: `${res.status}: ${t.slice(0, 200)}` } }
50
- const json = await res.json()
51
- return { provider, models: ep.pick(json) || [], last_ok_at: Date.now() }
52
- } catch (e) { return { provider, error: String(e.message || e) } }
53
- }
54
-
55
- async function probeAcp(provider) {
56
- const b = ACP_BACKENDS[provider]
57
- try {
58
- const r = await fetch(b.url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}', signal: AbortSignal.timeout(2000) })
59
- if (!r.ok) return { provider, error: `${r.status}`, models: b.staticModels }
60
- return { provider, models: b.staticModels, last_ok_at: Date.now() }
61
- } catch (e) { return { provider, error: String(e.message || e), models: b.staticModels } }
62
- }
63
-
64
- async function probeCli(provider) {
65
- if (provider !== 'claude-cli') return { provider, error: 'unknown_cli', models: [] }
66
- try {
67
- const { spawn } = await import('node:child_process')
68
- const ok = await new Promise(res => {
69
- const p = spawn('claude', ['--version'], { stdio: 'ignore', shell: false })
70
- const t = setTimeout(() => { p.kill(); res(false) }, 3000)
71
- p.on('exit', c => { clearTimeout(t); res(c === 0) })
72
- p.on('error', () => { clearTimeout(t); res(false) })
73
- })
74
- if (!ok) return { provider, error: 'claude_cli_not_available', models: [] }
75
- return { provider, models: CLI_BACKENDS['claude-cli'].models, last_ok_at: Date.now() }
76
- } catch (e) { return { provider, error: String(e.message || e), models: [] } }
77
- }
12
+ const NON_KEY_PROVIDERS = ['claude-cli', 'kilo', 'opencode', 'ollama']
78
13
 
79
14
  export function listKnownProviders() {
80
- return [...Object.keys(BRANDS), ...Object.keys(EXTRA), ...Object.keys(ACP_BACKENDS), ...Object.keys(CLI_BACKENDS)]
15
+ const cached = getConfigValue('agent.discovered_models', {}) || {}
16
+ const set = new Set([...Object.keys(cached), ...Object.keys(_sdk.PROVIDER_KEYS || {}), ...NON_KEY_PROVIDERS])
17
+ return [...set]
81
18
  }
82
19
 
83
20
  export async function discoverModels({ provider } = {}) {
84
- const providers = provider ? [provider] : listKnownProviders()
85
- const results = await Promise.all(providers.map(p => {
86
- if (BRANDS[p]) return probeBrand(p)
87
- if (EXTRA[p]) return probeExtra(p)
88
- if (ACP_BACKENDS[p]) return probeAcp(p)
89
- if (CLI_BACKENDS[p]) return probeCli(p)
90
- return Promise.resolve({ provider: p, error: 'unknown_provider' })
91
- }))
92
- const byProvider = {}
93
- for (const r of results) byProvider[r.provider] = r
94
- log.info('discovered', { count: results.length, ok: results.filter(r => !r.error).length })
95
- return byProvider
21
+ const base = getAcptoapiUrl().replace(/\/v1\/?$/, '')
22
+ try {
23
+ const r = await fetch(base + '/v1/models', {
24
+ headers: { authorization: 'Bearer none' },
25
+ signal: AbortSignal.timeout(10000),
26
+ })
27
+ if (!r.ok) {
28
+ const text = await r.text()
29
+ log.warn('discover failed', { status: r.status, body: text.slice(0, 200) })
30
+ return {}
31
+ }
32
+ const json = await r.json()
33
+ const byProvider = {}
34
+ for (const m of (json.data || [])) {
35
+ const id = m.id || ''
36
+ const slash = id.indexOf('/')
37
+ if (slash <= 0) continue
38
+ const p = id.slice(0, slash)
39
+ const modelName = id.slice(slash + 1)
40
+ if (provider && p !== provider) continue
41
+ byProvider[p] = byProvider[p] || { provider: p, models: [], last_ok_at: Date.now() }
42
+ byProvider[p].models.push(modelName)
43
+ }
44
+ log.info('discovered', { count: Object.keys(byProvider).length })
45
+ return byProvider
46
+ } catch (e) {
47
+ log.warn('discover error', { error: e.message })
48
+ return {}
49
+ }
96
50
  }
97
51
 
98
52
  export async function discoverAndPersist({ provider } = {}) {
@@ -101,7 +55,6 @@ export async function discoverAndPersist({ provider } = {}) {
101
55
  const merged = { ...existing }
102
56
  for (const [p, r] of Object.entries(result)) {
103
57
  if (!r.error) merged[p] = { models: r.models, last_ok_at: r.last_ok_at }
104
- else merged[p] = { ...(existing[p] || {}), models: r.models || (existing[p]?.models) || [], error: r.error, last_error_at: Date.now() }
105
58
  }
106
59
  saveConfigValue('agent.discovered_models', merged)
107
60
  return result
@@ -0,0 +1,23 @@
1
+ // Browser entry for freddie. Polymorphic with the Node CLI.
2
+ // node:* imports are expected to be satisfied by the host environment shims
3
+ // (thebird provides them via docs/vendor/esm/node/ + docs/shell-node-*.js).
4
+ //
5
+ // This entry deliberately re-exports only the browser-safe surface needed
6
+ // to embed freddie as an agent in a web OS: the xstate-driven agent machine,
7
+ // the host bootstrapper, and configuration defaults. It avoids re-exporting
8
+ // CLI/TUI/dashboard/MCP/ACP server code which pulls in commander, express,
9
+ // child_process, and other Node-only subsystems.
10
+
11
+ export { bootHost, host, resetHostForTests } from '../host/index.js'
12
+ export { createAgentMachine, runTurn } from '../agent/machine.js'
13
+ export { createActor, createMachine, assign, fromPromise, waitFor } from 'xstate'
14
+
15
+ // Re-export config defaults under the documented browser name.
16
+ import { DEFAULT_CONFIG } from '../config.js'
17
+ export const FREDDIE_DEFAULT_CONFIG = DEFAULT_CONFIG
18
+ export { DEFAULT_CONFIG }
19
+
20
+ // Optional extras that are browser-friendly when their node:* imports are shimmed.
21
+ export { buildContext, blocksToSystemMessage, ContextPlugins } from '../context/engine.js'
22
+ export { listSkills, findSkill, skillAsUserMessage } from '../skills/index.js'
23
+ export { log, logger } from '../observability/log.js'
@@ -16,6 +16,7 @@ export const HOOK_NAMES = [
16
16
  'onSessionStart', 'onSessionEnd',
17
17
  'onTurnStart', 'onTurnEnd',
18
18
  'onMessageInbound', 'onMessageOutbound',
19
+ 'onPreCompact', 'onPostCompact',
19
20
  ]
20
21
 
21
22
  export const FREDDIE_TO_SDK_HOOK = {
@@ -25,6 +26,8 @@ export const FREDDIE_TO_SDK_HOOK = {
25
26
  onSessionEnd: HookType.SESSION_END,
26
27
  onMessageInbound: HookType.PROMPT_SUBMIT,
27
28
  onMessageOutbound: HookType.STOP,
29
+ onPreCompact: HookType.PRE_COMPACT,
30
+ onPostCompact: HookType.POST_COMPACT,
28
31
  }
29
32
 
30
33
  export const FREDDIE_TO_NATIVE_HOOK = {
@@ -34,6 +37,8 @@ export const FREDDIE_TO_NATIVE_HOOK = {
34
37
  onSessionEnd: 'SessionEnd',
35
38
  onMessageInbound: 'UserPromptSubmit',
36
39
  onMessageOutbound: 'Stop',
40
+ onPreCompact: 'PreCompact',
41
+ onPostCompact: 'PostCompact',
37
42
  }
38
43
 
39
44
  export function validatePlugin(p) {
@@ -48,6 +48,8 @@ function ccPayloadFor(name, payload) {
48
48
  return { tool_name: payload?.name, tool_input: payload?.args || payload?.input, tool_response: payload?.result }
49
49
  if (name === 'onMessageInbound' || name === 'onMessageOutbound')
50
50
  return { prompt: payload?.content || payload?.text || '' }
51
+ if (name === 'onPreCompact' || name === 'onPostCompact')
52
+ return { trigger: payload?.trigger || 'auto', messages_count: payload?.messages?.length ?? 0, summary: payload?.summary ?? null }
51
53
  return payload || {}
52
54
  }
53
55
 
@@ -90,12 +92,17 @@ export function makeHooksRegistry(ccHost) {
90
92
  let cur = payload
91
93
  for (const fn of reg2[name] || []) cur = (await fn(cur)) ?? cur
92
94
  const native = FREDDIE_TO_NATIVE_HOOK[name]
93
- if (native && ccHost.plugins().length) {
95
+ if (native && ccHost.plugins().length && !process.env.FREDDIE_DISABLE_CC_HOOKS) {
94
96
  const r = await ccHost.dispatch(native, ccPayloadFor(name, cur))
95
- if (r.decision === 'block') return { ...cur, behavior: 'block', reason: r.reason }
97
+ const extras = {}
98
+ if (typeof r.systemMessage === 'string' && r.systemMessage.length) extras.systemMessage = r.systemMessage
99
+ const addCtx = r.hookSpecificOutput?.additionalContext
100
+ if (typeof addCtx === 'string' && addCtx.length) extras.additionalContext = addCtx
101
+ if (r.decision === 'block') return { ...cur, ...extras, behavior: 'block', reason: r.reason }
96
102
  const pd = r.hookSpecificOutput?.permissionDecision
97
- if (pd === 'deny') return { ...cur, behavior: 'block', reason: r.hookSpecificOutput?.permissionDecisionReason || 'denied' }
98
- if (r.hookSpecificOutput?.updatedInput) return { ...cur, ...r.hookSpecificOutput.updatedInput }
103
+ if (pd === 'deny') return { ...cur, ...extras, behavior: 'block', reason: r.hookSpecificOutput?.permissionDecisionReason || 'denied' }
104
+ if (r.hookSpecificOutput?.updatedInput) return { ...cur, ...extras, ...r.hookSpecificOutput.updatedInput }
105
+ if (Object.keys(extras).length) return { ...cur, ...extras }
99
106
  }
100
107
  return cur
101
108
  },
@@ -127,6 +134,7 @@ export function makeCcLoaders(ccHost, env) {
127
134
  }
128
135
  return ccHost.plugins().length
129
136
  }
137
+ const CC_EXCLUDE = new Set(['gm-cc'])
130
138
  async function loadCcFromNodeModules(startDir) {
131
139
  const seen = new Set(ccHost.plugins().map(p => p.root))
132
140
  let cur = path.resolve(startDir)
@@ -138,7 +146,7 @@ export function makeCcLoaders(ccHost, env) {
138
146
  ? fs.readdirSync(path.join(nm, entry.name), { withFileTypes: true }).filter(e => e.isDirectory()).map(e => path.join(nm, entry.name, e.name))
139
147
  : [path.join(nm, entry.name)]
140
148
  for (const d of dirs) {
141
- if (seen.has(d) || !isCcPluginDir(d)) continue
149
+ if (seen.has(d) || !isCcPluginDir(d) || CC_EXCLUDE.has(path.basename(d))) continue
142
150
  seen.add(d); await useCcDir(d)
143
151
  }
144
152
  }
package/src/web/server.js CHANGED
@@ -12,8 +12,8 @@ export async function createDashboard({ port = 0 } = {}) {
12
12
  app.use(express.static(__dirname))
13
13
  const fromNodeModules = path.join(__dirname, '..', '..', 'node_modules', 'anentrypoint-design', 'dist')
14
14
  app.use('/vendor/anentrypoint-design', express.static(fromNodeModules))
15
- const nmDesktop = path.join(__dirname, '..', '..', 'node_modules', 'anentrypoint-design', 'src', 'desktop')
16
- app.use('/vendor/anentrypoint-design/desktop', express.static(nmDesktop))
15
+ const nmKitsOs = path.join(__dirname, '..', '..', 'node_modules', 'anentrypoint-design', 'src', 'kits', 'os')
16
+ app.use('/vendor/anentrypoint-design/kits/os', express.static(nmKitsOs))
17
17
  for (const r of host.gui.routes.list()) {
18
18
  const verb = r.method.toLowerCase()
19
19
  if (typeof app[verb] === 'function') app[verb](r.path, r.handler)
@@ -1,26 +0,0 @@
1
- import { createRequire } from 'module'
2
- import path from 'path'
3
- import fs from 'fs'
4
- import { loadClaudePlugin } from 'plugsdk'
5
-
6
- const _require = createRequire(import.meta.url)
7
- const gmCcBase = path.dirname(_require.resolve('gm-cc/package.json'))
8
-
9
- export default {
10
- name: 'gm-cc',
11
- surfaces: 'pi',
12
- register({ pi }) {
13
- if (!fs.existsSync(path.join(gmCcBase, 'skills'))) return
14
- const cc = loadClaudePlugin(gmCcBase)
15
- for (const s of cc.skills) {
16
- pi.skills.register({
17
- name: 'gm:' + s.name,
18
- description: s.fields.description || '',
19
- content: s.body,
20
- source: 'gm-cc',
21
- frontmatter: s.fields,
22
- file: s.file,
23
- })
24
- }
25
- },
26
- }
@@ -1,13 +0,0 @@
1
- import { createRequire } from 'module'
2
- const _require = createRequire(import.meta.url)
3
- const _sdk = _require('acptoapi')
4
-
5
- export const isAvailable = _sdk.isAvailable
6
- export const markFailed = _sdk.markFailed
7
- export const markOk = _sdk.markOk
8
- export const resetAvailability = _sdk.resetAvailability
9
- export const getStatus = _sdk.getStatus
10
- export const probe = _sdk.probe
11
- export const startSampler = _sdk.startSampler
12
- export const stopSampler = _sdk.stopSampler
13
- export const createSampler = _sdk.createSampler