freddie 0.0.122 → 0.0.124
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 +5 -8
- package/src/agent/acptoapi-bridge.js +77 -77
- package/src/agent/codex_responses_adapter.js +3 -1
- package/src/agent/gemini_native_adapter.js +3 -1
- package/src/agent/image_gen_provider.js +3 -1
- package/src/agent/llm_resolver.js +22 -4
- package/src/agent/model-discovery.js +3 -1
- package/src/host/index.js +20 -14
- package/src/agent/__browser_shims/alias-plugin.mjs +0 -24
- package/src/agent/__browser_shims/build.mjs +0 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freddie",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.124",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Open JS agent harness built on pi-mono, floosie, xstate, and anentrypoint-design",
|
|
6
6
|
"bin": {
|
|
@@ -18,16 +18,14 @@
|
|
|
18
18
|
"start": "node bin/freddie.js",
|
|
19
19
|
"test": "node test.js",
|
|
20
20
|
"build:browser": "vite build --config vite.browser.config.js",
|
|
21
|
+
"build:browser-shim": "node scripts/browser-shim-build/build.mjs",
|
|
21
22
|
"link-local-design": "node scripts/link-local-design.mjs"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"@anentrypoint/libsql-plugkit-client": "github:AnEntrypoint/libsql-plugkit-client",
|
|
25
25
|
"@libsql/client": "^0.5.0",
|
|
26
|
-
"@mariozechner/pi-agent-core": "^0.70.6",
|
|
27
26
|
"@mariozechner/pi-ai": "^0.70.6",
|
|
28
|
-
"@mariozechner/pi-coding-agent": "^0.70.6",
|
|
29
27
|
"@mariozechner/pi-tui": "^0.70.6",
|
|
30
|
-
"acptoapi": "^1.0.
|
|
28
|
+
"acptoapi": "^1.0.144",
|
|
31
29
|
"anentrypoint-design": "latest",
|
|
32
30
|
"commander": "^14.0.0",
|
|
33
31
|
"express": "^5.0.0",
|
|
@@ -36,10 +34,8 @@
|
|
|
36
34
|
"gm-plugkit": "^2.0.1535",
|
|
37
35
|
"gm-skill": "latest",
|
|
38
36
|
"js-yaml": "^4.1.0",
|
|
39
|
-
"libsql-plugkit-client": "^0.0.10",
|
|
40
37
|
"plugsdk": "^1.0.20",
|
|
41
|
-
"xstate": "^5.31.0"
|
|
42
|
-
"zod": "^4.0.0"
|
|
38
|
+
"xstate": "^5.31.0"
|
|
43
39
|
},
|
|
44
40
|
"optionalDependencies": {
|
|
45
41
|
"@libsql/darwin-arm64": "0.3.19",
|
|
@@ -85,6 +81,7 @@
|
|
|
85
81
|
"xstate"
|
|
86
82
|
],
|
|
87
83
|
"devDependencies": {
|
|
84
|
+
"esbuild": "^0.28.0",
|
|
88
85
|
"vite": "^8.0.13"
|
|
89
86
|
}
|
|
90
87
|
}
|
|
@@ -3,47 +3,51 @@ import { parseTextToolCalls } from './tool_call_text.js'
|
|
|
3
3
|
|
|
4
4
|
const log = logger('acptoapi')
|
|
5
5
|
|
|
6
|
-
// acptoapi may take minutes to answer when it serially walks dead providers
|
|
7
|
-
// (each ~90s timeout) before a live one responds. Node's default undici
|
|
8
|
-
// headersTimeout/bodyTimeout (~300s) would throw UND_ERR_HEADERS_TIMEOUT
|
|
9
|
-
// mid-walk, so we raise them to 0 (disabled) on the global dispatcher and let
|
|
10
|
-
// our AbortController be the single source of truth for the overall deadline
|
|
11
|
-
// (default 240s, override via FREDDIE_LLM_TIMEOUT_MS). Use setGlobalDispatcher
|
|
12
|
-
// once rather than a per-request Agent so we don't leak a dispatcher per call.
|
|
13
6
|
// Browser-safe env read: this module evaluates in a plain browser context where
|
|
14
7
|
// `process` is undefined (no node shim yet), so a bare process.env throws
|
|
15
8
|
// "process is not defined" and aborts the whole bundle import. envVal() reads
|
|
16
9
|
// live (picks up a late-installed shim) and never throws.
|
|
17
10
|
const envVal = (k) => { try { return (typeof process !== 'undefined' && process.env) ? process.env[k] : undefined } catch { return undefined } }
|
|
18
11
|
const ACPTOAPI_TIMEOUT_MS = Number(envVal('FREDDIE_LLM_TIMEOUT_MS')) || 240000
|
|
19
|
-
let _dispatcherSet = false
|
|
20
|
-
async function ensureLongTimeoutDispatcher() {
|
|
21
|
-
if (_dispatcherSet) return
|
|
22
|
-
_dispatcherSet = true
|
|
23
|
-
try {
|
|
24
|
-
const undici = await import('undici')
|
|
25
|
-
// headersTimeout/bodyTimeout 0: tolerate acptoapi's minutes-long walk.
|
|
26
|
-
// keepAlive*Timeout 1ms: close the socket right after the response so no
|
|
27
|
-
// keep-alive socket lingers between calls.
|
|
28
|
-
undici.setGlobalDispatcher(new undici.Agent({
|
|
29
|
-
headersTimeout: 0,
|
|
30
|
-
bodyTimeout: 0,
|
|
31
|
-
keepAliveTimeout: 1,
|
|
32
|
-
keepAliveMaxTimeout: 1,
|
|
33
|
-
}))
|
|
34
|
-
} catch { /* undici not available — rely on AbortController + defaults */ }
|
|
35
|
-
}
|
|
36
12
|
|
|
37
13
|
export function getAcptoapiUrl() {
|
|
38
|
-
|
|
14
|
+
// Returns the configured dialable acptoapi URL, or null when unset. Most
|
|
15
|
+
// callers (the dashboard health row, CLI banner) treat this as display/logging
|
|
16
|
+
// only -- there is no listening port required for the in-process callLLM()
|
|
17
|
+
// path below. However codex_responses_adapter.js, gemini_native_adapter.js,
|
|
18
|
+
// image_gen_provider.js, and model-discovery.js still fetch() this value as a
|
|
19
|
+
// live HTTP base and DO require a real dialable URL (FREDDIE_LLM_URL set) --
|
|
20
|
+
// they must guard against a null return rather than building a request
|
|
21
|
+
// against a placeholder string.
|
|
22
|
+
return envVal('FREDDIE_LLM_URL') || null
|
|
39
23
|
}
|
|
40
24
|
|
|
41
25
|
export function getAcptoapiModel() {
|
|
42
26
|
return envVal('FREDDIE_LLM_MODEL') || 'claude/haiku'
|
|
43
27
|
}
|
|
44
28
|
|
|
29
|
+
let _acptoapi = null
|
|
30
|
+
async function getAcptoapi() {
|
|
31
|
+
if (!_acptoapi) {
|
|
32
|
+
const mod = await import('acptoapi')
|
|
33
|
+
// acptoapi is a CJS package; Node's CJS-to-ESM interop only statically
|
|
34
|
+
// detects a SUBSET of module.exports keys as named exports (witnessed:
|
|
35
|
+
// `chat` is a real named export, `listAllModelsAndQueues` is not, even
|
|
36
|
+
// though both are plain keys on the same module.exports object) -- read
|
|
37
|
+
// through `.default` (the full CJS exports object) so every export is
|
|
38
|
+
// reachable regardless of which subset the interop happened to pick up.
|
|
39
|
+
_acptoapi = mod.default && typeof mod.default === 'object' ? mod.default : mod
|
|
40
|
+
}
|
|
41
|
+
return _acptoapi
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// In-process call: acptoapi's own chat() walks its provider/model resolution
|
|
45
|
+
// (including a comma-list or named chain for multi-model fallback) with no
|
|
46
|
+
// HTTP hop and no separate listening process -- eliminates the standalone
|
|
47
|
+
// acptoapi.js daemon on :4800 entirely, along with its witnessed failure mode
|
|
48
|
+
// (an uncaught ACP-timeout exception crashing the whole bridge process).
|
|
45
49
|
export async function callLLM({ messages, tools = [], model, tool_choice, cwd = null } = {}) {
|
|
46
|
-
const
|
|
50
|
+
const acptoapi = await getAcptoapi()
|
|
47
51
|
const useModel = model || getAcptoapiModel()
|
|
48
52
|
const hasTools = Array.isArray(tools) && tools.length > 0
|
|
49
53
|
const adaptedMessages = messages.map(adaptMessage)
|
|
@@ -59,46 +63,40 @@ export async function callLLM({ messages, tools = [], model, tool_choice, cwd =
|
|
|
59
63
|
if (sysIdx >= 0) adaptedMessages[sysIdx] = { ...adaptedMessages[sysIdx], content: (adaptedMessages[sysIdx].content || '') + cwdNote }
|
|
60
64
|
else adaptedMessages.unshift({ role: 'system', content: cwdNote.trim() })
|
|
61
65
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (hasTools && tool_choice) body.tool_choice = tool_choice
|
|
72
|
-
const headers = { 'content-type': 'application/json', authorization: 'Bearer none' }
|
|
73
|
-
if (hasTools && cwd) headers['x-cwd'] = cwd
|
|
74
|
-
// Rely on AbortController timeout. acptoapi v1+ ships CORS + Private
|
|
75
|
-
// Network Access headers so cross-origin loopback (gh-pages → localhost)
|
|
76
|
-
// succeeds when acptoapi is running. The earlier preemptive loopback
|
|
77
|
-
// refusal caused false negatives on reachable endpoints.
|
|
78
|
-
await ensureLongTimeoutDispatcher()
|
|
79
|
-
const _ac = new AbortController()
|
|
80
|
-
const _tid = setTimeout(() => _ac.abort(new Error('acptoapi fetch timeout')), ACPTOAPI_TIMEOUT_MS)
|
|
81
|
-
let res
|
|
82
|
-
try {
|
|
83
|
-
res = await fetch(base.replace(/\/$/, '') + '/chat/completions', {
|
|
84
|
-
method: 'POST',
|
|
85
|
-
headers,
|
|
86
|
-
body: JSON.stringify(body),
|
|
87
|
-
signal: _ac.signal,
|
|
88
|
-
})
|
|
89
|
-
} finally { clearTimeout(_tid) }
|
|
90
|
-
if (!res.ok) {
|
|
91
|
-
const text = await res.text()
|
|
92
|
-
throw new Error(`acptoapi ${res.status}: ${text.slice(0, 400)}`)
|
|
93
|
-
}
|
|
66
|
+
// acptoapi's chat() is a plain Promise with no AbortSignal support (each
|
|
67
|
+
// underlying provider has its own internal per-call timeout, e.g. the
|
|
68
|
+
// openai-compat provider's OPENAI_COMPAT_TIMEOUT_MS, default 180s) -- so the
|
|
69
|
+
// overall deadline here is enforced by racing a timeout, not by aborting the
|
|
70
|
+
// in-flight call itself.
|
|
71
|
+
let _timeoutHandle
|
|
72
|
+
const _timeout = new Promise((_, reject) => {
|
|
73
|
+
_timeoutHandle = setTimeout(() => reject(new Error('acptoapi call timeout')), ACPTOAPI_TIMEOUT_MS)
|
|
74
|
+
})
|
|
94
75
|
let json
|
|
95
76
|
try {
|
|
96
|
-
json = await
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
77
|
+
json = await Promise.race([
|
|
78
|
+
acptoapi.chat({
|
|
79
|
+
model: useModel,
|
|
80
|
+
messages: adaptedMessages,
|
|
81
|
+
...(hasTools ? { tools: tools.map(adaptTool) } : {}),
|
|
82
|
+
max_tokens: 4096,
|
|
83
|
+
}),
|
|
84
|
+
_timeout,
|
|
85
|
+
])
|
|
86
|
+
} finally { clearTimeout(_timeoutHandle) }
|
|
100
87
|
log.info('completed', { model: useModel, usage: json.usage })
|
|
101
|
-
|
|
88
|
+
const adapted = adaptResponse(json)
|
|
89
|
+
// acptoapi's chat()/toParams() does not forward tool_choice to any provider
|
|
90
|
+
// (confirmed: a pre-existing gap, not introduced by going in-process -- the
|
|
91
|
+
// old HTTP path silently dropped it too). Enforce the documented contract
|
|
92
|
+
// client-side: when the caller forced a tool call and none came back, this
|
|
93
|
+
// was previously a SILENT no-op -- log loud so the gap is visible instead of
|
|
94
|
+
// masquerading as "the model chose not to call a tool".
|
|
95
|
+
const forcedToolChoice = tool_choice === 'required' || tool_choice?.type === 'required'
|
|
96
|
+
if (forcedToolChoice && hasTools && !adapted.tool_calls.length) {
|
|
97
|
+
log.warn('tool_choice required but no tool call returned (acptoapi does not enforce tool_choice)', { model: useModel })
|
|
98
|
+
}
|
|
99
|
+
return adapted
|
|
102
100
|
}
|
|
103
101
|
|
|
104
102
|
function adaptMessage(m) {
|
|
@@ -145,21 +143,23 @@ function adaptResponse(r) {
|
|
|
145
143
|
|
|
146
144
|
function tryParseJson(s) { try { return typeof s === 'string' ? JSON.parse(s) : (s || {}) } catch { return {} } }
|
|
147
145
|
|
|
146
|
+
// In-process reachability: acptoapi is a library import now, not a port to
|
|
147
|
+
// dial, so there is no cheap side-channel that answers "is the CONFIGURED
|
|
148
|
+
// model reachable" for an arbitrary provider (witnessed: listAllModelsAndQueues
|
|
149
|
+
// only enumerates configured matrix/queue sources, empty by default; the
|
|
150
|
+
// sampler cache starts empty until something actually probes/fails a
|
|
151
|
+
// provider; chatjimmy -- casey's configured model -- is itself a remote
|
|
152
|
+
// hosted proxy at https://chatjimmy.ai with its own internal model list, not
|
|
153
|
+
// exported from acptoapi's top-level API at all). The only generically
|
|
154
|
+
// correct answer is a real minimal call: send a trivial message with a short
|
|
155
|
+
// timeout and treat success as reachable.
|
|
148
156
|
export async function isReachable(timeoutMs = 10000) {
|
|
149
157
|
try {
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
})
|
|
157
|
-
clearTimeout(timeoutId)
|
|
158
|
-
if (!res.ok) return false
|
|
159
|
-
const json = await res.json()
|
|
160
|
-
return Array.isArray(json.data) && json.data.length > 0
|
|
161
|
-
} finally {
|
|
162
|
-
clearTimeout(timeoutId)
|
|
163
|
-
}
|
|
158
|
+
const acptoapi = await getAcptoapi()
|
|
159
|
+
const result = await Promise.race([
|
|
160
|
+
acptoapi.chat({ model: getAcptoapiModel(), messages: [{ role: 'user', content: 'ping' }], max_tokens: 4 }),
|
|
161
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('reachability probe timeout')), timeoutMs)),
|
|
162
|
+
])
|
|
163
|
+
return !!(result && result.choices && result.choices.length)
|
|
164
164
|
} catch { return false }
|
|
165
165
|
}
|
|
@@ -4,7 +4,9 @@ import { isCodexModel } from '../cli/codex_models.js'
|
|
|
4
4
|
|
|
5
5
|
export async function chat({ input, model = 'o3-mini', tools = [], reasoning_effort = 'medium' } = {}) {
|
|
6
6
|
if (!isCodexModel(model)) console.warn('[codex_responses] non-codex model: ' + model)
|
|
7
|
-
const
|
|
7
|
+
const url = getAcptoapiUrl()
|
|
8
|
+
if (!url) throw new Error('FREDDIE_LLM_URL must be set for this adapter (acptoapi is in-process only otherwise)')
|
|
9
|
+
const base = url.replace(/\/v1\/?$/, '')
|
|
8
10
|
const r = await fetch(base + '/v1/responses', {
|
|
9
11
|
method: 'POST',
|
|
10
12
|
headers: { authorization: 'Bearer none', 'content-type': 'application/json' },
|
|
@@ -3,7 +3,9 @@ import { getAcptoapiUrl } from './acptoapi-bridge.js'
|
|
|
3
3
|
import { adaptToolForGemini, adaptMessagesForGemini } from './gemini_schema.js'
|
|
4
4
|
|
|
5
5
|
export async function chat({ messages, model = 'gemini-2.5-flash', tools = [] } = {}) {
|
|
6
|
-
const
|
|
6
|
+
const acptoapiUrl = getAcptoapiUrl()
|
|
7
|
+
if (!acptoapiUrl) throw new Error('FREDDIE_LLM_URL must be set for this adapter (acptoapi is in-process only otherwise)')
|
|
8
|
+
const base = acptoapiUrl.replace(/\/v1\/?$/, '')
|
|
7
9
|
const url = `${base}/v1beta/models/${model}:generateContent`
|
|
8
10
|
const body = { contents: adaptMessagesForGemini(messages), ...(tools.length ? { tools: [{ function_declarations: tools.map(adaptToolForGemini) }] } : {}) }
|
|
9
11
|
const r = await fetch(url, {
|
|
@@ -5,7 +5,9 @@ const PROVIDERS = ['openai', 'replicate', 'stability']
|
|
|
5
5
|
|
|
6
6
|
export async function generate({ provider = 'openai', prompt, size, model } = {}) {
|
|
7
7
|
if (!PROVIDERS.includes(provider)) throw new Error('unknown image provider: ' + provider)
|
|
8
|
-
const
|
|
8
|
+
const url = getAcptoapiUrl()
|
|
9
|
+
if (!url) throw new Error('FREDDIE_LLM_URL must be set for this adapter (acptoapi is in-process only otherwise)')
|
|
10
|
+
const base = url.replace(/\/v1\/?$/, '')
|
|
9
11
|
const body = provider === 'replicate'
|
|
10
12
|
? { version: model || 'black-forest-labs/flux-schnell', input: { prompt } }
|
|
11
13
|
: { model: model || 'gpt-image-1', prompt, size }
|
|
@@ -5,6 +5,22 @@ import { parseTextToolCalls } from './tool_call_text.js'
|
|
|
5
5
|
import * as sdkNs from 'acptoapi'
|
|
6
6
|
export { matrixUsable } from './model-matrix.js'
|
|
7
7
|
|
|
8
|
+
// Reachability memoization: bridgeReachable() now performs a REAL LLM call
|
|
9
|
+
// (acptoapi-bridge.js's isReachable() sends a live 'ping' completion), so
|
|
10
|
+
// calling it on every turn doubles LLM cost/latency. Cache the result for a
|
|
11
|
+
// short TTL so a burst of turns within the window reuses one probe. Does NOT
|
|
12
|
+
// touch acptoapi-bridge.js's exported isReachable -- health-check/dashboard
|
|
13
|
+
// callers still need a live, uncached probe.
|
|
14
|
+
let _lastReachable = { at: 0, ok: false }
|
|
15
|
+
const REACHABLE_TTL_MS = 5000
|
|
16
|
+
async function cachedReachable() {
|
|
17
|
+
const now = Date.now()
|
|
18
|
+
if (now - _lastReachable.at < REACHABLE_TTL_MS) return _lastReachable.ok
|
|
19
|
+
const ok = await bridgeReachable()
|
|
20
|
+
_lastReachable = { at: now, ok }
|
|
21
|
+
return ok
|
|
22
|
+
}
|
|
23
|
+
|
|
8
24
|
// `acptoapi` is externalized by vite (browser) so the host environment
|
|
9
25
|
// supplies it (thebird ships docs/lib/acptoapi-browser.js via importmap).
|
|
10
26
|
// Node CLI gets the real CJS package. Defensive `|| {}` keeps the bundle
|
|
@@ -78,7 +94,7 @@ async function buildModel({ provider, model, inputModel }) {
|
|
|
78
94
|
const keyed = Array.isArray(auto) ? auto.filter(l => { const p = l.model.split('/')[0]; const env = PROVIDER_KEYS[p]; return env && process.env[env] }) : []
|
|
79
95
|
if (keyed.length) return keyed.map(l => l.model).join(', ')
|
|
80
96
|
// No local provider keys — delegate to acptoapi if reachable.
|
|
81
|
-
if (await
|
|
97
|
+
if (await cachedReachable()) return process.env.FREDDIE_LLM_MODEL || 'auto'
|
|
82
98
|
return null
|
|
83
99
|
}
|
|
84
100
|
|
|
@@ -87,12 +103,12 @@ export function resolveCallLLM({ provider, model } = {}) {
|
|
|
87
103
|
const m = await buildModel({ provider, model, inputModel: input.model })
|
|
88
104
|
if (!m) {
|
|
89
105
|
const status = typeof sdk.getStatus === 'function' ? sdk.getStatus().map(s => `${s.provider}(ok=${s.ok},fails=${s.failCount})`).join(', ') : ''
|
|
90
|
-
throw new Error('no LLM backend reachable: set a provider API key or
|
|
106
|
+
throw new Error('no LLM backend reachable: set a provider API key or FREDDIE_LLM_MODEL' + (status ? ' | sampler: ' + status : ''))
|
|
91
107
|
}
|
|
92
108
|
try {
|
|
93
109
|
const isSimple = typeof m === 'string' && !m.includes(',') && !/^queue\//.test(m)
|
|
94
110
|
|
|
95
|
-
if (isSimple && await
|
|
111
|
+
if (isSimple && await cachedReachable()) {
|
|
96
112
|
return await bridgeCall({ ...input, model: m })
|
|
97
113
|
}
|
|
98
114
|
|
|
@@ -105,7 +121,9 @@ export function resolveCallLLM({ provider, model } = {}) {
|
|
|
105
121
|
if (m.includes(',') || /^queue\//.test(m)) opts.matrixSource = process.env.FREDDIE_MATRIX_URL || MATRIX_FILE
|
|
106
122
|
|
|
107
123
|
if (typeof sdk.chat !== 'function') {
|
|
108
|
-
// Browser context:
|
|
124
|
+
// Browser/no-sdk context: fall back to acptoapi-bridge's in-process
|
|
125
|
+
// call (may be a no-op/broken in true browser bundles since acptoapi
|
|
126
|
+
// is externalized for vite -- unverified post-rewrite, see build:browser).
|
|
109
127
|
return await bridgeCall({ ...input, model: m })
|
|
110
128
|
}
|
|
111
129
|
const r = await sdk.chat(opts)
|
|
@@ -17,7 +17,9 @@ export function listKnownProviders() {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export async function discoverModels({ provider } = {}) {
|
|
20
|
-
const
|
|
20
|
+
const url = getAcptoapiUrl()
|
|
21
|
+
if (!url) throw new Error('FREDDIE_LLM_URL must be set for this adapter (acptoapi is in-process only otherwise)')
|
|
22
|
+
const base = url.replace(/\/v1\/?$/, '')
|
|
21
23
|
try {
|
|
22
24
|
const r = await fetch(base + '/v1/models', {
|
|
23
25
|
headers: { authorization: 'Bearer none' },
|
package/src/host/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getFreddieHome } from '../home.js'
|
|
|
5
5
|
import { applyActiveProjectFromRegistry } from '../projects.js'
|
|
6
6
|
|
|
7
7
|
let _host = null
|
|
8
|
-
let
|
|
8
|
+
let _loadPromise = null
|
|
9
9
|
|
|
10
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
11
11
|
const REPO_PLUGINS = path.resolve(__dirname, '..', '..', 'plugins')
|
|
@@ -16,18 +16,24 @@ export function host() {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export async function bootHost(extraRoots = []) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
// Memoize the IN-FLIGHT promise, not a boolean flag: a boolean set true
|
|
20
|
+
// before the awaits below complete let concurrent callers observe a
|
|
21
|
+
// partially-loaded host. Returning the same promise means every caller
|
|
22
|
+
// (including ones that arrive mid-load) awaits the exact same completion.
|
|
23
|
+
if (_loadPromise) return _loadPromise
|
|
24
|
+
_loadPromise = (async () => {
|
|
25
|
+
const h = host()
|
|
26
|
+
if (!process.env.FREDDIE_HOME && !process.env.FREDDIE_PROFILE) applyActiveProjectFromRegistry()
|
|
27
|
+
const roots = [REPO_PLUGINS, path.join(getFreddieHome(), 'plugins'), path.join(process.cwd(), '.freddie', 'plugins'), ...extraRoots]
|
|
28
|
+
const plugins = await discoverPlugins(roots)
|
|
29
|
+
await h.load(plugins)
|
|
30
|
+
const ccRoots = [path.join(getFreddieHome(), 'cc-plugins'), path.join(process.cwd(), '.freddie', 'cc-plugins')]
|
|
31
|
+
await h.loadCcPlugins(ccRoots)
|
|
32
|
+
const extra = (process.env.FREDDIE_EXTRA_CC_ROOTS || '').split(path.delimiter).filter(Boolean)
|
|
33
|
+
for (const r of [__dirname, process.cwd(), ...extra]) await h.loadCcFromNodeModules(r)
|
|
34
|
+
return h
|
|
35
|
+
})()
|
|
36
|
+
return _loadPromise
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
export function resetHostForTests() { _host = null;
|
|
39
|
+
export function resetHostForTests() { _host = null; _loadPromise = null }
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import path from 'path';
|
|
3
|
-
const F='C:/dev/freddie';
|
|
4
|
-
const R=p=>path.resolve(p);
|
|
5
|
-
const ALIAS={
|
|
6
|
-
[R(F+'/src/host/index.js')]: R(F+'/src/agent/__browser_shims/host.js'),
|
|
7
|
-
[R(F+'/src/toolsets.js')]: R(F+'/src/agent/__browser_shims/toolsets.js'),
|
|
8
|
-
[R(F+'/src/agent/llm_resolver.js')]: R(F+'/src/agent/__browser_shims/llm_resolver.js'),
|
|
9
|
-
[R(F+'/src/observability/log.js')]: R(F+'/src/agent/__browser_shims/log.js'),
|
|
10
|
-
[R(F+'/src/config.js')]: R(F+'/src/agent/__browser_shims/config.js'),
|
|
11
|
-
[R(F+'/src/home.js')]: R(F+'/src/agent/__browser_shims/home.js'),
|
|
12
|
-
};
|
|
13
|
-
export default {
|
|
14
|
-
name: 'freddie-browser-alias',
|
|
15
|
-
setup(build){
|
|
16
|
-
build.onResolve({filter: /.*/}, async args=>{
|
|
17
|
-
if(args.path.startsWith('node:') || (!args.path.startsWith('.') && !path.isAbsolute(args.path))) return null;
|
|
18
|
-
const resolved = path.resolve(args.resolveDir||'', args.path);
|
|
19
|
-
const candidates=[resolved, resolved+'.js', resolved+'/index.js'];
|
|
20
|
-
for(const c of candidates) if(ALIAS[c]) return { path: ALIAS[c] };
|
|
21
|
-
return null;
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
};
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import * as esbuild from 'esbuild';
|
|
3
|
-
import alias from './alias-plugin.mjs';
|
|
4
|
-
await esbuild.build({
|
|
5
|
-
entryPoints: ['C:/dev/freddie/src/agent/__browser_shims/entry.js'],
|
|
6
|
-
bundle: true,
|
|
7
|
-
format: 'esm',
|
|
8
|
-
platform: 'browser',
|
|
9
|
-
conditions: ['browser','module','import'],
|
|
10
|
-
outfile: 'C:/dev/thebird/docs/freddie-runtime.js',
|
|
11
|
-
plugins: [alias],
|
|
12
|
-
legalComments: 'none',
|
|
13
|
-
minify: false,
|
|
14
|
-
sourcemap: false,
|
|
15
|
-
logLevel: 'info',
|
|
16
|
-
external: [],
|
|
17
|
-
});
|
|
18
|
-
console.log('bundle ok');
|