freddie 0.0.76 → 0.0.78
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 +8 -3
- package/CHANGELOG.md +23 -0
- package/README.md +2 -0
- package/package.json +3 -2
- package/plugins/gui-chat/plugin.js +10 -3
- package/plugins/gui-profiles-commands-health/plugin.js +26 -0
- package/src/agent/credential_sources.js +20 -4
- package/src/agent/llm_resolver.js +111 -9
- package/src/agent/machine.js +11 -2
- package/src/agent/model-sampler.js +13 -0
- package/src/config.js +3 -2
- package/src/sessions.js +11 -5
- package/src/web/app.js +53 -69
- package/src/web/index.html +4 -4
- package/src/web/routes.js +2 -0
- package/src/web/server.js +2 -7
- package/src/web/state.js +84 -0
- package/src/web/vendor/anentrypoint-design/desktop/freddie-dashboard.css +0 -32
- package/src/web/vendor/anentrypoint-design/desktop/freddie-dashboard.js +0 -405
- package/src/web/vendor/anentrypoint-design/desktop/icons.js +0 -17
- package/src/web/vendor/anentrypoint-design/desktop/index.js +0 -3
- package/src/web/vendor/anentrypoint-design/desktop/launcher.css +0 -44
- package/src/web/vendor/anentrypoint-design/desktop/shell.js +0 -187
- package/src/web/vendor/anentrypoint-design/desktop/theme.css +0 -409
- package/src/web/vendor/anentrypoint-design/desktop/validate.css +0 -19
- package/src/web/vendor/anentrypoint-design/desktop/wm.css +0 -115
- package/src/web/vendor/anentrypoint-design/dist/247420.app.js +0 -3
- package/src/web/vendor/anentrypoint-design/dist/247420.css +0 -2359
- package/src/web/vendor/anentrypoint-design/dist/247420.js +0 -184
package/AGENTS.md
CHANGED
|
@@ -242,7 +242,10 @@ All 21 named integration tests in `test.js` pass (exit 0). Subsystem coverage:
|
|
|
242
242
|
## LLM backends and acptoapi
|
|
243
243
|
|
|
244
244
|
- **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.
|
|
245
|
-
- **
|
|
245
|
+
- **acptoapi dep pattern** (2026-05-10) — `package.json` pins `"acptoapi": "file:../acptoapi"` (same pattern as `anentrypoint-design`). Always tracks local SDK without publish cycles. CJS/ESM boundary bridged via `createRequire(import.meta.url)` in freddie ESM files that import acptoapi CJS exports.
|
|
246
|
+
- **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}`.
|
|
247
|
+
- **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`.
|
|
248
|
+
- **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.
|
|
246
249
|
- **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).
|
|
247
250
|
|
|
248
251
|
## Pre-rename validation snapshot (2026-05-03)
|
|
@@ -261,10 +264,12 @@ All 12 test.js named groups passing: home+config+skin, sessions+FTS5, tools+tool
|
|
|
261
264
|
- 2026-05-03 (session 3): Ingested libsql-async-debt-class into AGENTS.md Substrate gotchas (sessions.js + cron/scheduler.js async callsites; silent TypeError class; test.js passes while CLI broken). rs-learn store still unavailable (exec:memorize/exec:recall not on PATH). 0 migration audit items queried. 1 new fact added.
|
|
262
265
|
- 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.
|
|
263
266
|
- 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.
|
|
267
|
+
- 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.
|
|
268
|
+
- 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.
|
|
264
269
|
|
|
265
270
|
## Dashboard web UI caveats
|
|
266
271
|
|
|
267
|
-
- **anentrypoint-design
|
|
272
|
+
- **anentrypoint-design source/dist pattern** (2026-05-10) — No vendor dir in freddie (`src/web/vendor/` removed, commit 52d0732). `server.js` always serves SDK from `node_modules/anentrypoint-design/dist/`. freddie's `package.json` pins `"anentrypoint-design": "file:../anentrypoint-design"` so `npm install` symlinks directly to the live source — no publish cycle needed. Dist is rebuilt by CI on push to the SDK repo; rebuild locally after SDK edits via `node scripts/build.mjs` in `C:/dev/anentrypoint-design` (emits dist/247420.js ~441KB + 247420.css; build ~150ms; warning "[247420] missing css: vendor/rippleui-1.12.1.css" is benign). Skip rebuild before using a new component and the pageerror "component is not a function" kills app mount silently.
|
|
268
273
|
- **Live page rerender caveat** — AppState.body caching (page computed once at navigation, body saved) breaks for live routes like #/chat where AppState is mutated mid-flight (SSE pushes new messages). Fix: detect live routes in rerender(), recompute body: `if (AppState.hash === '#/chat') { Promise.resolve(PAGES['#/chat']()).then(b => { AppState.body = b; _mount() }); return }`. Any future live-streaming pages (cron output, traces) need the same treatment.
|
|
269
274
|
- **libuv spawn caveat** — Spawning createDashboard() from exec:nodejs and keeping process alive triggers libuv UV_HANDLE_CLOSING crash on shutdown. Reliable alternative: boot via `node bin/freddie.js dashboard --port <port>`. Liveness checks: exec:browser → page.goto → window.__debug.dashboard() returns {booted, ts, framework, route}; window.__debug.chat() exposes {messages, streaming, draft}; window.__debug.sendChat(text) drives round-trips.
|
|
270
275
|
- **anentrypoint-design theme token descendant selector fix** (2026-05-04) — SDK CSS scopes light/dark theme variables under descendant selectors `.ds-247420 [data-theme="dark"]` and `.ds-247420 [data-theme="light"]`. Placing both `class="ds-247420"` and `data-theme="dark"` on the same node (e.g. `<html>`) breaks theming: the descendant selector does NOT match when both attributes are on the same element. Fix: scope `class="ds-247420"` on `<html>` and `data-theme="dark"` on `<body>`. Theme toggle must write to `document.body`, not `document.documentElement`. Browser-witnessed commit 17dfce0: pre-fix dark/light backgrounds identical (rgb 26,27,30); post-fix dark=rgb(26,27,30), light=rgb(245,240,228). Symptom: toggle theme in dashboard, page background does not change.
|
|
@@ -273,7 +278,7 @@ All 12 test.js named groups passing: home+config+skin, sessions+FTS5, tools+tool
|
|
|
273
278
|
|
|
274
279
|
- **Structured-YAML rendering** — `website/theme.mjs` (164L) renders structured YAML via 247420 design vocabulary, not raw markdown. Consumes `page.hero` (heading/subheading/accent/body/badges/ctas), `page.sections[]` (rotating rail color green→purple→mascot→sun→flame→sky by section index, optional `lede` + per-item `benefit` italic), `page.examples[]` (railed link list with mono numeric ranks + ↗ glyph). Falls back to `page.body` markdown for prose. Style block inlined so rail/dot/chip/btn classes work without ds-247420 SDK CSS loading first. To get a specific rail color, reorder sections. Prefer enriching hero+sections+examples over expanding body markdown; copy existing YAML structure as template for new pages.
|
|
275
280
|
- **YAML colon-space trap** — In `website/content/pages/*.yaml`, any value containing `: ` outside backticks (e.g. `[linux, macos, windows]`, `requiresEnv: ['MY_KEY']` code snippets) MUST be double-quoted. The parser otherwise interprets the embedded colon as a mapping and the file fails to load. Hit twice: tools.yaml line 72, skills.yaml line 40. Fix is wrapping the whole value in `"..."`.
|
|
276
|
-
- **SSR innerHTML injection beats client dispatch** — anentrypoint-design
|
|
281
|
+
- **SSR innerHTML injection beats client dispatch** — anentrypoint-design exposes Hero/HomeView/Panel/Row/Section/WorksList (and more), but pre-mounted SSR injection via innerHTML is more reliable than dispatching components client-side at build — avoids depending on SDK loading before the static HTML paints. Emitted HTML carries rail/dot/chip/btn classes with inline styles to be self-sufficient.
|
|
277
282
|
|
|
278
283
|
## Residual complement (NOT ported this session)
|
|
279
284
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
- `src/agent/llm_resolver.js`: fixed tool-calling for all openai-compat providers (groq, mistral, cerebras, openrouter, nvidia, etc.) — `sdk.chat()` was internally using `from:'openai'` format which stripped `url` and `apiKey` before sending to the provider, causing "Failed to parse URL from undefined"; replaced with direct `fetch()` for openai-compat, bypassing the sdk format pipeline entirely
|
|
5
|
+
- `src/agent/llm_resolver.js`: tool schemas (from `getEnabledToolSchemas`) were never passed to the LLM API — `tools: undefined` was hardcoded; now converts freddie tool schemas to OpenAI `{type:'function', function:{...}}` format and passes them in the request body
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- `src/web/vendor/anentrypoint-design/` removed; `server.js` now always serves SDK from `node_modules`; `file:../anentrypoint-design` dep keeps node_modules in sync with live SDK source
|
|
9
|
+
- `src/web/app.js` refactored from 548L monolith to 59L shim; state/host helpers extracted to `src/web/state.js` (131L); all page components extracted to `src/web/routes.js` (289L)
|
|
10
|
+
- `acptoapi` dep switched to `file:../acptoapi` — always tracks local SDK, same pattern as `anentrypoint-design`
|
|
11
|
+
- `src/agent/model-sampler.js` is now a thin re-export shim; sampler logic lives in acptoapi `lib/sampler.js`
|
|
12
|
+
- `src/agent/llm_resolver.js` simplified to 95 lines: `PROVIDER_KEYS`/`DEFAULTS` imported from acptoapi, `OPENAI_COMPAT` block removed, auto-scan uses acptoapi `buildAutoChain()`, preference failover calls acptoapi `chat()`
|
|
13
|
+
- `resolveCallLLM`: acptoapi is now priority 1 (before direct API keys and preference list); direct-key providers remain as fallback when acptoapi is unreachable
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- Per-provider model list probing: `POST /api/providers/:name/probe` fetches live model list; `/api/providers` includes cached `models`/`modelsError`; models page shows model list per provider with probe-all button
|
|
17
|
+
- `hasKey()` fixed to use `resolveKey()` 4-source chain (env → auth-store → freddie.env → acptoapi.env); previously only checked `process.env`, causing keys stored via auth-store to be invisible to the LLM resolver
|
|
18
|
+
- Expanded LLM provider support: cerebras, google, mistral, codestral, cloudflare-workers-ai, xai, zai, opencode, nvidia, sambanova, qwen (15 providers total)
|
|
19
|
+
- `src/agent/model-sampler.js`: background availability sampler with exponential backoff (30s→60s→120s→240s→480s cap)
|
|
20
|
+
- `agent.model_preference` config key (ordered list of {provider, model} objects) for user-defined failover priority
|
|
21
|
+
- `resolveCallLLM` now walks preference list, skips unavailable providers via sampler, marks failed providers on error
|
|
22
|
+
- Config schema version bumped to 2 with automatic migration
|
|
23
|
+
|
|
1
24
|
# Changelog
|
|
2
25
|
|
|
3
26
|
## [0.0.51] - 2026-05-04
|
package/README.md
CHANGED
|
@@ -134,6 +134,8 @@ v0.1.1 complete and witnessed: 12/12 named tests passing, dashboard + website bo
|
|
|
134
134
|
- **Full context compressor** (`src/agent/compress/*`) with handoff-framed summary prefix, structured summarizer prompt, head/middle/tail policy, tool-output pre-pruning, summary-budget ratio, iterative summary update, and 600s failure cooldown
|
|
135
135
|
- **Documentation site** at `website/` powered by `flatspace` (NOT docusaurus). Build with `cd website && node ../node_modules/flatspace/bin/flatspace.js build` — output to `website/docs/` for GitHub Pages.
|
|
136
136
|
|
|
137
|
+
**LLM providers**: anthropic, openai, groq, openrouter, cerebras, google, mistral, codestral, cloudflare-workers-ai, xai, zai, opencode, nvidia, sambanova, qwen — plus acptoapi localhost bridge. Set `agent.model_preference` in `~/.freddie/config.yaml` for ordered failover with exponential backoff.
|
|
138
|
+
|
|
137
139
|
What's not in the box yet (residual, see AGENTS.md): real credentials per platform / memory backend; modal / daytona / singularity environments; bedrock / codex provider adapters.
|
|
138
140
|
|
|
139
141
|
## Testing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freddie",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.78",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Open JS agent harness built on pi-mono, floosie, xstate, and anentrypoint-design",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"plugsdk": "^1.0.15",
|
|
27
27
|
"xstate": "^5.31.0",
|
|
28
28
|
"zod": "^4.0.0",
|
|
29
|
-
"anentrypoint-design": "^0.0.
|
|
29
|
+
"anentrypoint-design": "^0.0.67",
|
|
30
|
+
"acptoapi": "^1.0.48"
|
|
30
31
|
},
|
|
31
32
|
"optionalDependencies": {
|
|
32
33
|
"@libsql/darwin-arm64": "0.3.19",
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { runTurn } from '../../src/agent/machine.js'
|
|
2
|
+
import { createSession } from '../../src/sessions.js'
|
|
2
3
|
export default {
|
|
3
4
|
name: 'gui-chat', surfaces: 'gui',
|
|
4
5
|
register({ gui }) {
|
|
5
6
|
gui.route('POST', '/api/chat', async (req, res) => {
|
|
6
|
-
const { prompt, sessionId = null } = req.body || {}
|
|
7
|
+
const { prompt, sessionId: incomingSessionId = null, cwd, skill, provider, model } = req.body || {}
|
|
7
8
|
if (!prompt) return res.status(400).json({ error: 'prompt required' })
|
|
8
9
|
res.setHeader('Content-Type', 'text/event-stream')
|
|
9
10
|
res.setHeader('Cache-Control', 'no-cache')
|
|
10
11
|
res.setHeader('Connection', 'keep-alive')
|
|
11
12
|
const send = (event, data) => res.write('event: ' + event + '\ndata: ' + JSON.stringify(data) + '\n\n')
|
|
13
|
+
let sessionId = incomingSessionId
|
|
14
|
+
if (!sessionId) {
|
|
15
|
+
try {
|
|
16
|
+
sessionId = await createSession({ platform: 'web', title: prompt.slice(0, 80), cwd: cwd || null, skill: skill || null, model: model || null })
|
|
17
|
+
} catch (_) { sessionId = null }
|
|
18
|
+
}
|
|
12
19
|
send('start', { ts: Date.now(), sessionId })
|
|
13
20
|
try {
|
|
14
|
-
const out = await runTurn({ prompt, timeoutMs:
|
|
21
|
+
const out = await runTurn({ prompt, timeoutMs: 120000, cwd, skill, provider, model })
|
|
15
22
|
if (out.error) { send('error', { error: out.error }); res.end(); return }
|
|
16
23
|
for (const m of out.messages) send('message', m)
|
|
17
|
-
send('done', { result: out.result || '', iterations: out.iterations })
|
|
24
|
+
send('done', { result: out.result || '', iterations: out.iterations, sessionId })
|
|
18
25
|
} catch (e) { send('error', { error: String(e.message || e) }) }
|
|
19
26
|
res.end()
|
|
20
27
|
})
|
|
@@ -1,11 +1,37 @@
|
|
|
1
|
+
import { createRequire } from 'module'
|
|
1
2
|
import { listAllProfiles } from '../../src/commands/profile.js'
|
|
2
3
|
import { COMMAND_REGISTRY } from '../../src/commands/registry.js'
|
|
3
4
|
import { getFreddieHome } from '../../src/home.js'
|
|
5
|
+
import { PROVIDER_KEYS, DEFAULTS } from '../../src/agent/llm_resolver.js'
|
|
6
|
+
import { getStatus } from '../../src/agent/model-sampler.js'
|
|
7
|
+
import { resolveKey } from '../../src/agent/credential_sources.js'
|
|
8
|
+
const _require = createRequire(import.meta.url)
|
|
9
|
+
const { probeModels, getCachedModels } = _require('acptoapi')
|
|
4
10
|
export default {
|
|
5
11
|
name: 'gui-profiles-commands-health', surfaces: 'gui',
|
|
6
12
|
register({ gui }) {
|
|
7
13
|
gui.route('GET', '/api/profiles', (_, res) => res.json(listAllProfiles()))
|
|
8
14
|
gui.route('GET', '/api/commands', (_, res) => res.json(COMMAND_REGISTRY))
|
|
9
15
|
gui.route('GET', '/api/health', (_, res) => res.json({ ok: true, ts: Date.now(), freddie_home: getFreddieHome() }))
|
|
16
|
+
gui.route('GET', '/api/providers', async (_, res) => {
|
|
17
|
+
const status = getStatus()
|
|
18
|
+
const providers = await Promise.all(Object.keys(PROVIDER_KEYS).map(async name => {
|
|
19
|
+
const envKey = PROVIDER_KEYS[name]
|
|
20
|
+
const resolved = await resolveKey(name).catch(() => ({ value: null }))
|
|
21
|
+
const configured = !!(resolved.value || (envKey && process.env[envKey]))
|
|
22
|
+
const s = status.find(x => x.provider === name)
|
|
23
|
+
const available = configured && (s ? s.ok !== false : true)
|
|
24
|
+
const cached = getCachedModels(name)
|
|
25
|
+
return { name, configured, available, defaultModel: DEFAULTS[name] || '', models: cached?.models || null, modelsError: cached?.error || null }
|
|
26
|
+
}))
|
|
27
|
+
res.json(providers)
|
|
28
|
+
})
|
|
29
|
+
gui.route('POST', '/api/providers/:name/probe', async (req, res) => {
|
|
30
|
+
const { name } = req.params
|
|
31
|
+
const resolved = await resolveKey(name).catch(() => ({ value: null }))
|
|
32
|
+
if (!resolved.value) return res.status(400).json({ error: 'no key' })
|
|
33
|
+
const result = await probeModels(name, resolved.value)
|
|
34
|
+
res.json(result)
|
|
35
|
+
})
|
|
10
36
|
},
|
|
11
37
|
}
|
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
3
4
|
import { getFreddieHome } from '../home.js'
|
|
4
5
|
import { getAuthStore } from '../auth.js'
|
|
5
6
|
const ENV_MAP = { anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY', groq: 'GROQ_API_KEY', xai: 'XAI_API_KEY', openrouter: 'OPENROUTER_API_KEY', mistral: 'MISTRAL_API_KEY', deepseek: 'DEEPSEEK_API_KEY' }
|
|
7
|
+
function parseDotEnv(filePath, envKey) {
|
|
8
|
+
if (!fs.existsSync(filePath)) return null
|
|
9
|
+
const m = fs.readFileSync(filePath, 'utf8').match(new RegExp('^' + envKey + '=(.+)$', 'm'))
|
|
10
|
+
return m ? m[1].replace(/^["']|["']$/g, '') : null
|
|
11
|
+
}
|
|
12
|
+
function acptoApiEnvPath() {
|
|
13
|
+
try {
|
|
14
|
+
const req = createRequire(import.meta.url)
|
|
15
|
+
const pkgMain = req.resolve('acptoapi')
|
|
16
|
+
return path.join(path.dirname(pkgMain), '.env')
|
|
17
|
+
} catch { return null }
|
|
18
|
+
}
|
|
6
19
|
export async function resolveKey(provider) {
|
|
7
20
|
const env = ENV_MAP[provider] || (provider.toUpperCase() + '_API_KEY')
|
|
8
21
|
if (process.env[env]) return { source: 'env', value: process.env[env] }
|
|
9
22
|
const stored = await getAuthStore().getCredential(env)
|
|
10
23
|
if (stored?.value) return { source: 'auth-store', value: stored.value }
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
const frederickDotEnv = path.join(getFreddieHome(), '.env')
|
|
25
|
+
const fromFreddie = parseDotEnv(frederickDotEnv, env)
|
|
26
|
+
if (fromFreddie) return { source: 'dotenv', value: fromFreddie }
|
|
27
|
+
const acpPath = acptoApiEnvPath()
|
|
28
|
+
if (acpPath) {
|
|
29
|
+
const fromAcp = parseDotEnv(acpPath, env)
|
|
30
|
+
if (fromAcp) return { source: 'acptoapi-dotenv', value: fromAcp }
|
|
15
31
|
}
|
|
16
32
|
return { source: 'none', value: null }
|
|
17
33
|
}
|
|
@@ -1,22 +1,124 @@
|
|
|
1
|
+
import { createRequire } from 'module'
|
|
1
2
|
import { callLLM as acptoapiCall, isReachable as acptoapiReachable } from './acptoapi-bridge.js'
|
|
2
|
-
import {
|
|
3
|
+
import { isAvailable, markFailed, getStatus } from './model-sampler.js'
|
|
4
|
+
import { getConfigValue } from '../config.js'
|
|
5
|
+
import { resolveKey } from './credential_sources.js'
|
|
3
6
|
|
|
4
|
-
const
|
|
7
|
+
const _require = createRequire(import.meta.url)
|
|
8
|
+
const sdk = _require('acptoapi')
|
|
9
|
+
|
|
10
|
+
export const PROVIDER_KEYS = sdk.PROVIDER_KEYS
|
|
11
|
+
export const DEFAULTS = sdk.PROVIDER_DEFAULTS
|
|
12
|
+
|
|
13
|
+
function toOpenAITools(schemas) {
|
|
14
|
+
if (!schemas?.length) return undefined
|
|
15
|
+
return schemas.map(s => ({ type: 'function', function: { name: s.name, description: s.description || '', parameters: s.parameters || { type: 'object', properties: {} } } }))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function directOpenAICompatChat(url, apiKey, model, messages, tools) {
|
|
19
|
+
const body = { model, messages, ...(tools?.length ? { tools } : {}) }
|
|
20
|
+
const res = await fetch(url, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
|
|
23
|
+
body: JSON.stringify(body),
|
|
24
|
+
signal: AbortSignal.timeout(120000),
|
|
25
|
+
})
|
|
26
|
+
if (!res.ok) { const t = await res.text(); throw new Error(`${res.status} ${t.slice(0, 200)}`) }
|
|
27
|
+
return res.json()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function sdkChat(provider, model, input) {
|
|
31
|
+
const { resolveModel } = sdk
|
|
32
|
+
const r = resolveModel(`${provider}/${model}`)
|
|
33
|
+
const resolved = await resolveKey(provider).catch(() => ({ value: null }))
|
|
34
|
+
const apiKey = resolved.value || (r.env ? process.env[r.env] : undefined)
|
|
35
|
+
const openaiTools = toOpenAITools(input.tools)
|
|
36
|
+
let result
|
|
37
|
+
if (r.provider === 'openai-compat') {
|
|
38
|
+
result = await directOpenAICompatChat(r.url, apiKey, r.model, input.messages, openaiTools)
|
|
39
|
+
} else {
|
|
40
|
+
const { buffer: sdkBuffer } = sdk
|
|
41
|
+
result = await sdkBuffer({ from: null, to: 'openai', provider: r.provider, model: r.model, messages: input.messages, apiKey, ...(openaiTools ? { tools: openaiTools } : {}) })
|
|
42
|
+
}
|
|
43
|
+
const choice = result?.choices?.[0]?.message || {}
|
|
44
|
+
const content = typeof choice.content === 'string' ? choice.content : ''
|
|
45
|
+
const tool_calls = Array.isArray(choice.tool_calls)
|
|
46
|
+
? choice.tool_calls.map(tc => ({ id: tc.id, name: tc.function?.name, arguments: tryParseJson(tc.function?.arguments) }))
|
|
47
|
+
: []
|
|
48
|
+
return { content, tool_calls, raw: result }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function tryParseJson(s) { try { return typeof s === 'string' ? JSON.parse(s) : (s || {}) } catch { return {} } }
|
|
52
|
+
|
|
53
|
+
async function hasKey(provider) {
|
|
54
|
+
const resolved = await resolveKey(provider).catch(() => ({ value: null }))
|
|
55
|
+
return !!resolved.value
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function defaultModel(provider) {
|
|
59
|
+
return DEFAULTS[provider] || ''
|
|
60
|
+
}
|
|
5
61
|
|
|
6
62
|
export function resolveCallLLM({ provider, model } = {}) {
|
|
7
63
|
return async (input) => {
|
|
8
64
|
const explicitProvider = provider || input.provider
|
|
9
|
-
|
|
10
|
-
if (explicitProvider &&
|
|
11
|
-
|
|
65
|
+
|
|
66
|
+
if (explicitProvider && await hasKey(explicitProvider)) {
|
|
67
|
+
const m = model || input.model || defaultModel(explicitProvider)
|
|
68
|
+
if (!isAvailable(explicitProvider)) {
|
|
69
|
+
const status = getStatus().map(s => `${s.provider}(ok=${s.ok},fails=${s.failCount})`).join(', ')
|
|
70
|
+
throw new Error(`provider ${explicitProvider} is in backoff | sampler: ${status}`)
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
return await sdkChat(explicitProvider, m, input)
|
|
74
|
+
} catch (e) {
|
|
75
|
+
markFailed(explicitProvider)
|
|
76
|
+
throw e
|
|
77
|
+
}
|
|
12
78
|
}
|
|
79
|
+
|
|
13
80
|
if (await acptoapiReachable()) {
|
|
14
81
|
return await acptoapiCall({ ...input, model: model || input.model })
|
|
15
82
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
83
|
+
|
|
84
|
+
const preference = getConfigValue('agent.model_preference', [])
|
|
85
|
+
if (Array.isArray(preference) && preference.length > 0) {
|
|
86
|
+
const errors = []
|
|
87
|
+
for (const pref of preference) {
|
|
88
|
+
const p = pref.provider
|
|
89
|
+
const m = pref.model || model || input.model || defaultModel(p)
|
|
90
|
+
if (!await hasKey(p)) continue
|
|
91
|
+
if (!isAvailable(p)) continue
|
|
92
|
+
try {
|
|
93
|
+
return await sdkChat(p, m, input)
|
|
94
|
+
} catch (e) {
|
|
95
|
+
markFailed(p)
|
|
96
|
+
errors.push(`${p}: ${e.message}`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (errors.length) {
|
|
100
|
+
const status = getStatus().map(s => `${s.provider}(ok=${s.ok},fails=${s.failCount})`).join(', ')
|
|
101
|
+
throw new Error(`all preference providers failed: ${errors.join('; ')} | sampler: ${status}`)
|
|
102
|
+
}
|
|
19
103
|
}
|
|
20
|
-
|
|
104
|
+
|
|
105
|
+
const links = sdk.buildAutoChain(model || input.model)
|
|
106
|
+
const availableLinks = (await Promise.all(links.map(async l => {
|
|
107
|
+
const prefix = l.model.split('/')[0]
|
|
108
|
+
return (await hasKey(prefix)) && isAvailable(prefix) ? l : null
|
|
109
|
+
}))).filter(Boolean)
|
|
110
|
+
|
|
111
|
+
for (const link of availableLinks) {
|
|
112
|
+
const prefix = link.model.split('/')[0]
|
|
113
|
+
const m = link.model.replace(/^[^/]+\//, '')
|
|
114
|
+
try {
|
|
115
|
+
return await sdkChat(prefix, m, input)
|
|
116
|
+
} catch (e) {
|
|
117
|
+
markFailed(prefix)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const status = getStatus().map(s => `${s.provider}(ok=${s.ok},fails=${s.failCount})`).join(', ')
|
|
122
|
+
throw new Error('no LLM backend reachable: set a provider API key or start acptoapi (http://127.0.0.1:4800/v1)' + (status ? ' | sampler: ' + status : ''))
|
|
21
123
|
}
|
|
22
124
|
}
|
package/src/agent/machine.js
CHANGED
|
@@ -85,9 +85,18 @@ export function createAgentMachine({ provider, model, maxIterations = 90, callLL
|
|
|
85
85
|
})
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
export async function runTurn({ prompt, messages = [], model, provider, callLLM, enabledToolsets, disabledToolsets, maxIterations = 90, timeoutMs = 30000 } = {}) {
|
|
88
|
+
export async function runTurn({ prompt, messages = [], model, provider, callLLM, enabledToolsets, disabledToolsets, maxIterations = 90, timeoutMs = 30000, cwd, skill } = {}) {
|
|
89
|
+
const initMessages = [...messages]
|
|
90
|
+
const systemParts = []
|
|
91
|
+
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.`)
|
|
92
|
+
if (skill) {
|
|
93
|
+
const h = await bootHost()
|
|
94
|
+
const skillDef = h.pi.skills.get(skill)
|
|
95
|
+
if (skillDef?.content) systemParts.push('Skill context:\n' + skillDef.content)
|
|
96
|
+
}
|
|
97
|
+
if (systemParts.length > 0) initMessages.unshift({ role: 'user', content: systemParts.join('\n\n') })
|
|
89
98
|
const machine = createAgentMachine({ model, provider, callLLM, enabledToolsets, disabledToolsets, maxIterations })
|
|
90
|
-
const actor = createActor(machine, { input: { messages } })
|
|
99
|
+
const actor = createActor(machine, { input: { messages: initMessages } })
|
|
91
100
|
actor.start()
|
|
92
101
|
actor.send({ type: 'SUBMIT', prompt })
|
|
93
102
|
return await new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
package/src/config.js
CHANGED
|
@@ -4,9 +4,9 @@ import yaml from 'js-yaml'
|
|
|
4
4
|
import { getFreddieHome } from './home.js'
|
|
5
5
|
|
|
6
6
|
export const DEFAULT_CONFIG = {
|
|
7
|
-
_config_version:
|
|
7
|
+
_config_version: 2,
|
|
8
8
|
display: { skin: 'default', tool_progress_command: false, background_process_notifications: 'all' },
|
|
9
|
-
agent: { provider: 'anthropic', model: '', max_iterations: 90, fallback_model: null, save_trajectories: false },
|
|
9
|
+
agent: { provider: 'anthropic', model: '', max_iterations: 90, fallback_model: null, save_trajectories: false, model_preference: [] },
|
|
10
10
|
memory: { provider: null },
|
|
11
11
|
skills: { config: {} },
|
|
12
12
|
terminal: { cwd: null },
|
|
@@ -17,6 +17,7 @@ export const DEFAULT_CONFIG = {
|
|
|
17
17
|
|
|
18
18
|
const MIGRATIONS = {
|
|
19
19
|
1: cfg => cfg,
|
|
20
|
+
2: cfg => { if (!cfg.agent) cfg.agent = {}; if (!Array.isArray(cfg.agent.model_preference)) cfg.agent.model_preference = []; return cfg },
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export function configPath() { return path.join(getFreddieHome(), 'config.yaml') }
|
package/src/sessions.js
CHANGED
|
@@ -12,7 +12,8 @@ async function initDb() {
|
|
|
12
12
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
13
13
|
id TEXT PRIMARY KEY,
|
|
14
14
|
platform TEXT, user_id TEXT, chat_id TEXT, thread_id TEXT,
|
|
15
|
-
title TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, model TEXT
|
|
15
|
+
title TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, model TEXT,
|
|
16
|
+
cwd TEXT, skill TEXT
|
|
16
17
|
);
|
|
17
18
|
CREATE TABLE IF NOT EXISTS messages (
|
|
18
19
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -23,6 +24,11 @@ async function initDb() {
|
|
|
23
24
|
CREATE INDEX IF NOT EXISTS idx_msg_session ON messages(session_id, ts);
|
|
24
25
|
`)
|
|
25
26
|
|
|
27
|
+
// migrate: add cwd and skill columns if absent
|
|
28
|
+
for (const col of ['cwd', 'skill']) {
|
|
29
|
+
try { await d.exec(`ALTER TABLE sessions ADD COLUMN ${col} TEXT`) } catch {}
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
// libsql supports FTS5 natively; create FTS virtual table
|
|
27
33
|
try {
|
|
28
34
|
await d.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(content, session_id UNINDEXED, content='messages', content_rowid='id')`)
|
|
@@ -38,12 +44,12 @@ async function db() {
|
|
|
38
44
|
return await initDb()
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
export async function createSession({ platform = 'cli', userId = null, chatId = null, threadId = null, title = null, model = null } = {}) {
|
|
47
|
+
export async function createSession({ platform = 'cli', userId = null, chatId = null, threadId = null, title = null, model = null, cwd = null, skill = null } = {}) {
|
|
42
48
|
const d = await db()
|
|
43
49
|
const id = randomUUID()
|
|
44
50
|
const now = Date.now()
|
|
45
|
-
await d.prepare(`INSERT INTO sessions (id, platform, user_id, chat_id, thread_id, title, created_at, updated_at, model) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
46
|
-
.run(id, platform, userId, chatId, threadId, title, now, now, model)
|
|
51
|
+
await d.prepare(`INSERT INTO sessions (id, platform, user_id, chat_id, thread_id, title, created_at, updated_at, model, cwd, skill) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
52
|
+
.run(id, platform, userId, chatId, threadId, title, now, now, model, cwd, skill)
|
|
47
53
|
return id
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -64,7 +70,7 @@ export async function getMessages(sessionId) {
|
|
|
64
70
|
|
|
65
71
|
export async function listSessions(limit = 50) {
|
|
66
72
|
const d = await db()
|
|
67
|
-
return await d.prepare(`SELECT id, platform, title, created_at, updated_at, model FROM sessions ORDER BY updated_at DESC LIMIT ?`).all(limit)
|
|
73
|
+
return await d.prepare(`SELECT id, platform, title, created_at, updated_at, model, cwd, skill FROM sessions ORDER BY updated_at DESC LIMIT ?`).all(limit)
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
export async function search(query, limit = 20) {
|
package/src/web/app.js
CHANGED
|
@@ -1,80 +1,64 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { h, applyDiff, installStyles, components } from 'anentrypoint-design';
|
|
2
|
+
import { fetchHost, ROUTES } from './state.js';
|
|
3
|
+
import { PAGES } from './routes.js';
|
|
4
|
+
|
|
5
|
+
const { AppShell, Topbar, Side, Crumb, Status, EmptyState, Chip } = components;
|
|
3
6
|
|
|
4
7
|
await installStyles();
|
|
5
8
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
const root = document.getElementById('app');
|
|
10
|
+
root.textContent = 'loading…';
|
|
11
|
+
const host0 = await fetchHost();
|
|
12
|
+
root.innerHTML = '';
|
|
10
13
|
|
|
11
|
-
const
|
|
12
|
-
const post = (u, body) => j(u, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
|
14
|
+
const state = { active: 'home', ts: new Date().toLocaleTimeString(), body: null, error: null };
|
|
13
15
|
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
function buildSide() {
|
|
17
|
+
return Side({ sections: [{ group: 'FREDDIE', items: ROUTES.map(r => ({
|
|
18
|
+
glyph: r.glyph, label: r.label, href: '#fd-' + r.path,
|
|
19
|
+
active: state.active === r.path,
|
|
20
|
+
onClick: ev => { ev.preventDefault(); setActive(r.path); },
|
|
21
|
+
})) }] });
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const skillList = [...(skillsResp.home || []), ...(skillsResp.bundled || [])];
|
|
32
|
-
const projList = projectsResp.projects || [];
|
|
33
|
-
return {
|
|
34
|
-
kind: 'freddie-web', version: 'web',
|
|
35
|
-
pi: {
|
|
36
|
-
tools: regFromArray(tools),
|
|
37
|
-
skills: regFromArray(skillList, (s) => s.name || s.id),
|
|
38
|
-
cli: regFromArray(commands),
|
|
39
|
-
projects: {
|
|
40
|
-
list: () => projList,
|
|
41
|
-
active: () => projectsResp.active,
|
|
42
|
-
create: ({ name, path }) => post('/api/projects', { name, path }).then(() => location.reload()),
|
|
43
|
-
remove: (name) => fetch('/api/projects/' + encodeURIComponent(name), { method: 'DELETE' }).then(() => location.reload()),
|
|
44
|
-
setActive: (name) => post('/api/projects/active', { name }).then(() => location.reload()),
|
|
45
|
-
},
|
|
46
|
-
sessions: {
|
|
47
|
-
list: () => j('/api/sessions').catch(() => []),
|
|
48
|
-
getMessages: (id) => j('/api/sessions/' + encodeURIComponent(id) + '/messages').catch(() => []),
|
|
49
|
-
search: (q) => j('/api/search?q=' + encodeURIComponent(q)).catch(() => []),
|
|
50
|
-
},
|
|
51
|
-
cron: {
|
|
52
|
-
list: () => Promise.resolve(cron),
|
|
53
|
-
create: (job) => post('/api/cron', job),
|
|
54
|
-
delete: (id) => fetch('/api/cron/' + id, { method: 'DELETE' }),
|
|
55
|
-
},
|
|
56
|
-
env: { list: () => env, isSet: (k) => (env.find(e => e.key === k) || {}).set || false },
|
|
57
|
-
gateway: { platforms: () => gateway.platforms || [] },
|
|
58
|
-
agents: () => j('/api/agents').catch(() => ({ count: 0, turns: 0, active: null })),
|
|
59
|
-
health: () => health,
|
|
60
|
-
config: {
|
|
61
|
-
load: () => j('/api/config').catch(() => ({})),
|
|
62
|
-
saveValue: (path, value) => post('/api/config', { path, value }),
|
|
63
|
-
},
|
|
64
|
-
chat: { send: (text) => post('/api/chat', { text }) },
|
|
65
|
-
batch: { run: (prompts, conc) => post('/api/batch', { prompts, concurrency: conc }) },
|
|
66
|
-
hooks: {},
|
|
67
|
-
},
|
|
68
|
-
};
|
|
24
|
+
function view() {
|
|
25
|
+
const route = ROUTES.find(r => r.path === state.active) || ROUTES[0];
|
|
26
|
+
const body = state.body || EmptyState({ text: 'loading…', glyph: '◌' });
|
|
27
|
+
const main = h('div', { key: state.active, class: 'fd-page' }, ...(Array.isArray(body) ? body : [body]));
|
|
28
|
+
return AppShell({
|
|
29
|
+
topbar: Topbar({ brand: 'freddie', leaf: 'dashboard', items: [], active: '' }),
|
|
30
|
+
crumb: Crumb({ trail: ['freddie'], leaf: route.path, right: state.error ? Chip({ tone: 'miss', children: 'error' }) : Chip({ tone: 'ok', children: 'live' }) }),
|
|
31
|
+
side: buildSide(),
|
|
32
|
+
main,
|
|
33
|
+
status: Status({ left: ['ds-247420 · webjsx · ' + ROUTES.length + ' routes'], right: [state.ts] }),
|
|
34
|
+
});
|
|
69
35
|
}
|
|
70
36
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
37
|
+
function rerender() { applyDiff(root, view()); }
|
|
38
|
+
|
|
39
|
+
function setActive(p) { state.active = p; state.body = null; rerender(); loadActive(); }
|
|
40
|
+
if (typeof window !== 'undefined') window.__fd_nav = setActive;
|
|
41
|
+
|
|
42
|
+
async function loadActive() {
|
|
43
|
+
const active = state.active;
|
|
44
|
+
try {
|
|
45
|
+
const page = PAGES[active] || PAGES.home;
|
|
46
|
+
const body = await page(host0);
|
|
47
|
+
if (state.active !== active) return;
|
|
48
|
+
state.body = body;
|
|
49
|
+
state.error = null;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
if (state.active !== active) return;
|
|
52
|
+
state.error = String(e && e.stack || e);
|
|
53
|
+
const { Panel } = components;
|
|
54
|
+
state.body = Panel({ title: 'page error', children: h('pre', { class: 'fd-pre', style: 'max-height:200px;overflow-y:auto' }, state.error) });
|
|
55
|
+
}
|
|
56
|
+
state.ts = new Date().toLocaleTimeString();
|
|
57
|
+
applyDiff(root, view());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
applyDiff(root, view());
|
|
61
|
+
loadActive();
|
|
78
62
|
|
|
79
63
|
if (!window.__debug) window.__debug = {};
|
|
80
|
-
window.__debug.dashboard = () => ({ booted: true,
|
|
64
|
+
window.__debug.dashboard = () => ({ booted: true, tools: host0.pi.tools.size, skills: host0.pi.skills.size, active: state.active });
|
package/src/web/index.html
CHANGED
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Freddie Dashboard</title>
|
|
7
|
+
<link rel="stylesheet" href="/vendor/anentrypoint-design/247420.css">
|
|
7
8
|
<script type="importmap">
|
|
8
9
|
{ "imports": { "anentrypoint-design": "/vendor/anentrypoint-design/247420.js" } }
|
|
9
10
|
</script>
|
|
10
11
|
<style>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
#app { padding: 16px 20px; box-sizing: border-box; }
|
|
12
|
+
html, body { margin: 0; padding: 0; height: 100%; }
|
|
13
|
+
#app { height: 100%; }
|
|
14
14
|
</style>
|
|
15
15
|
</head>
|
|
16
|
-
<body data-theme="
|
|
16
|
+
<body data-theme="light">
|
|
17
17
|
<div id="app"></div>
|
|
18
18
|
<script type="module" src="./app.js"></script>
|
|
19
19
|
</body>
|