ethagent 3.0.1 → 3.1.0
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/README.md +6 -1
- package/package.json +3 -1
- package/src/app/FirstRun.tsx +1 -24
- package/src/app/firstRunConfig.ts +26 -0
- package/src/auth/openaiOAuth/landingPage.ts +2 -11
- package/src/chat/ChatScreen.tsx +32 -117
- package/src/chat/MessageList.tsx +18 -260
- package/src/chat/chatEnvironment.ts +16 -0
- package/src/chat/chatTurnContext.ts +50 -0
- package/src/chat/chatTurnOrchestrator.ts +5 -112
- package/src/chat/chatTurnRows.ts +64 -0
- package/src/chat/commands.ts +3 -178
- package/src/chat/continuityEditReview.ts +42 -0
- package/src/chat/input/ChatInput.tsx +10 -144
- package/src/chat/input/chatInputHelpers.ts +62 -0
- package/src/chat/input/inputRendering.tsx +93 -0
- package/src/chat/messageMarkdown.ts +220 -0
- package/src/chat/messageRows.ts +43 -0
- package/src/chat/planImplementation.ts +62 -0
- package/src/chat/slashCommandHandlers.ts +165 -0
- package/src/chat/slashCommandViews.ts +120 -0
- package/src/cli/main.tsx +7 -0
- package/src/identity/continuity/challenges.ts +123 -0
- package/src/identity/continuity/envelope.ts +49 -1484
- package/src/identity/continuity/envelopeCreate.ts +322 -0
- package/src/identity/continuity/envelopeCrypto.ts +182 -0
- package/src/identity/continuity/envelopeParse.ts +441 -0
- package/src/identity/continuity/envelopeTypes.ts +204 -0
- package/src/identity/continuity/envelopeVersion.ts +1 -0
- package/src/identity/continuity/payloadNormalization.ts +183 -0
- package/src/identity/continuity/publicSkills.ts +5 -5
- package/src/identity/continuity/skills/loadSkills.ts +12 -69
- package/src/identity/continuity/skills/skillPaths.ts +76 -0
- package/src/identity/continuity/skillsNormalization.ts +119 -0
- package/src/identity/continuity/snapshotToken.ts +28 -0
- package/src/identity/hub/continuity/completion.ts +67 -0
- package/src/identity/hub/continuity/effects.ts +5 -62
- package/src/identity/hub/profile/effects.ts +6 -170
- package/src/identity/hub/profile/operatorSave.ts +202 -0
- package/src/identity/registry/erc8004/metadata.ts +31 -23
- package/src/identity/wallet/browserWallet/html.ts +1 -57
- package/src/identity/wallet/browserWallet/walletPageSource.ts +85 -0
- package/src/identity/wallet/page/controller.ts +1 -1
- package/src/identity/wallet/page/errorView.ts +122 -0
- package/src/identity/wallet/page/view.ts +3 -114
- package/src/mcp/manager.ts +8 -66
- package/src/mcp/managerHelpers.ts +70 -0
- package/src/models/ModelPicker.tsx +69 -889
- package/src/models/huggingface.ts +20 -137
- package/src/models/huggingfaceStorage.ts +136 -0
- package/src/models/llamacpp.ts +37 -303
- package/src/models/llamacppCommands.ts +44 -0
- package/src/models/llamacppConfig.ts +34 -0
- package/src/models/llamacppDiscovery.ts +176 -0
- package/src/models/llamacppOutput.ts +65 -0
- package/src/models/modelPickerCatalogFlow.ts +56 -0
- package/src/models/modelPickerCredentials.ts +166 -0
- package/src/models/modelPickerData.ts +41 -0
- package/src/models/modelPickerDisplay.tsx +132 -0
- package/src/models/modelPickerHfFlow.ts +192 -0
- package/src/models/modelPickerLocalRunnerFlow.ts +115 -0
- package/src/models/modelPickerTypes.ts +69 -0
- package/src/models/modelPickerUninstallFlow.ts +48 -0
- package/src/models/modelPickerViewHelpers.ts +174 -0
- package/src/providers/openai-chat.ts +5 -124
- package/src/providers/openaiChatWire.ts +124 -0
- package/src/runtime/providerTurn.ts +38 -0
- package/src/runtime/textToolParser.ts +161 -0
- package/src/runtime/toolIntent.ts +1 -1
- package/src/runtime/turn.ts +43 -499
- package/src/runtime/turnNudges.ts +223 -0
- package/src/runtime/turnTypes.ts +86 -0
- package/src/ui/terminalTitle.ts +30 -0
package/src/models/llamacpp.ts
CHANGED
|
@@ -4,6 +4,43 @@ import path from 'node:path'
|
|
|
4
4
|
import { atomicWriteText } from '../storage/atomicWrite.js'
|
|
5
5
|
import { ensureConfigDir, getConfigDir } from '../storage/config.js'
|
|
6
6
|
import os from 'node:os'
|
|
7
|
+
import {
|
|
8
|
+
buildFailure,
|
|
9
|
+
formatInstallFailure,
|
|
10
|
+
humanInstallError,
|
|
11
|
+
installFailureDetail,
|
|
12
|
+
installerProgressLabel,
|
|
13
|
+
summarizeInstallOutput,
|
|
14
|
+
} from './llamacppOutput.js'
|
|
15
|
+
import {
|
|
16
|
+
getLocalRunnerConfigPath,
|
|
17
|
+
loadLocalRunnerConfig,
|
|
18
|
+
saveLocalRunnerConfig,
|
|
19
|
+
setLlamaCppServerPath,
|
|
20
|
+
type LocalRunnerConfig,
|
|
21
|
+
} from './llamacppConfig.js'
|
|
22
|
+
import { runCommand } from './llamacppCommands.js'
|
|
23
|
+
import {
|
|
24
|
+
detectLlamaCppServerBinary,
|
|
25
|
+
discoverLlamaCppCliPaths,
|
|
26
|
+
discoverLlamaCppServerPaths,
|
|
27
|
+
findAndPersistLlamaCppServer,
|
|
28
|
+
} from './llamacppDiscovery.js'
|
|
29
|
+
|
|
30
|
+
export { humanInstallError, summarizeInstallOutput } from './llamacppOutput.js'
|
|
31
|
+
export {
|
|
32
|
+
getLocalRunnerConfigPath,
|
|
33
|
+
loadLocalRunnerConfig,
|
|
34
|
+
saveLocalRunnerConfig,
|
|
35
|
+
setLlamaCppServerPath,
|
|
36
|
+
} from './llamacppConfig.js'
|
|
37
|
+
export {
|
|
38
|
+
detectLlamaCppServerBinary,
|
|
39
|
+
discoverLlamaCppServerPaths,
|
|
40
|
+
llamaCppSearchRoots,
|
|
41
|
+
llamaCppServerCandidates,
|
|
42
|
+
} from './llamacppDiscovery.js'
|
|
43
|
+
export type { LocalRunnerConfig } from './llamacppConfig.js'
|
|
7
44
|
|
|
8
45
|
export const DEFAULT_LLAMA_HOST = process.env.LLAMACPP_HOST ?? 'http://localhost:8080'
|
|
9
46
|
|
|
@@ -15,12 +52,6 @@ export type LlamaCppStatus = {
|
|
|
15
52
|
servedModels: string[]
|
|
16
53
|
}
|
|
17
54
|
|
|
18
|
-
type RunResult = {
|
|
19
|
-
code: number
|
|
20
|
-
stdout: string
|
|
21
|
-
stderr: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
55
|
type RunInstallResult = { ok: true } | { ok: false; message: string; detail?: string }
|
|
25
56
|
|
|
26
57
|
export type LlamaCppInstallPhase = 'checking' | 'installing' | 'finding' | 'building'
|
|
@@ -77,47 +108,6 @@ type LlamaCppStartDeps = {
|
|
|
77
108
|
rogueDrainPollMs?: number
|
|
78
109
|
}
|
|
79
110
|
|
|
80
|
-
export type LocalRunnerConfig = {
|
|
81
|
-
llamaServerPath?: string
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function runCommand(cmd: string, args: string[], timeoutMs = 2000): Promise<RunResult | null> {
|
|
85
|
-
return new Promise(resolve => {
|
|
86
|
-
let settled = false
|
|
87
|
-
let child: ReturnType<typeof spawn>
|
|
88
|
-
try {
|
|
89
|
-
child = spawn(cmd, args, { windowsHide: true })
|
|
90
|
-
} catch {
|
|
91
|
-
resolve(null)
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let stdout = ''
|
|
96
|
-
let stderr = ''
|
|
97
|
-
const timer = setTimeout(() => {
|
|
98
|
-
if (settled) return
|
|
99
|
-
settled = true
|
|
100
|
-
try { child.kill() } catch { void 0 }
|
|
101
|
-
resolve(null)
|
|
102
|
-
}, timeoutMs)
|
|
103
|
-
|
|
104
|
-
child.stdout?.on('data', chunk => { stdout += chunk.toString() })
|
|
105
|
-
child.stderr?.on('data', chunk => { stderr += chunk.toString() })
|
|
106
|
-
child.on('error', () => {
|
|
107
|
-
if (settled) return
|
|
108
|
-
settled = true
|
|
109
|
-
clearTimeout(timer)
|
|
110
|
-
resolve(null)
|
|
111
|
-
})
|
|
112
|
-
child.on('close', code => {
|
|
113
|
-
if (settled) return
|
|
114
|
-
settled = true
|
|
115
|
-
clearTimeout(timer)
|
|
116
|
-
resolve({ code: code ?? -1, stdout, stderr })
|
|
117
|
-
})
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
|
-
|
|
121
111
|
function runInstallCommand(
|
|
122
112
|
plan: LlamaCppInstallPlan,
|
|
123
113
|
timeoutMs: number,
|
|
@@ -168,85 +158,6 @@ async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Respons
|
|
|
168
158
|
}
|
|
169
159
|
}
|
|
170
160
|
|
|
171
|
-
export function getLocalRunnerConfigPath(): string {
|
|
172
|
-
return path.join(getConfigDir(), 'local-runner.json')
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export async function loadLocalRunnerConfig(): Promise<LocalRunnerConfig> {
|
|
176
|
-
try {
|
|
177
|
-
const raw = await fs.readFile(getLocalRunnerConfigPath(), 'utf8')
|
|
178
|
-
const parsed = JSON.parse(raw) as unknown
|
|
179
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
|
|
180
|
-
const value = (parsed as { llamaServerPath?: unknown }).llamaServerPath
|
|
181
|
-
return typeof value === 'string' && value.trim() ? { llamaServerPath: value.trim() } : {}
|
|
182
|
-
} catch (err: unknown) {
|
|
183
|
-
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {}
|
|
184
|
-
return {}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export async function saveLocalRunnerConfig(config: LocalRunnerConfig): Promise<void> {
|
|
189
|
-
await ensureConfigDir()
|
|
190
|
-
await atomicWriteText(getLocalRunnerConfigPath(), JSON.stringify(config, null, 2) + '\n')
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export async function setLlamaCppServerPath(serverPath: string): Promise<void> {
|
|
194
|
-
await saveLocalRunnerConfig({ llamaServerPath: serverPath.trim() })
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export async function detectLlamaCppServerBinary(extraCandidates: string[] = []): Promise<{ path: string | null; version: string | null }> {
|
|
198
|
-
const config = await loadLocalRunnerConfig()
|
|
199
|
-
const candidates = [
|
|
200
|
-
...llamaCppServerCandidates(process.env, process.platform, config.llamaServerPath),
|
|
201
|
-
...extraCandidates,
|
|
202
|
-
]
|
|
203
|
-
for (const candidate of candidates) {
|
|
204
|
-
const result = await runCommand(candidate, ['--version'])
|
|
205
|
-
if (!result) continue
|
|
206
|
-
const output = `${result.stdout}\n${result.stderr}`.trim()
|
|
207
|
-
if (result.code === 0 || output.length > 0) {
|
|
208
|
-
return { path: candidate, version: firstLine(output) || 'installed' }
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return { path: null, version: null }
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function llamaCppServerCandidates(
|
|
215
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
216
|
-
platform: NodeJS.Platform = process.platform,
|
|
217
|
-
configuredPath?: string,
|
|
218
|
-
): string[] {
|
|
219
|
-
const candidates: string[] = []
|
|
220
|
-
appendCandidate(candidates, configuredPath)
|
|
221
|
-
appendCandidate(candidates, env.LLAMA_SERVER_PATH)
|
|
222
|
-
appendCandidate(candidates, env.LLAMACPP_SERVER_PATH)
|
|
223
|
-
appendCandidate(candidates, 'llama-server')
|
|
224
|
-
appendCandidate(candidates, 'llama-server.exe')
|
|
225
|
-
|
|
226
|
-
if (platform === 'win32') {
|
|
227
|
-
appendCandidate(candidates, env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'Programs', 'llama.cpp', 'llama-server.exe') : undefined)
|
|
228
|
-
appendCandidate(candidates, env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'llama.cpp', 'llama-server.exe') : undefined)
|
|
229
|
-
appendCandidate(candidates, env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'Microsoft', 'WindowsApps', 'llama-server.exe') : undefined)
|
|
230
|
-
appendCandidate(candidates, env.ProgramFiles ? path.join(env.ProgramFiles, 'llama.cpp', 'llama-server.exe') : undefined)
|
|
231
|
-
appendCandidate(candidates, env['ProgramFiles(x86)'] ? path.join(env['ProgramFiles(x86)'], 'llama.cpp', 'llama-server.exe') : undefined)
|
|
232
|
-
appendCandidate(candidates, env.USERPROFILE ? path.join(env.USERPROFILE, 'scoop', 'shims', 'llama-server.exe') : undefined)
|
|
233
|
-
appendCandidate(candidates, env.USERPROFILE ? path.join(env.USERPROFILE, 'scoop', 'apps', 'llama.cpp', 'current', 'llama-server.exe') : undefined)
|
|
234
|
-
} else if (platform === 'darwin') {
|
|
235
|
-
appendCandidate(candidates, '/opt/homebrew/bin/llama-server')
|
|
236
|
-
appendCandidate(candidates, '/usr/local/bin/llama-server')
|
|
237
|
-
appendCandidate(candidates, '/opt/local/bin/llama-server')
|
|
238
|
-
appendCandidate(candidates, env.HOME ? path.join(env.HOME, '.nix-profile', 'bin', 'llama-server') : undefined)
|
|
239
|
-
appendCandidate(candidates, env.HOME ? path.join(env.HOME, '.local', 'bin', 'llama-server') : undefined)
|
|
240
|
-
} else {
|
|
241
|
-
appendCandidate(candidates, '/usr/local/bin/llama-server')
|
|
242
|
-
appendCandidate(candidates, '/usr/bin/llama-server')
|
|
243
|
-
appendCandidate(candidates, env.HOME ? path.join(env.HOME, '.nix-profile', 'bin', 'llama-server') : undefined)
|
|
244
|
-
appendCandidate(candidates, env.HOME ? path.join(env.HOME, '.local', 'bin', 'llama-server') : undefined)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return candidates
|
|
248
|
-
}
|
|
249
|
-
|
|
250
161
|
export function llamaCppInstallPlans(platform: NodeJS.Platform = process.platform): LlamaCppInstallPlan[] {
|
|
251
162
|
if (platform === 'win32') {
|
|
252
163
|
return [
|
|
@@ -768,183 +679,6 @@ function startupDetail(output: string, fallback: string): string {
|
|
|
768
679
|
return output ? `${fallback}\n${output}` : fallback
|
|
769
680
|
}
|
|
770
681
|
|
|
771
|
-
function firstLine(text: string): string {
|
|
772
|
-
return text.split(/\r?\n/).map(line => line.trim()).find(Boolean) ?? ''
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
function appendCandidate(candidates: string[], candidate: string | undefined): void {
|
|
776
|
-
if (!candidate || candidates.includes(candidate)) return
|
|
777
|
-
candidates.push(candidate)
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
export function llamaCppSearchRoots(
|
|
781
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
782
|
-
platform: NodeJS.Platform = process.platform,
|
|
783
|
-
): string[] {
|
|
784
|
-
const roots: string[] = []
|
|
785
|
-
if (platform === 'win32') {
|
|
786
|
-
appendCandidate(roots, env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'Microsoft', 'WinGet', 'Packages') : undefined)
|
|
787
|
-
appendCandidate(roots, env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'Microsoft', 'WindowsApps') : undefined)
|
|
788
|
-
appendCandidate(roots, env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'Programs', 'llama.cpp') : undefined)
|
|
789
|
-
appendCandidate(roots, env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'llama.cpp') : undefined)
|
|
790
|
-
appendCandidate(roots, env.ProgramFiles ? path.join(env.ProgramFiles, 'llama.cpp') : undefined)
|
|
791
|
-
appendCandidate(roots, env.ProgramFiles ? path.join(env.ProgramFiles, 'WindowsApps') : undefined)
|
|
792
|
-
appendCandidate(roots, env.USERPROFILE ? path.join(env.USERPROFILE, 'scoop', 'apps', 'llama.cpp') : undefined)
|
|
793
|
-
appendCandidate(roots, env.USERPROFILE ? path.join(env.USERPROFILE, 'scoop', 'shims') : undefined)
|
|
794
|
-
appendCandidate(roots, path.join(getConfigDir(), 'runners', 'llama.cpp', 'build'))
|
|
795
|
-
appendCandidate(roots, path.join(getConfigDir(), 'runners', 'llama.cpp', 'build', 'bin'))
|
|
796
|
-
return roots
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
appendCandidate(roots, '/opt/homebrew/bin')
|
|
800
|
-
appendCandidate(roots, '/usr/local/bin')
|
|
801
|
-
appendCandidate(roots, '/opt/local/bin')
|
|
802
|
-
appendCandidate(roots, '/usr/bin')
|
|
803
|
-
appendCandidate(roots, env.HOME ? path.join(env.HOME, '.nix-profile', 'bin') : undefined)
|
|
804
|
-
appendCandidate(roots, env.HOME ? path.join(env.HOME, '.local', 'bin') : undefined)
|
|
805
|
-
appendCandidate(roots, path.join(getConfigDir(), 'runners', 'llama.cpp', 'build'))
|
|
806
|
-
appendCandidate(roots, path.join(getConfigDir(), 'runners', 'llama.cpp', 'build', 'bin'))
|
|
807
|
-
return roots
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
export async function discoverLlamaCppServerPaths(
|
|
811
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
812
|
-
platform: NodeJS.Platform = process.platform,
|
|
813
|
-
): Promise<string[]> {
|
|
814
|
-
return discoverExecutablePaths(platform === 'win32' ? ['llama-server.exe', 'llama-server'] : ['llama-server'], env, platform)
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
async function discoverLlamaCppCliPaths(
|
|
818
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
819
|
-
platform: NodeJS.Platform = process.platform,
|
|
820
|
-
): Promise<string[]> {
|
|
821
|
-
return discoverExecutablePaths(platform === 'win32' ? ['llama-cli.exe', 'llama-cli'] : ['llama-cli'], env, platform)
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
async function discoverExecutablePaths(
|
|
825
|
-
names: string[],
|
|
826
|
-
env: NodeJS.ProcessEnv,
|
|
827
|
-
platform: NodeJS.Platform,
|
|
828
|
-
): Promise<string[]> {
|
|
829
|
-
const found: string[] = []
|
|
830
|
-
const lowered = new Set(names.map(name => name.toLowerCase()))
|
|
831
|
-
for (const root of llamaCppSearchRoots(env, platform)) {
|
|
832
|
-
await walkForExecutable(root, lowered, found, 0, 5)
|
|
833
|
-
if (found.length >= 20) break
|
|
834
|
-
}
|
|
835
|
-
return found
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
async function walkForExecutable(
|
|
839
|
-
dir: string,
|
|
840
|
-
names: Set<string>,
|
|
841
|
-
found: string[],
|
|
842
|
-
depth: number,
|
|
843
|
-
maxDepth: number,
|
|
844
|
-
): Promise<void> {
|
|
845
|
-
if (depth > maxDepth || found.length >= 20) return
|
|
846
|
-
let entries: Array<import('node:fs').Dirent>
|
|
847
|
-
try {
|
|
848
|
-
entries = await fs.readdir(dir, { withFileTypes: true })
|
|
849
|
-
} catch {
|
|
850
|
-
return
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
for (const entry of entries) {
|
|
854
|
-
if (found.length >= 20) return
|
|
855
|
-
const fullPath = path.join(dir, entry.name)
|
|
856
|
-
const lowerName = entry.name.toLowerCase()
|
|
857
|
-
if ((entry.isFile() || entry.isSymbolicLink()) && names.has(lowerName)) {
|
|
858
|
-
appendCandidate(found, fullPath)
|
|
859
|
-
continue
|
|
860
|
-
}
|
|
861
|
-
if (entry.isDirectory() && shouldDescendRunnerDir(entry.name, depth)) {
|
|
862
|
-
await walkForExecutable(fullPath, names, found, depth + 1, maxDepth)
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
function shouldDescendRunnerDir(name: string, depth: number): boolean {
|
|
868
|
-
const lower = name.toLowerCase()
|
|
869
|
-
if (/(llama|ggml|bin|build|release|debug|current|package|windowsapps|x64|arm64)/.test(lower)) return true
|
|
870
|
-
return depth > 0 && lower.length <= 24
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
async function findAndPersistLlamaCppServer(
|
|
874
|
-
platform: NodeJS.Platform = process.platform,
|
|
875
|
-
): Promise<{ path: string | null; version: string | null }> {
|
|
876
|
-
const direct = await detectLlamaCppServerBinary()
|
|
877
|
-
if (direct.path) return direct
|
|
878
|
-
const discovered = await discoverLlamaCppServerPaths(process.env, platform)
|
|
879
|
-
const found = await detectLlamaCppServerBinary(discovered)
|
|
880
|
-
if (found.path) {
|
|
881
|
-
await setLlamaCppServerPath(found.path).catch(() => {})
|
|
882
|
-
}
|
|
883
|
-
return found
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
export function summarizeInstallOutput(output: string): string | undefined {
|
|
887
|
-
const lines = output
|
|
888
|
-
.split(/\r?\n/)
|
|
889
|
-
.map(cleanInstallLine)
|
|
890
|
-
.filter(Boolean)
|
|
891
|
-
.filter(line => !/^[\-\\|/_.=\s]+$/.test(line))
|
|
892
|
-
.filter(line => !/^\d+(\.\d+)?\s*(B|KB|MB|GB)\s*\/\s*\d+/i.test(line))
|
|
893
|
-
const unique = [...new Set(lines)]
|
|
894
|
-
return unique.slice(-6).join('\n') || undefined
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
export function humanInstallError(plan: LlamaCppInstallPlan, code: number | null): string {
|
|
898
|
-
if (plan.command === 'winget') return 'Windows could not install the local runner automatically.'
|
|
899
|
-
if (plan.command === 'brew') return 'Homebrew could not install the local runner automatically.'
|
|
900
|
-
if (plan.command === 'nix') return 'Nix could not install the local runner automatically.'
|
|
901
|
-
if (plan.command === 'port') return 'MacPorts could not install the local runner automatically.'
|
|
902
|
-
if (plan.command === 'git') return 'ethagent could not download the local runner source.'
|
|
903
|
-
if (plan.command === 'cmake') return 'ethagent could not build the local runner.'
|
|
904
|
-
return code === null
|
|
905
|
-
? `${plan.label} did not complete.`
|
|
906
|
-
: `${plan.label} failed with exit code ${code}.`
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
function installFailureDetail(code: number | null, output: string): string | undefined {
|
|
910
|
-
const details = [
|
|
911
|
-
code === null ? undefined : `exit code ${code}`,
|
|
912
|
-
summarizeInstallOutput(output),
|
|
913
|
-
].filter((item): item is string => Boolean(item))
|
|
914
|
-
return details.join('\n') || undefined
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
function cleanInstallLine(line: string): string {
|
|
918
|
-
return line
|
|
919
|
-
.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '')
|
|
920
|
-
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '')
|
|
921
|
-
.replace(/\s+/g, ' ')
|
|
922
|
-
.trim()
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
function installerProgressLabel(plan: LlamaCppInstallPlan): string {
|
|
926
|
-
if (plan.command === 'winget') return 'installing with Windows package manager...'
|
|
927
|
-
if (plan.command === 'brew') return 'installing with Homebrew...'
|
|
928
|
-
if (plan.command === 'nix') return 'installing with Nix...'
|
|
929
|
-
if (plan.command === 'port') return 'installing with MacPorts...'
|
|
930
|
-
return `installing with ${plan.label}...`
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
function formatInstallFailure(label: string, result: RunInstallResult): string {
|
|
934
|
-
if (result.ok) return label
|
|
935
|
-
return [label, result.message, result.detail].filter(Boolean).join(': ')
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function buildFailure(result: RunInstallResult): LlamaCppInstallResult {
|
|
939
|
-
return {
|
|
940
|
-
ok: false,
|
|
941
|
-
code: 'build-failed',
|
|
942
|
-
message: 'ethagent could not build the local runner.',
|
|
943
|
-
detail: result.ok ? undefined : [result.message, result.detail].filter(Boolean).join('\n'),
|
|
944
|
-
recovery: ['runner-path', 'retry-install', 'back'],
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
682
|
function sourceBuildServerCandidates(buildDir: string, platform: NodeJS.Platform): string[] {
|
|
949
683
|
const exe = platform === 'win32' ? 'llama-server.exe' : 'llama-server'
|
|
950
684
|
return [
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
export type RunResult = {
|
|
4
|
+
code: number
|
|
5
|
+
stdout: string
|
|
6
|
+
stderr: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function runCommand(cmd: string, args: string[], timeoutMs = 2000): Promise<RunResult | null> {
|
|
10
|
+
return new Promise(resolve => {
|
|
11
|
+
let settled = false
|
|
12
|
+
let child: ReturnType<typeof spawn>
|
|
13
|
+
try {
|
|
14
|
+
child = spawn(cmd, args, { windowsHide: true })
|
|
15
|
+
} catch {
|
|
16
|
+
resolve(null)
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let stdout = ''
|
|
21
|
+
let stderr = ''
|
|
22
|
+
const timer = setTimeout(() => {
|
|
23
|
+
if (settled) return
|
|
24
|
+
settled = true
|
|
25
|
+
try { child.kill() } catch { void 0 }
|
|
26
|
+
resolve(null)
|
|
27
|
+
}, timeoutMs)
|
|
28
|
+
|
|
29
|
+
child.stdout?.on('data', chunk => { stdout += chunk.toString() })
|
|
30
|
+
child.stderr?.on('data', chunk => { stderr += chunk.toString() })
|
|
31
|
+
child.on('error', () => {
|
|
32
|
+
if (settled) return
|
|
33
|
+
settled = true
|
|
34
|
+
clearTimeout(timer)
|
|
35
|
+
resolve(null)
|
|
36
|
+
})
|
|
37
|
+
child.on('close', code => {
|
|
38
|
+
if (settled) return
|
|
39
|
+
settled = true
|
|
40
|
+
clearTimeout(timer)
|
|
41
|
+
resolve({ code: code ?? -1, stdout, stderr })
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { atomicWriteText } from '../storage/atomicWrite.js'
|
|
4
|
+
import { ensureConfigDir, getConfigDir } from '../storage/config.js'
|
|
5
|
+
|
|
6
|
+
export type LocalRunnerConfig = {
|
|
7
|
+
llamaServerPath?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getLocalRunnerConfigPath(): string {
|
|
11
|
+
return path.join(getConfigDir(), 'local-runner.json')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function loadLocalRunnerConfig(): Promise<LocalRunnerConfig> {
|
|
15
|
+
try {
|
|
16
|
+
const raw = await fs.readFile(getLocalRunnerConfigPath(), 'utf8')
|
|
17
|
+
const parsed = JSON.parse(raw) as unknown
|
|
18
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
|
|
19
|
+
const value = (parsed as { llamaServerPath?: unknown }).llamaServerPath
|
|
20
|
+
return typeof value === 'string' && value.trim() ? { llamaServerPath: value.trim() } : {}
|
|
21
|
+
} catch (err: unknown) {
|
|
22
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {}
|
|
23
|
+
return {}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function saveLocalRunnerConfig(config: LocalRunnerConfig): Promise<void> {
|
|
28
|
+
await ensureConfigDir()
|
|
29
|
+
await atomicWriteText(getLocalRunnerConfigPath(), JSON.stringify(config, null, 2) + '\n')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function setLlamaCppServerPath(serverPath: string): Promise<void> {
|
|
33
|
+
await saveLocalRunnerConfig({ llamaServerPath: serverPath.trim() })
|
|
34
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { getConfigDir } from '../storage/config.js'
|
|
4
|
+
import {
|
|
5
|
+
loadLocalRunnerConfig,
|
|
6
|
+
setLlamaCppServerPath,
|
|
7
|
+
} from './llamacppConfig.js'
|
|
8
|
+
import { runCommand } from './llamacppCommands.js'
|
|
9
|
+
|
|
10
|
+
export async function detectLlamaCppServerBinary(extraCandidates: string[] = []): Promise<{ path: string | null; version: string | null }> {
|
|
11
|
+
const config = await loadLocalRunnerConfig()
|
|
12
|
+
const candidates = [
|
|
13
|
+
...llamaCppServerCandidates(process.env, process.platform, config.llamaServerPath),
|
|
14
|
+
...extraCandidates,
|
|
15
|
+
]
|
|
16
|
+
for (const candidate of candidates) {
|
|
17
|
+
const result = await runCommand(candidate, ['--version'])
|
|
18
|
+
if (!result) continue
|
|
19
|
+
const output = `${result.stdout}\n${result.stderr}`.trim()
|
|
20
|
+
if (result.code === 0 || output.length > 0) {
|
|
21
|
+
return { path: candidate, version: firstLine(output) || 'installed' }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { path: null, version: null }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function llamaCppServerCandidates(
|
|
28
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
29
|
+
platform: NodeJS.Platform = process.platform,
|
|
30
|
+
configuredPath?: string,
|
|
31
|
+
): string[] {
|
|
32
|
+
const candidates: string[] = []
|
|
33
|
+
appendCandidate(candidates, configuredPath)
|
|
34
|
+
appendCandidate(candidates, env.LLAMA_SERVER_PATH)
|
|
35
|
+
appendCandidate(candidates, env.LLAMACPP_SERVER_PATH)
|
|
36
|
+
appendCandidate(candidates, 'llama-server')
|
|
37
|
+
appendCandidate(candidates, 'llama-server.exe')
|
|
38
|
+
|
|
39
|
+
if (platform === 'win32') {
|
|
40
|
+
appendCandidate(candidates, env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'Programs', 'llama.cpp', 'llama-server.exe') : undefined)
|
|
41
|
+
appendCandidate(candidates, env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'llama.cpp', 'llama-server.exe') : undefined)
|
|
42
|
+
appendCandidate(candidates, env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'Microsoft', 'WindowsApps', 'llama-server.exe') : undefined)
|
|
43
|
+
appendCandidate(candidates, env.ProgramFiles ? path.join(env.ProgramFiles, 'llama.cpp', 'llama-server.exe') : undefined)
|
|
44
|
+
appendCandidate(candidates, env['ProgramFiles(x86)'] ? path.join(env['ProgramFiles(x86)'], 'llama.cpp', 'llama-server.exe') : undefined)
|
|
45
|
+
appendCandidate(candidates, env.USERPROFILE ? path.join(env.USERPROFILE, 'scoop', 'shims', 'llama-server.exe') : undefined)
|
|
46
|
+
appendCandidate(candidates, env.USERPROFILE ? path.join(env.USERPROFILE, 'scoop', 'apps', 'llama.cpp', 'current', 'llama-server.exe') : undefined)
|
|
47
|
+
} else if (platform === 'darwin') {
|
|
48
|
+
appendCandidate(candidates, '/opt/homebrew/bin/llama-server')
|
|
49
|
+
appendCandidate(candidates, '/usr/local/bin/llama-server')
|
|
50
|
+
appendCandidate(candidates, '/opt/local/bin/llama-server')
|
|
51
|
+
appendCandidate(candidates, env.HOME ? path.join(env.HOME, '.nix-profile', 'bin', 'llama-server') : undefined)
|
|
52
|
+
appendCandidate(candidates, env.HOME ? path.join(env.HOME, '.local', 'bin', 'llama-server') : undefined)
|
|
53
|
+
} else {
|
|
54
|
+
appendCandidate(candidates, '/usr/local/bin/llama-server')
|
|
55
|
+
appendCandidate(candidates, '/usr/bin/llama-server')
|
|
56
|
+
appendCandidate(candidates, env.HOME ? path.join(env.HOME, '.nix-profile', 'bin', 'llama-server') : undefined)
|
|
57
|
+
appendCandidate(candidates, env.HOME ? path.join(env.HOME, '.local', 'bin', 'llama-server') : undefined)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return candidates
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function llamaCppSearchRoots(
|
|
64
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
65
|
+
platform: NodeJS.Platform = process.platform,
|
|
66
|
+
): string[] {
|
|
67
|
+
const roots: string[] = []
|
|
68
|
+
if (platform === 'win32') {
|
|
69
|
+
appendCandidate(roots, env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'Microsoft', 'WinGet', 'Packages') : undefined)
|
|
70
|
+
appendCandidate(roots, env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'Microsoft', 'WindowsApps') : undefined)
|
|
71
|
+
appendCandidate(roots, env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'Programs', 'llama.cpp') : undefined)
|
|
72
|
+
appendCandidate(roots, env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'llama.cpp') : undefined)
|
|
73
|
+
appendCandidate(roots, env.ProgramFiles ? path.join(env.ProgramFiles, 'llama.cpp') : undefined)
|
|
74
|
+
appendCandidate(roots, env.ProgramFiles ? path.join(env.ProgramFiles, 'WindowsApps') : undefined)
|
|
75
|
+
appendCandidate(roots, env.USERPROFILE ? path.join(env.USERPROFILE, 'scoop', 'apps', 'llama.cpp') : undefined)
|
|
76
|
+
appendCandidate(roots, env.USERPROFILE ? path.join(env.USERPROFILE, 'scoop', 'shims') : undefined)
|
|
77
|
+
appendCandidate(roots, path.join(getConfigDir(), 'runners', 'llama.cpp', 'build'))
|
|
78
|
+
appendCandidate(roots, path.join(getConfigDir(), 'runners', 'llama.cpp', 'build', 'bin'))
|
|
79
|
+
return roots
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
appendCandidate(roots, '/opt/homebrew/bin')
|
|
83
|
+
appendCandidate(roots, '/usr/local/bin')
|
|
84
|
+
appendCandidate(roots, '/opt/local/bin')
|
|
85
|
+
appendCandidate(roots, '/usr/bin')
|
|
86
|
+
appendCandidate(roots, env.HOME ? path.join(env.HOME, '.nix-profile', 'bin') : undefined)
|
|
87
|
+
appendCandidate(roots, env.HOME ? path.join(env.HOME, '.local', 'bin') : undefined)
|
|
88
|
+
appendCandidate(roots, path.join(getConfigDir(), 'runners', 'llama.cpp', 'build'))
|
|
89
|
+
appendCandidate(roots, path.join(getConfigDir(), 'runners', 'llama.cpp', 'build', 'bin'))
|
|
90
|
+
return roots
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function discoverLlamaCppServerPaths(
|
|
94
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
95
|
+
platform: NodeJS.Platform = process.platform,
|
|
96
|
+
): Promise<string[]> {
|
|
97
|
+
return discoverExecutablePaths(platform === 'win32' ? ['llama-server.exe', 'llama-server'] : ['llama-server'], env, platform)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function discoverLlamaCppCliPaths(
|
|
101
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
102
|
+
platform: NodeJS.Platform = process.platform,
|
|
103
|
+
): Promise<string[]> {
|
|
104
|
+
return discoverExecutablePaths(platform === 'win32' ? ['llama-cli.exe', 'llama-cli'] : ['llama-cli'], env, platform)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function findAndPersistLlamaCppServer(
|
|
108
|
+
platform: NodeJS.Platform = process.platform,
|
|
109
|
+
): Promise<{ path: string | null; version: string | null }> {
|
|
110
|
+
const direct = await detectLlamaCppServerBinary()
|
|
111
|
+
if (direct.path) return direct
|
|
112
|
+
const discovered = await discoverLlamaCppServerPaths(process.env, platform)
|
|
113
|
+
const found = await detectLlamaCppServerBinary(discovered)
|
|
114
|
+
if (found.path) {
|
|
115
|
+
await setLlamaCppServerPath(found.path).catch(() => {})
|
|
116
|
+
}
|
|
117
|
+
return found
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function discoverExecutablePaths(
|
|
121
|
+
names: string[],
|
|
122
|
+
env: NodeJS.ProcessEnv,
|
|
123
|
+
platform: NodeJS.Platform,
|
|
124
|
+
): Promise<string[]> {
|
|
125
|
+
const found: string[] = []
|
|
126
|
+
const lowered = new Set(names.map(name => name.toLowerCase()))
|
|
127
|
+
for (const root of llamaCppSearchRoots(env, platform)) {
|
|
128
|
+
await walkForExecutable(root, lowered, found, 0, 5)
|
|
129
|
+
if (found.length >= 20) break
|
|
130
|
+
}
|
|
131
|
+
return found
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function walkForExecutable(
|
|
135
|
+
dir: string,
|
|
136
|
+
names: Set<string>,
|
|
137
|
+
found: string[],
|
|
138
|
+
depth: number,
|
|
139
|
+
maxDepth: number,
|
|
140
|
+
): Promise<void> {
|
|
141
|
+
if (depth > maxDepth || found.length >= 20) return
|
|
142
|
+
let entries: Array<import('node:fs').Dirent>
|
|
143
|
+
try {
|
|
144
|
+
entries = await fs.readdir(dir, { withFileTypes: true })
|
|
145
|
+
} catch {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
if (found.length >= 20) return
|
|
151
|
+
const fullPath = path.join(dir, entry.name)
|
|
152
|
+
const lowerName = entry.name.toLowerCase()
|
|
153
|
+
if ((entry.isFile() || entry.isSymbolicLink()) && names.has(lowerName)) {
|
|
154
|
+
appendCandidate(found, fullPath)
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
if (entry.isDirectory() && shouldDescendRunnerDir(entry.name, depth)) {
|
|
158
|
+
await walkForExecutable(fullPath, names, found, depth + 1, maxDepth)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function shouldDescendRunnerDir(name: string, depth: number): boolean {
|
|
164
|
+
const lower = name.toLowerCase()
|
|
165
|
+
if (/(llama|ggml|bin|build|release|debug|current|package|windowsapps|x64|arm64)/.test(lower)) return true
|
|
166
|
+
return depth > 0 && lower.length <= 24
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function firstLine(text: string): string {
|
|
170
|
+
return text.split(/\r?\n/).map(line => line.trim()).find(Boolean) ?? ''
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function appendCandidate(candidates: string[], candidate: string | undefined): void {
|
|
174
|
+
if (!candidate || candidates.includes(candidate)) return
|
|
175
|
+
candidates.push(candidate)
|
|
176
|
+
}
|