freddie 0.0.53 → 0.0.55
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/gm-cc/plugin.js +18 -20
- package/src/host/contract.js +16 -4
- package/src/host/host.js +48 -36
- package/src/host/index.js +2 -0
- package/src/skills/index.js +4 -7
- package/src/web/server.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freddie",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.55",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Open JS agent harness built on pi-mono, floosie, xstate, and anentrypoint-design",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"floosie": "^0.6.14",
|
|
25
25
|
"gm-cc": "^2.0.727",
|
|
26
26
|
"js-yaml": "^4.1.0",
|
|
27
|
-
"plugsdk": "^1.0.
|
|
27
|
+
"plugsdk": "^1.0.12",
|
|
28
28
|
"xstate": "^5.31.0",
|
|
29
29
|
"zod": "^4.0.0"
|
|
30
30
|
},
|
package/plugins/gm-cc/plugin.js
CHANGED
|
@@ -1,28 +1,26 @@
|
|
|
1
|
-
import { createRequire } from 'module'
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
1
|
+
import { createRequire } from 'module'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import { loadClaudePlugin } from 'plugsdk'
|
|
5
5
|
|
|
6
|
-
const _require = createRequire(import.meta.url)
|
|
7
|
-
const gmCcBase = path.dirname(_require.resolve('gm-cc/package.json'))
|
|
6
|
+
const _require = createRequire(import.meta.url)
|
|
7
|
+
const gmCcBase = path.dirname(_require.resolve('gm-cc/package.json'))
|
|
8
8
|
|
|
9
9
|
export default {
|
|
10
10
|
name: 'gm-cc',
|
|
11
11
|
surfaces: 'pi',
|
|
12
12
|
register({ pi }) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
for (const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const description = descMatch ? descMatch[1].trim() : '';
|
|
25
|
-
pi.skills.register({ name: 'gm:' + name, description, content: raw, source: 'gm-cc' });
|
|
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
|
+
})
|
|
26
24
|
}
|
|
27
25
|
},
|
|
28
|
-
}
|
|
26
|
+
}
|
package/src/host/contract.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import { HookType } from 'plugsdk'
|
|
2
|
+
|
|
3
|
+
export { HookType }
|
|
4
|
+
export function definePlugin(p) { return p }
|
|
5
|
+
export class PluginRunner {}
|
|
6
|
+
export class PluginRuntime {}
|
|
2
7
|
|
|
3
8
|
export const SURFACES = ['pi', 'gui', 'both']
|
|
4
9
|
|
|
@@ -13,15 +18,22 @@ export const HOOK_NAMES = [
|
|
|
13
18
|
'onMessageInbound', 'onMessageOutbound',
|
|
14
19
|
]
|
|
15
20
|
|
|
16
|
-
import { HookType } from 'plugsdk'
|
|
17
|
-
|
|
18
21
|
export const FREDDIE_TO_SDK_HOOK = {
|
|
19
22
|
preToolCall: HookType.PRE_TOOL_USE,
|
|
20
23
|
postToolCall: HookType.POST_TOOL_USE,
|
|
21
24
|
onSessionStart: HookType.SESSION_START,
|
|
22
25
|
onSessionEnd: HookType.SESSION_END,
|
|
23
26
|
onMessageInbound: HookType.PROMPT_SUBMIT,
|
|
24
|
-
onMessageOutbound: HookType.
|
|
27
|
+
onMessageOutbound: HookType.STOP,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const FREDDIE_TO_NATIVE_HOOK = {
|
|
31
|
+
preToolCall: 'PreToolUse',
|
|
32
|
+
postToolCall: 'PostToolUse',
|
|
33
|
+
onSessionStart: 'SessionStart',
|
|
34
|
+
onSessionEnd: 'SessionEnd',
|
|
35
|
+
onMessageInbound: 'UserPromptSubmit',
|
|
36
|
+
onMessageOutbound: 'Stop',
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
export function validatePlugin(p) {
|
package/src/host/host.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
|
-
import {
|
|
4
|
+
import { loadClaudePlugin, createHookDispatcher } from 'plugsdk'
|
|
5
|
+
import { validatePlugin, topoSort, HOOK_NAMES, PI_VERBS, GUI_VERBS, FREDDIE_TO_NATIVE_HOOK } from './contract.js'
|
|
5
6
|
|
|
6
7
|
function makePiSurface() {
|
|
7
8
|
const tools = new Map()
|
|
@@ -68,23 +69,32 @@ function makeGuiSurface() {
|
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
function
|
|
72
|
+
function ccPayloadFor(freddieHook, payload) {
|
|
73
|
+
if (freddieHook === 'preToolCall' || freddieHook === 'postToolCall')
|
|
74
|
+
return { tool_name: payload?.name, tool_input: payload?.args || payload?.input, tool_response: payload?.result }
|
|
75
|
+
if (freddieHook === 'onMessageInbound' || freddieHook === 'onMessageOutbound')
|
|
76
|
+
return { prompt: payload?.content || payload?.text || '' }
|
|
77
|
+
return payload || {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function makeHooks(ccPlugins) {
|
|
72
81
|
const reg = Object.fromEntries(HOOK_NAMES.map(n => [n, []]))
|
|
82
|
+
const ccDispatch = createHookDispatcher(ccPlugins)
|
|
73
83
|
return {
|
|
74
84
|
on(name, fn) {
|
|
75
85
|
if (!HOOK_NAMES.includes(name)) throw new Error(`unknown hook: ${name}`)
|
|
76
86
|
reg[name].push(fn)
|
|
77
87
|
},
|
|
78
88
|
async invoke(name, payload) {
|
|
79
|
-
const sdkHook = FREDDIE_TO_SDK_HOOK[name]
|
|
80
89
|
let cur = payload
|
|
81
|
-
for (const fn of reg[name] || [])
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
90
|
+
for (const fn of reg[name] || []) cur = (await fn(cur)) ?? cur
|
|
91
|
+
const native = FREDDIE_TO_NATIVE_HOOK[name]
|
|
92
|
+
if (native && ccPlugins.length) {
|
|
93
|
+
const r = await ccDispatch(native, ccPayloadFor(name, cur))
|
|
94
|
+
if (r.decision === 'block') return { ...cur, behavior: 'block', reason: r.reason }
|
|
95
|
+
const pd = r.hookSpecificOutput?.permissionDecision
|
|
96
|
+
if (pd === 'deny') return { ...cur, behavior: 'block', reason: r.hookSpecificOutput?.permissionDecisionReason || 'denied' }
|
|
97
|
+
if (r.hookSpecificOutput?.updatedInput) return { ...cur, ...r.hookSpecificOutput.updatedInput }
|
|
88
98
|
}
|
|
89
99
|
return cur
|
|
90
100
|
},
|
|
@@ -119,44 +129,22 @@ function nullStore() {
|
|
|
119
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))) }
|
|
120
130
|
}
|
|
121
131
|
|
|
122
|
-
const SDK_TO_FREDDIE = Object.fromEntries(Object.entries(FREDDIE_TO_SDK_HOOK).map(([f, s]) => [s, f]))
|
|
123
|
-
|
|
124
|
-
function wrapPlugsdkPlugin(p) {
|
|
125
|
-
if (typeof p.register === 'function') return p
|
|
126
|
-
if (!p.tools && !p.hooks) return p
|
|
127
|
-
return {
|
|
128
|
-
name: p.name,
|
|
129
|
-
surfaces: 'pi',
|
|
130
|
-
register(ctx) {
|
|
131
|
-
for (const [id, tool] of Object.entries(p.tools || {})) {
|
|
132
|
-
ctx.pi.tools.register({
|
|
133
|
-
name: id,
|
|
134
|
-
schema: { name: id, description: tool.description, parameters: tool.parameters },
|
|
135
|
-
handler: (args, rctx) => tool.execute(args, rctx),
|
|
136
|
-
})
|
|
137
|
-
}
|
|
138
|
-
for (const [hookType, fn] of Object.entries(p.hooks || {})) {
|
|
139
|
-
const freddieName = SDK_TO_FREDDIE[hookType]
|
|
140
|
-
if (freddieName) ctx.hooks.on(freddieName, fn)
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
132
|
export function createHost({ surfaces = ['pi', 'gui'], configStore = nullStore(), env = process.env } = {}) {
|
|
147
133
|
const pi = makePiSurface()
|
|
148
134
|
const gui = makeGuiSurface()
|
|
149
|
-
const
|
|
135
|
+
const ccPlugins = []
|
|
136
|
+
const hooks = makeHooks(ccPlugins)
|
|
150
137
|
const loaded = []
|
|
151
138
|
const host = {
|
|
152
139
|
pi: surfaces.includes('pi') ? pi : null,
|
|
153
140
|
gui: surfaces.includes('gui') ? gui : null,
|
|
154
141
|
hooks,
|
|
142
|
+
ccPlugins: () => ccPlugins.slice(),
|
|
155
143
|
plugins: () => loaded.map(p => ({ name: p.name, version: p.version || null, surfaces: p.surfaces, requires: p.requires || [] })),
|
|
156
144
|
get: (name) => loaded.find(p => p.name === name) || null,
|
|
157
145
|
}
|
|
158
146
|
async function loadAll(plugins) {
|
|
159
|
-
const validated = plugins.map(
|
|
147
|
+
const validated = plugins.map(validatePlugin)
|
|
160
148
|
const sorted = topoSort(validated)
|
|
161
149
|
for (const p of sorted) {
|
|
162
150
|
const want = p.surfaces
|
|
@@ -170,7 +158,31 @@ export function createHost({ surfaces = ['pi', 'gui'], configStore = nullStore()
|
|
|
170
158
|
}
|
|
171
159
|
return loaded.length
|
|
172
160
|
}
|
|
161
|
+
async function loadCcPlugins(roots) {
|
|
162
|
+
for (const root of roots) {
|
|
163
|
+
if (!root || !fs.existsSync(root)) continue
|
|
164
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
165
|
+
if (!entry.isDirectory()) continue
|
|
166
|
+
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}`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return ccPlugins.length
|
|
183
|
+
}
|
|
173
184
|
host.load = loadAll
|
|
185
|
+
host.loadCcPlugins = loadCcPlugins
|
|
174
186
|
return host
|
|
175
187
|
}
|
|
176
188
|
|
package/src/host/index.js
CHANGED
|
@@ -21,6 +21,8 @@ export async function bootHost(extraRoots = []) {
|
|
|
21
21
|
const roots = [REPO_PLUGINS, path.join(getFreddieHome(), 'plugins'), path.join(process.cwd(), '.freddie', 'plugins'), ...extraRoots]
|
|
22
22
|
const plugins = await discoverPlugins(roots)
|
|
23
23
|
await h.load(plugins)
|
|
24
|
+
const ccRoots = [path.join(getFreddieHome(), 'cc-plugins'), path.join(process.cwd(), '.freddie', 'cc-plugins')]
|
|
25
|
+
await h.loadCcPlugins(ccRoots)
|
|
24
26
|
return h
|
|
25
27
|
}
|
|
26
28
|
|
package/src/skills/index.js
CHANGED
|
@@ -9,18 +9,15 @@ const FRONTMATTER = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/
|
|
|
9
9
|
export function listSkills(extraDirs = []) {
|
|
10
10
|
const dirs = [path.join(getFreddieHome(), 'skills'), path.join(process.cwd(), 'skills'), ...extraDirs]
|
|
11
11
|
const out = []
|
|
12
|
-
for (const d of dirs)
|
|
13
|
-
|
|
14
|
-
walk(d, out)
|
|
15
|
-
}
|
|
16
|
-
return out.filter(s => platformOk(s))
|
|
12
|
+
for (const d of dirs) if (fs.existsSync(d)) walk(d, out)
|
|
13
|
+
return out.filter(platformOk)
|
|
17
14
|
}
|
|
18
15
|
|
|
19
16
|
function walk(d, out) {
|
|
20
17
|
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
21
18
|
const full = path.join(d, entry.name)
|
|
22
|
-
if (entry.isDirectory())
|
|
23
|
-
if (entry.name === 'SKILL.md') out.push(loadSkill(full))
|
|
19
|
+
if (entry.isDirectory()) walk(full, out)
|
|
20
|
+
else if (entry.name === 'SKILL.md') out.push(loadSkill(full))
|
|
24
21
|
}
|
|
25
22
|
}
|
|
26
23
|
|
package/src/web/server.js
CHANGED
|
@@ -17,7 +17,7 @@ export async function createDashboard({ port = 0 } = {}) {
|
|
|
17
17
|
}
|
|
18
18
|
const debugApi = host.gui._state.apis.get('debug')
|
|
19
19
|
if (debugApi?.attach) debugApi.attach(app)
|
|
20
|
-
const server = await new Promise(
|
|
20
|
+
const server = await new Promise((res, rej) => { const s = app.listen(port, () => res(s)); s.once('error', rej) })
|
|
21
21
|
const actualPort = server.address().port
|
|
22
22
|
return { server, port: actualPort, url: `http://127.0.0.1:${actualPort}/`, stop: () => new Promise(r => server.close(() => r())) }
|
|
23
23
|
}
|