freddie 0.0.57 → 0.0.59
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/src/host/host.js +116 -123
- package/src/host/index.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freddie",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.59",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Open JS agent harness built on pi-mono, floosie, xstate, and anentrypoint-design",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"floosie": "^0.6.14",
|
|
24
24
|
"gm-cc": "^2.0.727",
|
|
25
25
|
"js-yaml": "^4.1.0",
|
|
26
|
-
"plugsdk": "^1.0.
|
|
26
|
+
"plugsdk": "^1.0.15",
|
|
27
27
|
"xstate": "^5.31.0",
|
|
28
28
|
"zod": "^4.0.0"
|
|
29
29
|
},
|
package/src/host/host.js
CHANGED
|
@@ -1,34 +1,28 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
|
-
import { loadClaudePlugin,
|
|
4
|
+
import { loadClaudePlugin, createHost as createPluginHost } from 'plugsdk'
|
|
5
5
|
import { validatePlugin, topoSort, HOOK_NAMES, PI_VERBS, GUI_VERBS, FREDDIE_TO_NATIVE_HOOK } from './contract.js'
|
|
6
6
|
|
|
7
|
-
function
|
|
8
|
-
const tools = new Map()
|
|
9
|
-
const envs = new Map()
|
|
10
|
-
const commands = new Map()
|
|
11
|
-
const crons = new Map()
|
|
12
|
-
const platforms = new Map()
|
|
13
|
-
const memory = new Map()
|
|
14
|
-
const skills = new Map()
|
|
15
|
-
const contexts = new Map()
|
|
16
|
-
const agentExts = new Map()
|
|
17
|
-
const cli = new Map()
|
|
7
|
+
function reg(map, kind) {
|
|
18
8
|
return {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
9
|
+
register(spec) { if (!spec?.name) throw new Error(`${kind}.name required`); map.set(spec.name, spec) },
|
|
10
|
+
get: (n) => map.get(n), list: () => [...map.values()], has: (n) => map.has(n), size: () => map.size,
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function makePi() {
|
|
15
|
+
const m = { tools:new Map(), envs:new Map(), commands:new Map(), crons:new Map(), platforms:new Map(),
|
|
16
|
+
memory:new Map(), skills:new Map(), contexts:new Map(), agentExts:new Map(), cli:new Map() }
|
|
17
|
+
return {
|
|
18
|
+
_state: m,
|
|
19
|
+
tools: reg(m.tools, 'tool'), envs: reg(m.envs, 'env'),
|
|
20
|
+
commands: reg(m.commands, 'command'), crons: reg(m.crons, 'cron'),
|
|
21
|
+
platforms: reg(m.platforms, 'platform'), memory: reg(m.memory, 'memory'),
|
|
22
|
+
skills: reg(m.skills, 'skill'), contexts: reg(m.contexts, 'context'),
|
|
23
|
+
agentExts: reg(m.agentExts, 'agentExt'), cli: reg(m.cli, 'cli'),
|
|
30
24
|
async dispatchTool(name, args = {}, ctx = {}) {
|
|
31
|
-
const t = tools.get(name)
|
|
25
|
+
const t = m.tools.get(name)
|
|
32
26
|
if (!t) return JSON.stringify({ error: `unknown tool: ${name}` })
|
|
33
27
|
if (t.checkFn && t.checkFn(t) === false) return JSON.stringify({ error: `tool unavailable: ${name}`, requires: t.requiresEnv || [] })
|
|
34
28
|
try { const r = await t.handler(args, ctx); return typeof r === 'string' ? r : JSON.stringify(r) }
|
|
@@ -37,60 +31,68 @@ function makePiSurface() {
|
|
|
37
31
|
}
|
|
38
32
|
}
|
|
39
33
|
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
register(spec) { if (!spec?.name) throw new Error(`${kind}.name required`); map.set(spec.name, spec) },
|
|
43
|
-
get(name) { return map.get(name) },
|
|
44
|
-
list() { return [...map.values()] },
|
|
45
|
-
has(name) { return map.has(name) },
|
|
46
|
-
size() { return map.size },
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function makeGuiSurface() {
|
|
51
|
-
const routes = []
|
|
52
|
-
const pages = new Map()
|
|
53
|
-
const nav = []
|
|
54
|
-
const debugs = new Map()
|
|
55
|
-
const apis = new Map()
|
|
56
|
-
const assets = new Map()
|
|
34
|
+
function makeGui() {
|
|
35
|
+
const r=[], pages=new Map(), nav=[], debugs=new Map(), apis=new Map(), assets=new Map()
|
|
57
36
|
return {
|
|
58
|
-
_state: { routes, pages, nav, debugs, apis, assets },
|
|
59
|
-
route(method,
|
|
60
|
-
page(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
routes: { list: () => routes },
|
|
66
|
-
pages: { get: (s) => pages.get(s), list: () => [...pages.values()], has: (s) => pages.has(s) },
|
|
67
|
-
navItems: { list: () => nav },
|
|
68
|
-
debugs: { list: () => [...debugs.entries()].map(([name, fn]) => ({ name, snapshot: fn })), get: (n) => debugs.get(n) },
|
|
37
|
+
_state: { routes:r, pages, nav, debugs, apis, assets },
|
|
38
|
+
route:(method,p,h)=>r.push({method:method.toUpperCase(),path:p,handler:h}),
|
|
39
|
+
page:(s,d)=>pages.set(s,d), nav:(i)=>nav.push(i),
|
|
40
|
+
debug:(n,fn)=>debugs.set(n,fn), api:(g,d)=>apis.set(g,d), asset:(p,c)=>assets.set(p,c),
|
|
41
|
+
routes:{ list:()=>r }, pages:{ get:(s)=>pages.get(s), list:()=>[...pages.values()], has:(s)=>pages.has(s) },
|
|
42
|
+
navItems:{ list:()=>nav },
|
|
43
|
+
debugs:{ list:()=>[...debugs.entries()].map(([n,f])=>({name:n,snapshot:f})), get:(n)=>debugs.get(n) },
|
|
69
44
|
}
|
|
70
45
|
}
|
|
71
46
|
|
|
72
|
-
function ccPayloadFor(
|
|
73
|
-
if (
|
|
47
|
+
function ccPayloadFor(name, payload) {
|
|
48
|
+
if (name === 'preToolCall' || name === 'postToolCall')
|
|
74
49
|
return { tool_name: payload?.name, tool_input: payload?.args || payload?.input, tool_response: payload?.result }
|
|
75
|
-
if (
|
|
50
|
+
if (name === 'onMessageInbound' || name === 'onMessageOutbound')
|
|
76
51
|
return { prompt: payload?.content || payload?.text || '' }
|
|
77
52
|
return payload || {}
|
|
78
53
|
}
|
|
79
54
|
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
55
|
+
function guard(surface, allowed, name, verbs) {
|
|
56
|
+
if (allowed) return surface
|
|
57
|
+
return new Proxy({}, { get(_, key) {
|
|
58
|
+
if (verbs.includes(String(key))) return () => { throw new Error(`plugin ${name}: surface verb '${String(key)}' not allowed (declared surfaces=${name})`) }
|
|
59
|
+
return surface[key]
|
|
60
|
+
} })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function scopedCfg(name, store) {
|
|
64
|
+
const k = `plugins.${name}`
|
|
65
|
+
return { get:(kk,d)=>store.get(`${k}.${kk}`,d), set:(kk,v)=>store.set(`${k}.${kk}`,v), all:()=>store.all(k)||{} }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const nullStore = () => { const m=new Map(); return { get:(k,d)=>m.has(k)?m.get(k):d, set:(k,v)=>m.set(k,v), all:(p)=>Object.fromEntries([...m.entries()].filter(([k])=>k.startsWith(p))) } }
|
|
69
|
+
|
|
70
|
+
export function createHost({ surfaces = ['pi','gui'], configStore = nullStore(), env = process.env } = {}) {
|
|
71
|
+
const pi = makePi(), gui = makeGui()
|
|
72
|
+
const binPaths = []
|
|
73
|
+
const inboundListeners = []
|
|
74
|
+
const ccHost = createPluginHost({ env, on: {
|
|
75
|
+
onSkill: (p, s) => surfaces.includes('pi') && pi.skills.register({ name: p.manifest.name + ':' + s.name, description: s.description, content: s.body, source: 'cc:' + p.manifest.name, frontmatter: s.fields, file: s.file }),
|
|
76
|
+
onAgent: (p, a) => surfaces.includes('pi') && pi.agentExts.register({ name: p.manifest.name + ':' + a.name, description: a.description, frontmatter: a.fields, body: a.body, source: 'cc:' + p.manifest.name, file: a.file }),
|
|
77
|
+
onCommand: (p, c) => surfaces.includes('pi') && pi.commands.register({ name: p.manifest.name + ':' + c.name, description: c.description, body: c.body, frontmatter: c.fields, source: 'cc:' + p.manifest.name }),
|
|
78
|
+
onTheme: (p, t) => surfaces.includes('pi') && pi.contexts.register({ name: 'theme:' + p.manifest.name + ':' + t.slug, description: t.name, theme: t }),
|
|
79
|
+
onOutputStyle: (p, o) => surfaces.includes('pi') && pi.contexts.register({ name: 'style:' + p.manifest.name + ':' + o.name, description: o.description, body: o.body, frontmatter: o.fields }),
|
|
80
|
+
onChannel: (p, c) => surfaces.includes('pi') && pi.platforms.register({ name: 'cc:' + p.manifest.name + ':' + c.server, server: c.server, userConfig: c.userConfig || {}, source: 'cc:' + p.manifest.name }),
|
|
81
|
+
onSetting: (p, s) => { if (s.agent && surfaces.includes('pi') && !pi.agentExts.has('default')) pi.agentExts.register({ name: 'default', target: p.manifest.name + ':' + s.agent }) },
|
|
82
|
+
onBin: (_, dir) => binPaths.push(dir),
|
|
83
|
+
onMcpTool: (p, server, tool, call) => surfaces.includes('pi') && pi.tools.register({ name: 'cc:' + p.manifest.name + ':' + server + ':' + tool.name, schema: { name: tool.name, description: tool.description || '', parameters: tool.inputSchema || {} }, handler: (args) => call(args) }),
|
|
84
|
+
onMonitorLine: (p, mon, line) => { for (const fn of inboundListeners) fn({ source: 'monitor:' + p.manifest.name + ':' + mon.name, text: line }) },
|
|
85
|
+
} })
|
|
86
|
+
|
|
87
|
+
const reg2 = Object.fromEntries(HOOK_NAMES.map(n => [n, []]))
|
|
88
|
+
const hooks = {
|
|
89
|
+
on(name, fn) { if (!HOOK_NAMES.includes(name)) throw new Error(`unknown hook: ${name}`); reg2[name].push(fn) },
|
|
88
90
|
async invoke(name, payload) {
|
|
89
91
|
let cur = payload
|
|
90
|
-
for (const fn of
|
|
92
|
+
for (const fn of reg2[name] || []) cur = (await fn(cur)) ?? cur
|
|
91
93
|
const native = FREDDIE_TO_NATIVE_HOOK[name]
|
|
92
|
-
if (native &&
|
|
93
|
-
const r = await
|
|
94
|
+
if (native && ccHost.plugins().length) {
|
|
95
|
+
const r = await ccHost.dispatch(native, ccPayloadFor(name, cur))
|
|
94
96
|
if (r.decision === 'block') return { ...cur, behavior: 'block', reason: r.reason }
|
|
95
97
|
const pd = r.hookSpecificOutput?.permissionDecision
|
|
96
98
|
if (pd === 'deny') return { ...cur, behavior: 'block', reason: r.hookSpecificOutput?.permissionDecisionReason || 'denied' }
|
|
@@ -98,91 +100,82 @@ function makeHooks(ccPlugins) {
|
|
|
98
100
|
}
|
|
99
101
|
return cur
|
|
100
102
|
},
|
|
101
|
-
names()
|
|
102
|
-
listeners(name) { return [...(reg[name] || [])] },
|
|
103
|
+
names: () => HOOK_NAMES, listeners: (n) => [...(reg2[n] || [])],
|
|
103
104
|
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function guard(surfaceObj, allowed, pluginName, verbs) {
|
|
107
|
-
if (allowed) return surfaceObj
|
|
108
|
-
return new Proxy({}, {
|
|
109
|
-
get(_, key) {
|
|
110
|
-
if (verbs.includes(String(key))) {
|
|
111
|
-
return () => { throw new Error(`plugin ${pluginName}: surface verb '${String(key)}' not allowed (declared surfaces=${pluginName})`) }
|
|
112
|
-
}
|
|
113
|
-
return surfaceObj[key]
|
|
114
|
-
},
|
|
115
|
-
})
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function scopedConfig(name, store) {
|
|
119
|
-
const key = `plugins.${name}`
|
|
120
|
-
return {
|
|
121
|
-
get(k, d) { return store.get(`${key}.${k}`, d) },
|
|
122
|
-
set(k, v) { return store.set(`${key}.${k}`, v) },
|
|
123
|
-
all() { return store.all(key) || {} },
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
105
|
|
|
127
|
-
function nullStore() {
|
|
128
|
-
const m = new Map()
|
|
129
|
-
return { get: (k, d) => m.has(k) ? m.get(k) : d, set: (k, v) => m.set(k, v), all: (prefix) => Object.fromEntries([...m.entries()].filter(([k]) => k.startsWith(prefix))) }
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function createHost({ surfaces = ['pi', 'gui'], configStore = nullStore(), env = process.env } = {}) {
|
|
133
|
-
const pi = makePiSurface()
|
|
134
|
-
const gui = makeGuiSurface()
|
|
135
|
-
const ccPlugins = []
|
|
136
|
-
const hooks = makeHooks(ccPlugins)
|
|
137
106
|
const loaded = []
|
|
138
107
|
const host = {
|
|
139
108
|
pi: surfaces.includes('pi') ? pi : null,
|
|
140
109
|
gui: surfaces.includes('gui') ? gui : null,
|
|
141
110
|
hooks,
|
|
142
|
-
|
|
111
|
+
binPaths: () => binPaths.slice(),
|
|
112
|
+
ccPlugins: () => ccHost.plugins(),
|
|
113
|
+
onInbound: (fn) => inboundListeners.push(fn),
|
|
143
114
|
plugins: () => loaded.map(p => ({ name: p.name, version: p.version || null, surfaces: p.surfaces, requires: p.requires || [] })),
|
|
144
|
-
get: (
|
|
115
|
+
get: (n) => loaded.find(p => p.name === n) || null,
|
|
116
|
+
shutdown: () => ccHost.shutdown(),
|
|
145
117
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const sorted = topoSort(
|
|
118
|
+
|
|
119
|
+
async function load(plugins) {
|
|
120
|
+
const sorted = topoSort(plugins.map(validatePlugin))
|
|
149
121
|
for (const p of sorted) {
|
|
150
122
|
const want = p.surfaces
|
|
151
|
-
const ctxPi
|
|
123
|
+
const ctxPi = (want === 'pi' || want === 'both') && surfaces.includes('pi') ? pi : guard(pi, false, p.name, PI_VERBS)
|
|
152
124
|
const ctxGui = (want === 'gui' || want === 'both') && surfaces.includes('gui') ? gui : guard(gui, false, p.name, GUI_VERBS)
|
|
153
|
-
const log = (
|
|
154
|
-
const logger = { info:
|
|
155
|
-
const ctx = { pi: ctxPi, gui: ctxGui, hooks, log: logger, config:
|
|
125
|
+
const log = (lv, m, f) => { const line = JSON.stringify({ ts: Date.now(), plugin: p.name, level: lv, msg: m, ...(f || {}) }); if (env.FREDDIE_LOG_STDOUT) console.log(line) }
|
|
126
|
+
const logger = { info:(m,f)=>log('info',m,f), warn:(m,f)=>log('warn',m,f), error:(m,f)=>log('error',m,f) }
|
|
127
|
+
const ctx = { pi: ctxPi, gui: ctxGui, hooks, log: logger, config: scopedCfg(p.name, configStore), host, env }
|
|
156
128
|
await p.register(ctx)
|
|
157
129
|
loaded.push(p)
|
|
158
130
|
}
|
|
159
131
|
return loaded.length
|
|
160
132
|
}
|
|
133
|
+
function isCcPluginDir(dir) {
|
|
134
|
+
if (fs.existsSync(path.join(dir, '.claude-plugin', 'plugin.json'))) return true
|
|
135
|
+
if (!fs.existsSync(path.join(dir, 'plugin.json'))) return false
|
|
136
|
+
return fs.existsSync(path.join(dir, 'hooks', 'hooks.json'))
|
|
137
|
+
|| fs.existsSync(path.join(dir, 'skills'))
|
|
138
|
+
|| fs.existsSync(path.join(dir, 'agents'))
|
|
139
|
+
}
|
|
140
|
+
async function useCcDir(dir) {
|
|
141
|
+
try { await ccHost.use(loadClaudePlugin(dir)) }
|
|
142
|
+
catch (e) { if (env.FREDDIE_LOG_STDOUT) console.error(`cc-plugin ${dir} failed: ${e.message}`) }
|
|
143
|
+
}
|
|
161
144
|
async function loadCcPlugins(roots) {
|
|
162
145
|
for (const root of roots) {
|
|
163
146
|
if (!root || !fs.existsSync(root)) continue
|
|
164
147
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
165
148
|
if (!entry.isDirectory()) continue
|
|
166
149
|
const dir = path.join(root, entry.name)
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
150
|
+
if (isCcPluginDir(dir)) await useCcDir(dir)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return ccHost.plugins().length
|
|
154
|
+
}
|
|
155
|
+
async function loadCcFromNodeModules(startDir) {
|
|
156
|
+
const seen = new Set(ccHost.plugins().map(p => p.root))
|
|
157
|
+
let cur = path.resolve(startDir)
|
|
158
|
+
while (true) {
|
|
159
|
+
const nm = path.join(cur, 'node_modules')
|
|
160
|
+
if (fs.existsSync(nm)) for (const entry of fs.readdirSync(nm, { withFileTypes: true })) {
|
|
161
|
+
if (!entry.isDirectory()) continue
|
|
162
|
+
const dirs = entry.name.startsWith('@')
|
|
163
|
+
? fs.readdirSync(path.join(nm, entry.name), { withFileTypes: true }).filter(e => e.isDirectory()).map(e => path.join(nm, entry.name, e.name))
|
|
164
|
+
: [path.join(nm, entry.name)]
|
|
165
|
+
for (const d of dirs) {
|
|
166
|
+
if (seen.has(d) || !isCcPluginDir(d)) continue
|
|
167
|
+
seen.add(d); await useCcDir(d)
|
|
179
168
|
}
|
|
180
169
|
}
|
|
170
|
+
const parent = path.dirname(cur)
|
|
171
|
+
if (parent === cur) break
|
|
172
|
+
cur = parent
|
|
181
173
|
}
|
|
182
|
-
return
|
|
174
|
+
return ccHost.plugins().length
|
|
183
175
|
}
|
|
184
|
-
host.load =
|
|
176
|
+
host.load = load
|
|
185
177
|
host.loadCcPlugins = loadCcPlugins
|
|
178
|
+
host.loadCcFromNodeModules = loadCcFromNodeModules
|
|
186
179
|
return host
|
|
187
180
|
}
|
|
188
181
|
|
package/src/host/index.js
CHANGED
|
@@ -23,6 +23,8 @@ export async function bootHost(extraRoots = []) {
|
|
|
23
23
|
await h.load(plugins)
|
|
24
24
|
const ccRoots = [path.join(getFreddieHome(), 'cc-plugins'), path.join(process.cwd(), '.freddie', 'cc-plugins')]
|
|
25
25
|
await h.loadCcPlugins(ccRoots)
|
|
26
|
+
await h.loadCcFromNodeModules(__dirname)
|
|
27
|
+
await h.loadCcFromNodeModules(process.cwd())
|
|
26
28
|
return h
|
|
27
29
|
}
|
|
28
30
|
|