ethagent 3.3.4 → 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.
- package/.claude-plugin/marketplace.json +11 -0
- package/.claude-plugin/plugin.json +35 -0
- package/LICENSE +1 -1
- package/README.md +64 -104
- package/commands/ethagent.md +40 -0
- package/package.json +16 -16
- package/src/app/keybindings/KeybindingProvider.tsx +1 -6
- package/src/app/keybindings/types.ts +1 -6
- package/src/cli/ResetConfirmView.tsx +54 -53
- package/src/cli/demo.ts +86 -0
- package/src/cli/hookIo.ts +45 -0
- package/src/cli/main.tsx +94 -123
- package/src/cli/memoryGuard.ts +49 -0
- package/src/cli/reset.ts +28 -70
- package/src/cli/sessionStart.ts +33 -0
- package/src/cli/status.ts +46 -0
- package/src/cli/sync.ts +167 -0
- package/src/cli/syncAdapters/claude-code.ts +86 -0
- package/src/cli/syncAdapters/codex.ts +66 -0
- package/src/cli/syncAdapters/index.ts +45 -0
- package/src/cli/syncAdapters/managedBlock.ts +175 -0
- package/src/cli/syncAdapters/shared.ts +63 -0
- package/src/identity/continuity/envelopeParse.ts +20 -1
- package/src/identity/continuity/publicSkills.ts +3 -1
- package/src/identity/continuity/skills/publicSkillsSync.ts +2 -1
- package/src/identity/continuity/skills/scaffold.ts +5 -2
- package/src/identity/continuity/snapshots.ts +12 -5
- package/src/identity/continuity/storage/defaults.ts +20 -19
- package/src/identity/continuity/storage/status.ts +1 -1
- package/src/identity/ens/ensLookup/constants.ts +1 -1
- package/src/identity/manager/IdentityManager.tsx +33 -0
- package/src/identity/{hub → manager}/OperationalRoutes.tsx +37 -18
- package/src/identity/{hub → manager}/Routes.tsx +48 -34
- package/src/identity/{hub → manager}/continuity/ContinuityDashboardScreen.tsx +9 -19
- package/src/identity/{hub → manager}/continuity/RebackupStorageScreen.tsx +3 -3
- package/src/identity/manager/continuity/RecoveryConfirmScreen.tsx +102 -0
- package/src/identity/{hub → manager}/continuity/SavePromptScreen.tsx +2 -3
- package/src/identity/{hub → manager}/continuity/completion.ts +1 -1
- package/src/identity/{hub → manager}/continuity/effects.ts +1 -1
- package/src/identity/{hub → manager}/continuity/skills/DeleteSkillConfirmScreen.tsx +2 -2
- package/src/identity/{hub → manager}/continuity/skills/NewSkillScreen.tsx +0 -5
- package/src/identity/{hub → manager}/continuity/skills/NewSkillVisibilityScreen.tsx +4 -4
- package/src/identity/{hub → manager}/continuity/skills/SkillActionsScreen.tsx +6 -22
- package/src/identity/{hub → manager}/continuity/skills/SkillsTreeScreen.tsx +5 -17
- package/src/identity/{hub → manager}/continuity/snapshot.ts +1 -1
- package/src/identity/{hub → manager}/continuity/vault.ts +1 -1
- package/src/identity/{hub → manager}/create/CreateFlow.tsx +59 -32
- package/src/identity/{hub → manager}/create/effects.ts +19 -10
- package/src/identity/manager/create/importScan.ts +122 -0
- package/src/identity/{hub → manager}/custody/CustodyEditFlow.tsx +17 -61
- package/src/identity/{hub → manager}/custody/actions.ts +1 -15
- package/src/identity/{hub → manager}/custody/routes.tsx +20 -40
- package/src/identity/{hub → manager}/custody/transactions.ts +1 -0
- package/src/identity/{hub → manager}/custody/types.ts +1 -2
- package/src/identity/{hub → manager}/custody/useCustodyEffects.ts +1 -1
- package/src/identity/{hub → manager}/ens/EnsEditAdvancedScreens.tsx +2 -2
- package/src/identity/{hub → manager}/ens/EnsEditMaintenanceScreens.tsx +12 -23
- package/src/identity/{hub → manager}/ens/EnsEditReviewScreens.tsx +18 -42
- package/src/identity/{hub → manager}/ens/EnsEditRunners.tsx +1 -1
- package/src/identity/{hub → manager}/ens/EnsEditShared.tsx +0 -2
- package/src/identity/{hub → manager}/ens/EnsEditSimpleScreens.tsx +10 -19
- package/src/identity/{hub → manager}/ens/EnsFlow.tsx +133 -41
- package/src/identity/{hub → manager}/ens/EnsOperatorWalletsScreen.tsx +14 -19
- package/src/identity/{hub → manager}/ens/editCopy.ts +1 -14
- package/src/identity/{hub → manager}/profile/EditProfileFlow.tsx +99 -66
- package/src/identity/{hub → manager}/profile/effects.ts +1 -3
- package/src/identity/{hub → manager}/profile/operatorSave.ts +1 -1
- package/src/identity/{hub → manager}/profile/state.ts +1 -1
- package/src/identity/{hub/identityHubReducer.ts → manager/reducer.ts} +25 -26
- package/src/identity/{hub → manager}/restore/RestoreFlow.tsx +16 -24
- package/src/identity/{hub → manager}/restore/apply.ts +1 -1
- package/src/identity/{hub → manager}/restore/auth.ts +1 -1
- package/src/identity/{hub → manager}/restore/discover.ts +1 -1
- package/src/identity/{hub → manager}/restore/fetch.ts +1 -1
- package/src/identity/{hub → manager}/restore/restoreAdmin.ts +1 -1
- package/src/identity/{hub → manager}/restore/useRestoreEffects.ts +2 -9
- package/src/identity/{hub → manager}/settings/StorageCredentialScreen.tsx +10 -25
- package/src/identity/{hub → manager}/shared/components/DetailsScreen.tsx +5 -7
- package/src/identity/{hub → manager}/shared/components/ErrorScreen.tsx +6 -10
- package/src/identity/{hub → manager}/shared/components/FlowTimeline.tsx +4 -3
- package/src/identity/{hub → manager}/shared/components/IdentitySummary.tsx +19 -59
- package/src/identity/manager/shared/components/LazyMenu.tsx +147 -0
- package/src/identity/manager/shared/components/MenuScreen.tsx +220 -0
- package/src/identity/manager/shared/components/OperationCompleteScreen.tsx +28 -0
- package/src/identity/{hub → manager}/shared/components/UnlinkedIdentityScreen.tsx +9 -10
- package/src/identity/{hub → manager}/shared/components/WalletApprovalScreen.tsx +1 -2
- package/src/identity/manager/shared/components/Wordmark.tsx +54 -0
- package/src/identity/{hub → manager}/shared/components/menuFlagsFromReconciliation.ts +39 -15
- package/src/identity/{hub → manager}/shared/effects/profilePrep.ts +1 -1
- package/src/identity/manager/shared/effects/types.ts +30 -0
- package/src/identity/{hub → manager}/shared/model/copy.ts +0 -4
- package/src/identity/{hub → manager}/shared/model/errors.ts +32 -3
- package/src/identity/{hub → manager}/shared/model/network.ts +2 -2
- package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/hook.ts +5 -0
- package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/run.ts +1 -1
- package/src/identity/{hub/shared/reconciliation/useAgentReconciliation.ts → manager/shared/reconciliation/index.ts} +6 -0
- package/src/identity/{hub → manager}/shared/utils.ts +6 -10
- package/src/identity/{hub → manager}/transfer/TokenTransferFlow.tsx +3 -3
- package/src/identity/{hub → manager}/transfer/TokenTransferScreens.tsx +4 -10
- package/src/identity/{hub → manager}/transfer/effects.ts +1 -1
- package/src/identity/{hub → manager}/types.ts +5 -6
- package/src/identity/{hub/useIdentityHubContinuity.ts → manager/useContinuity.ts} +59 -27
- package/src/identity/{hub/useIdentityHubController.ts → manager/useController.ts} +38 -35
- package/src/identity/{hub/useIdentityHubSideEffects.ts → manager/useSideEffects.ts} +40 -4
- package/src/identity/registry/erc8004/discovery.ts +3 -17
- package/src/identity/registry/erc8004/utils.ts +1 -1
- package/src/identity/storage/ipfs.ts +21 -1
- package/src/identity/wallet/browserWallet/html.ts +10 -2
- package/src/identity/wallet/browserWallet/http.ts +18 -0
- package/src/identity/wallet/browserWallet/requestServer.ts +5 -1
- package/src/identity/wallet/browserWallet/requests.ts +10 -28
- package/src/identity/wallet/browserWallet/session.ts +26 -33
- package/src/identity/wallet/browserWallet/validation.ts +14 -0
- package/src/identity/wallet/browserWallet/walletPageSource.ts +22 -40
- package/src/identity/wallet/page/boot.ts +43 -0
- package/src/identity/wallet/page/config.ts +59 -0
- package/src/identity/wallet/page/constants.ts +12 -0
- package/src/identity/wallet/page/copy.ts +47 -68
- package/src/identity/wallet/page/css.ts +638 -0
- package/src/identity/wallet/page/{errorView.ts → errors.ts} +5 -14
- package/src/identity/wallet/page/{controller.ts → flow.ts} +4 -71
- package/src/identity/wallet/page/markup.ts +44 -34
- package/src/identity/wallet/page/{walletProvider.ts → provider.ts} +0 -3
- package/src/identity/wallet/page/resize.ts +95 -0
- package/src/identity/wallet/page/state.ts +135 -8
- package/src/identity/wallet/page/timeline.ts +161 -0
- package/src/identity/wallet/page/view.ts +22 -302
- package/src/storage/config.ts +30 -80
- package/src/storage/reset.ts +31 -0
- package/src/storage/secrets.ts +1 -16
- package/src/ui/Select.tsx +27 -5
- package/src/ui/Spinner.tsx +16 -15
- package/src/ui/Surface.tsx +21 -17
- package/src/ui/TextArea.tsx +173 -0
- package/src/ui/TextInput.tsx +31 -133
- package/src/ui/theme.ts +22 -13
- package/src/utils/clipboard.ts +0 -140
- package/src/app/FirstRun.tsx +0 -577
- package/src/app/FirstRunTimeline.tsx +0 -51
- package/src/app/firstRunConfig.ts +0 -26
- package/src/app/hooks/useCancelRequest.ts +0 -22
- package/src/app/hooks/useDoublePress.ts +0 -46
- package/src/app/hooks/useExitOnCtrlC.ts +0 -36
- package/src/auth/openaiOAuth/credentials.ts +0 -47
- package/src/auth/openaiOAuth/crypto.ts +0 -23
- package/src/auth/openaiOAuth/index.ts +0 -238
- package/src/auth/openaiOAuth/landingPage.ts +0 -116
- package/src/auth/openaiOAuth/listener.ts +0 -151
- package/src/auth/openaiOAuth/refresh.ts +0 -70
- package/src/auth/openaiOAuth/shared.ts +0 -115
- package/src/chat/ChatBottomPane.tsx +0 -296
- package/src/chat/ChatScreen.tsx +0 -1685
- package/src/chat/ConversationStack.tsx +0 -56
- package/src/chat/MessageList.tsx +0 -638
- package/src/chat/SessionStatus.tsx +0 -53
- package/src/chat/chatEnvironment.ts +0 -16
- package/src/chat/chatScreenUtils.ts +0 -194
- package/src/chat/chatSessionState.ts +0 -146
- package/src/chat/chatTurnContext.ts +0 -50
- package/src/chat/chatTurnOrchestrator.ts +0 -603
- package/src/chat/chatTurnRows.ts +0 -64
- package/src/chat/commands.ts +0 -494
- package/src/chat/continuityEditReview.ts +0 -42
- package/src/chat/display/DiffView.tsx +0 -193
- package/src/chat/display/SyntaxText.tsx +0 -192
- package/src/chat/display/toolCallDisplay.ts +0 -103
- package/src/chat/display/toolResultDisplay.ts +0 -19
- package/src/chat/input/ChatInput.tsx +0 -625
- package/src/chat/input/chatInputHelpers.ts +0 -62
- package/src/chat/input/chatInputState.ts +0 -247
- package/src/chat/input/chatPaste.ts +0 -49
- package/src/chat/input/imageRefs.ts +0 -30
- package/src/chat/input/inputRendering.tsx +0 -93
- package/src/chat/input/textCursor.ts +0 -212
- package/src/chat/messageMarkdown.ts +0 -220
- package/src/chat/messageRows.ts +0 -43
- package/src/chat/planImplementation.ts +0 -62
- package/src/chat/slashCommandHandlers.ts +0 -122
- package/src/chat/slashCommandViews.ts +0 -120
- package/src/chat/transcript/TranscriptView.tsx +0 -184
- package/src/chat/transcript/transcriptViewport.ts +0 -295
- package/src/chat/views/ContextLimitView.tsx +0 -95
- package/src/chat/views/ContinuityEditReviewView.tsx +0 -50
- package/src/chat/views/CopyPicker.tsx +0 -50
- package/src/chat/views/PermissionPrompt.tsx +0 -156
- package/src/chat/views/PermissionsView.tsx +0 -165
- package/src/chat/views/PlanApprovalView.tsx +0 -91
- package/src/chat/views/ResumeView.tsx +0 -273
- package/src/chat/views/RewindView.tsx +0 -412
- package/src/cli/preview.tsx +0 -14
- package/src/cli/updateNotice.ts +0 -54
- package/src/identity/continuity/privateEdit/apply.ts +0 -170
- package/src/identity/continuity/privateEdit/diff.ts +0 -6
- package/src/identity/continuity/privateEdit/files.ts +0 -23
- package/src/identity/continuity/privateEdit/types.ts +0 -28
- package/src/identity/continuity/privateEdit.ts +0 -46
- package/src/identity/hub/IdentityHub.tsx +0 -14
- package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +0 -104
- package/src/identity/hub/ens/effects.ts +0 -218
- package/src/identity/hub/shared/components/MenuScreen.tsx +0 -241
- package/src/identity/hub/shared/effects/types.ts +0 -53
- package/src/identity/hub/shared/reconciliation/index.ts +0 -14
- package/src/identity/wallet/page/grainient.ts +0 -278
- package/src/identity/wallet/page/html.ts +0 -28
- package/src/identity/wallet/page/styles/base.ts +0 -260
- package/src/identity/wallet/page/styles/components.ts +0 -262
- package/src/identity/wallet/page/styles/index.ts +0 -5
- package/src/identity/wallet/page/styles/responsive.ts +0 -247
- package/src/identity/wallet/page.tsx +0 -38
- package/src/mcp/approvals.ts +0 -113
- package/src/mcp/config.ts +0 -235
- package/src/mcp/manager.ts +0 -482
- package/src/mcp/managerHelpers.ts +0 -70
- package/src/mcp/names.ts +0 -19
- package/src/mcp/output.ts +0 -96
- package/src/models/ModelPicker.tsx +0 -1009
- package/src/models/catalog.ts +0 -327
- package/src/models/huggingface.ts +0 -712
- package/src/models/huggingfaceStorage.ts +0 -136
- package/src/models/llamacpp.ts +0 -848
- package/src/models/llamacppCommands.ts +0 -44
- package/src/models/llamacppConfig.ts +0 -34
- package/src/models/llamacppDiscovery.ts +0 -176
- package/src/models/llamacppOutput.ts +0 -65
- package/src/models/llamacppPreflight.ts +0 -158
- package/src/models/modelDisplay.ts +0 -180
- package/src/models/modelPickerCatalogFlow.ts +0 -56
- package/src/models/modelPickerCredentials.ts +0 -166
- package/src/models/modelPickerData.ts +0 -41
- package/src/models/modelPickerDisplay.tsx +0 -132
- package/src/models/modelPickerHfFlow.ts +0 -192
- package/src/models/modelPickerLocalRunnerFlow.ts +0 -115
- package/src/models/modelPickerOptions.ts +0 -457
- package/src/models/modelPickerTypes.ts +0 -69
- package/src/models/modelPickerUninstallFlow.ts +0 -48
- package/src/models/modelPickerViewHelpers.ts +0 -174
- package/src/models/modelRecommendation.ts +0 -139
- package/src/models/providerDisplay.ts +0 -16
- package/src/models/runtimeDetection.ts +0 -81
- package/src/models/uncensoredCatalog.ts +0 -86
- package/src/providers/anthropic.ts +0 -290
- package/src/providers/contracts.ts +0 -71
- package/src/providers/errors.ts +0 -80
- package/src/providers/gemini.ts +0 -391
- package/src/providers/openai-chat.ts +0 -474
- package/src/providers/openai-responses-format.ts +0 -177
- package/src/providers/openai-responses.ts +0 -306
- package/src/providers/openaiChatWire.ts +0 -124
- package/src/providers/registry.ts +0 -120
- package/src/providers/retry.ts +0 -58
- package/src/providers/sse.ts +0 -93
- package/src/runtime/compaction.ts +0 -395
- package/src/runtime/cwd.ts +0 -43
- package/src/runtime/providerTurn.ts +0 -38
- package/src/runtime/sessionMode.ts +0 -55
- package/src/runtime/systemPrompt.ts +0 -213
- package/src/runtime/textToolParser.ts +0 -161
- package/src/runtime/toolClaimGuards.ts +0 -143
- package/src/runtime/toolExecution.ts +0 -304
- package/src/runtime/toolIntent.ts +0 -143
- package/src/runtime/turn.ts +0 -369
- package/src/runtime/turnNudges.ts +0 -223
- package/src/runtime/turnTypes.ts +0 -86
- package/src/storage/factoryReset.ts +0 -127
- package/src/storage/history.ts +0 -58
- package/src/storage/permissions.ts +0 -76
- package/src/storage/rewind.ts +0 -266
- package/src/storage/sessionExport.ts +0 -49
- package/src/storage/sessions.ts +0 -495
- package/src/tools/bashSafety.ts +0 -186
- package/src/tools/bashTool.ts +0 -140
- package/src/tools/changeDirectoryTool.ts +0 -213
- package/src/tools/contracts.ts +0 -192
- package/src/tools/deleteFileTool.ts +0 -116
- package/src/tools/editTool.ts +0 -165
- package/src/tools/editUtils.ts +0 -170
- package/src/tools/fileDiff.ts +0 -261
- package/src/tools/listDirectoryTool.ts +0 -55
- package/src/tools/listSkillFilesTool.ts +0 -77
- package/src/tools/listSkillsTool.ts +0 -68
- package/src/tools/mcpResourceTools.ts +0 -95
- package/src/tools/permissionRules.ts +0 -85
- package/src/tools/privateContinuityEditTool.ts +0 -187
- package/src/tools/privateContinuityReadTool.ts +0 -106
- package/src/tools/readSkillTool.ts +0 -107
- package/src/tools/readTool.ts +0 -85
- package/src/tools/registry.ts +0 -103
- package/src/tools/writeFileTool.ts +0 -167
- package/src/ui/BrandSplash.tsx +0 -133
- package/src/ui/terminalTitle.ts +0 -30
- package/src/utils/images.ts +0 -140
- package/src/utils/markdownSegments.ts +0 -51
- package/src/utils/messages.ts +0 -37
- package/src/utils/withRetry.ts +0 -324
- /package/src/identity/{hub → manager}/continuity/state.ts +0 -0
- /package/src/identity/{hub → manager}/custody/helpers.ts +0 -0
- /package/src/identity/{hub → manager}/custody/preflight.ts +0 -0
- /package/src/identity/{hub → manager}/custody/state.ts +0 -0
- /package/src/identity/{hub → manager}/custody/useCustodyFlow.tsx +0 -0
- /package/src/identity/{hub → manager}/ens/EnsEditFlow.tsx +0 -0
- /package/src/identity/{hub → manager}/ens/advancedEnsValidation.ts +0 -0
- /package/src/identity/{hub → manager}/ens/state.ts +0 -0
- /package/src/identity/{hub → manager}/ens/transactions.ts +0 -0
- /package/src/identity/{hub → manager}/ens/types.ts +0 -0
- /package/src/identity/{hub → manager}/profile/identity.ts +0 -0
- /package/src/identity/{hub → manager}/restore/envelopes.ts +0 -0
- /package/src/identity/{hub → manager}/restore/helpers.ts +0 -0
- /package/src/identity/{hub → manager}/restore/recovery.ts +0 -0
- /package/src/identity/{hub → manager}/restore/resolve.ts +0 -0
- /package/src/identity/{hub → manager}/shared/components/BusyScreen.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/components/NetworkScreen.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/components/PinataJwtInput.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/effects/receipts.ts +0 -0
- /package/src/identity/{hub → manager}/shared/effects/sync.ts +0 -0
- /package/src/identity/{hub → manager}/shared/model/format.ts +0 -0
- /package/src/identity/{hub → manager}/shared/operatorWallets.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/ownership.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/types.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/walletSetup.ts +0 -0
- /package/src/identity/{hub → manager}/shared/txGuard.ts +0 -0
- /package/src/identity/{hub → manager}/transfer/progress.ts +0 -0
- /package/src/identity/{hub → manager}/transfer/state.ts +0 -0
package/src/models/llamacpp.ts
DELETED
|
@@ -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
|
-
}
|