ethagent 0.2.1 → 1.0.1

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 (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -32
  3. package/bin/ethagent.js +11 -2
  4. package/package.json +25 -7
  5. package/src/app/FirstRun.tsx +412 -0
  6. package/src/app/hooks/useCancelRequest.ts +22 -0
  7. package/src/app/hooks/useDoublePress.ts +46 -0
  8. package/src/app/hooks/useExitOnCtrlC.ts +36 -0
  9. package/src/app/input/AppInputProvider.tsx +116 -0
  10. package/src/app/input/appInputParser.ts +279 -0
  11. package/src/app/keybindings/KeybindingProvider.tsx +134 -0
  12. package/src/app/keybindings/resolver.ts +42 -0
  13. package/src/app/keybindings/types.ts +26 -0
  14. package/src/chat/ChatBottomPane.tsx +280 -0
  15. package/src/chat/ChatInput.tsx +722 -0
  16. package/src/chat/ChatScreen.tsx +1575 -0
  17. package/src/chat/ContextLimitView.tsx +95 -0
  18. package/src/chat/ContinuityEditReviewView.tsx +48 -0
  19. package/src/chat/ConversationStack.tsx +47 -0
  20. package/src/chat/CopyPicker.tsx +52 -0
  21. package/src/chat/MessageList.tsx +609 -0
  22. package/src/chat/PermissionPrompt.tsx +153 -0
  23. package/src/chat/PermissionsView.tsx +159 -0
  24. package/src/chat/PlanApprovalView.tsx +91 -0
  25. package/src/chat/ResumeView.tsx +267 -0
  26. package/src/chat/RewindView.tsx +386 -0
  27. package/src/chat/SessionStatus.tsx +51 -0
  28. package/src/chat/TranscriptView.tsx +202 -0
  29. package/src/chat/chatInputState.ts +247 -0
  30. package/src/chat/chatPaste.ts +49 -0
  31. package/src/chat/chatScreenUtils.ts +187 -0
  32. package/src/chat/chatSessionState.ts +142 -0
  33. package/src/chat/chatTurnOrchestrator.ts +701 -0
  34. package/src/chat/commands.ts +673 -0
  35. package/src/chat/textCursor.ts +202 -0
  36. package/src/chat/toolResultDisplay.ts +8 -0
  37. package/src/chat/transcriptViewport.ts +247 -0
  38. package/src/cli/ResetConfirmView.tsx +61 -0
  39. package/src/cli/main.tsx +177 -0
  40. package/src/cli/preview.tsx +19 -0
  41. package/src/cli/reset.ts +106 -0
  42. package/src/identity/continuity/editor.ts +149 -0
  43. package/src/identity/continuity/envelope.ts +345 -0
  44. package/src/identity/continuity/history.ts +153 -0
  45. package/src/identity/continuity/privateEdit.ts +334 -0
  46. package/src/identity/continuity/publicSkills.ts +173 -0
  47. package/src/identity/continuity/snapshots.ts +183 -0
  48. package/src/identity/continuity/storage.ts +507 -0
  49. package/src/identity/crypto/backupEnvelope.ts +486 -0
  50. package/src/identity/crypto/eth.ts +137 -0
  51. package/src/identity/hub/IdentityHub.tsx +845 -0
  52. package/src/identity/hub/identityHubEffects.ts +1100 -0
  53. package/src/identity/hub/identityHubModel.ts +291 -0
  54. package/src/identity/hub/identityHubReducer.ts +209 -0
  55. package/src/identity/hub/screens/BusyScreen.tsx +26 -0
  56. package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +139 -0
  57. package/src/identity/hub/screens/CreateFlow.tsx +206 -0
  58. package/src/identity/hub/screens/DetailsScreen.tsx +64 -0
  59. package/src/identity/hub/screens/EditProfileFlow.tsx +145 -0
  60. package/src/identity/hub/screens/ErrorScreen.tsx +35 -0
  61. package/src/identity/hub/screens/IdentitySummary.tsx +70 -0
  62. package/src/identity/hub/screens/MenuScreen.tsx +117 -0
  63. package/src/identity/hub/screens/NetworkScreen.tsx +41 -0
  64. package/src/identity/hub/screens/RebackupStorageScreen.tsx +50 -0
  65. package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +85 -0
  66. package/src/identity/hub/screens/RestoreFlow.tsx +206 -0
  67. package/src/identity/hub/screens/StorageCredentialScreen.tsx +128 -0
  68. package/src/identity/hub/screens/WalletApprovalScreen.tsx +43 -0
  69. package/src/identity/profile/imagePicker.ts +180 -0
  70. package/src/identity/registry/erc8004.ts +1106 -0
  71. package/src/identity/registry/registryConfig.ts +69 -0
  72. package/src/identity/storage/ipfs.ts +212 -0
  73. package/src/identity/storage/pinataJwt.ts +53 -0
  74. package/src/identity/wallet/browserWallet.ts +393 -0
  75. package/src/identity/wallet/wallet-page/wallet.html +1082 -0
  76. package/src/mcp/approvals.ts +113 -0
  77. package/src/mcp/config.ts +235 -0
  78. package/src/mcp/manager.ts +541 -0
  79. package/src/mcp/names.ts +19 -0
  80. package/src/mcp/output.ts +96 -0
  81. package/src/models/ModelPicker.tsx +1446 -0
  82. package/src/models/catalog.ts +296 -0
  83. package/src/models/huggingface.ts +651 -0
  84. package/src/models/llamacpp.ts +810 -0
  85. package/src/models/llamacppPreflight.ts +150 -0
  86. package/src/models/modelDisplay.ts +105 -0
  87. package/src/models/modelPickerOptions.ts +421 -0
  88. package/src/models/modelRecommendation.ts +140 -0
  89. package/src/models/runtimeDetection.ts +81 -0
  90. package/src/models/uncensoredCatalog.ts +86 -0
  91. package/src/providers/anthropic.ts +259 -0
  92. package/src/providers/contracts.ts +62 -0
  93. package/src/providers/errors.ts +62 -0
  94. package/src/providers/gemini.ts +152 -0
  95. package/src/providers/openai-chat.ts +472 -0
  96. package/src/providers/registry.ts +42 -0
  97. package/src/providers/retry.ts +58 -0
  98. package/src/providers/sse.ts +93 -0
  99. package/src/runtime/compaction.ts +389 -0
  100. package/src/runtime/cwd.ts +43 -0
  101. package/src/runtime/sessionMode.ts +55 -0
  102. package/src/runtime/systemPrompt.ts +209 -0
  103. package/src/runtime/toolClaimGuards.ts +143 -0
  104. package/src/runtime/toolExecution.ts +304 -0
  105. package/src/runtime/toolIntent.ts +163 -0
  106. package/src/runtime/turn.ts +858 -0
  107. package/src/storage/atomicWrite.ts +68 -0
  108. package/src/storage/config.ts +189 -0
  109. package/src/storage/factoryReset.ts +130 -0
  110. package/src/storage/history.ts +58 -0
  111. package/src/storage/identity.ts +99 -0
  112. package/src/storage/permissions.ts +76 -0
  113. package/src/storage/rewind.ts +246 -0
  114. package/src/storage/secrets.ts +181 -0
  115. package/src/storage/sessionExport.ts +49 -0
  116. package/src/storage/sessions.ts +482 -0
  117. package/src/tools/bashSafety.ts +174 -0
  118. package/src/tools/bashTool.ts +140 -0
  119. package/src/tools/changeDirectoryTool.ts +213 -0
  120. package/src/tools/contracts.ts +179 -0
  121. package/src/tools/deleteFileTool.ts +111 -0
  122. package/src/tools/editTool.ts +160 -0
  123. package/src/tools/editUtils.ts +170 -0
  124. package/src/tools/listDirectoryTool.ts +55 -0
  125. package/src/tools/mcpResourceTools.ts +95 -0
  126. package/src/tools/permissionRules.ts +85 -0
  127. package/src/tools/privateContinuityEditTool.ts +178 -0
  128. package/src/tools/privateContinuityReadTool.ts +107 -0
  129. package/src/tools/readTool.ts +85 -0
  130. package/src/tools/registry.ts +67 -0
  131. package/src/tools/writeFileTool.ts +142 -0
  132. package/src/ui/BrandSplash.tsx +193 -0
  133. package/src/ui/ProgressBar.tsx +34 -0
  134. package/src/ui/Select.tsx +143 -0
  135. package/src/ui/Spinner.tsx +269 -0
  136. package/src/ui/Surface.tsx +47 -0
  137. package/src/ui/TextInput.tsx +97 -0
  138. package/src/ui/theme.ts +59 -0
  139. package/src/utils/clipboard.ts +216 -0
  140. package/src/utils/markdownSegments.ts +51 -0
  141. package/src/utils/messages.ts +35 -0
  142. package/src/utils/withRetry.ts +280 -0
  143. package/src/cli.tsx +0 -147
@@ -0,0 +1,845 @@
1
+ import React, { useEffect, useReducer, useState } from 'react'
2
+ import { Text } from 'ink'
3
+ import { theme } from '../../ui/theme.js'
4
+ import { type EthagentConfig, type EthagentIdentity, type SelectableNetwork } from '../../storage/config.js'
5
+ import { clearIdentity, setTokenIdentity } from '../../storage/identity.js'
6
+ import { copyToClipboard } from '../../utils/clipboard.js'
7
+ import { catFromIpfs, DEFAULT_IPFS_API_URL } from '../storage/ipfs.js'
8
+ import { openImageFilePicker } from '../profile/imagePicker.js'
9
+ import { hasPinataJwt, clearPinataJwt, savePinataJwt } from '../storage/pinataJwt.js'
10
+ import { registryConfigFromConfig } from '../registry/registryConfig.js'
11
+ import { identityHubErrorView, isRegistrationPreflightError, pinataErrorText } from './identityHubModel.js'
12
+ import { identityHubReducer, type ProfileUpdates, type Step } from './identityHubReducer.js'
13
+ import {
14
+ runCreatePreflight,
15
+ runCreateSigning,
16
+ runRestoreConnectWallet,
17
+ runRestoreDiscover,
18
+ runRestoreTokenIdSubmit,
19
+ runRestoreFetch,
20
+ runRestoreAuthorize,
21
+ runRegistrySubmit,
22
+ runRestoreRegistrySubmit,
23
+ runStorageSubmit,
24
+ runRebackupPreflight,
25
+ runRebackupSigning,
26
+ runRebackupStorageSubmit,
27
+ runPublicProfilePreflight,
28
+ runPublicProfileSigning,
29
+ runPublicProfileStorageSubmit,
30
+ runRecoveryRefetch,
31
+ isAgentTokenIdRequiredError,
32
+ type EffectCallbacks,
33
+ type RestoreProgress,
34
+ } from './identityHubEffects.js'
35
+ import {
36
+ continuityVaultRef,
37
+ continuityVaultStatus,
38
+ continuityWorkingTreeStatus,
39
+ ensurePublicSkillsFile,
40
+ type ContinuityWorkingTreeStatus,
41
+ } from '../continuity/storage.js'
42
+ import { openFileInEditor } from '../continuity/editor.js'
43
+ import { listPublishedContinuitySnapshots } from '../continuity/snapshots.js'
44
+ import type { BrowserWalletReady } from '../wallet/browserWallet.js'
45
+ import { MenuScreen } from './screens/MenuScreen.js'
46
+ import { CreateFlow } from './screens/CreateFlow.js'
47
+ import { RestoreFlow } from './screens/RestoreFlow.js'
48
+ import { NetworkScreen } from './screens/NetworkScreen.js'
49
+ import { DetailsScreen } from './screens/DetailsScreen.js'
50
+ import { ErrorScreen } from './screens/ErrorScreen.js'
51
+ import { WalletApprovalScreen } from './screens/WalletApprovalScreen.js'
52
+ import { RebackupStorageScreen } from './screens/RebackupStorageScreen.js'
53
+ import { BusyScreen } from './screens/BusyScreen.js'
54
+ import { EditProfileFlow } from './screens/EditProfileFlow.js'
55
+ import { StorageCredentialScreen } from './screens/StorageCredentialScreen.js'
56
+ import {
57
+ PrivateContinuityScreen,
58
+ PublicSkillsScreen,
59
+ } from './screens/ContinuityDashboardScreen.js'
60
+ import { RecoveryConfirmScreen } from './screens/RecoveryConfirmScreen.js'
61
+ import { chainIdForNetwork, erc8004ConfigForSupportedChain, type Erc8004RegistryConfig } from '../registry/erc8004.js'
62
+
63
+ const MIN_BUSY_ERROR_MS = 2000
64
+
65
+ function isWalletCancelled(err: unknown): boolean {
66
+ if (!err) return false
67
+ const message = err instanceof Error ? err.message : String(err)
68
+ return /browser wallet request was cancelled/i.test(message)
69
+ || /user rejected/i.test(message)
70
+ }
71
+
72
+ function isStorageError(err: unknown): boolean {
73
+ const message = err instanceof Error ? err.message : String(err)
74
+ return /pinata|ipfs|pin|storage/i.test(message)
75
+ }
76
+
77
+ function waitForMinimumBusyTime(startedAt: number): Promise<void> {
78
+ const remaining = MIN_BUSY_ERROR_MS - (Date.now() - startedAt)
79
+ return remaining > 0
80
+ ? new Promise(resolve => setTimeout(resolve, remaining))
81
+ : Promise.resolve()
82
+ }
83
+
84
+ export type IdentityHubResult =
85
+ | { kind: 'token'; identity: EthagentIdentity }
86
+ | { kind: 'updated'; config: EthagentConfig; message: string }
87
+ | { kind: 'skip' }
88
+ | { kind: 'cancel' }
89
+
90
+ type IdentityHubProps = {
91
+ mode: 'first-run' | 'manage'
92
+ config?: EthagentConfig
93
+ cwd?: string
94
+ initialAction?: IdentityHubInitialAction
95
+ onComplete: (result: IdentityHubResult) => void
96
+ onConfigChange?: (config: EthagentConfig) => void
97
+ }
98
+
99
+ export type IdentityHubInitialAction = 'create' | 'load' | 'settings' | 'save-snapshot'
100
+
101
+ export const IdentityHub: React.FC<IdentityHubProps> = ({ mode, config, initialAction, onComplete, onConfigChange }) => {
102
+ const identity = config?.identity
103
+ const [step, dispatch] = useReducer(identityHubReducer, initialStepForAction(initialAction, config))
104
+ const [walletSession, setWalletSession] = useState<BrowserWalletReady | null>(null)
105
+ const [restoreProgress, setRestoreProgress] = useState<RestoreProgress | null>(null)
106
+ const [jwtSaved, setJwtSaved] = useState<boolean>(false)
107
+ const [copyNotice, setCopyNotice] = useState<string | null>(null)
108
+ const [continuityReady, setContinuityReady] = useState<boolean>(false)
109
+ const [workingStatus, setWorkingStatus] = useState<ContinuityWorkingTreeStatus | null>(null)
110
+ const canRebackup = Boolean(identity?.agentId && (identity?.identityRegistryAddress || config?.erc8004?.identityRegistryAddress))
111
+
112
+ const setStep = (s: Step) => dispatch({ type: 'preflightResolved', step: s })
113
+ const back = () => dispatch({ type: 'back', from: step })
114
+
115
+ useEffect(() => { setWalletSession(null) }, [step.kind])
116
+ useEffect(() => {
117
+ if (step.kind !== 'restore-authorizing') setRestoreProgress(null)
118
+ }, [step.kind])
119
+
120
+ useEffect(() => {
121
+ let cancelled = false
122
+ hasPinataJwt().then(v => { if (!cancelled) setJwtSaved(v) }).catch(() => {})
123
+ return () => { cancelled = true }
124
+ }, [step.kind])
125
+
126
+ useEffect(() => { setCopyNotice(null) }, [step.kind])
127
+
128
+ useEffect(() => {
129
+ let cancelled = false
130
+ if (!identity) {
131
+ setContinuityReady(false)
132
+ return
133
+ }
134
+ if (!step.kind.startsWith('continuity') && step.kind !== 'details' && step.kind !== 'menu') return
135
+ continuityVaultStatus(identity)
136
+ .then(status => { if (!cancelled) setContinuityReady(status.ready) })
137
+ .catch(() => { if (!cancelled) setContinuityReady(false) })
138
+ return () => { cancelled = true }
139
+ }, [identity, step.kind])
140
+
141
+ const completeTokenIdentity = async (nextIdentity: EthagentIdentity, message: string): Promise<void> => {
142
+ if (mode === 'first-run' || !config) {
143
+ onComplete({ kind: 'token', identity: nextIdentity })
144
+ return
145
+ }
146
+ const nextConfig = await setTokenIdentity(config, nextIdentity)
147
+ onComplete({ kind: 'updated', config: nextConfig, message })
148
+ }
149
+
150
+ const callbacks: EffectCallbacks = {
151
+ onStep: setStep,
152
+ onWalletReady: setWalletSession,
153
+ onIdentityComplete: completeTokenIdentity,
154
+ onRestoreProgress: setRestoreProgress,
155
+ }
156
+
157
+ const errorStep = (err: unknown, backStep: Step): void => {
158
+ setStep({ kind: 'error', error: identityHubErrorView(err), back: backStep })
159
+ }
160
+
161
+ const handleStepError = (err: unknown, backStep: Step, softCancel: Step = backStep): void => {
162
+ if (isWalletCancelled(err)) {
163
+ setStep(softCancel)
164
+ return
165
+ }
166
+ errorStep(err, backStep)
167
+ }
168
+
169
+ const resolveRegistryForIdentity = (target: EthagentIdentity): Erc8004RegistryConfig | null => {
170
+ const resolution = registryConfigFromConfig(config)
171
+ if (target.chainId && target.identityRegistryAddress) {
172
+ return {
173
+ chainId: target.chainId,
174
+ rpcUrl: target.rpcUrl ?? resolution.defaultRpcUrl,
175
+ identityRegistryAddress: target.identityRegistryAddress as `0x${string}`,
176
+ }
177
+ }
178
+ if (resolution.config) return resolution.config
179
+ return null
180
+ }
181
+
182
+ const triggerRebackup = (backStep: Step, profileUpdates?: ProfileUpdates): void => {
183
+ if (!identity) return
184
+ const registry = resolveRegistryForIdentity(identity)
185
+ if (!registry) {
186
+ errorStep(new Error('no agent registry configured for this identity'), backStep)
187
+ return
188
+ }
189
+ runRebackupPreflight(identity, registry, callbacks, profileUpdates, backStep)
190
+ .catch((err: unknown) => errorStep(err, backStep))
191
+ }
192
+
193
+ const triggerPublicProfilePublish = (backStep: Step, profileUpdates?: ProfileUpdates): void => {
194
+ if (!identity) return
195
+ const registry = resolveRegistryForIdentity(identity)
196
+ if (!registry) {
197
+ errorStep(new Error('no agent registry configured for this identity'), backStep)
198
+ return
199
+ }
200
+ runPublicProfilePreflight(identity, registry, callbacks, profileUpdates, backStep)
201
+ .catch((err: unknown) => errorStep(err, backStep))
202
+ }
203
+
204
+ useEffect(() => {
205
+ if (step.kind !== 'rebackup-start') return
206
+ triggerRebackup(step.back)
207
+ }, [step])
208
+
209
+ useEffect(() => {
210
+ let cancelled = false
211
+ if (!identity || step.kind !== 'menu') return
212
+
213
+ const checkStatus = async () => {
214
+ try {
215
+ const [latest] = await listPublishedContinuitySnapshots(identity, 1)
216
+ const status = await continuityWorkingTreeStatus(identity, latest)
217
+ if (cancelled) return
218
+ setWorkingStatus(status)
219
+ } catch {
220
+ if (cancelled) return
221
+ setWorkingStatus(null)
222
+ }
223
+ }
224
+
225
+ void checkStatus()
226
+
227
+ return () => {
228
+ cancelled = true
229
+ }
230
+ }, [identity, step.kind])
231
+
232
+ useEffect(() => {
233
+ if (step.kind !== 'create-preflight') return
234
+ let cancelled = false
235
+ const startedAt = Date.now()
236
+ runCreatePreflight(step, config, callbacks)
237
+ .catch(async (err: unknown) => {
238
+ await waitForMinimumBusyTime(startedAt)
239
+ if (!cancelled) errorStep(err, { kind: 'create-network', name: step.name, description: step.description })
240
+ })
241
+ return () => { cancelled = true }
242
+ }, [step])
243
+
244
+ useEffect(() => {
245
+ if (step.kind !== 'create-signing') return
246
+ let cancelled = false
247
+ const backStep: Step = { kind: 'create-network', name: step.name, description: step.description }
248
+ runCreateSigning(step, callbacks)
249
+ .catch((err: unknown) => {
250
+ if (cancelled) return
251
+ if (isRegistrationPreflightError(err)) {
252
+ errorStep(err, backStep)
253
+ return
254
+ }
255
+ if (isStorageError(err)) {
256
+ setStep({
257
+ kind: 'create-storage',
258
+ name: step.name,
259
+ description: step.description,
260
+ registry: step.registry,
261
+ error: pinataErrorText(err),
262
+ pinataJwt: step.pinataJwt,
263
+ })
264
+ return
265
+ }
266
+ handleStepError(err, backStep)
267
+ })
268
+ return () => { cancelled = true }
269
+ }, [step])
270
+
271
+ useEffect(() => {
272
+ if (step.kind !== 'restore-discovering') return
273
+ let cancelled = false
274
+ const startedAt = Date.now()
275
+ runRestoreDiscover(step, config, callbacks)
276
+ .catch(async (err: unknown) => {
277
+ await waitForMinimumBusyTime(startedAt)
278
+ if (cancelled) return
279
+ if (isAgentTokenIdRequiredError(err)) {
280
+ setStep({ kind: 'restore-token-id', ownerHandle: err.ownerAddress, registry: err.registry, error: err.message, purpose: step.purpose })
281
+ return
282
+ }
283
+ errorStep(err, { kind: 'restore-network', ownerHandle: step.ownerHandle, purpose: step.purpose })
284
+ })
285
+ return () => { cancelled = true }
286
+ }, [step])
287
+
288
+ useEffect(() => {
289
+ if (step.kind !== 'restore-wallet') return
290
+ let cancelled = false
291
+ runRestoreConnectWallet(step, callbacks)
292
+ .catch((err: unknown) => { if (!cancelled) handleStepError(err, { kind: 'restore-owner', purpose: step.purpose }) })
293
+ return () => { cancelled = true }
294
+ }, [step])
295
+
296
+ useEffect(() => {
297
+ if (step.kind !== 'restore-fetching') return
298
+ let cancelled = false
299
+ const startedAt = Date.now()
300
+ runRestoreFetch(step, callbacks)
301
+ .catch(async (err: unknown) => {
302
+ await waitForMinimumBusyTime(startedAt)
303
+ if (!cancelled) errorStep(err, { kind: 'restore-network', ownerHandle: step.candidate.ownerAddress, purpose: step.purpose })
304
+ })
305
+ return () => { cancelled = true }
306
+ }, [step])
307
+
308
+ useEffect(() => {
309
+ if (step.kind !== 'restore-authorizing') return
310
+ let cancelled = false
311
+ runRestoreAuthorize(step, callbacks)
312
+ .catch((err: unknown) => {
313
+ if (!cancelled) handleStepError(err, { kind: 'restore-network', ownerHandle: step.candidate.ownerAddress, purpose: step.purpose })
314
+ })
315
+ return () => { cancelled = true }
316
+ }, [step])
317
+
318
+ useEffect(() => {
319
+ if (step.kind !== 'rebackup-signing') return
320
+ let cancelled = false
321
+ runRebackupSigning(step, callbacks)
322
+ .catch((err: unknown) => {
323
+ if (cancelled) return
324
+ if (isStorageError(err)) {
325
+ setStep({
326
+ kind: 'rebackup-storage',
327
+ identity: step.identity,
328
+ registry: step.registry,
329
+ error: pinataErrorText(err),
330
+ pinataJwt: step.pinataJwt,
331
+ profileUpdates: step.profileUpdates,
332
+ returnTo: step.returnTo,
333
+ })
334
+ return
335
+ }
336
+ handleStepError(err, step.returnTo ?? { kind: 'menu' })
337
+ })
338
+ return () => { cancelled = true }
339
+ }, [step])
340
+
341
+ useEffect(() => {
342
+ if (step.kind !== 'public-profile-signing') return
343
+ let cancelled = false
344
+ runPublicProfileSigning(step, callbacks)
345
+ .catch((err: unknown) => {
346
+ if (cancelled) return
347
+ if (isStorageError(err)) {
348
+ setStep({
349
+ kind: 'public-profile-storage',
350
+ identity: step.identity,
351
+ registry: step.registry,
352
+ error: pinataErrorText(err),
353
+ pinataJwt: step.pinataJwt,
354
+ profileUpdates: step.profileUpdates,
355
+ returnTo: step.returnTo,
356
+ })
357
+ return
358
+ }
359
+ handleStepError(err, step.returnTo ?? { kind: 'continuity-public' })
360
+ })
361
+ return () => { cancelled = true }
362
+ }, [step])
363
+
364
+
365
+
366
+ useEffect(() => {
367
+ if (step.kind !== 'recovery-refetching') return
368
+ let cancelled = false
369
+ runRecoveryRefetch(step.identity, step.registry, callbacks)
370
+ .then(() => {
371
+ if (!cancelled) setContinuityReady(true)
372
+ })
373
+ .catch((err: unknown) => {
374
+ if (!cancelled) handleStepError(err, { kind: 'menu' })
375
+ })
376
+ return () => { cancelled = true }
377
+ }, [step])
378
+
379
+
380
+ const openContinuityFile = async (kind: 'soul' | 'memory' | 'skills'): Promise<void> => {
381
+ if (!identity) return
382
+ try {
383
+ if (kind === 'skills') {
384
+ await ensurePublicSkillsFile(identity, {
385
+ fallback: () => readPublishedPublicSkills(identity),
386
+ })
387
+ }
388
+ const ref = continuityVaultRef(identity)
389
+ const file = kind === 'soul' ? ref.soulPath : kind === 'memory' ? ref.memoryPath : ref.publicSkillsPath
390
+ const result = await openFileInEditor(file)
391
+ const message = result.ok
392
+ ? `opened ${kind === 'soul' ? 'SOUL.md' : kind === 'memory' ? 'MEMORY.md' : 'skills.json'} with ${result.method}.`
393
+ : `open failed: ${result.error}`
394
+ setStep(kind === 'skills'
395
+ ? { kind: 'continuity-public', notice: message }
396
+ : { kind: 'continuity-private', notice: message })
397
+ } catch (err: unknown) {
398
+ errorStep(err, kind === 'skills' ? { kind: 'continuity-public' } : { kind: 'continuity-private' })
399
+ }
400
+ }
401
+
402
+ const footer = <Text color={theme.dim}>enter select · esc back</Text>
403
+
404
+ if (step.kind === 'menu') {
405
+ return (
406
+ <MenuScreen
407
+ mode={mode}
408
+ config={config}
409
+ identity={identity}
410
+ workingStatus={workingStatus}
411
+ canRebackup={canRebackup}
412
+ footer={footer}
413
+ onCreate={() => {
414
+ if (identity) setStep({ kind: 'replace-confirm', next: 'create' })
415
+ else setStep({ kind: 'create-name' })
416
+ }}
417
+ onLoad={() => {
418
+ setCopyNotice(null)
419
+ setStep({ kind: 'restore-wallet', purpose: identity ? 'switch' : 'restore' })
420
+ }}
421
+ onBackupNow={() => setStep({ kind: 'rebackup-confirm' })}
422
+ onRefetchLatest={() => setStep({ kind: 'recovery-refetch-confirm' })}
423
+ onPublicProfile={() => setStep({ kind: 'continuity-public' })}
424
+ onPrivateMemory={() => setStep({ kind: 'continuity-private' })}
425
+ onCopyValues={() => setStep({ kind: 'details' })}
426
+ onStorageCredential={() => setStep({ kind: 'storage-credential' })}
427
+ onSkip={() => onComplete({ kind: 'skip' })}
428
+ onCancel={() => onComplete({ kind: 'cancel' })}
429
+ />
430
+ )
431
+ }
432
+
433
+ if (isCreateStep(step)) {
434
+ return (
435
+ <CreateFlow
436
+ step={step}
437
+ walletSession={walletSession}
438
+ onSetStep={setStep}
439
+ onNameSubmit={name => setStep({ kind: 'create-description', name })}
440
+ onDescriptionSubmit={(name, description) => setStep({ kind: 'create-network', name, description })}
441
+ onRegistrySubmit={async value => {
442
+ if (step.kind !== 'create-registry') return
443
+ try {
444
+ await runRegistrySubmit(value, step, config, onConfigChange, callbacks)
445
+ } catch (err: unknown) {
446
+ setStep({ kind: 'create-registry', name: step.name, description: step.description, resolution: step.resolution, error: (err as Error).message })
447
+ }
448
+ }}
449
+ onStorageSubmit={async input => {
450
+ if (step.kind !== 'create-storage') return
451
+ try {
452
+ await runStorageSubmit(input, step, callbacks)
453
+ } catch (err: unknown) {
454
+ setStep({
455
+ kind: 'create-storage',
456
+ name: step.name,
457
+ description: step.description,
458
+ registry: step.registry,
459
+ error: (err as Error).message,
460
+ pinataJwt: step.pinataJwt,
461
+ })
462
+ }
463
+ }}
464
+ onStorageError={error => {
465
+ if (step.kind !== 'create-storage') return
466
+ setStep({ ...step, error })
467
+ }}
468
+ onBack={back}
469
+ onMenu={() => setStep({ kind: 'menu' })}
470
+ />
471
+ )
472
+ }
473
+
474
+ if (step.kind === 'create-network') {
475
+ return (
476
+ <NetworkScreen
477
+ subtitle="Choose where to create this agent."
478
+ footer={footer}
479
+ onSelect={(network: SelectableNetwork) => {
480
+ setStep({ kind: 'create-preflight', name: step.name, description: step.description, network })
481
+ }}
482
+ onCancel={back}
483
+ />
484
+ )
485
+ }
486
+
487
+ if (step.kind === 'restore-network') {
488
+ return (
489
+ <NetworkScreen
490
+ subtitle="Choose a network to search for your agents."
491
+ footer={footer}
492
+ onSelect={(network: SelectableNetwork) => {
493
+ try {
494
+ const registry = erc8004ConfigForSupportedChain(chainIdForNetwork(network))
495
+ setStep({ kind: 'restore-discovering', ownerHandle: step.ownerHandle, registry, purpose: step.purpose })
496
+ } catch (err: unknown) {
497
+ errorStep(err, { kind: 'restore-network', ownerHandle: step.ownerHandle, purpose: step.purpose })
498
+ }
499
+ }}
500
+ onCancel={back}
501
+ />
502
+ )
503
+ }
504
+
505
+ if (isRestoreStep(step)) {
506
+ return (
507
+ <RestoreFlow
508
+ step={step}
509
+ config={config}
510
+ walletSession={walletSession}
511
+ restoreProgress={restoreProgress}
512
+ onConnectWallet={() => {
513
+ const purpose = step.kind === 'restore-owner' ? step.purpose : undefined
514
+ setStep({ kind: 'restore-wallet', purpose })
515
+ }}
516
+ onRestoreRegistrySubmit={async value => {
517
+ if (step.kind !== 'restore-registry') return
518
+ try {
519
+ await runRestoreRegistrySubmit(value, step, config, onConfigChange, callbacks)
520
+ } catch (err: unknown) {
521
+ setStep({ kind: 'restore-registry', ownerHandle: step.ownerHandle, error: (err as Error).message, purpose: step.purpose })
522
+ }
523
+ }}
524
+ onTokenIdSubmit={async value => {
525
+ if (step.kind !== 'restore-token-id') return
526
+ try {
527
+ await runRestoreTokenIdSubmit(value, step, callbacks)
528
+ } catch (err: unknown) {
529
+ setStep({ ...step, error: (err as Error).message })
530
+ }
531
+ }}
532
+ onTokenSelect={value => {
533
+ if (step.kind !== 'restore-select-token') return
534
+ const candidate = step.candidates.find(item => item.agentId.toString() === value)
535
+ if (!candidate?.backup?.cid) return
536
+ setStep({ kind: 'restore-fetching', cid: candidate.backup.cid, apiUrl: DEFAULT_IPFS_API_URL, candidate, purpose: step.purpose })
537
+ }}
538
+ onBack={back}
539
+ />
540
+ )
541
+ }
542
+
543
+ if (step.kind === 'details') {
544
+ return (
545
+ <DetailsScreen
546
+ identity={identity}
547
+ config={config}
548
+ copyNotice={copyNotice}
549
+ footer={footer}
550
+ onCopy={async (label, value) => {
551
+ const result = await copyToClipboard(value)
552
+ setCopyNotice(result.ok ? `copied ${label} via ${result.method}.` : `copy failed: ${result.error}`)
553
+ setStep({ kind: 'details' })
554
+ }}
555
+ onBack={back}
556
+ />
557
+ )
558
+ }
559
+
560
+ const openPublicProfileEdit = (backStep: Step): void => {
561
+ if (!identity) return
562
+ const registry = resolveRegistryForIdentity(identity)
563
+ if (!registry) {
564
+ errorStep(new Error('no agent registry configured for this identity'), backStep)
565
+ return
566
+ }
567
+ setStep({ kind: 'edit-profile-name', identity, registry, returnTo: backStep })
568
+ }
569
+
570
+ if (step.kind === 'rebackup-confirm') {
571
+ return (
572
+ <RecoveryConfirmScreen
573
+ mode="publish"
574
+ workingStatus={workingStatus}
575
+ footer={footer}
576
+ onConfirm={() => triggerRebackup({ kind: 'menu' })}
577
+ onBack={back}
578
+ />
579
+ )
580
+ }
581
+
582
+ if (step.kind === 'recovery-refetch-confirm') {
583
+ return (
584
+ <RecoveryConfirmScreen
585
+ mode="refetch"
586
+ workingStatus={workingStatus}
587
+ footer={footer}
588
+ onConfirm={() => {
589
+ if (!identity) return
590
+ const registry = resolveRegistryForIdentity(identity)
591
+ if (!registry) {
592
+ errorStep(new Error('no agent registry configured for this identity'), { kind: 'menu' })
593
+ return
594
+ }
595
+ setStep({ kind: 'recovery-refetching', identity, registry })
596
+ }}
597
+ onBack={back}
598
+ />
599
+ )
600
+ }
601
+
602
+ if (step.kind === 'recovery-refetching') {
603
+ return (
604
+ <WalletApprovalScreen
605
+ title="Refetch Latest Snapshot"
606
+ subtitle="Wallet approval decrypts the latest published snapshot and overwrites local SOUL.md, MEMORY.md, and skills.json."
607
+ walletSession={walletSession}
608
+ label={restoreProgress?.label ?? 'fetching latest snapshot from chain...'}
609
+ onCancel={() => setStep({ kind: 'menu' })}
610
+ />
611
+ )
612
+ }
613
+
614
+ if (step.kind === 'continuity-private') {
615
+ return (
616
+ <PrivateContinuityScreen
617
+ identity={identity}
618
+ config={config}
619
+ ready={continuityReady}
620
+ notice={step.notice}
621
+ canBackup={canRebackup}
622
+ footer={footer}
623
+ onOpenSoul={() => { void openContinuityFile('soul') }}
624
+ onOpenMemory={() => { void openContinuityFile('memory') }}
625
+ onBackup={() => triggerRebackup({ kind: 'continuity-private' })}
626
+ onBack={back}
627
+ />
628
+ )
629
+ }
630
+
631
+ if (step.kind === 'continuity-public') {
632
+ return (
633
+ <PublicSkillsScreen
634
+ identity={identity}
635
+ config={config}
636
+ ready={continuityReady}
637
+ notice={step.notice}
638
+ canPublish={canRebackup}
639
+ footer={footer}
640
+ onEditProfile={() => openPublicProfileEdit({ kind: 'continuity-public' })}
641
+ onOpenSkills={() => { void openContinuityFile('skills') }}
642
+ onPublish={() => triggerPublicProfilePublish({ kind: 'continuity-public' })}
643
+ onBack={back}
644
+ />
645
+ )
646
+ }
647
+
648
+ if (step.kind === 'storage-credential' || step.kind === 'storage-credential-input' || step.kind === 'storage-credential-forget-confirm') {
649
+ return (
650
+ <StorageCredentialScreen
651
+ step={step}
652
+ hasCredential={jwtSaved}
653
+ footer={footer}
654
+ onEdit={() => setStep({ kind: 'storage-credential-input' })}
655
+ onForget={() => setStep({ kind: 'storage-credential-forget-confirm' })}
656
+ onConfirmForget={async () => {
657
+ await clearPinataJwt().catch(() => {})
658
+ setJwtSaved(false)
659
+ setCopyNotice('IPFS storage credential removed.')
660
+ setStep({ kind: 'menu' })
661
+ }}
662
+ onSubmit={async input => {
663
+ try {
664
+ await savePinataJwt(input)
665
+ setJwtSaved(true)
666
+ setCopyNotice('IPFS storage credential saved.')
667
+ setStep({ kind: 'menu' })
668
+ } catch (err: unknown) {
669
+ setStep({ kind: 'storage-credential-input', error: (err as Error).message })
670
+ }
671
+ }}
672
+ onCancel={back}
673
+ />
674
+ )
675
+ }
676
+
677
+ if (step.kind === 'edit-profile-name' || step.kind === 'edit-profile-description' || step.kind === 'edit-profile-image') {
678
+ return (
679
+ <EditProfileFlow
680
+ step={step}
681
+ onNameSubmit={name => {
682
+ if (step.kind !== 'edit-profile-name') return
683
+ setStep({ kind: 'edit-profile-description', identity: step.identity, registry: step.registry, name, returnTo: step.returnTo })
684
+ }}
685
+ onDescriptionSubmit={description => {
686
+ if (step.kind !== 'edit-profile-description') return
687
+ setStep({ kind: 'edit-profile-image', identity: step.identity, registry: step.registry, name: step.name, description, returnTo: step.returnTo })
688
+ }}
689
+ onImageSubmit={imagePath => {
690
+ if (step.kind !== 'edit-profile-image') return
691
+ const updates: ProfileUpdates = { name: step.name, description: step.description, ...(imagePath ? { imagePath } : {}) }
692
+ runPublicProfilePreflight(step.identity, step.registry, callbacks, updates, step.returnTo ?? { kind: 'continuity-public' })
693
+ .catch((err: unknown) => errorStep(err, step.returnTo ?? { kind: 'continuity-public' }))
694
+ }}
695
+ onImagePick={() => {
696
+ if (step.kind !== 'edit-profile-image') return
697
+ const imageStep = step
698
+ void openImageFilePicker()
699
+ .then(result => {
700
+ if (!result.ok) {
701
+ setStep({ ...imageStep, error: result.cancelled ? 'image selection cancelled.' : `${result.error}. enter a path manually if needed.` })
702
+ return
703
+ }
704
+ const updates: ProfileUpdates = { name: imageStep.name, description: imageStep.description, imagePath: result.file }
705
+ runPublicProfilePreflight(imageStep.identity, imageStep.registry, callbacks, updates, imageStep.returnTo ?? { kind: 'continuity-public' })
706
+ .catch((err: unknown) => errorStep(err, imageStep.returnTo ?? { kind: 'continuity-public' }))
707
+ })
708
+ .catch((err: unknown) => {
709
+ setStep({ ...imageStep, error: `${(err as Error).message}. enter a path manually if needed.` })
710
+ })
711
+ }}
712
+ onBack={back}
713
+ onMenu={() => setStep(step.returnTo ?? { kind: 'continuity-public' })}
714
+ />
715
+ )
716
+ }
717
+
718
+ if (step.kind === 'rebackup-signing') {
719
+ return (
720
+ <WalletApprovalScreen
721
+ title="Approve Encrypted Snapshot"
722
+ subtitle="Signs, encrypts private SOUL.md and MEMORY.md, pins them, and refreshes recovery metadata."
723
+ walletSession={walletSession}
724
+ label="waiting for wallet approval..."
725
+ onCancel={() => setStep(step.returnTo ?? { kind: 'menu' })}
726
+ />
727
+ )
728
+ }
729
+
730
+ if (step.kind === 'public-profile-signing') {
731
+ return (
732
+ <WalletApprovalScreen
733
+ title="Approve Public Profile"
734
+ subtitle="Pins skills.json and the Agent Card, then updates tokenURI. Private files are not read."
735
+ walletSession={walletSession}
736
+ label="waiting for wallet approval..."
737
+ onCancel={() => setStep(step.returnTo ?? { kind: 'continuity-public' })}
738
+ />
739
+ )
740
+ }
741
+
742
+ if (step.kind === 'rebackup-start') {
743
+ return (
744
+ <BusyScreen
745
+ title="Identity Hub"
746
+ label="preparing encrypted snapshot..."
747
+ onCancel={back}
748
+ />
749
+ )
750
+ }
751
+
752
+
753
+
754
+
755
+ if (step.kind === 'rebackup-storage' || step.kind === 'public-profile-storage') {
756
+ return (
757
+ <RebackupStorageScreen
758
+ step={step}
759
+ footer={footer}
760
+ onSubmit={async input => {
761
+ try {
762
+ if (step.kind === 'rebackup-storage') {
763
+ await runRebackupStorageSubmit(input, step, callbacks)
764
+ } else {
765
+ await runPublicProfileStorageSubmit(input, step, callbacks)
766
+ }
767
+ } catch (err: unknown) {
768
+ setStep({ ...step, error: (err as Error).message })
769
+ }
770
+ }}
771
+ onCancel={back}
772
+ />
773
+ )
774
+ }
775
+
776
+ if (step.kind === 'restore-wallet') {
777
+ return (
778
+ <WalletApprovalScreen
779
+ title="Connect Wallet"
780
+ subtitle="Select the wallet that owns the agent you want to load."
781
+ walletSession={walletSession}
782
+ label="waiting for wallet..."
783
+ onCancel={back}
784
+ />
785
+ )
786
+ }
787
+
788
+ if (step.kind === 'busy') {
789
+ return (
790
+ <BusyScreen
791
+ title="Identity Hub"
792
+ label={step.label}
793
+ onCancel={back}
794
+ />
795
+ )
796
+ }
797
+
798
+ if (step.kind === 'error') {
799
+ return (
800
+ <ErrorScreen
801
+ error={step.error}
802
+ back={step.back}
803
+ footer={footer}
804
+ onBack={backStep => setStep(backStep)}
805
+ onClose={() => onComplete({ kind: 'cancel' })}
806
+ />
807
+ )
808
+ }
809
+
810
+ return null
811
+ }
812
+
813
+ async function readPublishedPublicSkills(identity: EthagentIdentity): Promise<string> {
814
+ const cid = identity.publicSkills?.cid
815
+ if (!cid) throw new Error('no published public skills CID')
816
+ return new TextDecoder().decode(await catFromIpfs(
817
+ identity.backup?.ipfsApiUrl ?? DEFAULT_IPFS_API_URL,
818
+ cid,
819
+ ))
820
+ }
821
+
822
+ function isCreateStep(step: Step): step is Extract<Step, { kind: 'replace-confirm' | 'create-name' | 'create-description' | 'create-preflight' | 'create-registry' | 'create-signing' | 'create-storage' }> {
823
+ return step.kind === 'replace-confirm'
824
+ || step.kind === 'create-name'
825
+ || step.kind === 'create-description'
826
+ || step.kind === 'create-preflight'
827
+ || step.kind === 'create-registry'
828
+ || step.kind === 'create-signing'
829
+ || step.kind === 'create-storage'
830
+ }
831
+
832
+ function isRestoreStep(step: Step): step is Exclude<Extract<Step, { kind: `restore-${string}` }>, { kind: 'restore-wallet' | 'restore-network' }> {
833
+ return step.kind.startsWith('restore-') && step.kind !== 'restore-wallet' && step.kind !== 'restore-network'
834
+ }
835
+
836
+ function initialStepForAction(
837
+ action: IdentityHubInitialAction | undefined,
838
+ config: EthagentConfig | undefined,
839
+ ): Step {
840
+ if (action === 'create') return config?.identity ? { kind: 'replace-confirm', next: 'create' } : { kind: 'create-name' }
841
+ if (action === 'load') return { kind: 'restore-wallet', purpose: config?.identity ? 'switch' : 'restore' }
842
+ if (action === 'save-snapshot') return config?.identity ? { kind: 'rebackup-start', back: { kind: 'menu' } } : { kind: 'menu' }
843
+ if (action === 'settings') return { kind: 'menu' }
844
+ return { kind: 'menu' }
845
+ }