freddie 0.0.121 → 0.0.122
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/AGENTS.md +35 -10
- package/package.json +3 -2
- package/plugins/core-cli/plugin.js +132 -5
- package/plugins/gm-skill/plugin.js +16 -4
- package/plugins/gui-auth/plugin.js +45 -0
- package/plugins/gui-chat/plugin.js +21 -4
- package/plugins/gui-env/plugin.js +14 -1
- package/plugins/gui-sessions/plugin.js +7 -1
- package/plugins/memory/handler.js +23 -46
- package/plugins/platform-discord/handler.js +83 -1
- package/plugins/platform-whatsapp/handler.js +31 -5
- package/src/acp/server.js +1 -1
- package/src/acp/tools.js +5 -1
- package/src/agent/acptoapi-bridge.js +33 -9
- package/src/agent/llm_resolver.js +10 -1
- package/src/agent/machine.js +61 -12
- package/src/agent/tool_call_text.js +68 -0
- package/src/batch.js +4 -3
- package/src/browser/index.js +4 -0
- package/src/cli/cli_output.js +4 -4
- package/src/cli/interactive.js +58 -9
- package/src/cli/memory_setup.js +18 -4
- package/src/cli/relaunch.js +2 -2
- package/src/cli/stdin_secret.js +31 -0
- package/src/context/engine.js +7 -4
- package/src/gateway/run.js +18 -6
- package/src/index.js +1 -0
- package/src/learn/gm-learn.js +179 -0
- package/src/plugins/case/index.js +28 -0
- package/src/plugins/case/toolset.js +312 -0
- package/src/sessions.js +38 -1
- package/src/toolset_distributions.js +3 -0
- package/src/toolsets.js +1 -1
- package/src/web/app.js +55 -12
- package/src/web/index.html +30 -3
- package/src/web/server.js +51 -2
- package/src/web/state.js +74 -32
package/src/cli/interactive.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import readline from 'node:readline'
|
|
2
2
|
import { runTurn } from '../agent/machine.js'
|
|
3
|
-
import { resolveCommand,
|
|
3
|
+
import { resolveCommand, COMMANDS_BY_CATEGORY } from '../commands/registry.js'
|
|
4
4
|
import { getActiveSkin } from '../skin/engine.js'
|
|
5
|
-
import { createSession, appendMessage } from '../sessions.js'
|
|
5
|
+
import { createSession, appendMessage, listSessions, getMessages } from '../sessions.js'
|
|
6
6
|
import { listAllProfiles, switchProfile } from '../commands/profile.js'
|
|
7
|
+
import { listAuthProviders, hasUsableSecret, envForProvider } from '../auth.js'
|
|
8
|
+
import { listProjects, getActiveProject, setActiveProject } from '../projects.js'
|
|
7
9
|
|
|
10
|
+
// REPL slash-command handlers. Each returns a string to print (or sets
|
|
11
|
+
// state.exit / mutates state for resume). Handlers may be async — the line
|
|
12
|
+
// loop awaits them.
|
|
8
13
|
const HANDLERS = {
|
|
9
14
|
help: () => {
|
|
10
15
|
const out = []
|
|
@@ -12,6 +17,7 @@ const HANDLERS = {
|
|
|
12
17
|
out.push(`\n# ${cat}`)
|
|
13
18
|
for (const c of cmds) out.push(` /${c.name}${c.args_hint ? ' ' + c.args_hint : ''}\t${c.description}`)
|
|
14
19
|
}
|
|
20
|
+
out.push('\n# Conversation\n /sessions\tList recent conversations\n /resume <id>\tContinue a past conversation\n /keys\tShow which provider keys are set\n /project [name]\tShow or switch active project')
|
|
15
21
|
return out.join('\n')
|
|
16
22
|
},
|
|
17
23
|
quit: (state) => { state.exit = true; return 'bye.' },
|
|
@@ -20,13 +26,55 @@ const HANDLERS = {
|
|
|
20
26
|
if (args[0] === 'switch' && args[1]) { switchProfile(args[1]); return 'switched: ' + args[1] }
|
|
21
27
|
return 'usage: /profile [list|switch <name>]'
|
|
22
28
|
},
|
|
23
|
-
sessions: () =>
|
|
29
|
+
sessions: async () => {
|
|
30
|
+
const rows = await listSessions(20)
|
|
31
|
+
if (!rows.length) return '(no sessions yet)'
|
|
32
|
+
return rows.map(s => ` ${s.id.slice(0, 8)} ${new Date(s.updated_at).toISOString().slice(0, 16).replace('T', ' ')} ${s.title || '(untitled)'}`).join('\n')
|
|
33
|
+
},
|
|
34
|
+
resume: async (state, args) => {
|
|
35
|
+
const wanted = args[0]
|
|
36
|
+
const rows = await listSessions(50)
|
|
37
|
+
if (!rows.length) return '(no sessions to resume)'
|
|
38
|
+
const target = wanted
|
|
39
|
+
? rows.find(s => s.id === wanted || s.id.startsWith(wanted))
|
|
40
|
+
: rows[0]
|
|
41
|
+
if (!target) return `no session matching: ${wanted}`
|
|
42
|
+
const msgs = await getMessages(target.id)
|
|
43
|
+
state.session = target.id
|
|
44
|
+
state.messages = msgs.map(m => ({ role: m.role, content: m.content, tool_calls: m.tool_calls || undefined, tool_call_id: m.tool_call_id || undefined }))
|
|
45
|
+
return `resumed ${target.id.slice(0, 8)} (${msgs.length} messages) — ${target.title || '(untitled)'}`
|
|
46
|
+
},
|
|
47
|
+
keys: async () => {
|
|
48
|
+
const lines = []
|
|
49
|
+
for (const p of listAuthProviders()) {
|
|
50
|
+
const ok = await hasUsableSecret(p)
|
|
51
|
+
lines.push(` ${p.padEnd(12)} ${envForProvider(p) || ''}\t${ok ? '[set]' : '[--]'}`)
|
|
52
|
+
}
|
|
53
|
+
return lines.join('\n')
|
|
54
|
+
},
|
|
55
|
+
project: (_s, args) => {
|
|
56
|
+
if (!args[0]) {
|
|
57
|
+
const active = getActiveProject()
|
|
58
|
+
return listProjects().map(p => ` ${p.name === active.name ? '[*]' : '[ ]'} ${p.name.padEnd(16)} ${p.path}`).join('\n')
|
|
59
|
+
}
|
|
60
|
+
try { const p = setActiveProject(args[0]); return `switched to project: ${p.name} (${p.path})\nrestart the REPL to load this project's plugins` }
|
|
61
|
+
catch (e) { return 'error: ' + e.message }
|
|
62
|
+
},
|
|
24
63
|
clear: (state) => { state.messages = []; return 'cleared.' },
|
|
25
64
|
}
|
|
26
65
|
|
|
27
|
-
export async function interactive({ callLLM, input = process.stdin, output = process.stdout } = {}) {
|
|
66
|
+
export async function interactive({ callLLM, resume = null, input = process.stdin, output = process.stdout } = {}) {
|
|
28
67
|
const skin = getActiveSkin()
|
|
29
|
-
const state = { messages: [], session:
|
|
68
|
+
const state = { messages: [], session: null, exit: false }
|
|
69
|
+
// Resume a prior conversation when requested (--resume [id]); otherwise start
|
|
70
|
+
// a fresh session. createSession/listSessions are async (libsql) and MUST be
|
|
71
|
+
// awaited — a bare call silently wraps in a rejecting Promise so the row is
|
|
72
|
+
// never persisted and history is lost.
|
|
73
|
+
if (resume !== null && resume !== false) {
|
|
74
|
+
const msg = await HANDLERS.resume(state, typeof resume === 'string' ? [resume] : [])
|
|
75
|
+
output.write(msg + '\n')
|
|
76
|
+
}
|
|
77
|
+
if (!state.session) state.session = await createSession({ platform: 'cli' })
|
|
30
78
|
output.write(`${skin.branding.welcome}\n`)
|
|
31
79
|
const rl = readline.createInterface({ input, output, terminal: input.isTTY })
|
|
32
80
|
const prompt = () => { if (!state.exit) rl.setPrompt(skin.branding.prompt_symbol); rl.prompt() }
|
|
@@ -35,21 +83,22 @@ export async function interactive({ callLLM, input = process.stdin, output = pro
|
|
|
35
83
|
if (!line) return prompt()
|
|
36
84
|
if (line.startsWith('/')) {
|
|
37
85
|
const parts = line.slice(1).split(/\s+/)
|
|
38
|
-
const name = resolveCommand('/' + parts[0])
|
|
86
|
+
const name = resolveCommand('/' + parts[0]) || parts[0]
|
|
39
87
|
const handler = HANDLERS[name]
|
|
40
88
|
if (!handler) { output.write(`unknown command: /${parts[0]}\n`); return prompt() }
|
|
41
|
-
output.write(handler(state, parts.slice(1)) + '\n')
|
|
89
|
+
try { output.write((await handler(state, parts.slice(1))) + '\n') }
|
|
90
|
+
catch (e) { output.write(`error: ${e.message}\n`) }
|
|
42
91
|
if (state.exit) rl.close()
|
|
43
92
|
else prompt()
|
|
44
93
|
return
|
|
45
94
|
}
|
|
46
|
-
appendMessage(state.session, { role: 'user', content: line })
|
|
95
|
+
await appendMessage(state.session, { role: 'user', content: line })
|
|
47
96
|
try {
|
|
48
97
|
const out = await runTurn({ prompt: line, messages: state.messages, callLLM, timeoutMs: 60000 })
|
|
49
98
|
state.messages = out.messages
|
|
50
99
|
const reply = out.result || out.error || '(no response)'
|
|
51
100
|
output.write(`${skin.branding.response_label}${reply}\n`)
|
|
52
|
-
appendMessage(state.session, { role: 'assistant', content: reply })
|
|
101
|
+
await appendMessage(state.session, { role: 'assistant', content: reply })
|
|
53
102
|
} catch (e) {
|
|
54
103
|
output.write(`error: ${e.message}\n`)
|
|
55
104
|
}
|
package/src/cli/memory_setup.js
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import { saveConfigValue } from '../config.js'
|
|
2
2
|
import { getAuthStore } from '../auth.js'
|
|
3
3
|
import { listMemoryProviders } from '../plugins/memory/provider.js'
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
// gm rs-learn is freddie's canonical, default learning store — no configuration required.
|
|
6
|
+
// The third-party providers below are LEGACY: opt-in only, kept behind explicit configure.
|
|
7
|
+
export const DEFAULT_PROVIDER = 'gm'
|
|
8
|
+
export const LEGACY_PROVIDER_ENV = { honcho: 'HONCHO_API_KEY', mem0: 'MEM0_API_KEY', supermemory: 'SUPERMEMORY_API_KEY', byterover: 'BYTEROVER_API_KEY', hindsight: 'HINDSIGHT_API_KEY', openviking: 'OPENVIKING_API_KEY', retaindb: 'RETAINDB_API_KEY' }
|
|
9
|
+
// Back-compat alias.
|
|
10
|
+
export const PROVIDER_ENV = LEGACY_PROVIDER_ENV
|
|
11
|
+
|
|
12
|
+
export function listProviders() { return [DEFAULT_PROVIDER, ...listMemoryProviders().filter(n => n !== DEFAULT_PROVIDER)] }
|
|
13
|
+
|
|
6
14
|
export async function configureProvider(name, apiKey, options = {}) {
|
|
15
|
+
// gm needs no key/config; selecting it just clears any legacy provider override.
|
|
16
|
+
if (name === DEFAULT_PROVIDER || name === 'rs-learn') {
|
|
17
|
+
saveConfigValue('memory.provider', DEFAULT_PROVIDER)
|
|
18
|
+
saveConfigValue('memory.options', options)
|
|
19
|
+
return { configured: DEFAULT_PROVIDER, hasKey: false, default: true }
|
|
20
|
+
}
|
|
7
21
|
if (!listMemoryProviders().includes(name)) throw new Error('unknown memory provider: ' + name)
|
|
8
22
|
saveConfigValue('memory.provider', name)
|
|
9
23
|
saveConfigValue('memory.options', options)
|
|
10
|
-
if (apiKey &&
|
|
11
|
-
return { configured: name, hasKey: Boolean(apiKey) }
|
|
24
|
+
if (apiKey && LEGACY_PROVIDER_ENV[name]) await getAuthStore().setCredential(LEGACY_PROVIDER_ENV[name], apiKey)
|
|
25
|
+
return { configured: name, hasKey: Boolean(apiKey), legacy: true }
|
|
12
26
|
}
|
package/src/cli/relaunch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export function relaunch({ argv = process.argv } = {}) {
|
|
2
|
-
const { spawn } =
|
|
1
|
+
export async function relaunch({ argv = process.argv } = {}) {
|
|
2
|
+
const { spawn } = await import('node:child_process')
|
|
3
3
|
const child = spawn(argv[0], argv.slice(1), { detached: true, stdio: 'ignore', env: process.env })
|
|
4
4
|
child.unref()
|
|
5
5
|
setTimeout(() => process.exit(0), 100)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import readline from 'node:readline'
|
|
2
|
+
|
|
3
|
+
// Read a secret (API key) without it landing in shell history or `ps` output.
|
|
4
|
+
// - When stdin is a pipe (not a TTY), read the first line of stdin verbatim:
|
|
5
|
+
// `echo $KEY | freddie auth set anthropic`.
|
|
6
|
+
// - When stdin is a TTY, prompt and read with the terminal echo masked so the
|
|
7
|
+
// key is not shown on screen.
|
|
8
|
+
// Never accept the secret as an argv argument — argv leaks to history and ps.
|
|
9
|
+
export async function readStdinSecret(promptText = 'secret: ', { input = process.stdin, output = process.stdout } = {}) {
|
|
10
|
+
if (!input.isTTY) {
|
|
11
|
+
// Piped input: take the first non-empty line.
|
|
12
|
+
const rl = readline.createInterface({ input, terminal: false })
|
|
13
|
+
for await (const line of rl) { rl.close(); return line.trim() }
|
|
14
|
+
return ''
|
|
15
|
+
}
|
|
16
|
+
// Interactive TTY: prompt and mask the echo.
|
|
17
|
+
return await new Promise((resolve) => {
|
|
18
|
+
const rl = readline.createInterface({ input, output, terminal: true })
|
|
19
|
+
let masked = false
|
|
20
|
+
const onData = () => { if (masked) output.write('\x1b[2K\r' + promptText + '*'.repeat(rl.line.length)) }
|
|
21
|
+
output.write(promptText)
|
|
22
|
+
masked = true
|
|
23
|
+
input.on('data', onData)
|
|
24
|
+
rl.question('', (answer) => {
|
|
25
|
+
input.off('data', onData)
|
|
26
|
+
output.write('\n')
|
|
27
|
+
rl.close()
|
|
28
|
+
resolve(answer.trim())
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
}
|
package/src/context/engine.js
CHANGED
|
@@ -15,11 +15,14 @@ export const ContextPlugins = {
|
|
|
15
15
|
skills: async () => {
|
|
16
16
|
return listSkills().map(s => ({ name: 'skill:' + s.name, body: s.description }))
|
|
17
17
|
},
|
|
18
|
-
memory: async ({
|
|
19
|
-
|
|
18
|
+
memory: async ({ message = '', namespace = null } = {}) => {
|
|
19
|
+
// Query-aware semantic recall from gm rs-learn — freddie's primary learning store.
|
|
20
20
|
try {
|
|
21
|
-
const
|
|
22
|
-
|
|
21
|
+
const { recall, projectNamespace } = await import('../learn/gm-learn.js')
|
|
22
|
+
const ns = namespace || await projectNamespace()
|
|
23
|
+
const q = (message || '').toString().trim() || 'project notes facts decisions'
|
|
24
|
+
const hits = await recall(q, { limit: 5, namespace: ns })
|
|
25
|
+
return hits.map((h, i) => ({ name: 'memory:' + i, body: h.text }))
|
|
23
26
|
} catch { return [] }
|
|
24
27
|
},
|
|
25
28
|
}
|
package/src/gateway/run.js
CHANGED
|
@@ -36,13 +36,13 @@ export class Gateway {
|
|
|
36
36
|
this.actor = createActor(this.machine, { input: { platformNames: [...this.platforms.keys()] } })
|
|
37
37
|
// Persist lifecycle transitions so the gateway's state is observable +
|
|
38
38
|
// resumable; an active snapshot on boot means the gateway was running.
|
|
39
|
-
this.actor.subscribe((snap) => { persist('gateway', 'lifecycle', this.actor.getPersistedSnapshot()).catch(
|
|
39
|
+
this.actor.subscribe((snap) => { persist('gateway', 'lifecycle', this.actor.getPersistedSnapshot()).catch(e => log.error('gateway lifecycle persist failed', { err: String(e) })) })
|
|
40
40
|
this.actor.start()
|
|
41
41
|
}
|
|
42
42
|
get state() { return this.actor.getSnapshot().value }
|
|
43
43
|
register(name, adapter) {
|
|
44
44
|
this.platforms.set(name, adapter)
|
|
45
|
-
adapter.on?.('message', (m) => this.handleInbound(name, m))
|
|
45
|
+
adapter.on?.('message', (m) => { this.handleInbound(name, m).catch(e => log.error('message listener error', { platform: name, from: m.from, error: String(e) })) })
|
|
46
46
|
}
|
|
47
47
|
addHook(stage, fn) { this.hooks[stage].push(fn) }
|
|
48
48
|
async start() {
|
|
@@ -63,16 +63,28 @@ export class Gateway {
|
|
|
63
63
|
// sender + content so a refresh mid-processing re-drives it instead of
|
|
64
64
|
// dropping it. The snapshot is cleared once the reply is sent.
|
|
65
65
|
const msgKey = msg.id || `${platform}:${msg.from}:${randomUUID()}`
|
|
66
|
-
|
|
66
|
+
// A persist failure must not drop the message: degrade to non-resumable
|
|
67
|
+
// processing (the reply still goes out) rather than throwing it away.
|
|
68
|
+
try {
|
|
69
|
+
await persist('gateway-msg', msgKey, { status: 'active', value: 'processing', context: { platform, from: msg.from, text: msg.text } })
|
|
70
|
+
} catch (e) {
|
|
71
|
+
log.warn('cannot persist gateway-msg, continuing without resumability', { msgKey, err: String(e) })
|
|
72
|
+
}
|
|
67
73
|
let cur = { ...msg, platform }
|
|
68
74
|
for (const h of this.hooks.inbound) cur = (await h(cur)) || cur
|
|
69
75
|
const result = await runStep(msgKey, 'run', () => runTurn({ prompt: cur.text || '', callLLM: this.callLLM }))
|
|
70
76
|
let reply = { to: msg.from, text: result.result || result.error || '', platform, result }
|
|
71
77
|
for (const h of this.hooks.outbound) reply = (await h(reply)) || reply
|
|
72
78
|
const adapter = this.platforms.get(platform)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
// Clear the in-flight snapshot whether or not the send throws: a send
|
|
80
|
+
// failure must not leave the message to be re-driven (and re-replied) on
|
|
81
|
+
// the next boot. The contract is "cleared once processing completes".
|
|
82
|
+
try {
|
|
83
|
+
await adapter.send?.(reply)
|
|
84
|
+
} finally {
|
|
85
|
+
await clear('gateway-msg', msgKey)
|
|
86
|
+
await clearSteps(msgKey)
|
|
87
|
+
}
|
|
76
88
|
return reply
|
|
77
89
|
}
|
|
78
90
|
}
|
package/src/index.js
CHANGED
|
@@ -21,6 +21,7 @@ export { createEnvironment, defaultEnvironment, LocalEnvironment, DockerEnvironm
|
|
|
21
21
|
export { getAuthStore } from './auth.js'
|
|
22
22
|
export { buildContext, blocksToSystemMessage, ContextPlugins } from './context/engine.js'
|
|
23
23
|
export { callLLM as piCallLLM } from './agent/pi-bridge.js'
|
|
24
|
+
export { callLLM as acptoapiCallLLM, isReachable as acptoapiReachable, getAcptoapiUrl, getAcptoapiModel } from './agent/acptoapi-bridge.js'
|
|
24
25
|
export { interactive } from './cli/interactive.js'
|
|
25
26
|
export { listMemoryProviders, createMemoryProvider, registerMemoryProvider } from './plugins/memory/provider.js'
|
|
26
27
|
export { compress, shouldCompress, computeCompressionPlan, SUMMARY_PREFIX, estimateMessagesTokens, pruneOldToolResults, markFailure as compressMarkFailure, shouldRetry as compressShouldRetry, clearFailure as compressClearFailure } from './agent/compress/index.js'
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// gm rs-learn — freddie's primary learning mechanism.
|
|
2
|
+
//
|
|
3
|
+
// Routes all of freddie's learning (memory tool, turn-context recall, auto-recall on turn
|
|
4
|
+
// entry, auto-learn on turn completion) through gm-plugkit's rs-learn store. One backend is
|
|
5
|
+
// chosen lazily on first use and cached process-wide:
|
|
6
|
+
//
|
|
7
|
+
// - Node: gm-plugkit's wasm wrapper is imported in-process (createPlugkit), resolving
|
|
8
|
+
// .gm/rs-learn.db from CLAUDE_PROJECT_DIR||cwd of this process.
|
|
9
|
+
// - Browser: a host-provided bridge (globalThis.__GM_DISPATCH__) routes verbs to the
|
|
10
|
+
// gm wasm instance the host already loaded in-page (e.g. thebird's
|
|
11
|
+
// window.__debug.gm.dispatch). This is what makes freddie LEARN on gh-pages,
|
|
12
|
+
// where node:module is unavailable and an in-process import would throw.
|
|
13
|
+
//
|
|
14
|
+
// Every call degrades to a no-op (never throws into the agent loop) when no backend is
|
|
15
|
+
// available, so a freddie process/page without gm installed still runs.
|
|
16
|
+
|
|
17
|
+
let _initPromise = null // shared in-flight backend selection (no double cold-load)
|
|
18
|
+
let _failed = false // sticky failure flag — stop retrying a missing/broken install
|
|
19
|
+
let _pk = null // cached backend: { dispatch(verb, body) -> json|Promise<json>, version() }
|
|
20
|
+
|
|
21
|
+
const _isBrowser = typeof window !== 'undefined' || typeof importScripts === 'function'
|
|
22
|
+
|
|
23
|
+
// The host-provided in-page bridge contract. A host (thebird) sets one of:
|
|
24
|
+
// globalThis.__GM_DISPATCH__(verb, body) -> json | Promise<json> (preferred)
|
|
25
|
+
// globalThis.__gm.dispatch(verb, body) -> json | Promise<json> (fallback shape)
|
|
26
|
+
// We probe lazily on every ensure() so a late-loading wasm (149MB cold-load) is picked up
|
|
27
|
+
// once it becomes available rather than being cached as "failed" forever.
|
|
28
|
+
function findBrowserBridge() {
|
|
29
|
+
const g = (typeof globalThis !== 'undefined') ? globalThis : null
|
|
30
|
+
if (!g) return null
|
|
31
|
+
if (typeof g.__GM_DISPATCH__ === 'function') return { dispatch: g.__GM_DISPATCH__ }
|
|
32
|
+
const gm = g.__gm || (g.__debug && g.__debug.gm)
|
|
33
|
+
if (gm && typeof gm.dispatch === 'function') return { dispatch: (v, b) => gm.dispatch(v, b) }
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function ensureNodePlugkit() {
|
|
38
|
+
const { createRequire } = await import('node:module')
|
|
39
|
+
const path = (await import('node:path')).default
|
|
40
|
+
const _require = createRequire(import.meta.url)
|
|
41
|
+
// index.js is CommonJS; the createPlugkit export lives on the ESM wrapper file, so resolve
|
|
42
|
+
// the wrapper path off the package and import it directly.
|
|
43
|
+
const pkgJson = _require.resolve('gm-plugkit/package.json')
|
|
44
|
+
const wrapper = path.join(path.dirname(pkgJson), 'plugkit-wasm-wrapper.js')
|
|
45
|
+
const url = 'file://' + wrapper.replace(/\\/g, '/')
|
|
46
|
+
const mod = await import(url)
|
|
47
|
+
if (typeof mod.createPlugkit !== 'function') throw new Error('gm-plugkit createPlugkit export missing (update gm-plugkit)')
|
|
48
|
+
return mod.createPlugkit()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function ensurePlugkit() {
|
|
52
|
+
if (_pk) return _pk
|
|
53
|
+
// In the browser the bridge can appear AFTER first probe (wasm cold-load). Never set the
|
|
54
|
+
// sticky _failed flag there — just re-probe each call until the host wires the global.
|
|
55
|
+
if (_isBrowser) {
|
|
56
|
+
const bridge = findBrowserBridge()
|
|
57
|
+
if (!bridge) return null
|
|
58
|
+
_pk = { dispatch: bridge.dispatch, version: () => 'browser-bridge' }
|
|
59
|
+
return _pk
|
|
60
|
+
}
|
|
61
|
+
if (_failed) return null
|
|
62
|
+
if (_initPromise) return _initPromise
|
|
63
|
+
_initPromise = (async () => {
|
|
64
|
+
try {
|
|
65
|
+
_pk = await ensureNodePlugkit()
|
|
66
|
+
return _pk
|
|
67
|
+
} catch (e) {
|
|
68
|
+
_failed = true
|
|
69
|
+
try { console.error('[gm-learn] disabled (gm rs-learn unavailable):', e && e.message) } catch (_) {}
|
|
70
|
+
return null
|
|
71
|
+
} finally {
|
|
72
|
+
_initPromise = null
|
|
73
|
+
}
|
|
74
|
+
})()
|
|
75
|
+
return _initPromise
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function learnAvailable() { return Boolean(_pk) || Boolean(_isBrowser && findBrowserBridge()) }
|
|
79
|
+
|
|
80
|
+
// Per-project namespace isolation, matching gm's namespace model.
|
|
81
|
+
// - Browser: the host sets globalThis.__GM_NAMESPACE__ (a string, or a fn returning one)
|
|
82
|
+
// to the active workspace/instance so memories isolate per thebird instance.
|
|
83
|
+
// - Node: derive from the freddie project registry (src/projects.js).
|
|
84
|
+
// Falls back to 'default' if neither is resolvable (e.g. early boot).
|
|
85
|
+
export async function projectNamespace() {
|
|
86
|
+
if (_isBrowser) {
|
|
87
|
+
try {
|
|
88
|
+
const g = globalThis
|
|
89
|
+
const ns = typeof g.__GM_NAMESPACE__ === 'function' ? g.__GM_NAMESPACE__() : g.__GM_NAMESPACE__
|
|
90
|
+
const s = (ns == null ? '' : String(ns)).trim()
|
|
91
|
+
return s || 'default'
|
|
92
|
+
} catch (_) { return 'default' }
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const mod = await import('../projects.js')
|
|
96
|
+
const p = mod.getActiveProject && mod.getActiveProject()
|
|
97
|
+
return (p && p.name) || 'default'
|
|
98
|
+
} catch (_) { return 'default' }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Normalize a recall response into a flat hit list: [{ text, score, key, namespace }].
|
|
102
|
+
function normalizeHits(resp) {
|
|
103
|
+
const hits = (resp && resp.data && Array.isArray(resp.data.hits)) ? resp.data.hits
|
|
104
|
+
: (resp && Array.isArray(resp.hits)) ? resp.hits
|
|
105
|
+
: []
|
|
106
|
+
return hits.map(h => ({
|
|
107
|
+
text: h.text != null ? String(h.text) : '',
|
|
108
|
+
score: typeof h.score === 'number' ? h.score : (typeof h.cos === 'number' ? h.cos : 0),
|
|
109
|
+
key: h.key || null,
|
|
110
|
+
namespace: h.namespace || 'default',
|
|
111
|
+
})).filter(h => h.text)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Persist a fact into rs-learn. Returns the stored key, or null on no-op/degrade.
|
|
115
|
+
export async function memorize(text, { namespace = 'default', key = null } = {}) {
|
|
116
|
+
const t = (text || '').toString().trim()
|
|
117
|
+
if (!t) return null
|
|
118
|
+
const pk = await ensurePlugkit()
|
|
119
|
+
if (!pk) return null
|
|
120
|
+
try {
|
|
121
|
+
const body = { text: t, namespace }
|
|
122
|
+
if (key) body.key = key
|
|
123
|
+
const r = await pk.dispatch('memorize-fire', body)
|
|
124
|
+
if (r && r.ok === false) return null
|
|
125
|
+
return (r && r.data && r.data.key) || (r && r.key) || null
|
|
126
|
+
} catch (e) {
|
|
127
|
+
try { console.error('[gm-learn] memorize failed:', e && e.message) } catch (_) {}
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Semantic recall against rs-learn. Returns [{ text, score, key, namespace }] (possibly []).
|
|
133
|
+
export async function recall(query, { limit = 5, namespace = 'default' } = {}) {
|
|
134
|
+
const q = (query || '').toString().trim()
|
|
135
|
+
if (!q) return []
|
|
136
|
+
const pk = await ensurePlugkit()
|
|
137
|
+
if (!pk) return []
|
|
138
|
+
try {
|
|
139
|
+
const r = await pk.dispatch('recall', { query: q, limit, namespace })
|
|
140
|
+
if (r && r.ok === false) return []
|
|
141
|
+
return normalizeHits(r).slice(0, limit)
|
|
142
|
+
} catch (e) {
|
|
143
|
+
try { console.error('[gm-learn] recall failed:', e && e.message) } catch (_) {}
|
|
144
|
+
return []
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Turn-entry auto-recall for a raw user prompt. Same store as recall(); kept distinct so the
|
|
149
|
+
// turn-entry pack can use the auto-recall verb's query derivation when present.
|
|
150
|
+
export async function autoRecall(prompt, { limit = 5, namespace = 'default' } = {}) {
|
|
151
|
+
const p = (prompt || '').toString().trim()
|
|
152
|
+
if (!p) return []
|
|
153
|
+
const pk = await ensurePlugkit()
|
|
154
|
+
if (!pk) return []
|
|
155
|
+
try {
|
|
156
|
+
const r = await pk.dispatch('auto-recall', p)
|
|
157
|
+
// auto-recall may return {hits} directly or under data; fall back to plain recall.
|
|
158
|
+
let hits = normalizeHits(r)
|
|
159
|
+
if (!hits.length) hits = await recall(p, { limit, namespace })
|
|
160
|
+
return hits.slice(0, limit)
|
|
161
|
+
} catch (_) {
|
|
162
|
+
return recall(p, { limit, namespace })
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Remove a memory by explicit key (never blind similarity-delete).
|
|
167
|
+
export async function prune(keys) {
|
|
168
|
+
const list = Array.isArray(keys) ? keys.filter(Boolean) : (keys ? [keys] : [])
|
|
169
|
+
if (!list.length) return { pruned: 0 }
|
|
170
|
+
const pk = await ensurePlugkit()
|
|
171
|
+
if (!pk) return { pruned: 0 }
|
|
172
|
+
try {
|
|
173
|
+
const r = await pk.dispatch('memorize-prune', { keys: list })
|
|
174
|
+
return (r && r.data) || r || { pruned: list.length }
|
|
175
|
+
} catch (e) {
|
|
176
|
+
try { console.error('[gm-learn] prune failed:', e && e.message) } catch (_) {}
|
|
177
|
+
return { pruned: 0 }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// freddie "case" plugin -- registers the case/enquiry toolset into the host so any
|
|
2
|
+
// runTurn can use it. Agentic code per the layering mandate; the data store and the
|
|
3
|
+
// field/enum/role config are injected by the host application (via toolCtx on each
|
|
4
|
+
// turn and plugins.case config), so this plugin is application-agnostic.
|
|
5
|
+
//
|
|
6
|
+
// The store is resolved PER TURN from toolCtx (ctx.store), never a global, so the
|
|
7
|
+
// agent answers for the asking worker. A configured fallback store
|
|
8
|
+
// (plugins.case.resolveStore, a function) is used only when a turn carries none
|
|
9
|
+
// (e.g. tests). Config is read via ctx.config (scopedCfg 'case').
|
|
10
|
+
|
|
11
|
+
import { buildCaseToolset } from './toolset.js'
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
name: 'case',
|
|
15
|
+
surfaces: 'pi',
|
|
16
|
+
register({ pi, config }) {
|
|
17
|
+
const resolveStore = config?.get('resolveStore', null)
|
|
18
|
+
const slimCase = config?.get('slimCase', (c) => c)
|
|
19
|
+
const slimEvent = config?.get('slimEvent', (e) => e)
|
|
20
|
+
const tools = buildCaseToolset({
|
|
21
|
+
resolveStore: typeof resolveStore === 'function' ? resolveStore : null,
|
|
22
|
+
config,
|
|
23
|
+
slimCase: typeof slimCase === 'function' ? slimCase : (c) => c,
|
|
24
|
+
slimEvent: typeof slimEvent === 'function' ? slimEvent : (e) => e,
|
|
25
|
+
})
|
|
26
|
+
for (const tool of tools) pi.tools.register(tool)
|
|
27
|
+
},
|
|
28
|
+
}
|