freddie 0.0.71 → 0.0.73
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/package.json +2 -2
- package/plugins/gui-projects/plugin.js +29 -0
- package/src/home.js +7 -0
- package/src/host/index.js +2 -0
- package/src/projects.js +79 -0
- package/src/web/app.js +61 -1
- package/src/web/vendor/anentrypoint-design/dist/247420.js +15 -15
- package/src/web/vendor/anentrypoint-design/247420.js +0 -183
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freddie",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.73",
|
|
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,7 @@
|
|
|
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.49"
|
|
30
30
|
},
|
|
31
31
|
"optionalDependencies": {
|
|
32
32
|
"@libsql/darwin-arm64": "0.3.19",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { listProjects, getActiveProject, createProject, deleteProject, setActiveProject } from '../../src/projects.js'
|
|
2
|
+
import { resetHostForTests } from '../../src/host/index.js'
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
name: 'gui-projects',
|
|
6
|
+
surfaces: 'gui',
|
|
7
|
+
register({ gui }) {
|
|
8
|
+
gui.route('GET', '/api/projects', (_, res) => res.json({ active: getActiveProject(), projects: listProjects() }))
|
|
9
|
+
gui.route('POST', '/api/projects', (req, res) => {
|
|
10
|
+
try {
|
|
11
|
+
const { name, path: projectPath } = req.body || {}
|
|
12
|
+
const created = createProject({ name, projectPath })
|
|
13
|
+
res.json(created)
|
|
14
|
+
} catch (e) { res.status(400).json({ error: e.message }) }
|
|
15
|
+
})
|
|
16
|
+
gui.route('DELETE', '/api/projects/:name', (req, res) => {
|
|
17
|
+
try { deleteProject(req.params.name); res.json({ ok: true }) }
|
|
18
|
+
catch (e) { res.status(400).json({ error: e.message }) }
|
|
19
|
+
})
|
|
20
|
+
gui.route('POST', '/api/projects/active', (req, res) => {
|
|
21
|
+
try {
|
|
22
|
+
const { name } = req.body || {}
|
|
23
|
+
const proj = setActiveProject(name)
|
|
24
|
+
resetHostForTests()
|
|
25
|
+
res.json({ ok: true, active: proj, note: 'restart dashboard to load new project plugins' })
|
|
26
|
+
} catch (e) { res.status(400).json({ error: e.message }) }
|
|
27
|
+
})
|
|
28
|
+
},
|
|
29
|
+
}
|
package/src/home.js
CHANGED
|
@@ -27,6 +27,13 @@ export function applyProfileOverride(name) {
|
|
|
27
27
|
_cached = null
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function applyHomeOverride(absPath) {
|
|
31
|
+
if (!absPath) { delete process.env.FREDDIE_HOME; _cached = null; return }
|
|
32
|
+
process.env.FREDDIE_HOME = absPath
|
|
33
|
+
_cached = null
|
|
34
|
+
ensure(absPath)
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
export function getProfilesRoot() {
|
|
31
38
|
if (process.env.FREDDIE_PROFILES_ROOT) return process.env.FREDDIE_PROFILES_ROOT
|
|
32
39
|
if (process.env.FREDDIE_HOME) return path.join(process.env.FREDDIE_HOME, 'profiles')
|
package/src/host/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from 'node:path'
|
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { createHost, discoverPlugins } from './host.js'
|
|
4
4
|
import { getFreddieHome } from '../home.js'
|
|
5
|
+
import { applyActiveProjectFromRegistry } from '../projects.js'
|
|
5
6
|
|
|
6
7
|
let _host = null
|
|
7
8
|
let _loaded = false
|
|
@@ -18,6 +19,7 @@ export async function bootHost(extraRoots = []) {
|
|
|
18
19
|
const h = host()
|
|
19
20
|
if (_loaded) return h
|
|
20
21
|
_loaded = true
|
|
22
|
+
if (!process.env.FREDDIE_HOME && !process.env.FREDDIE_PROFILE) applyActiveProjectFromRegistry()
|
|
21
23
|
const roots = [REPO_PLUGINS, path.join(getFreddieHome(), 'plugins'), path.join(process.cwd(), '.freddie', 'plugins'), ...extraRoots]
|
|
22
24
|
const plugins = await discoverPlugins(roots)
|
|
23
25
|
await h.load(plugins)
|
package/src/projects.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import { applyHomeOverride } from './home.js'
|
|
5
|
+
|
|
6
|
+
const REGISTRY_PATH = path.join(os.homedir(), '.freddie', 'projects.json')
|
|
7
|
+
|
|
8
|
+
const DEFAULT_REGISTRY = {
|
|
9
|
+
active: 'default',
|
|
10
|
+
projects: [
|
|
11
|
+
{ name: 'default', path: path.join(os.homedir(), '.freddie'), created_at: new Date().toISOString() },
|
|
12
|
+
],
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ensureRegistry() {
|
|
16
|
+
const dir = path.dirname(REGISTRY_PATH)
|
|
17
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
18
|
+
if (!fs.existsSync(REGISTRY_PATH)) {
|
|
19
|
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(DEFAULT_REGISTRY, null, 2))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function loadRegistry() {
|
|
24
|
+
ensureRegistry()
|
|
25
|
+
try {
|
|
26
|
+
const raw = JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'))
|
|
27
|
+
if (!raw.projects || !Array.isArray(raw.projects)) return DEFAULT_REGISTRY
|
|
28
|
+
if (!raw.projects.find(p => p.name === 'default')) raw.projects.unshift(DEFAULT_REGISTRY.projects[0])
|
|
29
|
+
if (!raw.active) raw.active = 'default'
|
|
30
|
+
return raw
|
|
31
|
+
} catch { return DEFAULT_REGISTRY }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function saveRegistry(reg) {
|
|
35
|
+
ensureRegistry()
|
|
36
|
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(reg, null, 2))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function listProjects() { return loadRegistry().projects }
|
|
40
|
+
|
|
41
|
+
export function getActiveProject() {
|
|
42
|
+
const reg = loadRegistry()
|
|
43
|
+
return reg.projects.find(p => p.name === reg.active) || reg.projects[0]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createProject({ name, projectPath }) {
|
|
47
|
+
if (!name || !projectPath) throw new Error('name and path are required')
|
|
48
|
+
if (!path.isAbsolute(projectPath)) throw new Error('path must be absolute')
|
|
49
|
+
const reg = loadRegistry()
|
|
50
|
+
if (reg.projects.find(p => p.name === name)) throw new Error('project name already exists')
|
|
51
|
+
if (!fs.existsSync(projectPath)) fs.mkdirSync(projectPath, { recursive: true })
|
|
52
|
+
reg.projects.push({ name, path: projectPath, created_at: new Date().toISOString() })
|
|
53
|
+
saveRegistry(reg)
|
|
54
|
+
return reg.projects[reg.projects.length - 1]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function deleteProject(name) {
|
|
58
|
+
if (name === 'default') throw new Error('cannot delete default project')
|
|
59
|
+
const reg = loadRegistry()
|
|
60
|
+
reg.projects = reg.projects.filter(p => p.name !== name)
|
|
61
|
+
if (reg.active === name) reg.active = 'default'
|
|
62
|
+
saveRegistry(reg)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function setActiveProject(name) {
|
|
66
|
+
const reg = loadRegistry()
|
|
67
|
+
const proj = reg.projects.find(p => p.name === name)
|
|
68
|
+
if (!proj) throw new Error('unknown project: ' + name)
|
|
69
|
+
reg.active = name
|
|
70
|
+
saveRegistry(reg)
|
|
71
|
+
applyHomeOverride(proj.path)
|
|
72
|
+
return proj
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function applyActiveProjectFromRegistry() {
|
|
76
|
+
const proj = getActiveProject()
|
|
77
|
+
if (proj) applyHomeOverride(proj.path)
|
|
78
|
+
return proj
|
|
79
|
+
}
|
package/src/web/app.js
CHANGED
|
@@ -13,6 +13,7 @@ window.__debug.agents = () => ({ registered: true, active: AppState.agents?.acti
|
|
|
13
13
|
const j = async (u, opts) => { try { const r = await fetch(u, opts); if (!r.ok) throw new Error(r.status + ' ' + r.statusText); return await r.json() } catch (e) { return { __error: String(e) } } }
|
|
14
14
|
|
|
15
15
|
const ROUTES = [
|
|
16
|
+
{ path: '#/projects', label: 'Projects', glyph: '◆' },
|
|
16
17
|
{ path: '#/home', label: 'Home', glyph: '⌂' },
|
|
17
18
|
{ path: '#/chat', label: 'Chat', glyph: '⌨' },
|
|
18
19
|
{ path: '#/sessions', label: 'Sessions', glyph: '✉' },
|
|
@@ -40,6 +41,7 @@ const AppState = {
|
|
|
40
41
|
chat: { messages: [], draft: '', streaming: false },
|
|
41
42
|
batch: { results: null, running: false },
|
|
42
43
|
agents: { count: 0, active: null },
|
|
44
|
+
projects: { active: null, all: [] },
|
|
43
45
|
}
|
|
44
46
|
function applyTheme() { document.body.setAttribute('data-theme', AppState.theme) }
|
|
45
47
|
applyTheme()
|
|
@@ -77,6 +79,51 @@ function toChatMsg(m, key) {
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
const PAGES = {
|
|
82
|
+
'#/projects': async () => {
|
|
83
|
+
const data = await j('/api/projects')
|
|
84
|
+
const all = data.projects || []
|
|
85
|
+
const active = data.active || null
|
|
86
|
+
AppState.projects = { active, all }
|
|
87
|
+
return [
|
|
88
|
+
Hero({ title: 'Projects', body: 'Each project is its own ~/.freddie home: separate sessions, agents, skills, config, env, cron, batches. Switch the active project to swap everything.', accent: active ? 'active · ' + active.name : 'no project active' }),
|
|
89
|
+
kpi([[all.length, 'Projects'], [active?.name || '—', 'Active'], [active?.path?.length > 30 ? '…'+active.path.slice(-28) : (active?.path || '—'), 'Path']]),
|
|
90
|
+
Panel({ title: 'Add a project', children: h('form', { class: 'row-form', onsubmit: async (ev) => {
|
|
91
|
+
ev.preventDefault()
|
|
92
|
+
const f = ev.target.elements
|
|
93
|
+
const r = await j('/api/projects', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: f.name.value, path: f.path.value }) })
|
|
94
|
+
if (r.__error || r.error) { alert('Error: ' + (r.error || r.__error)); return }
|
|
95
|
+
f.name.value = ''; f.path.value = ''; rerender()
|
|
96
|
+
} },
|
|
97
|
+
h('input', { name: 'name', placeholder: 'project name (e.g. penguins)', required: true }),
|
|
98
|
+
h('input', { name: 'path', placeholder: 'absolute path (e.g. C:\\dev\\penguins)', required: true, style: 'flex:2' }),
|
|
99
|
+
h('button', { type: 'submit', class: 'primary' }, 'Add')) }),
|
|
100
|
+
Panel({ title: 'All projects', count: all.length, right: active ? Chip({ tone: 'ok', children: 'active: ' + active.name }) : null,
|
|
101
|
+
children: h('div', {}, ...all.map(p => h('div', { class: 'row', style: 'cursor:pointer', onclick: async () => {
|
|
102
|
+
if (p.name === active?.name) return
|
|
103
|
+
const r = await j('/api/projects/active', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: p.name }) })
|
|
104
|
+
if (r.__error || r.error) { alert('Switch failed: ' + (r.error || r.__error)); return }
|
|
105
|
+
alert('Switched to project "' + p.name + '". Restart the dashboard to load its plugins/data, then reload this page.')
|
|
106
|
+
rerender()
|
|
107
|
+
} },
|
|
108
|
+
h('span', { class: 'code' }, p.name === active?.name ? '●' : '○'),
|
|
109
|
+
h('span', { class: 'title' }, p.name + (p.name === active?.name ? ' (active)' : '')),
|
|
110
|
+
h('span', { class: 'meta' }, p.path),
|
|
111
|
+
p.name !== 'default' ? h('button', { class: 'danger', style: 'margin-left:8px',
|
|
112
|
+
onclick: async (ev) => { ev.stopPropagation(); if (!confirm('Remove project "'+p.name+'" from registry? Files on disk are kept.')) return; await fetch('/api/projects/' + p.name, { method: 'DELETE' }); rerender() } }, 'remove') : null,
|
|
113
|
+
))) }),
|
|
114
|
+
Panel({ title: 'How encapsulation works', children: Receipt({ rows: [
|
|
115
|
+
['Sessions DB', '<project>/sessions.db'],
|
|
116
|
+
['Config', '<project>/config.json'],
|
|
117
|
+
['Skills', '<project>/skills/'],
|
|
118
|
+
['Plugins', '<project>/plugins/ + repo/plugins'],
|
|
119
|
+
['Cron jobs', '<project>/cron.db'],
|
|
120
|
+
['Batches', '<project>/batches/'],
|
|
121
|
+
['Logs', '<project>/logs/'],
|
|
122
|
+
['Auth credentials', '<project>/auth.json'],
|
|
123
|
+
]}) }),
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
|
|
80
127
|
'#/chat': async () => {
|
|
81
128
|
const messages = AppState.chat.messages.map((m, i) => toChatMsg(m, 'm' + i))
|
|
82
129
|
return AICat({
|
|
@@ -455,8 +502,15 @@ function render(state) {
|
|
|
455
502
|
onkeydown: (ev) => { if (ev.key === 'Enter') doSearch(ev.target.value) },
|
|
456
503
|
style: 'min-width:240px',
|
|
457
504
|
})
|
|
505
|
+
const projectPill = h('a', {
|
|
506
|
+
href: '#/projects',
|
|
507
|
+
class: 'project-pill',
|
|
508
|
+
title: state.projects.active ? 'Active project: ' + state.projects.active.name + ' — ' + state.projects.active.path : 'No active project',
|
|
509
|
+
style: 'display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:999px;background:var(--panel-2);color:var(--panel-text);text-decoration:none;font-size:12px;font-weight:500',
|
|
510
|
+
}, h('span', { style: 'opacity:0.6' }, '◆'), h('span', {}, state.projects.active?.name || 'default'))
|
|
458
511
|
const topbarWithControls = h('header', { class: 'app-topbar' },
|
|
459
512
|
Brand({ name: 'freddie', leaf: 'dashboard' }),
|
|
513
|
+
projectPill,
|
|
460
514
|
h('div', { style: 'flex:1' }),
|
|
461
515
|
searchInput,
|
|
462
516
|
themeBtn,
|
|
@@ -480,11 +534,17 @@ function render(state) {
|
|
|
480
534
|
|
|
481
535
|
let _mount
|
|
482
536
|
|
|
537
|
+
async function refreshProject() {
|
|
538
|
+
const data = await j('/api/projects')
|
|
539
|
+
if (!data.__error) AppState.projects = { active: data.active || null, all: data.projects || [] }
|
|
540
|
+
}
|
|
541
|
+
|
|
483
542
|
async function go() {
|
|
484
|
-
AppState.hash = location.hash || '#/
|
|
543
|
+
AppState.hash = location.hash || '#/projects'
|
|
485
544
|
AppState.ts = new Date().toLocaleTimeString()
|
|
486
545
|
AppState.body = EmptyState({ text: 'loading…', glyph: '◌' })
|
|
487
546
|
if (_mount) _mount()
|
|
547
|
+
refreshProject()
|
|
488
548
|
let body
|
|
489
549
|
if (AppState.hash.startsWith('#/session/')) {
|
|
490
550
|
body = await pageSessionDetail(AppState.hash.slice('#/session/'.length))
|