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,206 @@
|
|
|
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 { normalizeErc8004RegistryConfig } from '../../registry/erc8004.js'
|
|
9
|
+
import { networkLabel } from '../identityHubModel.js'
|
|
10
|
+
import type { Step } from '../identityHubReducer.js'
|
|
11
|
+
import { createStepNumber, CREATE_STEP_LABELS } from '../identityHubReducer.js'
|
|
12
|
+
import { WalletApprovalScreen } from './WalletApprovalScreen.js'
|
|
13
|
+
import { BusyScreen } from './BusyScreen.js'
|
|
14
|
+
import type { BrowserWalletReady } from '../../wallet/browserWallet.js'
|
|
15
|
+
|
|
16
|
+
const PINATA_API_KEYS_URL = 'https://app.pinata.cloud/developers/api-keys'
|
|
17
|
+
|
|
18
|
+
type StepIndicatorProps = {
|
|
19
|
+
steps: string[]
|
|
20
|
+
current: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const StepIndicator: React.FC<StepIndicatorProps> = ({ steps, current }) => {
|
|
24
|
+
const parts = steps.map((step, index) => {
|
|
25
|
+
const n = index + 1
|
|
26
|
+
const active = n === current
|
|
27
|
+
const done = n < current
|
|
28
|
+
return `${done ? 'done' : n}. ${step}${active ? ' <' : ''}`
|
|
29
|
+
})
|
|
30
|
+
return <Text color={theme.dim}>{parts.join(' · ')}</Text>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type CreateFlowProps = {
|
|
34
|
+
step: Extract<Step, {
|
|
35
|
+
kind:
|
|
36
|
+
| 'replace-confirm'
|
|
37
|
+
| 'create-name'
|
|
38
|
+
| 'create-description'
|
|
39
|
+
| 'create-preflight'
|
|
40
|
+
| 'create-registry'
|
|
41
|
+
| 'create-signing'
|
|
42
|
+
| 'create-storage'
|
|
43
|
+
}>
|
|
44
|
+
walletSession: BrowserWalletReady | null
|
|
45
|
+
onSetStep: (step: Step) => void
|
|
46
|
+
onNameSubmit: (name: string) => void
|
|
47
|
+
onDescriptionSubmit: (name: string, description: string) => void
|
|
48
|
+
onRegistrySubmit: (value: string) => void
|
|
49
|
+
onStorageSubmit: (input: string) => void
|
|
50
|
+
onStorageError: (error: string) => void
|
|
51
|
+
onBack: () => void
|
|
52
|
+
onMenu: () => void
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const CreateFlow: React.FC<CreateFlowProps> = ({
|
|
56
|
+
step,
|
|
57
|
+
walletSession,
|
|
58
|
+
onSetStep,
|
|
59
|
+
onNameSubmit,
|
|
60
|
+
onDescriptionSubmit,
|
|
61
|
+
onRegistrySubmit,
|
|
62
|
+
onStorageSubmit,
|
|
63
|
+
onBack,
|
|
64
|
+
onMenu,
|
|
65
|
+
}) => {
|
|
66
|
+
const stepNum = createStepNumber(step)
|
|
67
|
+
const indicator = stepNum > 0
|
|
68
|
+
? <StepIndicator steps={CREATE_STEP_LABELS} current={stepNum} />
|
|
69
|
+
: null
|
|
70
|
+
|
|
71
|
+
if (step.kind === 'replace-confirm') {
|
|
72
|
+
return (
|
|
73
|
+
<Surface title="Create a New Agent?" footer="enter selects · esc back">
|
|
74
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
75
|
+
<Text color={theme.dim}>
|
|
76
|
+
Your current agent stays in your wallet and remains loadable.
|
|
77
|
+
</Text>
|
|
78
|
+
<Text color={theme.dim}>
|
|
79
|
+
This mints a new agent to this wallet and uses it on this machine.
|
|
80
|
+
</Text>
|
|
81
|
+
</Box>
|
|
82
|
+
<Select<'replace' | 'back'>
|
|
83
|
+
options={[
|
|
84
|
+
{ value: 'back', role: 'section', prefix: '--', label: 'Current identity' },
|
|
85
|
+
{ value: 'back', label: 'keep current agent', hint: 'return without minting anything', role: 'utility' },
|
|
86
|
+
{ value: 'replace', role: 'section', prefix: '--', label: 'New identity' },
|
|
87
|
+
{ value: 'replace', label: 'mint and use new agent', hint: 'create separate token and make it active' },
|
|
88
|
+
]}
|
|
89
|
+
hintLayout="inline"
|
|
90
|
+
onSubmit={choice => {
|
|
91
|
+
if (choice === 'back') return onMenu()
|
|
92
|
+
return onSetStep({ kind: 'create-name' })
|
|
93
|
+
}}
|
|
94
|
+
onCancel={onBack}
|
|
95
|
+
/>
|
|
96
|
+
</Surface>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (step.kind === 'create-name') {
|
|
101
|
+
return (
|
|
102
|
+
<Surface title="Name Your Agent" subtitle={indicator} footer="enter continues · esc back">
|
|
103
|
+
{step.error ? <Text color="#e87070">{step.error}</Text> : null}
|
|
104
|
+
<TextInput
|
|
105
|
+
key="agent-name"
|
|
106
|
+
placeholder="agent name"
|
|
107
|
+
validate={value => value.trim().length >= 2 ? null : 'name must be at least 2 characters'}
|
|
108
|
+
onSubmit={name => onNameSubmit(name.trim())}
|
|
109
|
+
onCancel={onBack}
|
|
110
|
+
/>
|
|
111
|
+
</Surface>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (step.kind === 'create-description') {
|
|
116
|
+
return (
|
|
117
|
+
<Surface title="Describe Your Agent" subtitle={indicator} footer="enter continues · esc back">
|
|
118
|
+
<Text color={theme.dim}>Optional. One short sentence is enough.</Text>
|
|
119
|
+
<TextInput
|
|
120
|
+
key="agent-description"
|
|
121
|
+
placeholder="description"
|
|
122
|
+
onSubmit={description => onDescriptionSubmit(step.name, description.trim())}
|
|
123
|
+
onCancel={onBack}
|
|
124
|
+
/>
|
|
125
|
+
</Surface>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (step.kind === 'create-preflight') {
|
|
130
|
+
return (
|
|
131
|
+
<BusyScreen
|
|
132
|
+
title="Getting Ready"
|
|
133
|
+
subtitle={indicator}
|
|
134
|
+
label="checking IPFS storage and chain..."
|
|
135
|
+
onCancel={onBack}
|
|
136
|
+
/>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (step.kind === 'create-registry') {
|
|
141
|
+
return (
|
|
142
|
+
<Surface
|
|
143
|
+
title={`${step.resolution.network ? networkLabel(step.resolution.network).charAt(0).toUpperCase() + networkLabel(step.resolution.network).slice(1) : ''} Agent Registry`}
|
|
144
|
+
subtitle={step.error ?? 'Paste the agent registry address for this network.'}
|
|
145
|
+
footer="enter continues · esc back"
|
|
146
|
+
>
|
|
147
|
+
<Text color={theme.dim}>RPC defaults to {step.resolution.defaultRpcUrl}</Text>
|
|
148
|
+
<TextInput
|
|
149
|
+
key={`create-registry-${step.resolution.network}`}
|
|
150
|
+
placeholder="0x registry address"
|
|
151
|
+
validate={value => {
|
|
152
|
+
try {
|
|
153
|
+
normalizeErc8004RegistryConfig({ chainId: step.resolution.chainId, identityRegistryAddress: value.trim() })
|
|
154
|
+
return null
|
|
155
|
+
} catch (err: unknown) {
|
|
156
|
+
return (err as Error).message
|
|
157
|
+
}
|
|
158
|
+
}}
|
|
159
|
+
onSubmit={onRegistrySubmit}
|
|
160
|
+
onCancel={onBack}
|
|
161
|
+
/>
|
|
162
|
+
</Surface>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (step.kind === 'create-signing') {
|
|
167
|
+
return (
|
|
168
|
+
<WalletApprovalScreen
|
|
169
|
+
title="Approve in Wallet"
|
|
170
|
+
subtitle="One browser flow signs, saves the IPFS backup, and submits the token transaction."
|
|
171
|
+
walletSession={walletSession}
|
|
172
|
+
label="waiting for wallet approval..."
|
|
173
|
+
onCancel={onBack}
|
|
174
|
+
/>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<Surface
|
|
180
|
+
title="Connect IPFS Storage"
|
|
181
|
+
subtitle={step.error ?? 'Save a Pinata JWT so ethagent can pin encrypted state to IPFS.'}
|
|
182
|
+
footer="enter continues · esc back"
|
|
183
|
+
>
|
|
184
|
+
<Text>
|
|
185
|
+
<Text color={theme.dim}>Paste your Pinata JWT. Get one at </Text>
|
|
186
|
+
<Text color={theme.accentPrimary} underline>{PINATA_API_KEYS_URL}</Text>
|
|
187
|
+
</Text>
|
|
188
|
+
<Text color={theme.dim}>Saved encrypted on this device · used only for IPFS pinning</Text>
|
|
189
|
+
<TextInput
|
|
190
|
+
key="create-storage"
|
|
191
|
+
isSecret
|
|
192
|
+
placeholder="Pinata JWT"
|
|
193
|
+
validate={v => {
|
|
194
|
+
try {
|
|
195
|
+
extractPinataJwt(v)
|
|
196
|
+
return null
|
|
197
|
+
} catch (err: unknown) {
|
|
198
|
+
return (err as Error).message
|
|
199
|
+
}
|
|
200
|
+
}}
|
|
201
|
+
onSubmit={onStorageSubmit}
|
|
202
|
+
onCancel={onBack}
|
|
203
|
+
/>
|
|
204
|
+
</Surface>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box } from 'ink'
|
|
3
|
+
import { Surface } from '../../../ui/Surface.js'
|
|
4
|
+
import { Select, type SelectOption } from '../../../ui/Select.js'
|
|
5
|
+
import type { EthagentConfig, EthagentIdentity } from '../../../storage/config.js'
|
|
6
|
+
import { copyableIdentityFields } from '../identityHubModel.js'
|
|
7
|
+
import { IdentitySummary } from './IdentitySummary.js'
|
|
8
|
+
|
|
9
|
+
type CopyAction = `copy:${string}` | 'back'
|
|
10
|
+
|
|
11
|
+
type DetailsScreenProps = {
|
|
12
|
+
identity?: EthagentIdentity
|
|
13
|
+
config?: EthagentConfig
|
|
14
|
+
copyNotice?: string | null
|
|
15
|
+
footer: React.ReactNode
|
|
16
|
+
onCopy: (label: string, value: string) => void
|
|
17
|
+
onBack: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DetailsScreen: React.FC<DetailsScreenProps> = ({
|
|
21
|
+
identity,
|
|
22
|
+
config,
|
|
23
|
+
copyNotice,
|
|
24
|
+
footer,
|
|
25
|
+
onCopy,
|
|
26
|
+
onBack,
|
|
27
|
+
}) => {
|
|
28
|
+
const copyable = copyableIdentityFields(identity)
|
|
29
|
+
const options: Array<SelectOption<CopyAction>> = [
|
|
30
|
+
...(copyable.length > 0 ? [{ value: 'back' as const, role: 'section' as const, prefix: '--', label: 'Values' }] : []),
|
|
31
|
+
...copyable.map(field => ({
|
|
32
|
+
value: `copy:${field.label}` as const,
|
|
33
|
+
label: field.label,
|
|
34
|
+
hint: shortPreview(field.value),
|
|
35
|
+
})),
|
|
36
|
+
...(copyable.length === 0 ? [{ value: 'back' as const, role: 'notice' as const, label: 'no values available yet' }] : []),
|
|
37
|
+
{ value: 'back', role: 'section', prefix: '--', label: 'Navigation' },
|
|
38
|
+
{ value: 'back', label: 'back to identity hub', hint: 'return without copying', role: 'utility' },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Surface title="Copy Identity Values" subtitle={copyNotice ?? 'Choose one value to copy.'} footer={footer}>
|
|
43
|
+
<IdentitySummary identity={identity} config={config} compact />
|
|
44
|
+
<Box marginTop={1}>
|
|
45
|
+
<Select<CopyAction>
|
|
46
|
+
options={options}
|
|
47
|
+
hintLayout="inline"
|
|
48
|
+
onSubmit={choice => {
|
|
49
|
+
if (choice === 'back') return onBack()
|
|
50
|
+
const label = choice.slice('copy:'.length)
|
|
51
|
+
const found = copyable.find(field => field.label === label)
|
|
52
|
+
if (found) onCopy(found.label, found.value)
|
|
53
|
+
}}
|
|
54
|
+
onCancel={onBack}
|
|
55
|
+
/>
|
|
56
|
+
</Box>
|
|
57
|
+
</Surface>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function shortPreview(value: string): string {
|
|
62
|
+
if (value.length <= 42) return value
|
|
63
|
+
return `${value.slice(0, 18)}...${value.slice(-14)}`
|
|
64
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
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 type { Step } from '../identityHubReducer.js'
|
|
8
|
+
|
|
9
|
+
type EditProfileFlowProps = {
|
|
10
|
+
step: Extract<Step, { kind: 'edit-profile-name' | 'edit-profile-description' | 'edit-profile-image' }>
|
|
11
|
+
onNameSubmit: (name: string) => void
|
|
12
|
+
onDescriptionSubmit: (description: string) => void
|
|
13
|
+
onImageSubmit: (imagePath: string) => void
|
|
14
|
+
onImagePick: () => void
|
|
15
|
+
onBack: () => void
|
|
16
|
+
onMenu: () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const footerHint = (hint: string) => <Text color={theme.dim}>{hint}</Text>
|
|
20
|
+
|
|
21
|
+
export const EditProfileFlow: React.FC<EditProfileFlowProps> = ({
|
|
22
|
+
step,
|
|
23
|
+
onNameSubmit,
|
|
24
|
+
onDescriptionSubmit,
|
|
25
|
+
onImageSubmit,
|
|
26
|
+
onImagePick,
|
|
27
|
+
onBack,
|
|
28
|
+
onMenu,
|
|
29
|
+
}) => {
|
|
30
|
+
const [manualImagePath, setManualImagePath] = React.useState(false)
|
|
31
|
+
|
|
32
|
+
if (step.kind === 'edit-profile-name') {
|
|
33
|
+
const currentName = readStateString(step.identity.state, 'name')
|
|
34
|
+
return (
|
|
35
|
+
<Surface
|
|
36
|
+
title="Rename Agent Identity"
|
|
37
|
+
subtitle="This updates public token metadata and the Agent Card."
|
|
38
|
+
footer={footerHint('enter continues · esc back')}
|
|
39
|
+
>
|
|
40
|
+
<Text color={theme.dim}>Currently: {currentName || '(unnamed)'}</Text>
|
|
41
|
+
<TextInput
|
|
42
|
+
key="edit-profile-name"
|
|
43
|
+
initialValue={currentName}
|
|
44
|
+
placeholder="agent name"
|
|
45
|
+
validate={value => value.trim().length >= 2 ? null : 'name must be at least 2 characters'}
|
|
46
|
+
onSubmit={value => onNameSubmit(value.trim())}
|
|
47
|
+
onCancel={onMenu}
|
|
48
|
+
/>
|
|
49
|
+
</Surface>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (step.kind === 'edit-profile-image') {
|
|
54
|
+
const currentImage = readStateString(step.identity.state, 'imageUrl')
|
|
55
|
+
if (!manualImagePath) {
|
|
56
|
+
return (
|
|
57
|
+
<Surface
|
|
58
|
+
title="Upload Agent Image"
|
|
59
|
+
subtitle="Choose a local image. ethagent uploads it to IPFS and attaches it to token metadata."
|
|
60
|
+
footer={footerHint('enter select · esc back')}
|
|
61
|
+
>
|
|
62
|
+
<Box flexDirection="column">
|
|
63
|
+
<Text color={theme.dim}>Current: {currentImage || '(no image)'}</Text>
|
|
64
|
+
{step.error ? <Text color="#e87070">{step.error}</Text> : null}
|
|
65
|
+
</Box>
|
|
66
|
+
<Box marginTop={1}>
|
|
67
|
+
<Select<'choose' | 'manual' | 'skip' | 'delete' | 'back'>
|
|
68
|
+
options={[
|
|
69
|
+
{ value: 'choose', role: 'section', prefix: '--', label: 'Image' },
|
|
70
|
+
{ value: 'choose', label: 'choose image file', hint: 'open the operating system file picker' },
|
|
71
|
+
{ value: 'manual', label: 'enter path manually', hint: 'fallback if a file picker is unavailable' },
|
|
72
|
+
...(currentImage ? [{ value: 'delete' as const, label: 'delete current image', hint: 'remove the attached image from public profile' }] : []),
|
|
73
|
+
{ value: 'skip', label: currentImage ? 'keep current image' : 'no image', hint: 'publish without changing the image' },
|
|
74
|
+
{ value: 'back', role: 'section', prefix: '--', label: 'Navigation' },
|
|
75
|
+
{ value: 'back', label: 'back', hint: 'return to description', role: 'utility' },
|
|
76
|
+
]}
|
|
77
|
+
hintLayout="inline"
|
|
78
|
+
onSubmit={choice => {
|
|
79
|
+
if (choice === 'choose') return onImagePick()
|
|
80
|
+
if (choice === 'manual') return setManualImagePath(true)
|
|
81
|
+
if (choice === 'delete') return onImageSubmit('delete')
|
|
82
|
+
if (choice === 'skip') return onImageSubmit('')
|
|
83
|
+
return onBack()
|
|
84
|
+
}}
|
|
85
|
+
onCancel={onBack}
|
|
86
|
+
/>
|
|
87
|
+
</Box>
|
|
88
|
+
</Surface>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
return (
|
|
92
|
+
<Surface
|
|
93
|
+
title="Enter Image Path"
|
|
94
|
+
subtitle="Fallback path entry. URLs are rejected because ethagent uploads the local file itself."
|
|
95
|
+
footer={footerHint('enter saves · esc back')}
|
|
96
|
+
>
|
|
97
|
+
<Text color={theme.dim}>Current: {currentImage || '(no image)'}</Text>
|
|
98
|
+
<TextInput
|
|
99
|
+
key="edit-profile-image"
|
|
100
|
+
placeholder="local image path"
|
|
101
|
+
allowEmpty
|
|
102
|
+
validate={value => validateImagePath(value)}
|
|
103
|
+
onSubmit={value => onImageSubmit(value.trim())}
|
|
104
|
+
onCancel={() => setManualImagePath(false)}
|
|
105
|
+
/>
|
|
106
|
+
</Surface>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const currentDescription = readStateString(step.identity.state, 'description')
|
|
111
|
+
return (
|
|
112
|
+
<Surface
|
|
113
|
+
title="Describe Agent Identity"
|
|
114
|
+
subtitle="This updates public token metadata and the Agent Card."
|
|
115
|
+
footer={footerHint('enter continues · esc back')}
|
|
116
|
+
>
|
|
117
|
+
<Text color={theme.dim}>Currently: {currentDescription || '(no description)'}</Text>
|
|
118
|
+
<TextInput
|
|
119
|
+
key="edit-profile-description"
|
|
120
|
+
initialValue={currentDescription}
|
|
121
|
+
placeholder="description"
|
|
122
|
+
allowEmpty
|
|
123
|
+
onSubmit={value => onDescriptionSubmit(value.trim())}
|
|
124
|
+
onCancel={onBack}
|
|
125
|
+
/>
|
|
126
|
+
</Surface>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validateImagePath(value: string): string | null {
|
|
131
|
+
const trimmed = value.trim()
|
|
132
|
+
if (!trimmed) return null
|
|
133
|
+
if (/^https?:\/\//i.test(trimmed) || /^ipfs:\/\//i.test(trimmed)) {
|
|
134
|
+
return 'enter a local image file path; ethagent will upload it to IPFS'
|
|
135
|
+
}
|
|
136
|
+
if (!/\.(png|jpe?g|gif|webp|svg)$/i.test(trimmed)) {
|
|
137
|
+
return 'image must be png, jpg, gif, webp, or svg'
|
|
138
|
+
}
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function readStateString(state: Record<string, unknown> | undefined, key: string): string {
|
|
143
|
+
const value = state?.[key]
|
|
144
|
+
return typeof value === 'string' ? value : ''
|
|
145
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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 { theme } from '../../../ui/theme.js'
|
|
6
|
+
import type { IdentityHubErrorView } from '../identityHubModel.js'
|
|
7
|
+
import type { Step } from '../identityHubReducer.js'
|
|
8
|
+
|
|
9
|
+
type ErrorScreenProps = {
|
|
10
|
+
error: IdentityHubErrorView
|
|
11
|
+
back: Step
|
|
12
|
+
footer: React.ReactNode
|
|
13
|
+
onBack: (back: Step) => void
|
|
14
|
+
onClose: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const ErrorScreen: React.FC<ErrorScreenProps> = ({ error, back, footer, onBack, onClose }) => (
|
|
18
|
+
<Surface title={error.title} tone="error" subtitle={error.detail} footer={footer}>
|
|
19
|
+
{error.hint ? <Text color={theme.dim}>{error.hint}</Text> : null}
|
|
20
|
+
<Select<'back' | 'close'>
|
|
21
|
+
options={[
|
|
22
|
+
{ value: 'back', role: 'section', prefix: '--', label: 'Recovery' },
|
|
23
|
+
{ value: 'back', label: 'go back', hint: 'return to the previous identity step' },
|
|
24
|
+
{ value: 'close', role: 'section', prefix: '--', label: 'Exit' },
|
|
25
|
+
{ value: 'close', label: 'close hub', hint: 'return to the chat without retrying', role: 'utility' },
|
|
26
|
+
]}
|
|
27
|
+
hintLayout="inline"
|
|
28
|
+
onSubmit={choice => {
|
|
29
|
+
if (choice === 'back') onBack(back)
|
|
30
|
+
else onClose()
|
|
31
|
+
}}
|
|
32
|
+
onCancel={() => onBack(back)}
|
|
33
|
+
/>
|
|
34
|
+
</Surface>
|
|
35
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import { theme } from '../../../ui/theme.js'
|
|
4
|
+
import type { EthagentConfig, EthagentIdentity } from '../../../storage/config.js'
|
|
5
|
+
import { identitySummaryRows, lastBackupLabel } from '../identityHubModel.js'
|
|
6
|
+
|
|
7
|
+
import type { ContinuityWorkingTreeStatus } from '../../continuity/storage.js'
|
|
8
|
+
|
|
9
|
+
export const IdentitySummary: React.FC<{
|
|
10
|
+
identity?: EthagentIdentity
|
|
11
|
+
config?: EthagentConfig
|
|
12
|
+
workingStatus?: ContinuityWorkingTreeStatus | null
|
|
13
|
+
compact?: boolean
|
|
14
|
+
}> = ({ identity, config, workingStatus, compact = false }) => {
|
|
15
|
+
if (!identity) {
|
|
16
|
+
return (
|
|
17
|
+
<Text color={theme.dim}>no agent yet. create or load one.</Text>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const rows = identitySummaryRows(identity, config)
|
|
22
|
+
const lastBackup = lastBackupLabel(identity)
|
|
23
|
+
const stateName = typeof (identity.state as Record<string, unknown> | undefined)?.name === 'string'
|
|
24
|
+
? ((identity.state as Record<string, unknown>).name as string).trim()
|
|
25
|
+
: ''
|
|
26
|
+
|
|
27
|
+
const row = (label: string) => rows.find(item => item.label === label)
|
|
28
|
+
|
|
29
|
+
const needsBackup = workingStatus?.publishState === 'local-changes' || workingStatus?.publishState === 'not-published' || workingStatus?.publishState === 'verify-needed'
|
|
30
|
+
let changedFiles: string[] = []
|
|
31
|
+
if (needsBackup) {
|
|
32
|
+
if (workingStatus?.localContentHashes && workingStatus?.publishedContentHashes) {
|
|
33
|
+
if (workingStatus.localContentHashes['SOUL.md'] !== workingStatus.publishedContentHashes['SOUL.md']) changedFiles.push('SOUL.md')
|
|
34
|
+
if (workingStatus.localContentHashes['MEMORY.md'] !== workingStatus.publishedContentHashes['MEMORY.md']) changedFiles.push('MEMORY.md')
|
|
35
|
+
if (workingStatus.localContentHashes['skills.json'] !== workingStatus.publishedContentHashes['skills.json']) changedFiles.push('skills.json')
|
|
36
|
+
} else {
|
|
37
|
+
changedFiles = ['SOUL.md', 'MEMORY.md', 'skills.json']
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const lastSavedRow = needsBackup
|
|
42
|
+
? { label: 'unsaved', value: changedFiles.length > 0 ? changedFiles.join(', ') : 'markdown files', tone: 'warn' as const, highlight: true }
|
|
43
|
+
: { label: 'last saved', value: lastBackup, tone: lastBackup === 'never' ? 'dim' as const : 'ok' as const }
|
|
44
|
+
|
|
45
|
+
const summaryRows = [
|
|
46
|
+
{ label: 'token', value: row('token')?.value ?? 'not created', tone: row('token')?.tone ?? 'dim', highlight: true },
|
|
47
|
+
{ label: 'network', value: row('network')?.value ?? 'unknown', tone: row('network')?.tone ?? 'dim' },
|
|
48
|
+
{ label: 'owner', value: row('owner')?.value ?? 'not connected', tone: row('owner')?.tone ?? 'dim' },
|
|
49
|
+
{ label: 'snapshot', value: row('state')?.value ?? 'not saved yet', tone: row('state')?.tone ?? 'dim', highlight: true },
|
|
50
|
+
lastSavedRow,
|
|
51
|
+
{ label: 'skills', value: row('skills')?.value ?? 'not published', tone: row('skills')?.tone ?? 'dim' },
|
|
52
|
+
{ label: 'agent card', value: row('card')?.value ?? 'not published', tone: row('card')?.tone ?? 'dim' },
|
|
53
|
+
{ label: 'image', value: row('image')?.value ?? 'not attached', tone: row('image')?.tone ?? 'dim' },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Box flexDirection="column">
|
|
58
|
+
<Text color={theme.accentPrimary} bold>{stateName || 'active agent'}</Text>
|
|
59
|
+
{summaryRows.map(row => {
|
|
60
|
+
const valueColor = row.tone === 'warn' ? 'red' : (row.tone === 'ok' ? theme.text : theme.dim)
|
|
61
|
+
return (
|
|
62
|
+
<Text key={row.label}>
|
|
63
|
+
<Text color={theme.dim}>{row.label.padEnd(12)}</Text>
|
|
64
|
+
<Text color={valueColor} bold={row.highlight}>{row.value}</Text>
|
|
65
|
+
</Text>
|
|
66
|
+
)
|
|
67
|
+
})}
|
|
68
|
+
</Box>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box } from 'ink'
|
|
3
|
+
import { Surface } from '../../../ui/Surface.js'
|
|
4
|
+
import { Select, type SelectOption } from '../../../ui/Select.js'
|
|
5
|
+
import type { EthagentConfig, EthagentIdentity } from '../../../storage/config.js'
|
|
6
|
+
import type { ContinuityWorkingTreeStatus } from '../../continuity/storage.js'
|
|
7
|
+
import { IdentitySummary } from './IdentitySummary.js'
|
|
8
|
+
|
|
9
|
+
type MenuScreenProps = {
|
|
10
|
+
mode: 'first-run' | 'manage'
|
|
11
|
+
config?: EthagentConfig
|
|
12
|
+
identity?: EthagentIdentity
|
|
13
|
+
workingStatus?: ContinuityWorkingTreeStatus | null
|
|
14
|
+
canRebackup: boolean
|
|
15
|
+
footer: React.ReactNode
|
|
16
|
+
onCreate: () => void
|
|
17
|
+
onLoad: () => void
|
|
18
|
+
onBackupNow: () => void
|
|
19
|
+
onRefetchLatest: () => void
|
|
20
|
+
onPublicProfile: () => void
|
|
21
|
+
onPrivateMemory: () => void
|
|
22
|
+
onCopyValues: () => void
|
|
23
|
+
onStorageCredential: () => void
|
|
24
|
+
onSkip: () => void
|
|
25
|
+
onCancel: () => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type Action =
|
|
29
|
+
| 'public-profile'
|
|
30
|
+
| 'private-memory'
|
|
31
|
+
| 'backup'
|
|
32
|
+
| 'refetch'
|
|
33
|
+
| 'copy'
|
|
34
|
+
| 'storage-credential'
|
|
35
|
+
| 'create'
|
|
36
|
+
| 'load'
|
|
37
|
+
| 'skip'
|
|
38
|
+
| 'cancel'
|
|
39
|
+
|
|
40
|
+
export const MenuScreen: React.FC<MenuScreenProps> = ({
|
|
41
|
+
mode,
|
|
42
|
+
config,
|
|
43
|
+
identity,
|
|
44
|
+
workingStatus,
|
|
45
|
+
canRebackup,
|
|
46
|
+
footer,
|
|
47
|
+
onCreate,
|
|
48
|
+
onLoad,
|
|
49
|
+
onBackupNow,
|
|
50
|
+
onRefetchLatest,
|
|
51
|
+
onPublicProfile,
|
|
52
|
+
onPrivateMemory,
|
|
53
|
+
onCopyValues,
|
|
54
|
+
onStorageCredential,
|
|
55
|
+
onSkip,
|
|
56
|
+
onCancel,
|
|
57
|
+
}) => {
|
|
58
|
+
const title = mode === 'first-run' ? 'Set Up Agent Identity' : 'Agent Identity'
|
|
59
|
+
const subtitle = mode === 'first-run'
|
|
60
|
+
? 'Create a portable agent or load one you already own.'
|
|
61
|
+
: 'Public, private, recovery, storage, and device controls are separate.'
|
|
62
|
+
|
|
63
|
+
const canRefetch = Boolean(canRebackup && identity?.backup?.cid)
|
|
64
|
+
|
|
65
|
+
const options: Array<SelectOption<Action>> = identity
|
|
66
|
+
? [
|
|
67
|
+
{ value: 'public-profile', role: 'section', prefix: '--', label: 'Public metadata' },
|
|
68
|
+
{ value: 'public-profile', label: 'public profile', hint: 'name, image, skills.json, agent card' },
|
|
69
|
+
{ value: 'private-memory', role: 'section', prefix: '--', label: 'Private local files' },
|
|
70
|
+
{ value: 'private-memory', label: 'memory and persona', hint: 'SOUL.md and MEMORY.md only on this device' },
|
|
71
|
+
{ value: 'backup', role: 'section', prefix: '--', label: 'Recovery' },
|
|
72
|
+
{ value: 'backup', label: 'publish snapshot now', hint: 'encrypts and publishes local SOUL.md and MEMORY.md changes', disabled: !canRebackup },
|
|
73
|
+
{ value: 'refetch', label: 'refetch latest snapshot', hint: 'restore local files from the latest published snapshot', disabled: !canRefetch },
|
|
74
|
+
{ value: 'storage-credential', role: 'section', prefix: '--', label: 'Storage' },
|
|
75
|
+
{ value: 'storage-credential', label: 'IPFS credential', hint: 'save, replace, or forget Pinata JWT' },
|
|
76
|
+
{ value: 'copy', role: 'section', prefix: '--', label: 'Agent token' },
|
|
77
|
+
{ value: 'copy', label: 'copy values', hint: 'copy CIDs, token id, URI, or owner' },
|
|
78
|
+
{ value: 'load', label: 'switch agent', hint: 'load a different token owned by your wallet' },
|
|
79
|
+
{ value: 'create', label: 'new agent', hint: 'mint another token and make it active here' },
|
|
80
|
+
{ value: 'cancel', role: 'section', prefix: '--', label: 'Exit' },
|
|
81
|
+
{ value: 'cancel', label: 'close hub', hint: 'return to the chat without changing identity', role: 'utility' },
|
|
82
|
+
]
|
|
83
|
+
: [
|
|
84
|
+
{ value: 'create', role: 'section', prefix: '--', label: 'Setup' },
|
|
85
|
+
{ value: 'create', label: 'create new agent', hint: 'mint a wallet-owned token for this machine' },
|
|
86
|
+
{ value: 'load', label: 'load existing agent', hint: 'find an agent token your wallet already owns' },
|
|
87
|
+
{ value: 'skip', role: 'section', prefix: '--', label: 'Exit' },
|
|
88
|
+
...(mode === 'first-run'
|
|
89
|
+
? [{ value: 'skip' as Action, label: 'skip for now', hint: 'continue now; use /identity later', role: 'utility' as const }]
|
|
90
|
+
: [{ value: 'cancel' as Action, label: 'close hub', hint: 'return to the chat without changing identity', role: 'utility' as const }]),
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<Surface title={title} subtitle={subtitle} footer={footer}>
|
|
95
|
+
<IdentitySummary identity={identity} config={config} workingStatus={workingStatus} compact={Boolean(identity)} />
|
|
96
|
+
<Box marginTop={1}>
|
|
97
|
+
<Select<Action>
|
|
98
|
+
options={options}
|
|
99
|
+
hintLayout="inline"
|
|
100
|
+
onSubmit={choice => {
|
|
101
|
+
if (choice === 'skip') return onSkip()
|
|
102
|
+
if (choice === 'cancel') return onCancel()
|
|
103
|
+
if (choice === 'public-profile') return onPublicProfile()
|
|
104
|
+
if (choice === 'private-memory') return onPrivateMemory()
|
|
105
|
+
if (choice === 'backup') return onBackupNow()
|
|
106
|
+
if (choice === 'refetch') return onRefetchLatest()
|
|
107
|
+
if (choice === 'copy') return onCopyValues()
|
|
108
|
+
if (choice === 'storage-credential') return onStorageCredential()
|
|
109
|
+
if (choice === 'load') return onLoad()
|
|
110
|
+
if (choice === 'create') return onCreate()
|
|
111
|
+
}}
|
|
112
|
+
onCancel={() => mode === 'first-run' ? onSkip() : onCancel()}
|
|
113
|
+
/>
|
|
114
|
+
</Box>
|
|
115
|
+
</Surface>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Surface } from '../../../ui/Surface.js'
|
|
3
|
+
import { Select, type SelectOption } from '../../../ui/Select.js'
|
|
4
|
+
import type { SelectableNetwork } from '../../../storage/config.js'
|
|
5
|
+
import { SELECTABLE_NETWORKS } from '../../../storage/config.js'
|
|
6
|
+
import { networkLabel, networkSubtitle } from '../identityHubModel.js'
|
|
7
|
+
|
|
8
|
+
type NetworkScreenProps = {
|
|
9
|
+
subtitle: string
|
|
10
|
+
footer: React.ReactNode
|
|
11
|
+
onSelect: (network: SelectableNetwork) => void
|
|
12
|
+
onCancel: () => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const NetworkScreen: React.FC<NetworkScreenProps> = ({ subtitle, footer, onSelect, onCancel }) => {
|
|
16
|
+
const options: Array<SelectOption<SelectableNetwork>> = [
|
|
17
|
+
{ value: 'mainnet', role: 'section', prefix: '--', label: 'Main network' },
|
|
18
|
+
networkOption('mainnet'),
|
|
19
|
+
{ value: 'arbitrum', role: 'section', prefix: '--', label: 'Lower-fee networks' },
|
|
20
|
+
...SELECTABLE_NETWORKS.filter(network => network !== 'mainnet').map(networkOption),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Surface title="Network" subtitle={subtitle} footer={footer}>
|
|
25
|
+
<Select<SelectableNetwork>
|
|
26
|
+
options={options}
|
|
27
|
+
hintLayout="inline"
|
|
28
|
+
onSubmit={onSelect}
|
|
29
|
+
onCancel={onCancel}
|
|
30
|
+
/>
|
|
31
|
+
</Surface>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function networkOption(network: SelectableNetwork): SelectOption<SelectableNetwork> {
|
|
36
|
+
return {
|
|
37
|
+
value: network,
|
|
38
|
+
label: networkLabel(network),
|
|
39
|
+
hint: networkSubtitle(network),
|
|
40
|
+
}
|
|
41
|
+
}
|