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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freddie",
3
- "version": "0.0.57",
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.12",
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, createHookDispatcher } from 'plugsdk'
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 makePiSurface() {
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
- _state: { tools, envs, commands, crons, platforms, memory, skills, contexts, agentExts, cli },
20
- tools: regOf(tools, 'tool'),
21
- envs: regOf(envs, 'env'),
22
- commands: regOf(commands, 'command'),
23
- crons: regOf(crons, 'cron'),
24
- platforms: regOf(platforms, 'platform'),
25
- memory: regOf(memory, 'memory'),
26
- skills: regOf(skills, 'skill'),
27
- contexts: regOf(contexts, 'context'),
28
- agentExts: regOf(agentExts, 'agentExt'),
29
- cli: regOf(cli, 'cli'),
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 regOf(map, kind) {
41
- return {
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, path, handler) { routes.push({ method: method.toUpperCase(), path, handler }) },
60
- page(slug, def) { pages.set(slug, def) },
61
- nav(item) { nav.push(item) },
62
- debug(name, snapshotFn) { debugs.set(name, snapshotFn) },
63
- api(group, def) { apis.set(group, def) },
64
- asset(p, content) { assets.set(p, content) },
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(freddieHook, payload) {
73
- if (freddieHook === 'preToolCall' || freddieHook === 'postToolCall')
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 (freddieHook === 'onMessageInbound' || freddieHook === 'onMessageOutbound')
50
+ if (name === 'onMessageInbound' || name === 'onMessageOutbound')
76
51
  return { prompt: payload?.content || payload?.text || '' }
77
52
  return payload || {}
78
53
  }
79
54
 
80
- function makeHooks(ccPlugins) {
81
- const reg = Object.fromEntries(HOOK_NAMES.map(n => [n, []]))
82
- const ccDispatch = createHookDispatcher(ccPlugins)
83
- return {
84
- on(name, fn) {
85
- if (!HOOK_NAMES.includes(name)) throw new Error(`unknown hook: ${name}`)
86
- reg[name].push(fn)
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 reg[name] || []) cur = (await fn(cur)) ?? cur
92
+ for (const fn of reg2[name] || []) cur = (await fn(cur)) ?? cur
91
93
  const native = FREDDIE_TO_NATIVE_HOOK[name]
92
- if (native && ccPlugins.length) {
93
- const r = await ccDispatch(native, ccPayloadFor(name, cur))
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() { return HOOK_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
- ccPlugins: () => ccPlugins.slice(),
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: (name) => loaded.find(p => p.name === name) || null,
115
+ get: (n) => loaded.find(p => p.name === n) || null,
116
+ shutdown: () => ccHost.shutdown(),
145
117
  }
146
- async function loadAll(plugins) {
147
- const validated = plugins.map(validatePlugin)
148
- const sorted = topoSort(validated)
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 = (want === 'pi' || want === 'both') && surfaces.includes('pi') ? pi : guard(pi, false, p.name, PI_VERBS)
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 = (level, msg, fields) => { const line = JSON.stringify({ ts: Date.now(), plugin: p.name, level, msg, ...(fields || {}) }); if (env.FREDDIE_LOG_STDOUT) console.log(line) }
154
- const logger = { info: (m, f) => log('info', m, f), warn: (m, f) => log('warn', m, f), error: (m, f) => log('error', m, f) }
155
- const ctx = { pi: ctxPi, gui: ctxGui, hooks, log: logger, config: scopedConfig(p.name, configStore), host, env }
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 (!fs.existsSync(path.join(dir, '.claude-plugin', 'plugin.json'))) continue
168
- try {
169
- const cc = loadClaudePlugin(dir)
170
- ccPlugins.push(cc)
171
- if (surfaces.includes('pi')) {
172
- for (const s of cc.skills)
173
- pi.skills.register({ name: cc.manifest.name + ':' + s.name, description: s.fields.description || '', content: s.body, source: 'cc:' + cc.manifest.name, frontmatter: s.fields, file: s.file })
174
- for (const a of cc.agents)
175
- pi.agentExts.register({ name: cc.manifest.name + ':' + a.name, description: a.fields.description || '', frontmatter: a.fields, body: a.body, source: 'cc:' + cc.manifest.name, file: a.file })
176
- }
177
- } catch (e) {
178
- if (env.FREDDIE_LOG_STDOUT) console.error(`cc-plugin ${dir} failed: ${e.message}`)
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 ccPlugins.length
174
+ return ccHost.plugins().length
183
175
  }
184
- host.load = loadAll
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