ethagent 3.3.3 → 4.0.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.
Files changed (322) hide show
  1. package/.claude-plugin/marketplace.json +11 -0
  2. package/.claude-plugin/plugin.json +35 -0
  3. package/LICENSE +1 -1
  4. package/README.md +64 -104
  5. package/commands/ethagent.md +40 -0
  6. package/package.json +16 -16
  7. package/src/app/keybindings/KeybindingProvider.tsx +1 -6
  8. package/src/app/keybindings/types.ts +1 -6
  9. package/src/cli/ResetConfirmView.tsx +54 -53
  10. package/src/cli/demo.ts +86 -0
  11. package/src/cli/hookIo.ts +45 -0
  12. package/src/cli/main.tsx +94 -123
  13. package/src/cli/memoryGuard.ts +49 -0
  14. package/src/cli/reset.ts +28 -70
  15. package/src/cli/sessionStart.ts +33 -0
  16. package/src/cli/status.ts +46 -0
  17. package/src/cli/sync.ts +167 -0
  18. package/src/cli/syncAdapters/claude-code.ts +86 -0
  19. package/src/cli/syncAdapters/codex.ts +66 -0
  20. package/src/cli/syncAdapters/index.ts +45 -0
  21. package/src/cli/syncAdapters/managedBlock.ts +175 -0
  22. package/src/cli/syncAdapters/shared.ts +63 -0
  23. package/src/identity/continuity/envelopeParse.ts +20 -1
  24. package/src/identity/continuity/publicSkills.ts +3 -1
  25. package/src/identity/continuity/skills/publicSkillsSync.ts +2 -1
  26. package/src/identity/continuity/skills/scaffold.ts +5 -2
  27. package/src/identity/continuity/snapshots.ts +12 -5
  28. package/src/identity/continuity/storage/defaults.ts +20 -19
  29. package/src/identity/continuity/storage/status.ts +1 -1
  30. package/src/identity/ens/ensLookup/constants.ts +1 -1
  31. package/src/identity/manager/IdentityManager.tsx +33 -0
  32. package/src/identity/{hub → manager}/OperationalRoutes.tsx +37 -18
  33. package/src/identity/{hub → manager}/Routes.tsx +48 -34
  34. package/src/identity/{hub → manager}/continuity/ContinuityDashboardScreen.tsx +9 -19
  35. package/src/identity/{hub → manager}/continuity/RebackupStorageScreen.tsx +3 -3
  36. package/src/identity/manager/continuity/RecoveryConfirmScreen.tsx +102 -0
  37. package/src/identity/{hub → manager}/continuity/SavePromptScreen.tsx +2 -3
  38. package/src/identity/{hub → manager}/continuity/completion.ts +1 -1
  39. package/src/identity/{hub → manager}/continuity/effects.ts +1 -1
  40. package/src/identity/{hub → manager}/continuity/skills/DeleteSkillConfirmScreen.tsx +2 -2
  41. package/src/identity/{hub → manager}/continuity/skills/NewSkillScreen.tsx +0 -5
  42. package/src/identity/{hub → manager}/continuity/skills/NewSkillVisibilityScreen.tsx +4 -4
  43. package/src/identity/{hub → manager}/continuity/skills/SkillActionsScreen.tsx +6 -22
  44. package/src/identity/{hub → manager}/continuity/skills/SkillsTreeScreen.tsx +5 -17
  45. package/src/identity/{hub → manager}/continuity/snapshot.ts +1 -1
  46. package/src/identity/{hub → manager}/continuity/vault.ts +1 -1
  47. package/src/identity/{hub → manager}/create/CreateFlow.tsx +59 -32
  48. package/src/identity/{hub → manager}/create/effects.ts +19 -10
  49. package/src/identity/manager/create/importScan.ts +122 -0
  50. package/src/identity/{hub → manager}/custody/CustodyEditFlow.tsx +17 -61
  51. package/src/identity/{hub → manager}/custody/actions.ts +1 -15
  52. package/src/identity/{hub → manager}/custody/routes.tsx +20 -40
  53. package/src/identity/{hub → manager}/custody/transactions.ts +1 -0
  54. package/src/identity/{hub → manager}/custody/types.ts +1 -2
  55. package/src/identity/{hub → manager}/custody/useCustodyEffects.ts +1 -1
  56. package/src/identity/{hub → manager}/ens/EnsEditAdvancedScreens.tsx +2 -2
  57. package/src/identity/{hub → manager}/ens/EnsEditMaintenanceScreens.tsx +12 -23
  58. package/src/identity/{hub → manager}/ens/EnsEditReviewScreens.tsx +18 -42
  59. package/src/identity/{hub → manager}/ens/EnsEditRunners.tsx +1 -1
  60. package/src/identity/{hub → manager}/ens/EnsEditShared.tsx +0 -2
  61. package/src/identity/{hub → manager}/ens/EnsEditSimpleScreens.tsx +10 -19
  62. package/src/identity/{hub → manager}/ens/EnsFlow.tsx +133 -41
  63. package/src/identity/{hub → manager}/ens/EnsOperatorWalletsScreen.tsx +14 -19
  64. package/src/identity/{hub → manager}/ens/editCopy.ts +1 -14
  65. package/src/identity/{hub → manager}/profile/EditProfileFlow.tsx +99 -66
  66. package/src/identity/{hub → manager}/profile/effects.ts +1 -3
  67. package/src/identity/{hub → manager}/profile/operatorSave.ts +1 -1
  68. package/src/identity/{hub → manager}/profile/state.ts +1 -1
  69. package/src/identity/{hub/identityHubReducer.ts → manager/reducer.ts} +25 -26
  70. package/src/identity/{hub → manager}/restore/RestoreFlow.tsx +16 -24
  71. package/src/identity/{hub → manager}/restore/apply.ts +1 -1
  72. package/src/identity/{hub → manager}/restore/auth.ts +1 -1
  73. package/src/identity/{hub → manager}/restore/discover.ts +1 -1
  74. package/src/identity/{hub → manager}/restore/fetch.ts +1 -1
  75. package/src/identity/{hub → manager}/restore/restoreAdmin.ts +1 -1
  76. package/src/identity/{hub → manager}/restore/useRestoreEffects.ts +2 -9
  77. package/src/identity/{hub → manager}/settings/StorageCredentialScreen.tsx +10 -25
  78. package/src/identity/{hub → manager}/shared/components/DetailsScreen.tsx +5 -7
  79. package/src/identity/{hub → manager}/shared/components/ErrorScreen.tsx +6 -10
  80. package/src/identity/{hub → manager}/shared/components/FlowTimeline.tsx +4 -3
  81. package/src/identity/{hub → manager}/shared/components/IdentitySummary.tsx +19 -59
  82. package/src/identity/manager/shared/components/LazyMenu.tsx +147 -0
  83. package/src/identity/manager/shared/components/MenuScreen.tsx +220 -0
  84. package/src/identity/manager/shared/components/OperationCompleteScreen.tsx +28 -0
  85. package/src/identity/{hub → manager}/shared/components/UnlinkedIdentityScreen.tsx +9 -10
  86. package/src/identity/{hub → manager}/shared/components/WalletApprovalScreen.tsx +1 -2
  87. package/src/identity/manager/shared/components/Wordmark.tsx +54 -0
  88. package/src/identity/{hub → manager}/shared/components/menuFlagsFromReconciliation.ts +39 -15
  89. package/src/identity/{hub → manager}/shared/effects/profilePrep.ts +1 -1
  90. package/src/identity/manager/shared/effects/types.ts +30 -0
  91. package/src/identity/{hub → manager}/shared/model/copy.ts +0 -4
  92. package/src/identity/{hub → manager}/shared/model/errors.ts +32 -3
  93. package/src/identity/{hub → manager}/shared/model/network.ts +2 -2
  94. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/hook.ts +5 -0
  95. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/run.ts +1 -1
  96. package/src/identity/{hub/shared/reconciliation/useAgentReconciliation.ts → manager/shared/reconciliation/index.ts} +6 -0
  97. package/src/identity/{hub → manager}/shared/utils.ts +6 -10
  98. package/src/identity/{hub → manager}/transfer/TokenTransferFlow.tsx +3 -3
  99. package/src/identity/{hub → manager}/transfer/TokenTransferScreens.tsx +4 -10
  100. package/src/identity/{hub → manager}/transfer/effects.ts +1 -1
  101. package/src/identity/{hub → manager}/types.ts +5 -6
  102. package/src/identity/{hub/useIdentityHubContinuity.ts → manager/useContinuity.ts} +59 -27
  103. package/src/identity/{hub/useIdentityHubController.ts → manager/useController.ts} +38 -35
  104. package/src/identity/{hub/useIdentityHubSideEffects.ts → manager/useSideEffects.ts} +40 -4
  105. package/src/identity/registry/erc8004/discovery.ts +3 -17
  106. package/src/identity/registry/erc8004/utils.ts +1 -1
  107. package/src/identity/storage/ipfs.ts +21 -1
  108. package/src/identity/wallet/browserWallet/html.ts +10 -2
  109. package/src/identity/wallet/browserWallet/http.ts +18 -0
  110. package/src/identity/wallet/browserWallet/requestServer.ts +5 -1
  111. package/src/identity/wallet/browserWallet/requests.ts +10 -28
  112. package/src/identity/wallet/browserWallet/session.ts +26 -33
  113. package/src/identity/wallet/browserWallet/validation.ts +14 -0
  114. package/src/identity/wallet/browserWallet/walletPageSource.ts +22 -40
  115. package/src/identity/wallet/page/boot.ts +43 -0
  116. package/src/identity/wallet/page/config.ts +59 -0
  117. package/src/identity/wallet/page/constants.ts +12 -0
  118. package/src/identity/wallet/page/copy.ts +47 -68
  119. package/src/identity/wallet/page/css.ts +638 -0
  120. package/src/identity/wallet/page/{errorView.ts → errors.ts} +5 -14
  121. package/src/identity/wallet/page/{controller.ts → flow.ts} +4 -71
  122. package/src/identity/wallet/page/markup.ts +44 -34
  123. package/src/identity/wallet/page/{walletProvider.ts → provider.ts} +0 -3
  124. package/src/identity/wallet/page/resize.ts +95 -0
  125. package/src/identity/wallet/page/state.ts +135 -8
  126. package/src/identity/wallet/page/timeline.ts +161 -0
  127. package/src/identity/wallet/page/view.ts +22 -302
  128. package/src/storage/config.ts +30 -80
  129. package/src/storage/reset.ts +31 -0
  130. package/src/storage/secrets.ts +1 -16
  131. package/src/ui/Select.tsx +27 -5
  132. package/src/ui/Spinner.tsx +16 -15
  133. package/src/ui/Surface.tsx +21 -17
  134. package/src/ui/TextArea.tsx +173 -0
  135. package/src/ui/TextInput.tsx +31 -133
  136. package/src/ui/theme.ts +22 -13
  137. package/src/utils/clipboard.ts +0 -140
  138. package/src/app/FirstRun.tsx +0 -577
  139. package/src/app/FirstRunTimeline.tsx +0 -51
  140. package/src/app/firstRunConfig.ts +0 -26
  141. package/src/app/hooks/useCancelRequest.ts +0 -22
  142. package/src/app/hooks/useDoublePress.ts +0 -46
  143. package/src/app/hooks/useExitOnCtrlC.ts +0 -36
  144. package/src/auth/openaiOAuth/credentials.ts +0 -47
  145. package/src/auth/openaiOAuth/crypto.ts +0 -23
  146. package/src/auth/openaiOAuth/index.ts +0 -238
  147. package/src/auth/openaiOAuth/landingPage.ts +0 -116
  148. package/src/auth/openaiOAuth/listener.ts +0 -151
  149. package/src/auth/openaiOAuth/refresh.ts +0 -70
  150. package/src/auth/openaiOAuth/shared.ts +0 -115
  151. package/src/chat/ChatBottomPane.tsx +0 -296
  152. package/src/chat/ChatScreen.tsx +0 -1685
  153. package/src/chat/ConversationStack.tsx +0 -56
  154. package/src/chat/MessageList.tsx +0 -638
  155. package/src/chat/SessionStatus.tsx +0 -53
  156. package/src/chat/chatEnvironment.ts +0 -16
  157. package/src/chat/chatScreenUtils.ts +0 -194
  158. package/src/chat/chatSessionState.ts +0 -146
  159. package/src/chat/chatTurnContext.ts +0 -50
  160. package/src/chat/chatTurnOrchestrator.ts +0 -603
  161. package/src/chat/chatTurnRows.ts +0 -64
  162. package/src/chat/commands.ts +0 -494
  163. package/src/chat/continuityEditReview.ts +0 -42
  164. package/src/chat/display/DiffView.tsx +0 -193
  165. package/src/chat/display/SyntaxText.tsx +0 -192
  166. package/src/chat/display/toolCallDisplay.ts +0 -103
  167. package/src/chat/display/toolResultDisplay.ts +0 -19
  168. package/src/chat/input/ChatInput.tsx +0 -625
  169. package/src/chat/input/chatInputHelpers.ts +0 -62
  170. package/src/chat/input/chatInputState.ts +0 -247
  171. package/src/chat/input/chatPaste.ts +0 -49
  172. package/src/chat/input/imageRefs.ts +0 -30
  173. package/src/chat/input/inputRendering.tsx +0 -93
  174. package/src/chat/input/textCursor.ts +0 -212
  175. package/src/chat/messageMarkdown.ts +0 -220
  176. package/src/chat/messageRows.ts +0 -43
  177. package/src/chat/planImplementation.ts +0 -62
  178. package/src/chat/slashCommandHandlers.ts +0 -122
  179. package/src/chat/slashCommandViews.ts +0 -120
  180. package/src/chat/transcript/TranscriptView.tsx +0 -184
  181. package/src/chat/transcript/transcriptViewport.ts +0 -295
  182. package/src/chat/views/ContextLimitView.tsx +0 -95
  183. package/src/chat/views/ContinuityEditReviewView.tsx +0 -50
  184. package/src/chat/views/CopyPicker.tsx +0 -50
  185. package/src/chat/views/PermissionPrompt.tsx +0 -156
  186. package/src/chat/views/PermissionsView.tsx +0 -165
  187. package/src/chat/views/PlanApprovalView.tsx +0 -91
  188. package/src/chat/views/ResumeView.tsx +0 -273
  189. package/src/chat/views/RewindView.tsx +0 -412
  190. package/src/cli/preview.tsx +0 -14
  191. package/src/cli/updateNotice.ts +0 -54
  192. package/src/identity/continuity/privateEdit/apply.ts +0 -170
  193. package/src/identity/continuity/privateEdit/diff.ts +0 -6
  194. package/src/identity/continuity/privateEdit/files.ts +0 -23
  195. package/src/identity/continuity/privateEdit/types.ts +0 -28
  196. package/src/identity/continuity/privateEdit.ts +0 -46
  197. package/src/identity/hub/IdentityHub.tsx +0 -14
  198. package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +0 -104
  199. package/src/identity/hub/ens/effects.ts +0 -218
  200. package/src/identity/hub/shared/components/MenuScreen.tsx +0 -241
  201. package/src/identity/hub/shared/effects/types.ts +0 -53
  202. package/src/identity/hub/shared/reconciliation/index.ts +0 -14
  203. package/src/identity/wallet/page/grainient.ts +0 -278
  204. package/src/identity/wallet/page/html.ts +0 -28
  205. package/src/identity/wallet/page/styles/base.ts +0 -259
  206. package/src/identity/wallet/page/styles/components.ts +0 -262
  207. package/src/identity/wallet/page/styles/index.ts +0 -5
  208. package/src/identity/wallet/page/styles/responsive.ts +0 -247
  209. package/src/identity/wallet/page.tsx +0 -38
  210. package/src/mcp/approvals.ts +0 -113
  211. package/src/mcp/config.ts +0 -235
  212. package/src/mcp/manager.ts +0 -482
  213. package/src/mcp/managerHelpers.ts +0 -70
  214. package/src/mcp/names.ts +0 -19
  215. package/src/mcp/output.ts +0 -96
  216. package/src/models/ModelPicker.tsx +0 -1009
  217. package/src/models/catalog.ts +0 -327
  218. package/src/models/huggingface.ts +0 -712
  219. package/src/models/huggingfaceStorage.ts +0 -136
  220. package/src/models/llamacpp.ts +0 -848
  221. package/src/models/llamacppCommands.ts +0 -44
  222. package/src/models/llamacppConfig.ts +0 -34
  223. package/src/models/llamacppDiscovery.ts +0 -176
  224. package/src/models/llamacppOutput.ts +0 -65
  225. package/src/models/llamacppPreflight.ts +0 -158
  226. package/src/models/modelDisplay.ts +0 -180
  227. package/src/models/modelPickerCatalogFlow.ts +0 -56
  228. package/src/models/modelPickerCredentials.ts +0 -166
  229. package/src/models/modelPickerData.ts +0 -41
  230. package/src/models/modelPickerDisplay.tsx +0 -132
  231. package/src/models/modelPickerHfFlow.ts +0 -192
  232. package/src/models/modelPickerLocalRunnerFlow.ts +0 -115
  233. package/src/models/modelPickerOptions.ts +0 -457
  234. package/src/models/modelPickerTypes.ts +0 -69
  235. package/src/models/modelPickerUninstallFlow.ts +0 -48
  236. package/src/models/modelPickerViewHelpers.ts +0 -174
  237. package/src/models/modelRecommendation.ts +0 -139
  238. package/src/models/providerDisplay.ts +0 -16
  239. package/src/models/runtimeDetection.ts +0 -81
  240. package/src/models/uncensoredCatalog.ts +0 -86
  241. package/src/providers/anthropic.ts +0 -290
  242. package/src/providers/contracts.ts +0 -71
  243. package/src/providers/errors.ts +0 -80
  244. package/src/providers/gemini.ts +0 -391
  245. package/src/providers/openai-chat.ts +0 -474
  246. package/src/providers/openai-responses-format.ts +0 -177
  247. package/src/providers/openai-responses.ts +0 -306
  248. package/src/providers/openaiChatWire.ts +0 -124
  249. package/src/providers/registry.ts +0 -120
  250. package/src/providers/retry.ts +0 -58
  251. package/src/providers/sse.ts +0 -93
  252. package/src/runtime/compaction.ts +0 -395
  253. package/src/runtime/cwd.ts +0 -43
  254. package/src/runtime/providerTurn.ts +0 -38
  255. package/src/runtime/sessionMode.ts +0 -55
  256. package/src/runtime/systemPrompt.ts +0 -213
  257. package/src/runtime/textToolParser.ts +0 -161
  258. package/src/runtime/toolClaimGuards.ts +0 -143
  259. package/src/runtime/toolExecution.ts +0 -304
  260. package/src/runtime/toolIntent.ts +0 -143
  261. package/src/runtime/turn.ts +0 -369
  262. package/src/runtime/turnNudges.ts +0 -223
  263. package/src/runtime/turnTypes.ts +0 -86
  264. package/src/storage/factoryReset.ts +0 -127
  265. package/src/storage/history.ts +0 -58
  266. package/src/storage/permissions.ts +0 -76
  267. package/src/storage/rewind.ts +0 -266
  268. package/src/storage/sessionExport.ts +0 -49
  269. package/src/storage/sessions.ts +0 -495
  270. package/src/tools/bashSafety.ts +0 -186
  271. package/src/tools/bashTool.ts +0 -140
  272. package/src/tools/changeDirectoryTool.ts +0 -213
  273. package/src/tools/contracts.ts +0 -192
  274. package/src/tools/deleteFileTool.ts +0 -116
  275. package/src/tools/editTool.ts +0 -165
  276. package/src/tools/editUtils.ts +0 -170
  277. package/src/tools/fileDiff.ts +0 -261
  278. package/src/tools/listDirectoryTool.ts +0 -55
  279. package/src/tools/listSkillFilesTool.ts +0 -77
  280. package/src/tools/listSkillsTool.ts +0 -68
  281. package/src/tools/mcpResourceTools.ts +0 -95
  282. package/src/tools/permissionRules.ts +0 -85
  283. package/src/tools/privateContinuityEditTool.ts +0 -187
  284. package/src/tools/privateContinuityReadTool.ts +0 -106
  285. package/src/tools/readSkillTool.ts +0 -107
  286. package/src/tools/readTool.ts +0 -85
  287. package/src/tools/registry.ts +0 -103
  288. package/src/tools/writeFileTool.ts +0 -167
  289. package/src/ui/BrandSplash.tsx +0 -133
  290. package/src/ui/terminalTitle.ts +0 -30
  291. package/src/utils/images.ts +0 -140
  292. package/src/utils/markdownSegments.ts +0 -51
  293. package/src/utils/messages.ts +0 -37
  294. package/src/utils/withRetry.ts +0 -324
  295. /package/src/identity/{hub → manager}/continuity/state.ts +0 -0
  296. /package/src/identity/{hub → manager}/custody/helpers.ts +0 -0
  297. /package/src/identity/{hub → manager}/custody/preflight.ts +0 -0
  298. /package/src/identity/{hub → manager}/custody/state.ts +0 -0
  299. /package/src/identity/{hub → manager}/custody/useCustodyFlow.tsx +0 -0
  300. /package/src/identity/{hub → manager}/ens/EnsEditFlow.tsx +0 -0
  301. /package/src/identity/{hub → manager}/ens/advancedEnsValidation.ts +0 -0
  302. /package/src/identity/{hub → manager}/ens/state.ts +0 -0
  303. /package/src/identity/{hub → manager}/ens/transactions.ts +0 -0
  304. /package/src/identity/{hub → manager}/ens/types.ts +0 -0
  305. /package/src/identity/{hub → manager}/profile/identity.ts +0 -0
  306. /package/src/identity/{hub → manager}/restore/envelopes.ts +0 -0
  307. /package/src/identity/{hub → manager}/restore/helpers.ts +0 -0
  308. /package/src/identity/{hub → manager}/restore/recovery.ts +0 -0
  309. /package/src/identity/{hub → manager}/restore/resolve.ts +0 -0
  310. /package/src/identity/{hub → manager}/shared/components/BusyScreen.tsx +0 -0
  311. /package/src/identity/{hub → manager}/shared/components/NetworkScreen.tsx +0 -0
  312. /package/src/identity/{hub → manager}/shared/components/PinataJwtInput.tsx +0 -0
  313. /package/src/identity/{hub → manager}/shared/effects/receipts.ts +0 -0
  314. /package/src/identity/{hub → manager}/shared/effects/sync.ts +0 -0
  315. /package/src/identity/{hub → manager}/shared/model/format.ts +0 -0
  316. /package/src/identity/{hub → manager}/shared/operatorWallets.ts +0 -0
  317. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/ownership.ts +0 -0
  318. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/types.ts +0 -0
  319. /package/src/identity/{hub → manager}/shared/reconciliation/walletSetup.ts +0 -0
  320. /package/src/identity/{hub → manager}/shared/txGuard.ts +0 -0
  321. /package/src/identity/{hub → manager}/transfer/progress.ts +0 -0
  322. /package/src/identity/{hub → manager}/transfer/state.ts +0 -0
@@ -1,848 +0,0 @@
1
- import { spawn } from 'node:child_process'
2
- import fs from 'node:fs/promises'
3
- import path from 'node:path'
4
- import { atomicWriteText } from '../storage/atomicWrite.js'
5
- import { ensureConfigDir, getConfigDir } from '../storage/config.js'
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'
44
-
45
- export const DEFAULT_LLAMA_HOST = process.env.LLAMACPP_HOST ?? 'http://localhost:8080'
46
-
47
- export type LlamaCppStatus = {
48
- binaryPresent: boolean
49
- binaryPath: string | null
50
- version: string | null
51
- serverUp: boolean
52
- servedModels: string[]
53
- }
54
-
55
- type RunInstallResult = { ok: true } | { ok: false; message: string; detail?: string }
56
-
57
- export type LlamaCppInstallPhase = 'checking' | 'installing' | 'finding' | 'building'
58
- export type LlamaCppInstallRecovery = 'retry-install' | 'source-build' | 'runner-path' | 'back'
59
-
60
- export type LlamaCppInstallProgress = {
61
- phase: LlamaCppInstallPhase
62
- label: string
63
- progress: number
64
- }
65
-
66
- export type LlamaCppInstallResult =
67
- | { ok: true; serverPath?: string }
68
- | {
69
- ok: false
70
- code: 'install-failed' | 'server-not-found' | 'missing-tools' | 'build-failed'
71
- message: string
72
- detail?: string
73
- recovery: LlamaCppInstallRecovery[]
74
- candidatePaths?: string[]
75
- }
76
-
77
- export type LlamaCppStartFailureCode =
78
- | 'runner-not-installed'
79
- | 'model-file-missing'
80
- | 'different-model-running'
81
- | 'spawn-failed'
82
- | 'runner-exited'
83
- | 'readiness-timeout'
84
-
85
- export type LlamaCppStartResult =
86
- | { ok: true; alreadyRunning: boolean }
87
- | {
88
- ok: false
89
- code: LlamaCppStartFailureCode
90
- message: string
91
- detail?: string
92
- servedModels?: string[]
93
- }
94
-
95
- export type LlamaCppInstallPlan = {
96
- command: string
97
- args: string[]
98
- label: string
99
- timeoutMs?: number
100
- }
101
-
102
- type LlamaCppStartDeps = {
103
- access?: typeof fs.access
104
- binaryPath?: string
105
- spawnImpl?: (command: string, args: readonly string[], options: NonNullable<Parameters<typeof spawn>[2]>) => ReturnType<typeof spawn>
106
- killRogue?: (host: string) => Promise<KillRogueResult>
107
- rogueDrainTimeoutMs?: number
108
- rogueDrainPollMs?: number
109
- }
110
-
111
- function runInstallCommand(
112
- plan: LlamaCppInstallPlan,
113
- timeoutMs: number,
114
- ): Promise<RunInstallResult> {
115
- return new Promise(resolve => {
116
- let child: ReturnType<typeof spawn>
117
- try {
118
- child = spawn(plan.command, plan.args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
119
- } catch (err) {
120
- resolve({ ok: false, message: (err as Error).message })
121
- return
122
- }
123
-
124
- let settled = false
125
- const settle = (result: RunInstallResult): void => {
126
- if (settled) return
127
- settled = true
128
- clearTimeout(timer)
129
- try { child.kill() } catch { void 0 }
130
- resolve(result)
131
- }
132
- const timer = setTimeout(() => settle({ ok: false, message: `${plan.label} timed out` }), timeoutMs)
133
- let output = ''
134
- const onData = (chunk: Buffer | string): void => { output += chunk.toString() }
135
- child.stdout?.on('data', onData)
136
- child.stderr?.on('data', onData)
137
- child.on('error', err => settle({ ok: false, message: err.message }))
138
- child.on('close', code => {
139
- if (code === 0) settle({ ok: true })
140
- else settle({
141
- ok: false,
142
- message: humanInstallError(plan, code),
143
- detail: installFailureDetail(code, output),
144
- })
145
- })
146
- })
147
- }
148
-
149
- async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response | null> {
150
- const controller = new AbortController()
151
- const timer = setTimeout(() => controller.abort(), timeoutMs)
152
- try {
153
- return await fetch(url, { signal: controller.signal })
154
- } catch {
155
- return null
156
- } finally {
157
- clearTimeout(timer)
158
- }
159
- }
160
-
161
- export function llamaCppInstallPlans(platform: NodeJS.Platform = process.platform): LlamaCppInstallPlan[] {
162
- if (platform === 'win32') {
163
- return [
164
- {
165
- label: 'winget llama.cpp',
166
- command: 'winget',
167
- args: ['install', 'llama.cpp', '--accept-source-agreements', '--accept-package-agreements'],
168
- },
169
- {
170
- label: 'winget llama.cpp exact id',
171
- command: 'winget',
172
- args: ['install', '--id', 'ggml.llamacpp', '-e', '--accept-source-agreements', '--accept-package-agreements'],
173
- },
174
- ]
175
- }
176
- if (platform === 'darwin') {
177
- return [
178
- { label: 'brew llama.cpp', command: 'brew', args: ['install', 'llama.cpp'] },
179
- { label: 'nix llama.cpp', command: 'nix', args: ['profile', 'install', 'nixpkgs#llama-cpp'] },
180
- { label: 'macports llama.cpp', command: 'port', args: ['install', 'llama.cpp'] },
181
- ]
182
- }
183
- return [
184
- { label: 'brew llama.cpp', command: 'brew', args: ['install', 'llama.cpp'] },
185
- { label: 'nix llama.cpp', command: 'nix', args: ['profile', 'install', 'nixpkgs#llama-cpp'] },
186
- ]
187
- }
188
-
189
- export async function installLlamaCppRunner(
190
- onProgress?: (progress: LlamaCppInstallProgress) => void,
191
- platform: NodeJS.Platform = process.platform,
192
- ): Promise<LlamaCppInstallResult> {
193
- const plans = llamaCppInstallPlans(platform)
194
- const failures: string[] = []
195
- onProgress?.({ phase: 'checking', label: 'checking local runner installers...', progress: 0.08 })
196
- for (const plan of plans) {
197
- onProgress?.({ phase: 'installing', label: installerProgressLabel(plan), progress: 0.34 })
198
- const result = await runInstallCommand(plan, plan.timeoutMs ?? 10 * 60_000)
199
- if (result.ok) {
200
- onProgress?.({ phase: 'finding', label: 'finding llama-server...', progress: 0.78 })
201
- const binary = await findAndPersistLlamaCppServer(platform)
202
- if (binary.path) return { ok: true, serverPath: binary.path }
203
- const cliPaths = await discoverLlamaCppCliPaths(process.env, platform)
204
- return {
205
- ok: false,
206
- code: 'server-not-found',
207
- message: 'llama.cpp installed, but the local server was not found.',
208
- detail: cliPaths.length > 0
209
- ? `Found llama-cli, but ethagent needs llama-server to run local chat.\n${cliPaths.slice(0, 3).join('\n')}`
210
- : 'The package manager finished, but it did not expose llama-server on this machine.',
211
- recovery: ['source-build', 'runner-path', 'retry-install', 'back'],
212
- candidatePaths: await discoverLlamaCppServerPaths(process.env, platform),
213
- }
214
- }
215
- failures.push(formatInstallFailure(plan.label, result))
216
- }
217
- return {
218
- ok: false,
219
- code: 'install-failed',
220
- message: failures.length > 0
221
- ? 'ethagent could not install the local runner automatically.'
222
- : 'no supported local runner installer was found for this platform.',
223
- detail: failures.join('\n'),
224
- recovery: ['retry-install', 'source-build', 'runner-path', 'back'],
225
- }
226
- }
227
-
228
- export async function buildLlamaCppRunner(
229
- onProgress?: (progress: LlamaCppInstallProgress) => void,
230
- platform: NodeJS.Platform = process.platform,
231
- ): Promise<LlamaCppInstallResult> {
232
- return installLlamaCppFromSource(onProgress, platform)
233
- }
234
-
235
- export async function isLlamaCppServerUp(host: string = DEFAULT_LLAMA_HOST, timeoutMs = 800): Promise<boolean> {
236
- const response = await fetchServedModels(host, timeoutMs)
237
- return response.up
238
- }
239
-
240
- export async function listServedModels(host: string = DEFAULT_LLAMA_HOST): Promise<string[]> {
241
- const response = await fetchServedModels(host, 1500)
242
- return response.models
243
- }
244
-
245
- async function fetchServedModels(host: string = DEFAULT_LLAMA_HOST, timeoutMs = 1500): Promise<{ up: boolean; models: string[] }> {
246
- const response = await fetchWithTimeout(`${host.replace(/\/+$/, '')}/v1/models`, timeoutMs)
247
- if (!response || !response.ok) return { up: false, models: [] }
248
- try {
249
- const data = await response.json() as { data?: Array<{ id?: unknown }> }
250
- const models = (data.data ?? [])
251
- .map(item => typeof item.id === 'string' ? item.id : '')
252
- .filter(Boolean)
253
- return { up: true, models }
254
- } catch {
255
- return { up: true, models: [] }
256
- }
257
- }
258
-
259
- let cachedLlamaCppContextSize: number | null = null
260
- const llamaCppContextSizeListeners = new Set<(size: number) => void>()
261
-
262
- export async function fetchLlamaCppContextSize(
263
- host: string = DEFAULT_LLAMA_HOST,
264
- timeoutMs = 1500,
265
- ): Promise<number | null> {
266
- const response = await fetchWithTimeout(`${host.replace(/\/+$/, '')}/props`, timeoutMs)
267
- if (!response || !response.ok) return null
268
- try {
269
- const data = await response.json() as {
270
- n_ctx?: unknown
271
- default_generation_settings?: { n_ctx?: unknown }
272
- }
273
- const raw = typeof data.n_ctx === 'number'
274
- ? data.n_ctx
275
- : typeof data.default_generation_settings?.n_ctx === 'number'
276
- ? data.default_generation_settings.n_ctx
277
- : null
278
- if (typeof raw === 'number' && raw > 0) {
279
- const changed = cachedLlamaCppContextSize !== raw
280
- cachedLlamaCppContextSize = raw
281
- if (changed) {
282
- for (const listener of llamaCppContextSizeListeners) {
283
- try { listener(raw) } catch { void 0 }
284
- }
285
- }
286
- return raw
287
- }
288
- return null
289
- } catch {
290
- return null
291
- }
292
- }
293
-
294
- export function getCachedLlamaCppContextSize(): number | null {
295
- return cachedLlamaCppContextSize
296
- }
297
-
298
- export function setCachedLlamaCppContextSize(size: number): void {
299
- if (!(size > 0)) return
300
- const changed = cachedLlamaCppContextSize !== size
301
- cachedLlamaCppContextSize = size
302
- if (changed) {
303
- for (const listener of llamaCppContextSizeListeners) {
304
- try { listener(size) } catch { void 0 }
305
- }
306
- }
307
- }
308
-
309
- export function onLlamaCppContextSizeChange(listener: (size: number) => void): () => void {
310
- llamaCppContextSizeListeners.add(listener)
311
- return () => { llamaCppContextSizeListeners.delete(listener) }
312
- }
313
-
314
- export async function detectLlamaCpp(host: string = DEFAULT_LLAMA_HOST): Promise<LlamaCppStatus> {
315
- const [binary, serverUp] = await Promise.all([
316
- detectLlamaCppServerBinary(),
317
- isLlamaCppServerUp(host),
318
- ])
319
- const servedModels = serverUp ? await listServedModels(host) : []
320
- if (serverUp) void fetchLlamaCppContextSize(host)
321
- return {
322
- binaryPresent: binary.path !== null,
323
- binaryPath: binary.path,
324
- version: binary.version,
325
- serverUp,
326
- servedModels,
327
- }
328
- }
329
-
330
- export async function startLlamaCppServer(args: {
331
- modelPath: string
332
- modelAlias: string
333
- host?: string
334
- ctxSize?: number
335
- mmprojPath?: string
336
- readinessTimeoutMs?: number
337
- pollMs?: number
338
- deps?: LlamaCppStartDeps
339
- }): Promise<LlamaCppStartResult> {
340
- const host = args.host ?? DEFAULT_LLAMA_HOST
341
- let initialStatus = await servedModelStatus(host, args.modelAlias)
342
- if (initialStatus.state === 'ready' && args.mmprojPath) {
343
- const pid = await readPidFile()
344
- if (!pid) {
345
- await (args.deps?.killRogue ?? killRogueLlamaProcesses)(host).catch(() => null)
346
- const drained = await waitForHostDown(host, args.deps?.rogueDrainTimeoutMs ?? 6000, args.deps?.rogueDrainPollMs ?? 200)
347
- if (!drained) {
348
- return startFailure('different-model-running', {
349
- servedModels: initialStatus.models,
350
- detail: 'another process is holding the local model port and could not be stopped automatically',
351
- })
352
- }
353
- initialStatus = await servedModelStatus(host, args.modelAlias)
354
- }
355
- }
356
- if (initialStatus.state === 'ready') {
357
- void fetchLlamaCppContextSize(host)
358
- return { ok: true, alreadyRunning: true }
359
- }
360
- if (initialStatus.state === 'different') {
361
- return startFailure('different-model-running', {
362
- servedModels: initialStatus.models,
363
- })
364
- }
365
-
366
- const accessFn = args.deps?.access ?? fs.access
367
- try {
368
- await accessFn(args.modelPath)
369
- } catch {
370
- return startFailure('model-file-missing', { detail: args.modelPath })
371
- }
372
-
373
- if (args.mmprojPath) {
374
- try {
375
- await accessFn(args.mmprojPath)
376
- } catch {
377
- return startFailure('model-file-missing', { detail: args.mmprojPath })
378
- }
379
- }
380
-
381
- const binaryPath = args.deps?.binaryPath ?? (await findAndPersistLlamaCppServer()).path
382
- if (!binaryPath) {
383
- return startFailure('runner-not-installed')
384
- }
385
-
386
- const url = new URL(host)
387
- const listenHost = url.hostname || '127.0.0.1'
388
- const port = url.port || (url.protocol === 'https:' ? '443' : '8080')
389
- const spawnImpl = args.deps?.spawnImpl ?? spawn
390
- const spawnArgs: string[] = [
391
- '-m',
392
- args.modelPath,
393
- '--host',
394
- listenHost,
395
- '--port',
396
- port,
397
- '--alias',
398
- args.modelAlias,
399
- '--ctx-size',
400
- String(args.ctxSize ?? 32768),
401
- '--jinja',
402
- ]
403
- if (args.mmprojPath) spawnArgs.push('--mmproj', args.mmprojPath)
404
- let child: ReturnType<typeof spawn>
405
- try {
406
- child = spawnImpl(binaryPath, spawnArgs, {
407
- detached: true,
408
- stdio: ['ignore', 'pipe', 'pipe'],
409
- windowsHide: true,
410
- })
411
- } catch (err) {
412
- return startFailure('spawn-failed', { detail: (err as Error).message })
413
- }
414
-
415
- const capture = createStartupCapture(child)
416
- let childFailure: LlamaCppStartResult | null = null
417
- child.on('error', err => {
418
- childFailure = startFailure('spawn-failed', { detail: startupDetail(capture(), err.message) })
419
- })
420
- child.on('exit', (code, signal) => {
421
- childFailure ??= startFailure('runner-exited', {
422
- detail: startupDetail(capture(), `exit ${code ?? 'unknown'}${signal ? ` signal ${signal}` : ''}`),
423
- })
424
- })
425
- child.unref()
426
- if (typeof child.pid === 'number') {
427
- await writePidFile(child.pid).catch(() => {})
428
- }
429
-
430
- const ready = await waitForServedModel({
431
- host,
432
- modelAlias: args.modelAlias,
433
- timeoutMs: args.readinessTimeoutMs ?? 90_000,
434
- pollMs: args.pollMs ?? 500,
435
- childFailure: () => childFailure,
436
- })
437
- if (ready.ok) {
438
- void fetchLlamaCppContextSize(host)
439
- return { ok: true, alreadyRunning: false }
440
- }
441
- if (ready.code === 'readiness-timeout') {
442
- return startFailure('readiness-timeout', { detail: capture() })
443
- }
444
- return ready
445
- }
446
-
447
- async function waitForServedModel(args: {
448
- host: string
449
- modelAlias: string
450
- timeoutMs: number
451
- pollMs: number
452
- childFailure: () => LlamaCppStartResult | null
453
- }): Promise<{ ok: true } | Extract<LlamaCppStartResult, { ok: false }>> {
454
- const deadline = Date.now() + args.timeoutMs
455
- while (Date.now() < deadline) {
456
- const status = await servedModelStatus(args.host, args.modelAlias)
457
- if (status.state === 'ready') return { ok: true }
458
- if (status.state === 'different') return startFailure('different-model-running', { servedModels: status.models })
459
- const failure = args.childFailure()
460
- if (failure && !failure.ok) return failure
461
- await new Promise<void>(resolve => setTimeout(resolve, args.pollMs))
462
- }
463
-
464
- for (let i = 0; i < 3; i++) {
465
- const status = await servedModelStatus(args.host, args.modelAlias)
466
- if (status.state === 'ready') return { ok: true }
467
- if (status.state === 'different') return startFailure('different-model-running', { servedModels: status.models })
468
- const failure = args.childFailure()
469
- if (failure && !failure.ok) return failure
470
- await new Promise<void>(resolve => setTimeout(resolve, args.pollMs))
471
- }
472
-
473
- return startFailure('readiness-timeout')
474
- }
475
-
476
- function pidFilePath(): string {
477
- return path.join(getConfigDir(), 'llamacpp.pid')
478
- }
479
-
480
- async function writePidFile(pid: number): Promise<void> {
481
- await ensureConfigDir()
482
- await atomicWriteText(pidFilePath(), String(pid))
483
- }
484
-
485
- async function readPidFile(): Promise<number | null> {
486
- try {
487
- const raw = await fs.readFile(pidFilePath(), 'utf8')
488
- const pid = Number.parseInt(raw.trim(), 10)
489
- return Number.isInteger(pid) && pid > 0 ? pid : null
490
- } catch {
491
- return null
492
- }
493
- }
494
-
495
- async function clearPidFile(): Promise<void> {
496
- await fs.rm(pidFilePath(), { force: true }).catch(() => {})
497
- }
498
-
499
- export async function stopLlamaCppServer(args: {
500
- host?: string
501
- timeoutMs?: number
502
- pollMs?: number
503
- killImpl?: (pid: number, signal?: NodeJS.Signals | number) => void
504
- } = {}): Promise<
505
- | { ok: true; stopped: boolean; reason?: 'untracked-server'; servedModels?: string[] }
506
- | { ok: false; message: string }
507
- > {
508
- const pid = await readPidFile()
509
- if (!pid) {
510
- const host = args.host ?? DEFAULT_LLAMA_HOST
511
- const { up, models } = await fetchServedModels(host, 1500)
512
- if (up && models.length > 0) {
513
- return { ok: true, stopped: false, reason: 'untracked-server', servedModels: models }
514
- }
515
- return { ok: true, stopped: false }
516
- }
517
- const kill = args.killImpl ?? ((p, signal) => process.kill(p, signal))
518
- try {
519
- kill(pid, 'SIGTERM')
520
- } catch (err: unknown) {
521
- const code = (err as NodeJS.ErrnoException).code
522
- if (code === 'ESRCH') {
523
- await clearPidFile()
524
- return { ok: true, stopped: false }
525
- }
526
- return { ok: false, message: (err as Error).message }
527
- }
528
- const host = args.host ?? DEFAULT_LLAMA_HOST
529
- const deadline = Date.now() + (args.timeoutMs ?? 5000)
530
- const pollMs = args.pollMs ?? 250
531
- while (Date.now() < deadline) {
532
- const status = await servedModelStatus(host, '__nothing__')
533
- if (status.state === 'not-up' || status.models.length === 0) {
534
- await clearPidFile()
535
- return { ok: true, stopped: true }
536
- }
537
- await new Promise<void>(resolve => setTimeout(resolve, pollMs))
538
- }
539
- await clearPidFile()
540
- return { ok: true, stopped: true }
541
- }
542
-
543
- async function waitForHostDown(host: string, timeoutMs: number, pollMs: number): Promise<boolean> {
544
- const deadline = Date.now() + timeoutMs
545
- while (Date.now() < deadline) {
546
- const { up } = await fetchServedModels(host, 800)
547
- if (!up) return true
548
- await new Promise<void>(resolve => setTimeout(resolve, pollMs))
549
- }
550
- const { up } = await fetchServedModels(host, 800)
551
- return !up
552
- }
553
-
554
- async function servedModelStatus(host: string, modelAlias: string): Promise<
555
- | { state: 'not-up'; models: string[] }
556
- | { state: 'ready'; models: string[] }
557
- | { state: 'different'; models: string[] }
558
- > {
559
- const { up, models } = await fetchServedModels(host, 1500)
560
- if (!up) return { state: 'not-up', models }
561
- if (models.length === 0 || models.includes(modelAlias)) return { state: 'ready', models }
562
- return { state: 'different', models }
563
- }
564
-
565
- export type KillRogueResult = { killed: number; errors: string[] }
566
-
567
- export async function killRogueLlamaProcesses(host?: string): Promise<KillRogueResult> {
568
- const result: KillRogueResult = { killed: 0, errors: [] }
569
- try {
570
- await stopLlamaCppServer({ timeoutMs: 1500 })
571
- } catch (err: unknown) {
572
- result.errors.push(`tracked stop failed: ${(err as Error).message}`)
573
- }
574
- const platform = os.platform()
575
- const portOutcome = await killProcessOnPort(platform, host ?? DEFAULT_LLAMA_HOST)
576
- result.killed += portOutcome.killed
577
- if (portOutcome.error) result.errors.push(portOutcome.error)
578
- const targets = platform === 'win32'
579
- ? ['llama-server.exe', 'llama-cli.exe']
580
- : ['llama-server', 'llama-cli']
581
- for (const target of targets) {
582
- const outcome = await runKillCommand(platform, target)
583
- result.killed += outcome.killed
584
- if (outcome.error) result.errors.push(outcome.error)
585
- }
586
- await clearPidFile()
587
- return result
588
- }
589
-
590
- export async function killProcessOnPort(
591
- platform: NodeJS.Platform,
592
- host: string,
593
- ): Promise<{ killed: number; error?: string }> {
594
- const port = extractHostPort(host)
595
- if (!port) return { killed: 0, error: 'no port to scan' }
596
- const pids = await listListeningPids(platform, port)
597
- if (pids.length === 0) return { killed: 0 }
598
- let killed = 0
599
- const errors: string[] = []
600
- for (const pid of pids) {
601
- const outcome = await killByPid(platform, pid)
602
- if (outcome.killed) killed++
603
- if (outcome.error) errors.push(outcome.error)
604
- }
605
- return errors.length > 0 ? { killed, error: errors.join('; ') } : { killed }
606
- }
607
-
608
- function extractHostPort(host: string): number | null {
609
- try {
610
- const url = new URL(host)
611
- if (url.port) return Number.parseInt(url.port, 10)
612
- return url.protocol === 'https:' ? 443 : 80
613
- } catch {
614
- return null
615
- }
616
- }
617
-
618
- async function listListeningPids(platform: NodeJS.Platform, port: number): Promise<number[]> {
619
- if (platform === 'win32') {
620
- const result = await runCommand('netstat', ['-ano', '-p', 'tcp'], 4000)
621
- if (!result) return []
622
- return parseNetstatPids(result.stdout, port)
623
- }
624
- const result = await runCommand('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'], 4000)
625
- if (!result || result.code !== 0) return []
626
- return result.stdout.split(/\r?\n/).map(line => Number.parseInt(line.trim(), 10)).filter(n => Number.isInteger(n) && n > 0)
627
- }
628
-
629
- export function parseNetstatPids(output: string, port: number): number[] {
630
- const pids: number[] = []
631
- const seen = new Set<number>()
632
- const portSuffix = `:${port}`
633
- for (const raw of output.split(/\r?\n/)) {
634
- const line = raw.trim()
635
- if (!line || !line.toUpperCase().includes('LISTENING')) continue
636
- const cols = line.split(/\s+/)
637
- if (cols.length < 5) continue
638
- const local = cols[1] ?? ''
639
- if (!local.endsWith(portSuffix)) continue
640
- const pid = Number.parseInt(cols[cols.length - 1] ?? '', 10)
641
- if (!Number.isInteger(pid) || pid <= 0) continue
642
- if (pid === process.pid) continue
643
- if (seen.has(pid)) continue
644
- seen.add(pid)
645
- pids.push(pid)
646
- }
647
- return pids
648
- }
649
-
650
- async function killByPid(platform: NodeJS.Platform, pid: number): Promise<{ killed: boolean; error?: string }> {
651
- return new Promise(resolve => {
652
- const cmd = platform === 'win32' ? 'taskkill' : 'kill'
653
- const args = platform === 'win32' ? ['/F', '/T', '/PID', String(pid)] : ['-9', String(pid)]
654
- const child = spawn(cmd, args, { stdio: 'ignore' })
655
- child.on('error', err => resolve({ killed: false, error: `${cmd} ${pid}: ${err.message}` }))
656
- child.on('close', code => {
657
- if (code === 0) {
658
- resolve({ killed: true })
659
- return
660
- }
661
- resolve({ killed: false, error: `${cmd} ${pid} exited ${code}` })
662
- })
663
- })
664
- }
665
-
666
- async function runKillCommand(
667
- platform: NodeJS.Platform,
668
- target: string,
669
- ): Promise<{ killed: number; error?: string }> {
670
- return new Promise(resolve => {
671
- const cmd = platform === 'win32' ? 'taskkill' : 'pkill'
672
- const args = platform === 'win32'
673
- ? ['/F', '/T', '/IM', target]
674
- : ['-f', target]
675
- const child = spawn(cmd, args, { stdio: 'ignore' })
676
- child.on('error', err => resolve({ killed: 0, error: `${cmd} ${target}: ${err.message}` }))
677
- child.on('close', code => {
678
- if (code === 0) {
679
- resolve({ killed: 1 })
680
- return
681
- }
682
- if (platform === 'win32' && code === 128) {
683
- resolve({ killed: 0 })
684
- return
685
- }
686
- if (platform !== 'win32' && code === 1) {
687
- resolve({ killed: 0 })
688
- return
689
- }
690
- resolve({ killed: 0, error: `${cmd} ${target} exited ${code}` })
691
- })
692
- })
693
- }
694
-
695
- function startFailure(
696
- code: LlamaCppStartFailureCode,
697
- options: { detail?: string; servedModels?: string[] } = {},
698
- ): Extract<LlamaCppStartResult, { ok: false }> {
699
- const servedModels = options.servedModels?.filter(Boolean) ?? []
700
- return {
701
- ok: false,
702
- code,
703
- message: startFailureMessage(code, servedModels, options.detail),
704
- detail: options.detail || undefined,
705
- servedModels: servedModels.length > 0 ? servedModels : undefined,
706
- }
707
- }
708
-
709
- function startFailureMessage(code: LlamaCppStartFailureCode, servedModels: string[], detail?: string): string {
710
- switch (code) {
711
- case 'runner-not-installed':
712
- return 'local model runner is not installed yet'
713
- case 'model-file-missing':
714
- return detail ? `model file not found: ${detail}` : 'model file was not found'
715
- case 'different-model-running':
716
- return servedModels.length > 0
717
- ? `a different local model is already running (${servedModels.join(', ')}); stop it before switching models`
718
- : detail ?? 'a different local model is already running; stop it before switching models'
719
- case 'spawn-failed':
720
- return 'local runner could not be started'
721
- case 'runner-exited':
722
- return 'local runner closed before becoming ready'
723
- case 'readiness-timeout':
724
- return 'local runner is still loading or did not answer in time'
725
- }
726
- }
727
-
728
- function createStartupCapture(child: ReturnType<typeof spawn>): () => string {
729
- let output = ''
730
- const capture = (chunk: Buffer | string): void => {
731
- output = `${output}${chunk.toString()}`.slice(-4000)
732
- }
733
- child.stdout?.on('data', capture)
734
- child.stderr?.on('data', capture)
735
- return () => summarizeInstallOutput(output) ?? ''
736
- }
737
-
738
- function startupDetail(output: string, fallback: string): string {
739
- return output ? `${fallback}\n${output}` : fallback
740
- }
741
-
742
- function sourceBuildServerCandidates(buildDir: string, platform: NodeJS.Platform): string[] {
743
- const exe = platform === 'win32' ? 'llama-server.exe' : 'llama-server'
744
- return [
745
- path.join(buildDir, 'bin', exe),
746
- path.join(buildDir, 'bin', 'Release', exe),
747
- path.join(buildDir, 'bin', 'Debug', exe),
748
- path.join(buildDir, 'Release', exe),
749
- path.join(buildDir, 'Debug', exe),
750
- ]
751
- }
752
-
753
- async function firstAccessible(candidates: string[]): Promise<string | null> {
754
- for (const candidate of candidates) {
755
- try {
756
- await fs.access(candidate)
757
- return candidate
758
- } catch {
759
- continue
760
- }
761
- }
762
- return null
763
- }
764
-
765
- async function installLlamaCppFromSource(
766
- onProgress?: (progress: LlamaCppInstallProgress) => void,
767
- platform: NodeJS.Platform = process.platform,
768
- ): Promise<LlamaCppInstallResult> {
769
- const root = path.join(getConfigDir(), 'runners')
770
- const repoDir = path.join(root, 'llama.cpp')
771
- const buildDir = path.join(repoDir, 'build')
772
- const serverPath = path.join(buildDir, 'bin', platform === 'win32' ? 'llama-server.exe' : 'llama-server')
773
- await ensureConfigDir()
774
- await fs.mkdir(root, { recursive: true })
775
-
776
- onProgress?.({ phase: 'checking', label: 'checking build tools...', progress: 0.08 })
777
- const hasGit = await runCommand('git', ['--version'])
778
- if (!hasGit || hasGit.code !== 0) {
779
- return {
780
- ok: false,
781
- code: 'missing-tools',
782
- message: 'git is required to build the local runner.',
783
- recovery: ['runner-path', 'retry-install', 'back'],
784
- }
785
- }
786
- const hasCmake = await runCommand('cmake', ['--version'])
787
- if (!hasCmake || hasCmake.code !== 0) {
788
- return {
789
- ok: false,
790
- code: 'missing-tools',
791
- message: 'cmake is required to build the local runner.',
792
- recovery: ['runner-path', 'retry-install', 'back'],
793
- }
794
- }
795
-
796
- try {
797
- await fs.access(path.join(repoDir, '.git'))
798
- onProgress?.({ phase: 'building', label: 'updating local runner source...', progress: 0.22 })
799
- const update = await runInstallCommand(
800
- { label: 'update llama.cpp source', command: 'git', args: ['-C', repoDir, 'pull', '--ff-only'], timeoutMs: 5 * 60_000 },
801
- 5 * 60_000,
802
- )
803
- if (!update.ok) return buildFailure(update)
804
- } catch {
805
- onProgress?.({ phase: 'building', label: 'downloading local runner source...', progress: 0.22 })
806
- const clone = await runInstallCommand(
807
- { label: 'clone llama.cpp source', command: 'git', args: ['clone', '--depth', '1', 'https://github.com/ggml-org/llama.cpp.git', repoDir], timeoutMs: 10 * 60_000 },
808
- 10 * 60_000,
809
- )
810
- if (!clone.ok) return buildFailure(clone)
811
- }
812
-
813
- onProgress?.({ phase: 'building', label: 'configuring local runner...', progress: 0.48 })
814
- const configure = await runInstallCommand(
815
- { label: 'configure llama.cpp', command: 'cmake', args: ['-S', repoDir, '-B', buildDir, '-DCMAKE_BUILD_TYPE=Release'], timeoutMs: 5 * 60_000 },
816
- 5 * 60_000,
817
- )
818
- if (!configure.ok) return buildFailure(configure)
819
-
820
- onProgress?.({ phase: 'building', label: 'building local runner...', progress: 0.68 })
821
- const build = await runInstallCommand(
822
- {
823
- label: 'build llama-server',
824
- command: 'cmake',
825
- args: ['--build', buildDir, '--config', 'Release', '--target', 'llama-server', '-j', String(Math.max(1, os.cpus().length - 1))],
826
- timeoutMs: 30 * 60_000,
827
- },
828
- 30 * 60_000,
829
- )
830
- if (!build.ok) return buildFailure(build)
831
-
832
- const builtServerPath = await firstAccessible(sourceBuildServerCandidates(buildDir, platform))
833
- ?? (await discoverLlamaCppServerPaths(process.env, platform))[0]
834
- if (builtServerPath) {
835
- await setLlamaCppServerPath(builtServerPath)
836
- onProgress?.({ phase: 'finding', label: 'local runner ready...', progress: 1 })
837
- return { ok: true, serverPath: builtServerPath }
838
- }
839
-
840
- return {
841
- ok: false,
842
- code: 'server-not-found',
843
- message: 'built the local runner, but llama-server was not found.',
844
- detail: serverPath,
845
- recovery: ['runner-path', 'retry-install', 'back'],
846
- candidatePaths: sourceBuildServerCandidates(buildDir, platform),
847
- }
848
- }