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.
- package/LICENSE +21 -0
- package/README.md +114 -32
- package/bin/ethagent.js +11 -2
- package/package.json +25 -7
- package/src/app/FirstRun.tsx +412 -0
- package/src/app/hooks/useCancelRequest.ts +22 -0
- package/src/app/hooks/useDoublePress.ts +46 -0
- package/src/app/hooks/useExitOnCtrlC.ts +36 -0
- package/src/app/input/AppInputProvider.tsx +116 -0
- package/src/app/input/appInputParser.ts +279 -0
- package/src/app/keybindings/KeybindingProvider.tsx +134 -0
- package/src/app/keybindings/resolver.ts +42 -0
- package/src/app/keybindings/types.ts +26 -0
- package/src/chat/ChatBottomPane.tsx +280 -0
- package/src/chat/ChatInput.tsx +722 -0
- package/src/chat/ChatScreen.tsx +1575 -0
- package/src/chat/ContextLimitView.tsx +95 -0
- package/src/chat/ContinuityEditReviewView.tsx +48 -0
- package/src/chat/ConversationStack.tsx +47 -0
- package/src/chat/CopyPicker.tsx +52 -0
- package/src/chat/MessageList.tsx +609 -0
- package/src/chat/PermissionPrompt.tsx +153 -0
- package/src/chat/PermissionsView.tsx +159 -0
- package/src/chat/PlanApprovalView.tsx +91 -0
- package/src/chat/ResumeView.tsx +267 -0
- package/src/chat/RewindView.tsx +386 -0
- package/src/chat/SessionStatus.tsx +51 -0
- package/src/chat/TranscriptView.tsx +202 -0
- package/src/chat/chatInputState.ts +247 -0
- package/src/chat/chatPaste.ts +49 -0
- package/src/chat/chatScreenUtils.ts +187 -0
- package/src/chat/chatSessionState.ts +142 -0
- package/src/chat/chatTurnOrchestrator.ts +701 -0
- package/src/chat/commands.ts +673 -0
- package/src/chat/textCursor.ts +202 -0
- package/src/chat/toolResultDisplay.ts +8 -0
- package/src/chat/transcriptViewport.ts +247 -0
- package/src/cli/ResetConfirmView.tsx +61 -0
- package/src/cli/main.tsx +177 -0
- package/src/cli/preview.tsx +19 -0
- package/src/cli/reset.ts +106 -0
- package/src/identity/continuity/editor.ts +149 -0
- package/src/identity/continuity/envelope.ts +345 -0
- package/src/identity/continuity/history.ts +153 -0
- package/src/identity/continuity/privateEdit.ts +334 -0
- package/src/identity/continuity/publicSkills.ts +173 -0
- package/src/identity/continuity/snapshots.ts +183 -0
- package/src/identity/continuity/storage.ts +507 -0
- package/src/identity/crypto/backupEnvelope.ts +486 -0
- package/src/identity/crypto/eth.ts +137 -0
- package/src/identity/hub/IdentityHub.tsx +868 -0
- package/src/identity/hub/identityHubEffects.ts +1146 -0
- package/src/identity/hub/identityHubModel.ts +291 -0
- package/src/identity/hub/identityHubReducer.ts +212 -0
- package/src/identity/hub/screens/BusyScreen.tsx +26 -0
- package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +144 -0
- package/src/identity/hub/screens/CreateFlow.tsx +206 -0
- package/src/identity/hub/screens/DetailsScreen.tsx +64 -0
- package/src/identity/hub/screens/EditProfileFlow.tsx +145 -0
- package/src/identity/hub/screens/ErrorScreen.tsx +35 -0
- package/src/identity/hub/screens/IdentitySummary.tsx +70 -0
- package/src/identity/hub/screens/MenuScreen.tsx +117 -0
- package/src/identity/hub/screens/NetworkScreen.tsx +41 -0
- package/src/identity/hub/screens/RebackupStorageScreen.tsx +50 -0
- package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +85 -0
- package/src/identity/hub/screens/RestoreFlow.tsx +206 -0
- package/src/identity/hub/screens/StorageCredentialScreen.tsx +128 -0
- package/src/identity/hub/screens/WalletApprovalScreen.tsx +43 -0
- package/src/identity/profile/imagePicker.ts +180 -0
- package/src/identity/registry/erc8004.ts +1106 -0
- package/src/identity/registry/registryConfig.ts +69 -0
- package/src/identity/storage/ipfs.ts +212 -0
- package/src/identity/storage/pinataJwt.ts +53 -0
- package/src/identity/wallet/browserWallet.ts +393 -0
- package/src/identity/wallet/wallet-page/wallet.html +1082 -0
- package/src/mcp/approvals.ts +113 -0
- package/src/mcp/config.ts +235 -0
- package/src/mcp/manager.ts +541 -0
- package/src/mcp/names.ts +19 -0
- package/src/mcp/output.ts +96 -0
- package/src/models/ModelPicker.tsx +1446 -0
- package/src/models/catalog.ts +296 -0
- package/src/models/huggingface.ts +651 -0
- package/src/models/llamacpp.ts +810 -0
- package/src/models/llamacppPreflight.ts +150 -0
- package/src/models/modelDisplay.ts +105 -0
- package/src/models/modelPickerOptions.ts +421 -0
- package/src/models/modelRecommendation.ts +140 -0
- package/src/models/runtimeDetection.ts +81 -0
- package/src/models/uncensoredCatalog.ts +86 -0
- package/src/providers/anthropic.ts +259 -0
- package/src/providers/contracts.ts +62 -0
- package/src/providers/errors.ts +62 -0
- package/src/providers/gemini.ts +152 -0
- package/src/providers/openai-chat.ts +472 -0
- package/src/providers/registry.ts +42 -0
- package/src/providers/retry.ts +58 -0
- package/src/providers/sse.ts +93 -0
- package/src/runtime/compaction.ts +389 -0
- package/src/runtime/cwd.ts +43 -0
- package/src/runtime/sessionMode.ts +55 -0
- package/src/runtime/systemPrompt.ts +209 -0
- package/src/runtime/toolClaimGuards.ts +143 -0
- package/src/runtime/toolExecution.ts +304 -0
- package/src/runtime/toolIntent.ts +163 -0
- package/src/runtime/turn.ts +858 -0
- package/src/storage/atomicWrite.ts +68 -0
- package/src/storage/config.ts +189 -0
- package/src/storage/factoryReset.ts +130 -0
- package/src/storage/history.ts +58 -0
- package/src/storage/identity.ts +99 -0
- package/src/storage/permissions.ts +76 -0
- package/src/storage/rewind.ts +246 -0
- package/src/storage/secrets.ts +181 -0
- package/src/storage/sessionExport.ts +49 -0
- package/src/storage/sessions.ts +482 -0
- package/src/tools/bashSafety.ts +174 -0
- package/src/tools/bashTool.ts +140 -0
- package/src/tools/changeDirectoryTool.ts +213 -0
- package/src/tools/contracts.ts +179 -0
- package/src/tools/deleteFileTool.ts +111 -0
- package/src/tools/editTool.ts +160 -0
- package/src/tools/editUtils.ts +170 -0
- package/src/tools/listDirectoryTool.ts +55 -0
- package/src/tools/mcpResourceTools.ts +95 -0
- package/src/tools/permissionRules.ts +85 -0
- package/src/tools/privateContinuityEditTool.ts +178 -0
- package/src/tools/privateContinuityReadTool.ts +107 -0
- package/src/tools/readTool.ts +85 -0
- package/src/tools/registry.ts +67 -0
- package/src/tools/writeFileTool.ts +142 -0
- package/src/ui/BrandSplash.tsx +193 -0
- package/src/ui/ProgressBar.tsx +34 -0
- package/src/ui/Select.tsx +143 -0
- package/src/ui/Spinner.tsx +269 -0
- package/src/ui/Surface.tsx +47 -0
- package/src/ui/TextInput.tsx +97 -0
- package/src/ui/theme.ts +59 -0
- package/src/utils/clipboard.ts +216 -0
- package/src/utils/markdownSegments.ts +51 -0
- package/src/utils/messages.ts +35 -0
- package/src/utils/withRetry.ts +280 -0
- 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
|
+
}
|