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.
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 -260
  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,712 +0,0 @@
1
- import fs from 'node:fs/promises'
2
- import path from 'node:path'
3
- import { createHash } from 'node:crypto'
4
- import {
5
- findLocalHfModel,
6
- getLocalHfCacheDir,
7
- getLocalHfModelsPath,
8
- loadLocalHfModels,
9
- localPathFor,
10
- saveLocalHfModels,
11
- uninstallLocalHfModel,
12
- upsertLocalHfModel,
13
- } from './huggingfaceStorage.js'
14
-
15
- export {
16
- findLocalHfModel,
17
- getLocalHfCacheDir,
18
- getLocalHfModelsPath,
19
- loadLocalHfModels,
20
- saveLocalHfModels,
21
- uninstallLocalHfModel,
22
- upsertLocalHfModel,
23
- } from './huggingfaceStorage.js'
24
-
25
- export type HfFileFormat = 'gguf' | 'safetensors' | 'pickle/bin' | 'unknown'
26
- export type HfRuntime = 'llama.cpp runnable' | 'download-only' | 'unsupported'
27
- export type HfTask = 'chat/instruct' | 'base' | 'code' | 'embedding' | 'vision' | 'unknown'
28
- export type HfSizeClass = 'tiny' | 'small' | 'medium' | 'large'
29
- export type HfRisk = 'low' | 'medium' | 'high'
30
- export type HfCredibility = 'established' | 'normal' | 'low-signal'
31
- export type LocalHfStatus = 'ready' | 'incomplete'
32
-
33
- export type HuggingFaceRef = {
34
- repoId: string
35
- revision?: string
36
- filename?: string
37
- }
38
-
39
- export type HuggingFaceSibling = {
40
- filename: string
41
- sizeBytes?: number
42
- }
43
-
44
- export type HuggingFaceRepoInfo = {
45
- repoId: string
46
- author?: string
47
- sha?: string
48
- license?: string
49
- downloads?: number
50
- likes?: number
51
- lastModified?: string
52
- tags: string[]
53
- siblings: HuggingFaceSibling[]
54
- }
55
-
56
- export type HuggingFaceModelSearchItem = {
57
- repoId: string
58
- downloads?: number
59
- likes?: number
60
- lastModified?: string
61
- tags: string[]
62
- }
63
-
64
- export type HfSafetyReview = {
65
- risk: HfRisk
66
- credibility: HfCredibility
67
- format: HfFileFormat
68
- runtime: HfRuntime
69
- task: HfTask
70
- sizeClass: HfSizeClass
71
- quantization?: string
72
- reasons: string[]
73
- }
74
-
75
- export type HfMmprojCandidate = {
76
- filename: string
77
- sizeBytes: number
78
- localPath: string
79
- }
80
-
81
- export type HfDownloadPlan = {
82
- repo: HuggingFaceRepoInfo
83
- repoId: string
84
- requestedRevision: string
85
- resolvedRevision: string
86
- filename: string
87
- sizeBytes: number
88
- localPath: string
89
- displayName: string
90
- review: HfSafetyReview
91
- mmprojCandidate?: HfMmprojCandidate
92
- includeMmproj?: boolean
93
- }
94
-
95
- export type LocalHfModel = {
96
- id: string
97
- provider: 'llamacpp'
98
- repoId: string
99
- requestedRevision: string
100
- resolvedRevision: string
101
- filename: string
102
- displayName: string
103
- localPath: string
104
- sizeBytes: number
105
- format: HfFileFormat
106
- runtime: HfRuntime
107
- task: HfTask
108
- sizeClass: HfSizeClass
109
- quantization?: string
110
- risk: HfRisk
111
- credibility: HfCredibility
112
- license?: string
113
- downloads?: number
114
- likes?: number
115
- reviewedAt: string
116
- installedAt: string
117
- status: LocalHfStatus
118
- sha256?: string
119
- mmprojPath?: string
120
- mmprojAvailable?: boolean
121
- mmprojSizeBytes?: number
122
- }
123
-
124
- export type HfDownloadProgress = {
125
- status: string
126
- completed?: number
127
- total?: number
128
- }
129
-
130
- type FetchImpl = typeof fetch
131
- type ModelInfoResponse = {
132
- id?: unknown
133
- author?: unknown
134
- sha?: unknown
135
- downloads?: unknown
136
- likes?: unknown
137
- lastModified?: unknown
138
- tags?: unknown
139
- cardData?: unknown
140
- siblings?: unknown
141
- }
142
-
143
- type ModelSearchResponseItem = {
144
- id?: unknown
145
- modelId?: unknown
146
- downloads?: unknown
147
- likes?: unknown
148
- lastModified?: unknown
149
- tags?: unknown
150
- }
151
-
152
- const HF_BASE_URL = 'https://huggingface.co'
153
- const DEFAULT_REVISION = 'main'
154
- const COMMIT_RE = /^[a-f0-9]{40}$/i
155
- const DOWNLOAD_PROGRESS_MIN_MS = 100
156
- const DOWNLOAD_PROGRESS_MIN_BYTES = 16 * 1024 * 1024
157
-
158
- export function parseHuggingFaceRef(input: string): HuggingFaceRef {
159
- const trimmed = input.trim()
160
- if (!trimmed) throw new Error('Hugging Face model link or repo id is required')
161
-
162
- if (/^https?:\/\//i.test(trimmed)) {
163
- const url = new URL(trimmed)
164
- const host = url.hostname.toLowerCase()
165
- if (host !== 'huggingface.co' && host !== 'www.huggingface.co') {
166
- throw new Error('Expected a huggingface.co model link')
167
- }
168
- const parts = url.pathname.split('/').filter(Boolean)
169
- if (parts.length < 2) throw new Error('Expected a Hugging Face repo link')
170
- const repoId = `${decodeURIComponent(parts[0]!)}/${decodeURIComponent(parts[1]!)}`
171
- const mode = parts[2]
172
- if (mode === 'blob' || mode === 'resolve' || mode === 'tree') {
173
- const revision = parts[3] ? decodeURIComponent(parts[3]) : undefined
174
- const filename = parts.length > 4
175
- ? parts.slice(4).map(part => decodeURIComponent(part)).join('/')
176
- : undefined
177
- return { repoId, revision, filename }
178
- }
179
- return { repoId }
180
- }
181
-
182
- const withoutPrefix = trimmed.replace(/^hf:\/\//i, '')
183
- const parts = withoutPrefix.split('/').filter(Boolean)
184
- if (parts.length < 2) throw new Error('Expected repo id like org/model or a huggingface.co link')
185
- const repoId = `${parts[0]!}/${parts[1]!}`
186
- let fileParts = parts.slice(2)
187
- const mode = fileParts[0]
188
- if ((mode === 'blob' || mode === 'resolve' || mode === 'tree') && fileParts.length >= 2) {
189
- fileParts = fileParts.slice(2)
190
- }
191
- const filename = fileParts.length > 0 ? fileParts.join('/') : undefined
192
- return { repoId, filename }
193
- }
194
-
195
- export async function fetchHuggingFaceRepoInfo(
196
- ref: HuggingFaceRef,
197
- fetchImpl: FetchImpl = fetch,
198
- ): Promise<HuggingFaceRepoInfo> {
199
- const url = new URL(`${HF_BASE_URL}/api/models/${encodeRepoPath(ref.repoId)}`)
200
- url.searchParams.set('blobs', 'true')
201
- if (ref.revision) url.searchParams.set('revision', ref.revision)
202
- const response = await fetchImpl(url, { headers: { Accept: 'application/json' } })
203
- if (!response.ok) {
204
- if (response.status === 401 || response.status === 403) {
205
- throw new Error('Repo is gated or private')
206
- }
207
- if (response.status === 404) throw new Error('Hugging Face repo not found')
208
- throw new Error(`Hugging Face API HTTP ${response.status}`)
209
- }
210
- const data = await response.json() as ModelInfoResponse
211
- const tags = Array.isArray(data.tags)
212
- ? data.tags.filter((tag): tag is string => typeof tag === 'string')
213
- : []
214
- const siblings = Array.isArray(data.siblings)
215
- ? data.siblings.flatMap(sibling => parseSibling(sibling))
216
- : []
217
- return {
218
- repoId: typeof data.id === 'string' ? data.id : ref.repoId,
219
- author: typeof data.author === 'string' ? data.author : undefined,
220
- sha: typeof data.sha === 'string' ? data.sha : undefined,
221
- license: licenseFrom(data.cardData, tags),
222
- downloads: typeof data.downloads === 'number' ? data.downloads : undefined,
223
- likes: typeof data.likes === 'number' ? data.likes : undefined,
224
- lastModified: typeof data.lastModified === 'string' ? data.lastModified : undefined,
225
- tags,
226
- siblings,
227
- }
228
- }
229
-
230
- export async function searchHuggingFaceModels(
231
- query: string,
232
- options: { limit?: number; filter?: string } = {},
233
- fetchImpl: FetchImpl = fetch,
234
- ): Promise<HuggingFaceModelSearchItem[]> {
235
- const url = new URL(`${HF_BASE_URL}/api/models`)
236
- url.searchParams.set('search', query)
237
- url.searchParams.set('sort', 'downloads')
238
- url.searchParams.set('direction', '-1')
239
- url.searchParams.set('limit', String(options.limit ?? 20))
240
- if (options.filter) url.searchParams.set('filter', options.filter)
241
- const response = await fetchImpl(url, { headers: { Accept: 'application/json' } })
242
- if (!response.ok) throw new Error(`Hugging Face catalog HTTP ${response.status}`)
243
- const data = await response.json() as unknown
244
- if (!Array.isArray(data)) return []
245
- return data.flatMap(parseSearchItem)
246
- }
247
-
248
- export function ggufFiles(repo: HuggingFaceRepoInfo): HuggingFaceSibling[] {
249
- return repo.siblings
250
- .filter(file => file.filename.toLowerCase().endsWith('.gguf'))
251
- .sort((a, b) => a.filename.localeCompare(b.filename))
252
- }
253
-
254
- export function isMmprojFilename(filename: string): boolean {
255
- return filename.toLowerCase().startsWith('mmproj-') && filename.toLowerCase().endsWith('.gguf')
256
- }
257
-
258
- export function findMmprojSibling(repo: HuggingFaceRepoInfo): HuggingFaceSibling | undefined {
259
- return repo.siblings.find(file => isMmprojFilename(file.filename))
260
- }
261
-
262
- export async function createHfDownloadPlan(
263
- input: string,
264
- filename?: string,
265
- deps: { fetchImpl?: FetchImpl; now?: () => Date } = {},
266
- ): Promise<HfDownloadPlan> {
267
- const ref = parseHuggingFaceRef(input)
268
- const selectedFilename = filename?.trim() || ref.filename
269
- const repo = await fetchHuggingFaceRepoInfo(ref, deps.fetchImpl)
270
- const files = ggufFiles(repo)
271
- if (files.length === 0) {
272
- throw new Error('No compatible local model files found for this link')
273
- }
274
- const selected = selectedFilename
275
- ? files.find(file => file.filename === selectedFilename)
276
- : files[0]
277
- if (!selected) {
278
- throw new Error(`compatible file not found: ${selectedFilename}`)
279
- }
280
-
281
- const requestedRevision = ref.revision ?? DEFAULT_REVISION
282
- const resolvedRevision = repo.sha || requestedRevision
283
- const sizeBytes = selected.sizeBytes ?? 0
284
- const review = reviewHfModel({
285
- repo,
286
- filename: selected.filename,
287
- sizeBytes,
288
- requestedRevision,
289
- resolvedRevision,
290
- })
291
- const mmprojSibling = findMmprojSibling(repo)
292
- const mmprojCandidate: HfMmprojCandidate | undefined = mmprojSibling
293
- ? {
294
- filename: mmprojSibling.filename,
295
- sizeBytes: mmprojSibling.sizeBytes ?? 0,
296
- localPath: localPathFor(repo.repoId, resolvedRevision, mmprojSibling.filename),
297
- }
298
- : undefined
299
- return {
300
- repo,
301
- repoId: repo.repoId,
302
- requestedRevision,
303
- resolvedRevision,
304
- filename: selected.filename,
305
- sizeBytes,
306
- localPath: localPathFor(repo.repoId, resolvedRevision, selected.filename),
307
- displayName: displayNameFor(repo.repoId, selected.filename),
308
- review,
309
- mmprojCandidate,
310
- }
311
- }
312
-
313
- export function reviewHfModel(args: {
314
- repo: HuggingFaceRepoInfo
315
- filename: string
316
- sizeBytes: number
317
- requestedRevision: string
318
- resolvedRevision: string
319
- }): HfSafetyReview {
320
- const format = fileFormat(args.filename)
321
- const runtime: HfRuntime =
322
- format === 'gguf' ? 'llama.cpp runnable'
323
- : format === 'safetensors' ? 'download-only'
324
- : 'unsupported'
325
- const quantization = quantizationFromFilename(args.filename)
326
- const task = taskFor(args.repo, args.filename)
327
- const sizeClass = sizeClassFor(args.sizeBytes)
328
- const credibility = credibilityFor(args.repo)
329
- const pinned = COMMIT_RE.test(args.requestedRevision) || COMMIT_RE.test(args.resolvedRevision)
330
- const repoHasRiskyFiles = args.repo.siblings.some(file => fileFormat(file.filename) === 'pickle/bin')
331
- const reasons: string[] = []
332
-
333
- if (format !== 'gguf') reasons.push('selected file is not compatible with local chat')
334
- if (!pinned) reasons.push('revision is mutable')
335
- if (!args.repo.license) reasons.push('license is missing or unknown')
336
- if (credibility === 'low-signal') reasons.push('repo has limited public usage signals')
337
- if (repoHasRiskyFiles) reasons.push('repo also contains pickle/bin model files')
338
-
339
- const risk: HfRisk =
340
- format !== 'gguf'
341
- ? 'high'
342
- : repoHasRiskyFiles
343
- ? 'medium'
344
- : pinned && args.repo.license && credibility !== 'low-signal'
345
- ? 'low'
346
- : 'medium'
347
-
348
- if (reasons.length === 0) reasons.push('compatible local model file with usable repo metadata')
349
-
350
- return {
351
- risk,
352
- credibility,
353
- format,
354
- runtime,
355
- task,
356
- sizeClass,
357
- quantization,
358
- reasons,
359
- }
360
- }
361
-
362
- export async function* downloadHfModel(
363
- plan: HfDownloadPlan,
364
- signal?: AbortSignal,
365
- fetchImpl: FetchImpl = fetch,
366
- ): AsyncIterable<HfDownloadProgress> {
367
- if (plan.review.runtime !== 'llama.cpp runnable') {
368
- throw new Error('Selected file is not compatible with local chat')
369
- }
370
-
371
- await fs.mkdir(path.dirname(plan.localPath), { recursive: true })
372
- const partialPath = `${plan.localPath}.partial`
373
- const response = await fetchImpl(resolveUrl(plan.repoId, plan.resolvedRevision, plan.filename), { signal })
374
- if (!response.ok || !response.body) {
375
- throw new Error(response.ok ? 'empty download body' : `download HTTP ${response.status}`)
376
- }
377
-
378
- const total = Number.parseInt(response.headers.get('content-length') ?? '', 10)
379
- const hash = createHash('sha256')
380
- const handle = await fs.open(partialPath, 'w')
381
- let completed = 0
382
- let complete = false
383
- let lastProgressAt = Date.now()
384
- let lastProgressBytes = 0
385
- yield { status: 'starting', completed, total: Number.isFinite(total) ? total : undefined }
386
- try {
387
- const reader = response.body.getReader()
388
- while (true) {
389
- const { done, value } = await reader.read()
390
- if (done) break
391
- if (signal?.aborted) throw new Error('Cancelled')
392
- const buffer = Buffer.from(value)
393
- hash.update(buffer)
394
- await handle.write(buffer)
395
- completed += buffer.byteLength
396
- const now = Date.now()
397
- if (shouldReportDownloadProgress(completed, lastProgressBytes, now, lastProgressAt)) {
398
- lastProgressAt = now
399
- lastProgressBytes = completed
400
- yield { status: 'downloading', completed, total: Number.isFinite(total) ? total : undefined }
401
- }
402
- }
403
- complete = true
404
- } finally {
405
- await handle.close()
406
- if (!complete) {
407
- await fs.unlink(partialPath).catch(() => {})
408
- }
409
- }
410
-
411
- await fs.rename(partialPath, plan.localPath)
412
-
413
- let mmprojPath: string | undefined
414
- if (plan.includeMmproj && plan.mmprojCandidate) {
415
- yield* downloadMmprojFile(plan.repoId, plan.resolvedRevision, plan.mmprojCandidate, signal, fetchImpl)
416
- mmprojPath = plan.mmprojCandidate.localPath
417
- }
418
-
419
- await upsertLocalHfModel(modelFromPlan(plan, hash.digest('hex'), 'ready', mmprojPath))
420
- yield { status: 'success', completed, total: Number.isFinite(total) ? total : completed }
421
- }
422
-
423
- async function* downloadMmprojFile(
424
- repoId: string,
425
- resolvedRevision: string,
426
- candidate: HfMmprojCandidate,
427
- signal: AbortSignal | undefined,
428
- fetchImpl: FetchImpl,
429
- ): AsyncIterable<HfDownloadProgress> {
430
- await fs.mkdir(path.dirname(candidate.localPath), { recursive: true })
431
- const partialPath = `${candidate.localPath}.partial`
432
- const response = await fetchImpl(resolveUrl(repoId, resolvedRevision, candidate.filename), { signal })
433
- if (!response.ok || !response.body) {
434
- throw new Error(response.ok ? 'empty projector download body' : `projector download HTTP ${response.status}`)
435
- }
436
-
437
- const total = Number.parseInt(response.headers.get('content-length') ?? '', 10)
438
- const handle = await fs.open(partialPath, 'w')
439
- let completed = 0
440
- let complete = false
441
- let lastProgressAt = Date.now()
442
- let lastProgressBytes = 0
443
- yield { status: 'downloading-mmproj', completed, total: Number.isFinite(total) ? total : undefined }
444
- try {
445
- const reader = response.body.getReader()
446
- while (true) {
447
- const { done, value } = await reader.read()
448
- if (done) break
449
- if (signal?.aborted) throw new Error('Cancelled')
450
- const buffer = Buffer.from(value)
451
- await handle.write(buffer)
452
- completed += buffer.byteLength
453
- const now = Date.now()
454
- if (shouldReportDownloadProgress(completed, lastProgressBytes, now, lastProgressAt)) {
455
- lastProgressAt = now
456
- lastProgressBytes = completed
457
- yield { status: 'downloading-mmproj', completed, total: Number.isFinite(total) ? total : undefined }
458
- }
459
- }
460
- complete = true
461
- } finally {
462
- await handle.close()
463
- if (!complete) {
464
- await fs.unlink(partialPath).catch(() => {})
465
- }
466
- }
467
-
468
- await fs.rename(partialPath, candidate.localPath)
469
- }
470
-
471
- export async function backfillMmprojAvailability(
472
- model: LocalHfModel,
473
- fetchImpl: FetchImpl = fetch,
474
- ): Promise<LocalHfModel> {
475
- if (model.mmprojAvailable !== undefined) return model
476
- try {
477
- const repo = await fetchHuggingFaceRepoInfo({ repoId: model.repoId }, fetchImpl)
478
- const sibling = findMmprojSibling(repo)
479
- const next: LocalHfModel = {
480
- ...model,
481
- mmprojAvailable: Boolean(sibling),
482
- mmprojSizeBytes: sibling?.sizeBytes,
483
- }
484
- await upsertLocalHfModel(next)
485
- return next
486
- } catch {
487
- return model
488
- }
489
- }
490
-
491
- export async function backfillMmprojForModels(
492
- models: LocalHfModel[],
493
- fetchImpl: FetchImpl = fetch,
494
- ): Promise<LocalHfModel[]> {
495
- const repoIdToProbe = new Map<string, Promise<HuggingFaceRepoInfo | null>>()
496
- for (const model of models) {
497
- if (model.mmprojAvailable !== undefined) continue
498
- if (repoIdToProbe.has(model.repoId)) continue
499
- repoIdToProbe.set(
500
- model.repoId,
501
- fetchHuggingFaceRepoInfo({ repoId: model.repoId }, fetchImpl).catch(() => null),
502
- )
503
- }
504
- if (repoIdToProbe.size === 0) return models
505
- const resolved = new Map<string, HuggingFaceRepoInfo | null>()
506
- for (const [repoId, promise] of repoIdToProbe) {
507
- resolved.set(repoId, await promise)
508
- }
509
- const out: LocalHfModel[] = []
510
- for (const model of models) {
511
- if (model.mmprojAvailable !== undefined) {
512
- out.push(model)
513
- continue
514
- }
515
- const repo = resolved.get(model.repoId)
516
- if (!repo) {
517
- out.push(model)
518
- continue
519
- }
520
- const sibling = findMmprojSibling(repo)
521
- const next: LocalHfModel = {
522
- ...model,
523
- mmprojAvailable: Boolean(sibling),
524
- mmprojSizeBytes: sibling?.sizeBytes,
525
- }
526
- await upsertLocalHfModel(next)
527
- out.push(next)
528
- }
529
- return out
530
- }
531
-
532
- export async function* addMmprojToInstalledModel(
533
- modelId: string,
534
- signal?: AbortSignal,
535
- deps: { fetchImpl?: FetchImpl } = {},
536
- ): AsyncIterable<HfDownloadProgress> {
537
- const fetchImpl = deps.fetchImpl ?? fetch
538
- const existing = await findLocalHfModel(modelId)
539
- if (!existing) throw new Error(`model not installed: ${modelId}`)
540
- if (existing.mmprojPath) {
541
- yield { status: 'success', completed: 0 }
542
- return
543
- }
544
- const repo = await fetchHuggingFaceRepoInfo({ repoId: existing.repoId }, fetchImpl)
545
- const sibling = findMmprojSibling(repo)
546
- if (!sibling) throw new Error(`no vision encoder available for ${existing.repoId}`)
547
- const candidate: HfMmprojCandidate = {
548
- filename: sibling.filename,
549
- sizeBytes: sibling.sizeBytes ?? 0,
550
- localPath: localPathFor(existing.repoId, existing.resolvedRevision, sibling.filename),
551
- }
552
- yield* downloadMmprojFile(existing.repoId, existing.resolvedRevision, candidate, signal, fetchImpl)
553
- await upsertLocalHfModel({ ...existing, mmprojPath: candidate.localPath })
554
- yield { status: 'success', completed: candidate.sizeBytes }
555
- }
556
-
557
- export function shouldReportDownloadProgress(
558
- completed: number,
559
- lastCompleted: number,
560
- nowMs: number,
561
- lastReportedMs: number,
562
- ): boolean {
563
- return nowMs - lastReportedMs >= DOWNLOAD_PROGRESS_MIN_MS
564
- || completed - lastCompleted >= DOWNLOAD_PROGRESS_MIN_BYTES
565
- }
566
-
567
- export function modelFromPlan(
568
- plan: HfDownloadPlan,
569
- sha256: string | undefined,
570
- status: LocalHfStatus,
571
- mmprojPath?: string,
572
- ): LocalHfModel {
573
- const mmprojAvailable = Boolean(plan.mmprojCandidate)
574
- const now = new Date().toISOString()
575
- return {
576
- id: localModelId(plan.repoId, plan.filename),
577
- provider: 'llamacpp',
578
- repoId: plan.repoId,
579
- requestedRevision: plan.requestedRevision,
580
- resolvedRevision: plan.resolvedRevision,
581
- filename: plan.filename,
582
- displayName: plan.displayName,
583
- localPath: plan.localPath,
584
- sizeBytes: plan.sizeBytes,
585
- format: plan.review.format,
586
- runtime: plan.review.runtime,
587
- task: plan.review.task,
588
- sizeClass: plan.review.sizeClass,
589
- quantization: plan.review.quantization,
590
- risk: plan.review.risk,
591
- credibility: plan.review.credibility,
592
- license: plan.repo.license,
593
- downloads: plan.repo.downloads,
594
- likes: plan.repo.likes,
595
- reviewedAt: now,
596
- installedAt: now,
597
- status,
598
- sha256,
599
- mmprojPath,
600
- mmprojAvailable,
601
- mmprojSizeBytes: plan.mmprojCandidate?.sizeBytes,
602
- }
603
- }
604
-
605
- export function localModelId(repoId: string, filename: string): string {
606
- return `${repoId}#${filename}`
607
- }
608
-
609
- export function displayNameFor(repoId: string, filename: string): string {
610
- const basename = filename.split('/').pop() ?? filename
611
- return `${repoId} / ${basename}`
612
- }
613
-
614
- export function fileFormat(filename: string): HfFileFormat {
615
- const lower = filename.toLowerCase()
616
- if (lower.endsWith('.gguf')) return 'gguf'
617
- if (lower.endsWith('.safetensors')) return 'safetensors'
618
- if (/\.(bin|pt|pth|pkl|pickle)$/.test(lower)) return 'pickle/bin'
619
- return 'unknown'
620
- }
621
-
622
- export function quantizationFromFilename(filename: string): string | undefined {
623
- const match = filename.toUpperCase().match(/(?:^|[-_.])((?:IQ|Q)\d(?:_[A-Z0-9]+)*|F16|BF16|F32)(?=$|[-_.])/)
624
- return match?.[1]
625
- }
626
-
627
- function resolveUrl(repoId: string, revision: string, filename: string): string {
628
- return `${HF_BASE_URL}/${encodeRepoPath(repoId)}/resolve/${encodeURIComponent(revision)}/${encodePath(filename)}?download=true`
629
- }
630
-
631
- function encodeRepoPath(repoId: string): string {
632
- return repoId.split('/').map(part => encodeURIComponent(part)).join('/')
633
- }
634
-
635
- function encodePath(value: string): string {
636
- return value.split('/').map(part => encodeURIComponent(part)).join('/')
637
- }
638
-
639
- function parseSibling(value: unknown): HuggingFaceSibling[] {
640
- if (!value || typeof value !== 'object') return []
641
- const record = value as { rfilename?: unknown; filename?: unknown; size?: unknown; lfs?: unknown }
642
- const filename = typeof record.rfilename === 'string'
643
- ? record.rfilename
644
- : typeof record.filename === 'string' ? record.filename : ''
645
- if (!filename) return []
646
- return [{
647
- filename,
648
- sizeBytes: siblingSizeBytes(record),
649
- }]
650
- }
651
-
652
- function parseSearchItem(value: unknown): HuggingFaceModelSearchItem[] {
653
- if (!value || typeof value !== 'object' || Array.isArray(value)) return []
654
- const item = value as ModelSearchResponseItem
655
- const repoId = typeof item.id === 'string'
656
- ? item.id
657
- : typeof item.modelId === 'string' ? item.modelId : ''
658
- if (!repoId.includes('/')) return []
659
- return [{
660
- repoId,
661
- downloads: typeof item.downloads === 'number' ? item.downloads : undefined,
662
- likes: typeof item.likes === 'number' ? item.likes : undefined,
663
- lastModified: typeof item.lastModified === 'string' ? item.lastModified : undefined,
664
- tags: Array.isArray(item.tags)
665
- ? item.tags.filter((tag): tag is string => typeof tag === 'string')
666
- : [],
667
- }]
668
- }
669
-
670
- function siblingSizeBytes(record: { size?: unknown; lfs?: unknown }): number | undefined {
671
- if (typeof record.size === 'number' && Number.isFinite(record.size) && record.size >= 0) {
672
- return record.size
673
- }
674
- if (!record.lfs || typeof record.lfs !== 'object' || Array.isArray(record.lfs)) return undefined
675
- const size = (record.lfs as { size?: unknown }).size
676
- return typeof size === 'number' && Number.isFinite(size) && size >= 0 ? size : undefined
677
- }
678
-
679
- function licenseFrom(cardData: unknown, tags: string[]): string | undefined {
680
- if (cardData && typeof cardData === 'object' && !Array.isArray(cardData)) {
681
- const license = (cardData as { license?: unknown }).license
682
- if (typeof license === 'string' && license.trim()) return license.trim()
683
- }
684
- const tag = tags.find(item => item.startsWith('license:'))
685
- return tag ? tag.slice('license:'.length) : undefined
686
- }
687
-
688
- function credibilityFor(repo: HuggingFaceRepoInfo): HfCredibility {
689
- const downloads = repo.downloads ?? 0
690
- const likes = repo.likes ?? 0
691
- if (downloads >= 10_000 || likes >= 100) return 'established'
692
- if (downloads >= 100 || likes >= 5 || Boolean(repo.license)) return 'normal'
693
- return 'low-signal'
694
- }
695
-
696
- function taskFor(repo: HuggingFaceRepoInfo, filename: string): HfTask {
697
- const haystack = [repo.repoId, filename, ...repo.tags].join(' ').toLowerCase()
698
- if (/(embed|embedding)/.test(haystack)) return 'embedding'
699
- if (/(vision|vlm|multimodal)/.test(haystack)) return 'vision'
700
- if (/(code|coder|coding)/.test(haystack)) return 'code'
701
- if (/(chat|instruct|assistant)/.test(haystack)) return 'chat/instruct'
702
- if (/(base)/.test(haystack)) return 'base'
703
- return 'unknown'
704
- }
705
-
706
- function sizeClassFor(sizeBytes: number): HfSizeClass {
707
- const gb = sizeBytes / 1e9
708
- if (gb < 2) return 'tiny'
709
- if (gb < 8) return 'small'
710
- if (gb < 24) return 'medium'
711
- return 'large'
712
- }