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,291 @@
1
+ import type { EthagentConfig, EthagentIdentity, SelectableNetwork } from '../../storage/config.js'
2
+ import {
3
+ RegisterAgentPreflightError,
4
+ supportedErc8004ChainForId,
5
+ type Erc8004AgentCandidate,
6
+ } from '../registry/erc8004.js'
7
+ import { AgentStateOwnerMismatchError } from '../crypto/backupEnvelope.js'
8
+ import { ContinuitySnapshotOwnerMismatchError } from '../continuity/envelope.js'
9
+ import { resolveSelectedNetwork } from '../registry/registryConfig.js'
10
+
11
+ export const PREFLIGHT_AGENT_URI = 'ipfs://bafybeigdyrztma2dbfczw7q6ooozbxlqzyw5r7w4f3qw2axvvxqg3w6y7q'
12
+
13
+ export type IdentityHubErrorView = {
14
+ title: string
15
+ detail?: string
16
+ hint?: string
17
+ }
18
+
19
+ export function initialAgentState(name: string, description: string, ownerAddress: string): Record<string, unknown> {
20
+ return {
21
+ version: 1,
22
+ name,
23
+ description,
24
+ ownerAddress,
25
+ createdAt: new Date().toISOString(),
26
+ preferences: {},
27
+ memory: {},
28
+ }
29
+ }
30
+
31
+ export function identityHubErrorView(err: unknown): IdentityHubErrorView {
32
+ if (err instanceof RegisterAgentPreflightError) {
33
+ return {
34
+ title: err.title,
35
+ detail: err.detail,
36
+ hint: err.hint,
37
+ }
38
+ }
39
+ if (err instanceof AgentStateOwnerMismatchError) {
40
+ return {
41
+ title: 'backup locked to another wallet',
42
+ detail: `wallet ${shortAddress(err.currentOwner)} cannot read state encrypted for ${shortAddress(err.backupOwner)}.`,
43
+ hint: 'Use the wallet that created this backup.',
44
+ }
45
+ }
46
+ if (err instanceof ContinuitySnapshotOwnerMismatchError) {
47
+ return {
48
+ title: 'continuity locked to another wallet',
49
+ detail: `wallet ${shortAddress(err.currentOwner)} cannot read SOUL.md or MEMORY.md encrypted for ${shortAddress(err.snapshotOwner)}.`,
50
+ hint: 'Use the wallet that created this continuity snapshot.',
51
+ }
52
+ }
53
+ const message = err instanceof Error ? err.message : String(err)
54
+ if (message === 'fetch failed') {
55
+ return {
56
+ title: 'storage unavailable',
57
+ detail: 'could not reach storage.',
58
+ hint: 'check the connection, then try again.',
59
+ }
60
+ }
61
+ return {
62
+ title: 'identity error',
63
+ detail: message,
64
+ }
65
+ }
66
+
67
+ export function pinataErrorText(err: unknown): string {
68
+ const view = identityHubErrorView(err)
69
+ return view.detail ?? view.title
70
+ }
71
+
72
+ export function isRegistrationPreflightError(err: unknown): boolean {
73
+ return err instanceof RegisterAgentPreflightError
74
+ }
75
+
76
+ export function shortCid(cid: string): string {
77
+ if (cid.length <= 18) return cid
78
+ return `${cid.slice(0, 10)}...${cid.slice(-6)}`
79
+ }
80
+
81
+ export function shortAddress(address: string): string {
82
+ if (address.length <= 14) return address
83
+ return `${address.slice(0, 6)}...${address.slice(-4)}`
84
+ }
85
+
86
+ export function shortHash(value: string): string {
87
+ if (value.length <= 18) return value
88
+ return `${value.slice(0, 8)}...${value.slice(-6)}`
89
+ }
90
+
91
+ export function tokenLabel(candidate: Erc8004AgentCandidate): string {
92
+ return tokenCandidateLabel(candidate)
93
+ }
94
+
95
+ export function tokenCandidateLabel(candidate: Erc8004AgentCandidate): string {
96
+ return candidate.name?.trim() || `agent token #${candidate.agentId.toString()}`
97
+ }
98
+
99
+ export function tokenCandidateSelectLabel(
100
+ candidate: Erc8004AgentCandidate,
101
+ current = false,
102
+ ): string {
103
+ return `${tokenCandidateLabel(candidate)}${current ? ' *' : ''}`
104
+ }
105
+
106
+ export function tokenCandidateHint(candidate: Erc8004AgentCandidate): string {
107
+ const chain = supportedErc8004ChainForId(candidate.chainId)
108
+ const network = chain?.network ? networkLabel(chain.network) : chain?.name ?? `chain ${candidate.chainId}`
109
+ const parts = [
110
+ candidate.name?.trim() ? `token #${candidate.agentId.toString()}` : null,
111
+ network,
112
+ candidate.backup?.createdAt ? `backup ${formatDate(candidate.backup.createdAt)}` : null,
113
+ ].filter((part): part is string => Boolean(part))
114
+ return parts.join(' · ')
115
+ }
116
+
117
+ export function isCurrentAgentCandidate(
118
+ identity: EthagentIdentity | undefined,
119
+ candidate: Erc8004AgentCandidate,
120
+ ): boolean {
121
+ if (!identity?.agentId) return false
122
+ if (identity.agentId !== candidate.agentId.toString()) return false
123
+
124
+ const owner = identity.ownerAddress ?? identity.address
125
+ if (owner && owner.toLowerCase() !== candidate.ownerAddress.toLowerCase()) return false
126
+ if (identity.chainId !== undefined && identity.chainId !== candidate.chainId) return false
127
+ if (
128
+ identity.identityRegistryAddress
129
+ && identity.identityRegistryAddress.toLowerCase() !== candidate.identityRegistryAddress.toLowerCase()
130
+ ) {
131
+ return false
132
+ }
133
+ return true
134
+ }
135
+
136
+ export function storageLabel(apiUrl: string): string {
137
+ void apiUrl
138
+ return 'IPFS'
139
+ }
140
+
141
+ const NETWORK_LABELS: Record<SelectableNetwork, string> = {
142
+ mainnet: 'ethereum mainnet',
143
+ arbitrum: 'arbitrum one',
144
+ base: 'base',
145
+ optimism: 'optimism',
146
+ polygon: 'polygon',
147
+ }
148
+
149
+ export function networkLabel(network: SelectableNetwork): string {
150
+ return NETWORK_LABELS[network]
151
+ }
152
+
153
+ const NETWORK_SUBTITLES: Record<SelectableNetwork, string> = {
154
+ mainnet: 'agent tokens on ethereum mainnet.',
155
+ arbitrum: 'agent tokens on arbitrum one.',
156
+ base: 'agent tokens on base.',
157
+ optimism: 'agent tokens on optimism.',
158
+ polygon: 'agent tokens on polygon.',
159
+ }
160
+
161
+ export function networkSubtitle(network: SelectableNetwork): string {
162
+ return NETWORK_SUBTITLES[network]
163
+ }
164
+
165
+ export function networkMenuTagline(): string {
166
+ return 'choose where your agent token is created or found.'
167
+ }
168
+
169
+ export function currentNetworkLine(config?: EthagentConfig): string {
170
+ return networkLabel(resolveSelectedNetwork(config))
171
+ }
172
+
173
+ export function chainSummaryRow(config?: EthagentConfig, identity?: EthagentIdentity): {
174
+ label: string
175
+ value: string
176
+ tone: 'ok' | 'dim'
177
+ } {
178
+ const network = resolveSelectedNetwork(config)
179
+ const fromIdentity = identity?.chainId ? supportedErc8004ChainForId(identity.chainId)?.name.toLowerCase() : undefined
180
+ const value = fromIdentity ?? networkLabel(network)
181
+ return { label: 'chain', value, tone: identity?.chainId ? 'ok' : 'dim' }
182
+ }
183
+
184
+ export function lastBackupLabel(identity?: EthagentIdentity): string {
185
+ const created = identity?.backup?.createdAt
186
+ return created ? formatDate(created) : 'never'
187
+ }
188
+
189
+ export function identitySummaryRows(
190
+ identity: EthagentIdentity | undefined,
191
+ config?: EthagentConfig,
192
+ ): Array<{
193
+ label: string
194
+ value: string
195
+ tone: 'ok' | 'dim'
196
+ }> {
197
+ const backup = identity?.backup
198
+ const owner = identity?.ownerAddress ?? identity?.address
199
+ const ownerValue = owner ? shortAddress(owner) : 'not connected'
200
+ const tokenValue = identity?.agentId ? `#${identity.agentId}` : 'not created'
201
+ const chain = chainSummaryRow(config, identity)
202
+ const stateValue = backup?.cid ? shortCid(backup.cid) : 'not saved yet'
203
+ const skillsValue = identity?.publicSkills?.cid ? shortCid(identity.publicSkills.cid) : 'not published'
204
+ const cardValue = identity?.publicSkills?.agentCardCid ? shortCid(identity.publicSkills.agentCardCid) : 'not published'
205
+ const imageValue = typeof identity?.state?.imageUrl === 'string' && identity.state.imageUrl.trim() ? 'attached' : 'not attached'
206
+ return [
207
+ { label: 'owner', value: ownerValue, tone: identity ? 'ok' : 'dim' },
208
+ { label: 'token', value: tokenValue, tone: identity?.agentId ? 'ok' : 'dim' },
209
+ { label: 'network', value: chain.value, tone: chain.tone },
210
+ { label: 'state', value: stateValue, tone: backup ? 'ok' : 'dim' },
211
+ { label: 'skills', value: skillsValue, tone: identity?.publicSkills?.cid ? 'ok' : 'dim' },
212
+ { label: 'card', value: cardValue, tone: identity?.publicSkills?.agentCardCid ? 'ok' : 'dim' },
213
+ { label: 'image', value: imageValue, tone: imageValue === 'attached' ? 'ok' : 'dim' },
214
+ ]
215
+ }
216
+
217
+ export type IdentityDetailSection = {
218
+ title: string
219
+ rows: Array<{
220
+ label: string
221
+ value: string
222
+ tone: 'ok' | 'dim'
223
+ }>
224
+ }
225
+
226
+ export function identityDetailSections(
227
+ identity: EthagentIdentity | undefined,
228
+ config?: EthagentConfig,
229
+ ): IdentityDetailSection[] {
230
+ const backup = identity?.backup
231
+ const owner = identity?.ownerAddress ?? identity?.address
232
+ const chain = chainSummaryRow(config, identity)
233
+ const stateCid = backup?.cid ?? 'not saved yet'
234
+ const registrationCid = identity?.metadataCid ?? 'not saved yet'
235
+ const publicSkillsCid = identity?.publicSkills?.cid ?? 'not published'
236
+ const agentCardCid = identity?.publicSkills?.agentCardCid ?? 'not published'
237
+
238
+ return [
239
+ {
240
+ title: 'Agent',
241
+ rows: [
242
+ { label: 'token', value: identity?.agentId ? `#${identity.agentId}` : 'not created', tone: identity?.agentId ? 'ok' : 'dim' },
243
+ { label: 'network', value: chain.value, tone: chain.tone },
244
+ { label: 'registration', value: registrationCid, tone: identity?.metadataCid ? 'ok' : 'dim' },
245
+ ],
246
+ },
247
+ {
248
+ title: 'Owner',
249
+ rows: [
250
+ { label: 'wallet', value: owner ?? 'not connected', tone: owner ? 'ok' : 'dim' },
251
+ ],
252
+ },
253
+ {
254
+ title: 'Recovery',
255
+ rows: [
256
+ { label: 'state CID', value: stateCid, tone: backup?.cid ? 'ok' : 'dim' },
257
+ { label: 'skills CID', value: publicSkillsCid, tone: identity?.publicSkills?.cid ? 'ok' : 'dim' },
258
+ { label: 'agent card CID', value: agentCardCid, tone: identity?.publicSkills?.agentCardCid ? 'ok' : 'dim' },
259
+ { label: 'storage', value: backup?.ipfsApiUrl ? storageLabel(backup.ipfsApiUrl) : 'not saved yet', tone: backup?.ipfsApiUrl ? 'ok' : 'dim' },
260
+ { label: 'created', value: identity?.createdAt ? formatDate(identity.createdAt) : 'not created', tone: identity?.createdAt ? 'ok' : 'dim' },
261
+ { label: 'last backup', value: backup?.createdAt ? formatDate(backup.createdAt) : 'never', tone: backup?.createdAt ? 'ok' : 'dim' },
262
+ { label: 'status', value: backup?.status ?? 'unknown', tone: backup?.status ? 'ok' : 'dim' },
263
+ ],
264
+ },
265
+ ]
266
+ }
267
+
268
+ export type CopyableField = {
269
+ label: string
270
+ value: string
271
+ }
272
+
273
+ export function copyableIdentityFields(identity?: EthagentIdentity): CopyableField[] {
274
+ if (!identity) return []
275
+ const fields: CopyableField[] = []
276
+ if (identity.backup?.cid) fields.push({ label: 'state CID', value: identity.backup.cid })
277
+ if (identity.publicSkills?.cid) fields.push({ label: 'skills CID', value: identity.publicSkills.cid })
278
+ if (identity.publicSkills?.agentCardCid) fields.push({ label: 'agent card CID', value: identity.publicSkills.agentCardCid })
279
+ if (identity.metadataCid) fields.push({ label: 'registration CID', value: identity.metadataCid })
280
+ if (identity.agentUri) fields.push({ label: 'agent URI', value: identity.agentUri })
281
+ const owner = identity.ownerAddress ?? identity.address
282
+ if (owner) fields.push({ label: 'owner address', value: owner })
283
+ if (identity.agentId) fields.push({ label: 'token id', value: identity.agentId })
284
+ return fields
285
+ }
286
+
287
+ function formatDate(input: string): string {
288
+ const date = new Date(input)
289
+ if (Number.isNaN(date.getTime())) return input
290
+ return date.toISOString().slice(0, 10)
291
+ }
@@ -0,0 +1,209 @@
1
+ import type { EthagentConfig, EthagentIdentity, SelectableNetwork } from '../../storage/config.js'
2
+ import type { Erc8004AgentCandidate, Erc8004RegistryConfig } from '../registry/erc8004.js'
3
+ import type { RegistryResolution } from '../registry/registryConfig.js'
4
+ import type { AgentStateBackupEnvelope } from '../crypto/backupEnvelope.js'
5
+ import type { ContinuitySnapshotEnvelope } from '../continuity/envelope.js'
6
+ import type { IdentityHubErrorView } from './identityHubModel.js'
7
+
8
+ export type RestorePurpose = 'restore' | 'switch'
9
+ export type DetailsView = Extract<Step, { kind: 'details' }>
10
+ export type ProfileUpdates = { name?: string; description?: string; imagePath?: string }
11
+ export type RestorableBackupEnvelope = AgentStateBackupEnvelope | ContinuitySnapshotEnvelope
12
+
13
+ export type Step =
14
+ | { kind: 'menu' }
15
+ | { kind: 'replace-confirm'; next: 'create' | 'restore' }
16
+ | { kind: 'create-name'; error?: string }
17
+ | { kind: 'create-description'; name: string }
18
+ | { kind: 'create-network'; name: string; description: string }
19
+ | { kind: 'create-preflight'; name: string; description: string; network?: SelectableNetwork }
20
+ | { kind: 'create-registry'; name: string; description: string; resolution: RegistryResolution; error?: string }
21
+ | { kind: 'create-signing'; name: string; description: string; registry: Erc8004RegistryConfig; pinataJwt?: string }
22
+ | { kind: 'create-storage'; name: string; description: string; registry: Erc8004RegistryConfig; error?: string; pinataJwt?: string }
23
+ | { kind: 'restore-owner'; purpose?: RestorePurpose }
24
+ | { kind: 'restore-wallet'; purpose?: RestorePurpose }
25
+ | { kind: 'restore-network'; ownerHandle: string; purpose?: RestorePurpose }
26
+ | { kind: 'restore-registry'; ownerHandle: string; error?: string; purpose?: RestorePurpose }
27
+ | { kind: 'restore-discovering'; ownerHandle: string; registry: Erc8004RegistryConfig; purpose?: RestorePurpose }
28
+ | { kind: 'restore-token-id'; ownerHandle: string; registry: Erc8004RegistryConfig; error?: string; purpose?: RestorePurpose }
29
+ | { kind: 'restore-select-token'; ownerHandle: string; registry: Erc8004RegistryConfig; candidates: Erc8004AgentCandidate[]; purpose?: RestorePurpose }
30
+ | { kind: 'restore-fetching'; cid: string; apiUrl: string; candidate: Erc8004AgentCandidate; purpose?: RestorePurpose }
31
+ | { kind: 'restore-authorizing'; cid: string; apiUrl: string; envelope: RestorableBackupEnvelope; candidate: Erc8004AgentCandidate; purpose?: RestorePurpose }
32
+ | { kind: 'rebackup-signing'; identity: EthagentIdentity; registry: Erc8004RegistryConfig; pinataJwt?: string; profileUpdates?: ProfileUpdates; returnTo?: Step }
33
+ | { kind: 'rebackup-storage'; identity: EthagentIdentity; registry: Erc8004RegistryConfig; error?: string; pinataJwt?: string; profileUpdates?: ProfileUpdates; returnTo?: Step }
34
+ | { kind: 'public-profile-signing'; identity: EthagentIdentity; registry: Erc8004RegistryConfig; pinataJwt?: string; profileUpdates?: ProfileUpdates; returnTo?: Step }
35
+ | { kind: 'public-profile-storage'; identity: EthagentIdentity; registry: Erc8004RegistryConfig; error?: string; pinataJwt?: string; profileUpdates?: ProfileUpdates; returnTo?: Step }
36
+ | { kind: 'continuity-private'; notice?: string }
37
+ | { kind: 'continuity-public'; notice?: string }
38
+ | { kind: 'rebackup-confirm' }
39
+ | { kind: 'recovery-refetch-confirm' }
40
+ | { kind: 'recovery-refetching'; identity: EthagentIdentity; registry: Erc8004RegistryConfig }
41
+ | { kind: 'rebackup-start'; back: Step }
42
+ | { kind: 'edit-profile-name'; identity: EthagentIdentity; registry: Erc8004RegistryConfig; returnTo?: Step }
43
+ | { kind: 'edit-profile-description'; identity: EthagentIdentity; registry: Erc8004RegistryConfig; name: string; returnTo?: Step }
44
+ | { kind: 'edit-profile-image'; identity: EthagentIdentity; registry: Erc8004RegistryConfig; name: string; description: string; error?: string; returnTo?: Step }
45
+ | { kind: 'storage-credential' }
46
+ | { kind: 'storage-credential-input'; error?: string }
47
+ | { kind: 'storage-credential-forget-confirm' }
48
+ | { kind: 'details' }
49
+ | { kind: 'busy'; label: string }
50
+ | { kind: 'error'; error: IdentityHubErrorView; back: Step }
51
+
52
+ export type Action =
53
+ | { type: 'goMenu' }
54
+ | { type: 'startCreate'; hasIdentity: boolean }
55
+ | { type: 'confirmReplace' }
56
+ | { type: 'cancelReplace' }
57
+ | { type: 'nameSubmitted'; name: string }
58
+ | { type: 'descriptionSubmitted'; name: string; description: string }
59
+ | { type: 'preflightResolved'; step: Step }
60
+ | { type: 'registrySubmitted'; step: Step }
61
+ | { type: 'storageSubmitted'; step: Step }
62
+ | { type: 'walletSigned'; step: Step }
63
+ | { type: 'pinned'; step: Step }
64
+ | { type: 'registered'; step: Step }
65
+ | { type: 'startRestore' }
66
+ | { type: 'ownerSubmitted'; step: Step }
67
+ | { type: 'restoreRegistrySubmitted'; step: Step }
68
+ | { type: 'discovered'; step: Step }
69
+ | { type: 'tokenSelected'; step: Step }
70
+ | { type: 'fetched'; step: Step }
71
+ | { type: 'authorized' }
72
+ | { type: 'openDetails' }
73
+ | { type: 'openCopyPicker' }
74
+ | { type: 'closeCopyPicker' }
75
+ | { type: 'error'; error: IdentityHubErrorView; back: Step }
76
+ | { type: 'back'; from: Step }
77
+
78
+ export function identityHubReducer(state: Step, action: Action): Step {
79
+ switch (action.type) {
80
+ case 'goMenu':
81
+ return { kind: 'menu' }
82
+ case 'startCreate':
83
+ if (action.hasIdentity) return { kind: 'replace-confirm', next: 'create' }
84
+ return { kind: 'create-name' }
85
+ case 'confirmReplace':
86
+ return { kind: 'create-name' }
87
+ case 'cancelReplace':
88
+ return { kind: 'menu' }
89
+ case 'nameSubmitted':
90
+ return { kind: 'create-description', name: action.name }
91
+ case 'descriptionSubmitted':
92
+ return { kind: 'create-network', name: action.name, description: action.description }
93
+ case 'preflightResolved':
94
+ case 'registrySubmitted':
95
+ case 'storageSubmitted':
96
+ case 'walletSigned':
97
+ case 'pinned':
98
+ case 'registered':
99
+ case 'ownerSubmitted':
100
+ case 'restoreRegistrySubmitted':
101
+ case 'discovered':
102
+ case 'tokenSelected':
103
+ case 'fetched':
104
+ return action.step
105
+ case 'startRestore':
106
+ return { kind: 'restore-wallet' }
107
+ case 'openDetails':
108
+ return { kind: 'details' }
109
+ case 'openCopyPicker':
110
+ if (state.kind === 'details') return { kind: 'details' }
111
+ return state
112
+ case 'closeCopyPicker':
113
+ if (state.kind === 'details') return { kind: 'details' }
114
+ return state
115
+ case 'error':
116
+ return { kind: 'error', error: action.error, back: action.back }
117
+ case 'back':
118
+ return backStep(action.from)
119
+ default:
120
+ return state
121
+ }
122
+ }
123
+
124
+ function backStep(from: Step): Step {
125
+ switch (from.kind) {
126
+ case 'create-name':
127
+ return { kind: 'menu' }
128
+ case 'create-description':
129
+ return { kind: 'create-name' }
130
+ case 'create-network':
131
+ return { kind: 'create-description', name: from.name }
132
+ case 'create-preflight':
133
+ return { kind: 'create-network', name: from.name, description: from.description }
134
+ case 'create-registry':
135
+ return { kind: 'create-network', name: from.name, description: from.description }
136
+ case 'create-signing':
137
+ return { kind: 'create-network', name: from.name, description: from.description }
138
+ case 'create-storage':
139
+ return { kind: 'create-network', name: from.name, description: from.description }
140
+ case 'restore-owner':
141
+ return { kind: 'menu' }
142
+ case 'restore-wallet':
143
+ return { kind: 'restore-owner', purpose: from.purpose }
144
+ case 'restore-network':
145
+ return { kind: 'restore-owner', purpose: from.purpose }
146
+ case 'restore-registry':
147
+ return { kind: 'restore-network', ownerHandle: from.ownerHandle, purpose: from.purpose }
148
+ case 'restore-discovering':
149
+ return { kind: 'restore-network', ownerHandle: from.ownerHandle, purpose: from.purpose }
150
+ case 'restore-token-id':
151
+ return { kind: 'restore-network', ownerHandle: from.ownerHandle, purpose: from.purpose }
152
+ case 'restore-select-token':
153
+ return { kind: 'restore-network', ownerHandle: from.ownerHandle, purpose: from.purpose }
154
+ case 'restore-fetching':
155
+ return { kind: 'restore-network', ownerHandle: from.candidate.ownerAddress, purpose: from.purpose }
156
+ case 'restore-authorizing':
157
+ return { kind: 'restore-network', ownerHandle: from.candidate.ownerAddress, purpose: from.purpose }
158
+ case 'details':
159
+ return { kind: 'menu' }
160
+ case 'rebackup-signing':
161
+ case 'rebackup-storage':
162
+ case 'rebackup-start':
163
+ return from.kind === 'rebackup-start' ? from.back : from.returnTo ?? { kind: 'menu' }
164
+ case 'public-profile-signing':
165
+ case 'public-profile-storage':
166
+ return from.returnTo ?? { kind: 'continuity-public' }
167
+ case 'continuity-private':
168
+ case 'continuity-public':
169
+ return { kind: 'menu' }
170
+ case 'rebackup-confirm':
171
+ case 'recovery-refetch-confirm':
172
+ case 'recovery-refetching':
173
+ return { kind: 'menu' }
174
+ case 'edit-profile-name':
175
+ return from.returnTo ?? { kind: 'continuity-public' }
176
+ case 'edit-profile-description':
177
+ return { kind: 'edit-profile-name', identity: from.identity, registry: from.registry, returnTo: from.returnTo }
178
+ case 'edit-profile-image':
179
+ return { kind: 'edit-profile-description', identity: from.identity, registry: from.registry, name: from.name, returnTo: from.returnTo }
180
+ case 'storage-credential':
181
+ case 'storage-credential-input':
182
+ case 'storage-credential-forget-confirm':
183
+ return { kind: 'menu' }
184
+ case 'error':
185
+ return from.back
186
+ default:
187
+ return { kind: 'menu' }
188
+ }
189
+ }
190
+
191
+ export const CREATE_STEP_LABELS = ['name', 'describe', 'network', 'create']
192
+
193
+ export function createStepNumber(step: Step): number {
194
+ switch (step.kind) {
195
+ case 'create-name':
196
+ return 1
197
+ case 'create-description':
198
+ return 2
199
+ case 'create-network':
200
+ return 3
201
+ case 'create-preflight':
202
+ case 'create-storage':
203
+ case 'create-registry':
204
+ case 'create-signing':
205
+ return 4
206
+ default:
207
+ return 0
208
+ }
209
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react'
2
+ import { Text } from 'ink'
3
+ import { Surface } from '../../../ui/Surface.js'
4
+ import { Spinner } from '../../../ui/Spinner.js'
5
+ import { theme } from '../../../ui/theme.js'
6
+ import { useAppInput } from '../../../app/input/AppInputProvider.js'
7
+
8
+ type BusyScreenProps = {
9
+ title: string
10
+ subtitle?: React.ReactNode
11
+ label: string
12
+ footer?: React.ReactNode
13
+ onCancel?: () => void
14
+ }
15
+
16
+ export const BusyScreen: React.FC<BusyScreenProps> = ({ title, subtitle, label, footer, onCancel }) => {
17
+ useAppInput((_input, key) => {
18
+ if (key.escape && onCancel) onCancel()
19
+ }, { isActive: Boolean(onCancel) })
20
+ const resolvedFooter = footer ?? (onCancel ? <Text color={theme.dim}>esc cancels</Text> : undefined)
21
+ return (
22
+ <Surface title={title} subtitle={subtitle} footer={resolvedFooter}>
23
+ <Spinner label={label} />
24
+ </Surface>
25
+ )
26
+ }
@@ -0,0 +1,139 @@
1
+ import React from 'react'
2
+ import { Box, Text } from 'ink'
3
+ import { Surface } from '../../../ui/Surface.js'
4
+ import { Select } from '../../../ui/Select.js'
5
+ import { theme } from '../../../ui/theme.js'
6
+ import type { EthagentConfig, EthagentIdentity } from '../../../storage/config.js'
7
+ import { IdentitySummary } from './IdentitySummary.js'
8
+ import { shortCid } from '../identityHubModel.js'
9
+
10
+ type PrivateAction = 'soul' | 'memory' | 'backup' | 'back'
11
+ type PublicAction = 'edit' | 'skills' | 'publish' | 'back'
12
+
13
+ type CommonProps = {
14
+ identity?: EthagentIdentity
15
+ config?: EthagentConfig
16
+ ready: boolean
17
+ notice?: string
18
+ footer: React.ReactNode
19
+ onBack: () => void
20
+ }
21
+
22
+ export const PrivateContinuityScreen: React.FC<CommonProps & {
23
+ canBackup: boolean
24
+ onOpenSoul: () => void
25
+ onOpenMemory: () => void
26
+ onBackup: () => void
27
+ }> = ({
28
+ identity,
29
+ config,
30
+ ready,
31
+ notice,
32
+ footer,
33
+ canBackup,
34
+ onOpenSoul,
35
+ onOpenMemory,
36
+ onBackup,
37
+ onBack,
38
+ }) => (
39
+ <Surface title="Private Memory Files" subtitle={notice ?? privateSubtitle(ready)} footer={footer}>
40
+ <IdentitySummary identity={identity} config={config} compact />
41
+ <PrivateRows identity={identity} ready={ready} />
42
+ <Box marginTop={1}>
43
+ <Select<PrivateAction>
44
+ options={[
45
+ { value: 'soul', role: 'section', prefix: '--', label: 'Open local files' },
46
+ { value: 'soul', label: 'open SOUL.md', hint: 'edit persona and operating preferences', disabled: !ready },
47
+ { value: 'memory', label: 'open MEMORY.md', hint: 'edit private working memory for this agent', disabled: !ready },
48
+ { value: 'backup', role: 'section', prefix: '--', label: 'Publish' },
49
+ { value: 'backup', label: 'save snapshot', hint: 'save editor changes first; then encrypt and pin', disabled: !ready || !canBackup },
50
+ { value: 'back', role: 'section', prefix: '--', label: 'Navigation' },
51
+ { value: 'back', label: 'back to identity hub', hint: 'return without changing private files', role: 'utility' },
52
+ ]}
53
+ hintLayout="inline"
54
+ onSubmit={choice => {
55
+ if (choice === 'soul') return onOpenSoul()
56
+ if (choice === 'memory') return onOpenMemory()
57
+ if (choice === 'backup') return onBackup()
58
+ return onBack()
59
+ }}
60
+ onCancel={onBack}
61
+ />
62
+ </Box>
63
+ </Surface>
64
+ )
65
+
66
+ export const PublicSkillsScreen: React.FC<CommonProps & {
67
+ canPublish: boolean
68
+ onEditProfile: () => void
69
+ onOpenSkills: () => void
70
+ onPublish: () => void
71
+ }> = ({ identity, config, notice, footer, canPublish, onEditProfile, onOpenSkills, onPublish, onBack }) => (
72
+ <Surface title="Public Profile" subtitle={notice ?? 'Public token metadata only. Private SOUL.md and MEMORY.md are not touched here.'} footer={footer}>
73
+ <IdentitySummary identity={identity} config={config} compact />
74
+ <PublicProfileRows identity={identity} />
75
+ <Box marginTop={1}>
76
+ <Select<PublicAction>
77
+ options={[
78
+ { value: 'edit', role: 'section', prefix: '--', label: 'Profile' },
79
+ { value: 'edit', label: 'edit name, description, image', hint: 'upload a local image to IPFS automatically' },
80
+ { value: 'skills', role: 'section', prefix: '--', label: 'Capabilities' },
81
+ { value: 'skills', label: 'open skills.json', hint: 'edit public capabilities and notes' },
82
+ { value: 'publish', role: 'section', prefix: '--', label: 'Publish' },
83
+ { value: 'publish', label: 'publish public profile', hint: 'save skills.json first; pins metadata and updates tokenURI', disabled: !canPublish },
84
+ { value: 'back', role: 'section', prefix: '--', label: 'Navigation' },
85
+ { value: 'back', label: 'back to identity hub', hint: 'return without changing public metadata', role: 'utility' },
86
+ ]}
87
+ hintLayout="inline"
88
+ onSubmit={choice => {
89
+ if (choice === 'edit') return onEditProfile()
90
+ if (choice === 'skills') return onOpenSkills()
91
+ if (choice === 'publish') return onPublish()
92
+ return onBack()
93
+ }}
94
+ onCancel={onBack}
95
+ />
96
+ </Box>
97
+ </Surface>
98
+ )
99
+
100
+ const PrivateRows: React.FC<{ identity?: EthagentIdentity; ready: boolean }> = ({ identity, ready }) => (
101
+ <Box flexDirection="column" marginTop={1}>
102
+ <Text>
103
+ <Text color={theme.dim}>{'local files'.padEnd(13)}</Text>
104
+ <Text color={ready ? theme.text : theme.dim}>{ready ? 'SOUL.md and MEMORY.md ready' : 'missing local working files'}</Text>
105
+ </Text>
106
+ <Text>
107
+ <Text color={theme.dim}>{'snapshot'.padEnd(13)}</Text>
108
+ <Text color={identity?.backup?.cid ? theme.text : theme.dim}>{identity?.backup?.cid ? shortCid(identity.backup.cid) : 'not saved yet'}</Text>
109
+ </Text>
110
+ </Box>
111
+ )
112
+
113
+ const PublicProfileRows: React.FC<{ identity?: EthagentIdentity }> = ({ identity }) => (
114
+ <Box flexDirection="column" marginTop={1}>
115
+ <Text>
116
+ <Text color={theme.dim}>{'skills.json'.padEnd(13)}</Text>
117
+ <Text color={identity?.publicSkills?.cid ? theme.text : theme.dim}>{identity?.publicSkills?.cid ? shortCid(identity.publicSkills.cid) : 'not published'}</Text>
118
+ </Text>
119
+ <Text>
120
+ <Text color={theme.dim}>{'agent card'.padEnd(13)}</Text>
121
+ <Text color={identity?.publicSkills?.agentCardCid ? theme.text : theme.dim}>{identity?.publicSkills?.agentCardCid ? shortCid(identity.publicSkills.agentCardCid) : 'not published'}</Text>
122
+ </Text>
123
+ <Text>
124
+ <Text color={theme.dim}>{'image'.padEnd(13)}</Text>
125
+ <Text color={readStateString(identity?.state, 'imageUrl') ? theme.text : theme.dim}>{readStateString(identity?.state, 'imageUrl') ? 'attached' : 'not attached'}</Text>
126
+ </Text>
127
+ </Box>
128
+ )
129
+
130
+ function privateSubtitle(ready: boolean): string {
131
+ return ready
132
+ ? 'SOUL.md and MEMORY.md are private local files on this machine.'
133
+ : 'Use "refetch latest snapshot" from the hub menu to recover files.'
134
+ }
135
+
136
+ function readStateString(state: Record<string, unknown> | undefined, key: string): string {
137
+ const value = state?.[key]
138
+ return typeof value === 'string' ? value.trim() : ''
139
+ }