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,46 +0,0 @@
1
-
2
- import { useCallback, useEffect, useRef } from 'react'
3
-
4
- export const DOUBLE_PRESS_TIMEOUT_MS = 1800
5
-
6
- export function useDoublePress(
7
- setPending: (pending: boolean) => void,
8
- onDoublePress: () => void,
9
- ): () => void {
10
- const lastPressRef = useRef<number>(0)
11
- const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
12
-
13
- const clearPendingTimeout = useCallback(() => {
14
- if (timeoutRef.current) {
15
- clearTimeout(timeoutRef.current)
16
- timeoutRef.current = undefined
17
- }
18
- }, [])
19
-
20
- useEffect(() => {
21
- return () => {
22
- clearPendingTimeout()
23
- }
24
- }, [clearPendingTimeout])
25
-
26
- return useCallback(() => {
27
- const now = Date.now()
28
- const elapsed = now - lastPressRef.current
29
- const isDouble = elapsed <= DOUBLE_PRESS_TIMEOUT_MS && timeoutRef.current !== undefined
30
-
31
- if (isDouble) {
32
- clearPendingTimeout()
33
- setPending(false)
34
- onDoublePress()
35
- } else {
36
- setPending(true)
37
- clearPendingTimeout()
38
- timeoutRef.current = setTimeout(() => {
39
- setPending(false)
40
- timeoutRef.current = undefined
41
- }, DOUBLE_PRESS_TIMEOUT_MS)
42
- }
43
-
44
- lastPressRef.current = now
45
- }, [setPending, onDoublePress, clearPendingTimeout])
46
- }
@@ -1,36 +0,0 @@
1
- import { useCallback, useMemo, useState } from 'react'
2
- import { useApp } from 'ink'
3
- import { useKeybinding } from '../keybindings/KeybindingProvider.js'
4
- import { useDoublePress } from './useDoublePress.js'
5
-
6
- export type ExitState = {
7
- pending: boolean
8
- keyName: 'ctrl+c' | null
9
- }
10
-
11
- type Options = {
12
- isActive?: boolean
13
- onInterrupt?: () => boolean
14
- onExit?: () => void
15
- }
16
-
17
- export function useExitOnCtrlC({ isActive = true, onInterrupt, onExit }: Options = {}): ExitState {
18
- const { exit } = useApp()
19
- const [state, setState] = useState<ExitState>({ pending: false, keyName: null })
20
-
21
- const exitFn = useMemo(() => onExit ?? exit, [onExit, exit])
22
-
23
- const ctrlCDouble = useDoublePress(
24
- pending => setState({ pending, keyName: 'ctrl+c' }),
25
- exitFn,
26
- )
27
-
28
- const handleInterrupt = useCallback(() => {
29
- if (onInterrupt?.()) return
30
- ctrlCDouble()
31
- }, [onInterrupt, ctrlCDouble])
32
-
33
- useKeybinding('app:interrupt', handleInterrupt, { context: 'Global', isActive })
34
-
35
- return state
36
- }
@@ -1,47 +0,0 @@
1
- import { getSecret, rmSecret, setSecret } from '../../storage/secrets.js'
2
-
3
- const ACCOUNT = 'openai-oauth'
4
-
5
- export type OpenAIOAuthCredentials = {
6
- accessToken: string
7
- refreshToken: string
8
- idToken?: string
9
- accountId?: string
10
- expiresAt: number
11
- lastRefreshAt: number
12
- }
13
-
14
- export async function getOpenAIOAuthCredentials(): Promise<OpenAIOAuthCredentials | null> {
15
- const raw = await getSecret(ACCOUNT)
16
- if (!raw) return null
17
- try {
18
- const parsed = JSON.parse(raw) as Partial<OpenAIOAuthCredentials>
19
- if (typeof parsed.accessToken !== 'string' || !parsed.accessToken) return null
20
- if (typeof parsed.refreshToken !== 'string' || !parsed.refreshToken) return null
21
- if (typeof parsed.expiresAt !== 'number' || !Number.isFinite(parsed.expiresAt)) return null
22
- if (typeof parsed.lastRefreshAt !== 'number' || !Number.isFinite(parsed.lastRefreshAt)) return null
23
- return {
24
- accessToken: parsed.accessToken,
25
- refreshToken: parsed.refreshToken,
26
- idToken: typeof parsed.idToken === 'string' ? parsed.idToken : undefined,
27
- accountId: typeof parsed.accountId === 'string' ? parsed.accountId : undefined,
28
- expiresAt: parsed.expiresAt,
29
- lastRefreshAt: parsed.lastRefreshAt,
30
- }
31
- } catch {
32
- return null
33
- }
34
- }
35
-
36
- export async function setOpenAIOAuthCredentials(creds: OpenAIOAuthCredentials): Promise<void> {
37
- await setSecret(ACCOUNT, JSON.stringify(creds))
38
- }
39
-
40
- export async function rmOpenAIOAuthCredentials(): Promise<void> {
41
- await rmSecret(ACCOUNT)
42
- }
43
-
44
- export async function hasOpenAIOAuthCredentials(): Promise<boolean> {
45
- const creds = await getOpenAIOAuthCredentials()
46
- return creds !== null
47
- }
@@ -1,23 +0,0 @@
1
- import { randomBytes, webcrypto } from 'node:crypto'
2
-
3
- function base64UrlEncode(buffer: Buffer): string {
4
- return buffer
5
- .toString('base64')
6
- .replace(/\+/g, '-')
7
- .replace(/\//g, '_')
8
- .replace(/=/g, '')
9
- }
10
-
11
- export function generateCodeVerifier(): string {
12
- return base64UrlEncode(randomBytes(32))
13
- }
14
-
15
- export async function generateCodeChallenge(verifier: string): Promise<string> {
16
- const encoded = new TextEncoder().encode(verifier)
17
- const digest = await webcrypto.subtle.digest('SHA-256', encoded)
18
- return base64UrlEncode(Buffer.from(digest))
19
- }
20
-
21
- export function generateState(): string {
22
- return base64UrlEncode(randomBytes(32))
23
- }
@@ -1,238 +0,0 @@
1
- import { AuthCodeListener } from './listener.js'
2
- import { generateCodeChallenge, generateCodeVerifier, generateState } from './crypto.js'
3
- import {
4
- OPENAI_OAUTH_CALLBACK_PORT,
5
- OPENAI_OAUTH_CLIENT_ID,
6
- OPENAI_OAUTH_ISSUER,
7
- OPENAI_OAUTH_ORIGINATOR,
8
- OPENAI_OAUTH_SCOPE,
9
- OPENAI_OAUTH_TOKEN_URL,
10
- asTrimmedString,
11
- exchangeIdTokenForApiKey,
12
- parseChatgptAccountId,
13
- } from './shared.js'
14
- import { setOpenAIOAuthCredentials } from './credentials.js'
15
- import { renderOAuthLandingPage } from './landingPage.js'
16
-
17
- type OpenAIOAuthTokenResponse = {
18
- id_token?: string
19
- access_token?: string
20
- refresh_token?: string
21
- expires_in?: number
22
- }
23
-
24
- export type OpenAIOAuthResult =
25
- | { kind: 'apikey'; apiKey: string; accountId?: string }
26
- | { kind: 'oauth-only'; accountId?: string; reason?: string }
27
-
28
- export type OpenAIOAuthOnReady = (authUrl: string) => void | Promise<void>
29
-
30
- function buildAuthorizeUrl(args: { port: number; codeChallenge: string; state: string }): string {
31
- const redirectUri = `http://localhost:${args.port}/auth/callback`
32
- const params: Array<[string, string]> = [
33
- ['response_type', 'code'],
34
- ['client_id', OPENAI_OAUTH_CLIENT_ID],
35
- ['redirect_uri', redirectUri],
36
- ['scope', OPENAI_OAUTH_SCOPE],
37
- ['code_challenge', args.codeChallenge],
38
- ['code_challenge_method', 'S256'],
39
- ['id_token_add_organizations', 'true'],
40
- ['codex_cli_simplified_flow', 'true'],
41
- ['state', args.state],
42
- ['originator', OPENAI_OAUTH_ORIGINATOR],
43
- ]
44
- const qs = params
45
- .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
46
- .join('&')
47
- return `${OPENAI_OAUTH_ISSUER}/oauth/authorize?${qs}`
48
- }
49
-
50
- function renderSuccessPage(): string {
51
- return renderOAuthLandingPage({
52
- tone: 'success',
53
- pageTitle: 'OpenAI Sign-in Complete',
54
- headline: 'OpenAI sign-in complete',
55
- message: 'You can return to your terminal.',
56
- })
57
- }
58
-
59
- function renderErrorPage(reason: string): string {
60
- return renderOAuthLandingPage({
61
- tone: 'error',
62
- pageTitle: 'OpenAI Sign-in Failed',
63
- headline: 'OpenAI sign-in failed',
64
- message: reason,
65
- })
66
- }
67
-
68
- function renderCancelledPage(): string {
69
- return renderOAuthLandingPage({
70
- tone: 'cancelled',
71
- pageTitle: 'OpenAI Sign-in Cancelled',
72
- headline: 'OpenAI sign-in cancelled',
73
- message: 'You can return to your terminal.',
74
- })
75
- }
76
-
77
- async function exchangeAuthorizationCode(args: {
78
- authorizationCode: string
79
- codeVerifier: string
80
- port: number
81
- signal: AbortSignal
82
- }): Promise<OpenAIOAuthResult> {
83
- const redirectUri = `http://localhost:${args.port}/auth/callback`
84
- const body = new URLSearchParams({
85
- grant_type: 'authorization_code',
86
- code: args.authorizationCode,
87
- redirect_uri: redirectUri,
88
- client_id: OPENAI_OAUTH_CLIENT_ID,
89
- code_verifier: args.codeVerifier,
90
- })
91
-
92
- const response = await fetch(OPENAI_OAUTH_TOKEN_URL, {
93
- method: 'POST',
94
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
95
- body,
96
- signal: AbortSignal.any([args.signal, AbortSignal.timeout(15_000)]),
97
- })
98
-
99
- if (!response.ok) {
100
- const errorText = await response.text().catch(() => '')
101
- throw new Error(
102
- errorText.trim()
103
- ? `OpenAI OAuth token exchange failed (${response.status}): ${errorText.trim()}`
104
- : `OpenAI OAuth token exchange failed with status ${response.status}.`,
105
- )
106
- }
107
-
108
- const payload = (await response.json()) as OpenAIOAuthTokenResponse
109
- const accessToken = asTrimmedString(payload.access_token)
110
- const refreshToken = asTrimmedString(payload.refresh_token)
111
- const idToken = asTrimmedString(payload.id_token)
112
- if (!accessToken || !refreshToken) {
113
- throw new Error('OpenAI OAuth completed, but the response was missing access_token or refresh_token.')
114
- }
115
-
116
- const accountId = parseChatgptAccountId(idToken) ?? parseChatgptAccountId(accessToken)
117
- const expiresIn = typeof payload.expires_in === 'number' && payload.expires_in > 0
118
- ? payload.expires_in
119
- : 3600
120
- const now = Date.now()
121
- await setOpenAIOAuthCredentials({
122
- accessToken,
123
- refreshToken,
124
- idToken,
125
- accountId,
126
- expiresAt: now + expiresIn * 1000,
127
- lastRefreshAt: now,
128
- })
129
-
130
- if (!idToken) {
131
- return { kind: 'oauth-only', accountId, reason: 'no id_token returned' }
132
- }
133
-
134
- try {
135
- const apiKey = await exchangeIdTokenForApiKey(idToken)
136
- if (typeof apiKey !== 'string' || apiKey.length === 0) {
137
- return { kind: 'oauth-only', accountId, reason: 'API key exchange returned an empty token' }
138
- }
139
- return { kind: 'apikey', apiKey, accountId }
140
- } catch (err) {
141
- const reason = err instanceof Error ? err.message : String(err)
142
- return { kind: 'oauth-only', accountId, reason }
143
- }
144
- }
145
-
146
- export class OpenAIOAuthService {
147
- private listener: AuthCodeListener | null = null
148
- private exchangeAbort: AbortController | null = null
149
-
150
- async start(onReady: OpenAIOAuthOnReady): Promise<OpenAIOAuthResult> {
151
- const codeVerifier = generateCodeVerifier()
152
- const listener = new AuthCodeListener('/auth/callback')
153
- this.listener = listener
154
-
155
- let port: number
156
- try {
157
- port = await listener.start(OPENAI_OAUTH_CALLBACK_PORT)
158
- } catch (err) {
159
- const message = err instanceof Error ? err.message : String(err)
160
- if (message.includes('EADDRINUSE') || message.includes(String(OPENAI_OAUTH_CALLBACK_PORT))) {
161
- listener.close()
162
- this.listener = null
163
- throw new Error(
164
- `OpenAI sign-in needs localhost:${OPENAI_OAUTH_CALLBACK_PORT} for its callback. Close any app already using that port and try again.`,
165
- )
166
- }
167
- listener.close()
168
- this.listener = null
169
- throw err
170
- }
171
-
172
- const state = generateState()
173
- const codeChallenge = await generateCodeChallenge(codeVerifier)
174
- const authUrl = buildAuthorizeUrl({ port, codeChallenge, state })
175
-
176
- try {
177
- const authorizationCode = await listener.waitForAuthorization(state, async () => {
178
- await onReady(authUrl)
179
- })
180
-
181
- const abort = new AbortController()
182
- this.exchangeAbort = abort
183
- let result: OpenAIOAuthResult
184
- try {
185
- result = await exchangeAuthorizationCode({
186
- authorizationCode,
187
- codeVerifier,
188
- port,
189
- signal: abort.signal,
190
- })
191
- } finally {
192
- if (this.exchangeAbort === abort) this.exchangeAbort = null
193
- }
194
-
195
- if (this.listener !== listener) {
196
- throw new Error('OpenAI sign-in was cancelled.')
197
- }
198
-
199
- listener.handleSuccessRedirect([], res => {
200
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
201
- res.end(renderSuccessPage())
202
- })
203
-
204
- return result
205
- } catch (error) {
206
- const resolved = this.listener === listener
207
- ? error
208
- : new Error('OpenAI sign-in was cancelled.')
209
-
210
- if (listener.hasPendingResponse()) {
211
- const isCancellation = resolved instanceof Error && resolved.message === 'OpenAI sign-in was cancelled.'
212
- listener.handleErrorRedirect(res => {
213
- res.writeHead(isCancellation ? 200 : 400, { 'Content-Type': 'text/html; charset=utf-8' })
214
- res.end(isCancellation ? renderCancelledPage() : renderErrorPage(
215
- resolved instanceof Error ? resolved.message : String(resolved),
216
- ))
217
- })
218
- }
219
- throw resolved
220
- } finally {
221
- this.cleanup()
222
- }
223
- }
224
-
225
- cleanup(): void {
226
- const cancellationError = new Error('OpenAI sign-in was cancelled.')
227
- this.exchangeAbort?.abort(cancellationError)
228
- this.exchangeAbort = null
229
- if (this.listener?.hasPendingResponse()) {
230
- this.listener.handleErrorRedirect(res => {
231
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
232
- res.end(renderCancelledPage())
233
- })
234
- }
235
- this.listener?.cancelPendingAuthorization(cancellationError)
236
- this.listener = null
237
- }
238
- }
@@ -1,116 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import { transformSync } from 'esbuild'
3
- import { WALLET_CSS } from '../../identity/wallet/page/styles/index.js'
4
- import { glyphs } from '../../identity/wallet/page/html.js'
5
- import { walletPageSourceFile } from '../../identity/wallet/browserWallet/walletPageSource.js'
6
- import { escapeHtml } from './shared.js'
7
-
8
- export type LandingTone = 'success' | 'error' | 'cancelled'
9
-
10
- const GRAINIENT_SOURCE_FILE = walletPageSourceFile('page/grainient.ts')
11
-
12
- const COMPILED_GRAINIENT = compileGrainientModule()
13
-
14
- const GRAINIENT_OPTIONS = {
15
- color1: '#000422',
16
- color2: '#d8dcfa',
17
- color3: '#000422',
18
- timeSpeed: 0.25,
19
- colorBalance: 0,
20
- warpStrength: 1,
21
- warpFrequency: 5,
22
- warpSpeed: 2,
23
- warpAmplitude: 10,
24
- blendAngle: 0,
25
- blendSoftness: 0.05,
26
- rotationAmount: 500,
27
- noiseScale: 2,
28
- grainAmount: 0.1,
29
- grainScale: 2,
30
- grainAnimated: false,
31
- contrast: 1.5,
32
- gamma: 1,
33
- saturation: 1,
34
- centerX: 0,
35
- centerY: 0,
36
- zoom: 0.9,
37
- }
38
-
39
- const CHECK_SVG = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>'
40
-
41
- const LANDING_EXTRA_CSS = `
42
- main[data-flow="signin"] .body > .status { margin-top: 9px; }
43
- main[data-tone="error"] .status .marker {
44
- color: var(--c-danger);
45
- border-color: color-mix(in srgb, var(--c-danger) 45%, transparent);
46
- }
47
- `
48
-
49
- function markerHtml(tone: LandingTone): string {
50
- if (tone === 'success') return CHECK_SVG
51
- if (tone === 'error') return '<span aria-hidden="true">!</span>'
52
- return '<span aria-hidden="true">–</span>'
53
- }
54
-
55
- export function renderOAuthLandingPage(args: {
56
- tone: LandingTone
57
- pageTitle: string
58
- headline: string
59
- message: string
60
- }): string {
61
- const title = escapeHtml(args.pageTitle)
62
- const headline = escapeHtml(args.headline)
63
- const message = escapeHtml(args.message)
64
- const splash = escapeHtml(glyphs.eyes)
65
- const optionsJson = JSON.stringify(GRAINIENT_OPTIONS).replaceAll('<', '\\u003c')
66
- const safeScript = COMPILED_GRAINIENT.replaceAll('</', '<\\/')
67
- const marker = markerHtml(args.tone)
68
-
69
- return `<!doctype html>
70
- <html lang="en">
71
- <head>
72
- <meta charset="utf-8" />
73
- <meta name="viewport" content="width=device-width, initial-scale=1" />
74
- <title>${title}</title>
75
- <style>${WALLET_CSS}
76
- ${LANDING_EXTRA_CSS}</style>
77
- </head>
78
- <body>
79
- <canvas id="grainient" class="grainient-canvas" aria-hidden="true"></canvas>
80
- <main id="card" data-flow="signin" data-tone="${args.tone}">
81
- <div class="chrome">
82
- <span class="chrome-spacer"></span>
83
- <span class="chrome-title">ethagent</span>
84
- <span class="chrome-actions"></span>
85
- </div>
86
- <div class="body">
87
- <div class="splash-wrap"><pre class="splash">${splash}</pre></div>
88
- <h2 class="flow-title">${headline}</h2>
89
- <div class="status">
90
- <p class="status-line">
91
- <span class="marker">${marker}</span>
92
- <span>${message}</span>
93
- </p>
94
- <p class="status-hint">You may close this tab now.</p>
95
- </div>
96
- </div>
97
- </main>
98
- <script>
99
- ${safeScript}
100
- ;(function () {
101
- var canvas = document.getElementById('grainient');
102
- if (canvas) startGrainient(canvas, ${optionsJson});
103
- })();
104
- </script>
105
- </body>
106
- </html>`
107
- }
108
-
109
- function compileGrainientModule(): string {
110
- const source = readFileSync(GRAINIENT_SOURCE_FILE, 'utf8')
111
- const stripped = source
112
- .split(/\r?\n/)
113
- .map(line => line.replace(/^export\s+(?=(function|const|let|interface|type|class)\b)/, ''))
114
- .join('\n')
115
- return transformSync(stripped, { loader: 'ts', target: 'es2020' }).code
116
- }
@@ -1,151 +0,0 @@
1
- import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'
2
- import type { AddressInfo } from 'node:net'
3
-
4
- export class AuthCodeListener {
5
- private localServer: Server
6
- private port = 0
7
- private promiseResolver: ((authorizationCode: string) => void) | null = null
8
- private promiseRejecter: ((error: Error) => void) | null = null
9
- private expectedState: string | null = null
10
- private pendingResponse: ServerResponse | null = null
11
- private readonly callbackPath: string
12
-
13
- constructor(callbackPath = '/auth/callback') {
14
- this.localServer = createServer()
15
- this.callbackPath = callbackPath
16
- }
17
-
18
- async start(port?: number): Promise<number> {
19
- return new Promise((resolve, reject) => {
20
- const onError = (err: Error): void => {
21
- reject(new Error(`Failed to start OAuth callback server: ${err.message}`))
22
- }
23
- this.localServer.once('error', onError)
24
- this.localServer.listen(port ?? 0, 'localhost', () => {
25
- this.localServer.removeListener('error', onError)
26
- const address = this.localServer.address() as AddressInfo
27
- this.port = address.port
28
- resolve(this.port)
29
- })
30
- })
31
- }
32
-
33
- getPort(): number {
34
- return this.port
35
- }
36
-
37
- hasPendingResponse(): boolean {
38
- return this.pendingResponse !== null
39
- }
40
-
41
- async waitForAuthorization(
42
- state: string,
43
- onReady: () => Promise<void> | void,
44
- ): Promise<string> {
45
- return new Promise<string>((resolve, reject) => {
46
- this.promiseResolver = resolve
47
- this.promiseRejecter = reject
48
- this.expectedState = state
49
- this.localServer.on('request', this.handleRedirect.bind(this))
50
- this.localServer.on('error', err => this.cancelPendingAuthorization(err))
51
- void Promise.resolve(onReady()).catch(err => this.cancelPendingAuthorization(err as Error))
52
- })
53
- }
54
-
55
- handleSuccessRedirect(
56
- _scopes: string[],
57
- customHandler?: (res: ServerResponse) => void,
58
- ): void {
59
- if (!this.pendingResponse) return
60
- const response = this.pendingResponse
61
- try {
62
- if (customHandler) customHandler(response)
63
- else {
64
- response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
65
- response.end('OpenAI sign-in complete. You can close this tab.')
66
- }
67
- if (!response.writableEnded && !response.destroyed) response.end()
68
- } finally {
69
- this.pendingResponse = null
70
- }
71
- }
72
-
73
- handleErrorRedirect(customHandler?: (res: ServerResponse) => void): void {
74
- if (!this.pendingResponse) return
75
- const response = this.pendingResponse
76
- try {
77
- if (customHandler) customHandler(response)
78
- else {
79
- response.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
80
- response.end('OpenAI sign-in failed. You can close this tab.')
81
- }
82
- if (!response.writableEnded && !response.destroyed) response.end()
83
- } finally {
84
- this.pendingResponse = null
85
- }
86
- }
87
-
88
- cancelPendingAuthorization(error: Error = new Error('OAuth authorization was cancelled.')): void {
89
- this.reject(error)
90
- this.close()
91
- }
92
-
93
- private handleRedirect(req: IncomingMessage, res: ServerResponse): void {
94
- const parsedUrl = new URL(req.url || '', `http://${req.headers.host || 'localhost'}`)
95
- if (parsedUrl.pathname !== this.callbackPath) {
96
- res.writeHead(404)
97
- res.end()
98
- return
99
- }
100
- const authCode = parsedUrl.searchParams.get('code') ?? undefined
101
- const state = parsedUrl.searchParams.get('state') ?? undefined
102
- this.validateAndRespond(authCode, state, res)
103
- }
104
-
105
- private validateAndRespond(
106
- authCode: string | undefined,
107
- state: string | undefined,
108
- res: ServerResponse,
109
- ): void {
110
- if (!authCode) {
111
- res.writeHead(400)
112
- res.end('Authorization code not found')
113
- this.reject(new Error('No authorization code received'))
114
- return
115
- }
116
- if (state !== this.expectedState) {
117
- res.writeHead(400)
118
- res.end('Invalid state parameter')
119
- this.reject(new Error('Invalid state parameter'))
120
- return
121
- }
122
- this.pendingResponse = res
123
- this.resolve(authCode)
124
- }
125
-
126
- private resolve(authorizationCode: string): void {
127
- if (this.promiseResolver) {
128
- this.promiseResolver(authorizationCode)
129
- this.promiseResolver = null
130
- this.promiseRejecter = null
131
- this.expectedState = null
132
- }
133
- }
134
-
135
- private reject(error: Error): void {
136
- if (this.promiseRejecter) {
137
- this.promiseRejecter(error)
138
- this.promiseResolver = null
139
- this.promiseRejecter = null
140
- this.expectedState = null
141
- }
142
- }
143
-
144
- close(): void {
145
- if (this.pendingResponse) this.handleErrorRedirect()
146
- this.localServer.removeAllListeners()
147
- this.localServer.close()
148
- this.expectedState = null
149
- this.port = 0
150
- }
151
- }