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,306 +0,0 @@
1
- import type { ProviderId } from '../storage/config.js'
2
- import type { Message, Provider, ProviderCompleteOptions, StreamEvent } from './contracts.js'
3
- import { ProviderError } from './contracts.js'
4
- import { providerErrorFromResponse } from './errors.js'
5
- import { fetchWithRetryStreamEvents } from './retry.js'
6
- import { iterSseEvents } from './sse.js'
7
- import { buildResponsesBody } from './openai-responses-format.js'
8
- import { supportsOpenAIImages, type OpenAIToolDefinition } from './openai-chat.js'
9
- import { hasImageBlocks, ImageLoadError } from '../utils/images.js'
10
-
11
- const READ_TIMEOUT_MS = 45_000
12
-
13
- type DoneStopReason = 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence' | 'unknown'
14
-
15
- export type OpenAIResponsesProviderOptions = {
16
- model: string
17
- baseUrl: string
18
- accessToken: string
19
- accountId?: string
20
- originator?: string
21
- tools?: OpenAIToolDefinition[]
22
- maxRetries?: number
23
- refresh?: () => Promise<string>
24
- }
25
-
26
- type StreamingToolCall = {
27
- callId: string
28
- name: string
29
- inputJson: string
30
- started: boolean
31
- }
32
-
33
- export class OpenAIResponsesProvider implements Provider {
34
- readonly id: ProviderId = 'openai'
35
- readonly model: string
36
- readonly supportsTools: boolean
37
- private accessToken: string
38
- private readonly baseUrl: string
39
- private readonly accountId?: string
40
- private readonly originator: string
41
- private readonly tools: OpenAIToolDefinition[]
42
- private readonly maxRetries?: number
43
- private readonly refresh?: () => Promise<string>
44
-
45
- constructor(opts: OpenAIResponsesProviderOptions) {
46
- this.model = opts.model
47
- this.baseUrl = opts.baseUrl.replace(/\/+$/, '')
48
- this.accessToken = opts.accessToken
49
- this.accountId = opts.accountId
50
- this.originator = opts.originator ?? 'codex_cli_rs'
51
- this.tools = opts.tools ?? []
52
- this.maxRetries = opts.maxRetries
53
- this.refresh = opts.refresh
54
- this.supportsTools = this.tools.length > 0
55
- }
56
-
57
- async *complete(
58
- messages: Message[],
59
- signal: AbortSignal,
60
- options: ProviderCompleteOptions = {},
61
- ): AsyncIterable<StreamEvent> {
62
- if (!this.accessToken) {
63
- const error = new ProviderError('missing OAuth access token for openai (sign in again via the model picker)')
64
- yield { type: 'error', message: error.message }
65
- return
66
- }
67
-
68
- if (hasImageBlocks(messages) && !supportsOpenAIImages(this.model)) {
69
- yield { type: 'error', message: `image input is not enabled for ${this.model}` }
70
- return
71
- }
72
-
73
- let attempt = 0
74
- while (true) {
75
- attempt += 1
76
- let body: string
77
- try {
78
- body = JSON.stringify(await buildResponsesBody({
79
- model: this.model,
80
- messages,
81
- tools: this.tools,
82
- maxOutputTokens: options.maxTokens,
83
- }))
84
- } catch (err: unknown) {
85
- if (err instanceof ImageLoadError) {
86
- yield { type: 'error', message: err.message }
87
- return
88
- }
89
- throw err
90
- }
91
-
92
- let response: Response
93
- try {
94
- response = yield* fetchWithRetryStreamEvents(`${this.baseUrl}/responses`, {
95
- method: 'POST',
96
- headers: this.requestHeaders(),
97
- body,
98
- }, { signal, maxRetries: this.maxRetries, rateLimitResetProvider: 'openai-compatible' })
99
- } catch (err: unknown) {
100
- if (signal.aborted) return
101
- yield { type: 'error', message: networkErrorMessage(this.baseUrl, err) }
102
- return
103
- }
104
-
105
- if (response.status === 401 && this.refresh && attempt === 1) {
106
- try {
107
- this.accessToken = await this.refresh()
108
- continue
109
- } catch (refreshErr) {
110
- const message = refreshErr instanceof Error ? refreshErr.message : String(refreshErr)
111
- yield { type: 'error', message: `OpenAI sign-in expired and refresh failed: ${message}` }
112
- return
113
- }
114
- }
115
-
116
- if (!response.ok) {
117
- const error = await providerErrorFromResponse('openai', response)
118
- yield { type: 'error', message: error.message }
119
- return
120
- }
121
- if (!response.body) {
122
- yield { type: 'error', message: 'empty response body' }
123
- return
124
- }
125
-
126
- yield* this.parseStream(response.body, signal)
127
- return
128
- }
129
- }
130
-
131
- private requestHeaders(): Record<string, string> {
132
- const headers: Record<string, string> = {
133
- 'Content-Type': 'application/json',
134
- Accept: 'text/event-stream',
135
- Authorization: `Bearer ${this.accessToken}`,
136
- originator: this.originator,
137
- }
138
- if (this.accountId) headers['chatgpt-account-id'] = this.accountId
139
- return headers
140
- }
141
-
142
- private async *parseStream(body: ReadableStream<Uint8Array>, signal: AbortSignal): AsyncIterable<StreamEvent> {
143
- const toolCalls = new Map<string, StreamingToolCall>()
144
- let inputTokens: number | undefined
145
- let outputTokens: number | undefined
146
- let stopReason: DoneStopReason = 'unknown'
147
-
148
- try {
149
- for await (const frame of iterSseEvents(body, signal, READ_TIMEOUT_MS)) {
150
- const eventName = frame.event ?? ''
151
- if (!frame.data || frame.data === '[DONE]') continue
152
- let parsed: Record<string, unknown>
153
- try {
154
- parsed = JSON.parse(frame.data) as Record<string, unknown>
155
- } catch {
156
- continue
157
- }
158
-
159
- switch (eventName) {
160
- case 'response.output_text.delta': {
161
- const delta = typeof parsed.delta === 'string' ? parsed.delta : ''
162
- if (delta) yield { type: 'text', delta }
163
- break
164
- }
165
- case 'response.reasoning_summary_text.delta':
166
- case 'response.reasoning.delta':
167
- case 'response.reasoning_text.delta': {
168
- const delta = typeof parsed.delta === 'string' ? parsed.delta : ''
169
- if (delta) yield { type: 'thinking', delta }
170
- break
171
- }
172
- case 'response.output_item.added': {
173
- const item = (parsed.item ?? {}) as Record<string, unknown>
174
- if (item.type === 'function_call') {
175
- const callId = pickString(item.call_id) ?? pickString(item.id) ?? `tool-${toolCalls.size}`
176
- const name = pickString(item.name) ?? ''
177
- toolCalls.set(callId, { callId, name, inputJson: '', started: false })
178
- if (name) {
179
- toolCalls.get(callId)!.started = true
180
- yield { type: 'tool_use_start', id: callId, name }
181
- }
182
- }
183
- break
184
- }
185
- case 'response.function_call_arguments.delta': {
186
- const callId = resolveCallId(parsed, toolCalls)
187
- const delta = typeof parsed.delta === 'string' ? parsed.delta : ''
188
- if (!callId || !delta) break
189
- const existing = toolCalls.get(callId)
190
- if (!existing) break
191
- existing.inputJson += delta
192
- yield { type: 'tool_use_delta', id: callId, delta }
193
- break
194
- }
195
- case 'response.output_item.done': {
196
- const item = (parsed.item ?? {}) as Record<string, unknown>
197
- if (item.type === 'function_call') {
198
- const callId = pickString(item.call_id) ?? pickString(item.id)
199
- if (!callId) break
200
- const existing = toolCalls.get(callId)
201
- const name = pickString(item.name) ?? existing?.name ?? ''
202
- const argsJson = pickString(item.arguments) ?? existing?.inputJson ?? ''
203
- stopReason = 'tool_use'
204
- yield {
205
- type: 'tool_use_stop',
206
- id: callId,
207
- name,
208
- input: parseToolArguments(argsJson),
209
- }
210
- toolCalls.delete(callId)
211
- }
212
- break
213
- }
214
- case 'response.completed': {
215
- const usage = (parsed.response as { usage?: Record<string, unknown> } | undefined)?.usage
216
- const tokens = readUsage(usage)
217
- if (tokens.input !== undefined) inputTokens = tokens.input
218
- if (tokens.output !== undefined) outputTokens = tokens.output
219
- if (stopReason !== 'tool_use') stopReason = 'end_turn'
220
- break
221
- }
222
- case 'response.failed':
223
- case 'response.error':
224
- case 'error': {
225
- const error = parsed.error as { message?: string } | undefined
226
- const message = error?.message ?? 'Responses API error'
227
- yield { type: 'error', message }
228
- return
229
- }
230
- case 'response.incomplete': {
231
- const reason = pickString((parsed.response as { incomplete_details?: { reason?: string } } | undefined)?.incomplete_details?.reason)
232
- if (reason === 'max_output_tokens') stopReason = 'max_tokens'
233
- break
234
- }
235
- default:
236
- break
237
- }
238
- }
239
- } catch (err: unknown) {
240
- if (signal.aborted) return
241
- yield { type: 'error', message: networkErrorMessage(this.baseUrl, err, 'stream error') }
242
- return
243
- }
244
-
245
- if (signal.aborted) return
246
- yield { type: 'done', inputTokens, outputTokens, stopReason }
247
- }
248
- }
249
-
250
- function pickString(value: unknown): string | undefined {
251
- return typeof value === 'string' && value.length > 0 ? value : undefined
252
- }
253
-
254
- function resolveCallId(parsed: Record<string, unknown>, calls: Map<string, StreamingToolCall>): string | undefined {
255
- return (
256
- pickString(parsed.call_id)
257
- ?? pickString(parsed.item_id)
258
- ?? pickString((parsed.item as Record<string, unknown> | undefined)?.call_id)
259
- ?? pickString((parsed.item as Record<string, unknown> | undefined)?.id)
260
- ?? (calls.size === 1 ? Array.from(calls.keys())[0] : undefined)
261
- )
262
- }
263
-
264
- function readUsage(usage: Record<string, unknown> | undefined): { input?: number; output?: number } {
265
- if (!usage) return {}
266
- const input = typeof usage.input_tokens === 'number'
267
- ? usage.input_tokens
268
- : typeof usage.prompt_tokens === 'number' ? usage.prompt_tokens : undefined
269
- const output = typeof usage.output_tokens === 'number'
270
- ? usage.output_tokens
271
- : typeof usage.completion_tokens === 'number' ? usage.completion_tokens : undefined
272
- return { input, output }
273
- }
274
-
275
- function parseToolArguments(input: string): Record<string, unknown> {
276
- const trimmed = input.trim()
277
- if (!trimmed) return {}
278
- try {
279
- return coerceToToolArguments(JSON.parse(trimmed))
280
- } catch {
281
- return {}
282
- }
283
- }
284
-
285
- function coerceToToolArguments(value: unknown): Record<string, unknown> {
286
- if (typeof value === 'string') {
287
- const trimmed = value.trim()
288
- if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
289
- try {
290
- return coerceToToolArguments(JSON.parse(trimmed))
291
- } catch {
292
- return {}
293
- }
294
- }
295
- return {}
296
- }
297
- if (value && typeof value === 'object' && !Array.isArray(value)) {
298
- return value as Record<string, unknown>
299
- }
300
- return {}
301
- }
302
-
303
- function networkErrorMessage(baseUrl: string, err: unknown, fallback = 'network error'): string {
304
- const message = (err as Error).message || fallback
305
- return `openai request failed at ${baseUrl}: ${message}`
306
- }
@@ -1,124 +0,0 @@
1
- import type { Message, MessageContentBlock } from './contracts.js'
2
- import { messageTextContent } from '../utils/messages.js'
3
- import { loadImageBlock } from '../utils/images.js'
4
-
5
- export async function toWireMessages(messages: Message[]): Promise<Array<Record<string, unknown>>> {
6
- const out: Array<Record<string, unknown>> = []
7
-
8
- for (const message of messages) {
9
- if (typeof message.content === 'string') {
10
- out.push({ role: message.role, content: message.content })
11
- continue
12
- }
13
-
14
- if (message.role === 'user') {
15
- const toolResults = message.content.filter(isToolResultBlock)
16
- if (toolResults.length > 0) {
17
- for (const block of toolResults) {
18
- out.push({
19
- role: 'tool',
20
- tool_call_id: block.toolUseId,
21
- content: block.content,
22
- })
23
- }
24
- const nonToolBlocks = message.content.filter(block => block.type !== 'tool_result')
25
- if (nonToolBlocks.length > 0) {
26
- out.push({ role: 'user', content: await toOpenAIUserContent(nonToolBlocks) })
27
- }
28
- continue
29
- }
30
- out.push({ role: 'user', content: await toOpenAIUserContent(message.content) })
31
- continue
32
- }
33
-
34
- if (message.role === 'assistant') {
35
- const textParts = message.content.filter(isTextBlock).map(block => block.text)
36
- const toolCalls = message.content.filter(isToolUseBlock).map(block => ({
37
- id: block.id,
38
- type: 'function',
39
- function: {
40
- name: block.name,
41
- arguments: JSON.stringify(block.input),
42
- },
43
- }))
44
- out.push({
45
- role: 'assistant',
46
- content: textParts.join(''),
47
- tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
48
- })
49
- continue
50
- }
51
-
52
- const toolResults = message.content.filter(isToolResultBlock)
53
- if (toolResults.length > 0) {
54
- for (const block of toolResults) {
55
- out.push({
56
- role: 'tool',
57
- tool_call_id: block.toolUseId,
58
- content: block.content,
59
- })
60
- }
61
- continue
62
- }
63
-
64
- out.push({ role: message.role, content: messageTextContent(message) })
65
- }
66
-
67
- return normalizeSystemMessages(out)
68
- }
69
-
70
- async function toOpenAIUserContent(blocks: MessageContentBlock[]): Promise<Array<Record<string, unknown>>> {
71
- const parts: Array<Record<string, unknown>> = []
72
- for (const block of blocks) {
73
- if (block.type === 'text') {
74
- if (block.text.length > 0) parts.push({ type: 'text', text: block.text })
75
- continue
76
- }
77
- if (block.type === 'image') {
78
- const loaded = await loadImageBlock(block)
79
- if (loaded.url) {
80
- parts.push({ type: 'image_url', image_url: { url: loaded.url } })
81
- } else if (loaded.dataBase64 && loaded.mimeType) {
82
- parts.push({ type: 'image_url', image_url: { url: `data:${loaded.mimeType};base64,${loaded.dataBase64}` } })
83
- }
84
- continue
85
- }
86
- }
87
- return parts.length > 0 ? parts : [{ type: 'text', text: '' }]
88
- }
89
-
90
- function normalizeSystemMessages(messages: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
91
- const systemContents: string[] = []
92
- const nonSystem: Array<Record<string, unknown>> = []
93
-
94
- for (const message of messages) {
95
- if (message.role === 'system') {
96
- if (typeof message.content === 'string' && message.content.length > 0) {
97
- systemContents.push(message.content)
98
- }
99
- continue
100
- }
101
- nonSystem.push(message)
102
- }
103
-
104
- if (systemContents.length === 0) return nonSystem
105
- return [
106
- {
107
- role: 'system',
108
- content: systemContents.join('\n\n'),
109
- },
110
- ...nonSystem,
111
- ]
112
- }
113
-
114
- function isTextBlock(block: MessageContentBlock): block is Extract<MessageContentBlock, { type: 'text' }> {
115
- return block.type === 'text'
116
- }
117
-
118
- function isToolUseBlock(block: MessageContentBlock): block is Extract<MessageContentBlock, { type: 'tool_use' }> {
119
- return block.type === 'tool_use'
120
- }
121
-
122
- function isToolResultBlock(block: MessageContentBlock): block is Extract<MessageContentBlock, { type: 'tool_result' }> {
123
- return block.type === 'tool_result'
124
- }
@@ -1,120 +0,0 @@
1
- import type { EthagentConfig } from '../storage/config.js'
2
- import { localProviderBaseUrlFor } from '../storage/config.js'
3
- import { getKey } from '../storage/secrets.js'
4
- import type { Provider } from './contracts.js'
5
- import type { SessionMode } from '../runtime/sessionMode.js'
6
- import { AnthropicProvider } from './anthropic.js'
7
- import { GeminiProvider } from './gemini.js'
8
- import { OpenAIChatProvider } from './openai-chat.js'
9
- import { OpenAIResponsesProvider } from './openai-responses.js'
10
- import { anthropicTools, geminiTools, openAITools } from '../tools/registry.js'
11
- import { openAIBaseUrlFor, OPENAI_OAUTH_DEFAULT_MODEL, isOpenAIOAuthAllowedModel } from '../models/catalog.js'
12
- import {
13
- getOpenAIOAuthCredentials,
14
- setOpenAIOAuthCredentials,
15
- type OpenAIOAuthCredentials,
16
- } from '../auth/openaiOAuth/credentials.js'
17
- import { refreshOpenAIAccessToken, shouldRefresh } from '../auth/openaiOAuth/refresh.js'
18
- import type { Tool } from '../tools/contracts.js'
19
-
20
- export const OPENAI_CHATGPT_BACKEND_URL = 'https://chatgpt.com/backend-api/codex'
21
-
22
- export function isLocalProvider(provider: string): boolean {
23
- return provider === 'llamacpp'
24
- }
25
-
26
- export function createProvider(config: EthagentConfig, options: { mode?: SessionMode; dynamicTools?: Tool[] } = {}): Provider {
27
- const mode = options.mode ?? 'chat'
28
- const toolContext = { hasIdentity: Boolean(config.identity), dynamicTools: options.dynamicTools }
29
- switch (config.provider) {
30
- case 'llamacpp':
31
- return new OpenAIChatProvider({
32
- id: 'llamacpp',
33
- model: config.model,
34
- baseUrl: localProviderBaseUrlFor('llamacpp', config.baseUrl),
35
- apiKey: 'llamacpp',
36
- tools: openAITools(mode, toolContext),
37
- hasVisionProjector: Boolean(config.localMmprojPath),
38
- })
39
- case 'openai':
40
- return createOpenAIProvider(config, openAITools(mode, toolContext))
41
- case 'anthropic':
42
- return new AnthropicProvider({ model: config.model, tools: anthropicTools(mode, toolContext) })
43
- case 'gemini':
44
- return new GeminiProvider({ model: config.model, tools: geminiTools(mode, toolContext) })
45
- }
46
- }
47
-
48
- function createOpenAIProvider(config: EthagentConfig, tools: ReturnType<typeof openAITools>): Provider {
49
- return new OpenAIRoutingProvider(config, tools)
50
- }
51
-
52
- class OpenAIRoutingProvider implements Provider {
53
- readonly id = 'openai' as const
54
- readonly model: string
55
- readonly supportsTools: boolean
56
- private delegate: Provider | null = null
57
- private readonly config: EthagentConfig
58
- private readonly tools: ReturnType<typeof openAITools>
59
-
60
- constructor(config: EthagentConfig, tools: ReturnType<typeof openAITools>) {
61
- this.config = config
62
- this.tools = tools
63
- this.model = config.model
64
- this.supportsTools = tools.length > 0
65
- }
66
-
67
- async *complete(...args: Parameters<Provider['complete']>): ReturnType<Provider['complete']> {
68
- if (!this.delegate) this.delegate = await this.resolveDelegate()
69
- yield* this.delegate.complete(...args)
70
- }
71
-
72
- private async resolveDelegate(): Promise<Provider> {
73
- const oauth = await loadFreshOAuthCredentials()
74
- if (oauth) {
75
- const oauthModel = isOpenAIOAuthAllowedModel(this.model) ? this.model : OPENAI_OAUTH_DEFAULT_MODEL
76
- return new OpenAIResponsesProvider({
77
- model: oauthModel,
78
- baseUrl: OPENAI_CHATGPT_BACKEND_URL,
79
- accessToken: oauth.accessToken,
80
- accountId: oauth.accountId,
81
- tools: this.tools,
82
- refresh: async () => {
83
- const next = await loadFreshOAuthCredentials({ force: true })
84
- if (!next) throw new Error('No OAuth credentials available')
85
- return next.accessToken
86
- },
87
- })
88
- }
89
- return new OpenAIChatProvider({
90
- id: 'openai',
91
- model: this.model,
92
- baseUrl: openAIBaseUrlFor(this.config),
93
- loadApiKey: () => getKey('openai'),
94
- tools: this.tools,
95
- })
96
- }
97
- }
98
-
99
- async function loadFreshOAuthCredentials(options: { force?: boolean } = {}): Promise<OpenAIOAuthCredentials | null> {
100
- const current = await getOpenAIOAuthCredentials()
101
- if (!current) return null
102
- if (!options.force && !shouldRefresh(current)) return current
103
-
104
- try {
105
- const refreshed = await refreshOpenAIAccessToken(current.refreshToken)
106
- const now = Date.now()
107
- const next: OpenAIOAuthCredentials = {
108
- accessToken: refreshed.accessToken,
109
- refreshToken: refreshed.refreshToken,
110
- idToken: refreshed.idToken ?? current.idToken,
111
- accountId: refreshed.accountId ?? current.accountId,
112
- expiresAt: now + refreshed.expiresIn * 1000,
113
- lastRefreshAt: now,
114
- }
115
- await setOpenAIOAuthCredentials(next)
116
- return next
117
- } catch {
118
- return current
119
- }
120
- }
@@ -1,58 +0,0 @@
1
- import type { ProviderRetryStreamEvent } from './contracts.js'
2
- import { fetchWithRetry, type FetchWithRetryOptions, type RetryEvent } from '../utils/withRetry.js'
3
-
4
- type FetchSettled =
5
- | { state: 'resolved'; response: Response }
6
- | { state: 'rejected'; error: unknown }
7
-
8
- export async function* fetchWithRetryStreamEvents(
9
- input: string,
10
- init: RequestInit,
11
- options: FetchWithRetryOptions = {},
12
- ): AsyncGenerator<ProviderRetryStreamEvent, Response, void> {
13
- const retryEvents: ProviderRetryStreamEvent[] = []
14
- let settled: FetchSettled | undefined
15
- let wake: (() => void) | undefined
16
-
17
- const wakeWaiter = () => {
18
- const current = wake
19
- wake = undefined
20
- current?.()
21
- }
22
-
23
- const waitForChange = (): Promise<void> => new Promise(resolve => {
24
- wake = resolve
25
- if (settled || retryEvents.length > 0) wakeWaiter()
26
- })
27
-
28
- const fetchPromise = fetchWithRetry(input, init, {
29
- ...options,
30
- onRetry: (event: RetryEvent) => {
31
- options.onRetry?.(event)
32
- retryEvents.push({ type: 'retry', ...event })
33
- wakeWaiter()
34
- },
35
- }).then(
36
- response => {
37
- settled = { state: 'resolved', response }
38
- wakeWaiter()
39
- },
40
- error => {
41
- settled = { state: 'rejected', error }
42
- wakeWaiter()
43
- },
44
- )
45
-
46
- while (!settled || retryEvents.length > 0) {
47
- while (retryEvents.length > 0) {
48
- yield retryEvents.shift()!
49
- }
50
- if (settled) break
51
- await waitForChange()
52
- }
53
-
54
- await fetchPromise
55
- if (settled?.state === 'resolved') return settled.response
56
- if (settled?.state === 'rejected') throw settled.error
57
- throw new Error('Fetch retry completed without a response')
58
- }
@@ -1,93 +0,0 @@
1
- export type SseEvent = {
2
- event: string | null
3
- data: string
4
- }
5
-
6
- export async function* iterSseFrames(
7
- body: ReadableStream<Uint8Array>,
8
- signal: AbortSignal,
9
- readTimeoutMs: number,
10
- ): AsyncIterable<string> {
11
- for await (const event of iterSseEvents(body, signal, readTimeoutMs)) {
12
- yield event.data
13
- }
14
- }
15
-
16
- export async function* iterSseEvents(
17
- body: ReadableStream<Uint8Array>,
18
- signal: AbortSignal,
19
- readTimeoutMs: number,
20
- ): AsyncIterable<SseEvent> {
21
- const reader = body.getReader()
22
- const decoder = new TextDecoder()
23
- let buffer = ''
24
- try {
25
- while (!signal.aborted) {
26
- const { done, value } = await readWithTimeout(reader, readTimeoutMs)
27
- if (done) break
28
- buffer += decoder.decode(value, { stream: true })
29
- let boundary = findFrameBoundary(buffer)
30
- while (boundary !== -1) {
31
- const raw = buffer.slice(0, boundary)
32
- const separator = buffer.slice(boundary).match(/^\r?\n\r?\n/)?.[0] ?? '\n\n'
33
- buffer = buffer.slice(boundary + separator.length)
34
- const event = extractSseEvent(raw)
35
- if (event) yield event
36
- boundary = findFrameBoundary(buffer)
37
- }
38
- }
39
-
40
- const tail = buffer.trim()
41
- if (tail) {
42
- const event = extractSseEvent(tail)
43
- if (event) yield event
44
- }
45
- } finally {
46
- try { reader.releaseLock() } catch { void 0 }
47
- }
48
- }
49
-
50
- export async function readWithTimeout(
51
- reader: ReadableStreamDefaultReader<Uint8Array>,
52
- timeoutMs: number,
53
- ): Promise<ReadableStreamReadResult<Uint8Array>> {
54
- let timer: NodeJS.Timeout | undefined
55
- const timeout = new Promise<never>((_, reject) => {
56
- timer = setTimeout(() => {
57
- reject(new Error(`no response from model in ${Math.round(timeoutMs / 1000)}s`))
58
- }, timeoutMs)
59
- })
60
- try {
61
- return await Promise.race([reader.read(), timeout])
62
- } finally {
63
- if (timer) clearTimeout(timer)
64
- }
65
- }
66
-
67
- export function extractDataPayload(frame: string): string | null {
68
- return extractSseEvent(frame)?.data ?? null
69
- }
70
-
71
- function extractSseEvent(frame: string): SseEvent | null {
72
- const lines = frame.split(/\r?\n/)
73
- const dataLines: string[] = []
74
- let eventName: string | null = null
75
-
76
- for (const line of lines) {
77
- if (line.startsWith('event:')) {
78
- eventName = line.slice(6).trim() || null
79
- continue
80
- }
81
- if (line.startsWith('data:')) {
82
- dataLines.push(line.slice(5).replace(/^ /, ''))
83
- }
84
- }
85
-
86
- if (dataLines.length === 0) return null
87
- return { event: eventName, data: dataLines.join('\n') }
88
- }
89
-
90
- function findFrameBoundary(buffer: string): number {
91
- const match = /\r?\n\r?\n/.exec(buffer)
92
- return match?.index ?? -1
93
- }