freddie 0.0.52 → 0.0.54

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.52",
3
+ "version": "0.0.54",
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.7",
27
+ "plugsdk": "^1.0.12",
28
28
  "xstate": "^5.31.0",
29
29
  "zod": "^4.0.0"
30
30
  },
@@ -1,28 +1,26 @@
1
- import { createRequire } from 'module';
2
- import { fileURLToPath } from 'url';
3
- import path from 'path';
4
- import fs from 'fs';
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
- const skillsDir = path.join(gmCcBase, 'skills');
14
- if (!fs.existsSync(skillsDir)) return;
15
- for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
16
- const skillMd = entry.isDirectory()
17
- ? path.join(skillsDir, entry.name, 'SKILL.md')
18
- : entry.name.endsWith('.md') ? path.join(skillsDir, entry.name) : null;
19
- if (!skillMd || !fs.existsSync(skillMd)) continue;
20
- const raw = fs.readFileSync(skillMd, 'utf8');
21
- const nameMatch = raw.match(/^name:\s*(.+)$/m);
22
- const descMatch = raw.match(/^description:\s*(.+)$/m);
23
- const name = nameMatch ? nameMatch[1].trim() : entry.name.replace(/\.md$/, '');
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
+ }
@@ -1,4 +1,9 @@
1
- export { definePlugin, HookType, allowResult, blockResult, modifyResult, PluginRunner, PluginRuntime, piAdapter } from 'plugsdk'
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.AFTER_RESPONSE,
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 { validatePlugin, topoSort, HOOK_NAMES, PI_VERBS, GUI_VERBS, FREDDIE_TO_SDK_HOOK, piAdapter } from './contract.js'
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 makeHooks() {
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
- const raw = await fn(cur)
83
- if (raw !== undefined && raw !== null && sdkHook && typeof raw === 'object' && 'behavior' in raw) {
84
- cur = piAdapter.translateHookOutput(sdkHook, raw)
85
- } else {
86
- cur = raw ?? cur
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 hooks = makeHooks()
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(p => wrapPlugsdkPlugin(p)).map(validatePlugin)
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
 
@@ -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
- if (!fs.existsSync(d)) continue
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()) { walk(full, out); continue }
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