ethagent 0.2.1 → 1.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 (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 +868 -0
  52. package/src/identity/hub/identityHubEffects.ts +1146 -0
  53. package/src/identity/hub/identityHubModel.ts +291 -0
  54. package/src/identity/hub/identityHubReducer.ts +212 -0
  55. package/src/identity/hub/screens/BusyScreen.tsx +26 -0
  56. package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +144 -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,50 @@
1
+ import React from 'react'
2
+ import { Text } from 'ink'
3
+ import { Surface } from '../../../ui/Surface.js'
4
+ import { TextInput } from '../../../ui/TextInput.js'
5
+ import { theme } from '../../../ui/theme.js'
6
+ import { extractPinataJwt } from '../../storage/ipfs.js'
7
+ import type { Step } from '../identityHubReducer.js'
8
+
9
+ const PINATA_API_KEYS_URL = 'https://app.pinata.cloud/developers/api-keys'
10
+
11
+ type RebackupStorageScreenProps = {
12
+ step: Extract<Step, { kind: 'rebackup-storage' | 'public-profile-storage' }>
13
+ footer: React.ReactNode
14
+ onSubmit: (input: string) => void
15
+ onCancel: () => void
16
+ }
17
+
18
+ export const RebackupStorageScreen: React.FC<RebackupStorageScreenProps> = ({ step, footer, onSubmit, onCancel }) => {
19
+ const publicOnly = step.kind === 'public-profile-storage'
20
+ return (
21
+ <Surface
22
+ title="Connect IPFS Storage"
23
+ subtitle={step.error ?? (publicOnly
24
+ ? 'Save a Pinata JWT so ethagent can pin public profile metadata to IPFS.'
25
+ : 'Save a Pinata JWT so ethagent can pin encrypted state to IPFS.')}
26
+ footer={footer}
27
+ >
28
+ <Text>
29
+ <Text color={theme.dim}>Paste your Pinata JWT. Get one at </Text>
30
+ <Text color={theme.accentPrimary} underline>{PINATA_API_KEYS_URL}</Text>
31
+ </Text>
32
+ <Text color={theme.dim}>Saved encrypted on this device · used only for IPFS pinning</Text>
33
+ <TextInput
34
+ key="rebackup-storage"
35
+ isSecret
36
+ placeholder="Pinata JWT"
37
+ validate={v => {
38
+ try {
39
+ extractPinataJwt(v)
40
+ return null
41
+ } catch (err: unknown) {
42
+ return (err as Error).message
43
+ }
44
+ }}
45
+ onSubmit={onSubmit}
46
+ onCancel={onCancel}
47
+ />
48
+ </Surface>
49
+ )
50
+ }
@@ -0,0 +1,85 @@
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
+
7
+ import type { ContinuityWorkingTreeStatus } from '../../continuity/storage.js'
8
+
9
+ export type RecoveryConfirmMode = 'publish' | 'refetch'
10
+
11
+ type RecoveryConfirmScreenProps = {
12
+ mode: RecoveryConfirmMode
13
+ workingStatus?: ContinuityWorkingTreeStatus | null
14
+ footer: React.ReactNode
15
+ onConfirm: () => void
16
+ onBack: () => void
17
+ }
18
+
19
+ export const RecoveryConfirmScreen: React.FC<RecoveryConfirmScreenProps> = ({ mode, workingStatus, footer, onConfirm, onBack }) => {
20
+ const isPublish = mode === 'publish'
21
+ const title = isPublish ? 'Publish Snapshot?' : 'Refetch Latest From Chain?'
22
+ const subtitle = isPublish
23
+ ? 'This replaces the current on-chain snapshot pointer.'
24
+ : 'This overwrites local files with the on-chain version.'
25
+
26
+ const headlineColor = isPublish ? theme.accentPeach : theme.accentMint
27
+ const headline = isPublish
28
+ ? 'Publishing replaces the on-chain pointer for this agent.'
29
+ : 'Refetching replaces local SOUL.md, MEMORY.md, and skills.json with what is on chain.'
30
+ const detail = isPublish
31
+ ? 'The old snapshot pointer is overwritten. Local edits become the published state.'
32
+ : 'Unsaved local edits will be lost. Use this when local files are missing or out of sync with the latest published snapshot.'
33
+
34
+ const needsBackup = workingStatus?.publishState === 'local-changes' || workingStatus?.publishState === 'not-published' || workingStatus?.publishState === 'verify-needed'
35
+ let changedFiles: string[] = []
36
+ if (isPublish && needsBackup) {
37
+ if (workingStatus?.localContentHashes && workingStatus?.publishedContentHashes) {
38
+ if (workingStatus.localContentHashes['SOUL.md'] !== workingStatus.publishedContentHashes['SOUL.md']) changedFiles.push('SOUL.md')
39
+ if (workingStatus.localContentHashes['MEMORY.md'] !== workingStatus.publishedContentHashes['MEMORY.md']) changedFiles.push('MEMORY.md')
40
+ if (workingStatus.localContentHashes['skills.json'] !== workingStatus.publishedContentHashes['skills.json']) changedFiles.push('skills.json')
41
+ } else {
42
+ changedFiles = ['SOUL.md', 'MEMORY.md', 'skills.json']
43
+ }
44
+ }
45
+
46
+ return (
47
+ <Surface title={title} subtitle={subtitle} footer={footer} tone="primary">
48
+ <Box flexDirection="column">
49
+ <Text color={headlineColor}>{headline}</Text>
50
+ <Text color={theme.textSubtle}>{detail}</Text>
51
+ {isPublish && changedFiles.length > 0 && (
52
+ <Box marginTop={1}>
53
+ <Text color={theme.textSubtle}>unsaved changes: </Text>
54
+ <Text color="red" bold>{changedFiles.join(', ')}</Text>
55
+ </Box>
56
+ )}
57
+ </Box>
58
+ <Box marginTop={1}>
59
+ <Select<'confirm' | 'back'>
60
+ options={[
61
+ { value: 'confirm', role: 'section', prefix: '--', label: isPublish ? 'Publish' : 'Refetch' },
62
+ {
63
+ value: 'confirm',
64
+ label: isPublish ? 'Yes, Publish Snapshot Now' : 'Yes, Refetch From Chain',
65
+ hint: isPublish ? 'Sign and overwrite the on-chain pointer' : 'Wallet decrypts and overwrites local files',
66
+ },
67
+ { value: 'back', role: 'section', prefix: '--', label: 'Cancel' },
68
+ {
69
+ value: 'back',
70
+ label: 'No, Go Back',
71
+ hint: isPublish ? 'Return without saving anything' : 'Return without changing anything',
72
+ role: 'utility',
73
+ },
74
+ ]}
75
+ hintLayout="inline"
76
+ onSubmit={choice => {
77
+ if (choice === 'confirm') return onConfirm()
78
+ return onBack()
79
+ }}
80
+ onCancel={onBack}
81
+ />
82
+ </Box>
83
+ </Surface>
84
+ )
85
+ }
@@ -0,0 +1,206 @@
1
+ import React from 'react'
2
+ import { Text } from 'ink'
3
+ import { Surface } from '../../../ui/Surface.js'
4
+ import { Select } from '../../../ui/Select.js'
5
+ import { TextInput } from '../../../ui/TextInput.js'
6
+ import { theme } from '../../../ui/theme.js'
7
+ import { normalizeErc8004RegistryConfig } from '../../registry/erc8004.js'
8
+ import {
9
+ isCurrentAgentCandidate,
10
+ networkLabel,
11
+ tokenCandidateHint,
12
+ tokenCandidateSelectLabel,
13
+ } from '../identityHubModel.js'
14
+ import { registryConfigFromConfig } from '../../registry/registryConfig.js'
15
+ import type { Step } from '../identityHubReducer.js'
16
+ import { WalletApprovalScreen } from './WalletApprovalScreen.js'
17
+ import { BusyScreen } from './BusyScreen.js'
18
+ import type { BrowserWalletReady } from '../../wallet/browserWallet.js'
19
+ import type { EthagentConfig } from '../../../storage/config.js'
20
+ import type { RestoreProgress } from '../identityHubEffects.js'
21
+
22
+ type RestoreStep = Exclude<Extract<Step, { kind: `restore-${string}` }>, { kind: 'restore-wallet' | 'restore-network' }>
23
+
24
+ type RestoreFlowProps = {
25
+ step: RestoreStep
26
+ config?: EthagentConfig
27
+ walletSession: BrowserWalletReady | null
28
+ restoreProgress: RestoreProgress | null
29
+ onConnectWallet: () => void
30
+ onRestoreRegistrySubmit: (value: string) => void
31
+ onTokenIdSubmit: (value: string) => void
32
+ onTokenSelect: (tokenId: string) => void
33
+ onBack: () => void
34
+ }
35
+
36
+ const footerHint = (hint: string) => <Text color={theme.dim}>{hint}</Text>
37
+
38
+ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
39
+ step,
40
+ config,
41
+ walletSession,
42
+ restoreProgress,
43
+ onConnectWallet,
44
+ onRestoreRegistrySubmit,
45
+ onTokenIdSubmit,
46
+ onTokenSelect,
47
+ onBack,
48
+ }) => {
49
+ const purpose = 'purpose' in step ? step.purpose ?? 'restore' : 'restore'
50
+ const isSwitch = purpose === 'switch'
51
+
52
+ if (step.kind === 'restore-owner') {
53
+ return (
54
+ <Surface
55
+ title={isSwitch ? 'Switch Agent Identity' : 'Restore an Agent'}
56
+ subtitle="Connect the wallet that owns the agent you want to load."
57
+ footer={footerHint('enter select · esc back')}
58
+ >
59
+ <Select<'connect'>
60
+ options={[
61
+ { value: 'connect', role: 'section', prefix: '--', label: 'Wallet' },
62
+ { value: 'connect', label: 'connect wallet', hint: 'search tokens owned by browser wallet' },
63
+ ]}
64
+ hintLayout="inline"
65
+ onSubmit={onConnectWallet}
66
+ onCancel={onBack}
67
+ />
68
+ </Surface>
69
+ )
70
+ }
71
+
72
+ if (step.kind === 'restore-registry') {
73
+ const resolution = registryConfigFromConfig(config)
74
+ return (
75
+ <Surface
76
+ title={`${resolution.network ? networkLabel(resolution.network).charAt(0).toUpperCase() + networkLabel(resolution.network).slice(1) : ''} Agent Registry`}
77
+ subtitle={step.error ? `Lookup failed: ${step.error}` : 'Paste the agent registry address for this network.'}
78
+ footer={footerHint('enter discover · esc back')}
79
+ >
80
+ <Text color={theme.dim}>RPC defaults to {resolution.defaultRpcUrl}</Text>
81
+ <TextInput
82
+ initialValue={config?.erc8004?.identityRegistryAddress ?? ''}
83
+ placeholder="0x registry address"
84
+ validate={value => {
85
+ try {
86
+ normalizeErc8004RegistryConfig({
87
+ chainId: resolution.chainId,
88
+ rpcUrl: resolution.config?.rpcUrl ?? resolution.defaultRpcUrl,
89
+ identityRegistryAddress: value.trim(),
90
+ })
91
+ return null
92
+ } catch (err: unknown) {
93
+ return (err as Error).message
94
+ }
95
+ }}
96
+ onSubmit={onRestoreRegistrySubmit}
97
+ onCancel={onBack}
98
+ />
99
+ </Surface>
100
+ )
101
+ }
102
+
103
+ if (step.kind === 'restore-discovering') {
104
+ return (
105
+ <BusyScreen
106
+ title={isSwitch ? 'Finding Agent Identities' : 'Finding Agents'}
107
+ subtitle={step.ownerHandle}
108
+ label="searching this network..."
109
+ onCancel={onBack}
110
+ />
111
+ )
112
+ }
113
+
114
+ if (step.kind === 'restore-token-id') {
115
+ return (
116
+ <Surface
117
+ title="Enter Agent Token ID"
118
+ subtitle={step.error ?? `${networkLabelForRegistry(step.registry)} lookup needs the token ID.`}
119
+ footer={footerHint('enter continue · esc back')}
120
+ >
121
+ <TextInput
122
+ placeholder="#45744"
123
+ validate={value => parseTokenIdInput(value) ? null : 'enter a token id'}
124
+ onSubmit={value => onTokenIdSubmit(value.trim())}
125
+ onCancel={onBack}
126
+ />
127
+ </Surface>
128
+ )
129
+ }
130
+
131
+ if (step.kind === 'restore-select-token') {
132
+ return (
133
+ <Surface
134
+ title={isSwitch ? 'Switch to an Agent' : 'Choose Your Agent'}
135
+ subtitle={step.ownerHandle}
136
+ footer={footerHint('enter select · esc back')}
137
+ >
138
+ <Select<string>
139
+ options={[
140
+ { value: 'section:owned-agents', role: 'section', prefix: '--', label: 'Owned agents' },
141
+ ...step.candidates.map(candidate => {
142
+ const current = isSwitch && isCurrentAgentCandidate(config?.identity, candidate)
143
+ return {
144
+ value: candidate.agentId.toString(),
145
+ label: tokenCandidateSelectLabel(candidate, current),
146
+ hint: tokenCandidateHint(candidate),
147
+ }
148
+ }),
149
+ ]}
150
+ hintLayout="inline"
151
+ onSubmit={onTokenSelect}
152
+ onCancel={onBack}
153
+ />
154
+ </Surface>
155
+ )
156
+ }
157
+
158
+ if (step.kind === 'restore-fetching') {
159
+ return (
160
+ <BusyScreen
161
+ title={isSwitch ? 'Switching Agent Identity' : 'Restoring Your Agent'}
162
+ subtitle="IPFS"
163
+ label="opening encrypted state from IPFS..."
164
+ onCancel={onBack}
165
+ />
166
+ )
167
+ }
168
+
169
+ if (step.kind === 'restore-authorizing') {
170
+ if (restoreProgress) {
171
+ return (
172
+ <BusyScreen
173
+ title={isSwitch ? 'Switching Agent Identity' : 'Restoring Your Agent'}
174
+ subtitle="Wallet Signature Received"
175
+ label={restoreProgress.label}
176
+ />
177
+ )
178
+ }
179
+ return (
180
+ <WalletApprovalScreen
181
+ title={isSwitch ? 'Approve Switch' : 'Approve Restore'}
182
+ subtitle={isSwitch ? 'Use the wallet that owns this agent to switch.' : 'Use the wallet that owns this agent.'}
183
+ walletSession={walletSession}
184
+ label="waiting for approval..."
185
+ onCancel={onBack}
186
+ />
187
+ )
188
+ }
189
+
190
+ return null
191
+ }
192
+
193
+ function parseTokenIdInput(value: string): string | null {
194
+ const normalized = value.trim().replace(/^#/, '')
195
+ return /^\d+$/.test(normalized) ? normalized : null
196
+ }
197
+
198
+ function networkLabelForRegistry(registry: { chainId: number }): string {
199
+ const network = registry.chainId === 1 ? 'mainnet'
200
+ : registry.chainId === 42161 ? 'arbitrum'
201
+ : registry.chainId === 8453 ? 'base'
202
+ : registry.chainId === 10 ? 'optimism'
203
+ : registry.chainId === 137 ? 'polygon'
204
+ : undefined
205
+ return network ? networkLabel(network) : `chain ${registry.chainId}`
206
+ }
@@ -0,0 +1,128 @@
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 { TextInput } from '../../../ui/TextInput.js'
6
+ import { theme } from '../../../ui/theme.js'
7
+ import { extractPinataJwt } from '../../storage/ipfs.js'
8
+ import type { Step } from '../identityHubReducer.js'
9
+
10
+ const PINATA_API_KEYS_URL = 'https://app.pinata.cloud/developers/api-keys'
11
+
12
+ type StorageCredentialAction = 'edit' | 'forget' | 'back'
13
+
14
+ export const STORAGE_CREDENTIAL_FORGET_COPY = [
15
+ 'removes the saved IPFS storage token from this machine.',
16
+ 'existing pinned IPFS backups are not deleted.',
17
+ 'ethagent cannot pin new encrypted state with that account until you save a token again.',
18
+ 'agent identity and sessions stay on this machine.',
19
+ ] as const
20
+
21
+ type StorageCredentialScreenProps = {
22
+ step: Extract<Step, { kind: 'storage-credential' | 'storage-credential-input' | 'storage-credential-forget-confirm' }>
23
+ hasCredential: boolean
24
+ footer: React.ReactNode
25
+ onEdit: () => void
26
+ onForget: () => void
27
+ onConfirmForget: () => void
28
+ onSubmit: (input: string) => void
29
+ onCancel: () => void
30
+ }
31
+
32
+ export const StorageCredentialScreen: React.FC<StorageCredentialScreenProps> = ({
33
+ step,
34
+ hasCredential,
35
+ footer,
36
+ onEdit,
37
+ onForget,
38
+ onConfirmForget,
39
+ onSubmit,
40
+ onCancel,
41
+ }) => {
42
+ if (step.kind === 'storage-credential-input') {
43
+ return (
44
+ <Surface
45
+ title="IPFS Storage Credential"
46
+ subtitle={step.error ?? 'Save the token ethagent uses to pin encrypted state.'}
47
+ footer={footer}
48
+ >
49
+ <Text>
50
+ <Text color={theme.dim}>Paste your Pinata JWT. Get one at </Text>
51
+ <Text color={theme.accentPrimary} underline>{PINATA_API_KEYS_URL}</Text>
52
+ </Text>
53
+ <Text color={theme.dim}>Stored encrypted on this device. Used only for IPFS pinning.</Text>
54
+ <TextInput
55
+ key="storage-credential-input"
56
+ isSecret
57
+ placeholder="Pinata JWT"
58
+ validate={v => {
59
+ try {
60
+ extractPinataJwt(v)
61
+ return null
62
+ } catch (err: unknown) {
63
+ return (err as Error).message
64
+ }
65
+ }}
66
+ onSubmit={onSubmit}
67
+ onCancel={onCancel}
68
+ />
69
+ </Surface>
70
+ )
71
+ }
72
+
73
+ if (step.kind === 'storage-credential-forget-confirm') {
74
+ return (
75
+ <Surface
76
+ title="Forget IPFS Storage Credential?"
77
+ subtitle="This only removes the local token used for pinning."
78
+ footer={footer}
79
+ >
80
+ <Box flexDirection="column">
81
+ {STORAGE_CREDENTIAL_FORGET_COPY.map(line => (
82
+ <Text key={line} color={theme.dim}>- {line}</Text>
83
+ ))}
84
+ </Box>
85
+ <Box marginTop={1}>
86
+ <Select<StorageCredentialAction>
87
+ options={[
88
+ { value: 'forget', role: 'section', prefix: '--', label: 'Credential' },
89
+ { value: 'forget', label: 'forget credential', hint: 'remove local IPFS pinning token' },
90
+ { value: 'back', role: 'section', prefix: '--', label: 'Navigation' },
91
+ { value: 'back', label: 'keep credential', hint: 'return without changing storage access', role: 'utility' },
92
+ ]}
93
+ hintLayout="inline"
94
+ onSubmit={choice => choice === 'forget' ? onConfirmForget() : onCancel()}
95
+ onCancel={onCancel}
96
+ />
97
+ </Box>
98
+ </Surface>
99
+ )
100
+ }
101
+
102
+ return (
103
+ <Surface
104
+ title="IPFS Storage Credential"
105
+ subtitle="Controls whether ethagent can pin encrypted state from this machine."
106
+ footer={footer}
107
+ >
108
+ <Box marginTop={1}>
109
+ <Select<StorageCredentialAction>
110
+ options={[
111
+ { value: 'edit', role: 'section', prefix: '--', label: 'Credential' },
112
+ { value: 'edit', label: hasCredential ? 'replace credential' : 'save credential', hint: 'store Pinata JWT for IPFS pinning' },
113
+ { value: 'forget', label: 'forget credential', hint: 'remove the local pinning token; existing pins remain', disabled: !hasCredential },
114
+ { value: 'back', role: 'section', prefix: '--', label: 'Navigation' },
115
+ { value: 'back', label: 'back to identity hub', hint: 'return without changing storage access', role: 'utility' },
116
+ ]}
117
+ hintLayout="inline"
118
+ onSubmit={choice => {
119
+ if (choice === 'edit') return onEdit()
120
+ if (choice === 'forget') return onForget()
121
+ return onCancel()
122
+ }}
123
+ onCancel={onCancel}
124
+ />
125
+ </Box>
126
+ </Surface>
127
+ )
128
+ }
@@ -0,0 +1,43 @@
1
+ import React from 'react'
2
+ import { Box, Text } from 'ink'
3
+ import { exec } from 'node:child_process'
4
+ import { Surface } from '../../../ui/Surface.js'
5
+ import { Spinner } from '../../../ui/Spinner.js'
6
+ import { theme } from '../../../ui/theme.js'
7
+ import { useAppInput } from '../../../app/input/AppInputProvider.js'
8
+ import type { BrowserWalletReady } from '../../wallet/browserWallet.js'
9
+
10
+ type WalletApprovalScreenProps = {
11
+ title: string
12
+ subtitle: string
13
+ walletSession: BrowserWalletReady | null
14
+ label: string
15
+ onCancel?: () => void
16
+ }
17
+
18
+ export const WalletApprovalScreen: React.FC<WalletApprovalScreenProps> = ({ title, subtitle, walletSession, label, onCancel }) => {
19
+ useAppInput((_input, key) => {
20
+ if (key.escape && onCancel) onCancel()
21
+ if (key.return && walletSession?.url) {
22
+ const command = process.platform === 'win32' ? 'start ""' : process.platform === 'darwin' ? 'open' : 'xdg-open'
23
+ exec(`${command} "${walletSession.url}"`).on('error', () => {})
24
+ }
25
+ }, { isActive: Boolean(onCancel) || Boolean(walletSession) })
26
+ const footer = onCancel ? <Text color={theme.dim}>esc cancels</Text> : undefined
27
+ return (
28
+ <Surface title={title} subtitle={subtitle} footer={footer}>
29
+ {walletSession ? (
30
+ <Box flexDirection="column">
31
+ <Text color={theme.dim}>open this approval page</Text>
32
+ <Text color={theme.accentPrimary}>{walletSession.url}</Text>
33
+ <Text color={theme.dim}>press enter to open in browser...</Text>
34
+ <Box marginTop={1}>
35
+ <Spinner label={label} />
36
+ </Box>
37
+ </Box>
38
+ ) : (
39
+ <Spinner label="preparing wallet approval..." />
40
+ )}
41
+ </Surface>
42
+ )
43
+ }